Skip to content
Draft
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
4 changes: 3 additions & 1 deletion cloud/k3s/docopts.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Usage:
k3s delete <server> [<user>]
k3s info
k3s kubeconfig <server> [<user>]
k3s registry <server> [<user>]
k3s status
```

Expand All @@ -38,5 +39,6 @@ Usage:
delete uninstall k3s with ssh in <server> using <username> with sudo
info info on the server
kubeconfig recover the kubeconfig from a k3s server <server> with user <username>
registry configure the private registry endpoint used by K3s containerd
status status of the server
```
```
12 changes: 12 additions & 0 deletions cloud/k3s/opsfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ tasks:
k3sup install --k3s-version="{{.K3S_VERSION}}"
--host="{{._server_}}" --user="{{.INSTALL_USER}}"
--local-path=$OPS_TMP/kubeconfig
- bun registry.ts "{{._server_}}" "{{.INSTALL_USER}}"


cert-manager:
Expand All @@ -78,10 +79,21 @@ tasks:
desc: create a k3s with ssh in <server> using <username> with sudo
cmds:
- config OPERATOR_CONFIG_KUBE=k3s
- config REGISTRY_CONFIG_PULL_HOSTNAME="{{._server_}}:32000"
- task: install
- task: cert-manager
- cp "$OPS_TMP/kubeconfig" "$OPS_TMP/k3s-{{._server_}}.kubeconfig"

registry:
silent: true
desc: configure the OpenServerless registry endpoint on a K3s node
vars:
INSTALL_USER: '{{._user_ | default "root"}}'
cmds:
- test -n "{{._server_}}" || die "required <server> ip or hostname"
- bun registry.ts "{{._server_}}" "{{.INSTALL_USER}}"
- config REGISTRY_CONFIG_PULL_HOSTNAME="{{._server_}}:32000"

delete:
silent: true
desc: uninstall with ssh in <server> using <username> with sudo
Expand Down
33 changes: 33 additions & 0 deletions cloud/k3s/registry.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

import {expect, test} from "bun:test";
import {renderRegistryConfig} from "./registry";

test("renders an explicit HTTP mirror for the K3s node registry", () => {
expect(renderRegistryConfig("192.0.2.10")).toBe(
"# Managed by Apache OpenServerless\n" +
"mirrors:\n" +
" \"192.0.2.10:32000\":\n" +
" endpoint:\n" +
" - \"http://192.0.2.10:32000\"\n",
);
});

test("rejects shell syntax in a K3s host", () => {
expect(() => renderRegistryConfig("host;reboot")).toThrow("invalid K3s server");
});
97 changes: 97 additions & 0 deletions cloud/k3s/registry.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
// Licensed to the Apache Software Foundation (ASF) under one
// or more contributor license agreements. See the NOTICE file
// distributed with this work for additional information
// regarding copyright ownership. The ASF licenses this file
// to you under the Apache License, Version 2.0 (the
// "License"); you may not use this file except in compliance
// with the License. You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.

const HOST = /^[A-Za-z0-9][A-Za-z0-9.-]*$/;
const USER = /^[A-Za-z0-9_][A-Za-z0-9_.-]*$/;

export function renderRegistryConfig(server: string): string {
if (!HOST.test(server)) throw new Error(`invalid K3s server: ${server}`);
return `# Managed by Apache OpenServerless\nmirrors:\n "${server}:32000":\n endpoint:\n - "http://${server}:32000"\n`;
}

async function capture(args: string[]): Promise<{code: number; stdout: string; stderr: string}> {
const process = Bun.spawn(args, {stdout: "pipe", stderr: "pipe"});
const [stdout, stderr, code] = await Promise.all([
new Response(process.stdout).text(),
new Response(process.stderr).text(),
process.exited,
]);
return {code, stdout, stderr};
}

async function writeRemote(args: string[], content: string): Promise<void> {
const process = Bun.spawn(args, {stdin: "pipe", stdout: "inherit", stderr: "inherit"});
process.stdin.write(content);
process.stdin.end();
const code = await process.exited;
if (code !== 0) throw new Error(`command failed (${code}): ${args.join(" ")}`);
}

async function configure(server: string, user: string): Promise<void> {
if (!USER.test(user)) throw new Error(`invalid SSH user: ${user}`);
const destination = `${user}@${server}`;
const ssh = ["ssh", "-oStrictHostKeyChecking=no", destination];
const expected = renderRegistryConfig(server);
const current = await capture([...ssh, "sudo", "cat", "/etc/rancher/k3s/registries.yaml"]);
if (current.code === 0 && current.stdout === expected) {
console.log(`K3s registry already configured for ${server}:32000`);
return;
}
if (current.code === 0 && current.stdout.trim()) {
throw new Error(
"/etc/rancher/k3s/registries.yaml already contains unmanaged configuration; refusing to overwrite it",
);
}

const exists = await capture([
...ssh, "sudo", "test", "-e", "/etc/rancher/k3s/registries.yaml",
]);
if (exists.code === 0) {
throw new Error("cannot read the existing /etc/rancher/k3s/registries.yaml");
}

const directory = await capture([...ssh, "sudo", "mkdir", "-p", "/etc/rancher/k3s"]);
if (directory.code !== 0) throw new Error(directory.stderr || "cannot create K3s config directory");
await writeRemote(
[...ssh, "sudo", "tee", "/etc/rancher/k3s/registries.yaml"],
expected,
);
const restart = await capture([...ssh, "sudo", "systemctl", "restart", "k3s"]);
if (restart.code !== 0) throw new Error(restart.stderr || "cannot restart K3s");

for (let attempt = 0; attempt < 60; attempt += 1) {
const active = await capture([...ssh, "sudo", "systemctl", "is-active", "k3s"]);
if (active.code === 0 && active.stdout.trim() === "active") {
console.log(`K3s registry configured for ${server}:32000`);
return;
}
await Bun.sleep(1000);
}
throw new Error("K3s did not become active after registry configuration");
}

if (import.meta.main) {
const args = Bun.argv.slice(2);
if (args[0] === "--render") {
console.log(renderRegistryConfig(args[1]));
} else {
const server = args[0];
const user = args[1] || "root";
if (!server) throw new Error("K3s server is required");
await configure(server, user);
}
}
42 changes: 42 additions & 0 deletions ide/deploy/Builder.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
# Automatic runtime image builder

`ops ide deploy` can ensure custom runtime images before updating actions. The
build runs as a Kubernetes BuildKit Job through the OpenServerless system API;
the developer machine does not need Docker.

Projects opt in with generic runtime profiles in their root `package.json`:

```json
{
"openserverless": {
"runtimeProfiles": {
"python-custom": {
"builder": "python:3.13",
"requirements": "runtime/python-custom.txt",
"actions": ["samples/python-custom"]
}
}
}
}
```

Each action can belong to at most one profile. Requirements paths are relative
to the project root and cannot escape it. The builder id is resolved by the
server from an allowlisted catalog; projects cannot select an arbitrary source
image, target repository, namespace, or registry.

The system API derives a content digest from the builder contract, source
image, and requirements. It either returns an existing image or starts a Job.
The deploy task waits for a terminal state and only then adds `--docker` to the
action update. A failed or timed-out build makes `ops ide deploy` fail before
the action is changed.

An explicit `--docker` action argument takes precedence over the generated
profile image. `--dry-run` validates and reports profiles without contacting
the builder API.

Registry endpoints are supplied by cluster configuration:

- the push endpoint is reachable from the BuildKit pod;
- the pull endpoint is reachable from the node container runtime;
- neither endpoint is hardcoded in the deploy task.
Loading