From bf39a2f4e5beb29e807a8dd9a7b34ccdb1143712 Mon Sep 17 00:00:00 2001 From: Steven Borrelli Date: Wed, 4 Mar 2026 07:25:58 +0100 Subject: [PATCH 1/5] add support for required resources Signed-off-by: Steven Borrelli --- src/index.ts | 9 + src/proto/run_function.proto | 72 ++++- src/proto/run_function.ts | 572 +++++++++++++++++++++++++++++++++- src/request/request.test.ts | 419 ++++++++++++++++++++++++- src/request/request.ts | 146 +++++++++ src/response/response.test.ts | 293 ++++++++++++++++- src/response/response.ts | 118 +++++++ 7 files changed, 1618 insertions(+), 11 deletions(-) diff --git a/src/index.ts b/src/index.ts index 1b008da..2b546a5 100644 --- a/src/index.ts +++ b/src/index.ts @@ -13,6 +13,9 @@ export { getInput, getContextKey, getRequiredResources, + getRequiredResource, + getRequiredSchema, + getRequiredSchemas, getCredentials, } from './request/request.js'; @@ -30,6 +33,8 @@ export { update, setContextKey, setOutput, + requireSchema, + requireResource, DEFAULT_TTL, } from './response/response.js'; @@ -72,6 +77,10 @@ export { Resources, Credentials, CredentialData, + Requirements, + ResourceSelector, + SchemaSelector, + Schema, FunctionRunnerServiceService, } from './proto/run_function.js'; diff --git a/src/proto/run_function.proto b/src/proto/run_function.proto index 0c65361..9407449 100644 --- a/src/proto/run_function.proto +++ b/src/proto/run_function.proto @@ -90,6 +90,13 @@ message RunFunctionRequest { // satisfy the request. This field is only populated when the function uses // resources in its requirements. map required_resources = 8; + + // Optional schemas that the function specified in its requirements. The map + // key corresponds to the key in a RunFunctionResponse's requirements.schemas + // field. If a function requested a schema that could not be found, Crossplane + // sets the map key to an empty Schema message to indicate that it attempted + // to satisfy the request. + map required_schemas = 9; } // Credentials that a function may use to communicate with an external system. @@ -156,6 +163,44 @@ message RequestMeta { // An opaque string identifying a request. Requests with identical tags will // be otherwise identical. string tag = 1; + + // Capabilities supported by this version of Crossplane. Functions may use + // this to determine whether Crossplane will honor certain fields in their + // response, or populate certain fields in their request. + repeated Capability capabilities = 2; +} + +// Capability indicates that Crossplane supports a particular feature. +// Functions can check for capabilities to determine whether Crossplane will +// honor a particular request or response field. +enum Capability { + CAPABILITY_UNSPECIFIED = 0; + + // Crossplane sends capabilities in RequestMeta. If this capability is + // present, the function knows that if another capability is absent, it's + // because Crossplane doesn't support it (not because Crossplane predates + // capability advertisement). Added in Crossplane v2.2. + CAPABILITY_CAPABILITIES = 1; + + // Crossplane supports the requirements.resources field. Functions can return + // resource requirements and Crossplane will fetch the requested resources and + // return them in required_resources. Added in Crossplane v1.15. + CAPABILITY_REQUIRED_RESOURCES = 2; + + // Crossplane supports the credentials field. Functions can receive + // credentials from secrets specified in the Composition. Added in Crossplane + // v1.16. + CAPABILITY_CREDENTIALS = 3; + + // Crossplane supports the conditions field. Functions can return status + // conditions to be applied to the XR and optionally its claim. Added in + // Crossplane v1.17. + CAPABILITY_CONDITIONS = 4; + + // Crossplane supports the requirements.schemas field. Functions can request + // OpenAPI schemas and Crossplane will return them in required_schemas. Added + // in Crossplane v2.2. + CAPABILITY_REQUIRED_SCHEMAS = 5; } // Requirements that must be satisfied for a function to run successfully. @@ -169,6 +214,27 @@ message Requirements { // Resources that this function requires. The map key uniquely identifies the // group of resources. map resources = 2; + + // Schemas that this function requires. The map key uniquely identifies the + // schema request. + map schemas = 3; +} + +// SchemaSelector identifies a resource kind whose OpenAPI schema is requested. +message SchemaSelector { + // API version of the resource kind, e.g. "example.org/v1". + string api_version = 1; + + // Kind of resource, e.g. "MyResource". + string kind = 2; +} + +// Schema represents the OpenAPI schema for a resource kind. +message Schema { + // The OpenAPI v3 schema of the resource kind as unstructured JSON. + // For CRDs this is the spec.versions[].schema.openAPIV3Schema field. + // Empty if Crossplane could not find a schema for the requested kind. + optional google.protobuf.Struct openapi_v3 = 1; } // ResourceSelector selects a group of resources, either by name or by label. @@ -247,7 +313,7 @@ message Resource { // the observed connection details of a composite or composed resource. // // * A function should set this field in a RunFunctionResponse to indicate the - // desired connection details of the XR. + // desired connection details of legacy XRs. For modern XRs, this will be ignored. // // * A function should not set this field in a RunFunctionResponse to indicate // the desired connection details of a composed resource. This will be @@ -267,7 +333,7 @@ message Resource { // * A function should set this field to READY_TRUE in a RunFunctionResponse // to indicate that a desired XR is ready. This overwrites the standard // readiness detection that determines the ready state of the composite by the - // ready state of the the composed resources. + // ready state of the composed resources. // // Ready is only used for composition. It's ignored by Operations. Ready ready = 3; @@ -367,4 +433,4 @@ enum Status { STATUS_CONDITION_TRUE = 2; STATUS_CONDITION_FALSE = 3; -} +} \ No newline at end of file diff --git a/src/proto/run_function.ts b/src/proto/run_function.ts index 3412fa3..de19a01 100644 --- a/src/proto/run_function.ts +++ b/src/proto/run_function.ts @@ -23,6 +23,94 @@ import { Struct } from "./google/protobuf/struct.js"; export const protobufPackage = "apiextensions.fn.proto.v1"; +/** + * Capability indicates that Crossplane supports a particular feature. + * Functions can check for capabilities to determine whether Crossplane will + * honor a particular request or response field. + */ +export enum Capability { + CAPABILITY_UNSPECIFIED = 0, + /** + * CAPABILITY_CAPABILITIES - Crossplane sends capabilities in RequestMeta. If this capability is + * present, the function knows that if another capability is absent, it's + * because Crossplane doesn't support it (not because Crossplane predates + * capability advertisement). Added in Crossplane v2.2. + */ + CAPABILITY_CAPABILITIES = 1, + /** + * CAPABILITY_REQUIRED_RESOURCES - Crossplane supports the requirements.resources field. Functions can return + * resource requirements and Crossplane will fetch the requested resources and + * return them in required_resources. Added in Crossplane v1.15. + */ + CAPABILITY_REQUIRED_RESOURCES = 2, + /** + * CAPABILITY_CREDENTIALS - Crossplane supports the credentials field. Functions can receive + * credentials from secrets specified in the Composition. Added in Crossplane + * v1.16. + */ + CAPABILITY_CREDENTIALS = 3, + /** + * CAPABILITY_CONDITIONS - Crossplane supports the conditions field. Functions can return status + * conditions to be applied to the XR and optionally its claim. Added in + * Crossplane v1.17. + */ + CAPABILITY_CONDITIONS = 4, + /** + * CAPABILITY_REQUIRED_SCHEMAS - Crossplane supports the requirements.schemas field. Functions can request + * OpenAPI schemas and Crossplane will return them in required_schemas. Added + * in Crossplane v2.2. + */ + CAPABILITY_REQUIRED_SCHEMAS = 5, + UNRECOGNIZED = -1, +} + +export function capabilityFromJSON(object: any): Capability { + switch (object) { + case 0: + case "CAPABILITY_UNSPECIFIED": + return Capability.CAPABILITY_UNSPECIFIED; + case 1: + case "CAPABILITY_CAPABILITIES": + return Capability.CAPABILITY_CAPABILITIES; + case 2: + case "CAPABILITY_REQUIRED_RESOURCES": + return Capability.CAPABILITY_REQUIRED_RESOURCES; + case 3: + case "CAPABILITY_CREDENTIALS": + return Capability.CAPABILITY_CREDENTIALS; + case 4: + case "CAPABILITY_CONDITIONS": + return Capability.CAPABILITY_CONDITIONS; + case 5: + case "CAPABILITY_REQUIRED_SCHEMAS": + return Capability.CAPABILITY_REQUIRED_SCHEMAS; + case -1: + case "UNRECOGNIZED": + default: + return Capability.UNRECOGNIZED; + } +} + +export function capabilityToJSON(object: Capability): string { + switch (object) { + case Capability.CAPABILITY_UNSPECIFIED: + return "CAPABILITY_UNSPECIFIED"; + case Capability.CAPABILITY_CAPABILITIES: + return "CAPABILITY_CAPABILITIES"; + case Capability.CAPABILITY_REQUIRED_RESOURCES: + return "CAPABILITY_REQUIRED_RESOURCES"; + case Capability.CAPABILITY_CREDENTIALS: + return "CAPABILITY_CREDENTIALS"; + case Capability.CAPABILITY_CONDITIONS: + return "CAPABILITY_CONDITIONS"; + case Capability.CAPABILITY_REQUIRED_SCHEMAS: + return "CAPABILITY_REQUIRED_SCHEMAS"; + case Capability.UNRECOGNIZED: + default: + return "UNRECOGNIZED"; + } +} + /** Ready indicates whether a resource should be considered ready. */ export enum Ready { READY_UNSPECIFIED = 0, @@ -293,6 +381,14 @@ export interface RunFunctionRequest { * resources in its requirements. */ requiredResources: { [key: string]: Resources }; + /** + * Optional schemas that the function specified in its requirements. The map + * key corresponds to the key in a RunFunctionResponse's requirements.schemas + * field. If a function requested a schema that could not be found, Crossplane + * sets the map key to an empty Schema message to indicate that it attempted + * to satisfy the request. + */ + requiredSchemas: { [key: string]: Schema }; } export interface RunFunctionRequest_ExtraResourcesEntry { @@ -310,6 +406,11 @@ export interface RunFunctionRequest_RequiredResourcesEntry { value: Resources | undefined; } +export interface RunFunctionRequest_RequiredSchemasEntry { + key: string; + value: Schema | undefined; +} + /** Credentials that a function may use to communicate with an external system. */ export interface Credentials { /** Credential data loaded by Crossplane, for example from a Secret. */ @@ -388,6 +489,12 @@ export interface RequestMeta { * be otherwise identical. */ tag: string; + /** + * Capabilities supported by this version of Crossplane. Functions may use + * this to determine whether Crossplane will honor certain fields in their + * response, or populate certain fields in their request. + */ + capabilities: Capability[]; } /** Requirements that must be satisfied for a function to run successfully. */ @@ -406,6 +513,11 @@ export interface Requirements { * group of resources. */ resources: { [key: string]: ResourceSelector }; + /** + * Schemas that this function requires. The map key uniquely identifies the + * schema request. + */ + schemas: { [key: string]: SchemaSelector }; } export interface Requirements_ExtraResourcesEntry { @@ -418,6 +530,29 @@ export interface Requirements_ResourcesEntry { value: ResourceSelector | undefined; } +export interface Requirements_SchemasEntry { + key: string; + value: SchemaSelector | undefined; +} + +/** SchemaSelector identifies a resource kind whose OpenAPI schema is requested. */ +export interface SchemaSelector { + /** API version of the resource kind, e.g. "example.org/v1". */ + apiVersion: string; + /** Kind of resource, e.g. "MyResource". */ + kind: string; +} + +/** Schema represents the OpenAPI schema for a resource kind. */ +export interface Schema { + /** + * The OpenAPI v3 schema of the resource kind as unstructured JSON. + * For CRDs this is the spec.versions[].schema.openAPIV3Schema field. + * Empty if Crossplane could not find a schema for the requested kind. + */ + openapiV3?: { [key: string]: any } | undefined; +} + /** ResourceSelector selects a group of resources, either by name or by label. */ export interface ResourceSelector { /** API version of resources to select. */ @@ -513,7 +648,7 @@ export interface Resource { * the observed connection details of a composite or composed resource. * * * A function should set this field in a RunFunctionResponse to indicate the - * desired connection details of the XR. + * desired connection details of legacy XRs. For modern XRs, this will be ignored. * * * A function should not set this field in a RunFunctionResponse to indicate * the desired connection details of a composed resource. This will be @@ -534,7 +669,7 @@ export interface Resource { * * A function should set this field to READY_TRUE in a RunFunctionResponse * to indicate that a desired XR is ready. This overwrites the standard * readiness detection that determines the ready state of the composite by the - * ready state of the the composed resources. + * ready state of the composed resources. * * Ready is only used for composition. It's ignored by Operations. */ @@ -603,6 +738,7 @@ function createBaseRunFunctionRequest(): RunFunctionRequest { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; } @@ -632,6 +768,9 @@ export const RunFunctionRequest: MessageFns = { globalThis.Object.entries(message.requiredResources).forEach(([key, value]: [string, Resources]) => { RunFunctionRequest_RequiredResourcesEntry.encode({ key: key as any, value }, writer.uint32(66).fork()).join(); }); + globalThis.Object.entries(message.requiredSchemas).forEach(([key, value]: [string, Schema]) => { + RunFunctionRequest_RequiredSchemasEntry.encode({ key: key as any, value }, writer.uint32(74).fork()).join(); + }); return writer; }, @@ -715,6 +854,17 @@ export const RunFunctionRequest: MessageFns = { } continue; } + case 9: { + if (tag !== 74) { + break; + } + + const entry9 = RunFunctionRequest_RequiredSchemasEntry.decode(reader, reader.uint32()); + if (entry9.value !== undefined) { + message.requiredSchemas[entry9.key] = entry9.value; + } + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -774,6 +924,23 @@ export const RunFunctionRequest: MessageFns = { {}, ) : {}, + requiredSchemas: isObject(object.requiredSchemas) + ? (globalThis.Object.entries(object.requiredSchemas) as [string, any][]).reduce( + (acc: { [key: string]: Schema }, [key, value]: [string, any]) => { + acc[key] = Schema.fromJSON(value); + return acc; + }, + {}, + ) + : isObject(object.required_schemas) + ? (globalThis.Object.entries(object.required_schemas) as [string, any][]).reduce( + (acc: { [key: string]: Schema }, [key, value]: [string, any]) => { + acc[key] = Schema.fromJSON(value); + return acc; + }, + {}, + ) + : {}, }; }, @@ -821,6 +988,15 @@ export const RunFunctionRequest: MessageFns = { }); } } + if (message.requiredSchemas) { + const entries = globalThis.Object.entries(message.requiredSchemas) as [string, Schema][]; + if (entries.length > 0) { + obj.requiredSchemas = {}; + entries.forEach(([k, v]) => { + obj.requiredSchemas[k] = Schema.toJSON(v); + }); + } + } return obj; }, @@ -865,6 +1041,15 @@ export const RunFunctionRequest: MessageFns = { } return acc; }, {}); + message.requiredSchemas = (globalThis.Object.entries(object.requiredSchemas ?? {}) as [string, Schema][]).reduce( + (acc: { [key: string]: Schema }, [key, value]: [string, Schema]) => { + if (value !== undefined) { + acc[key] = Schema.fromPartial(value); + } + return acc; + }, + {}, + ); return message; }, }; @@ -1115,6 +1300,88 @@ export const RunFunctionRequest_RequiredResourcesEntry: MessageFns = { + encode(message: RunFunctionRequest_RequiredSchemasEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + if (message.value !== undefined) { + Schema.encode(message.value, writer.uint32(18).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): RunFunctionRequest_RequiredSchemasEntry { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRunFunctionRequest_RequiredSchemasEntry(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = Schema.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): RunFunctionRequest_RequiredSchemasEntry { + return { + key: isSet(object.key) ? globalThis.String(object.key) : "", + value: isSet(object.value) ? Schema.fromJSON(object.value) : undefined, + }; + }, + + toJSON(message: RunFunctionRequest_RequiredSchemasEntry): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + if (message.value !== undefined) { + obj.value = Schema.toJSON(message.value); + } + return obj; + }, + + create, I>>( + base?: I, + ): RunFunctionRequest_RequiredSchemasEntry { + return RunFunctionRequest_RequiredSchemasEntry.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>( + object: I, + ): RunFunctionRequest_RequiredSchemasEntry { + const message = createBaseRunFunctionRequest_RequiredSchemasEntry(); + message.key = object.key ?? ""; + message.value = (object.value !== undefined && object.value !== null) + ? Schema.fromPartial(object.value) + : undefined; + return message; + }, +}; + function createBaseCredentials(): Credentials { return { credentialData: undefined }; } @@ -1573,7 +1840,7 @@ export const RunFunctionResponse: MessageFns = { }; function createBaseRequestMeta(): RequestMeta { - return { tag: "" }; + return { tag: "", capabilities: [] }; } export const RequestMeta: MessageFns = { @@ -1581,6 +1848,11 @@ export const RequestMeta: MessageFns = { if (message.tag !== "") { writer.uint32(10).string(message.tag); } + writer.uint32(18).fork(); + for (const v of message.capabilities) { + writer.int32(v); + } + writer.join(); return writer; }, @@ -1599,6 +1871,24 @@ export const RequestMeta: MessageFns = { message.tag = reader.string(); continue; } + case 2: { + if (tag === 16) { + message.capabilities.push(reader.int32() as any); + + continue; + } + + if (tag === 18) { + const end2 = reader.uint32() + reader.pos; + while (reader.pos < end2) { + message.capabilities.push(reader.int32() as any); + } + + continue; + } + + break; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -1609,7 +1899,12 @@ export const RequestMeta: MessageFns = { }, fromJSON(object: any): RequestMeta { - return { tag: isSet(object.tag) ? globalThis.String(object.tag) : "" }; + return { + tag: isSet(object.tag) ? globalThis.String(object.tag) : "", + capabilities: globalThis.Array.isArray(object?.capabilities) + ? object.capabilities.map((e: any) => capabilityFromJSON(e)) + : [], + }; }, toJSON(message: RequestMeta): unknown { @@ -1617,6 +1912,9 @@ export const RequestMeta: MessageFns = { if (message.tag !== "") { obj.tag = message.tag; } + if (message.capabilities?.length) { + obj.capabilities = message.capabilities.map((e) => capabilityToJSON(e)); + } return obj; }, @@ -1626,12 +1924,13 @@ export const RequestMeta: MessageFns = { fromPartial, I>>(object: I): RequestMeta { const message = createBaseRequestMeta(); message.tag = object.tag ?? ""; + message.capabilities = object.capabilities?.map((e) => e) || []; return message; }, }; function createBaseRequirements(): Requirements { - return { extraResources: {}, resources: {} }; + return { extraResources: {}, resources: {}, schemas: {} }; } export const Requirements: MessageFns = { @@ -1642,6 +1941,9 @@ export const Requirements: MessageFns = { globalThis.Object.entries(message.resources).forEach(([key, value]: [string, ResourceSelector]) => { Requirements_ResourcesEntry.encode({ key: key as any, value }, writer.uint32(18).fork()).join(); }); + globalThis.Object.entries(message.schemas).forEach(([key, value]: [string, SchemaSelector]) => { + Requirements_SchemasEntry.encode({ key: key as any, value }, writer.uint32(26).fork()).join(); + }); return writer; }, @@ -1674,6 +1976,17 @@ export const Requirements: MessageFns = { } continue; } + case 3: { + if (tag !== 26) { + break; + } + + const entry3 = Requirements_SchemasEntry.decode(reader, reader.uint32()); + if (entry3.value !== undefined) { + message.schemas[entry3.key] = entry3.value; + } + continue; + } } if ((tag & 7) === 4 || tag === 0) { break; @@ -1711,6 +2024,15 @@ export const Requirements: MessageFns = { {}, ) : {}, + schemas: isObject(object.schemas) + ? (globalThis.Object.entries(object.schemas) as [string, any][]).reduce( + (acc: { [key: string]: SchemaSelector }, [key, value]: [string, any]) => { + acc[key] = SchemaSelector.fromJSON(value); + return acc; + }, + {}, + ) + : {}, }; }, @@ -1734,6 +2056,15 @@ export const Requirements: MessageFns = { }); } } + if (message.schemas) { + const entries = globalThis.Object.entries(message.schemas) as [string, SchemaSelector][]; + if (entries.length > 0) { + obj.schemas = {}; + entries.forEach(([k, v]) => { + obj.schemas[k] = SchemaSelector.toJSON(v); + }); + } + } return obj; }, @@ -1758,6 +2089,15 @@ export const Requirements: MessageFns = { }, {}, ); + message.schemas = (globalThis.Object.entries(object.schemas ?? {}) as [string, SchemaSelector][]).reduce( + (acc: { [key: string]: SchemaSelector }, [key, value]: [string, SchemaSelector]) => { + if (value !== undefined) { + acc[key] = SchemaSelector.fromPartial(value); + } + return acc; + }, + {}, + ); return message; }, }; @@ -1922,6 +2262,228 @@ export const Requirements_ResourcesEntry: MessageFns = { + encode(message: Requirements_SchemasEntry, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.key !== "") { + writer.uint32(10).string(message.key); + } + if (message.value !== undefined) { + SchemaSelector.encode(message.value, writer.uint32(18).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Requirements_SchemasEntry { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseRequirements_SchemasEntry(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.key = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.value = SchemaSelector.decode(reader, reader.uint32()); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Requirements_SchemasEntry { + return { + key: isSet(object.key) ? globalThis.String(object.key) : "", + value: isSet(object.value) ? SchemaSelector.fromJSON(object.value) : undefined, + }; + }, + + toJSON(message: Requirements_SchemasEntry): unknown { + const obj: any = {}; + if (message.key !== "") { + obj.key = message.key; + } + if (message.value !== undefined) { + obj.value = SchemaSelector.toJSON(message.value); + } + return obj; + }, + + create, I>>(base?: I): Requirements_SchemasEntry { + return Requirements_SchemasEntry.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): Requirements_SchemasEntry { + const message = createBaseRequirements_SchemasEntry(); + message.key = object.key ?? ""; + message.value = (object.value !== undefined && object.value !== null) + ? SchemaSelector.fromPartial(object.value) + : undefined; + return message; + }, +}; + +function createBaseSchemaSelector(): SchemaSelector { + return { apiVersion: "", kind: "" }; +} + +export const SchemaSelector: MessageFns = { + encode(message: SchemaSelector, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.apiVersion !== "") { + writer.uint32(10).string(message.apiVersion); + } + if (message.kind !== "") { + writer.uint32(18).string(message.kind); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): SchemaSelector { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSchemaSelector(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.apiVersion = reader.string(); + continue; + } + case 2: { + if (tag !== 18) { + break; + } + + message.kind = reader.string(); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): SchemaSelector { + return { + apiVersion: isSet(object.apiVersion) + ? globalThis.String(object.apiVersion) + : isSet(object.api_version) + ? globalThis.String(object.api_version) + : "", + kind: isSet(object.kind) ? globalThis.String(object.kind) : "", + }; + }, + + toJSON(message: SchemaSelector): unknown { + const obj: any = {}; + if (message.apiVersion !== "") { + obj.apiVersion = message.apiVersion; + } + if (message.kind !== "") { + obj.kind = message.kind; + } + return obj; + }, + + create, I>>(base?: I): SchemaSelector { + return SchemaSelector.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): SchemaSelector { + const message = createBaseSchemaSelector(); + message.apiVersion = object.apiVersion ?? ""; + message.kind = object.kind ?? ""; + return message; + }, +}; + +function createBaseSchema(): Schema { + return { openapiV3: undefined }; +} + +export const Schema: MessageFns = { + encode(message: Schema, writer: BinaryWriter = new BinaryWriter()): BinaryWriter { + if (message.openapiV3 !== undefined) { + Struct.encode(Struct.wrap(message.openapiV3), writer.uint32(10).fork()).join(); + } + return writer; + }, + + decode(input: BinaryReader | Uint8Array, length?: number): Schema { + const reader = input instanceof BinaryReader ? input : new BinaryReader(input); + const end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSchema(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: { + if (tag !== 10) { + break; + } + + message.openapiV3 = Struct.unwrap(Struct.decode(reader, reader.uint32())); + continue; + } + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skip(tag & 7); + } + return message; + }, + + fromJSON(object: any): Schema { + return { + openapiV3: isObject(object.openapiV3) + ? object.openapiV3 + : isObject(object.openapi_v3) + ? object.openapi_v3 + : undefined, + }; + }, + + toJSON(message: Schema): unknown { + const obj: any = {}; + if (message.openapiV3 !== undefined) { + obj.openapiV3 = message.openapiV3; + } + return obj; + }, + + create, I>>(base?: I): Schema { + return Schema.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): Schema { + const message = createBaseSchema(); + message.openapiV3 = object.openapiV3 ?? undefined; + return message; + }, +}; + function createBaseResourceSelector(): ResourceSelector { return { apiVersion: "", kind: "", matchName: undefined, matchLabels: undefined, namespace: undefined }; } diff --git a/src/request/request.test.ts b/src/request/request.test.ts index 5309605..3fe007b 100644 --- a/src/request/request.test.ts +++ b/src/request/request.test.ts @@ -8,6 +8,9 @@ import { getContextKey, getRequiredResources, getCredentials, + getRequiredResource, + getRequiredSchema, + getRequiredSchemas, } from './request.js'; import type { RunFunctionRequest, @@ -27,6 +30,7 @@ describe('getDesiredCompositeResource', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getDesiredCompositeResource(req); @@ -46,6 +50,7 @@ describe('getDesiredCompositeResource', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getDesiredCompositeResource(req); @@ -79,6 +84,7 @@ describe('getDesiredCompositeResource', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getDesiredCompositeResource(req); @@ -98,6 +104,7 @@ describe('getObservedCompositeResource', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getObservedCompositeResource(req); @@ -131,6 +138,7 @@ describe('getObservedCompositeResource', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getObservedCompositeResource(req); @@ -150,6 +158,7 @@ describe('getDesiredComposedResources', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getDesiredComposedResources(req); @@ -170,6 +179,7 @@ describe('getDesiredComposedResources', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getDesiredComposedResources(req); @@ -214,6 +224,7 @@ describe('getDesiredComposedResources', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getDesiredComposedResources(req); @@ -235,6 +246,7 @@ describe('getObservedComposedResources', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getObservedComposedResources(req); @@ -274,6 +286,7 @@ describe('getObservedComposedResources', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getObservedComposedResources(req); @@ -294,6 +307,7 @@ describe('getInput', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getInput(req); @@ -319,12 +333,13 @@ describe('getInput', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getInput(req); expect(result).toEqual(input); - expect(result?.spec?.region).toBe('us-west-2'); - expect(result?.spec?.replicas).toBe(3); + expect((result as any)?.spec?.region).toBe('us-west-2'); + expect((result as any)?.spec?.replicas).toBe(3); }); it('should handle empty input object', () => { @@ -337,6 +352,7 @@ describe('getInput', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getInput(req); @@ -355,6 +371,7 @@ describe('getContextKey', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const [value, found] = getContextKey(req, 'apiserver-kind'); @@ -374,6 +391,7 @@ describe('getContextKey', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const [value, found] = getContextKey(req, 'non-existent-key'); @@ -399,6 +417,7 @@ describe('getContextKey', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const [kind, kindFound] = getContextKey(req, 'apiserver-kind'); @@ -427,6 +446,7 @@ describe('getContextKey', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const [nullValue, nullFound] = getContextKey(req, 'null-value'); @@ -450,6 +470,7 @@ describe('getRequiredResources', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; const result = getRequiredResources(req); @@ -485,6 +506,7 @@ describe('getRequiredResources', () => { requiredResources: { namespaces: resources, }, + requiredSchemas: {}, }; const result = getRequiredResources(req); @@ -534,6 +556,7 @@ describe('getRequiredResources', () => { requiredResources: { 'config-and-secrets': resources, }, + requiredSchemas: {}, }; const result = getRequiredResources(req); @@ -557,6 +580,7 @@ describe('getRequiredResources', () => { group2: { items: [] }, group3: { items: [] }, }, + requiredSchemas: {}, }; const result = getRequiredResources(req); @@ -578,6 +602,7 @@ describe('getCredentials', () => { extraResources: {}, credentials: {}, requiredResources: {}, + requiredSchemas: {}, }; expect(() => getCredentials(req, 'aws-creds')).toThrow('credentials "aws-creds" not found'); @@ -601,6 +626,7 @@ describe('getCredentials', () => { }, }, requiredResources: {}, + requiredSchemas: {}, }; expect(() => getCredentials(req, 'aws-creds')).toThrow('credentials "aws-creds" not found'); @@ -627,6 +653,7 @@ describe('getCredentials', () => { 'aws-creds': awsCreds, }, requiredResources: {}, + requiredSchemas: {}, }; const result = getCredentials(req, 'aws-creds'); @@ -661,6 +688,7 @@ describe('getCredentials', () => { }, }, requiredResources: {}, + requiredSchemas: {}, }; const awsResult = getCredentials(req, 'aws-creds'); @@ -690,6 +718,7 @@ describe('getCredentials', () => { }, }, requiredResources: {}, + requiredSchemas: {}, }; const result = getCredentials(req, 'empty-creds'); @@ -697,3 +726,389 @@ describe('getCredentials', () => { expect(result?.credentialData?.data).toEqual({}); }); }); + +describe('getRequiredResource', () => { + it('should return empty array and false when no required resources exist', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: {}, + requiredSchemas: {}, + }; + + const [resources, resolved, error] = getRequiredResource(req, 'test'); + expect(resources).toEqual([]); + expect(resolved).toBe(false); + expect(error).toBeUndefined(); + }); + + it('should return empty array and false when resource not found', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: { + other: { + items: [ + { + resource: { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { name: 'other-config' }, + }, + connectionDetails: {}, + ready: 0, + }, + ], + }, + }, + requiredSchemas: {}, + }; + + const [resources, resolved, error] = getRequiredResource(req, 'test'); + expect(resources).toEqual([]); + expect(resolved).toBe(false); + expect(error).toBeUndefined(); + }); + + it('should return resources when found', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: { + test: { + items: [ + { + resource: { + apiVersion: 'v1', + kind: 'ConfigMap', + metadata: { name: 'test-config' }, + data: { key: 'value' }, + }, + connectionDetails: {}, + ready: 0, + }, + ], + }, + }, + requiredSchemas: {}, + }; + + const [resources, resolved, error] = getRequiredResource(req, 'test'); + expect(error).toBeUndefined(); + expect(resolved).toBe(true); + expect(resources).toHaveLength(1); + expect(resources[0]?.resource?.kind).toBe('ConfigMap'); + expect(resources[0]?.resource?.metadata?.name).toBe('test-config'); + expect(resources[0]?.resource?.data?.key).toBe('value'); + }); + + it('should return multiple resources when found', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: { + secrets: { + items: [ + { + resource: { + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: 'secret-1', namespace: 'default' }, + }, + connectionDetails: {}, + ready: 0, + }, + { + resource: { + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: 'secret-2', namespace: 'default' }, + }, + connectionDetails: {}, + ready: 0, + }, + ], + }, + }, + requiredSchemas: {}, + }; + + const [resources, resolved, error] = getRequiredResource(req, 'secrets'); + expect(error).toBeUndefined(); + expect(resolved).toBe(true); + expect(resources).toHaveLength(2); + expect(resources[0]?.resource?.metadata?.name).toBe('secret-1'); + expect(resources[1]?.resource?.metadata?.name).toBe('secret-2'); + }); + + it('should handle empty items array', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: { + empty: { + items: [], + }, + }, + requiredSchemas: {}, + }; + + const [resources, resolved, error] = getRequiredResource(req, 'empty'); + expect(error).toBeUndefined(); + expect(resolved).toBe(true); + expect(resources).toEqual([]); + }); + + it('should preserve connection details and ready status', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: { + test: { + items: [ + { + resource: { + apiVersion: 'v1', + kind: 'Secret', + metadata: { name: 'test-secret' }, + }, + connectionDetails: { + username: Buffer.from('admin'), + password: Buffer.from('secret'), + }, + ready: 2, // READY_TRUE + }, + ], + }, + }, + requiredSchemas: {}, + }; + + const [resources, resolved, error] = getRequiredResource(req, 'test'); + expect(error).toBeUndefined(); + expect(resolved).toBe(true); + expect(resources).toHaveLength(1); + expect(resources[0]?.connectionDetails?.username).toEqual(Buffer.from('admin')); + expect(resources[0]?.connectionDetails?.password).toEqual(Buffer.from('secret')); + expect(resources[0]?.ready).toBe(2); + }); +}); + +describe('getRequiredSchemas', () => { + it('should return empty object when no required schemas exist', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: {}, + requiredSchemas: {}, + }; + + const result = getRequiredSchemas(req); + expect(result).toEqual({}); + expect(Object.keys(result)).toHaveLength(0); + }); + + it('should return all schemas', () => { + const xrSchema = { + type: 'object', + properties: { + spec: { type: 'object' }, + }, + }; + + const composedSchema = { + type: 'object', + properties: { + metadata: { type: 'object' }, + }, + }; + + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: {}, + requiredSchemas: { + 'xr-schema': { + openapiV3: xrSchema, + }, + 'composed-schema': { + openapiV3: composedSchema, + }, + }, + }; + + const result = getRequiredSchemas(req); + expect(Object.keys(result)).toHaveLength(2); + expect(result['xr-schema']).toEqual(xrSchema); + expect(result['composed-schema']).toEqual(composedSchema); + }); + + it('should handle undefined schemas', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: {}, + requiredSchemas: { + 'found-schema': { + openapiV3: { type: 'object' }, + }, + 'not-found-schema': { + openapiV3: undefined, + }, + }, + }; + + const result = getRequiredSchemas(req); + expect(Object.keys(result)).toHaveLength(2); + expect(result['found-schema']).toEqual({ type: 'object' }); + expect(result['not-found-schema']).toBeUndefined(); + }); +}); + +describe('getRequiredSchema', () => { + it('should return undefined and false when no required schemas exist', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: {}, + requiredSchemas: {}, + }; + + const [schema, resolved] = getRequiredSchema(req, 'test'); + expect(schema).toBeUndefined(); + expect(resolved).toBe(false); + }); + + it('should return undefined and false when schema not found', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: {}, + requiredSchemas: { + other: { + openapiV3: { type: 'object' }, + }, + }, + }; + + const [schema, resolved] = getRequiredSchema(req, 'test'); + expect(schema).toBeUndefined(); + expect(resolved).toBe(false); + }); + + it('should return schema and true when found', () => { + const xrSchema = { + type: 'object', + properties: { + apiVersion: { type: 'string' }, + kind: { type: 'string' }, + spec: { + type: 'object', + properties: { + region: { type: 'string' }, + }, + }, + }, + required: ['apiVersion', 'kind', 'spec'], + }; + + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: {}, + requiredSchemas: { + 'xr-schema': { + openapiV3: xrSchema, + }, + }, + }; + + const [schema, resolved] = getRequiredSchema(req, 'xr-schema'); + expect(resolved).toBe(true); + expect(schema).toEqual(xrSchema); + expect((schema as any)?.type).toBe('object'); + expect((schema as any)?.properties?.spec?.properties?.region?.type).toBe('string'); + }); + + it('should return undefined and true when schema was requested but not found', () => { + const req: RunFunctionRequest = { + meta: undefined, + observed: undefined, + desired: undefined, + input: undefined, + context: undefined, + extraResources: {}, + credentials: {}, + requiredResources: {}, + requiredSchemas: { + 'not-found': { + openapiV3: undefined, + }, + }, + }; + + const [schema, resolved] = getRequiredSchema(req, 'not-found'); + expect(resolved).toBe(true); + expect(schema).toBeUndefined(); + }); +}); diff --git a/src/request/request.ts b/src/request/request.ts index 15b6ede..7b78cce 100644 --- a/src/request/request.ts +++ b/src/request/request.ts @@ -226,3 +226,149 @@ export function getCredentials(req: RunFunctionRequest, name: string): Credentia } return creds; } + +/** + * Get a required resource from the request by name. + * + * Required resources are resources that the function specified it needs using + * response.requireResource. Crossplane fetches the requested resources and includes + * them in the next request. The boolean return value indicates whether Crossplane + * has resolved the requirement. + * + * @param req - The RunFunctionRequest containing required resources + * @param name - The name of the required resource group to retrieve + * @returns A tuple of [resources, resolved, error]: + * - resources: Array of Resource objects (empty if not found or error) + * - resolved: true if Crossplane attempted to resolve the requirement, false otherwise + * - error: Error if conversion failed, undefined otherwise + * + * @example + * ```typescript + * // After calling requireResource in a previous function invocation: + * const [resources, resolved, error] = getRequiredResource(req, "app-config"); + * if (error) { + * console.error("Failed to convert resources:", error); + * } else if (!resolved) { + * console.log("Resource requirement not yet resolved by Crossplane"); + * } else if (resources.length === 0) { + * console.log("Resource requirement resolved but no resources found"); + * } else { + * console.log("Found resources:", resources); + * resources.forEach(r => console.log(r.resource)); + * } + * ``` + */ +export function getRequiredResource( + req: RunFunctionRequest, + name: string +): [Resource[], boolean, Error | undefined] { + if (!req.requiredResources) { + return [[], false, undefined]; + } + + const rrs = req.requiredResources[name]; + if (!rrs) { + return [[], false, undefined]; + } + + const out: Resource[] = []; + for (const item of rrs.items || []) { + if (!item || !item.resource) { + continue; + } + + out.push({ + resource: item.resource, + connectionDetails: item.connectionDetails || {}, + ready: item.ready || 0, + }); + } + + return [out, true, undefined]; +} + +/** + * Get all required schemas from the request. + * + * Returns all schemas that were requested using response.requireSchema and + * resolved by Crossplane. The map key corresponds to the name used in + * requireSchema. Each value is the OpenAPI v3 schema as an unstructured object, + * or undefined if the schema could not be found. + * + * @param req - The RunFunctionRequest containing required schemas + * @returns A map of schema names to OpenAPI v3 schema objects (or undefined if not found) + * + * @example + * ```typescript + * const schemas = getRequiredSchemas(req); + * for (const [name, schema] of Object.entries(schemas)) { + * if (schema) { + * console.log(`Schema ${name}:`, schema); + * } else { + * console.log(`Schema ${name} was requested but not found`); + * } + * } + * ``` + */ +export function getRequiredSchemas( + req: RunFunctionRequest +): Record | undefined> { + const out: Record | undefined> = {}; + + if (!req.requiredSchemas) { + return out; + } + + for (const [name, schema] of Object.entries(req.requiredSchemas)) { + out[name] = schema?.openapiV3; + } + + return out; +} + +/** + * Get a required schema from the request by name. + * + * Returns the OpenAPI v3 schema for a resource kind that was requested using + * response.requireSchema. The boolean return value indicates whether Crossplane + * has resolved the requirement. + * + * @param req - The RunFunctionRequest containing required schemas + * @param name - The name of the required schema to retrieve + * @returns A tuple of [schema, resolved]: + * - schema: The OpenAPI v3 schema object, or undefined if not found + * - resolved: true if Crossplane attempted to resolve the requirement, false otherwise + * + * Note: When resolved is true but schema is undefined, it means Crossplane tried + * to find the schema but it doesn't exist for that resource kind. + * + * @example + * ```typescript + * // After calling requireSchema in a previous function invocation: + * const [schema, resolved] = getRequiredSchema(req, "xr-schema"); + * if (!resolved) { + * console.log("Schema not yet resolved by Crossplane"); + * } else if (!schema) { + * console.log("Schema resolved but not found (kind may not exist)"); + * } else { + * console.log("Schema properties:", schema.properties); + * console.log("Required fields:", schema.required); + * } + * ``` + */ +export function getRequiredSchema( + req: RunFunctionRequest, + name: string +): [Record | undefined, boolean] { + const schemas = req.requiredSchemas; + if (!schemas) { + return [undefined, false]; + } + + const schema = schemas[name]; + if (!schema) { + return [undefined, false]; + } + + return [schema.openapiV3, true]; +} diff --git a/src/response/response.test.ts b/src/response/response.test.ts index 4f155e5..e39c21b 100644 --- a/src/response/response.test.ts +++ b/src/response/response.test.ts @@ -1,5 +1,10 @@ import { describe, it, expect } from 'vitest'; -import { setDesiredCompositeStatus, setDesiredResources } from './response.js'; +import { + setDesiredCompositeStatus, + setDesiredResources, + requireSchema, + requireResource, +} from './response.js'; import type { RunFunctionResponse } from '../proto/run_function.js'; import { Ready } from '../proto/run_function.js'; @@ -439,3 +444,289 @@ describe('setDesiredResources', () => { expect(Object.keys(result.desired?.resources || {})).toHaveLength(0); }); }); + +describe('requireSchema', () => { + it('should add a schema requirement to an empty response', () => { + const rsp: RunFunctionResponse = { + conditions: [], + context: undefined, + desired: undefined, + meta: { tag: '', ttl: { seconds: 60, nanos: 0 } }, + requirements: undefined, + results: [], + }; + + const result = requireSchema(rsp, 'xr-schema', 'example.org/v1', 'MyResource'); + + expect(result.requirements).toBeDefined(); + expect(result.requirements?.schemas).toBeDefined(); + expect(result.requirements?.schemas?.['xr-schema']).toEqual({ + apiVersion: 'example.org/v1', + kind: 'MyResource', + }); + }); + + it('should add a schema requirement when requirements already exist', () => { + const rsp: RunFunctionResponse = { + conditions: [], + context: undefined, + desired: undefined, + meta: { tag: '', ttl: { seconds: 60, nanos: 0 } }, + requirements: { + extraResources: {}, + resources: { + 'existing-resource': { + apiVersion: 'v1', + kind: 'ConfigMap', + matchName: 'my-config', + }, + }, + schemas: {}, + }, + results: [], + }; + + const result = requireSchema(rsp, 'composite-schema', 'database.example.org/v1', 'Database'); + + expect(result.requirements?.resources?.['existing-resource']).toBeDefined(); + expect(result.requirements?.schemas?.['composite-schema']).toEqual({ + apiVersion: 'database.example.org/v1', + kind: 'Database', + }); + }); + + it('should add multiple schema requirements', () => { + const rsp: RunFunctionResponse = { + conditions: [], + context: undefined, + desired: undefined, + meta: { tag: '', ttl: { seconds: 60, nanos: 0 } }, + requirements: undefined, + results: [], + }; + + let result = requireSchema(rsp, 'xr-schema', 'example.org/v1', 'XR'); + result = requireSchema(result, 'composed-schema', 'example.org/v1', 'ComposedResource'); + result = requireSchema(result, 'claim-schema', 'example.org/v1', 'Claim'); + + expect(Object.keys(result.requirements?.schemas || {})).toHaveLength(3); + expect(result.requirements?.schemas?.['xr-schema']?.kind).toBe('XR'); + expect(result.requirements?.schemas?.['composed-schema']?.kind).toBe('ComposedResource'); + expect(result.requirements?.schemas?.['claim-schema']?.kind).toBe('Claim'); + }); + + it('should overwrite existing schema requirement with same name', () => { + const rsp: RunFunctionResponse = { + conditions: [], + context: undefined, + desired: undefined, + meta: { tag: '', ttl: { seconds: 60, nanos: 0 } }, + requirements: { + extraResources: {}, + resources: {}, + schemas: { + 'my-schema': { + apiVersion: 'old.example.org/v1', + kind: 'OldKind', + }, + }, + }, + results: [], + }; + + const result = requireSchema(rsp, 'my-schema', 'new.example.org/v2', 'NewKind'); + + expect(result.requirements?.schemas?.['my-schema']).toEqual({ + apiVersion: 'new.example.org/v2', + kind: 'NewKind', + }); + }); +}); + +describe('requireResource', () => { + it('should add a resource requirement by name', () => { + const rsp: RunFunctionResponse = { + conditions: [], + context: undefined, + desired: undefined, + meta: { tag: '', ttl: { seconds: 60, nanos: 0 } }, + requirements: undefined, + results: [], + }; + + const result = requireResource(rsp, 'app-config', { + apiVersion: 'v1', + kind: 'ConfigMap', + matchName: 'my-app-config', + namespace: 'production', + }); + + expect(result.requirements).toBeDefined(); + expect(result.requirements?.resources).toBeDefined(); + expect(result.requirements?.resources?.['app-config']).toEqual({ + apiVersion: 'v1', + kind: 'ConfigMap', + matchName: 'my-app-config', + namespace: 'production', + }); + }); + + it('should add a resource requirement by labels', () => { + const rsp: RunFunctionResponse = { + conditions: [], + context: undefined, + desired: undefined, + meta: { tag: '', ttl: { seconds: 60, nanos: 0 } }, + requirements: undefined, + results: [], + }; + + const result = requireResource(rsp, 'db-secrets', { + apiVersion: 'v1', + kind: 'Secret', + matchLabels: { + labels: { + app: 'database', + tier: 'backend', + }, + }, + namespace: 'production', + }); + + expect(result.requirements?.resources?.['db-secrets']).toEqual({ + apiVersion: 'v1', + kind: 'Secret', + matchLabels: { + labels: { + app: 'database', + tier: 'backend', + }, + }, + namespace: 'production', + }); + }); + + it('should add multiple resource requirements', () => { + const rsp: RunFunctionResponse = { + conditions: [], + context: undefined, + desired: undefined, + meta: { tag: '', ttl: { seconds: 60, nanos: 0 } }, + requirements: undefined, + results: [], + }; + + let result = requireResource(rsp, 'config', { + apiVersion: 'v1', + kind: 'ConfigMap', + matchName: 'app-config', + }); + + result = requireResource(result, 'secret', { + apiVersion: 'v1', + kind: 'Secret', + matchName: 'app-secret', + }); + + expect(Object.keys(result.requirements?.resources || {})).toHaveLength(2); + expect(result.requirements?.resources?.['config']?.kind).toBe('ConfigMap'); + expect(result.requirements?.resources?.['secret']?.kind).toBe('Secret'); + }); + + it('should add resource requirement when schemas already exist', () => { + const rsp: RunFunctionResponse = { + conditions: [], + context: undefined, + desired: undefined, + meta: { tag: '', ttl: { seconds: 60, nanos: 0 } }, + requirements: { + extraResources: {}, + resources: {}, + schemas: { + 'existing-schema': { + apiVersion: 'example.org/v1', + kind: 'MyKind', + }, + }, + }, + results: [], + }; + + const result = requireResource(rsp, 'namespaces', { + apiVersion: 'v1', + kind: 'Namespace', + matchLabels: { + labels: { + environment: 'production', + }, + }, + }); + + expect(result.requirements?.schemas?.['existing-schema']).toBeDefined(); + expect(result.requirements?.resources?.['namespaces']).toEqual({ + apiVersion: 'v1', + kind: 'Namespace', + matchLabels: { + labels: { + environment: 'production', + }, + }, + }); + }); + + it('should handle cluster-scoped resources without namespace', () => { + const rsp: RunFunctionResponse = { + conditions: [], + context: undefined, + desired: undefined, + meta: { tag: '', ttl: { seconds: 60, nanos: 0 } }, + requirements: undefined, + results: [], + }; + + const result = requireResource(rsp, 'all-namespaces', { + apiVersion: 'v1', + kind: 'Namespace', + matchLabels: { + labels: { + managed: 'true', + }, + }, + }); + + expect(result.requirements?.resources?.['all-namespaces']?.namespace).toBeUndefined(); + }); + + it('should overwrite existing resource requirement with same name', () => { + const rsp: RunFunctionResponse = { + conditions: [], + context: undefined, + desired: undefined, + meta: { tag: '', ttl: { seconds: 60, nanos: 0 } }, + requirements: { + extraResources: {}, + resources: { + 'my-resource': { + apiVersion: 'v1', + kind: 'ConfigMap', + matchName: 'old-config', + }, + }, + schemas: {}, + }, + results: [], + }; + + const result = requireResource(rsp, 'my-resource', { + apiVersion: 'v1', + kind: 'Secret', + matchName: 'new-secret', + }); + + expect(result.requirements?.resources?.['my-resource']).toEqual({ + apiVersion: 'v1', + kind: 'Secret', + matchName: 'new-secret', + }); + }); +}); diff --git a/src/response/response.ts b/src/response/response.ts index af45c27..17981d0 100644 --- a/src/response/response.ts +++ b/src/response/response.ts @@ -12,6 +12,7 @@ import { RunFunctionResponse, Severity, Ready, + ResourceSelector, } from '../proto/run_function.js'; import { Duration } from '../proto/google/protobuf/duration.js'; import { merge } from 'ts-deepmerge'; @@ -464,3 +465,120 @@ export function setOutput( rsp.output = output; return rsp; } + +/** + * Add a schema requirement to the response. + * + * This tells Crossplane to fetch the OpenAPI schema for the specified resource kind + * and include it in the next request's required_schemas field. Use request.getRequiredSchema + * to retrieve the resolved schema. + * + * For CRDs, Crossplane returns the spec.versions[].schema.openAPIV3Schema field. + * If Crossplane cannot find a schema for the requested kind, the schema will be + * empty (getRequiredSchema will return null with ok true). + * + * @param rsp - The RunFunctionResponse to update + * @param name - A unique name to identify this schema requirement + * @param apiVersion - API version of the resource kind (e.g., "example.org/v1") + * @param kind - Kind of resource (e.g., "MyResource") + * @returns The updated response + * + * @example + * ```typescript + * // Request the OpenAPI schema for an XR type + * rsp = requireSchema(rsp, "xr-schema", "example.org/v1", "MyResource"); + * + * // In the next function invocation, retrieve the schema: + * const [schema, ok] = getRequiredSchema(req, "xr-schema"); + * if (ok && schema) { + * console.log("Schema properties:", schema.properties); + * } + * ``` + */ +export function requireSchema( + rsp: RunFunctionResponse, + name: string, + apiVersion: string, + kind: string +): RunFunctionResponse { + if (!rsp.requirements) { + rsp.requirements = { + extraResources: {}, + resources: {}, + schemas: {}, + }; + } + if (!rsp.requirements.schemas) { + rsp.requirements.schemas = {}; + } + rsp.requirements.schemas[name] = { + apiVersion, + kind, + }; + return rsp; +} + +/** + * Add a resource requirement to the response. + * + * This tells Crossplane to fetch resources matching the selector and include them + * in the next request's required_resources field. Use request.getRequiredResource + * to retrieve the resolved resources. + * + * The selector can match resources by name or by labels. If namespace is omitted, + * cluster-scoped resources are matched, or namespaced resources across all namespaces + * when matching by labels. + * + * @param rsp - The RunFunctionResponse to update + * @param name - A unique name to identify this resource requirement + * @param selector - The resource selector specifying which resources to fetch + * @returns The updated response + * + * @example + * ```typescript + * // Match a specific ConfigMap by name + * rsp = requireResource(rsp, "app-config", { + * apiVersion: "v1", + * kind: "ConfigMap", + * matchName: "my-app-config", + * namespace: "production" + * }); + * + * // Match all Secrets with specific labels + * rsp = requireResource(rsp, "db-secrets", { + * apiVersion: "v1", + * kind: "Secret", + * matchLabels: { + * labels: { + * app: "database", + * tier: "backend" + * } + * }, + * namespace: "production" + * }); + * + * // In the next function invocation, retrieve the resources: + * const [resources, ok] = getRequiredResource(req, "app-config"); + * if (ok && resources.length > 0) { + * console.log("Found config:", resources[0].resource); + * } + * ``` + */ +export function requireResource( + rsp: RunFunctionResponse, + name: string, + selector: ResourceSelector +): RunFunctionResponse { + if (!rsp.requirements) { + rsp.requirements = { + extraResources: {}, + resources: {}, + schemas: {}, + }; + } + if (!rsp.requirements.resources) { + rsp.requirements.resources = {}; + } + rsp.requirements.resources[name] = selector; + return rsp; +} From 0a79b3ea89e343d9ae4d1f0262bcea7c4b427670 Mon Sep 17 00:00:00 2001 From: Steven Borrelli Date: Wed, 4 Mar 2026 07:32:23 +0100 Subject: [PATCH 2/5] simplify function return Signed-off-by: Steven Borrelli --- src/resource/resource.ts | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/resource/resource.ts b/src/resource/resource.ts index a4ce49e..bf40cda 100644 --- a/src/resource/resource.ts +++ b/src/resource/resource.ts @@ -1,5 +1,5 @@ // Resource utilities for working with Kubernetes resources and protobuf conversion -import { Ready, Resource } from '../proto/run_function.js'; +import { Ready, Resource } from "../proto/run_function.js"; // Type aliases for better readability export type ConnectionDetails = { [key: string]: Buffer }; @@ -51,7 +51,9 @@ export function newDesiredComposed(): DesiredComposed { * @param struct - The protobuf Struct to convert * @returns A plain JavaScript object representing the Kubernetes resource */ -export function asObject(struct: Record | undefined): Record { +export function asObject( + struct: Record | undefined, +): Record { if (!struct) { return {}; } @@ -73,7 +75,9 @@ export function asObject(struct: Record | undefined): Record): Record { +export function asStruct( + obj: Record, +): Record { // In our TypeScript implementation, this is essentially a pass-through // The actual conversion happens in the protobuf serialization layer return obj; @@ -92,14 +96,16 @@ export function asStruct(obj: Record): Record * @returns A Struct representation * @throws Error if conversion fails */ -export function mustStructObject(obj: Record): Record { +export function mustStructObject( + obj: Record, +): Record { try { return asStruct(obj); } catch (error) { throw new Error( `Failed to convert object to struct: ${ error instanceof Error ? error.message : String(error) - }` + }`, ); } } @@ -123,7 +129,9 @@ export function mustStructJSON(json: string): Record { return asStruct(obj); } catch (error) { throw new Error( - `Failed to parse JSON to struct: ${error instanceof Error ? error.message : String(error)}` + `Failed to parse JSON to struct: ${ + error instanceof Error ? error.message : String(error) + }`, ); } } @@ -140,7 +148,7 @@ export function mustStructJSON(json: string): Record { export function fromObject( obj: Record, connectionDetails?: ConnectionDetails, - ready?: Ready + ready?: Ready, ): Resource { return Resource.fromJSON({ resource: obj, @@ -156,7 +164,9 @@ export function fromObject( * @param resource - The Resource to extract from * @returns The plain JavaScript object, or undefined if not present */ -export function toObject(resource: Resource): Record | undefined { +export function toObject( + resource: Resource, +): Record | undefined { return resource.resource; } @@ -188,7 +198,9 @@ export function toObject(resource: Resource): Record | undefine export function fromModel>( obj: { toJSON: () => T }, connectionDetails?: ConnectionDetails, - ready?: Ready + ready?: Ready, ): Resource { - return fromObject(obj.toJSON() as Record, connectionDetails, ready); + // T already extends Record, so the cast is unnecessary. + // Pass the result directly to fromObject which accepts T via its signature. + return fromObject(obj.toJSON(), connectionDetails, ready); } From 515f1f049f7841c61605dfb7c33b0f911c59a3cc Mon Sep 17 00:00:00 2001 From: Steven Borrelli Date: Wed, 4 Mar 2026 09:46:07 +0100 Subject: [PATCH 3/5] drop error Signed-off-by: Steven Borrelli --- src/request/request.ts | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/src/request/request.ts b/src/request/request.ts index 7b78cce..b8d50b3 100644 --- a/src/request/request.ts +++ b/src/request/request.ts @@ -237,18 +237,15 @@ export function getCredentials(req: RunFunctionRequest, name: string): Credentia * * @param req - The RunFunctionRequest containing required resources * @param name - The name of the required resource group to retrieve - * @returns A tuple of [resources, resolved, error]: - * - resources: Array of Resource objects (empty if not found or error) + * @returns A tuple of [resources, resolved]: + * - resources: Array of Resource objects (empty if not found) * - resolved: true if Crossplane attempted to resolve the requirement, false otherwise - * - error: Error if conversion failed, undefined otherwise * * @example * ```typescript * // After calling requireResource in a previous function invocation: - * const [resources, resolved, error] = getRequiredResource(req, "app-config"); - * if (error) { - * console.error("Failed to convert resources:", error); - * } else if (!resolved) { + * const [resources, resolved] = getRequiredResource(req, "app-config"); + * if (!resolved) { * console.log("Resource requirement not yet resolved by Crossplane"); * } else if (resources.length === 0) { * console.log("Resource requirement resolved but no resources found"); @@ -261,14 +258,14 @@ export function getCredentials(req: RunFunctionRequest, name: string): Credentia export function getRequiredResource( req: RunFunctionRequest, name: string -): [Resource[], boolean, Error | undefined] { +): [Resource[], boolean] { if (!req.requiredResources) { - return [[], false, undefined]; + return [[], false]; } const rrs = req.requiredResources[name]; if (!rrs) { - return [[], false, undefined]; + return [[], false]; } const out: Resource[] = []; @@ -284,7 +281,7 @@ export function getRequiredResource( }); } - return [out, true, undefined]; + return [out, true]; } /** From a69015dc240197eecd285a5324de08eaf1b2bdd4 Mon Sep 17 00:00:00 2001 From: Steven Borrelli Date: Wed, 4 Mar 2026 09:51:22 +0100 Subject: [PATCH 4/5] fix copilot review issues Signed-off-by: Steven Borrelli --- src/request/request.test.ts | 18 ++++++------------ src/request/request.ts | 5 +---- src/resource/resource.ts | 28 +++++++++------------------- src/response/response.ts | 6 +++--- 4 files changed, 19 insertions(+), 38 deletions(-) diff --git a/src/request/request.test.ts b/src/request/request.test.ts index 3fe007b..524d48b 100644 --- a/src/request/request.test.ts +++ b/src/request/request.test.ts @@ -741,10 +741,9 @@ describe('getRequiredResource', () => { requiredSchemas: {}, }; - const [resources, resolved, error] = getRequiredResource(req, 'test'); + const [resources, resolved] = getRequiredResource(req, 'test'); expect(resources).toEqual([]); expect(resolved).toBe(false); - expect(error).toBeUndefined(); }); it('should return empty array and false when resource not found', () => { @@ -774,10 +773,9 @@ describe('getRequiredResource', () => { requiredSchemas: {}, }; - const [resources, resolved, error] = getRequiredResource(req, 'test'); + const [resources, resolved] = getRequiredResource(req, 'test'); expect(resources).toEqual([]); expect(resolved).toBe(false); - expect(error).toBeUndefined(); }); it('should return resources when found', () => { @@ -808,8 +806,7 @@ describe('getRequiredResource', () => { requiredSchemas: {}, }; - const [resources, resolved, error] = getRequiredResource(req, 'test'); - expect(error).toBeUndefined(); + const [resources, resolved] = getRequiredResource(req, 'test'); expect(resolved).toBe(true); expect(resources).toHaveLength(1); expect(resources[0]?.resource?.kind).toBe('ConfigMap'); @@ -853,8 +850,7 @@ describe('getRequiredResource', () => { requiredSchemas: {}, }; - const [resources, resolved, error] = getRequiredResource(req, 'secrets'); - expect(error).toBeUndefined(); + const [resources, resolved] = getRequiredResource(req, 'secrets'); expect(resolved).toBe(true); expect(resources).toHaveLength(2); expect(resources[0]?.resource?.metadata?.name).toBe('secret-1'); @@ -878,8 +874,7 @@ describe('getRequiredResource', () => { requiredSchemas: {}, }; - const [resources, resolved, error] = getRequiredResource(req, 'empty'); - expect(error).toBeUndefined(); + const [resources, resolved] = getRequiredResource(req, 'empty'); expect(resolved).toBe(true); expect(resources).toEqual([]); }); @@ -914,8 +909,7 @@ describe('getRequiredResource', () => { requiredSchemas: {}, }; - const [resources, resolved, error] = getRequiredResource(req, 'test'); - expect(error).toBeUndefined(); + const [resources, resolved] = getRequiredResource(req, 'test'); expect(resolved).toBe(true); expect(resources).toHaveLength(1); expect(resources[0]?.connectionDetails?.username).toEqual(Buffer.from('admin')); diff --git a/src/request/request.ts b/src/request/request.ts index b8d50b3..ca833cf 100644 --- a/src/request/request.ts +++ b/src/request/request.ts @@ -255,10 +255,7 @@ export function getCredentials(req: RunFunctionRequest, name: string): Credentia * } * ``` */ -export function getRequiredResource( - req: RunFunctionRequest, - name: string -): [Resource[], boolean] { +export function getRequiredResource(req: RunFunctionRequest, name: string): [Resource[], boolean] { if (!req.requiredResources) { return [[], false]; } diff --git a/src/resource/resource.ts b/src/resource/resource.ts index bf40cda..0c476d0 100644 --- a/src/resource/resource.ts +++ b/src/resource/resource.ts @@ -1,5 +1,5 @@ // Resource utilities for working with Kubernetes resources and protobuf conversion -import { Ready, Resource } from "../proto/run_function.js"; +import { Ready, Resource } from '../proto/run_function.js'; // Type aliases for better readability export type ConnectionDetails = { [key: string]: Buffer }; @@ -51,9 +51,7 @@ export function newDesiredComposed(): DesiredComposed { * @param struct - The protobuf Struct to convert * @returns A plain JavaScript object representing the Kubernetes resource */ -export function asObject( - struct: Record | undefined, -): Record { +export function asObject(struct: Record | undefined): Record { if (!struct) { return {}; } @@ -75,9 +73,7 @@ export function asObject( * @param obj - The plain JavaScript object to convert * @returns A protobuf Struct representation */ -export function asStruct( - obj: Record, -): Record { +export function asStruct(obj: Record): Record { // In our TypeScript implementation, this is essentially a pass-through // The actual conversion happens in the protobuf serialization layer return obj; @@ -96,16 +92,14 @@ export function asStruct( * @returns A Struct representation * @throws Error if conversion fails */ -export function mustStructObject( - obj: Record, -): Record { +export function mustStructObject(obj: Record): Record { try { return asStruct(obj); } catch (error) { throw new Error( `Failed to convert object to struct: ${ error instanceof Error ? error.message : String(error) - }`, + }` ); } } @@ -129,9 +123,7 @@ export function mustStructJSON(json: string): Record { return asStruct(obj); } catch (error) { throw new Error( - `Failed to parse JSON to struct: ${ - error instanceof Error ? error.message : String(error) - }`, + `Failed to parse JSON to struct: ${error instanceof Error ? error.message : String(error)}` ); } } @@ -148,7 +140,7 @@ export function mustStructJSON(json: string): Record { export function fromObject( obj: Record, connectionDetails?: ConnectionDetails, - ready?: Ready, + ready?: Ready ): Resource { return Resource.fromJSON({ resource: obj, @@ -164,9 +156,7 @@ export function fromObject( * @param resource - The Resource to extract from * @returns The plain JavaScript object, or undefined if not present */ -export function toObject( - resource: Resource, -): Record | undefined { +export function toObject(resource: Resource): Record | undefined { return resource.resource; } @@ -198,7 +188,7 @@ export function toObject( export function fromModel>( obj: { toJSON: () => T }, connectionDetails?: ConnectionDetails, - ready?: Ready, + ready?: Ready ): Resource { // T already extends Record, so the cast is unnecessary. // Pass the result directly to fromObject which accepts T via its signature. diff --git a/src/response/response.ts b/src/response/response.ts index 17981d0..1550d83 100644 --- a/src/response/response.ts +++ b/src/response/response.ts @@ -475,7 +475,7 @@ export function setOutput( * * For CRDs, Crossplane returns the spec.versions[].schema.openAPIV3Schema field. * If Crossplane cannot find a schema for the requested kind, the schema will be - * empty (getRequiredSchema will return null with ok true). + * empty (getRequiredSchema will return undefined with resolved true). * * @param rsp - The RunFunctionResponse to update * @param name - A unique name to identify this schema requirement @@ -558,8 +558,8 @@ export function requireSchema( * }); * * // In the next function invocation, retrieve the resources: - * const [resources, ok] = getRequiredResource(req, "app-config"); - * if (ok && resources.length > 0) { + * const [resources, resolved, error] = getRequiredResource(req, "app-config"); + * if (resolved && !error && resources.length > 0) { * console.log("Found config:", resources[0].resource); * } * ``` From b9447a8b92facdd705d86b5b902b0e52484e5a99 Mon Sep 17 00:00:00 2001 From: Steven Borrelli Date: Wed, 4 Mar 2026 10:51:06 +0100 Subject: [PATCH 5/5] formt and export capability Signed-off-by: Steven Borrelli --- src/index.ts | 77 ++++++++++++++++++++++++++-------------------------- 1 file changed, 39 insertions(+), 38 deletions(-) diff --git a/src/index.ts b/src/index.ts index 2b546a5..ebadd69 100644 --- a/src/index.ts +++ b/src/index.ts @@ -6,82 +6,83 @@ export { FunctionRunner, getServer } from './function/function.js'; // Request helpers export { - getDesiredCompositeResource, - getObservedCompositeResource, + getContextKey, + getCredentials, getDesiredComposedResources, - getObservedComposedResources, + getDesiredCompositeResource, getInput, - getContextKey, - getRequiredResources, + getObservedComposedResources, + getObservedCompositeResource, getRequiredResource, + getRequiredResources, getRequiredSchema, getRequiredSchemas, - getCredentials, } from './request/request.js'; // Response helpers export { - to, + DEFAULT_TTL, fatal, normal, - warning, + requireResource, + requireSchema, + setContextKey, setDesiredComposedResources, - setDesiredResources, - setDesiredCompositeStatus, setDesiredCompositeResource, - updateDesiredComposedResources, - update, - setContextKey, + setDesiredCompositeStatus, + setDesiredResources, setOutput, - requireSchema, - requireResource, - DEFAULT_TTL, + to, + update, + updateDesiredComposedResources, + warning, } from './response/response.js'; // Resource utilities export { asObject, asStruct, - fromObject, + type Composite, + type ConnectionDetails, + type DesiredComposed, fromModel, - toObject, - newDesiredComposed, - mustStructObject, + fromObject, mustStructJSON, - type Composite, + mustStructObject, + newDesiredComposed, type ObservedComposed, - type DesiredComposed, - type ConnectionDetails, + toObject, } from './resource/resource.js'; // Runtime utilities export { - newGrpcServer, - startServer, getServerCredentials, + newGrpcServer, type ServerOptions, + startServer, } from './runtime/runtime.js'; // Protocol buffer types export { - RunFunctionRequest, - RunFunctionResponse, - Resource, - Severity, - Result, - State, - Ready, - Target, - Status, + Capability, Condition, - Resources, - Credentials, CredentialData, + Credentials, + FunctionRunnerServiceService, + Ready, Requirements, + Resource, + Resources, ResourceSelector, - SchemaSelector, + Result, + RunFunctionRequest, + RunFunctionResponse, Schema, - FunctionRunnerServiceService, + SchemaSelector, + Severity, + State, + Status, + Target, } from './proto/run_function.js'; export type { Logger } from 'pino';