Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .vale/styles/config/vocabularies/Unikraft/accept.txt
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ CNAME
COPY
DCO
EOF
ESM
FROM
HIPPA
HMR
Expand Down
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,11 @@
"react": ">=19.0.0",
"react-dom": ">=19.0.0",
"regex-utilities": "^2.3.0",
"zudoku": "^0.86.0"
"zudoku": "^0.88.0"
},
"devDependencies": {
"@types/react": "^19",
"@types/react-dom": "^19",
"@typescript-eslint/eslint-plugin": "^8.0.0",
"@typescript-eslint/parser": "^8.0.0",
"eslint": "^9.14.0"
Expand Down
21 changes: 19 additions & 2 deletions pages/guides/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,31 @@
title: "Guides Overview"
---

This page lists all available guides for deploying applications and services on Unikraft Cloud, grouped by category.
These guides mimic the [examples](https://github.com/unikraft-cloud/examples) repository, and are constantly updated with new content.
This page lists all available guides for Unikraft Cloud.
The guides are in two groups:

* **Tutorials** explain a platform topic that applies to any app.
Two examples are root filesystem formats and environment variables.
* **Example apps** show how to deploy one specific app or service.
They mimic the [examples](https://github.com/unikraft-cloud/examples) repository, and are constantly updated with new content.

:::note
Unikraft Cloud can run any workload—define it in a `Dockerfile` and it will run it.
These guides are here to make that journey as fast as possible for you.
:::

## Tutorials

- [Docker To Unikraft Cloud](/tutorials/docker-to-ukc)
- [KraftKit To Unikraft](/tutorials/kraftkit-to-unikraft)
- [Environment Variables](/tutorials/environment-variables)
- [Rootfs Formats](/tutorials/rootfs-formats)
- [Rootfs Compression](/tutorials/rootfs-compression)
- [Rootfses, Volumes and ROMs](/tutorials/rootfs-volumes-roms)
- [Scale To Zero Triggers](/tutorials/scale-to-zero-triggers)

The sections below list the example apps, grouped by category.

{/* vale off */}

## HTTP Servers
Expand Down
2 changes: 1 addition & 1 deletion pages/introduction.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ New: GPU support in enterprise preview - read more [here](/platform/instances#gp

## Quick start

<Stepper>
<Stepper toc={false}>

1. [Create a free account](https://console.unikraft.cloud/signup).

Expand Down
555 changes: 50 additions & 505 deletions pages/sdks/js.mdx

Large diffs are not rendered by default.

102 changes: 102 additions & 0 deletions pages/sdks/js/client.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
---
title: Client
description: Configure the UnikraftCloud client, its endpoints, and its transport
navigation_icon: settings
---

The `UnikraftCloud` class is the entry point of the SDK.
One client holds one token, one metro scope, and one transport, and every resource belongs to it.

## Create a client

The constructor takes one configuration object.
Every field is optional.

```ts
import { UnikraftCloud } from "@unikraft/cloud";

const ukc = new UnikraftCloud({
token: process.env.UKC_TOKEN,
metro: "fra",
});
```

## Options

| Option | Default | Description |
| ----------------- | ----------------- | ----------------------------------------------------------------------------------------------------------- |
| `token` | `UKC_TOKEN` | The bearer token. |
| `metro` | `UKC_METRO` | One metro code, such as `fra`, or a full `http(s)://` address. Operations that need one metro use this one. |
| `metros` | `"all"` | The metros that operations cover: `"all"`, one metro code, or a list of codes. |
| `baseUrl` | | The platform API address. It overrides `metro`. |
| `controlPlaneUrl` | | The control plane API address. |
| `fetch` | The global `fetch` | A custom `fetch` implementation. |
| `headers` | | Extra headers that the client sends with every request. |
| `userAgent` | `@unikraft/cloud` | The `User-Agent` header. |
| `proxyFromEnv` | `true` | Read the proxy environment variables on Node.js. |

`metros` sets the scope of reads and bulk operations.
`metro` sets the target of operations that need exactly one metro, such as `create`.
When you set both, `metros` wins for the scope, and `metro` stays the target.
When you set neither, the scope is every metro that the account can reach, and the target is `fra`.
[Metros](/sdks/js/metros) explains the scope in detail.

## Endpoints

A metro code expands to `https://api.<metro>.unikraft.cloud`.
The client asks the control plane for the metros of the account, and it uses the endpoint that the control plane reports.
As a result, a new metro works without an SDK upgrade.

## Self-hosted and staging deployments

`metro` and `UKC_METRO` also accept a full `http(s)://` address.
The client uses the address as given.
It drops a trailing `/v1`, because every operation path already carries it.

```sh
export UKC_METRO=https://api.staging.example.internal
```

A named address is the only endpoint that the client uses.
It does no metro discovery, and it invents no hostnames, whatever the scope says.
Point the control plane at the same deployment with `controlPlaneUrl`:

```ts
const ukc = new UnikraftCloud({
token,
metro: "https://api.staging.example.internal",
controlPlaneUrl: "https://controlplane.staging.example.internal",
});
```

## Runtime

The SDK needs Node.js 22.12 or later, and it ships as ECMAScript modules (ESM) only.
From that version, Node.js can `require()` an ESM package, so CommonJS code can load it too.

The client uses the global `fetch`, which Node.js supplies.
To use another runtime, or to control the transport, pass your own:

```ts
const ukc = new UnikraftCloud({ token, fetch: myFetch });
```

## Proxy support

On Node.js, the client reads the standard proxy environment variables.
This lets you route its traffic through a proxy such as [mitmproxy](https://mitmproxy.org), Charles, or Proxyman without a code change.

```sh
npm install undici
export HTTPS_PROXY=http://127.0.0.1:8080
export NODE_EXTRA_CA_CERTS=~/.mitmproxy/mitmproxy-ca-cert.pem
node your-script.js
```

The client recognises `HTTP_PROXY`, `HTTPS_PROXY`, `ALL_PROXY`, and `NO_PROXY`, in either letter case.
The global `fetch` of Node.js ignores these variables, so the SDK applies them through the `EnvHttpProxyAgent` of `undici`.
`undici` is an optional peer dependency, and the SDK imports it only when you set a proxy variable.
If `undici` isn't installed, the SDK sends the requests without a proxy, and it logs one warning.

To turn this off for one client, set `proxyFromEnv: false`.
Browsers and Deno ignore these variables.
80 changes: 80 additions & 0 deletions pages/sdks/js/errors.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
---
title: Errors
description: The error classes that the SDK throws, the error kinds, and how waits behave
navigation_icon: triangle-alert
---

The SDK throws for every network failure and for every response with a status outside 2xx.
It never returns an error envelope as a value.

## `UnikraftCloudError`

Every failure of a request or of a wait is an `UnikraftCloudError`, or a subclass of it.
Two other cases throw a native error instead:

- A wrong argument throws a `TypeError` or a `RangeError`. For example, a reference with no `uuid` and no `name`, or a wait duration that isn't a usable number.
- A wait that your `AbortSignal` cancels rethrows the reason of that signal.

Thus, check `instanceof UnikraftCloudError` before you read `kind`, `status`, or `errors`.

```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);
}
}
```

| Property | Content |
| --------- | -------------------------------------------------------------------------- |
| `kind` | Which layer failed. See [error kinds](#error-kinds). |
| `status` | The HTTP status, when the server answered. |
| `message` | A sentence that names the failure. |
| `errors` | The error list from the response envelope, when the server sent one. |
| `body` | The parsed response body, when there is one. |
| `cause` | The failure underneath, for a wait that ran out of time. |

## Error kinds

| `kind` | Meaning |
| ----------- | ----------------------------------------------------------------------------------------------------------------------- |
| `"http"` | The server answered with a status outside 2xx. `err.status` and `err.errors` carry the detail. |
| `"network"` | The request got no answer: a refused connection, a reset, a DNS miss, or an unreachable proxy. |
| `"parse"` | The server answered, but the body isn't the JSON that the operation expects. |
| `"fanout"` | A multi-metro operation failed in part, or the scope is unusable. See [`MetroFanoutError`](#errors-of-the-metro-fan-out). |
| `"config"` | The SDK couldn't send the call as configured: a missing token, or two options that contradict each other. |
| `"timeout"` | A wait ran out of time. It carries no `status`, because no single request failed. The last failure is in `err.cause`. |

## Errors of the metro fan-out

Two subclasses come from operations that span metros.
[Metros](/sdks/js/metros) explains both in context.

| Class | When | Extra properties |
| ------------------- | ------------------------------------------------------------------------------- | ----------------------------------------------- |
| `AmbiguousRefError` | A `{ name }` ref matched a resource in more than one metro. | `metros`, and `matches` with the resources |
| `MetroFanoutError` | One or more metros failed while the others answered. | `failures`, and `results` for a bulk operation |

## Waits and retries

The transport sends each request exactly once.
It has no retries, and nothing in the SDK retries a call that you made.

A wait is a separate, explicit step, and it takes one of two shapes:

- **The server waits.**
`instance.wait({ state: "running" })` sends the deadline to the platform, and the platform holds the connection open.
This is one request.
- **The SDK polls.**
Where the platform has nothing to hold open, the SDK asks again on a schedule.
The delay doubles from 100 ms to 2 s with jitter, and the deadline is 60 s.
The deadline and a `signal` both stop the probe in flight, not only the loop around it.
Only a failure that can still change gets another probe: a network fault, or a `404`, `502`, `503`, or `504`.
The SDK throws a `401` or a `403` at once, so a rejected token reports itself instead of a timeout.

Both shapes reject on failure.
A poll that runs out of time throws `kind: "timeout"`.
142 changes: 142 additions & 0 deletions pages/sdks/js/metros.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
---
title: Metros
description: How one client covers many metros, and how names behave across them
navigation_icon: earth
---

The platform API is per metro, but the instances of one account live in more than one metro.
The SDK treats the metros in scope as one namespace.
A read asks every metro at the same time and merges the answers, and every result carries the `metro` it came from.

## The scope

Set the scope for the client, for a group of calls, or for one call.

```ts
// The client. The default is every metro the account can reach.
const ukc = new UnikraftCloud({ token });
new UnikraftCloud({ token, metro: "fra" });
new UnikraftCloud({ token, metros: ["fra", "dal"] });

// A group of calls.
ukc.metro("fra").instances.list();
ukc.metros(["fra", "dal"]).instances.list();

// One call.
ukc.instances.list({ metros: ["fra", "dal"] });
ukc.instances.list({ metros: "all" });
```

An explicit scope skips metro discovery.
`UKC_METRO` works like `metro`, so it limits the client to that metro.

To see which metros the account can reach:

```ts
for (const { metro, baseUrl } of await ukc.availableMetros()) {
console.log(metro, baseUrl);
}
```

The client discovers the metros once, and it caches the result.
It asks the control plane and trusts the endpoint that the control plane reports, so a new [metro](/platform/metros) works without an SDK upgrade.
`KNOWN_METROS` lists the metros known at the release of this version:

| Code | Location |
| ----- | ------------------- |
| `fra` | Frankfurt, DE |
| `dal` | Dallas, TX, USA |
| `sin` | Singapore |
| `was` | Washington, DC, USA |
| `sfo` | San Francisco, USA |

## How operations behave in a scope

- **A read covers the whole scope.**
Pages arrive in the order that the metros answer, so a slow metro doesn't delay a fast one.
- **`create` never fans out.**
It needs one metro: the metro of the client, or the default target (`metro`, `UKC_METRO`, or `fra`) when the scope is wider.
- **A bulk operation acts on the whole scope.**
The SDK locates the refs first, then sends one call to each metro that matched.
In a wide scope, `delete([{ name: "web" }])` deletes every `web` in scope.
Make the scope narrower, or qualify the ref, to act on one resource.
- **The control plane is global.**
The scope doesn't affect it.

## Names across metros

A name identifies a resource within one metro.
The same name can exist in more than one metro at once, often because you deployed the same thing everywhere.
As a result, a name with a wide scope can match more than one resource.
A `{ uuid }` ref never has this problem, because a UUID identifies one resource wherever it lives.

You have three ways to say which resource you mean:

```ts
// 1. Qualify the ref. One request, no search.
await ukc.instances.get({ name: "web", metro: "fra" }).suspend();

// 2. Narrow the scope, which qualifies every ref in it.
await ukc.metro("fra").instances.get({ name: "web" }).suspend();

// 3. Address every metro that holds the name.
await ukc.instances.each({ name: "web" }).suspend();
```

### One match: `get()`

`get()` requires exactly one match, because the next step is often a mutation.
If a name matches in more than one metro, `get()` throws an `AmbiguousRefError`.
The error carries the matches, so a recovery costs no further request:

```ts
import { AmbiguousRefError } from "@unikraft/cloud";

try {
await ukc.instances.get({ name: "web" }).suspend();
} catch (err) {
if (err instanceof AmbiguousRefError) {
console.log(err.metros); // ["fra", "dal", "sin"]
console.log(err.matches); // the instances, each with its metro
}
}
```

### Every match: `each()`

`each(ref)` is the deliberate plural.
It resolves the matches once, then runs each operation in the metro that holds the match:

```ts
const web = ukc.instances.each({ name: "web" });

await web.where(); // ["fra", "dal", "sin"]
await web.size(); // 3
const instances = await web;
await web.suspend(); // one result per metro
await web.edit().set({ memory_mb: 512 }).apply();
```

An operation on a set returns an array.
`each()` exists on instances, volumes, services, and certificates.

## Partial failure

One unreachable metro doesn't discard the rest of the answer.
The SDK drains the healthy metros first, then it throws a `MetroFanoutError` that names the failures:

```ts
import { MetroFanoutError } from "@unikraft/cloud";

try {
for await (const inst of ukc.instances.list()) console.log(inst.name);
} catch (err) {
if (err instanceof MetroFanoutError) {
console.log(err.message); // "1 of 4 metros failed: sin (503)"
console.log(err.failures); // [{ metro: "sin", error: UnikraftCloudError }]
}
}
```

A bulk operation can't yield results as it goes, so the error carries the results that did succeed as `err.results`.
The same rule applies to a set from `each()`.
Loading
Loading