| 1 | import * as Lambda from "@/AWS/Lambda"; |
| 2 | import * as OSIS from "@/AWS/OSIS"; |
| 3 | import * as Duration from "effect/Duration"; |
| 4 | import * as Effect from "effect/Effect"; |
| 5 | import * as Layer from "effect/Layer"; |
| 6 | import { HttpServerRequest } from "effect/http/HttpServerRequest"; |
| 7 | import * as HttpServerResponse from "effect/http/HttpServerResponse"; |
| 8 | import path from "pathe"; |
| 9 | |
| 10 | const main = path.resolve(import.meta.dirname, "handler.ts"); |
| 11 | |
| 12 | export class OsisTestFunction extends Lambda.Function<Lambda.Function>()( |
| 13 | "OsisTestFunction", |
| 14 | ) {} |
| 15 | |
| 16 | // A syntactically-valid Data Prepper configuration (http source -> s3 sink). |
| 17 | // ValidatePipeline is a static check; the role/bucket are never assumed. |
| 18 | const validConfig = `version: "2" |
| 19 | log-pipeline: |
| 20 | source: |
| 21 | http: |
| 22 | path: "/logs/ingest" |
| 23 | sink: |
| 24 | - s3: |
| 25 | aws: |
| 26 | sts_role_arn: "arn:aws:iam::123456789012:role/osis-validate-fixture" |
| 27 | region: "us-west-2" |
| 28 | bucket: "osis-validate-fixture" |
| 29 | threshold: |
| 30 | event_collect_timeout: "60s" |
| 31 | codec: |
| 32 | ndjson: |
| 33 | `; |
| 34 | |
| 35 | // Structurally broken: the sink references a plugin that does not exist. |
| 36 | const invalidConfig = `version: "2" |
| 37 | bad-pipeline: |
| 38 | source: |
| 39 | http: |
| 40 | path: "/logs/ingest" |
| 41 | sink: |
| 42 | - not-a-real-sink-plugin: |
| 43 | some_option: true |
| 44 | `; |
| 45 | |
| 46 | export default OsisTestFunction.make( |
| 47 | { |
| 48 | main, |
| 49 | functionUrl: true, |
| 50 | timeout: Duration.seconds(30), |
| 51 | }, |
| 52 | Effect.gen(function* () { |
| 53 | // Account-level bindings — no resource argument. |
| 54 | const validatePipeline = yield* OSIS.ValidatePipeline(); |
| 55 | const listPipelineBlueprints = yield* OSIS.ListPipelineBlueprints(); |
| 56 | const getPipelineBlueprint = yield* OSIS.GetPipelineBlueprint(); |
| 57 | const listPipelineEndpointConnections = |
| 58 | yield* OSIS.ListPipelineEndpointConnections(); |
| 59 | |
| 60 | const bound = { |
| 61 | validatePipeline, |
| 62 | listPipelineBlueprints, |
| 63 | getPipelineBlueprint, |
| 64 | listPipelineEndpointConnections, |
| 65 | }; |
| 66 | |
| 67 | // ValidatePipeline surfaces some invalid configs as a typed |
| 68 | // ValidationException rather than `isValid: false`; normalize both. |
| 69 | const validate = (body: string) => |
| 70 | validatePipeline({ PipelineConfigurationBody: body }).pipe( |
| 71 | Effect.map((response) => ({ |
| 72 | isValid: response.isValid === true, |
| 73 | errors: (response.Errors ?? []).flatMap((e) => |
| 74 | e.Message !== undefined ? [e.Message] : [], |
| 75 | ), |
| 76 | })), |
| 77 | Effect.catchTag("ValidationException", (error) => |
| 78 | Effect.succeed({ |
| 79 | isValid: false, |
| 80 | errors: [error.message ?? "ValidationException"], |
| 81 | }), |
| 82 | ), |
| 83 | ); |
| 84 | |
| 85 | return { |
| 86 | fetch: Effect.gen(function* () { |
| 87 | const request = yield* HttpServerRequest; |
| 88 | const url = new URL(request.originalUrl); |
| 89 | const pathname = url.pathname; |
| 90 | |
| 91 | if (request.method === "GET" && pathname === "/bindings") { |
| 92 | return yield* HttpServerResponse.json({ |
| 93 | bound: Object.keys(bound), |
| 94 | }); |
| 95 | } |
| 96 | |
| 97 | if (request.method === "GET" && pathname === "/validate-good") { |
| 98 | return yield* HttpServerResponse.json(yield* validate(validConfig)); |
| 99 | } |
| 100 | |
| 101 | if (request.method === "GET" && pathname === "/validate-bad") { |
| 102 | return yield* HttpServerResponse.json(yield* validate(invalidConfig)); |
| 103 | } |
| 104 | |
| 105 | if (request.method === "GET" && pathname === "/blueprints") { |
| 106 | const { Blueprints } = yield* listPipelineBlueprints(); |
| 107 | return yield* HttpServerResponse.json({ |
| 108 | names: (Blueprints ?? []).flatMap((blueprint) => |
| 109 | blueprint.BlueprintName !== undefined |
| 110 | ? [blueprint.BlueprintName] |
| 111 | : [], |
| 112 | ), |
| 113 | }); |
| 114 | } |
| 115 | |
| 116 | if (request.method === "GET" && pathname === "/blueprint") { |
| 117 | const name = url.searchParams.get("name") ?? ""; |
| 118 | const { Blueprint } = yield* getPipelineBlueprint({ |
| 119 | BlueprintName: name, |
| 120 | }); |
| 121 | return yield* HttpServerResponse.json({ |
| 122 | name: Blueprint?.BlueprintName, |
| 123 | hasBody: |
| 124 | typeof Blueprint?.PipelineConfigurationBody === "string" && |
| 125 | Blueprint.PipelineConfigurationBody.length > 0, |
| 126 | }); |
| 127 | } |
| 128 | |
| 129 | if (request.method === "GET" && pathname === "/endpoint-connections") { |
| 130 | const { PipelineEndpointConnections } = |
| 131 | yield* listPipelineEndpointConnections(); |
| 132 | return yield* HttpServerResponse.json({ |
| 133 | count: (PipelineEndpointConnections ?? []).length, |
| 134 | }); |
| 135 | } |
| 136 | |
| 137 | return yield* HttpServerResponse.json( |
| 138 | { error: "Not found", method: request.method, pathname }, |
| 139 | { status: 404 }, |
| 140 | ); |
| 141 | }).pipe(Effect.orDie), |
| 142 | }; |
| 143 | }).pipe( |
| 144 | Effect.provide( |
| 145 | Layer.mergeAll( |
| 146 | OSIS.ValidatePipelineHttp, |
| 147 | OSIS.ListPipelineBlueprintsHttp, |
| 148 | OSIS.GetPipelineBlueprintHttp, |
| 149 | OSIS.ListPipelineEndpointConnectionsHttp, |
| 150 | ), |
| 151 | ), |
| 152 | ), |
| 153 | ); |