diff --git a/.github/actions/setup-www/action.yaml b/.github/actions/setup-www/action.yaml index d7138b70..67c495be 100644 --- a/.github/actions/setup-www/action.yaml +++ b/.github/actions/setup-www/action.yaml @@ -70,15 +70,15 @@ runs: if [[ $(unikraft services list --filter "name==${{ inputs.name }},metro==${{ inputs.metro }}" -o raw | jq 'length') -ne 1 ]]; then if [[ "${{ inputs.domain }}" == "" ]]; then unikraft services create --metro "${{ inputs.metro }}" \ - --domains "${{ inputs.name }}" \ + --domain "${{ inputs.name }}" \ --name "${{ inputs.name }}" \ - --services "443:${{ inputs.port }}/http+tls" \ + --service "443:${{ inputs.port }}/http+tls" \ ; else unikraft services create --metro "${{ inputs.metro }}" \ - --domains "${{ inputs.domain }}" \ + --domain "${{ inputs.domain }}" \ --name "${{ inputs.name }}" \ - --services "443:${{ inputs.port }}/http+tls" \ + --service "443:${{ inputs.port }}/http+tls" \ ; fi fi diff --git a/.vale/styles/config/vocabularies/Unikraft/accept.txt b/.vale/styles/config/vocabularies/Unikraft/accept.txt index 52c7c3e1..88e61939 100644 --- a/.vale/styles/config/vocabularies/Unikraft/accept.txt +++ b/.vale/styles/config/vocabularies/Unikraft/accept.txt @@ -24,6 +24,7 @@ FUSE (?i)webservers? ALIAS ANAME +ARM ASCII BYOC CNAME @@ -41,6 +42,7 @@ OCI OLAP OOM PORT +QEMU REST RUN SNI @@ -71,6 +73,9 @@ CSI CNI VPC XFS +MAC +IPAM +BPF # ============================================================================== # NAMES OF THINGS diff --git a/Makefile b/Makefile index dc77fcd7..53fdb384 100644 --- a/Makefile +++ b/Makefile @@ -56,7 +56,7 @@ sync: readme="$$example/README.md"; \ if [ -f "$$readme" ]; then \ # TODO: remove - Only transform updated READMEs \ - if grep -q "# Set metro to Frankfurt, DE" "$$readme" ; then \ + if grep -q "Make sure to log into Unikraft Cloud" "$$readme" ; then \ guide="$(GUIDES_DIR)/$$name.mdx" ;\ echo " 📄 $$name -> $$(basename $$guide)" ;\ $(WORKDIR)/scripts/transform_readme.py "$$readme" "$$guide" "$$name" ;\ diff --git a/pages/cli/fields.mdx b/pages/cli/fields.mdx index c4a1e12c..9ca6d704 100644 --- a/pages/cli/fields.mdx +++ b/pages/cli/fields.mdx @@ -268,6 +268,23 @@ The `--add` flag works the same way, appending the given value instead of removi You can combine `--set`, `--add`, and `--del` flags in a single `edit` invocation. +### List and map fields + +Each flag carries exactly one element, so a field holding a list or a map takes one flag per entry. +The CLI never splits a value on commas—a comma inside a value is part of that value. + +```bash +unikraft instances edit httpserver-rust175-tokio \ + --set tags=production \ + --set tags=customer_A +``` + +To pass a whole list or map in a single flag, write it as JSON instead. + +```bash +unikraft instances edit httpserver-rust175-tokio --set tags='["production","customer_A"]' +``` + ## Visual editing Pass `--visual` to open the resource in your editor and edit its fields interactively. diff --git a/pages/cli/registries.mdx b/pages/cli/registries.mdx index 8ad532dd..3c9c0fdb 100644 --- a/pages/cli/registries.mdx +++ b/pages/cli/registries.mdx @@ -202,7 +202,7 @@ kraft cloud image rm /[:] ## Kernel layer deduplication -An image has two OCI layers: a kernel layer and a rootfs layer. +An image typically has two OCI layers: a kernel layer and a rootfs layer. When you push an update to an existing image, the CLI checks whether the kernel layer is already present in the registry and skips re-uploading it. Only the rootfs layer travels over the wire. This makes iterative pushes faster and cheaper on bandwidth. @@ -210,6 +210,7 @@ This makes iterative pushes faster and cheaper on bandwidth. [ROMs](/features/roms) follow the same packaging system but go out without a kernel layer. They only have a rootfs. They appear in the image list alongside regular images. +Images that boot on the [platform kernel](/platform/images#platform-kernel) carry a rootfs layer alone in the same way. ## Instance creation diff --git a/pages/faq.mdx b/pages/faq.mdx index e2b0d07c..4c9abba5 100644 --- a/pages/faq.mdx +++ b/pages/faq.mdx @@ -88,6 +88,9 @@ spec: v0.7 runtime: python:3.12 +targets: + - kraftcloud/x86_64 + rootfs: source: ./Dockerfile format: erofs @@ -98,6 +101,11 @@ cmd: ["/usr/bin/python3", "/src/server.py"] The `runtime` specifies one of the Unikraft Cloud runtimes (microVMs) built to run different languages and apps. Here it specifies a Python runtime. +The `targets` parameter specifies the platform and architecture pairs to build for. +Here it targets `kraftcloud` (Unikraft Cloud) on `x86_64`, which is the architecture the hosted platform runs on. +No architecture is the default, so list every one you need, for example `kraftcloud/arm64` alongside `kraftcloud/x86_64`. +See [architectures](/platform/images#architectures) for more detail. + The `rootfs` parameter tells the CLI to use a `Dockerfile` in the same directory to build the root filesystem as an [EROFS image](/tutorials/rootfs-formats). The `cmd` parameter tells the platform which command to run when deployed. @@ -212,6 +220,23 @@ The CLI can run on Mac and Linux out of the box. Windows isn't officially supported, but you can use [WSL](https://learn.microsoft.com/en-us/windows/wsl/install) to run the CLI on Windows. +{/* vale off */} +#### What CPU architectures can you deploy to? +{/* vale on */} + +Both `x86_64` and `arm64`, and the hosted platform runs on `x86_64`. +The architecture of the guest image must match the host architecture, so name the architectures you deploy to in the `targets` key of your `Kraftfile` or through the `--arch` flag. +Neither architecture is the default, and a single `unikraft build` invocation produces an image for every architecture you ask for. +See [architectures](/platform/images#architectures) for more detail. + +{/* vale off */} +:::caution[**Limited Access**] +`arm64` hosts are available as part of enterprise plans. +To deploy to them, reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). +::: +{/* vale on */} + + #### How can you track instances on Unikraft Cloud? Unikraft provides full `stdout` and `stderr` log support for all users. @@ -296,13 +321,15 @@ You should first experiment with this or discuss it with a Unikraft Engineer to #### What can you push as an image? -An image is specifically a pair of a kernel and a rootfs bundled together as different layers of an OCI package. +An image typically bundles a kernel and a rootfs together as different layers of an OCI package. To keep things light, certain optimizations are in place to not repush existing kernels and to reuse them from your account. If these aren't automatically detected, the CLI pushes the kernel again. At the same time, things like [ROMs](/features/roms) use the same packaging systems, but push images with a missing kernel and only a rootfs. These have specific metadata that identifies them as ROMs. +An image can also ship a rootfs alone and boot on the kernel installed on the node instead of one of its own, which the [platform kernel](/platform/images#platform-kernel) section covers. + Finally, kernels can also run without a rootfs. They don't do anything, but support exists to run them. diff --git a/pages/features/annotations.mdx b/pages/features/annotations.mdx index 40b18b9c..f8974ad0 100644 --- a/pages/features/annotations.mdx +++ b/pages/features/annotations.mdx @@ -6,7 +6,7 @@ navigation_icon: sticky-note {/* vale off */} :::caution[**Limited Access**] Instance annotations are available as part of enterprise plans. -To enable them for your account, reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). +To try them out, reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: {/* vale on */} @@ -14,11 +14,12 @@ To enable them for your account, reach out to the [Unikraft Cloud Discord](https They use the same key syntax as [Kubernetes annotations](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/), so you can reuse your existing keys and tooling. Unlike [tags](/platform/tagging), which act as controller-level labels that only the platform sees, annotations also reach the guest. -You can use them for two things: +You can use them for three things: {/* vale off */} 1. **Custom metadata**—the instance's startdata includes its annotations, so the guest can read them at runtime. -2. **Structured log output**—you select which annotations the platform injects into the VM's console log output. +2. **Guest network configuration**—the guest applies the [CNI result](#guest-network-configuration-with-cni) in the `unikraft.com/cni` annotation to its network interfaces. +3. **Structured log output**—you select which annotations the platform injects into the VM's console log output. {/* vale on */} {/* vale off */} @@ -30,7 +31,9 @@ In the meantime, drive annotations through the [API](/api/platform/v1/instances) ## Annotation keys -An annotation is a set of key-value pairs, and values can be any string. +An annotation is a set of key-value pairs. +A value is any string without ASCII control characters (`0x00`–`0x1f` and `0x7f`, which covers tab, newline and carriage return), and multi-byte UTF-8 goes through fine. +The API rejects a value carrying an ASCII control character with `400 Bad Request`. Keys follow the [Kubernetes annotation key syntax](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/#syntax-and-character-set): - A key can be a plain name (`my-key`) or carry an optional DNS prefix separated by a slash (`example.com/annotation2`). @@ -58,7 +61,7 @@ Each entry is a key-value pair: ```bash title="unikraft" unikraft api /v1/instances -X POST --metro fra \ - -d '{ + '{ "name": "annotations-demo", "image": "/annotations-demo:latest", "annotations": { @@ -98,6 +101,88 @@ cat /sys/class/uio/uio0/device/startdata Your app parses this JSON and reads the `annotations` object like any other startdata field. +## Guest network configuration with CNI + +`unikraft.com/cni` is a reserved annotation. +Its value is a [CNI Result](https://www.cni.dev/docs/spec/#section-5-result-types) document, and the guest applies the addresses it holds to its own network interfaces. + +This hands the addressing of an instance to an external [CNI](https://www.cni.dev/) plugin or IPAM system. +The plugin decides the addresses, writes its result into the annotation, and the guest configures itself from it. +Your image needs no agent for it and no boot script of its own. + +The annotation pairs with an [interface without an address](/features/custom-network-configuration#interfaces-without-an-address), which reaches the guest as a bare link on a TAP device of your own. +See [Kubernetes](/integrations/kubernetes#custom-networking-with-cni) for the CNI integration that Kraftlet drives on the cluster side. + +The annotation value is the document as a JSON string, so the JSON inside it carries escapes: + + + +```bash title="unikraft" +unikraft api /v1/instances -X POST --metro fra \ + '{ + "name": "cni-demo", + "image": "/cni-demo:latest", + "network_interfaces": [ + {}, + { + "name": "cni-demo-vpc", + "tap_name": "vpc0", + "mac": "0a:58:c0:a8:01:05", + "autoconfig": false + } + ], + "annotations": { + "unikraft.com/cni": "{\"cniVersion\":\"1.0.0\",\"interfaces\":[{\"name\":\"eth0\",\"mac\":\"0a:58:c0:a8:01:05\"}],\"ips\":[{\"interface\":0,\"address\":\"192.168.1.5/24\"}]}" + } + }' +``` + + + +That instance keeps a pool-allocated primary interface and adds a second one on the `vpc0` TAP device, with a MAC address you pick and no IP address. +Unescaped, the annotation holds a standard CNI Result (`types/100`): + +```json +{ + "cniVersion": "1.0.0", + "interfaces": [ + { "name": "eth0", "mac": "0a:58:c0:a8:01:05" } + ], + "ips": [ + { "interface": 0, "address": "192.168.1.5/24" } + ] +} +``` + +The guest gives `192.168.1.5/24` to the link whose MAC address is `0a:58:c0:a8:01:05`, and brings that link up. + +### Writing the document + +Give every address a MAC address to land on: + +- Each `ips[]` entry points through its `interface` index at an entry of the document's own `interfaces[]` array, not at the instance's `network_interfaces`. + An entry with no `interface` field uses index `0`. +- That `interfaces[]` entry supplies the MAC address of the interface to configure, so `interfaces[].name` can say anything. + Without a MAC address to match, the index counts the instance's non-loopback interfaces from `0` instead. + The entry carries no `mac`, the document holds no `interfaces[]` array, or the index sits past the end of that array. +- Set the MAC address yourself when you create the instance, or read it from the [instance status](/api/platform/v1/instances#get-instance-by-uuid). +- Addresses take CIDR notation, IPv4 or IPv6, and the prefix length is mandatory. +- The document needs a `cniVersion` of `0.3.1`, `0.4.0`, `1.0.0`, or `1.1.0`, and has to be a result rather than a network configuration. +- Keep the document on one line, because annotation values reject control characters. + +Only `interfaces[].mac`, `ips[].interface`, and `ips[].address` have any effect. +`routes`, `gateway`, `dns`, `mtu`, and `sandbox` do nothing, so you can pass a plugin's full result through untouched. +Set the default gateway and the resolver with the instance's own `gateway` and `nameserver` fields. + +### Changing addresses + +Running instances pick up updates to their annotations, and reconcile them against what's currently configured, so an address change doesn't require a restart. + +### When nothing happens + +A single MAC address that matches no interface stops the whole document from applying, leaving every interface untouched. +Check that each MAC address in the document matches one on the instance, then read the [instance logs](/platform/instances#instance-logs), which name any document that fails to parse or apply. + ## Patching annotations Update the annotations on an existing instance with a [`PATCH /instances`](/api/platform/v1/instances) request. @@ -116,7 +201,7 @@ If you patch a running instance, the platform accepts the request and queues the ```bash title="unikraft" unikraft api /v1/instances -X PATCH --metro fra \ - -d '[{ + '[{ "name": "annotations-demo", "prop": "annotations", "op": "set", @@ -138,7 +223,7 @@ It overwrites keys that already exist and leaves the other keys untouched: ```bash title="unikraft" unikraft api /v1/instances -X PATCH --metro fra \ - -d '[{ + '[{ "name": "annotations-demo", "prop": "annotations", "op": "add", @@ -159,7 +244,7 @@ The value is an array of key names (or a single key as a string): ```bash title="unikraft" unikraft api /v1/instances -X PATCH --metro fra \ - -d '[{ + '[{ "name": "annotations-demo", "prop": "annotations", "op": "del", @@ -204,16 +289,39 @@ When the guest writes to the app console (for example, `echo "blub" > /dev/vport } ``` +## Annotation events + +The platform's event log carries a `vm.annotate` event, which it emits whenever an instance's annotations change: + +```json +{ + "type": "vm.annotate", + "timestamp": "2026-08-19T10:04:11Z", + "data": { + "vm": "b495f451-0370-4d8e-8b94-a5b22d2212f9", + "annotations": { "env": "prod", "team": "search" } + } +} +``` + +An external system consumes the event to keep track of the instance's annotation values, without polling the instance API. +The event also gives log processing a way to map an instance UUID back to its annotations. + ## Limitations - An instance holds at most 256 annotations. -- Annotation keys must follow the [Kubernetes key syntax](#annotation-keys), and values are strings. +- Annotation keys must follow the [Kubernetes key syntax](#annotation-keys), and values are strings with no ASCII control characters in them. - A [patched](#patching-annotations) annotation change applies once the instance reaches the `stopped` state. +- A [`unikraft.com/cni`](#guest-network-configuration-with-cni) document holds at most eight interfaces and 16 addresses, and an `ips[].interface` index stays below eight. +- The guest applies only `interfaces[].mac`, `ips[].interface` and `ips[].address` from that document, and needs a recent base-compat guest kernel to apply it at all. - Dedicated `unikraft` CLI subcommands aren't available yet, so drive annotations through the [API](/api/platform/v1/instances). - The platform sets up [log-output](#annotations-in-log-output) forwarding at the node level, not through the instances API. +- The [`vm.annotate` event](#annotation-events) needs switching on in the node's event configuration. ## Learn more * [Tags](/platform/tagging): controller-level labels for organizing platform resources. +* [Custom network configuration](/features/custom-network-configuration): bring your own interfaces, MAC addresses and TAP devices, including [interfaces without an address](/features/custom-network-configuration#interfaces-without-an-address) for a CNI plugin to fill in. +* [Kubernetes](/integrations/kubernetes#custom-networking-with-cni): how Kraftlet runs the CNI plugins of your cluster for Unikraft Cloud instances. * [Instances](/platform/instances): create and manage instances. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). diff --git a/pages/features/autokill.mdx b/pages/features/autokill.mdx index 27551ec9..90a3d674 100644 --- a/pages/features/autokill.mdx +++ b/pages/features/autokill.mdx @@ -78,7 +78,7 @@ curl -X POST \ ```bash title="unikraft" unikraft api /v1/instances -X PATCH --metro fra \ - -d '[{ + '[{ "name": "", "prop": "autokill", "op": "set", diff --git a/pages/features/autoscale.mdx b/pages/features/autoscale.mdx index 79653bf2..328e8f93 100644 --- a/pages/features/autoscale.mdx +++ b/pages/features/autoscale.mdx @@ -104,8 +104,8 @@ Create a new persistent [service](/platform/services) for autoscale to manage: ```bash title="unikraft" unikraft service create --metro fra \ --name nginx-autoscale-service \ - --domains nginx-autoscale \ - --services 443:8080/http+tls + --domain nginx-autoscale \ + --service 443:8080/http+tls ``` The command prints the new service's details, including its `uuid` (for example, `996d006f-a491-44ae-a1dc-7a6186971351`). @@ -125,7 +125,7 @@ Achieve this with the following commands: ```bash title="unikraft" unikraft api /v1/services//autoscale -X POST --metro fra \ - -d '{ + '{ "min_size": 1, "max_size": 8, "warmup_time_ms": 1000, @@ -136,7 +136,7 @@ unikraft api /v1/services//autoscale -X POST --metro fra \ }' unikraft api /v1/services//autoscale/policies -X POST --metro fra \ - -d '{ + '{ "name": "scale-out-policy", "type": "step", "metric": "cpu", @@ -148,7 +148,7 @@ unikraft api /v1/services//autoscale/policies -X POST --metro fra }' unikraft api /v1/services//autoscale/policies -X POST --metro fra \ - -d '{ + '{ "name": "scale-in-policy", "type": "step", "metric": "cpu", @@ -176,6 +176,23 @@ Refer to the API autoscale [reference](/api/platform/v1/autoscale) for more deta Keep in mind that a few restrictions apply to how you define scale-in/scale-out steps. ::: +### High watermark + +{/* vale off */} +:::caution[**Limited Access**] +The high watermark signal is available to enterprise customers. +If you would like to try it out now, please reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). +::: +{/* vale on */} + +An autoscale group can be configured with a high watermark - as an early warning that it's getting close to its maximum size. +Set it through the `high_watermark` field, as a fraction of `max_size` between `0` and `1`. +The default is `0`, which turns the signal off. + +Once a service crosses its watermark, the platform can report this on the responses it sends back. +A client that spreads load across metros uses this to steer new work elsewhere before the service fills up. +Your metro operator switches that response header on. + To check it's working, list the autoscale properties of the service: @@ -314,7 +331,7 @@ You should order steps by lower bound with no gaps between them and no overlaps: ```bash title="unikraft" unikraft api /v1/services//autoscale/policies -X POST --metro fra \ - -d '{ + '{ "name": "my-step-policy", "type": "step", "metric": "cpu", @@ -354,7 +371,7 @@ This prevents request queuing but introduces cold start delays. ```bash title="unikraft" unikraft api /v1/services//autoscale -X POST --metro fra \ - -d '{ + '{ "policies": [ { "name": "scale-out", @@ -392,7 +409,7 @@ Setting `replace` to true deletes the original VM after the new one starts. ```bash title="unikraft" unikraft api /v1/services//autoscale -X POST --metro fra \ - -d '{ + '{ "policies": [ { "name": "create", @@ -434,7 +451,7 @@ The `idle` policy scales in (removes instances) when the service has been idle ```bash title="unikraft" unikraft api /v1/services//autoscale -X POST --metro fra \ - -d '{ + '{ "policies": [ { "name": "scale-in", diff --git a/pages/features/branching.mdx b/pages/features/branching.mdx index 1007fb77..cf1d5391 100644 --- a/pages/features/branching.mdx +++ b/pages/features/branching.mdx @@ -8,11 +8,6 @@ navigation_icon: git-branch Branching is a new feature which is available to enterprise customers, and is coming soon to the hosted platform. If you would like to try it out now, please reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: - -:::caution -Dedicated `unikraft` CLI subcommands for branching are coming soon. -In the meantime, configure branching through the [API](/api/platform/v1/instances#create-instance), which you can invoke with `curl` or the [`unikraft api`](/cli/unikraft/api) command. -::: {/* vale on */} Branching lets you create a clone (a **branch**) of an existing instance, including its memory and volume state. @@ -37,45 +32,44 @@ The source instance's [state](/platform/instances#instance-states) determines ho ## Creating a branch -To create a branch, send a [`POST /instances`](/api/platform/v1/instances#create-instance) request with a `branch_from` field that names the source instance: +To create a branch, pass the source instance to `--branch`: ```bash title="unikraft" -unikraft api /v1/instances \ - -d '{ - "name": "my-branch", - "branch_from": { - "name": "my-instance" - } - }' +unikraft instance create --metro fra \ + --name my-branch \ + --branch my-instance ``` You can also reference the source by its UUID instead of its name. +Use `unikraft run --branch` instead of `unikraft instance create --branch` to create the branch and follow its logs in one step. -Apart from `branch_from`, the request accepts the same fields as a regular instance creation. +Apart from the branch source, a branch accepts the same options as a regular instance creation. You can attach [ROMs](/features/roms) and [volumes](/platform/volumes), configure a [service](/platform/services), and set other options as when you create an instance from a template. ## What the branch inherits A branch derives the following from its source, so you **can't** set these fields in the create request: -- The image -- The number of vCPUs (`vcpus`) -- The memory size (`memory_mb`) -- The app arguments (`args`) -- The environment variables (`env`) +- The image (`--image`) +- The number of vCPUs (`--vcpus`) +- The memory size (`--memory`) +- The app arguments (`--args`) +- The environment variables (`--env`) - The memory and volume state at the time of the snapshot -Setting any of these alongside `branch_from` makes the request fail (see [Error handling](#error-handling)). +Setting any of these alongside a branch source makes the request fail (see [Error handling](#error-handling)). You can still give the branch its own configuration, as when instantiating a template, including a `name`, [ROMs](/features/roms), [volumes](/platform/volumes), and a [service](/platform/services). ## Error handling -A `branch_from` request can fail for the following reasons: +The CLI rejects `--branch` alongside `--image`, `--template`, or `--checkpoint` before it sends anything, with `only one of --image, --template, --branch, or --checkpoint may be specified`. +It also rejects a source in a different metro from the new instance, with `cannot create instance: metro mismatch between branch ("fra") and instance ("dal2")`. +Otherwise, a branch request can fail for the following reasons: | Error message | Cause | |---------------|-------| @@ -88,6 +82,10 @@ A `branch_from` request can fail for the following reasons: ## Limitations +- A branch must stay in the metro of its source. + The CLI catches a cross-metro source before it sends the request. +- Branching needs the default `micro` instance type. + QEMU-backed full VMs (`type: full`), which [GPU instances](/platform/instances#gpus) require, don't support branching, and a create request can't combine `type` with a branch source. - Branching only works with block-based volumes (for example, `ext4`). The branch clones the source's volume state consistently with its memory snapshot, which isn't supported for other volume types. - Branching requires a license that includes the feature (see the note at the top of this page). diff --git a/pages/features/checkpoints.mdx b/pages/features/checkpoints.mdx index 9d81cb40..c7d99abe 100644 --- a/pages/features/checkpoints.mdx +++ b/pages/features/checkpoints.mdx @@ -8,11 +8,6 @@ navigation_icon: save Checkpointing is a new feature which is available to enterprise customers, and is coming soon to the hosted platform. If you would like to try it out now, please reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). ::: - -:::caution -Dedicated `unikraft` CLI subcommands for checkpointing are coming soon. -In the meantime, configure checkpointing through the [API](/api/platform/v1/instances/checkpoints), which you can invoke with `curl` or the [`unikraft api`](/cli/unikraft/api) command. -::: {/* vale on */} A **checkpoint** captures the full state of an instance—its memory and volume state—at any moment, so you can later start new instances that resume from exactly that state. @@ -37,13 +32,38 @@ You can only [load](#loading-a-checkpoint) a checkpoint once it reaches the `che ## Creating a checkpoint -To create a checkpoint, send a [`POST /instances/checkpoints`](/api/platform/v1/instances) request with a `from` field that names the source instance: +To create a checkpoint, pass the source instance to `unikraft instance checkpoint create`: + + + +```bash title="unikraft" +unikraft instance checkpoint create my-instance +``` + + + +The CLI doesn't name the checkpoint, so the platform derives a name from the source instance's name plus a random suffix. + +Checkpoint creation is asynchronous, so the new checkpoint reports `starting` until its snapshot completes. +Either wait for it afterward, or have the create call wait for you: ```bash title="unikraft" +# wait for an existing checkpoint to become ready +unikraft instance checkpoint wait my-checkpoint --until state==checkpoint + +# or wait during creation, for up to 60 seconds +unikraft instance checkpoint create my-instance --set wait-timeout=60s +``` + + + +To choose the name yourself, or to give the checkpoint an [autokill](#autokill) policy, call [`POST /instances/checkpoints`](/api/platform/v1/instances) directly—the CLI covers neither: + +```bash title="POST /instances/checkpoints" unikraft api /v1/instances/checkpoints \ - -d '{ + '{ "name": "my-checkpoint", "from": { "name": "my-instance" @@ -51,38 +71,7 @@ unikraft api /v1/instances/checkpoints \ }' ``` - - -The request accepts the following fields: - -| Field | Required | Description | -|-------|----------|-------------| -| `from` | Yes | The source instance to checkpoint, referenced by `name` or `uuid`. | -| `name` | No | The checkpoint name. If omitted, Unikraft Cloud derives it from the source instance's name and appends a random suffix. | -| `autokill` | No | An autokill policy with a `time_ms` field that removes the checkpoint automatically when nothing loads it for the configured time. See [Autokill](#autokill). | -| `timeout_s` | No | Wait up to this many seconds for the checkpoint to become ready. By default the call returns immediately while the checkpoint is still in the `starting` state. | - -The response reports the new checkpoint and its current state: - -```json title="response" -{ - "status": "success", - "data": { - "instances": [ - { - "status": "success", - "uuid": "c8186a7d-48e4-4663-ac13-a52b9502d8c7", - "name": "my-checkpoint", - "state": "starting" - } - ] - }, - "op_time_us": 1234 -} -``` - -Because checkpoint creation is asynchronous, the checkpoint reports `starting` until its snapshot completes. -Either poll its [status](#checkpoint-information) until it reaches the `checkpoint` state, or pass a `timeout_s` to have the request wait for you. +To take checkpoints on a recurring schedule rather than on demand, give the instance a `checkpoint` [scheduled operation](/features/cron-jobs#scheduled-checkpoints). ## Checkpoint history @@ -98,51 +87,29 @@ For example, given this sequence: The history of `inst2` lists both `chk2` (taken directly from `inst2`) and `chk1` (inherited from the checkpoint `inst2` was loaded from). -To query this history, call the instance history endpoint: +To query this history, run `unikraft instance history`: ```bash title="unikraft" -unikraft api "/v1/instances/history?name=my-instance" +unikraft instance history my-instance ``` -```json title="response" -{ - "status": "success", - "data": { - "instances": [ - { - "status": "success", - "uuid": "c014c3ff-79e9-4cc0-b074-4c9b40822163", - "name": "my-instance", - "history": [ - { - "uuid": "1ea900f7-b6a7-4162-b848-f4fa0309fb96", - "name": "my-other-checkpoint", - "created_at": "2026-06-19T11:40:05Z" - }, - { - "uuid": "c8186a7d-48e4-4663-ac13-a52b9502d8c7", - "name": "my-checkpoint", - "created_at": "2026-06-19T11:38:22Z" - } - ] - } - ] - }, - "op_time_us": 113 -} +```text title="output" +METRO TARGET NAME CREATED +fra fra/my-instance my-other-checkpoint 2 minutes ago +fra fra/my-instance my-checkpoint 4 minutes ago ``` The checkpoints themselves also carry this history of their lineage. -You can query it directly through the checkpoint history endpoint, which returns the same shape: +You can query it directly with `unikraft instance checkpoint history`, which returns the same shape: ```bash title="unikraft" -unikraft api "/v1/instances/checkpoints/history?name=my-checkpoint" +unikraft instance checkpoint history my-checkpoint ``` @@ -154,40 +121,43 @@ To retrieve detailed information about a checkpoint, including its instance conf ```bash title="unikraft" -unikraft api "/v1/instances/checkpoints?name=my-checkpoint" +unikraft instance checkpoint get my-checkpoint ``` -As with all status-like endpoints, you can list all available checkpoints by omitting the identifier: +List all available checkpoints by omitting the identifier: ```bash title="unikraft" -unikraft api /v1/instances/checkpoints +unikraft instance checkpoint list ``` +```text title="output" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED +fra my-checkpoint checkpoint nginx 256MiB 2 4 minutes ago +``` + ## Loading a checkpoint You load a checkpoint by creating a new instance from it. -Send a [`POST /instances`](/api/platform/v1/instances#create-instance) request with a `checkpoint` field that names the checkpoint: +Pass the checkpoint to `--checkpoint`: ```bash title="unikraft" -unikraft api /v1/instances \ - -d '{ - "name": "my-new-instance", - "checkpoint": { - "name": "my-checkpoint" - } - }' +unikraft instance create --metro fra \ + --name my-new-instance \ + --checkpoint my-checkpoint ``` +Use `unikraft run --checkpoint` instead to load the checkpoint and follow the new instance's logs in one step. + The new instance inherits the image, vCPUs, memory, arguments, environment, and saved memory and volume state from the checkpoint. As with [branching](/features/branching) and [instance templates](/platform/instances#instance-templates), you can configure the remaining properties—the instance `name`, [volumes](/platform/volumes), [ROMs](/features/roms), and [services](/platform/services). @@ -199,13 +169,11 @@ Loading a checkpoint that's still in the `starting` state fails (see [Error hand Like instance templates, checkpoints persist on the machine once created, holding onto the storage their snapshot occupies. A checkpoint can carry an [autokill](/features/autokill) policy that removes it automatically when nothing loads it for a configured time, measured from the last load. -Set the autokill policy (with the time in milliseconds) when you create the checkpoint: +The `unikraft` CLI doesn't expose checkpoint autokill yet, so set the policy (with the time in milliseconds) through the API when you create the checkpoint: - - -```bash title="unikraft" +```bash title="POST /instances/checkpoints" unikraft api /v1/instances/checkpoints \ - -d '{ + '{ "name": "my-checkpoint", "from": { "name": "my-instance" @@ -216,15 +184,11 @@ unikraft api /v1/instances/checkpoints \ }' ``` - - -You can also set or update the policy later with a [patch](#patching): - - +You can also set or update the policy on an existing checkpoint: -```bash title="unikraft" +```bash title="PATCH /instances/checkpoints" unikraft api /v1/instances/checkpoints -X PATCH \ - -d '[{ + '[{ "name": "my-checkpoint", "prop": "autokill", "op": "set", @@ -234,39 +198,28 @@ unikraft api /v1/instances/checkpoints -X PATCH \ }]' ``` - - This example removes the checkpoint after 1 hour without a load. -The configured policy appears as a `checkpoint_autokill` object in the checkpoint's [status](#checkpoint-information). +The configured policy appears as a `checkpoint_autokill` object in the API status response. +`unikraft instance checkpoint get` doesn't report it. -## Patching +## Editing a checkpoint -Use the patch endpoint to update a checkpoint's tags, delete lock, and autokill policy. -The request body is an array of patch operations, each naming the target checkpoint and the property to change: +Use `unikraft instance checkpoint edit` to update a checkpoint's tags and delete lock: ```bash title="unikraft" -unikraft api /v1/instances/checkpoints -X PATCH \ - -d '[{ - "name": "my-checkpoint", - "prop": "tags", - "op": "add", - "value": [ - "my-new-tag" - ] - }]' -``` +# add a tag, keeping the existing ones +unikraft instance checkpoint edit my-checkpoint --add tags=my-new-tag - +# replace the tags outright +unikraft instance checkpoint edit my-checkpoint --tag my-only-tag -The endpoint supports the following properties: +# protect the checkpoint from deletion +unikraft instance checkpoint edit my-checkpoint --delete-lock +``` -| Property | Operations | Value | -|----------|------------|-------| -| `tags` | `add`, `del`, `set` | An array of tag strings. | -| `delete_lock` | `set` | A boolean enabling or disabling [deletion protection](/platform/delete-locks). | -| `autokill` | `set` | An object with a `time_ms` field (see [Autokill](#autokill)). | + ## Deleting a checkpoint @@ -275,13 +228,17 @@ Delete a checkpoint by name or UUID: ```bash title="unikraft" -unikraft api "/v1/instances/checkpoints?name=my-checkpoint" -X DELETE +unikraft instance checkpoint delete my-checkpoint ``` :::note -If a checkpoint has a [delete lock](/platform/delete-locks) set, the delete request fails until you remove the lock with a [patch](#patching). +If a checkpoint has a [delete lock](/platform/delete-locks) set, the delete request fails until you remove the lock: + +```bash +unikraft instance checkpoint edit my-checkpoint --set delete-lock=false +``` ::: Deleting the instance that owns a set of checkpoints doesn't remove the checkpoints. @@ -289,6 +246,9 @@ They remain available to load until you delete them explicitly or their [autokil ## Error handling +The CLI rejects a checkpoint in a different metro from the new instance before it sends anything, with `cannot create instance: metro mismatch between checkpoint ("fra") and instance ("dal2")`. +Otherwise, a checkpoint request can fail for the following reasons: + | Error message | Cause | |---------------|-------| | `Insufficient license. Please make sure your license is valid and includes checkpointing` | Your account's license doesn't include the checkpointing feature. | @@ -299,6 +259,10 @@ They remain available to load until you delete them explicitly or their [autokil ## Limitations +- A checkpoint and the instances you load from it must stay in the same metro. + The CLI catches a cross-metro checkpoint before it sends the request. +- Checkpointing needs the default `micro` instance type. + QEMU-backed full VMs (`type: full`), which [GPU instances](/platform/instances#gpus) require, don't support checkpointing, and a create request can't combine `type` with a checkpoint source. - Checkpointing builds on [branching](/features/branching), so it only works with block-based volumes (for example, `ext4`). The checkpoint clones the source's volume state consistently with its memory snapshot, which isn't supported for other volume types. - Checkpointing requires a license that includes the feature (see the note at the top of this page). @@ -310,4 +274,5 @@ They remain available to load until you delete them explicitly or their [autokil * [Autokill](/features/autokill): automatically removing checkpoints and templates the platform hasn't loaded recently. * [Snapshots](/features/snapshots): the copy-on-write snapshotting that underpins checkpoints. * [Serverless databases](/use-cases/serverless-databases): a use case that checkpoints a running PostgreSQL instance to capture and restore its state. +* [Cron jobs / scheduled wake-ups](/features/cron-jobs#scheduled-checkpoints): the `checkpoint` action periodically creates restore points for an instance. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). diff --git a/pages/features/cron-jobs.mdx b/pages/features/cron-jobs.mdx index 34e3b0c2..5d4b8a51 100644 --- a/pages/features/cron-jobs.mdx +++ b/pages/features/cron-jobs.mdx @@ -3,8 +3,8 @@ title: Cron Jobs / Scheduled Wake-ups navigation_icon: clock --- -Scheduled operations let you automatically start, stop, delete, or exec a command in instances on a calendar-based schedule. -Each scheduled operation specifies a name, a calendar expression, and an action (`start`, `stop`, `delete`, or `exec`). +Scheduled operations let you automatically start, stop, delete, checkpoint, or exec a command in instances on a calendar-based schedule. +Each scheduled operation specifies a name, a calendar expression, and an action (`start`, `stop`, `delete`, `exec`, or `checkpoint`). :::info When you clone an instance from a [template](/platform/instances#instance-templates), the clone inherits its scheduled operations. @@ -20,7 +20,7 @@ Unikraft Cloud uses systemd calendar events (see [`systemd.time(7)`](https://www Calendar expressions have the following fields: ```text title="" -[weekday] [[year-]month-day] [hour:minute[:second]] +[WEEKDAY] [[YEAR-]MONTH-DAY] [HOUR:MINUTE[:SECOND]] ``` The expression syntax supports ranges, steps, and comma-separated lists: @@ -41,7 +41,7 @@ For example, to start an instance every day at 09:00 UTC and stop it at 18:00 UT ```bash title="unikraft" unikraft api /v1/instances -X POST --metro fra \ - -d '{ + '{ "image": "...", "schedules": [ { @@ -85,7 +85,7 @@ To run a command inside the instance every night at midnight, use the `exec` act ```bash title="unikraft" unikraft api /v1/instances -X POST --metro fra \ - -d '{ + '{ "image": "...", "schedules": [ { @@ -123,7 +123,7 @@ Add, set, or remove scheduled operations via [PATCH](/api/platform/v1/instances# ```bash title="unikraft" unikraft api /v1/instances/ -X PATCH --metro fra \ - -d '{ + '{ "prop": "schedules", "op": "add", "value": [ @@ -159,7 +159,7 @@ Add an `exec` schedule the same way, but include `args` with the command to run: ```bash title="unikraft" unikraft api /v1/instances/ -X PATCH --metro fra \ - -d '{ + '{ "prop": "schedules", "op": "add", "value": [ @@ -197,7 +197,7 @@ curl -X PATCH \ ```bash title="unikraft" unikraft api /v1/instances/ -X PATCH --metro fra \ - -d '{ + '{ "prop": "schedules", "op": "del", "value": [ @@ -228,6 +228,7 @@ curl -X PATCH \ | `stop` | Stop the instance at the scheduled time | | `delete` | Delete the instance at the scheduled time | | `exec` | Execute a command inside the instance at the scheduled time. Requires the `args` field. | +| `checkpoint` | Create a [checkpoint](/features/checkpoints) of the instance at the scheduled time, as described under [scheduled checkpoints](#scheduled-checkpoints) | The `args` field is an array of strings specifying the command to run when using the `exec` action. The first element is the executable and the remaining elements are its arguments. @@ -243,6 +244,50 @@ The first element is the executable and the remaining elements are its arguments No need to specify it for all other actions. +## Scheduled checkpoints + +{/* vale off */} +:::caution[**Limited Access**] +Checkpointing is a new feature which is available to enterprise customers, and is coming soon to the hosted platform. +If you would like to try it out now, please reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). +::: +{/* vale on */} + +The `checkpoint` action periodically creates restore points for an instance. +Every occurrence creates a [checkpoint](/features/checkpoints) of the instance, the same way a call to [`POST /instances/checkpoints`](/api/platform/v1/instances) does. +This builds a history of restore points without an external scheduler, but it doesn't rotate older checkpoints - they persist until explicitly deleted. + +To checkpoint an instance every night at 03:00 UTC: + + +```bash title="unikraft" +unikraft api /v1/instances -X POST --metro fra \ + '{ + "image": "...", + "schedules": [ + { + "name": "nightly-checkpoint", + "when": "*-*-* 03:00:00", + "action": "checkpoint" + } + ] + }' +``` + + +The platform names each new checkpoint after the instance and appends a random suffix, which matches the name a checkpoint gets when you create one through the API without one. +Find the resulting checkpoints in the instance's [checkpoint history](/features/checkpoints#checkpoint-history). + +Note the following: + +* Scheduled checkpoints need a license that covers checkpointing. + The platform checks the license when you configure the schedule and again at every occurrence, so an expired or replaced license stops an existing schedule from taking further checkpoints. +* Scheduled checkpoints only work with block-based volumes (for example, `ext4`) - occurrences fail for other volume types. +* Full VMs (`"type": "full"`) don't support checkpointing yet, so the action fails on them. +* A failing occurrence leaves the schedule in place and doesn't affect the other scheduled operations of the instance. + Because the instance status reports only the next elapse of a schedule and not the result of past runs, use the checkpoint history to confirm that occurrences produce checkpoints. + ## Learn more +* [Checkpoints](/features/checkpoints): creating, listing, and loading the restore points a `checkpoint` schedule produces. * Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). diff --git a/pages/features/custom-network-configuration.mdx b/pages/features/custom-network-configuration.mdx index 60da1936..698664ca 100644 --- a/pages/features/custom-network-configuration.mdx +++ b/pages/features/custom-network-configuration.mdx @@ -11,7 +11,9 @@ If you would like to try it out now, please reach out to the [Unikraft Cloud Dis {/* vale on */} By default, Unikraft Cloud gives every instance a single network interface, allocated from the platform's address and TAP device pool. -Custom network configuration lets you take control of this setup: attach up to four interfaces, supply your own IP addresses and TAP devices, and override the gateway and nameserver. +Custom network configuration lets you take control of this setup. +It allows you to attach up to four interfaces, supply your own IP addresses, MAC addresses, and TAP devices, or take an interface with no address at all. +You can also override the gateway and nameserver that the guest uses. This is the building block for private networking between your instances, and for the [network shield and relay](/features/network-shield). @@ -47,7 +49,7 @@ Add a `network_interfaces` array to your [`POST /instances`](/api/platform/v1/in ```bash title="unikraft" unikraft api /v1/instances \ - -d '{ + '{ "name": "my-instance", "image": "nginx:latest", "network_interfaces": [ @@ -73,40 +75,113 @@ Each entry in `network_interfaces` accepts the following fields: | Field | Required | Description | |-------|----------|-------------| | `name` | No | The interface name. If omitted, Unikraft Cloud generates one as `-ethX`, falling back to `eth-` when the instance name is too long. | -| `ip` | No | The interface IP address in CIDR notation. Provide it together with `tap_name` to bring your own interface. | -| `tap_name` | No | The TAP device to attach the interface to (15 characters or less). Provide it together with `ip`. | -| `autoconfig` | No | Whether the guest configures the interface itself. Defaults to `true`. | +| `ip` | No | The interface IP address in CIDR notation. | +| `mac` | No | The interface MAC address as `hh:hh:hh:hh:hh:hh`, see [custom MAC addresses](#custom-mac-addresses). | +| `tap_name` | No | The TAP device to attach the interface to (15 characters or less), which on its own gives you [an interface without an address](#interfaces-without-an-address). | +| `autoconfig` | No | Whether the platform configures the interface inside the guest, as described under [autoconfiguration](#autoconfiguration). | | `relay` | No | An object that routes all traffic through another interface, as described under [network shield and relay](/features/network-shield). | -You either specify both `ip` and `tap_name`, or neither. -When you omit them, Unikraft Cloud picks an interface from the platform's TAP device pool. -The `ip` and `tap_name` you supply must not come from that pool. +An `ip` and a `mac` each require a `tap_name`, while a `tap_name` on its own is enough. +When you omit all three, Unikraft Cloud allocates the interface, its address, and its MAC from the platform's own pools. +Each value you supply has to stay outside the matching pool: the platform's address ranges for an `ip`, its `12:b0:` prefix for a `mac`, and its TAP device names for a `tap_name`. +The TAP device itself is yours to create: Unikraft Cloud attaches the interface to a device that already exists. +Nothing caps a custom network at a `/30`, though a `/31` or a `/32` leaves the platform no address to derive a gateway or nameserver from. The top-level `gateway` and `nameserver` fields override the values configured inside the guest. -If you omit them, Unikraft Cloud derives them from the last address in the IP network of the first pool-allocated interface, or, if there is none, the primary interface. +If you omit them, Unikraft Cloud derives them from the last usable address in the IP network of the first pool-allocated interface. +An instance whose interfaces are all custom gets neither, so supply both fields or configure them in the guest. Like other object names, an interface `name` is **global to your account**, not local to the instance. This lets you reference one instance's interface from another. +### Custom MAC addresses + +By default the platform sets an interface's MAC address. +Pass the `mac` field to pick the address yourself, so a guest that derives its licensing, clustering, or peer identity from its MAC address stays recognizable across recreations of the instance: + +```json title="POST /instances" +{ + ... + "network_interfaces": [ + { + "tap_name": "tap-custom0", + "mac": "02:1a:2b:3c:4d:5e", + "ip": "192.168.0.1/24" + } + ] +} +``` + +Write the address as `hh:hh:hh:hh:hh:hh`, without shorthand groups or surrounding whitespace, and observe two rules: + +- The address has to be unicast, so its first octet has to be even. +- The address has to sit outside the platform's own pool, so it must not start with `12:b0:`. + +A `mac` requires a `tap_name`. +Whether you pick the address or leave it to the platform, [`GET /instances`](/api/platform/v1/instances#list-instances) reports the address each interface ended up with as the `mac` field of its entry in `network_interfaces`. + +### Interfaces without an address + +An interface that carries a `tap_name` but no `ip` gives the guest a link on your TAP device with no address on it: + +```json title="POST /instances" +{ + ... + "network_interfaces": [ + { + "tap_name": "tap-custom0", + "mac": "02:1a:2b:3c:4d:5e", + "autoconfig": false + } + ] +} +``` + +This is what a fully customer-managed network needs—an external IPAM, a CNI plugin, or a virtual appliance that handles its own addressing. +Your side provides the TAP device and decides the addressing on it, and the platform only attaches the guest's interface to that device. + +A CNI plugin can also supply the link's addressing without requiring your application or image to configure it. +Write the plugin's result into the instance's [`unikraft.com/cni` annotation](/features/annotations#guest-network-configuration-with-cni), and the guest matches the addresses in it to this interface by MAC address and configures them itself. + +Such an interface differs from an addressed one in three ways: + +- `autoconfig` has to be `false`, because the platform has no address to configure in the guest. +- The interface reports no `private_ip`, and neither does the instance when the address-less interface is its primary one. +- The platform keeps the interface out of the internal DNS and routes no service traffic to it. + +The platform also never derives a gateway or nameserver from a custom interface's network. +Set the instance-level `gateway` and `nameserver` fields, or configure both inside the guest. + ### Autoconfiguration -`autoconfig` is independent of which TAP device an interface uses and defaults to `true`. -When enabled, the guest configures the interface for you. -Disable it to leave the interface untouched, so your app or image can set up networking on its own. +`autoconfig` decides whether the platform configures the interface inside the guest. +With it on, the platform hands the guest the interface's address—together with the gateway, nameserver, and hostname—at boot, and the guest brings the interface up with them. +With it off, the platform passes nothing and skips the interface, so your app or image sets up networking on its own. + +It defaults to on for the default pool-allocated interface and for any interface with an `ip`. +An interface without one has to keep it off, because the platform then has no address to configure in the guest. ### Network shield and relay An interface can route all its traffic through another instance's interface, which is how the [network shield](/features/network-shield) filters a workload's traffic and injects secrets on its behalf. Configure it with the `relay` field of a `network_interfaces` entry. +Both ends of a relay have to be interfaces that Unikraft Cloud allocates, meaning entries where you set no `ip`, `mac`, or `tap_name` of your own. + ## Error handling | Error message | Cause | |---------------|-------| | `Insufficient license. Please make sure your license is valid and includes custom network configurations` | Your account's license doesn't include custom network configuration. | -| `Network interface must specify an IP address when a TAP device is provided and vice versa` | You set only one of `ip` and `tap_name`. Provide both or neither. | +| `Network interface IP and MAC address require a TAP device` | You set `ip` or `mac` without a `tap_name`, so either add the TAP device or drop the address. | | `Network interface IP address cannot be from the platform's address pool` | The `ip` overlaps the platform's managed address pool. Use an address outside it. | | `Network interface TAP device cannot be from the platform's device pool` | The `tap_name` refers to a platform-managed TAP device. Use one of your own. | +| `Invalid MAC address ''` | The `mac` isn't a `hh:hh:hh:hh:hh:hh` address, or it's all-zero, which the platform reads as an omitted field. | +| `Network interface MAC address must be a unicast address` | The `mac` has its multicast bit set, so choose an address whose first octet is even. | +| `Network interface MAC address cannot be from the platform's address pool` | The `mac` starts with `12:b0:`, which the platform reserves for the addresses it derives itself. | +| `Network interface without an IP address cannot be auto-configured in the guest` | You set `autoconfig` to `true` on an interface without an `ip`, which has to keep autoconfiguration off. | +| `Network interface without an IP address cannot use a relay` | You configured a `relay` on an interface without an `ip`, and the relay datapath keys on the client interface's address. | +| `Invalid relay ''. Please make sure the relay exists and does not create a circular dependency` | The interface named as the relay doesn't exist, already relays through another interface, or is itself a custom interface. | | `Unknown member 'network_interfaces'` | Your account lacks the `net_manager` permission required for this feature. | ## Limitations @@ -115,11 +190,14 @@ Configure it with the `relay` field of a `network_interfaces` entry. - The feature requires the `net_manager` permission and a license that includes it (see the note at the top of this page). - Creating a [template](/platform/instances#instance-templates) from an instance **drops** its network configuration, since binding a template to a specific TAP device or IP defeats the purpose of cloning many instances from it. - You currently **can't** [branch](/features/branching) or [checkpoint](/features/checkpoints) an instance with more than one interface. +- The [relay](/features/network-shield) feature works between pool-allocated interfaces only, on both ends. - The create call sets the configuration once; **no** PATCH support exists yet. ## Learn more * [Network shield and relay](/features/network-shield): route an instance's traffic through another instance that filters it and injects credentials. +* [Annotations](/features/annotations#guest-network-configuration-with-cni): hand a CNI result to the guest through the `unikraft.com/cni` annotation, and let it address its own interfaces. +* [Kubernetes](/integrations/kubernetes#custom-networking-with-cni): how Kraftlet runs the CNI plugins of your cluster for Unikraft Cloud instances. * [Instances](/platform/instances): how instances work, including their lifecycle and configuration. * [Branching](/features/branching) and [Checkpoints](/features/checkpoints): create independent copies and reusable restore points of running instances. * [Instance templates](/platform/instances#instance-templates): reusable images that you clone into new instances. diff --git a/pages/features/managed-volumes.mdx b/pages/features/managed-volumes.mdx index 365aa0c2..27c5fd8c 100644 --- a/pages/features/managed-volumes.mdx +++ b/pages/features/managed-volumes.mdx @@ -46,7 +46,7 @@ Currently, you drive managed volumes through the [API](/api/platform/v1/instance ```bash title="unikraft" unikraft api /v1/instances -X POST --metro fra \ - -d '{ + '{ "image": "...", "memory_mb": 1024, "autostart": true, diff --git a/pages/features/network-shield.mdx b/pages/features/network-shield.mdx index cddbe267..632b475c 100644 --- a/pages/features/network-shield.mdx +++ b/pages/features/network-shield.mdx @@ -63,7 +63,7 @@ To use a relay, reference another interface by name inside a `relay` object: ```bash title="Relay instance" unikraft api /v1/instances \ - -d '{ + '{ "name": "my-relay", "network_interfaces": [ { @@ -75,7 +75,7 @@ unikraft api /v1/instances \ ```bash title="Sandbox instance (relay client)" unikraft api /v1/instances \ - -d '{ + '{ "name": "my-sandbox", "network_interfaces": [ { @@ -104,7 +104,7 @@ The `relay` object accepts the following fields: - All network traffic passes over the relay. Many VMs can share a single relay. - - This feature supports managed interfaces with custom TAP devices. + - Both ends of a relay have to be interfaces that Unikraft Cloud allocates, meaning entries where you set no `ip`, `mac`, or `tap_name` of your own. - You don't need to enable VM-to-VM communication to use this feature (which you don't want in a sandbox environment). @@ -119,12 +119,17 @@ The `relay` object accepts the following fields: |---------------|-------| | `Insufficient license. Please make sure your license is valid and includes custom network configurations` | Your account's license doesn't include custom network configuration, which relay is part of. | | `Unknown member 'network_interfaces'` | Your account lacks the `net_manager` permission required for this feature. | +| `Invalid relay ''. Please make sure the relay exists and does not create a circular dependency` | The interface you named doesn't exist, already relays through another interface, or is itself a custom interface. | +| `Network interface without an IP address cannot use a relay` | You configured a `relay` on an interface without an `ip`, and the relay datapath keys on the client interface's address. | ## Limitations - The feature requires the `net_manager` permission and a license that includes it (see the note at the top of this page). - Creating a [template](/platform/instances#instance-templates) from an instance **drops** its network configuration, since binding a template to a specific TAP device or IP defeats the purpose of cloning many instances from it. - The create call sets the configuration once, and **no** PATCH support exists yet. +- Both ends of a relay have to be interfaces that Unikraft Cloud allocates, meaning entries where you set no `ip`, `mac`, or `tap_name` of your own. +- Neither direction of a relay chain works. + An interface that already relays can't serve as a relay, and one serving as a relay for others can't take a relay itself. - Every [custom network configuration](/features/custom-network-configuration#limitations) limitation applies here too, since relay is part of that feature. ## Learn more diff --git a/pages/features/on-demand-templates.mdx b/pages/features/on-demand-templates.mdx index d122be60..324c9e9d 100644 --- a/pages/features/on-demand-templates.mdx +++ b/pages/features/on-demand-templates.mdx @@ -20,13 +20,18 @@ Currently, there is no unikraft CLI support for on-demand instance template crea You must do this by calling the platform [API](/api/platform/v1/instances#create-instance) with `unikraft api` or curl. ::: +:::note +Like any [instance template](/platform/instances#instance-templates), on-demand templates need the default `micro` [instance type](/platform/instances#instance-types). +QEMU-backed full VMs (`--type full`), which [GPU instances](/platform/instances#gpus) require, don't support them. +::: + ## On-demand template creation To create a template on demand, add `prepare: true` and a `create_args` object to the `template` block of a normal instance-create request: ```bash title="POST /instances" unikraft api /v1/instances --metro=fra \ - -d '{ + '{ "name": "my-instance", "template": { "name": "my-template", @@ -92,7 +97,7 @@ To pass data that differs per instance—a session ID, a config file, a function ```bash title="POST /instances" unikraft api /v1/instances --metro=fra \ - -d '{ + '{ "name": "my-instance", "template": { "name": "my-template", @@ -143,7 +148,7 @@ A common shape is a generic worker template, a more specific function template b ```bash title="POST /instances" unikraft api /v1/instances --metro=fra \ - -d '{ + '{ "name": "my-instance", "template": { "name": "my-function-template", @@ -191,7 +196,7 @@ To release that space automatically, give the template an [autokill](/features/a ```bash title="POST /instances" unikraft api /v1/instances --metro=fra \ - -d '{ + '{ "name": "my-instance", "template": { "name": "my-template", diff --git a/pages/features/plugins.mdx b/pages/features/plugins.mdx index 236e16c0..a7c49c6b 100644 --- a/pages/features/plugins.mdx +++ b/pages/features/plugins.mdx @@ -22,6 +22,22 @@ Unikraft Cloud built plugins to provide a native sandbox experience. In the sandbox case, a plugin runs a small HTTP server that accepts commands to run inside the instance, exposes filesystem services, and more. The same mechanism fits any helper you want to reach over a per-instance, authenticated endpoint. +## Official plugins + +### Sandbox plugin + +{/* vale off */} +:::caution[**Limited Access**] +The sandbox plugin is available as part of enterprise plans. +To try it out, reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). +::: +{/* vale on */} + +The sandbox plugin lets you run commands and manipulate the filesystem inside an instance, over the plugin endpoint: + +- **Commands**: start a shell command inside the instance, inspect it, read its output, feed its standard input, wait for it, and signal it. +- **Filesystem**: create directories, and read, write, and upload files. + ## How it works Each plugin loads from its own [ROM image](/features/roms). @@ -39,7 +55,9 @@ A request to `.../plugins/my-plugin/files/list` reaches the plugin with the path This design has a few notable properties: -- **Authenticated like any API call.** The platform checks the request against your account and confirms that you own the target instance before it forwards anything. This sets plugins apart from running an HTTP server as a regular [service](/platform/services). +- **Authorized before it reaches the plugin.** By default the platform checks the request against your account and confirms your access to the target instance. + A plugin can carry [a token of its own](#authorization) instead, which lets callers without a platform account in. + Either method settles the request before the plugin sees it, which sets plugins apart from running an HTTP server as a regular [service](/platform/services). - **A direct line to one instance.** You talk to a single instance, so no [load balancing](/features/load-balancing) or [autoscale](/features/autoscale) sits in the path, and the instance needs no [service group](/platform/services). - **Works with scale-to-zero.** When [scale-to-zero](/features/scale-to-zero) has put the instance to sleep, the platform wakes it to serve the request and keeps it up for the duration, the same way a normal request does. @@ -67,7 +85,7 @@ List your plugins in the `plugins` field of a [`POST /instances`](/api/platform/ ```bash title="unikraft" unikraft api /v1/instances \ - -d '{ + '{ "name": "my-instance", "plugins": [ { @@ -90,9 +108,70 @@ Each entry in the `plugins` array accepts these fields: | `name` | Yes | The plugin name. It becomes the `` segment in the plugin endpoint. See [Plugin names](#plugin-names) for the allowed format. | | `rom` | Yes | The plugin's ROM image, given as an image reference string such as `user/myplugin:latest`, or as an image object with a `url` and optional `headers` and `pull_policy`, the same as elsewhere in the API. | | `config` | No | Arbitrary JSON that the platform passes to the plugin's `init` on `STDIN`. Any JSON value works, including a string, a number, or an object. | +| `authorization` | No | How the platform authorizes requests to this plugin, as described under [authorization](#authorization). | You can attach up to 8 plugins to an instance. +## Authorization + +Every request to a plugin endpoint carries a token in an `Authorization: Bearer ` header. +The platform authorizes the request before it forwards anything, and strips that header on the way, so the plugin never sees the token. + +A plugin picks how the platform checks the token with an optional `authorization` object: + +| Field | Required | Description | +|-------|----------|-------------| +| `type` | Yes | Either `userdb` or `bearer`. | +| `token` | For `bearer` | The plugin's own token, at most 256 characters, which only a `bearer` plugin may carry. | + +### Platform users + +A plugin without an `authorization` object uses the `userdb` method. +The token has to belong to a platform user with access to the instance, so the token you use for the rest of the API reaches the plugin as well. + +### A token of the plugin's own + +With `"type": "bearer"` the platform compares the supplied token against the plugin's own token and consults no user account: + + + +```bash title="unikraft" +unikraft api /v1/instances \ + '{ + "name": "my-instance", + "plugins": [ + { + "name": "my-plugin", + "rom": "user/myplugin:latest", + "authorization": { + "type": "bearer", + "token": "s3cr3t" + } + } + ] + }' +``` + + + +A caller then reaches the plugin with that token alone: + +```bash +curl -H "Authorization: Bearer s3cr3t" \ + https://api.fra.unikraft.cloud/v1/instances//plugins/my-plugin/files/list +``` + +This suits callers with no platform account and no business holding one: a sidecar, your own control plane, or a webhook from a third-party service. +The token opens that one plugin on that one instance, and nothing else of the API. + +The instance status echoes a non-default `type` back in the plugin's `authorization` object, and never echoes the token. +A plugin on the default method reports no `authorization` object at all. + +Two consequences follow from `bearer` consulting no user account: + +- Your platform token stops working on that plugin, so the plugin's token becomes the only way in. +- Changing the token takes the same [PATCH path](#adding-plugins-to-an-existing-instance) as adding a plugin, so it lands the next time the instance stops. + ## Plugin names A plugin name has a maximum length of 63 characters and contains only these characters: @@ -110,7 +189,7 @@ Set `prop` to `plugins`, and pass the plugins in `value`: ```bash title="unikraft" unikraft api /v1/instances -X PATCH \ - -d '[{ + '[{ "name": "my-instance", "prop": "plugins", "op": "add", @@ -160,9 +239,12 @@ Plugins fit other patterns too. A few ideas: - You can attach at most 8 plugins to an instance. - Adding plugins works today, and unloading a plugin from an instance is coming in a future release. - A plugin name has a maximum length of 63 characters and uses only letters, digits, `-`, and `_`. +- A `bearer` token has a maximum length of 256 characters. ## Learn more +* [Plugin SDK](/integrations/sdks/plugin): build a plugin in Go without hand-rolling the platform contract. +* [Sandboxes](/use-cases/sandboxes): run untrusted code in a hardware-isolated microVM. * [ROMs](/features/roms): the image format that plugins build on. * [Instances](/platform/instances): create and manage the instances that host plugins. * [Scale-to-zero](/features/scale-to-zero): how an idle instance wakes to serve a plugin request. diff --git a/pages/features/roms.mdx b/pages/features/roms.mdx index 61e4a5c7..0e2c83c2 100644 --- a/pages/features/roms.mdx +++ b/pages/features/roms.mdx @@ -119,6 +119,9 @@ spec: v0.7 runtime: base-compat:latest +targets: + - kraftcloud/x86_64 + rootfs: source: ./Dockerfile format: erofs @@ -195,6 +198,12 @@ The commands below will use the `at` field to automount the ROM. That's why the `Kraftfile` omits the `wrapper.sh` script and the app can read the ROM file directly from `/rom/rom.py`. ::: +:::note +This base image builds for `x86_64` only, and its `Dockerfile` copies libraries from architecture-specific paths such as `/lib/x86_64-linux-gnu`. +To run the same base image on ARM hosts, add `kraftcloud/arm64` to `targets`, and copy from the matching `aarch64-linux-gnu` paths. +See [architectures](/platform/images#architectures) for more detail. +::: + Package and push the base image: @@ -248,6 +257,9 @@ def function(): ```yaml title="rom1/Kraftfile" spec: v0.7 +targets: +- kraftcloud/x86_64 + roms: - ./fs ``` @@ -260,11 +272,21 @@ def function(): ```yaml title="rom2/Kraftfile" spec: v0.7 +targets: +- kraftcloud/x86_64 + roms: - ./fs ``` +:::caution +ROMs are architecture-specific. +A ROM `Kraftfile` has no `runtime` to take architectures from, so it names them itself under `targets`, or you pass `--arch` on the command line. +A ROM build that does neither fails. +Listing more than one architecture builds the ROM for each of them in one invocation, and each instance needs the ROM that matches the architecture of its base image. +::: + Package and push the ROMs: diff --git a/pages/features/scale-to-zero.mdx b/pages/features/scale-to-zero.mdx index ac445e4f..253942c2 100644 --- a/pages/features/scale-to-zero.mdx +++ b/pages/features/scale-to-zero.mdx @@ -24,6 +24,11 @@ This differs from [autoscale](/features/autoscale), in which you *don't* specify The platform does this for you based on traffic load. ::: +:::note +Scale-to-zero needs the default `micro` [instance type](/platform/instances#instance-types). +QEMU-backed full VMs (`--type full`), which [GPU instances](/platform/instances#gpus) require, don't support it. +::: + ## Policies diff --git a/pages/guides/build-environments.mdx b/pages/guides/build-environments.mdx new file mode 100644 index 00000000..53a20275 --- /dev/null +++ b/pages/guides/build-environments.mdx @@ -0,0 +1,415 @@ +--- +title: "Build/Test Environments with ROMs" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This guide shows how to deploy a Go runtime that compiles ROM-provided code before execution. +The base image contains a generic server and Go toolchain. +Each ROM only contains a `rom.go` file, which the instance compiles into a Go plugin (`.so`) at startup. + +This pattern is useful for build and test environments where runtime code changes frequently but the execution environment stays stable. + +## Prerequisites + +1. Install the CLI: + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, set up and use BuildKit directly, see the [quick start](https://github.com/moby/buildkit#quick-start). + + :::note + The unikraft CLI is the current standard, while kraft is the legacy version. + Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. + ::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/build-environments` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/build-environments/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +export UKC_METRO=fra +``` + + + +## Deployment Workflow + +### Package the base image + +Package and push the base Go runtime image (see `server.go` for the runtime implementation): + + + +```bash title="unikraft" +unikraft build . --output /go-build-env:latest +``` + +```bash title="kraft" +kraft pkg \ + --name index.unikraft.io//go-build-env:latest \ + --plat kraftcloud \ + --arch x86_64 \ + --rootfs-type erofs \ + --push \ + . +``` + + + +The server in [`server.go`](./server.go) loads `/rom/rom.go`, compiles it to `/run/rom.so` using `go build -buildmode=plugin`, and invokes `Handler()` from the plugin. + +### Create an instance template from the base image + +Create a short-lived instance from the base image (without ROM attached). +The server writes to `/uk/libukp/template_instance` and turns the instance into a template before serving requests (see /platform/instances#instance-templates): + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name go-build-env \ + -m 512M \ + --image /go-build-env:latest +``` + +```bash title="kraft" +kraft cloud instance create \ + --start \ + --name go-build-env \ + -M 512Mi \ + /go-build-env:latest +``` + + + +The output shows the instance details: + + + +```ansi title="unikraft" +metro: fra +name: go-build-env +uuid: 650dbbe7-3949-4c93-88e7-6619a9216e0c +state: starting +image: /go-build-env +resources: + memory: 512MiB + vcpus: 1 +networks: +- uuid: 6f7a8b9c-0d1e-2f3a-4b5c-f6a7b8c9d0e1 + private-ip: 10.0.5.4 + mac: 12:b0:6c:3e:ab:95 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: go-build-env + ├───────── uuid: 650dbbe7-3949-4c93-88e7-6619a9216e0c + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├──────── image: oci://unikraft.io//go-build-env@sha256:1f57e9bb8702d031743acf43164b24cf182158c398f1eda8c5583208ccc9c300 + ├─────── memory: 512 MiB + ├─ private fqdn: go-build-env.internal + └─── private ip: 10.0.5.4 +``` + + + +If you are fast enough, you can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + +This instance is short-lived, since right before the server starts, it triggers a conversion into a template. +To check the template is ready, run: + + + +```bash title="unikraft" +unikraft instances templates list +``` + +```bash title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED +fra go-build-env template /go-build-env 512MiB 1 2 minutes ago +``` + +```bash title="kraft" +kraft cloud instance template list +``` + +```bash title="kraft" +NAME IMAGE ARGS CREATED AT +go-build-env oci://unikraft.io//go-build-env@sha256:1f57e9bb8702d031743acf43164b24cf182158c398f1eda8c5583208ccc9c300 3 minutes ago +``` + + + +### Package the ROMs + +Each ROM contains a Go function implementation (see [`rom1/fs/rom.go`](./rom1/fs/rom.go) and [`rom2/fs/rom.go`](./rom2/fs/rom.go)). + + + +```bash title="unikraft" +unikraft build rom1/ --output /go-rom1:latest +unikraft build rom2/ --output /go-rom2:latest +``` + +```bash title="kraft" +kraft pkg \ + --name index.unikraft.io//go-rom1:latest \ + --rom ./fs \ + --rom-type erofs \ + --plat kraftcloud \ + --arch x86_64 \ + --push \ + rom1/ +kraft pkg \ + --name index.unikraft.io//go-rom2:latest \ + --rom ./fs \ + --rom-type erofs \ + --plat kraftcloud \ + --arch x86_64 \ + --push \ + rom2/ +``` + + + +### Create instances from the template with different ROMs attached + +Create an instance with the first ROM: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name go-build-env-rom1 \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ + --rom image=/go-rom1:latest,at=/rom \ + --template go-build-env +``` + +```bash title="kraft" +# kraft does not support creating instances with attached ROMs, but you can use the API directly +curl -X POST "$UKC_METRO/instances" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "go-build-env-rom1", + "template": { + "name": "go-build-env" + }, + "autostart": true, + "service_group": { + "services": [ + { + "port": 443, + "destination_port": 8080, + "handlers": ["tls", "http"] + } + ] + }, + "scale_to_zero": { + "policy": "on", + "stateful": true, + "cooldown_time_ms": 1000 + }, + "roms": [ + { + "name": "go_function", + "image": "index.unikraft.io//go-rom1:latest", + "at": "/rom" + } + ] +}' +``` + + + +The instance will compile the ROM into a plugin on first start, which may take a few seconds. +To check the progress, you can view the instance logs: + + + +```bash title="unikraft" +unikraft instances logs go-build-env-rom1 -f +``` + +```bash title="kraft" +kraft cloud instance logs go-build-env-rom1 -f +``` + + + +Create another instance with the second ROM: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name go-build-env-rom2 \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ + --rom image=/go-rom2:latest,at=/rom \ + --template go-build-env +``` + +```bash title="kraft" +# kraft does not support creating instances with attached ROMs, but you can use the API directly +curl -X POST "$UKC_METRO/instances" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "go-build-env-rom2", + "template": { + "name": "go-build-env" + }, + "autostart": true, + "service_group": { + "services": [ + { + "port": 443, + "destination_port": 8080, + "handlers": ["tls", "http"] + } + ] + }, + "scale_to_zero": { + "policy": "on", + "stateful": true, + "cooldown_time_ms": 1000 + }, + "roms": [ + { + "name": "go_function", + "image": "index.unikraft.io//go-rom2:latest", + "at": "/rom" + } + ] +}' +``` + + + +The instance will compile the ROM into a plugin on first start, which may take a few seconds. +To check the progress, you can view the instance logs: + + + +```bash title="unikraft" +unikraft instances logs go-build-env-rom2 -f +``` + +```bash title="kraft" +kraft cloud instance logs go-build-env-rom2 -f +``` + + + +List the instances and note their FQDN values: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra go-build-env-rom2 standby /go-build-env 512MiB 1 nameless-wood-gw7pbnls.fra.unikraft.app 2 minutes ago +fra go-build-env-rom1 standby /go-build-env 512MiB 1 sparkling-dawn-syowlbtj.fra.unikraft.app 3 minutes ago +``` + +```bash title="kraft" +kraft cloud instance list +``` + +```bash title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +go-build-env-rom2 nameless-wood-gw7pbnls.fra.unikraft.app standby standby oci://unikraft.io//go-build-env@sha256:1cbd64... 512 MiB 1 6.98 ms +go-build-env-rom1 sparkling-dawn-syowlbtj.fra.unikraft.app standby standby oci://unikraft.io//go-build-env@sha256:1cbd64... 512 MiB 1 7.86 ms +``` + + + +Test both instances: + +```bash +curl https://sparkling-dawn-syowlbtj.fra.unikraft.app +curl https://nameless-wood-gw7pbnls.fra.unikraft.app +``` + +```text +Bye, World! +Auf Wiedersehen! +``` + +## Cleanup + + + +```bash title="unikraft" +unikraft instances delete go-build-env-rom1 go-build-env-rom2 +unikraft instances template delete go-build-env +``` + +```bash title="kraft" +kraft cloud instance remove go-build-env-rom1 go-build-env-rom2 +kraft cloud instance template remove go-build-env +``` + + + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/chromium-cdp-auth.mdx b/pages/guides/chromium-cdp-auth.mdx new file mode 100644 index 00000000..e8bc2002 --- /dev/null +++ b/pages/guides/chromium-cdp-auth.mdx @@ -0,0 +1,248 @@ +--- +title: "Chromium CDP with Token Authentication" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This example uses Chromium, a headless browser exposing a [CDP (Chrome DevTools Protocol)](https://chromedevtools.github.io/devtools-protocol/) websocket interface, with token-based authentication and persistent storage. + +## Features + +- **Token authentication**: All CDP requests (HTTP and WebSocket) require a valid token +- **Admin API**: Create, list, and revoke tokens via REST endpoints +- **Bootstrap token**: Set an initial admin token via environment variable +- **Persistent storage**: The token database is stored on a volume that survives restarts + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + + :::note + The unikraft CLI is the current standard, while kraft is the legacy version. + Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. + ::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/chromium-cdp-auth/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/chromium-cdp-auth/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +export UKC_METRO=fra +``` + + + +The `UKC_TOKEN` and `UKC_METRO` environment variables are only supported by the legacy CLI. + +Pick a bootstrap admin token that will be used for initial setup. +You pass it to the instance as the `BOOTSTRAP_ADMIN_TOKEN` environment variable (see the deploy commands below) and use it to create additional tokens: + +```bash +export BOOTSTRAP_ADMIN_TOKEN=my-secret-admin-token +``` + +The token database is persisted on a [volume](/platform/volumes) mounted at `/app/data`, so it survives restarts. +First create the volume: + + + +```bash title="unikraft" +unikraft volume create --set metro=fra --set name=chromium-cdp-auth-data --set size=64M +``` + +```bash title="kraft" +kraft cloud volume create --name chromium-cdp-auth-data --size 64Mi +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud, mounting the volume and passing the bootstrap token: + + + +```bash title="unikraft" +unikraft build . --output /chromium-cdp-auth +unikraft run --metro fra \ + -m 4G \ + -p 443:8080/tls+http \ + --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ + -e BOOTSTRAP_ADMIN_TOKEN="$BOOTSTRAP_ADMIN_TOKEN" \ + --volume chromium-cdp-auth-data:/app/data \ + --image /chromium-cdp-auth +``` + +```bash title="kraft" +kraft cloud deploy \ + -M 4Gi \ + -p 443:8080/tls+http \ + --scale-to-zero idle \ + --scale-to-zero-stateful \ + --scale-to-zero-cooldown 1s \ + --env BOOTSTRAP_ADMIN_TOKEN="$BOOTSTRAP_ADMIN_TOKEN" \ + --volume chromium-cdp-auth-data:/app/data \ + . +``` + + + +The output shows the instance address and other details. + + + +```ansi title="unikraft" +metro: fra +name: chromium-cdp-auth-d0l6y +uuid: debe81b0-8418-4e01-b795-b3546e0e5aac +state: starting +image: /chromium-cdp-auth +resources: + memory: 4GiB + vcpus: 1 +service: + uuid: 516e239b-2ab1-9fb9-599d-fb891cc39edb + name: spring-dream-p5wxwwl0 + domains: + - fqdn: spring-dream-p5wxwwl0.fra.unikraft.app +networks: +- uuid: 7d3633e4-7835-942c-7b32-5d392ba538d7 + private-ip: 10.0.4.141 + mac: 12:b0:7b:d3:eb:de +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: chromium-cdp-auth-d0l6y + ├───────── uuid: debe81b0-8418-4e01-b795-b3546e0e5aac + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://spring-dream-p5wxwwl0.fra.unikraft.app + ├──────── image: oci://unikraft.io//chromium-cdp-auth@sha256:9e22546a9234efbd586b3cc3ff2ab71d64b56e87b8af431a3dfffd4aff274cc3 + ├─────── memory: 4096 MiB + ├────── service: spring-dream-p5wxwwl0 + ├─ private fqdn: chromium-cdp-auth-d0l6y.internal + └─── private ip: 10.0.4.141 +``` + + + +In this case, the instance name is `chromium-cdp-auth-d0l6y` and the address is `https://spring-dream-p5wxwwl0.fra.unikraft.app`. +They're different for each run. + +## Authentication + +All CDP endpoints require a valid token, passed either as: + +- **Query parameter**: `?token=` +- **Authorization header**: `Authorization: Bearer ` + +The bootstrap admin token (set via the `BOOTSTRAP_ADMIN_TOKEN` environment variable) can be used for initial setup. +Use it to create additional tokens. + +### Token management API (admin only) + +**Create a token:** +```bash +curl -X POST https:///api/tokens \ + -H "Authorization: Bearer $BOOTSTRAP_ADMIN_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{"name": "my-client", "expiresInDays": 7}' +``` + +**List tokens:** +```bash +curl https:///api/tokens \ + -H "Authorization: Bearer $BOOTSTRAP_ADMIN_TOKEN" +``` + +**Revoke a token:** +```bash +curl -X DELETE https:///api/tokens/ \ + -H "Authorization: Bearer $BOOTSTRAP_ADMIN_TOKEN" +``` + +### Public endpoints + +- `GET /health` — health check (no auth required) + +## Testing + +To query the service you need to use a CDP client. +You can use the Python-based implementation in the `test/` directory. +See [`test/README.md`](test/README.md) for setup and a screenshot example that passes a token. + +## Instance management + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete chromium-cdp-auth-d0l6y +``` + +```bash title="kraft" +kraft cloud instance remove chromium-cdp-auth-d0l6y +``` + + + +## Learn more + +- [CDP Documentation](https://chromedevtools.github.io/devtools-protocol/) +- [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) +- [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/chromium-cdp.mdx b/pages/guides/chromium-cdp.mdx new file mode 100644 index 00000000..fca92fb6 --- /dev/null +++ b/pages/guides/chromium-cdp.mdx @@ -0,0 +1,187 @@ +--- +title: "Chromium CDP" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This example uses Chromium, a headless browser exposing a [CDP (Chrome DevTools Protocol)](https://chromedevtools.github.io/devtools-protocol/) websocket interface. + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + + :::note + The unikraft CLI is the current standard, while kraft is the legacy version. + Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. + ::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/chromium-cdp/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/chromium-cdp/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +export UKC_METRO=fra +``` + + + +The `UKC_TOKEN` and `UKC_METRO` environment variables are only supported by the legacy CLI. + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /chromium-cdp +unikraft run --metro fra \ + -m 4G \ + -p 443:8080/tls+http \ + --image /chromium-cdp +``` + +```bash title="kraft" +kraft cloud deploy \ + -M 4Gi \ + -p 443:8080/tls+http \ + . +``` + + + +The output shows the instance address and other details. + + + +```ansi title="unikraft" +metro: fra +name: chromium-cdp-d0l6y +uuid: debe81b0-8418-4e01-b795-b3546e0e5aac +state: starting +image: /chromium-cdp +resources: + memory: 4GiB + vcpus: 1 +service: + uuid: 516e239b-2ab1-9fb9-599d-fb891cc39edb + name: spring-dream-p5wxwwl0 + domains: + - fqdn: spring-dream-p5wxwwl0.fra.unikraft.app +networks: +- uuid: 7d3633e4-7835-942c-7b32-5d392ba538d7 + private-ip: 10.0.4.141 + mac: 12:b0:7b:d3:eb:de +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: chromium-cdp-d0l6y + ├───────── uuid: debe81b0-8418-4e01-b795-b3546e0e5aac + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://spring-dream-p5wxwwl0.fra.unikraft.app + ├──────── image: oci://unikraft.io//chromium-cdp@sha256:9e22546a9234efbd586b3cc3ff2ab71d64b56e87b8af431a3dfffd4aff274cc3 + ├─────── memory: 4096 MiB + ├────── service: spring-dream-p5wxwwl0 + ├─ private fqdn: chromium-cdp-d0l6y.internal + └─── private ip: 10.0.4.141 +``` + + + +In this case, the instance name is `chromium-cdp-d0l6y` and the address is `https://spring-dream-p5wxwwl0.fra.unikraft.app`. +They're different for each run. + +To query the service you need to use a CDP client. +You can use the Python-based implementation in the `test/` directory. + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + +
+ + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra chromium-cdp-d0l6y running /chromium-cdp 4096MiB 1 spring-dream-p5wxwwl0.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +chromium-cdp-d0l6y spring-dream-p5wxwwl0.fra.unikraft.app running 1 minute ago oci://unikraft.io//chromium-cdp@sha256:... 4 GiB 1 350.51 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete chromium-cdp-d0l6y +``` + +```bash title="kraft" +kraft cloud instance remove chromium-cdp-d0l6y +``` + + + +## Learn more + +- [CDP Documentation](https://chromedevtools.github.io/devtools-protocol/) +- [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) +- [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) + + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/minecraft.mdx b/pages/guides/minecraft.mdx new file mode 100644 index 00000000..6e65ead8 --- /dev/null +++ b/pages/guides/minecraft.mdx @@ -0,0 +1,273 @@ +--- +title: "Minecraft" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This example runs a Minecraft Java server on Unikraft Cloud with: + +- A reusable base image (`minecraft:latest`) built from [itzg/minecraft-server](https://hub.docker.com/r/itzg/minecraft-server) (Java 25) +- Configuration overrides through auxiliary ROMs (optional) +- Template-based startup for faster instance creation +- TLS-exposed Minecraft and SSH endpoints + +## Prerequisites + +1. Install the [unikraft CLI](/cli/unikraft). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/minecraft/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/minecraft/ + ``` + +3. Review and adjust the base server settings in [`base/.env`](./base/.env). + You can check out [this documentation](https://docker-minecraft-server.readthedocs.io/en/latest/) for available configuration options. + Make sure to also set your `PUBKEY` for SSH access, and optionally set `TEMPLATE_WITH_WORLD` if you want the template to include the world (see below). + Optionally, create per-config overrides in `/.env` (for example [`bingo/.env`](./bingo/.env)). + All the `.env` files are packaged as [auxiliary ROMs](/features/roms) and mounted at `/rom/`. + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + +```bash title="unikraft" +unikraft login +``` + +## Deployment Workflow + +### Package the base image + +First, package and push the base Minecraft server image: + +```bash title="unikraft" +unikraft build . --output /minecraft:latest +``` + +The image is built from the Docker image [itzg/minecraft-server](https://hub.docker.com/r/itzg/minecraft-server) and includes a few tweaks: + +- The entrypoint is wrapped around a custom [`wrapper.sh`](./wrapper.sh) script that: + - Starts an SSH server + - Loads the environment configuration from the attached ROM(s) + - Disables scale-to-zero before executing the original entrypoint +- The server configuration scripts from the original image are patched (see [patches/](./patches)) to trigger the template snapshot before the full warm-up of the server, which allows faster instance creation from the template. + Scale-to-zero is enabled after server initialization. + +### Create an instance template from the base image + +This step is required to enable fast startups from the base image, which is particularly important for a Minecraft server given the long initialization time. +If you only customized the server using the base [`.env`](./base/.env) file, create the instance with one ROM: + +```bash title="unikraft" +unikraft run --metro fra \ + --name minecraft-tpl \ + -m 4096M \ + --vcpus 4 \ + --rom dir=base,at=/rom/base \ + --image /minecraft:latest +``` + +The output shows the instance details: + +```ansi title="unikraft" +metro: fra +name: minecraft-tpl +uuid: d9d7be54-5495-45d0-b5af-48be7f30d1d8 +state: starting +image: /minecraft +resources: + memory: 4GiB + vcpus: 4 +roms: +- name: rom + image: c99125f6-6b8c-4f94-b94c-e2d9551b253b + at: /rom +networks: +- uuid: 123b0ed0-9f2f-417c-9307-34424d80e4cb + private-ip: 10.0.0.29 + mac: 12:b0:0a:00:00:1d +timestamps: + created: just now +``` + +If you also have per-config overrides (for example in [`bingo/.env`](./bingo/.env)), create the instance with multiple ROMs: + +```bash title="unikraft" +unikraft run --metro fra \ + --name minecraft-tpl \ + -m 4096M \ + --vcpus 4 \ + --rom dir=base,at=/rom/base \ + --rom dir=bingo,at=/rom/bingo \ + --image /minecraft:latest +``` + +The instance will run until initialization is complete, then it will be snapshotted as a template and immediately deleted. +The exact moment of snapshotting can be configured in [`patches/start-finalExec`](./patches/start-finalExec), but it can also be configured by the following environment variable: + +- If `TEMPLATE_WITH_WORLD=true`, then the snapshot will be triggered **after** world generation +- Otherwise, the snapshot will be triggered **after** extracting the jar files and writing configuration files, **before** world generation + +You can follow the logs of the instance to check the progress: + +```bash title="unikraft" +unikraft instances logs minecraft-tpl -f +``` + +Once they stop, the template is ready and you can check it with: + + + +```bash title="unikraft" +unikraft instances templates list +``` + +```bash title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED +fra minecraft-tpl template /minecraft 4GiB 4 5 seconds ago +``` + + + +### Create an instance from the template + +You can now create new instances from the template, which will boot much faster than the original base image: + +```bash title="unikraft" +unikraft run --metro fra \ + --name minecraft \ + -p 2222:2222/tls \ + -p 25565:25565/tls \ + --scale-to-zero policy=on,cooldown-time=5000,stateful=true \ + --template minecraft-tpl +``` + +The output shows the instance address and other details: + +```ansi title="unikraft" +metro: fra +name: minecraft +uuid: 56787b5d-14fb-4908-88d0-e9609d1ac1e0 +state: running +image: /minecraft +resources: + memory: 4GiB + vcpus: 4 +service: hidden-water-ewr8l9sp +roms: +- name: rom + image: c99125f6-6b8c-4f94-b94c-e2d9551b253b + at: /rom +networks: +- uuid: b6c7d615-7e74-49d6-a8b6-14c39323233b + private-ip: 10.0.0.29 + mac: 12:b0:0a:00:00:1d +timestamps: + created: just now +scale-to-zero: policy=on,stateful=true,cooldown-time=5s +``` + +In this case, the instance name is `minecraft` and the address is `https://hidden-water-ewr8l9sp.fra.unikraft.app`. + +A sample log output of the server getting ready: + +```bash title="unikraft" +unikraft instances logs minecraft -f +``` + +```text +[12:59:45] [Server thread/INFO]: Preparing spawn area: 100% +[12:59:45] [Server thread/INFO]: Time elapsed: 3641 ms +[12:59:45] [Server thread/INFO]: Done (3.850s)! For help, type "help" +[12:59:45] [Server thread/INFO]: Starting remote control listener +[12:59:45] [Server thread/INFO]: Thread RCON Listener started +[12:59:45] [Server thread/INFO]: RCON running on 0.0.0.0:25575 +[12:59:45] [Server thread/INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:overworld +[12:59:45] [Server thread/INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:the_end +[12:59:45] [Server thread/INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:the_nether +[12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage (world): All chunks are saved +[12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved +[12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved +[12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved +``` + +## Connect to Minecraft and SSH + +At the moment, Unikraft Cloud can only expose these services over TLS. +Since Minecraft clients and SSH clients do not support TLS, you can use `socat` to terminate TLS locally and expose plain TCP endpoints for your clients to connect to. + +### Minecraft proxy + +```bash +socat TCP-LISTEN:25565,reuseaddr,fork OPENSSL:hidden-water-ewr8l9sp.fra.unikraft.app:25565,verify=0 +``` + +Then connect your Minecraft client to `localhost:25565`. + +### SSH proxy + +```bash +socat TCP-LISTEN:2222,reuseaddr,fork OPENSSL:hidden-water-ewr8l9sp.fra.unikraft.app:2222,verify=0 +``` + +Then connect your SSH client to `localhost:2222` with the username `root` and the SSH key you configured in the `.env` file. + +## Administration + +### RCON + +SSH into the instance (via the local TLS proxy), then run: + +```bash +rcon-cli --host 127.0.0.1 +``` + +You can then execute any Minecraft server command through the RCON interface, for example: + +```bash +say Hello from Unikraft Cloud! +``` + +You can also give yourself operator permissions to execute these commands in-game: + +```bash +/op +``` + +### Whitelist in offline mode + +Offline mode does not produce online UUIDs. +If you whitelist players manually, compute UUIDs per username and update `/data/whitelist.json`. + +```python +import hashlib, uuid; h = hashlib.md5(b'OfflinePlayer:').digest(); u = uuid.UUID(bytes=h); print(u) +``` + +Format: + +```json +[ + { + "uuid": "", + "name": "" + } +] +``` + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + +```bash title="unikraft" +unikraft --help +``` + +Or visit the [CLI Reference](/cli/unikraft). +{/* vale on */} diff --git a/pages/guides/node-code-execution.mdx b/pages/guides/node-code-execution.mdx new file mode 100644 index 00000000..236b667e --- /dev/null +++ b/pages/guides/node-code-execution.mdx @@ -0,0 +1,347 @@ +--- +title: "Node.js Code Execution with ROMs" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This guide explains how to deploy TypeScript/JavaScript functions as auxiliary Read-Only Memory (ROM) images, then load them dynamically in a Node.js runtime. +With Unikraft Cloud, you can create a base image with a generic runtime, package custom code as ROMs, and attach different ROMs to instances of the same base image. + +## Prerequisites + +1. Install the CLI: + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, set up and use BuildKit directly, see the [quick start](https://github.com/moby/buildkit#quick-start). + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node-code-execution` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/node-code-execution/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +export UKC_METRO=fra +``` + + + +## Deployment Workflow + +### Package the base image + +First, package and push the base Node.js image (see `server.ts` for the runtime implementation): + + + +```bash title="unikraft" +unikraft build . --output /node-code-exec:latest +``` + +```bash title="kraft" +kraft pkg \ + --name index.unikraft.io//node-code-exec:latest \ + --plat kraftcloud \ + --arch x86_64 \ + --rootfs-type erofs \ + --push \ + . +``` + + + +The server implementation in `server.ts` is a simple Node.js application that listens for HTTP requests and executes JavaScript code from the attached ROM, if available. +There is a little tweak—right before loading the ROM code and starting the server, it writes `1` to the special file `/uk/libukp/template_instance` (see /platform/instances#instance-templates), triggering a conversion of the instance into a template. + +### Create an instance template from the base image + +Create an instance that uses the base Node.js image without any ROM attached: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name node-exec \ + -m 512M \ + --image /node-code-exec:latest +``` + +```bash title="kraft" +kraft cloud instance create \ + --start \ + --name node-exec \ + -M 512Mi \ + /node-code-exec:latest +``` + + + +The output shows the instance details: + + + +```ansi title="unikraft" +metro: fra +name: node-exec +uuid: 96608ed2-45e0-4c8f-8269-5d8cd3e4b41a +state: starting +image: /node-code-exec +resources: + memory: 512MiB + vcpus: 1 +networks: +- uuid: 6f7a8b9c-0d1e-2f3a-4b5c-f6a7b8c9d0e1 + private-ip: 10.0.5.4 + mac: 12:b0:6c:3e:ab:95 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: node-exec + ├───────── uuid: 96608ed2-45e0-4c8f-8269-5d8cd3e4b41a + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├──────── image: oci://unikraft.io//node-code-exec@sha256:71487fd6196987cf65fb89eb84405cb796677aba177dabacf391f09618313328 + ├─────── memory: 512 MiB + ├─ private fqdn: node-exec.internal + └─── private ip: 10.0.5.4 +``` + + + +This instance is short-lived, since right before the server starts, it triggers a conversion into a template. +To check that the template is ready, run: + + + +```bash title="unikraft" +unikraft instances templates list +``` + +```bash title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED +fra node-exec template /node-code-exec 512MiB 1 5 seconds ago +``` + +```bash title="kraft" +kraft cloud instance template list +``` + +```bash title="kraft" +NAME IMAGE ARGS CREATED AT +node-exec oci://unikraft.io//node-code-exec@sha256:71487fd6196987cf65fb89eb84405cb796677aba177dabacf391f09618313328 5 seconds ago +``` + + + +### Package the ROMs + +Create and push the ROMs with the code (see `rom1/fs/rom.js` and `rom2/fs/rom.ts`): + + + +```bash title="unikraft" +unikraft build rom1/ --output /node-rom1:latest +unikraft build rom2/ --output /node-rom2:latest +``` + +```bash title="kraft" +kraft pkg \ + --name index.unikraft.io//node-rom1:latest \ + --rom ./fs \ + --rom-type erofs \ + --plat kraftcloud \ + --arch x86_64 \ + --push \ + rom1/ +kraft pkg \ + --name index.unikraft.io//node-rom2:latest \ + --rom ./fs \ + --rom-type erofs \ + --plat kraftcloud \ + --arch x86_64 \ + --push \ + rom2/ +``` + + + +### Create instances from the template with different ROMs attached + +Create a new instance from the template, attaching the first ROM: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name node-exec-rom1 \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ + --rom image=/node-rom1:latest,at=/rom \ + --template node-exec +``` + +```bash title="kraft" +# kraft does not support creating instances with attached ROMs, but you can use the API directly +curl -X POST "$UKC_METRO/instances" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "node-exec-rom1", + "template": { + "name": "node-exec" + }, + "autostart": true, + "service_group": { + "services": [ + { + "port": 443, + "destination_port": 8080, + "handlers": ["tls", "http"] + } + ] + }, + "scale_to_zero": { + "policy": "on", + "stateful": true, + "cooldown_time_ms": 1000 + }, + "roms": [ + { + "name": "js_function", + "image": "index.unikraft.io//node-rom1:latest", + "at": "/rom" + } + ] +}' +``` + + + +Create another instance from the same template, but with the second ROM attached: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name node-exec-rom2 \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ + --rom image=/node-rom2:latest,at=/rom \ + --template node-exec +``` + +```bash title="kraft" +# kraft does not support creating instances with attached ROMs, but you can use the API directly +curl -X POST "$UKC_METRO/instances" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "node-exec-rom2", + "template": { + "name": "node-exec" + }, + "autostart": true, + "service_group": { + "services": [ + { + "port": 443, + "destination_port": 8080, + "handlers": ["tls", "http"] + } + ] + }, + "scale_to_zero": { + "policy": "on", + "stateful": true, + "cooldown_time_ms": 1000 + }, + "roms": [ + { + "name": "ts_function", + "image": "index.unikraft.io//node-rom2:latest", + "at": "/rom" + } + ] +}' +``` + + + +List the instances and note their FQDN values: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node-exec-rom2 standby /node-code-exec 512MiB 1 nameless-wood-gw7pbnls.fra.unikraft.app 2 minutes ago +fra node-exec-rom1 standby /node-code-exec 512MiB 1 sparkling-dawn-syowlbtj.fra.unikraft.app 3 minutes ago +``` + +```bash title="kraft" +kraft cloud instance list +``` + +```bash title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +node-exec-rom2 nameless-wood-gw7pbnls.fra.unikraft.app standby standby oci://unikraft.io//node-code-exec@sha256:71487f... 512 MiB 1 6.98 ms +node-exec-rom1 sparkling-dawn-syowlbtj.fra.unikraft.app standby standby oci://unikraft.io//node-code-exec@sha256:71487f... 512 MiB 1 7.86 ms +``` + + + +Test both instances: + +```bash +curl https://sparkling-dawn-syowlbtj.fra.unikraft.app +curl https://nameless-wood-gw7pbnls.fra.unikraft.app +``` + +```text +Bye, World! +Auf Wiedersehen! +``` + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/openclaw.mdx b/pages/guides/openclaw.mdx new file mode 100644 index 00000000..cabaaeb5 --- /dev/null +++ b/pages/guides/openclaw.mdx @@ -0,0 +1,192 @@ +--- +title: "OpenClaw" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This guide explains how to create and deploy your very own OpenClaw gateway on Unikraft Cloud. +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/openclaw` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/openclaw/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + +```bash +unikraft login +``` + +When done, you may create the OpenClaw Unikraft Cloud image and deploy an instance from it like so: + +```bash +unikraft build . --output /openclaw:latest +unikraft run --metro fra \ + -m 4G \ + -p 18789:18789/tls \ + -p 2222:2222/tls \ + --scale-to-zero policy=on,cooldown-time=10000,stateful=true \ + -e PUBKEY="...." \ + --image /openclaw:latest +``` + +Make sure to replace `` with your username / org-name and to set your SSH public key as the `PUBKEY` environment variable above. + +The output shows the instance address and other details: + +```ansi +metro: fra +name: openclaw-8tosm +uuid: e2a6183a-721b-4145-bfaf-37a5f859bbc1 +state: running +image: /openclaw +runtime: + env: + PUBKEY: * +resources: + memory: 4GiB + vcpus: 1 +service: + uuid: 7ab20338-b04d-4869-947b-9433e21677b1 + name: divine-flower-bxsaapup + domains: + - fqdn: divine-flower-bxsaapup.fra.unikraft.app +networks: +- uuid: 2b0b120b-6ce5-4b19-ac4c-04ee8f11526e + private-ip: 10.0.12.97 + mac: 12:b0:0a:00:0c:61 +timestamps: + created: just now +``` + +In this case, the instance name is `openclaw-8tosm` and the address is `divine-flower-bxsaapup.fra.unikraft.app`. +These will be different for each run. + +You can now SSH into this instance and run the OpenClaw onboarding process. + +In order to SSH, you need to set up a tunnel that handles the TLS connection to the Unikraft Cloud instance. +This way, you have a non-TLS port that your SSH client can connect to: + +```bash +socat TCP-LISTEN:2222,reuseaddr,fork OPENSSL:divine-flower-bxsaapup.fra.unikraft.app:2222,verify=0 +``` + +Then connect to the instance via SSH using: + +```bash +ssh -l root localhost -p 2222 +``` + +You can list information about the instance by running: + +```bash +unikraft instances list +``` + +```ansi +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra openclaw-8tosm running /openclaw:latest 4GiB 1 divine-flower-bxsaapup.fra.unikraft.app 2 minutes ago +``` + +When done, you can remove the instance using: + +```bash +unikraft instances delete openclaw-8tosm +``` + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + +```bash +unikraft --help +``` + +## OpenClaw Setup + +Once you have SSH'd into your instance, you may run: + +```bash +openclaw onboard +``` + +This will set up your OpenClaw gateway on the instance. +You will be asked to provide your LLM's API key here. + +Once done, make note of your `gateway.auth.token` (henceforth referenced as ``) from `~/.openclaw/openclaw.json` + +```bash +cat ~/.openclaw/openclaw.json +``` + +Set `gateway.controlUi.allowedOrigins` in `~/.openclaw/openclaw.json`: + +```json +... + "gateway": { + ... + "controlUi": { + "allowedOrigins": [ + "https://proud-smoke-cjf0wro8.fra.unikraft.app:18789" + ] + }, + ... + }, +... +``` + +Replace the domain in the above URL with the address of your instance (noted earlier). + +Run the gateway: + +```bash +openclaw gateway run --bind lan +``` + +You may now access the web dashboard using the following URL: + +```ansi +https://
:18789?token= +``` + +Where `
` is your above noted address and `` is your above noted token. + +For security reasons, you will have to manually approve your web "device" in order to start using the web dashboard. +Create a new SSH connection to your OpenClaw instance: + +```bash +ssh -l root localhost -p 2222 +``` + +First, find your device ID: + +```bash +openclaw devices list +``` + +Look under the `Request` column. +Device IDs look like `cabd915e-137a-4bc4-b640-d0e507684d65` + +Finally, approve your device with: + +```bash +openclaw devices approve +``` + +Once this is done, refresh your OpenClaw web dashboard. + +You now have full access to your very own OpenClaw deployment on Unikraft Cloud! +{/* vale on */} diff --git a/pages/guides/overview.mdx b/pages/guides/overview.mdx index 5dec58e7..ebd42dd5 100644 --- a/pages/guides/overview.mdx +++ b/pages/guides/overview.mdx @@ -35,6 +35,7 @@ Deploy HTTP servers written in your language of choice on Unikraft Cloud. ### Go - [Go HTTP Server](/guides/httpserver-go1.21) +- [Go and Redis HTTP Server](/guides/httpserver-go1.22-redis) ### Java @@ -50,9 +51,9 @@ Deploy HTTP servers written in your language of choice on Unikraft Cloud. - [Puppeteer HTTP Server](/guides/httpserver-node-express-puppeteer) - [Prisma HTTP Server](/guides/httpserver-prisma-expressjs4.19-node18) - [Next.js HTTP Server](/guides/httpserver-node21-nextjs) -- [Remix HTTP Server](/guides/httpserver-node21-remix) +- [React Router HTTP Server](/guides/httpserver-node22-react-router) - [SolidJS HTTP Server](/guides/httpserver-node21-solid-start) -- [SvelteKit HTTP Server](/guides/httpserver-node21-sveltekit) +- [SvelteKit HTTP Server](/guides/httpserver-node22-sveltekit) - [Vite HTTP Server (Nginx)](/guides/httpserver-nginx-vite-vanilla) - [Vite HTTP Server (Node)](/guides/httpserver-node-vite-vanilla) - [Vite SSR HTTP Server (Node)](/guides/httpserver-node-vite-ssr-vanilla) @@ -76,6 +77,7 @@ Deploy HTTP servers written in your language of choice on Unikraft Cloud. - [FastAPI HTTP Server](/guides/httpserver-python3.12-fastapi-0.121.3) - [Flask HTTP Server](/guides/httpserver-python3.12-flask3.0) - [Flask and SQLite HTTP Server](/guides/httpserver-python3.12-flask3.0-sqlite) +- [Flask + Redis HTTP Server](/guides/httpserver-flask-redis) ### Ruby @@ -84,9 +86,9 @@ Deploy HTTP servers written in your language of choice on Unikraft Cloud. ### Rust - [Rust HTTP Server](/guides/httpserver-rust1.91) -- [Rust (Actix Web) HTTP Server](/guides/httpserver-rust1.87-actix-web4) +- [Rust (Actix Web) HTTP Server](/guides/httpserver-rust1.88-actix-web4) - [Rust (Leptos + Trunk) HTTP Server](/guides/httpserver-rust-trunkrs-leptos) -- [Rust (Rocket) HTTP Server](/guides/httpserver-rust1.81-rocket0.5) +- [Rust (Rocket) HTTP Server](/guides/httpserver-rust1.88-rocket0.5) - [Rust (Tokio) HTTP Server](/guides/httpserver-rust1.75-tokio) ## Web Servers & Reverse Proxies @@ -98,6 +100,7 @@ Serve static content or route traffic with popular web server and proxy tools. - [HAProxy](/guides/haproxy) - [Skipper](/guides/skipper0.18) - [Traefik](/guides/traefik) +- [Tyk](/guides/tyk) ## Databases & Caching @@ -127,12 +130,20 @@ Run WebAssembly workloads on Unikraft Cloud. - [Spin](/guides/spin-wagi-http) - [Wazero](/guides/wazero-import-go) +## Code Execution & Build Environments + +Package user code as auxiliary ROMs and run it on a generic base image. + +- [Build/Test Environments with ROMs](/guides/build-environments) +- [Node.js Code Execution with ROMs](/guides/node-code-execution) + ## AI & MCP Servers Deploy Model Context Protocol (MCP) servers and AI-enabled services. - [Simple MCP Server](/guides/mcp-server-simple) - [ArXiv MCP Server](/guides/mcp-server-arxiv) +- [OpenClaw](/guides/openclaw) ## Observability & Monitoring @@ -155,14 +166,19 @@ Access remote development environments and file transfer services. Deploy complete web applications and content management systems. -- [WordPress](/guides/wordpress-all-in-one) +- [WordPress (All-in-One)](/guides/wordpress-all-in-one) +- [WordPress with Nginx and MariaDB](/guides/wordpress) - [Ruby on Rails](/guides/ruby3.2-rails) +- [Phoenix with PostgreSQL](/guides/phoenix-postgres) +- [Flask with MongoDB](/guides/nginx-flask-mongo) - [Hugo](/guides/hugo0.122) ## Browser Automation Run browser automation and end-to-end testing workloads on Unikraft Cloud. +- [Chromium CDP](/guides/chromium-cdp) +- [Chromium CDP with Token Authentication](/guides/chromium-cdp-auth) - [Playwright Chromium (Node)](/guides/node-playwright-chromium) - [Playwright Firefox (Node)](/guides/node-playwright-firefox) - [Playwright WebKit (Node)](/guides/node-playwright-webkit) @@ -176,6 +192,7 @@ Multiplayer and WebSocket-based applications. - [Agar.io Clone](/guides/node18-agario) - [Wings.io Clone](/guides/node18-wingsio) - [Node AllKaraoke](/guides/node24-karaoke) +- [Minecraft](/guides/minecraft) ## Integration & Automation diff --git a/pages/integrations/karpenter.mdx b/pages/integrations/karpenter.mdx new file mode 100644 index 00000000..72bdbd8c --- /dev/null +++ b/pages/integrations/karpenter.mdx @@ -0,0 +1,166 @@ +--- +title: Karpenter +navigation_icon: scaling +--- + +{/* vale off */} +:::caution[**Limited Access**] +The Karpenter provider for Unikraft is a preview feature available as part of enterprise plans. +The behavior described here reflects the current implementation and may change before general availability. +To try the preview, reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). +::: +{/* vale on */} + +[Kraftlet](/integrations/kubernetes) lets a Kubernetes cluster run Pods on Unikraft Cloud, but the cluster still needs nodes to schedule those Pods onto. +The Karpenter provider for Unikraft creates them on demand. + +When a Pod has nowhere to run, the provider launches a machine running the Unikraft Cloud platform and installs Kraftlet against that machine. +Then the provider joins it to the cluster as a node. +Once the capacity goes unused, the provider removes the node and the machine again. + +The provider builds on [Karpenter](https://karpenter.sh), the Kubernetes node autoscaler, so the objects and the workflow are the ones Karpenter users already know. + +## How a node comes up + +{/* vale off */} +
+
+ + + An unschedulable Pod triggers a NodeClaim, the Karpenter provider reads the + nodeclass and creates a machine through the cloud API, then installs Kraftlet, + which connects to the machine and registers it as a node that runs the Pod. + + + + + + + + + {/* Cluster boundary */} + + Kubernetes cluster (EKS, GKE, AKS) + + {/* Control plane */} + + Control plane + + + etcd + + + Kubernetes + API + + + + {/* Kubernetes objects */} + + NodeClaim + + + Pod + (unschedulable) + + {/* Unikraft components */} + + Karpenter provider + for Unikraft + + + Kraftlet + + {/* Outside the cluster */} + + Cloud API + (AWS, GCP) + + + Unikraft Cloud + machine + + {/* Steps */} + + + + + + + + + + + + + 1. Triggers a NodeClaim + 2. Reads the + UnikraftNodeClass + 3. Creates a + machine + 4. Starts the + machine + 5. Installs Kraftlet + with its Helm chart + 6. Connects to the + platform API + 8. Runs the Pod + 7. Registers as a node + + +
+
+ Karpenter turns unschedulable Pods into machines, and Kraftlet turns each machine into a node. +
+
+{/* vale on */} + +No kubelet runs on the machine. +Kraftlet takes that role from inside your cluster and reaches the machine over the network, so each machine needs an endpoint your cluster can reach. + +A node joins the cluster only once the platform on its machine reports healthy. + +## What it's good for + +Without it, a Kraftlet cluster scales to a limited, static capacity. +The provider makes Unikraft Cloud capacity elastic from inside Kubernetes, driven by the autoscaler Kubernetes users already run. +Nodes arrive when Pods need them and go away once they empty, so a deployment that swings between nothing and thousands of instances holds nodes only while it runs. +It also keeps a full node from stalling the cluster. +A Pod that no longer fits goes back to its controller and lands on a node with room, or on a new one, as [Pod rescheduling](/integrations/kubernetes#pod-rescheduling) describes. + +## What a setup looks like + +The provider needs the following objects in the cluster: + +1. **A `UnikraftNodeClass`** describes the machine behind a node: its image, where it runs, how it gets its DNS records and certificate, and the Kraftlet release that fronts it. + +2. **A `NodePool`** specifies which machine sizes to pick, how many nodes to allow, and when to remove the ones that empty. + +3. **Your workloads** reach the pool through a toleration and a node selector, the same way they reach a fixed Kraftlet node. + +Everything the [Kubernetes integration](/integrations/kubernetes) describes then applies to the Pods that land there, including services, volumes, ROMs, plugins, and annotations. + +Unikraft provides the machine images, and the setup steps for your cluster. +The enterprise documentation covers the configuration in full. + +## Requirements and limitations + +* The provider needs a Kubernetes cluster and a cloud account of your own. + At the moment, the provider can provision machines on AWS and GCP. +* Machines launch as on-demand instances, so a NodePool that asks for `karpenter.sh/capacity-type: spot` gets none. +* A node advertises the platform quotas of its machine rather than the size of the machine underneath. + You can cap the total vCPUs, memory, and volume storage a node advertises, and [scale-to-zero](/features/scale-to-zero) lets you overcommit those caps by a wide margin. +* Changing a nodeclass replaces the machines made from it rather than reconfiguring them in place. diff --git a/pages/integrations/kubernetes.mdx b/pages/integrations/kubernetes.mdx index 9ed53ffe..f3f96f07 100644 --- a/pages/integrations/kubernetes.mdx +++ b/pages/integrations/kubernetes.mdx @@ -92,6 +92,12 @@ With `kraftlet.replication.enabled=true`, each replica registers a node named af Select on `unikraft.com/virtual-kubelet` rather than on `kubernetes.io/hostname` to spread Pods across every replica. ::: +## Autoscaling the cluster + +The chart above registers one node per Kraftlet, against capacity that already exists. +To let the cluster create that capacity on demand, add the [Karpenter provider for Unikraft](/integrations/karpenter). +It launches a machine when a Pod has nowhere to run, installs Kraftlet against it, joins it as a node, and removes both again once the capacity goes unused. + ## Examples Below are examples of Kubernetes configurations that define Unikraft Cloud apps through Kubernetes concepts. @@ -518,9 +524,12 @@ It derives the Pod phase from the state of the backing instances: | `standby` | `Running`, or `Pending` while the platform reports a failure | | `stopped` after a clean shutdown | `Succeeded` | | `stopped` after a fault or a failed image pull | `Failed` | +| Any state, once the platform stopped the instance for insufficient quota | `Failed` | The container status carries the detail behind a failure. -An instance the platform stopped for running out of memory surfaces as `OOMKilled` with exit code 137, a failed image pull surfaces as `ErrImagePull`, and any other platform-side stop surfaces as `PlatformError`. +An instance the platform stopped for running out of memory surfaces as `OOMKilled` with exit code 137. +A failed image pull surfaces as `ErrImagePull`, a stop for insufficient quota surfaces as `QuotaExceeded` with exit code 1, and any other platform-side stop surfaces as `PlatformError`. +Note that the running quota is dynamic, so on the next request, an instance which failed to start with `QuotaExceeded` might succeed. An instance with exactly one network interface also contributes its private IP as the Pod IP. Kraftlet records what it does on the Pod as events: @@ -559,6 +568,26 @@ It also maps quota exhaustion and platform health onto node conditions: An unreachable platform turns `Ready` to false, so the scheduler stops placing new Pods on the node until the platform answers again. +### Pod rescheduling + +Idle instances go to `standby` and hold no live memory, which lets a node carry far more Pods than its live memory quota can run at once. +Resuming one can fail once the quota is full, and the platform then leaves the instance in a stopped or scaled-to-zero state rather than starting it. + +Kraftlet reports such a Pod as `Failed` with the reason `QuotaExceeded`, and then deletes the Pod so the controller that owns it schedules a replacement. +The node reports `MemoryPressure` as soon as its next quota refresh finds the live memory quota exhausted. +That condition becomes a `node.kubernetes.io/memory-pressure:NoSchedule` taint, which your Pods don't tolerate, so the replacement stays off that node until live memory frees up. +On a cluster that [autoscales with Karpenter](/integrations/karpenter), the replacement lands on another node, or on a node the autoscaler creates for it. + +Keep these in mind when running your workloads: + +* Run Pods under a Deployment, ReplicaSet, StatefulSet or Job, since Kraftlet reclaims only a Pod that has a controller behind it. + It leaves a bare Pod `Failed` in place, because deleting that Pod would leave nothing behind to replace it. +* Reclaiming a Pod runs the same teardown as a delete, so its instances and Pod-scoped volumes (including `emptyDir` and `hostPath` volumes) go with it. + Volumes behind PersistentVolumeClaims remain until you delete their claims. + +Kraftlet reclaims failed Pods by default. +Set `kraftlet.enableFailedPodReclaim=false` to leave them in place instead. + ### Resource lifecycle Kraftlet adds the `cloud.unikraft.v1/resources` finalizer to every Pod it accepts, so a delete only completes once Kraftlet removes the Unikraft Cloud resources. @@ -613,6 +642,11 @@ Kraftlet supports the following Unikraft Cloud platform features on Kraftlet-man Instances can carry addresses and TAP devices that you choose instead of ones from the platform pool. Kraftlet drives this through CNI plugins. +* [**Annotations**](/features/annotations) + + Instance metadata that also reaches the guest. + Kraftlet writes the [CNI result](/features/annotations#guest-network-configuration-with-cni) that a guest configures its own interfaces from. + ## Custom networking with CNI {/* vale off */} @@ -633,6 +667,11 @@ Instances join the same networks as native Pods and follow the same address mana A companion component, `remote-cni`, exposes the plugins over gRPC for clusters where Kraftlet runs apart from the machine that hosts the instances. +An instance's `network_interfaces` only accept IPv4 addresses. +Therefore, the Kraftlet hands the rest of a plugin result—IPv6 addresses, in practice—to the guest through the [`unikraft.com/cni` annotation](/features/annotations#guest-network-configuration-with-cni), and the guest configures them on its own interfaces. +Kraftlet does this for the Pod's default network, a secondary network contributes its IPv4 addresses alone. +You can write the same annotation yourself on an instance you create through the API, for an [interface without an address](/features/custom-network-configuration#interfaces-without-an-address) whose addressing an IPAM system owns. + ## Annotations Kraftlet reads the following annotations from Pod and Service objects to configure Unikraft Cloud resources. @@ -705,6 +744,7 @@ The values below cover the settings most deployments touch. | `kraftlet.replication.enabled` and `kraftlet.replication.replicas` | `false` and `1` | Run more than one Kraftlet, each registering its own node. | | `kraftlet.enablePvcWatcher` | `false` | Manage the lifecycle of `ukc-volume` claims. | | `kraftlet.enableInstanceFqdnAnnotations` | `false` | Annotate Pods with the FQDNs of their instances. | +| `kraftlet.enableFailedPodReclaim` | `true` | Delete a Pod that fails because the live memory quota is full, so its controller replaces it. | | `kraftlet.podStatusUpdateInterval` | `15s` | How often Kraftlet refreshes Pod status from the platform. | | `kraftlet.podSyncWorkers` | `1` | Number of Pod reconcile workers. | | `kraftlet.logLevel` and `kraftlet.logType` | `info` and `json` | Log verbosity and format. | diff --git a/pages/integrations/sdks/js.mdx b/pages/integrations/sdks/js.mdx new file mode 100644 index 00000000..3a318889 --- /dev/null +++ b/pages/integrations/sdks/js.mdx @@ -0,0 +1,451 @@ +--- +title: JavaScript SDK +navigation_icon: code +--- + +{/* vale off */} +{/* The prose below comes from the SDK README verbatim. */} +`@unikraft/cloud` is the official JavaScript/TypeScript SDK for the [Unikraft Cloud](https://unikraft.com) API. + +## Installation + +```bash +npm install @unikraft/cloud +``` + +Requires Node.js 18 or later. + +## Quickstart + +```ts title="quickstart.ts" +import { UnikraftCloud } from "@unikraft/cloud"; + +const ukc = new UnikraftCloud({ token: process.env.UKC_TOKEN }); + +// By default the client is account-wide: every metro is asked in parallel and +// the results are merged, each tagged with the metro it came from. +for await (const inst of ukc.instances.list({ details: true })) { + console.log(inst.metro, inst.name, inst.state); +} + +// Creating a resource happens in one metro, so name it. +const fra = ukc.metro("fra"); +const instance = await fra.instances.create({ + image: "nginx:latest", + autostart: true, + memory_mb: 256, + service_group: { + services: [{ port: 443, handlers: ["tls", "http"], destination_port: 80 }], + }, +}); + +// Operations chain off a reference, so a ref is written once. +await fra.instances.get({ name: instance.name! }).wait({ state: "running" }); +await ukc.instances.get({ name: instance.name! }).suspend(); +await fra.instances.delete([{ uuid: instance.uuid! }]); +``` + +## Authentication + +Pass a bearer `token` to the constructor, or set the `UKC_TOKEN` environment variable. +Create a token in the [Unikraft Cloud dashboard](https://console.unikraft.cloud). + +{/* vale on */} + +## Client options + +The `UnikraftCloud` constructor takes one configuration object, and every field is optional: + +| Option | Description | +| -------------------------- | -------------------------------------------------------------------------------------------------- | +| `token` | Bearer token, which falls back to the `UKC_TOKEN` environment variable | +| `metro` | Pin the client to one metro (for example, `fra`), or to a full `http(s)://` base address, which falls back to `UKC_METRO` | +| `metros` | The metros operations cover: `"all"` (the default), one metro, or a list | +| `baseUrl` | Explicit platform API base address, which overrides `metro` | +| `controlPlaneUrl` | Override the control plane API base address | +| `fetch` | Custom `fetch` implementation | +| `headers` | Extra headers sent with every request | +| `userAgent` | Override the `User-Agent` header | +| `proxyFromEnv` | Honour the proxy environment variables on Node, which defaults to `true` | + +`metros` takes precedence over `metro`, which stays the target for operations that need exactly one metro. +Set neither, and the scope covers every metro the account can reach, with `fra` as the target for single-metro operations. +The base address of a metro is `https://api..unikraft.cloud`. +A `baseUrl`, or a `metro` that already is an address, pins the client to that one endpoint, as described under [self-hosted and staging deployments](#self-hosted-and-staging-deployments). + +{/* vale off */} +## The two layers: porcelain and plumbing + +The SDK is explicitly two layers, and you choose per call which one you are in: + +| Layer | Where | What you get | +| ------------- | ----------------------------------------------------------- | ----------------------------------------------------------------------------- | +| **Porcelain** | `@unikraft/cloud` — `ukc.instances`, `ukc.volumes`, … | Short verbs, no envelope, auto-pagination, metro fan-out, chainable handles | +| **Plumbing** | `@unikraft/cloud/api/platform`, `.../api/controlplane` | The OpenAPI specification as written: `operationId` methods, raw envelope, one metro per call | + +The porcelain layer **holds** a plumbing client rather than extending one, so the two never blur together — and the raw client is always one property away: + +```ts +// Porcelain: unwrapped, metro-aware. +const inst = await ukc.instances.get({ name: "web" }); + +// Plumbing, from the same client and credentials. +const res = await ukc.api.platform.instances.getInstances({ count: 10, details: true }); +res.status; // "success" +res.data?.instances; + +// Plumbing, per resource (the escape hatch for anything not wrapped yet). +await ukc.instances.api.getInstanceLogs({ name: ["web"] }); + +// Plumbing, standalone — no porcelain involved. +import { InstancesApi, PlatformApi } from "@unikraft/cloud/api/platform"; +import { ControlPlaneApi } from "@unikraft/cloud/api/controlplane"; + +const api = new PlatformApi({ + baseUrl: "https://api.fra.unikraft.cloud", + token: process.env.UKC_TOKEN, +}); +await api.instances.getInstances({ count: 10 }); +``` + +Plumbing clients are metro-scoped by construction: they talk to whatever `baseUrl` names, and a single call can be redirected with `{ baseUrl }`. +Fanning out across metros is the porcelain layer's job. + +### Two APIs: platform and control plane + +Both Unikraft Cloud APIs defined by our [OpenAPI](https://github.com/unikraft-cloud/openapi) specification are wrapped: + +- **Platform** — metro-scoped resources (instances, volumes, services, certificates, autoscale, images). + Wrapped idiomatically at the top level; raw under `@unikraft/cloud/api/platform` and `ukc.api.platform`. +- **Control plane** — the global (non-metro) API for account, metros, images and self-hosted nodes. + Raw only, under `@unikraft/cloud/api/controlplane` and `ukc.api.controlplane`. + +```ts +for (const metro of (await ukc.api.controlplane.metros.listMetros()).data?.metros ?? []) { + console.log(metro.iata_code, metro.endpoint); +} +``` + +## Metros: one account, many regions + +The platform API is per-metro, but an account's instances are spread across them. +The porcelain layer treats **the metros in scope** as one namespace: reads fan out concurrently and are merged, and every result carries the `metro` it came from. + +```ts +const ukc = new UnikraftCloud({ token }); + +// Every metro the account can reach (discovered once, then cached). +for await (const inst of ukc.instances.list()) console.log(inst.metro, inst.name); + +// Explicit scopes — these also skip metro discovery entirely. +ukc.metro("fra").instances.list(); // one metro +ukc.metros(["fra", "dal"]).instances.list(); // several +ukc.instances.list({ metros: ["fra", "dal"] }); // just this call +ukc.instances.list({ metros: "all" }); // back to everything + +// What the account can reach. +for (const { metro, baseUrl } of await ukc.availableMetros()) console.log(metro, baseUrl); +``` + +Set the default scope at construction, too: + +```ts +new UnikraftCloud({ token }); // all metros (default) +new UnikraftCloud({ token, metro: "fra" }); // one metro +new UnikraftCloud({ token, metros: ["fra", "dal"] }); // several +``` + +`UKC_METRO` acts like `metro:` — setting it pins the client to that metro. + +| Code | Location | +| ----- | ------------------- | +| `fra` | Frankfurt, DE | +| `dal` | Dallas, TX, USA | +| `sin` | Singapore | +| `was` | Washington, DC, USA | +| `sfo` | San Francisco, USA | + +Metro discovery asks the control plane and trusts the endpoint it reports, so new [metros](/platform/metros) work without an SDK upgrade. +`KNOWN_METROS` lists the ones known when this version was published. + +### Rules the fan-out follows + +- **Reads** cover the whole scope. + Pages are interleaved in arrival order, so a slow metro never holds up a fast one. +- **A name identifies a resource within a metro.** + The same name can exist in several metros at once — usually because you deployed the same thing everywhere — so a name plus a wide scope may match more than one resource. + See [Names across metros](#names-across-metros). +- **`create` never fans out.** + It needs one metro: the client's, or the default metro (`metro:` / `UKC_METRO` / `fra`) when the scope is wider. +- **Bulk operations are bounded by the scope.** + Refs are located first and one call goes to each metro that matched, so `delete([{ name: "web" }])` under a wide scope deletes every `web` in scope. + Narrow the scope or qualify the ref to act on one. +- **The control plane is global**, so it is unaffected by scope. + +### Names across metros + +Names are scoped to a metro, so the same name can name a different resource in every metro. +Three ways to say what you mean: + +```ts +// 1. Qualify the ref. No search, no discovery — one request. +await ukc.instances.get({ name: "web", metro: "fra" }).suspend(); + +// 2. Narrow the scope, which qualifies every ref through it. +await ukc.metro("fra").instances.get({ name: "web" }).suspend(); + +// 3. Address every metro holding it, on purpose. +await ukc.instances.each({ name: "web" }).suspend(); // one result per metro +``` + +`{ uuid }` refs never need qualifying: a UUID identifies one resource wherever it lives. + +`get()` insists on exactly one match, because the next thing you write might be a mutation. +When a name matches in several metros it throws an `AmbiguousRefError` carrying the matches, so recovering costs no further requests: + +```ts +import { AmbiguousRefError } from "@unikraft/cloud"; + +try { + await ukc.instances.get({ name: "web" }).suspend(); +} catch (err) { + if (err instanceof AmbiguousRefError) { + err.metros; // ["fra", "dal", "sin"] + err.matches; // the instances themselves, each tagged with .metro + } +} +``` + +`each(ref)` is the deliberate plural. +It resolves the matches once, then runs each operation in the metro that holds it: + +```ts +const web = ukc.instances.each({ name: "web" }); + +await web.where(); // ["fra", "dal", "sin"] +await web.size(); // 3 +for (const inst of await web) console.log(inst); // the instances +await web.suspend(); // one result per metro +await web.edit().set({ memory_mb: 512 }).apply(); +``` + +Set operations return arrays and follow the same partial-failure rule as reads: successes are returned on the thrown `MetroFanoutError` as `err.results`. +`each()` exists on instances, volumes, services and certificates. + +### Partial failure + +A metro that is unreachable does not throw away the rest of the answer. +Healthy metros are drained first, then a `MetroFanoutError` naming the failures is thrown: + +```ts +import { MetroFanoutError } from "@unikraft/cloud"; + +try { + for await (const inst of ukc.instances.list()) use(inst); // fra, dal, was delivered +} catch (err) { + if (err instanceof MetroFanoutError) { + err.message; // "1 of 4 metros failed: sin (503)" + err.failures; // [{ metro: "sin", error: UnikraftCloudError }] + } +} +``` + +For a bulk operation, which cannot yield as it goes, the results that did succeed are attached to the thrown error as `err.results`. + +## Chainable handles + +Single-resource operations return a **handle**: a lazily-evaluated reference to one resource in one metro. +Awaiting a handle gives the resource; calling an operation on it returns another handle: + +```ts +await ukc.instances.get({ name: "web" }); // -> Instance +await ukc.instances.get({ name: "web" }).suspend(); // -> the suspended instance +await ukc.instances.get({ name: "web" }).update({ memory_mb: 512 }); + +await ukc.metro("fra").instances + .create({ image: "nginx:latest" }) + .wait({ state: "running", timeoutSeconds: 30 }) + .logs({ offset: -4096 }); + +await ukc.volumes.get({ name: "data" }).attach({ attach_to: { name: "web" }, at: "/data" }); +``` + +Nothing is sent until a handle is awaited or chained onto, and each step runs at most once however many times you await it. +What that costs depends on the scope: + +| Ref and scope | `get(ref).suspend()` | +| ------------------------------ | ----------------------------------------------------------------- | +| `{ name, metro }`, any scope | 1 request — the suspend. The ref says where. | +| `{ name }`, one metro in scope | 1 request — the scope says where. | +| `{ name }`, many metros | Locate first (one concurrent read per metro), then suspend where it lives — or throw `AmbiguousRefError` if several match. | + +Handles also answer where they landed, and mutating steps return what that endpoint reports (`suspend()` resolves to `{ uuid, name, state, previous_state }`, not a full instance): + +```ts +const web = ukc.instances.get({ name: "web" }); +await web.where(); // "dal" +await web.resolve(); // { ref: { name: "web" }, metro: "dal", baseUrl: "..." } +``` + +Every idiomatic method is also available in non-chained form (`ukc.instances.logs({ name: "web" }, { offset: -4096 })`), which is exactly shorthand for `get(ref).logs(...)`. + +## Updating a resource + +The API models an update as a list of `{ prop, op, value }` triples, with `value` typed `unknown`. +That is the plumbing. +Idiomatically you write a **patch object** and the op is worked out for you — a value sets it, `null` removes it, and an omitted (or `undefined`) property is left alone, following JSON Merge Patch: + +```ts +await ukc.instances.get({ name: "web" }).update({ memory_mb: 512, vcpus: 2 }); +await ukc.instances.get({ name: "web" }).update({ env: { LOG_LEVEL: "debug" } }); +await ukc.instances.get({ name: "web" }).update({ autokill: null }); // remove it +await ukc.volumes.get({ name: "data" }).update({ size_mb: 2048 }); +await ukc.services.get({ name: "web" }).update({ soft_limit: 5, hard_limit: 20 }); +``` + +Every property is typed, so `memory_mb: "512"` and `tags: "prod"` no longer compile. +`image` takes the same string shorthand as `create`. + +When `set` is not what you mean — merging into a property, or removing individual members — stage the operations with `edit()` and send them as one request: + +```ts +await ukc.instances.get({ name: "web" }).edit() + .set({ memory_mb: 512 }) + .add({ env: { LOG_LEVEL: "debug" }, tags: ["prod"] }) + .del({ env: ["OLD_FLAG"], tags: ["staging"] }) + .apply(); + +// `null` in del() removes the property outright, not just some members. +await ukc.services.edit({ name: "web" }).del({ domains: null }).apply(); +``` + +`apply()` returns a handle like any other operation, so chaining continues: + +```ts +await ukc.metro("fra").instances + .edit({ name: "web" }) + .set({ memory_mb: 1024 }) + .apply() + .wait({ state: "running" }); +``` + +Both forms are one request, and both are available with a ref instead of a handle (`ukc.instances.update({ name: "web" }, { memory_mb: 512 })`, `ukc.instances.edit({ name: "web" })`). +The raw triples still work as an escape hatch: `update({ name: "web" }, [{ prop: "memory_mb", op: "set", value: 512 }])`. + +## Refs + +Every operation on an existing resource takes a **ref**: either `{ name }` or `{ uuid }`, never both. +The API validates each identifier it is given, so a name sent in the `uuid` filter fails with `Invalid uuid ''` — the ref makes you state which kind you hold, and only that field is sent. + +```ts +await ukc.instances.get({ name: "web" }); +await ukc.instances.get({ uuid: "550e8400-e29b-41d4-a716-446655440000" }); + +// A name belongs to a metro, so it can be qualified — see "Names across metros". +await ukc.instances.get({ name: "web", metro: "fra" }); + +// Bulk operations take one ref or an array of them. +await ukc.instances.stop([{ name: "web" }, { uuid: "550e8400-e29b-41d4-a716-446655440000" }]); +``` + +## Idiomatic methods + +| Resource | Methods | +| ------------------ | ----------------------------------------------------------------------------------------------- | +| `ukc.instances` | `create`, `get`, `each`, `list`, `update`, `edit`, `delete`, `start`, `stop`, `suspend`, `wait`, `metrics`, `history`, `logs` | +| `ukc.volumes` | `create`, `get`, `each`, `list`, `update`, `edit`, `delete`, `attach`, `detach` | +| `ukc.services` | `create`, `get`, `each`, `list`, `update`, `edit`, `delete` | +| `ukc.certificates` | `create`, `get`, `each`, `list`, `update`, `delete` | +| `ukc.users` | `quotas`, `quotasByUuid`, `add` | + +Handles add the per-resource operations: `refresh`, `start`, `stop`, `suspend`, `delete`, `update`, `edit`, `wait`, `logs`, `metrics`, `history` on an instance; `attach`, `detach`, `update`, `edit`, `delete` on a volume; `update`, `edit` and `delete` on a service group. + +Anything not listed — autoscale, the image registry, node information, the whole control plane — is reachable raw via `ukc.api.platform.*` and `ukc.api.controlplane.*`: + +```ts +await ukc.api.platform.autoscale.getAutoscaleConfigurations({ uuid: ["sg1"] }); +await ukc.api.platform.images.getImages({}); +``` + +See [`examples/`](https://github.com/unikraft-cloud/js-sdk/tree/HEAD/examples) for complete programs. + +## Errors + +Any network failure or non-2xx response throws an `UnikraftCloudError`: + +```ts +import { UnikraftCloudError } from "@unikraft/cloud"; + +try { + await ukc.metro("fra").instances.get({ name: "does-not-exist" }); +} catch (err) { + if (err instanceof UnikraftCloudError) { + console.error(err.kind, err.status, err.message, err.errors); + } +} +``` + +`err.kind` is `"http"`, `"network"`, `"parse"`, or `"fanout"` (a multi-metro operation that partly failed, or an unusable scope — see `MetroFanoutError` above). + +## Self-hosted and staging deployments + +`metro` (and `UKC_METRO`) also accepts a full `http(s)://` base URL, which is used verbatim instead of being expanded into `https://api..unikraft.cloud`. +A trailing `/v1` is dropped, since every operation path already carries it: + +```bash +export UKC_METRO=https://api.staging.example.internal +``` + +A named endpoint is the only endpoint there is: no metro discovery is attempted and no hostnames are invented, whatever the scope says. +Point the control plane at a matching deployment with `controlPlaneUrl`: + +```ts +const ukc = new UnikraftCloud({ + token, + metro: "https://api.staging.example.internal", + controlPlaneUrl: "https://controlplane.staging.example.internal", +}); +``` + +## Runtime support + +`fetch` is used from the global scope. +On Node.js 18+ it is built in. +For older runtimes, or to customise transport, pass your own: + +```ts +import { UnikraftCloud } from "@unikraft/cloud"; +const ukc = new UnikraftCloud({ token, fetch: myFetch }); +``` + +## Debugging with a proxy (MITM) + +On Node, the client honours the standard proxy environment variables so you can route traffic through a man-in-the-middle proxy such as [mitmproxy](https://mitmproxy.org), Charles, or Proxyman without any code change: + +```bash +npm install undici # optional peer dependency, enables proxy support +export HTTPS_PROXY=http://127.0.0.1:8080 +export NODE_EXTRA_CA_CERTS=~/.mitmproxy/mitmproxy-ca-cert.pem # trust the proxy CA +node your-script.js +``` + +`HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY` and `NO_PROXY` are all recognised, in upper- or lower-case. +Node's global `fetch` ignores these by itself, so the SDK applies them via undici's `EnvHttpProxyAgent`, imported lazily only when a proxy variable is set. +If `undici` is not installed, requests proceed unproxied and a one-time warning is logged. + +Opt out per client with `new UnikraftCloud({ proxyFromEnv: false })`. +Proxy support is Node-only; browsers and Deno ignore these variables. + +{/* vale on */} + +## Source + +The SDK source lives at [github.com/unikraft-cloud/js-sdk](https://github.com/unikraft-cloud/js-sdk), and the published package at [npmjs.com/package/@unikraft/cloud](https://www.npmjs.com/package/@unikraft/cloud). + +## Learn more + +* [Go SDK](/integrations/sdks/go): the client library for the same platform API in Go. +* [Metros](/platform/metros): the regions that the SDK fans out across. +* [Instances](/platform/instances): create and manage the instances that the SDK drives. +* [Services](/platform/services): the load-balanced networking that `ukc.services` configures. +* Unikraft Cloud's [REST API reference](/api/platform/v1), which the plumbing layer mirrors. diff --git a/pages/integrations/sdks/plugin.mdx b/pages/integrations/sdks/plugin.mdx new file mode 100644 index 00000000..13307ea9 --- /dev/null +++ b/pages/integrations/sdks/plugin.mdx @@ -0,0 +1,599 @@ +--- +title: Plugin SDK +navigation_icon: puzzle +--- + +The Unikraft Cloud Plugin SDK is a small framework for building [plugins](/features/plugins) in Go. +A plugin runs as a sidecar HTTP server next to your app inside an instance, and loads from its own [ROM image](/features/roms). +It answers the requests that the platform forwards to it over a per-instance, authenticated endpoint. + +Every plugin meets the same platform contract. +It parses the `init` command line, adopts the socket the platform hands it, and decodes the JSON `config` from `STDIN`. +It also stands up a router with middleware and drains in-flight requests on shutdown. +The SDK takes care of each step. +You write a configuration struct and a route registration function, and `pluginsdk.Main` does the rest. +A complete plugin takes about 25 lines. + +## Installation + +```bash +go get unikraft.com/cloud/pluginsdk +``` + +Requires Go 1.26.4 or later. + +## Quickstart + +```go title="main.go" +package main + +import ( + "context" + + "github.com/gin-gonic/gin" + + "unikraft.com/cloud/pluginsdk" +) + +// Config takes its values from the platform `config` JSON on STDIN, through +// each field's `json` tag. A --greeting flag overrides that value; an `env` +// tag would bind an environment variable as well. +type Config struct { + Greeting string `json:"greeting" default:"Hello"` +} + +func main() { + pluginsdk.Main(&pluginsdk.Plugin[Config]{ + Name: "example", + Register: func(ctx context.Context, cfg *Config, engine *gin.Engine) error { + engine.GET("/hello", func(g *gin.Context) { + data := gin.H{"message": cfg.Greeting + ", world!"} + + env, code, _ := pluginsdk.OK(&data) + g.JSON(code, env) + }) + + return nil + }, + }) +} +``` + +That covers the whole plugin. +`pluginsdk.Main` takes these steps in order: + +1. Parses the command line: `--api_fd`, `--api-addr`, `--log-level`, `--log-type`, and one flag per field of `Config`. +2. Reads the JSON `config` from `STDIN` and decodes it into `Config`. +3. Configures logging, then runs the optional `Setup` hook. +4. Adopts the `--api_fd` socket as its listener. +5. Builds the [gin](https://gin-gonic.com) engine with the default middleware and calls `Register`. +6. Serves until `SIGINT` or `SIGTERM` arrives, then drains and shuts down. + +Run the plugin locally with `--api-addr` in place of `--api_fd`: + +```bash +echo '{"greeting":"Hey"}' | go run . --api-addr :8080 --log-type text +curl -s localhost:8080/hello +``` + +```json +{"status":"success","data":{"message":"Hey, world!"}} +``` + +## Configuration + +A plugin declares its configuration once, as a plain Go struct, and that declaration acts as the single source of truth. +It validates the platform `config`, supplies the defaults, and derives the command-line flags and environment variables. + +### Where values come from + +Four sources feed a plugin's configuration. +The SDK merges them in this order, from lowest precedence to highest: + +``` +struct default < platform config on STDIN < environment < command-line flag +``` + +- **Struct default**: the fallback that the `default` tag bakes into the binary. +- **Platform `config`**: the JSON you attach to the plugin when you create the instance, which is the production path. +- **Environment**: any field that carries an `env` tag. +- **Command-line flag**: an explicit flag for local development and debugging. + +A field that defaults to `/tmp` stays `/tmp` unless the platform `config` sets `"workdir":"/data"`, unless `WORKDIR=/srv` sits in the environment, unless `--workdir /mnt` appears on the command line. + +### Struct tags + +A config field carries two kinds of tag. +A `json` tag names the field's key in the platform `config`, and [kong](https://github.com/alecthomas/kong) tags govern the command-line flag and the environment override. +The two work independently: JSON keys conventionally use `snake_case` while flags use `kebab-case`, so `json:"source_type"` and `name:"source-type"` routinely sit on the same field. + +| Tag | Purpose | Example | +|-----|---------|---------| +| `json:"…"` | Key in the platform `config` on `STDIN` | `json:"source_url"` | +| `name:"…"` | Flag name, in kebab-case | `name:"source-url"` | +| `env:"…"` | Environment variable to bind the field to | `env:"SOURCE_URL"` | +| `help:"…"` | Help text | `help:"Repository URL."` | +| `default:"…"` | Default value | `default:"/tmp"` | +| `enum:"a,b,c"` | Restrict the value to a set | `enum:"git,tar"` | +| `placeholder:"…"` | Help placeholder | `placeholder:"dir"` | +| `required:""` | Make the flag mandatory | `required:""` | +| `hidden:""` | Hide the flag from `--help` | `hidden:""` | +| `kong:"-"` | Ignore the field entirely | `kong:"-"` | + +The `json` tag belongs to the SDK rather than to kong. +The SDK reads it to map each config key onto a field, and kong owns every other tag. +A field without a `json` tag takes its exact Go field name as the key, so `Greeting` matches `"Greeting"` and misses `"greeting"`. +That lookup runs case-sensitively, unlike `encoding/json`, so give every field a `json` tag. +The map covers a struct's own exported fields, so a field inside an embedded struct gets no config key at all. + +```go +type Config struct { + Workdir string `json:"workdir" name:"workdir" env:"WORKDIR" help:"Working directory." default:"/tmp"` + SourceType string `json:"source_type" name:"source-type" env:"SOURCE_TYPE" help:"Source type." enum:"git,tar" default:"git"` + Verbose bool `json:"verbose" name:"verbose" env:"VERBOSE" help:"Enable verbose output."` + + // Never surfaces as a flag or a config key; code populates it. + internalToken string `json:"-" kong:"-"` +} +``` + +### The platform config + +Attach a `config` to the plugin in the [create instance](/api/platform/v1/instances#create-instance) request, and it reaches the plugin's `init` on `STDIN`: + +```json +{ + "name": "example", + "rom": "user/example:latest", + "config": { "greeting": "Bonjour" } +} +``` + +The platform accepts a non-object `config` as well: a bare string or a number counts as valid JSON. +A value of that shape maps onto no fields, so reach for the raw bytes instead: + +```go +raw := pluginsdk.RawConfig(ctx) // []byte, exactly as STDIN delivered it +``` + +## Routing + +`Register` receives the base context, your typed configuration, and the gin engine: + +```go +Register: func(ctx context.Context, cfg *Config, engine *gin.Engine) error { + engine.GET("/files/:name", handleGetFile) + engine.POST("/files", handleWriteFile) + + v1 := engine.Group("/v1") + v1.GET("/status", handleStatus) + + return nil +} +``` + +Define your routes relative to `/`. +The platform strips the `plugins//` prefix before the request reaches the plugin, so a call to `.../plugins/example/files/list` arrives as `GET /files/list`. +A plugin never needs to know its own name to route correctly. + +The SDK builds the engine with `gin.New()` in release mode, turns on `HandleMethodNotAllowed`, and rejects unknown JSON fields, which keeps generated types strict. + +### Default middleware + +Ahead of your routes, the SDK installs a global middleware stack from `unikraft.com/x/middleware`: + +| Middleware | Effect | +|------------|--------| +| `CORS()` | Cross-origin resource sharing headers | +| `ExtraHeaders()` | Static response headers | +| `Logger(ctx)` | Structured per-request logging | +| `DefaultCacheControl()` | Sensible `Cache-Control` defaults | + +Append your own middleware, or replace the defaults outright: + +```go +pluginsdk.Main(&pluginsdk.Plugin[Config]{ + Name: "example", + Register: register, + Middleware: []gin.HandlerFunc{myMiddleware()}, // appended to the defaults + + // ...or drop the defaults and bring your own stack: + // DisableDefaultMiddleware: true, +}) +``` + +### Generated services + +Plugins describe their API in [TypeSpec](https://typespec.io) and generate a typed gin service interface from it. +Register that generated service from inside `Register`: + +```go +Register: func(ctx context.Context, cfg *Config, engine *gin.Engine) error { + api.RegisterX(engine, handler, nil) + + return nil +} +``` + +`pluginsdk.OK` and `pluginsdk.Error` return the `(payload, status, error)` triple that a generated handler method produces, so a handler body often comes down to a single `return pluginsdk.OK(&data)`. + +## Responses + +The platform wraps every response in a standard envelope, and two helpers build it for you: + +```go +// Success. OK takes a pointer and returns three values. +env, code, _ := pluginsdk.OK(&data) +g.JSON(code, env) + +// Error. +env, code, _ := pluginsdk.Error[any](http.StatusBadRequest, "bad input") +g.JSON(code, env) +``` + +A handler that calls `OK` produces the same shape as the rest of the API: + +```json +{ + "status": "success", + "data": { "message": "Bonjour, world!" } +} +``` + +The `status` field holds `success`, `partial_success`, or `error`. +In Go the envelope type is `platform.Response[T]` from `unikraft.com/cloud/sdk/platform`, which the [Go SDK](/integrations/sdks/go) uses as well. + +:::note +`Error` derives the HTTP status from its `status` argument. +A non-positive status yields `0`, which gin rejects, so always pass a real status code. +::: + +## Global flags + +Every plugin accepts these four flags, whatever its own configuration declares: + +| Flag | Environment | Meaning | +|------|-------------|---------| +| `--api_fd ` | `API_FD` | Serve on file descriptor `n`, which the platform supplies | +| `--api-addr ` | `API_ADDR` | Serve on a TCP address such as `:8080`, for local development | +| `--log-level` | `LOG_LEVEL` | One of `trace`, `debug`, `info`, `warn`, `error`, `fatal` | +| `--log-type` | `LOG_TYPE` | Either `text` or `json` | + +The platform spells `--api_fd` with an underscore, and every other flag uses a hyphen. + +## Lifecycle + +`Main` runs this sequence: + +1. Read the `config` from `STDIN`, then parse the command line. +2. Configure logging from `--log-level` and `--log-type`. +3. Run the optional `Setup` hook. +4. Bind the listener: `--api_fd` in production, `--api-addr` locally. +5. Build the router with the default and user middleware, then call the registration function. +6. Serve on the listener. +7. Block until a signal arrives or the context cancels. +8. Drain: stop accepting connections, finish in-flight requests, then shut down. + +`Serve` covers step 2, then steps 4 through 8. +It takes an already-resolved configuration rather than reading the command line or `STDIN`, and it runs no `Setup` hook. + +`--api_fd` wins whenever the command line carries it, and otherwise the SDK falls back to `--api-addr`. +At least one of the two has to resolve, or startup fails with `ErrNoListener`. +`Serve` can also take a listener directly through `WithListener`, which outranks both flags. +A shutdown gives in-flight requests a 30-second window to drain. + +## Scale-to-zero + +[Scale-to-zero](/features/scale-to-zero) puts an idle instance to sleep. +A plugin request wakes the instance and keeps it up for the length of that request. +Background work that outlives the request has to hold the instance awake on its own. + +The `scaletozero` sub-package keeps a counter for exactly that: + +```go +import "unikraft.com/cloud/pluginsdk/scaletozero" + +// Hold the instance awake while the work runs. +if err := scaletozero.Increment(); err != nil { + return err +} +defer scaletozero.Decrement() +``` + +A counter rather than a flag means many independent workers increment and decrement without coordinating. +Scale-to-zero stays suspended while the count sits above zero and resumes at zero. +`Set(n)`, `IncrementBy(n)`, and `DecrementBy(n)` cover the remaining cases. + +## Logging + +The SDK configures a structured logger from `--log-level` and `--log-type`, which default to `info` and `text`, and makes it available to your handlers through the request context. +The logger comes from `unikraft.com/x/log`. +Pass `--log-type json` in production. + +## Project layout + +A plugin keeps its API description and its image recipes next to the source: + +``` +plugins// +├── api.tsp # TypeSpec API description (optional) +├── openapi.yaml # generated from api.tsp +├── main.go # the entrypoint +├── api/ # generated service: interface, models, register function +├── Dockerfile # builds the single `init` executable into a scratch image +├── Kraftfile # ROM image recipe +└── go.mod +``` + +A small plugin needs no more than `main.go`, `go.mod`, a `Dockerfile`, and a `Kraftfile`. + +## Building the plugin image + +The deliverable is a ROM image with a single `init` executable at its root. +The platform appends `--api_fd ` and pipes the `config` JSON to `STDIN` at launch, so `/init` has to be the SDK-based binary that understands both. + +A plugin runs as a process inside the host instance rather than as its own unikernel, so `init` has to stand on its own. +A static Go build produces that. + +### Dockerfile + +A build stage produces `init`, and a `scratch` image carries it: + +```dockerfile title="Dockerfile" +FROM golang:1.26-bookworm AS build + +WORKDIR /plugin +COPY . . +RUN CGO_ENABLED=0 go build -ldflags="-s -w" -o /plugin/dist/init . + +FROM scratch + +COPY --from=build /plugin/dist/init /init +ENTRYPOINT ["/init"] +``` + +### Kraftfile + +A plugin ships as a ROM, which carries no kernel of its own. +The platform mounts it at `/uk/plugins/` inside the host instance and runs its `init`. +Declare the image under `roms`: + +```yaml title="Kraftfile" +spec: v0.7 + +targets: +- kraftcloud/x86_64 + +roms: +- ./Dockerfile +``` + +A ROM has no `runtime` to take an architecture from, so it names one under `targets`, or you pass `--arch` on the command line. +A build that does neither fails, and each plugin needs the architecture of the instance that hosts it. + +:::caution +Set no `runtime`. +A `runtime` produces a bootable unikernel that the plugin loader can't mount, and the host instance then fails to boot. +The platform runs the image's `init` and appends `--api_fd` on its own, so set no `cmd` and never hard-code `--api_fd`. +::: + +Package and push the ROM image: + + + +```bash title="unikraft" +unikraft build . --output /example:latest +``` + +```bash title="kraft" +kraft pkg \ + --plat kraftcloud \ + --arch x86_64 \ + --name index.unikraft.io//example:latest \ + --rom-type erofs \ + --push \ + . +``` + + + +## Deploying + +Attach the plugin when you create the instance, naming the ROM image and an optional `config`: + + + +```bash title="unikraft" +unikraft api /v1/instances \ + '{ + "name": "my-instance", + "plugins": [ + { + "name": "example", + "rom": "/example:latest", + "config": { + "greeting": "Bonjour" + } + } + ] + }' +``` + + + +Then call the plugin through the instance's authenticated endpoint: + +```bash +curl -H "Authorization: Bearer $UKC_TOKEN" \ + https://api.fra.unikraft.cloud/v1/instances//plugins/example/hello +``` + +The [plugins](/features/plugins) page covers the platform side in full: authorization, adding a plugin to an existing instance, and the limits. + +## API reference + +### Entrypoints + +`Main` suits the common case of one plugin and one server. +It parses the command line, reads `STDIN`, serves, blocks, and exits with a non-zero status on error. + +```go +func Main[C any](p *Plugin[C]) +``` + +`Serve` takes an already-resolved configuration, reads neither the command line nor `STDIN`, and returns an error instead of exiting. +Cancelling `ctx` triggers the graceful shutdown. +Reach for it in tests, when you embed a plugin in a larger program, or when you bring your own command grammar. + +```go +func Serve[C any](ctx context.Context, cfg *C, register RegisterFunc[C], opts ...Option) error +``` + +### The plugin declaration + +```go +type Plugin[C any] struct { + Name string + Version string + Register RegisterFunc[C] + Setup SetupFunc[C] + Middleware []gin.HandlerFunc + DisableDefaultMiddleware bool +} + +type RegisterFunc[C any] func(ctx context.Context, cfg *C, engine *gin.Engine) error +type SetupFunc[C any] func(ctx context.Context, cfg *C) error +``` + +| Field | Description | +|-------|-------------| +| `Name` | The plugin name, for logs and diagnostics only, since routing ignores it | +| `Version` | An optional version string that the startup logs carry | +| `Register` | Attaches the routes, and a non-nil error aborts startup (required) | +| `Setup` | An optional hook that runs once after the SDK resolves the configuration and before the server accepts requests | +| `Middleware` | Middleware to append to the default stack | +| `DisableDefaultMiddleware` | Drops the built-in stack and leaves the plugin in control | + +### Options + +An `Option` customizes `Serve`. +A later option overwrites an earlier one that sets the same field. +`WithMiddleware` appends instead of overwriting, and the listener keeps a fixed priority whatever the order: `WithListener`, then `WithAPIFD`, then `WithAddr`. + +| Option | Effect | +|--------|--------| +| `WithOptions(o Options)` | Seeds the runtime from the parsed global flags | +| `WithAPIFD(fd int)` | Forces the listener to adopt a file descriptor | +| `WithAddr(addr string)` | Forces a TCP listen address | +| `WithListener(ln net.Listener)` | Serves on a listener you supply | +| `WithMiddleware(mw ...gin.HandlerFunc)` | Appends global middleware | +| `WithoutDefaultMiddleware()` | Drops the default middleware stack | +| `WithRawConfig(raw []byte)` | Supplies the raw `STDIN` config for `RawConfig` | + +`Options` itself holds the four global flags as an embeddable kong struct: `APIFd`, `APIAddr`, `LogLevel`, and `LogType`. +Embed it in your own command grammar to keep the standard flags. + +### Context and config helpers + +Every request context descends from the base context, so these helpers work inside a handler through `g.Request.Context()`. + +| Symbol | Description | +|--------|-------------| +| `FromContext[C any](ctx) *C` | Retrieves the typed configuration from a request context | +| `RawConfig(ctx) []byte` | The raw platform `config` bytes from `STDIN` | +| `APIFD(ctx) (int, bool)` | The adopted file descriptor, and whether one exists | +| `ReadConfig(stdin io.Reader) []byte` | Reads the `STDIN` config verbatim | +| `NewConfigResolver[C any](raw []byte) (kong.Resolver, bool)` | Builds the kong resolver for a custom grammar | + +### Response helpers + +| Symbol | Description | +|--------|-------------| +| `OK[T any](data *T) (*platform.Response[T], int, error)` | A success envelope that wraps `data` | +| `Error[T any](status int, msg string) (platform.Response[T], int, error)` | An error envelope | + +### Other exported symbols + +| Symbol | Description | +|--------|-------------| +| `ShutdownTimeout` | The graceful-drain bound, at 30 seconds | +| `ReadHeaderTimeout` | The bound on waiting for request headers, at 30 seconds | +| `ErrNoListener` | Neither `--api_fd` nor `--api-addr` resolved | +| `ErrNoRegister` | A plugin reached `Serve` without a registration function | +| `ErrInvalidAPIFD` | The `--api_fd` descriptor yielded no usable file | + +## Bring your own command grammar + +`Main` wires one command line for you. +A plugin that needs subcommands or extra pre-serve work defines its own kong grammar and calls `Serve` from the subcommand: + +```go +type CLI struct { + pluginsdk.Options // --api_fd, --api-addr, --log-level, --log-type + + Config Config `embed:""` + + Run RunCmd `cmd:"" help:"Fetch source, then serve."` +} + +type RunCmd struct { + SourceURL string `name:"source-url" env:"SOURCE_URL" help:"Repository to clone."` +} + +func (c *RunCmd) Run(ctx context.Context, cli *CLI, raw rawConfig) error { + if err := fetchSource(ctx, c.SourceURL); err != nil { + return err + } + + return pluginsdk.Serve(ctx, &cli.Config, register, + pluginsdk.WithOptions(cli.Options), + pluginsdk.WithRawConfig(raw), + ) +} +``` + +Two exported building blocks tie it together in `main`. +`ReadConfig` reads the platform config off `STDIN`, and `NewConfigResolver` turns those bytes into the kong resolver that gives you the standard precedence: + +```go +type rawConfig []byte + +func main() { + ctx, cancel := signal.NotifyContext(context.Background(), + syscall.SIGINT, syscall.SIGTERM) + defer cancel() + + raw := pluginsdk.ReadConfig(os.Stdin) + + var cli CLI + + opts := []kong.Option{ + kong.Name("example"), + kong.UsageOnError(), + kong.BindTo(ctx, (*context.Context)(nil)), + kong.Bind(rawConfig(raw)), + } + if resolver, ok := pluginsdk.NewConfigResolver[Config](raw); ok { + opts = append(opts, kong.Resolvers(resolver)) + } + + k := kong.Parse(&cli, opts...) + k.FatalIfErrorf(k.Run()) +} +``` + +`pluginsdk.Options` contributes the standard global flags and `WithOptions` feeds them to `Serve`, so the listener selection and the logging match `Main` with your own command tree on top. +Extra middleware alone calls for no custom grammar, since `Middleware` and `DisableDefaultMiddleware` on `Plugin` already cover that. + +## Source + +The SDK source lives at [github.com/unikraft-cloud/plugin-sdk](https://github.com/unikraft-cloud/plugin-sdk), and the API reference at [pkg.go.dev/unikraft.com/cloud/pluginsdk](https://pkg.go.dev/unikraft.com/cloud/pluginsdk). + +## Learn more + +* [Plugins](/features/plugins): the platform feature that this SDK targets. +* [ROMs](/features/roms): the image format that a plugin ships as. +* [Scale-to-zero](/features/scale-to-zero): how an idle instance wakes to serve a plugin request. +* [Go SDK](/integrations/sdks/go): the client library for the platform API itself. +* [JavaScript SDK](/integrations/sdks/js): the same platform API from JavaScript and TypeScript. +* [Instances](/platform/instances): create and manage the instances that host plugins. diff --git a/pages/platform/delete-locks.mdx b/pages/platform/delete-locks.mdx index 88965060..0b205a49 100644 --- a/pages/platform/delete-locks.mdx +++ b/pages/platform/delete-locks.mdx @@ -34,7 +34,7 @@ You can set a delete lock on a resource via a `PATCH` operation: ```bash title="unikraft" unikraft api /v1/instances -X PATCH --metro fra \ - -d '[{ + '[{ "name": "", "prop": "delete_lock", "op": "set", @@ -60,7 +60,7 @@ curl -X PATCH \ ```bash title="unikraft" unikraft api /v1/instances/templates -X PATCH --metro fra \ - -d '[{ + '[{ "name": "", "prop": "delete_lock", "op": "set", @@ -85,7 +85,7 @@ curl -X PATCH \ ```bash title="unikraft" unikraft api /v1/volumes -X PATCH --metro fra \ - -d '[{ + '[{ "name": "", "prop": "delete_lock", "op": "set", @@ -110,7 +110,7 @@ curl -X PATCH \ ```bash title="unikraft" unikraft api /v1/volumes/templates -X PATCH --metro fra \ - -d '[{ + '[{ "name": "", "prop": "delete_lock", "op": "set", @@ -148,7 +148,7 @@ Before you can delete a resource, you need to remove the delete lock by setting ```bash title="unikraft" unikraft api /v1/instances -X PATCH --metro fra \ - -d '[{ + '[{ "name": "", "prop": "delete_lock", "op": "set", @@ -173,7 +173,7 @@ curl -X PATCH \ ```bash title="unikraft" unikraft api /v1/instances/templates -X PATCH --metro fra \ - -d '[{ + '[{ "name": "", "prop": "delete_lock", "op": "set", @@ -198,7 +198,7 @@ curl -X PATCH \ ```bash title="unikraft" unikraft api /v1/volumes -X PATCH --metro fra \ - -d '[{ + '[{ "name": "", "prop": "delete_lock", "op": "set", @@ -223,7 +223,7 @@ curl -X PATCH \ ```bash title="unikraft" unikraft api /v1/volumes/templates -X PATCH --metro fra \ - -d '[{ + '[{ "name": "", "prop": "delete_lock", "op": "set", diff --git a/pages/platform/images.mdx b/pages/platform/images.mdx index 612d6d82..f9704305 100644 --- a/pages/platform/images.mdx +++ b/pages/platform/images.mdx @@ -7,7 +7,7 @@ Unikraft Cloud uses a **registry** to store images used to instantiate apps. At a high level, you use the CLI to build and push an image to the registry, and then ask the controller to start an app from it. This process utilizes two types of registries: central and local. -Built images default to the central registry (`index.unikraft.io`). +Built images default to the central registry (`index.unikraft.io`). The platform pulls from this registry automatically upon starting an instance for the first time. You can choose to push an image directly to a node's local registry instead. @@ -40,6 +40,9 @@ spec: v0.7 runtime: base-compat:latest +targets: + - kraftcloud/x86_64 + rootfs: source: ./Dockerfile format: erofs @@ -47,11 +50,11 @@ rootfs: cmd: ["/usr/bin/python3", "/src/server.py"] ``` -The file is simple. -It defines the start command, instructs the CLI to build the root filesystem, and specifies the `base-compat:latest` runtime. +The file is simple. +It defines the start command, instructs the CLI to build the root filesystem, specifies the `base-compat:latest` runtime, and targets `kraftcloud` on `x86_64`. On Unikraft Cloud, a runtime provides a base image containing the minimal code your app needs to execute. -The CLI combines your app files with this base image during the build step. +The CLI combines your app files with this base image during the build step. It uses the `Dockerfile` as the source to generate the directory structure. When packaging these files into a root filesystem, Unikraft offers two main options: [CPIO and EROFS](/tutorials/rootfs-formats). The file specifies the EROFS format to create a read-only disk image, which is the [preferred format for Unikraft Cloud](/tutorials/rootfs-formats#conclusion). @@ -65,18 +68,6 @@ FROM python:3.12 AS build RUN set -xe; \ /usr/sbin/ldconfig /usr/local/lib -FROM scratch - -# copy the dependencies from the base image -COPY --from=build /usr/local/lib /usr/local/lib -COPY --from=build /usr/local/bin/python3 /usr/bin/python3 -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /usr/lib/x86_64-linux-gnu/libz.so.1 /usr/lib/x86_64-linux-gnu/libz.so.1 -COPY --from=build /usr/lib/x86_64-linux-gnu/libcrypto.so.3 /usr/lib/x86_64-linux-gnu/libcrypto.so.3 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 -COPY --from=build /etc/ld.so.cache /etc/ld.so.cache - # copy the python image COPY ./server.py /src/server.py ``` @@ -100,6 +91,73 @@ Be aware that this choice may increase image size, memory use, and boot time. ::: +## Platform kernel + +{/* vale off */} +:::caution[**Limited Access**] +Kernel-less images are available as part of enterprise plans. +To try out this feature, reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). +::: +{/* vale on */} + +An image doesn't have to carry its own kernel. +An image that ships only a rootfs boots on the kernel installed on the node, which your metro operator maintains as a separate package. +Images that do ship a kernel keep working exactly as before. + +This decouples the guest kernel from your app. +A kernel update, including a security fix, becomes available to workloads that cold-boot after the operator rolls out the new kernel package. +You no longer rebuild and re-push every image to pick one up. +Images also get smaller, which shortens image pull times. + +A new kernel on the node applies to the next instance that starts. +Running instances keep the kernel they booted with until they fully shut down, and an instance resuming from a [snapshot](/features/snapshots) always boots the kernel that snapshot captured. + +## Architectures + +The Unikraft Cloud platform runs on both `x86_64` and `arm64` hosts, and the hosted platform runs on `x86_64`. +The architecture of the guest images must match the host architecture. + +{/* vale off */} +:::caution[**Limited Access**] +`arm64` hosts are available as part of enterprise plans. +To deploy to them, reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). +::: +{/* vale on */} + +List the architectures you want under `targets` in the `Kraftfile`: + +```yaml title="Kraftfile" +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + - kraftcloud/arm64 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: ["/usr/bin/python3", "/src/server.py"] +``` + +A single `unikraft build` invocation then produces an image for every architecture in the list. +The `--arch` flag names architectures on the command line instead, which is handy for narrowing a multi-architecture `Kraftfile` down to one architecture. + +How the build resolves architectures depends on what the `Kraftfile` declares: + +| `Kraftfile` | Architectures used | +|-------------|--------------------| +| `runtime` with no `targets` or `--arch` | Every architecture the runtime provides. | +| `runtime` with `targets` or `--arch` | Only the architectures listed in both `targets` and `--arch`, which must be ones the runtime provides. | +| [`roms`](/features/roms) with no `runtime` | Only the architectures listed in both `targets` and `--arch`, and the build fails when you supply neither. | + +:::caution +Multi-platform builds need a correctly configured BuildKit builder on the host. +Learn more [here](https://docs.docker.com/build/building/multi-platform/) +::: + ## Example workflows This guide uses a Python [app](/guides/httpserver-python3.12) as an example to show three workflows: diff --git a/pages/platform/instances.mdx b/pages/platform/instances.mdx index 0b12fcb2..f3b0c7b9 100644 --- a/pages/platform/instances.mdx +++ b/pages/platform/instances.mdx @@ -300,6 +300,19 @@ A manual start or stop of the instance aborts the restart sequence and resets th ## Creating instances +### Instance types + +Every instance runs as one of two virtual machine types, which you pick with `--type`: + +| Type | Description | +|------|-------------| +| `micro` | The default, running on Firecracker. | +| `full` | Runs on QEMU, which [GPU support](#gpus) requires, and needs a plan with full VM support. | + +Full VMs trade platform features for that extra hardware access. +A QEMU-backed instance supports neither [scale-to-zero](/features/scale-to-zero), [instance templates](#instance-templates), [branching](/features/branching), nor [checkpointing](/features/checkpoints), and works only with block-based volumes rather than [virtiofs](/platform/volumes#filesystems). +A create request therefore can't combine `--type` with `--template`, `--branch`, or `--checkpoint`. + ### Replicas The [`POST /instances`](/api/platform/v1/instances#create-instance) request accepts a `replicas` field (default 0) that creates more copies of the instance alongside the base one. @@ -331,29 +344,41 @@ kraft cloud deploy --replicas 2 ... :::caution[**Limited Access**] GPU support is currently only available as a preview for enterprise customers. +To try out the preview, reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). The interface described here reflects the current implementation and may change before general availability. ::: Instances can run with an NVIDIA GPU attached. -Pass the `gpus` field in the [`POST /instances`](/api/platform/v1/instances#create-instance) request to request one: +Pass `--gpus` when you create the instance, together with the `full` instance type that GPUs require: -```json title="POST /instances" -{ - ... - "image": "my-inference-app:latest", - "memory_mb": 4096, - "type": "full", - "gpus": 1, - ... -} + + +```bash title="unikraft" +unikraft instance create --metro fra \ + --name my-inference-app \ + --image my-inference-app:latest \ + --memory 4096 \ + --type full \ + --gpus 1 ``` -An instance can currently hold at most one GPU. + -:::note -GPU instances are only available on GPU-provisioned hardware. -Please [get in touch](https://unikraft.com/contact) if you need it. -::: +An instance can currently hold at most one GPU, and you can't choose which one. +The platform picks a free GPU from the host and keeps it assigned for the instance's entire lifetime, releasing it when you delete the instance. + +To see the GPUs an instance holds, inspect it: + + + +```bash title="unikraft" +unikraft instance get my-inference-app +``` + + + +The `gpus.*.uuid` and `gpus.*.model` fields report the GPUs themselves, and `resources.gpus` reports how many the instance holds. +Listings sort and filter on all three like any other [field](/docs/cli/fields). ### Wait for running @@ -419,6 +444,46 @@ unikraft instance create --rm ... To use the "delete-on-stop" feature, set the `restart_policy` to `never`. ::: +### Nested virtualization + +{/* vale off */} +:::caution[**Limited Access**] +Nested virtualization is available as part of enterprise plans. +To enable it for your account, reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to [support@unikraft.com](mailto:support@unikraft.com). +::: +{/* vale on */} + +An instance runs without the host's virtualization extensions by default, since the platform hides them from the guest. +Pass `nested-virt` in the features array to expose them, so that a workload shipping its own virtual machines runs as an ordinary instance: + +```json title="POST /instances" +{ + ... + "image": "my-workload:latest", + "memory_mb": 4096, + "features": ["nested-virt"], + ... +} +``` + +You can also use the CLI to set this up: + + + +```bash title="unikraft" +unikraft instance create --feature nested-virt ... +``` + + + +The guest kernel needs the matching driver to use the extensions. +A Linux guest needs `CONFIG_KVM` together with `CONFIG_KVM_INTEL` or `CONFIG_KVM_AMD`, and exposes `/dev/kvm` once it loads the module. + +:::note +A create request that starts from a snapshot—an [instance template](#instance-templates), a [branch](/features/branching), or a [checkpoint](/features/checkpoints)—can't add `nested-virt`. +Set the feature on the instance that you snapshot instead, and every instance that starts from that snapshot inherits it. +::: + ## Stopping instances ### Drain timeout @@ -471,6 +536,11 @@ Cloning a template creates a new instance that resumes from the exact original s It preserves memory contents, open files, and populated caches to bypass the standard boot sequence. Once you convert an instance into a template, you can't reverse the process. +:::note +Templates need the default `micro` [instance type](#instance-types). +QEMU-backed full VMs (`--type full`), which [GPU instances](#gpus) require, don't support them. +::: + To transition an actively running instance into a template, from the guest write the value 1 to `/uk/libukp/template_instance`: ```bash title="Convert instance to template" diff --git a/pages/platform/services.mdx b/pages/platform/services.mdx index 9205149a..26ac4959 100644 --- a/pages/platform/services.mdx +++ b/pages/platform/services.mdx @@ -95,7 +95,7 @@ First, create a new service with the CLI: ```bash title="unikraft" unikraft services create --metro fra \ --name my-service \ - --services 443:8080/http+tls + --service 443:8080/http+tls ``` ```bash title="kraft" @@ -183,7 +183,7 @@ The output shows the instance address and other details:  uuid: 9bfe76ea-2679-482a-954e-577de7d122b3  name: my-service  domains: - - fqdn: my-service-f4744h0c.fra.unikraft.app + - fqdn: my-service-pc4xjplc.fra.unikraft.app networks: - uuid: 51f79dc1-e989-908d-894b-bdf0a87e7901  private-ip: 10.0.3.3 @@ -211,12 +211,12 @@ The output shows the instance address and other details: In this case, the instance name is `httpserver-go121-9a2wv`. -The address is `https://my-service-f4744h0c.fra.unikraft.app`. +Notice that the service ID is the same from the service created earlier, and so is the FQDN, which is `https://my-service-pc4xjplc.fra.unikraft.app`. Use `curl` to query the Go HTTP server: ```bash -curl https://my-service-f4744h0c.fra.unikraft.app +curl https://my-service-pc4xjplc.fra.unikraft.app ``` ```text title="" @@ -286,9 +286,9 @@ For example, the following creates the service `my-service` with three published ```bash title="unikraft" unikraft services create --metro fra \ --name my-service \ - --services 443:8080/tls+http \ - --services 80:8080/http+redirect \ - --services 10000:10000/tls + --service 443:8080/tls+http \ + --service 80:8080/http+redirect \ + --service 10000:10000/tls ``` ```bash title="kraft" @@ -347,7 +347,7 @@ You can use the CLI to set the connection limits for a service: ```bash title="unikraft" unikraft services create --metro fra \ --name my-service \ - --services 443:8080/http+tls \ + --service 443:8080/http+tls \ --soft-limit 5 \ --hard-limit 10 ``` diff --git a/pages/platform/tagging.mdx b/pages/platform/tagging.mdx index f2c0ab3d..016ce757 100644 --- a/pages/platform/tagging.mdx +++ b/pages/platform/tagging.mdx @@ -31,7 +31,8 @@ unikraft instance create \ --metro fra \ --image ... \ --memory ... \ - --tags production,customer_A + --tag production \ + --tag customer_A ``` ```bash title="API" curl -X POST \ @@ -47,12 +48,12 @@ curl -X POST \ +Converting an instance relabels it in place, so the template keeps the instance's tags. +Tag the instance first, then convert it: + ```bash title="unikraft" -unikraft instance template create \ - --set image=... \ - --set resources.memory=... \ - --set tags=production,customer_A +unikraft instance template create ``` ```bash title="API" curl -X POST \ @@ -60,9 +61,7 @@ curl -X POST \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/instances/templates" \ -d '{ - "image": "...", - "memory_mb": ..., - "tags": ["production", "customer_A"] + "name": "" }' ``` @@ -73,7 +72,8 @@ curl -X POST \ unikraft volume create \ --metro fra \ --size ... \ - --tags production,customer_A + --tag production \ + --tag customer_A ``` ```bash title="API" curl -X POST \ @@ -88,11 +88,12 @@ curl -X POST \ +Converting a volume relabels it in place, so the template keeps the volume's tags. +Tag the volume first, then convert it: + ```bash title="unikraft" -unikraft volume template create \ - --set size=... \ - --set tags=production,customer_A +unikraft volume template create ``` ```bash title="API" curl -X POST \ @@ -100,8 +101,7 @@ curl -X POST \ -H "Content-Type: application/json" \ "https://api.fra.unikraft.cloud/v1/volumes/templates" \ -d '{ - "size_mb": ..., - "tags": ["production", "customer_A"] + "name": "" }' ``` @@ -121,7 +121,8 @@ Add tags to an existing resource (`"op": "set"` will replace all existing tags): ```bash title="unikraft" unikraft instance edit \ - --tags testsystem,customer_B + --tag testsystem \ + --tag customer_B ``` ```bash title="API" curl -X PATCH \ @@ -141,7 +142,8 @@ curl -X PATCH \ ```bash title="unikraft" unikraft instance template edit \ - --tags testsystem,customer_B + --tag testsystem \ + --tag customer_B ``` ```bash title="API" curl -X PATCH \ @@ -161,7 +163,8 @@ curl -X PATCH \ ```bash title="unikraft" unikraft volume edit \ - --tags testsystem,customer_B + --tag testsystem \ + --tag customer_B ``` ```bash title="API" curl -X PATCH \ @@ -181,7 +184,8 @@ curl -X PATCH \ ```bash title="unikraft" unikraft volume template edit \ - --tags testsystem,customer_B + --tag testsystem \ + --tag customer_B ``` ```bash title="API" curl -X PATCH \ @@ -439,7 +443,8 @@ curl -H "Authorization: Bearer ${UKC_TOKEN}" \ -Separate tags by commas (`,`). +When filtering, separate tags by commas (`,`). +Elsewhere, pass one tag per `--tag` flag—the CLI no longer splits a flag value on commas. ## Learn more diff --git a/pages/platform/troubleshooting.mdx b/pages/platform/troubleshooting.mdx index 4ed8475e..2e3680f6 100644 --- a/pages/platform/troubleshooting.mdx +++ b/pages/platform/troubleshooting.mdx @@ -154,6 +154,9 @@ spec: v0.7 runtime: base-compat:latest-dbg +targets: + - kraftcloud/x86_64 + rootfs: source: ./Dockerfile format: erofs diff --git a/pages/releases/r10-themisto.mdx b/pages/releases/r10-themisto.mdx index 4dc0f5db..34ee4b54 100644 --- a/pages/releases/r10-themisto.mdx +++ b/pages/releases/r10-themisto.mdx @@ -133,6 +133,10 @@ Set `format:` explicitly if you need to pin one. ```yaml spec: v0.7 + +targets: + - kraftcloud/x86_64 + rootfs: source: ./rootfs # format: cpio # set explicitly to pin; previously implied @@ -143,6 +147,10 @@ A Dockerfile-backed rootfs can now point at a Dockerfile independent of its buil ```yaml spec: v0.7 + +targets: + - kraftcloud/x86_64 + rootfs: source: type: dockerfile diff --git a/pages/tutorials/docker-to-ukc.mdx b/pages/tutorials/docker-to-ukc.mdx index 4989c8fb..f074ffb9 100644 --- a/pages/tutorials/docker-to-ukc.mdx +++ b/pages/tutorials/docker-to-ukc.mdx @@ -75,6 +75,9 @@ spec: v0.7 runtime: base-compat:latest +targets: + - kraftcloud/x86_64 + rootfs: source: ./Dockerfile format: erofs @@ -85,6 +88,8 @@ cmd: ["/path/from-entrypoint", "arg1", "arg2"] In this first-pass `Kraftfile`: * `runtime: base-compat:latest` selects the generic compatibility runtime. +* `targets: kraftcloud/x86_64` specifies the platform and architecture pairs to build for. + No architecture is the default, so add `kraftcloud/arm64` to the list to also build for ARM hosts. * `rootfs: ./Dockerfile` tells the CLI to build the root filesystem from the `Dockerfile`. * `cmd: [...]` explicitly sets the process arguments that should run inside the instance. diff --git a/pages/tutorials/rootfs-volumes-roms.mdx b/pages/tutorials/rootfs-volumes-roms.mdx index 6bf3840c..83efb10e 100644 --- a/pages/tutorials/rootfs-volumes-roms.mdx +++ b/pages/tutorials/rootfs-volumes-roms.mdx @@ -167,13 +167,16 @@ Finally, it's considered good practice to keep ROMs small and focused, and the i You could package ROMs as an OCI image and push them to the registry, or you could create them directly from a local directory. They show up as any other image in the registry, but don't have a kernel. +Because a ROM has no `runtime` to take architectures from, its `Kraftfile` must name them under `targets`, or you pass `--arch` on the command line. To package them you can use the CLI: ```bash title="unikraft" -# make sure to specify the rom path in the Kraftfile +# make sure to specify the rom path and the build targets in the Kraftfile # ... +# targets: +# - kraftcloud/x86_64 # roms: # - ./fs # ... diff --git a/pages/use-cases/serverless-databases.mdx b/pages/use-cases/serverless-databases.mdx index 005bd3aa..734a131a 100644 --- a/pages/use-cases/serverless-databases.mdx +++ b/pages/use-cases/serverless-databases.mdx @@ -303,34 +303,36 @@ Checkpoints are reusable, named restore points that keep a history. Start a fresh instance from any checkpoint to recover from a failed migration or an erroneous write. For a full description of how checkpointing works, see the [checkpointing feature page](/features/checkpoints). -To checkpoint a running instance, call the [`POST /instances/checkpoints`](/api/platform/v1/instances) endpoint with the source instance name: +To checkpoint a running instance, pass it to `unikraft instance checkpoint create`: ```bash title="unikraft" -unikraft api /v1/instances/checkpoints \ - -d '{ - "name": "before-migration", - "from": { - "name": "" - } - }' +unikraft instance checkpoint create +``` + +```bash title="kraft" +# checkpointing is not supported in the legacy CLI +# use the unikraft CLI ``` -Later, restore the database by creating a new instance from the checkpoint: +List the checkpoints you've taken with `unikraft instance history `. + +Later, restore the database by creating a new instance from one of those checkpoints: ```bash title="unikraft" -unikraft api /v1/instances \ - -d '{ - "name": "restored-postgres", - "checkpoint": { - "name": "before-migration" - } - }' +unikraft run --metro fra \ + -p 5432:5432/tls \ + --checkpoint +``` + +```bash title="kraft" +# checkpointing is not supported in the legacy CLI +# use the unikraft CLI ``` diff --git a/zudoku.config.tsx b/zudoku.config.tsx index 00fb5975..1c549b0a 100644 --- a/zudoku.config.tsx +++ b/zudoku.config.tsx @@ -159,7 +159,10 @@ const config: ZudokuConfig = { collapsed: false, items: [ "/integrations/kubernetes", + "/integrations/karpenter", "/integrations/sdks/go", + "/integrations/sdks/js", + "/integrations/sdks/plugin", ], }, ], @@ -174,11 +177,14 @@ const config: ZudokuConfig = { "/guides/httpserver-dotnet10.0", // .NET HTTP Server "/guides/node18-agario", // Agar.io (Node) "/guides/mcp-server-arxiv", // ArXiv MCP Server + "/guides/build-environments", // Build/Test Environments with ROMs "/guides/httpserver-bun", // Bun HTTP Server "/guides/httpserver-gcc13.2", // C HTTP Server "/guides/httpserver-boost1.74-gpp13.2", // C++ Boost HTTP Server "/guides/httpserver-gpp13.2", // C++ HTTP Server "/guides/caddy2.7-go1.21", // Caddy + "/guides/chromium-cdp", // Chromium CDP + "/guides/chromium-cdp-auth", // Chromium CDP with Token Authentication "/guides/debian-ssh", // Debian SSH server "/guides/httpserver-python3.12-django5.0", // Django HTTP Server "/guides/dragonflydb", // DragonflyDB @@ -202,6 +208,7 @@ const config: ZudokuConfig = { "/guides/httpserver-lua5.1", // Lua HTTP Server "/guides/mariadb", // MariaDB "/guides/memcached1.6", // Memcached + "/guides/minecraft", // Minecraft "/guides/minio", // Minio "/guides/mongodb", // MongoDB "/guides/mysql", // MySQL @@ -211,7 +218,9 @@ const config: ZudokuConfig = { "/guides/node24-karaoke", // Node AllKaraoke "/guides/httpserver-node26", // Node HTTP Server "/guides/node21-websocket", // Node WebSocket Server + "/guides/node-code-execution", // Node.js Code Execution with ROMs "/guides/novnc-browser", // noVNC + "/guides/openclaw", // OpenClaw "/guides/opentelemetry-collector", // OpenTelemetry Collector "/guides/httpserver-perl5.42", // Perl HTTP Server "/guides/phoenix-postgres", // Phoenix with PostgreSQL