| /** | |
| * HTTP client support for talking to the Kubernetes API from code running | |
| * inside a cluster. The module builds a `K8sHttpClient` service on top of the | |
| * shared HTTP client, points requests at the in-cluster API endpoint, and uses | |
| * the mounted service-account token when one is available. | |
| * | |
| * @since 4.0.0 | |
| */ | |
| import type * as v1 from "kubernetes-types/core/v1.d.ts" | |
| import * as Context from "../../Context.ts" | |
| import * as Effect from "../../Effect.ts" | |
| import * as FileSystem from "../../FileSystem.ts" | |
| import { identity } from "../../Function.ts" | |
| import * as Layer from "../../Layer.ts" | |
| import * as Option from "../../Option.ts" | |
| import * as Result from "../../Result.ts" | |
| import * as Schedule from "../../Schedule.ts" | |
| import * as Schema from "../../Schema.ts" | |
| import * as HttpClient from "../http/HttpClient.ts" | |
| import * as HttpClientError from "../http/HttpClientError.ts" | |
| import * as HttpClientRequest from "../http/HttpClientRequest.ts" | |
| import * as HttpClientResponse from "../http/HttpClientResponse.ts" | |
| /** | |
| * Service tag for the HTTP client used to call the Kubernetes API. | |
| * | |
| * @category services | |
| * @since 4.0.0 | |
| */ | |
| export class K8sHttpClient extends Context.Service< | |
| K8sHttpClient, | |
| HttpClient.HttpClient | |
| >()("effect/cluster/K8sHttpClient") {} | |
| /** | |
| * Layer that configures `K8sHttpClient` for the in-cluster Kubernetes API. | |
| * | |
| * **Details** | |
| * | |
| * It targets `https://kubernetes.default.svc/api`, adds the service-account | |
| * bearer token when available, requires successful HTTP statuses, and retries | |
| * transient failures. | |
| * | |
| * @category layers | |
| * @since 4.0.0 | |
| */ | |
| export const layer: Layer.Layer< | |
| K8sHttpClient, | |
| never, | |
| HttpClient.HttpClient | FileSystem.FileSystem | |
| > = Layer.effect( | |
| K8sHttpClient, | |
| Effect.gen(function*() { | |
| const fs = yield* FileSystem.FileSystem | |
| const token = yield* fs.readFileString("/var/run/secrets/kubernetes.io/serviceaccount/token").pipe( | |
| Effect.option | |
| ) | |
| return (yield* HttpClient.HttpClient).pipe( | |
| HttpClient.mapRequest(HttpClientRequest.prependUrl("https://kubernetes.default.svc/api")), | |
| token._tag === "Some" ? HttpClient.mapRequest(HttpClientRequest.bearerToken(token.value.trim())) : identity, | |
| HttpClient.filterStatusOk, | |
| HttpClient.retryTransient({ | |
| schedule: Schedule.spaced(5000) | |
| }) | |
| ) | |
| }) | |
| ) | |
| /** | |
| * Creates a cached effect that fetches running Kubernetes pods. | |
| * | |
| * **Details** | |
| * | |
| * The request can be limited by namespace and label selector, and the result is a | |
| * map keyed by pod IP address. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const makeGetPods: ( | |
| options?: { | |
| readonly namespace?: string | undefined | |
| readonly labelSelector?: string | undefined | |
| } | undefined | |
| ) => Effect.Effect< | |
| Effect.Effect<Map<string, Pod>, HttpClientError.HttpClientError | Schema.SchemaError, never>, | |
| never, | |
| K8sHttpClient | |
| > = Effect.fnUntraced(function*(options?: { | |
| readonly namespace?: string | undefined | |
| readonly labelSelector?: string | undefined | |
| }) { | |
| const client = yield* K8sHttpClient | |
| const getPods = HttpClientRequest.get( | |
| options?.namespace ? `/v1/namespaces/${options.namespace}/pods` : "/v1/pods" | |
| ).pipe( | |
| HttpClientRequest.setUrlParam("fieldSelector", "status.phase=Running"), | |
| options?.labelSelector ? HttpClientRequest.setUrlParam("labelSelector", options.labelSelector) : identity | |
| ) | |
| return yield* client.execute(getPods).pipe( | |
| Effect.flatMap(HttpClientResponse.schemaBodyJson(PodList)), | |
| Effect.map((list) => { | |
| const pods = new Map<string, Pod>() | |
| for (let i = 0; i < list.items.length; i++) { | |
| const pod = list.items[i] | |
| pods.set(pod.status.podIP, pod) | |
| } | |
| return pods | |
| }), | |
| Effect.tapCause((cause) => Effect.logWarning("Failed to fetch pods from Kubernetes API", cause)), | |
| Effect.cachedWithTTL("10 seconds") | |
| ) | |
| }) | |
| /** | |
| * Creates a scoped function that ensures a Kubernetes pod exists and waits until | |
| * it is ready. | |
| * | |
| * **Details** | |
| * | |
| * The pod defaults to the `default` namespace and is deleted when the surrounding | |
| * scope closes. | |
| * | |
| * @category constructors | |
| * @since 4.0.0 | |
| */ | |
| export const makeCreatePod = Effect.gen(function*() { | |
| const client = yield* K8sHttpClient | |
| return Effect.fnUntraced(function*(spec: v1.Pod) { | |
| spec = { | |
| apiVersion: "v1", | |
| kind: "Pod", | |
| metadata: { | |
| namespace: "default", | |
| ...spec.metadata | |
| }, | |
| ...spec | |
| } | |
| const namespace = spec.metadata?.namespace ?? "default" | |
| const name = spec.metadata!.name! | |
| const readPodRaw = HttpClientRequest.get(`/v1/namespaces/${namespace}/pods/${name}`).pipe( | |
| client.execute | |
| ) | |
| const readPod = readPodRaw.pipe( | |
| Effect.flatMap(HttpClientResponse.schemaBodyJson(Pod)), | |
| Effect.asSome, | |
| Effect.retry({ | |
| while: (e) => e._tag === "SchemaError", | |
| schedule: Schedule.spaced("1 seconds") | |
| }), | |
| Effect.catchFilter( | |
| (err) => | |
| HttpClientError.isHttpClientError(err) && err.reason._tag === "StatusCodeError" && | |
| err.reason.response.status === 404 | |
| ? Result.succeed(err) | |
| : Result.fail(err), | |
| () => Effect.succeedNone | |
| ), | |
| Effect.orDie | |
| ) | |
| const isPodFound = readPodRaw.pipe( | |
| Effect.as(true), | |
| Effect.catchFilter( | |
| (err) => | |
| HttpClientError.isHttpClientError(err) && err.reason._tag === "StatusCodeError" && | |
| err.reason.response.status === 404 | |
| ? Result.succeed(err) | |
| : Result.fail(err), | |
| () => Effect.succeed(false) | |
| ) | |
| ) | |
| const createPod = HttpClientRequest.post(`/v1/namespaces/${namespace}/pods`).pipe( | |
| HttpClientRequest.bodyJsonUnsafe(spec), | |
| client.execute, | |
| Effect.catchFilter( | |
| (err) => | |
| HttpClientError.isHttpClientError(err) && err.reason._tag === "StatusCodeError" && | |
| err.reason.response.status === 409 | |
| ? Result.succeed(err) | |
| : Result.fail(err), | |
| () => readPod | |
| ), | |
| Effect.tapCause(Effect.logInfo), | |
| Effect.orDie | |
| ) | |
| const deletePod = HttpClientRequest.delete(`/v1/namespaces/${namespace}/pods/${name}`).pipe( | |
| client.execute, | |
| Effect.flatMap((res) => res.json), | |
| Effect.catchFilter( | |
| (err) => | |
| HttpClientError.isHttpClientError(err) && err.reason._tag === "StatusCodeError" && | |
| err.reason.response.status === 404 | |
| ? Result.succeed(err) | |
| : Result.fail(err), | |
| () => Effect.void | |
| ), | |
| Effect.tapCause(Effect.logInfo), | |
| Effect.orDie, | |
| Effect.asVoid | |
| ) | |
| yield* Effect.addFinalizer(Effect.fnUntraced(function*() { | |
| yield* deletePod | |
| yield* isPodFound.pipe( | |
| Effect.repeat({ | |
| until: (found) => !found, | |
| schedule: Schedule.spaced("3 seconds") | |
| }), | |
| Effect.orDie | |
| ) | |
| })) | |
| let opod = Option.none<Pod>() | |
| while (Option.isNone(opod) || !opod.value.isReady) { | |
| if (Option.isNone(opod)) { | |
| yield* createPod | |
| } | |
| yield* Effect.sleep("3 seconds") | |
| opod = yield* readPod | |
| } | |
| return opod.value.status | |
| }, Effect.withSpan("K8sHttpClient.createPod")) | |
| }) | |
| /** | |
| * Schema for the subset of Kubernetes Pod status used by cluster helpers. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export class PodStatus extends Schema.Class<PodStatus>("@effect/cluster/K8sHttpClient/PodStatus")({ | |
| phase: Schema.String, | |
| conditions: Schema.Array(Schema.Struct({ | |
| type: Schema.String, | |
| status: Schema.String, | |
| lastTransitionTime: Schema.NullOr(Schema.String) | |
| })), | |
| podIP: Schema.String, | |
| hostIP: Schema.String | |
| }) {} | |
| /** | |
| * Schema for Kubernetes Pod values used by cluster helpers. | |
| * | |
| * **Details** | |
| * | |
| * The model exposes readiness helpers derived from the pod status conditions. | |
| * | |
| * @category schemas | |
| * @since 4.0.0 | |
| */ | |
| export class Pod extends Schema.Class<Pod>("@effect/cluster/K8sHttpClient/Pod")({ | |
| status: PodStatus | |
| }) { | |
| get isReady(): boolean { | |
| for (let i = 0; i < this.status.conditions.length; i++) { | |
| const condition = this.status.conditions[i] | |
| if (condition.type === "Ready") { | |
| return condition.status === "True" | |
| } | |
| } | |
| return false | |
| } | |
| get isReadyOrInitializing(): boolean { | |
| let initializedAt: string | null | undefined | |
| let readyAt: string | null | undefined | |
| for (let i = 0; i < this.status.conditions.length; i++) { | |
| const condition = this.status.conditions[i] | |
| switch (condition.type) { | |
| case "Initialized": { | |
| if (condition.status !== "True") { | |
| return true | |
| } | |
| initializedAt = condition.lastTransitionTime | |
| break | |
| } | |
| case "Ready": { | |
| if (condition.status === "True") { | |
| return true | |
| } | |
| readyAt = condition.lastTransitionTime | |
| break | |
| } | |
| } | |
| } | |
| // if the pod is still booting up, consider it ready as it would have | |
| // already registered itself with RunnerStorage by now | |
| return initializedAt === readyAt | |
| } | |
| } | |
| const PodList = Schema.Struct({ | |
| items: Schema.Array(Pod) | |
| }) | |
Xet Storage Details
- Size:
- 9.13 kB
- Xet hash:
- 86c079b7339283adf31ebad0093595be70b0af0fb0b579b5e403659a90508a33
·
Xet efficiently stores files, intelligently splitting them into unique chunks and accelerating uploads and downloads. More info.