k8ts
Version:
Powerful framework for building Kubernetes manifests in TypeScript.
203 lines (193 loc) • 6.53 kB
text/typescript
import {
PortSet,
ResourcesSpec,
Unit,
type CmdBuilder,
type InputPortSetRecord,
type Ref2_Of,
type TaggedImage
} from "@k8ts/instruments"
import type { CDK } from "@k8ts/sample-interfaces"
import { toContainerPorts } from "../../utils/adapters"
import { Resource_Child, Resource_Entity, Resource_Ref_Min, Resource_Top } from "@k8ts/instruments"
import { seq } from "doddle"
import { mapKeys, mapValues, omitBy } from "lodash"
import { Env } from "../../../env"
import type { Env_Leaf } from "../../../env/types"
import { v1 } from "../../../kinds/default"
import { Container_Mount_Device, type Container_Mount } from "./mounts"
const container_ResourcesSpec = ResourcesSpec.make({
cpu: Unit.Cpu,
memory: Unit.Data
})
type Container_Resources = (typeof container_ResourcesSpec)["__INPUT__"]
type Container_Mount_Some = Resource_Ref_Min<
v1.Pod.Container.DeviceMount._ | v1.Pod.Container.VolumeMount._
>
export type Container_Mounts = {
[key: string]: Container_Mount_Some
}
export interface Container_Env_From {
source: Ref2_Of<v1.ConfigMap._> | Ref2_Of<v1.Secret._>
prefix?: string
optional?: boolean
}
export interface Container_Props<
Ports extends string = never,
_Env extends Record<string, Env_Leaf> = Record<string, Env_Leaf>
> extends Omit<CDK.Container, "name"> {
$image: TaggedImage
$ports?: InputPortSetRecord<Ports>
$command?: CmdBuilder
$mounts?: Container_Mounts
$env?: _Env
$envFrom?: Container_Env_From[]
$resources?: Container_Resources
}
export class Container<Ports extends string = string> extends Resource_Child<
Container_Props<Ports>
> {
__PORTS__!: Ports
get kind() {
return v1.Pod.Container._
}
protected __needs__(): Record<string, Resource_Entity | Resource_Entity[] | undefined> {
const a = this.mounts
return mapValues(
mapKeys(a, x => x.path),
x => x.mount.volume
)
}
get mounts() {
return seq(Object.entries(this.props.$mounts ?? {}))
.map(([path, mount]) => {
return {
mount: mount as Container_Mount,
path: path as string
}
})
.toArray()
.pull()
}
get volumes() {
return seq(this.mounts.map(x => x.mount.volume))
.uniq()
.toArray()
.pull()
}
get ports() {
return PortSet.make(this.props.$ports)
}
protected __submanifest__(): CDK.Container {
const self = this
const { $image, $ports, $command, $env } = self.props
const untaggedProps = omitBy(self.props, (_, k) => k.startsWith("$"))
let resourcesObject = self._resources()?.toObject()
const containerPorts =
$ports &&
seq(toContainerPorts(PortSet.make($ports)).values())
.toArray()
.pull()
const env = Env($env)
for (const [key, value] of env.entries) {
if (typeof value !== "object") {
continue
}
if (!value) {
continue
}
const backend = value.$backend
if (backend instanceof Resource_Entity) {
if (backend.namespace !== self.namespace) {
throw new Error(
`Environment variable reference "${key}" must be in the same namespace as the container "${self}", but was ${backend}"`
)
}
}
}
for (const vol of self.volumes) {
if (vol.sourceNamespace !== self.namespace) {
throw new Error(
`Volume reference "${vol}" must be in the same namespace as the container "${self}"`
)
}
}
const envFroms = (self.props.$envFrom ?? []).map(ef => {
const source = ef.source as any as Resource_Entity
if (source.namespace !== self.namespace) {
throw new Error(
`EnvFrom source reference "${source}" must be in the same namespace as the container "${self}"`
)
}
if (source.is(v1.Secret._)) {
return {
secretRef: {
optional: ef.optional,
name: source.name
}
} as CDK.EnvFromSource
} else if (source.is(v1.ConfigMap._)) {
return {
configMapRef: {
optional: ef.optional,
name: source.name
}
} as CDK.EnvFromSource
} else {
throw new Error(
`EnvFrom source reference "${source}" must be a ConfigMap or Secret, but was ${source.kind}`
)
}
})
const container: CDK.Container = {
...untaggedProps,
name: self.name,
image: $image.toString(),
ports: containerPorts,
resources: resourcesObject,
command: $command?.toArray(),
env: Env($env).toEnvVars(),
envFrom: envFroms,
...self._groupedMounts()
}
return container
}
constructor(
parent: Resource_Entity,
name: string,
readonly subtype: "init" | "main",
override readonly props: Container_Props<Ports>
) {
super(parent, name, props)
}
private _groupedMounts() {
const x = {
volumeMounts: [],
volumeDevices: []
} as Pick<CDK.Container, "volumeMounts" | "volumeDevices">
for (const mnt of this.mounts) {
const { mount, path } = mnt
if (mount instanceof Container_Mount_Device) {
x.volumeDevices!.push(mount["__submanifest__"](path))
} else {
x.volumeMounts!.push(mount["__submanifest__"](path))
}
}
return x
}
private _resources() {
if (!this.props.$resources) {
return undefined
}
const result = container_ResourcesSpec.parse(this.props.$resources)
return result
}
}
export function make<Ports extends string>(
parent: Resource_Top,
name: string,
subtype: "init" | "main",
props: Container_Props<Ports>
) {
return new Container(parent, name, subtype, props)
}