first commit
This commit is contained in:
@@ -0,0 +1,230 @@
|
||||
import {RuntimeError, ValidationError} from '../cloud-spi/errors'
|
||||
import { azure, type AzureRuntimeClient } from '../azure'
|
||||
import { azureServerlessSchema } from '../cloud-spi/serverlessSchema'
|
||||
import type {
|
||||
CloudResource,
|
||||
CloudServiceAdapter,
|
||||
CloudServiceDescriptorOverride,
|
||||
CreateResourceInput,
|
||||
ResourceQuery,
|
||||
ServerlessInvokeResult,
|
||||
ServiceSchema,
|
||||
} from '../cloud-spi/types'
|
||||
|
||||
interface AzureFunctionRecord {
|
||||
id?: string
|
||||
name?: string
|
||||
kind?: string
|
||||
location?: string
|
||||
type?: string
|
||||
properties?: {
|
||||
state?: string
|
||||
status?: string
|
||||
runtime?: string
|
||||
functionAppName?: string
|
||||
lastModifiedTimeUtc?: string
|
||||
scriptHref?: string
|
||||
invokeUrlTemplate?: string
|
||||
config?: Record<string, unknown>
|
||||
files?: Record<string, string>
|
||||
}
|
||||
}
|
||||
|
||||
interface AzureFunctionListResponse {
|
||||
value?: AzureFunctionRecord[]
|
||||
}
|
||||
|
||||
export class AzureServerlessAdapter implements CloudServiceAdapter {
|
||||
readonly cloud = 'azure' as const
|
||||
readonly service = 'serverless' as const
|
||||
|
||||
constructor(private readonly client: AzureRuntimeClient = azure) { }
|
||||
|
||||
/**
|
||||
* floci-az answers 501 NotImplemented on /functions, so this adapter cannot
|
||||
* currently serve a request even though it is registered. Reporting
|
||||
* coming_soon keeps the nav honest; remove this once the runtime ships the
|
||||
* endpoint and the adapter's own tests exercise it against a real response.
|
||||
*/
|
||||
descriptorOverride(): CloudServiceDescriptorOverride {
|
||||
return {
|
||||
availability: 'coming_soon',
|
||||
reason: 'The Floci-AZ runtime returns 501 NotImplemented for the Azure Functions endpoint.',
|
||||
}
|
||||
}
|
||||
|
||||
schema(): ServiceSchema {
|
||||
return azureServerlessSchema()
|
||||
}
|
||||
|
||||
async list(query: ResourceQuery = {}): Promise<CloudResource[]> {
|
||||
const body = await this.azureJson<AzureFunctionListResponse | AzureFunctionRecord[]>(
|
||||
'/functions',
|
||||
{ method: 'GET' },
|
||||
{ emptyOnNotFound: true },
|
||||
)
|
||||
|
||||
const records = Array.isArray(body) ? body : body?.value ?? []
|
||||
return filterBySearch(records.map(toFunctionResource), query.search)
|
||||
}
|
||||
|
||||
async get(id: string): Promise<CloudResource | null> {
|
||||
const body = await this.azureJson<AzureFunctionRecord>(
|
||||
`/functions/${encodeURIComponent(id)}`,
|
||||
{ method: 'GET' },
|
||||
{ emptyOnNotFound: true },
|
||||
)
|
||||
|
||||
return body ? toFunctionResource(body) : null
|
||||
}
|
||||
|
||||
async create(input: CreateResourceInput): Promise<CloudResource> {
|
||||
const functionName = stringValue(input.values.functionName ?? input.values.name)
|
||||
const runtime = stringValue(input.values.runtime) || 'node'
|
||||
const handler = stringValue(input.values.handler)
|
||||
const code = stringValue(input.values.code)
|
||||
const location = stringValue(input.values.location)
|
||||
const functionAppName = stringValue(input.values.functionAppName)
|
||||
|
||||
if (!functionName) throw new ValidationError('functionName is required')
|
||||
|
||||
const body = await this.azureJson<AzureFunctionRecord>(
|
||||
'/functions',
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
name: functionName,
|
||||
runtime,
|
||||
handler: handler || undefined,
|
||||
code: code || undefined,
|
||||
location: location || undefined,
|
||||
functionAppName: functionAppName || undefined,
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
if (!body) throw new RuntimeError('Azure Functions create returned an empty response')
|
||||
return toFunctionResource(body)
|
||||
}
|
||||
|
||||
async delete(id: string): Promise<void> {
|
||||
await this.client.fetch(
|
||||
`/functions/${encodeURIComponent(id)}`,
|
||||
{ method: 'DELETE' },
|
||||
{ emptyOnNotFound: true },
|
||||
)
|
||||
}
|
||||
async invoke(id: string, payload: string): Promise<ServerlessInvokeResult> {
|
||||
const startedAt = performance.now()
|
||||
|
||||
const body = await this.azureJson<{
|
||||
statusCode?: number
|
||||
payload?: unknown
|
||||
body?: unknown
|
||||
error?: string
|
||||
functionError?: string
|
||||
logResult?: string
|
||||
}>(
|
||||
`/functions/${encodeURIComponent(id)}/invoke`,
|
||||
{
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
payload: payload?.trim() ? payload : '{}',
|
||||
}),
|
||||
},
|
||||
)
|
||||
|
||||
return {
|
||||
statusCode: body?.statusCode ?? 200,
|
||||
payload: stringifyPayload(body?.payload ?? body?.body ?? ''),
|
||||
functionError: body?.functionError ?? body?.error,
|
||||
logResult: body?.logResult,
|
||||
executionDuration: Math.round(performance.now() - startedAt),
|
||||
}
|
||||
}
|
||||
|
||||
private async azureJson<T>(
|
||||
path: string,
|
||||
init: RequestInit,
|
||||
options?: { emptyOnNotFound?: boolean },
|
||||
): Promise<T | null> {
|
||||
const res = await this.client.fetch(
|
||||
path,
|
||||
{
|
||||
...init,
|
||||
headers: {
|
||||
accept: 'application/json',
|
||||
'content-type': 'application/json',
|
||||
...(init.headers ?? {}),
|
||||
},
|
||||
},
|
||||
options,
|
||||
)
|
||||
|
||||
if (!res || res.status === 204) return null
|
||||
return await res.json() as T
|
||||
}
|
||||
}
|
||||
|
||||
function toFunctionResource(record: AzureFunctionRecord): CloudResource {
|
||||
const name = stringValue(record.name ?? record.id)
|
||||
const props = record.properties ?? {}
|
||||
|
||||
return {
|
||||
id: name,
|
||||
name,
|
||||
cloud: 'azure',
|
||||
service: 'serverless',
|
||||
type: 'azure-function',
|
||||
region: record.location ?? null,
|
||||
createdAt: props.lastModifiedTimeUtc ?? null,
|
||||
status: props.state ?? props.status ?? null,
|
||||
metadata: {
|
||||
provider: 'azure',
|
||||
serverlessService: 'functions',
|
||||
kind: record.kind,
|
||||
resourceType: record.type,
|
||||
runtime: props.runtime,
|
||||
functionAppName: props.functionAppName,
|
||||
lastModified: props.lastModifiedTimeUtc,
|
||||
triggerType: getTriggerType(props.config),
|
||||
scriptHref: props.scriptHref,
|
||||
invokeUrlTemplate: props.invokeUrlTemplate,
|
||||
config: props.config,
|
||||
files: props.files,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
function stringValue(value: unknown): string {
|
||||
return typeof value === 'string' ? value.trim() : ''
|
||||
}
|
||||
|
||||
function stringifyPayload(value: unknown): string {
|
||||
if (typeof value === 'string') return value
|
||||
if (value === undefined || value === null) return ''
|
||||
return JSON.stringify(value)
|
||||
}
|
||||
|
||||
function getTriggerType(config?: Record<string, unknown>): string | undefined {
|
||||
const bindings = config?.bindings
|
||||
if (!Array.isArray(bindings)) return undefined
|
||||
|
||||
const trigger = bindings.find((binding) => {
|
||||
if (!binding || typeof binding !== 'object') return false
|
||||
|
||||
const type = (binding as Record<string, unknown>).type
|
||||
return typeof type === 'string' && type.toLowerCase().endsWith('trigger')
|
||||
})
|
||||
|
||||
if (!trigger || typeof trigger !== 'object') return undefined
|
||||
|
||||
const type = (trigger as Record<string, unknown>).type
|
||||
return typeof type === 'string' ? type : undefined
|
||||
}
|
||||
|
||||
function filterBySearch(resources: CloudResource[], search?: string): CloudResource[] {
|
||||
const normalized = search?.trim().toLowerCase()
|
||||
if (!normalized) return resources
|
||||
return resources.filter((resource) => resource.name.toLowerCase().includes(normalized))
|
||||
}
|
||||
Reference in New Issue
Block a user