diff --git a/mcp-server-simple-distroless/Dockerfile b/mcp-server-simple-distroless/Dockerfile new file mode 100644 index 00000000..e1f3a80b --- /dev/null +++ b/mcp-server-simple-distroless/Dockerfile @@ -0,0 +1,16 @@ +FROM python:3.11-slim AS build + +WORKDIR /app + +RUN set -xe; \ + /usr/sbin/ldconfig /usr/local/lib + +COPY requirements.txt requirements.txt +RUN pip install -r requirements.txt + + +FROM gcr.io/distroless/python3-debian12 + +COPY --from=build /usr/local/lib/python3.11/site-packages /usr/local/lib/python3.11/dist-packages + +COPY ./server.py /src/server.py diff --git a/mcp-server-simple-distroless/Kraftfile b/mcp-server-simple-distroless/Kraftfile new file mode 100644 index 00000000..584d2467 --- /dev/null +++ b/mcp-server-simple-distroless/Kraftfile @@ -0,0 +1,12 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: [ "/usr/bin/python3", "/src/server.py" ] diff --git a/mcp-server-simple-distroless/README.md b/mcp-server-simple-distroless/README.md new file mode 100644 index 00000000..05262b7d --- /dev/null +++ b/mcp-server-simple-distroless/README.md @@ -0,0 +1,265 @@ +# Simple Distroless MCP Server + +This example demonstrates how to build a minimal custom MCP (Model Context Protocol) server from scratch using [FastMCP](https://github.com/jlowin/fastmcp). + +This is a great starting point for building your own custom MCP servers with business-specific tools and logic. + +## Running on Unikraft Cloud + +To run this MCP server on Unikraft Cloud: + +1. Install the CLI. + Use the [unikraft CLI](https://unikraft.com/docs/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/mcp-server-simple-distroless/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/mcp-server-simple-distroless/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +When done, invoke the following command to deploy this app on Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build . --output /mcp-server-simple-distroless:latest +unikraft run --metro fra \ + -m 512M \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ + --image /mcp-server-simple-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud deploy \ + -M 512Mi \ + -p 443:8080/tls+http \ + --scale-to-zero on \ + --scale-to-zero-stateful \ + --scale-to-zero-cooldown 1s \ + . +``` + +The output shows your instance details: + +**Using the unikraft CLI (Recommended)** +```ansi title="unikraft" +metro: fra +name: mcp-server-simple-distroless-bbdcb +uuid: e87d3591-3497-4f30-bd76-1dc886059647 +state: starting +image: /mcp-server-simple-distroless +resources: + memory: 512MiB + vcpus: 1 +service: + uuid: 3cfc78af-748d-c6ba-5bfb-427b3bd7e9aa + name: cool-paper-b6mht7jv + domains: + - fqdn: cool-paper-b6mht7jv.fra.unikraft.app +networks: +- uuid: 6a712117-5b67-d89a-7e2e-4a57e1732724 + private-ip: 10.0.0.193 + mac: 12:b0:39:2b:a3:15 +timestamps: + created: just now +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: mcp-server-simple-distroless-bbdcb + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: e87d3591-3497-4f30-bd76-1dc886059647 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: https://cool-paper-b6mht7jv.fra.unikraft.app + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//mcp-server-simple-distroless@sha256:cbbfb441ee313a6c7c0de571e9002f0f6031312e203ffb6be3b8f4950df3bc20 + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 512 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: cool-paper-b6mht7jv + β”œβ”€ private fqdn: mcp-server-simple-distroless-bbdcb.internal + └─── private ip: 10.0.0.193 +``` + +In this case, the instance name is `mcp-server-simple-distroless-bbdcb` and the service `cool-paper-b6mht7jv`. +They're different for each run. + +For testing, you can use the example client included in this directory. +First, [install `uv`](https://docs.astral.sh/uv/getting-started/installation) if you haven't already, then run: + +```bash +export MCP_SERVER_URL=https://cool-paper-b6mht7jv.fra.unikraft.app/mcp +uv run client.py +``` + +```bash +Connecting to https://cool-paper-b6mht7jv.fra.unikraft.app/mcp... + +--- Listing Tools --- +Name: get_weather +Description: Get current weather for a city. + +Args: + city: Name of the city + +Returns: + Weather information including temperature, conditions, and humidity + +Name: get_time +Description: Get current time in a timezone. +... +``` + +You can list information about the instance by running: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances list +``` + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN + CREATED +fra mcp-server-simple-distroless-bbdcb standby /mcp-server-simple-distroless 512MiB 1 cool-paper-b6mht7jv.fra.unikraft.app 2 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance list +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE + MEMORY VCPUS ARGS BOOT TIME +mcp-server-simple-distroless-bbdcb cool-paper-b6mht7jv.fra.unikraft.app standby standby oci://unikraft.io//mcp-server-simple-distroless@sha256:... 512 MiB 1 9.15 ms +``` + +When done, you can delete the instance with: + +```bash +kraft cloud instance remove mcp-server-simple-distroless-bbdcb +``` + +## Available tools + +This MCP Server provides the following tools: + +* **get_weather**: Get simulated weather for a city +* **get_time**: Get current time in a timezone +* **calculate**: Perform basic arithmetic operations (add, subtract, multiply, divide) + +## Building your own tools + +This example shows the basic structure of an MCP server using FastMCP. +To add your own tools: + +1. **Define the tool** using the `@mcp.tool()` decorator: + * Add type hints for parameters + * Include a docstring describing what the tool does + * Parameters are automatically converted to JSON schema + +1. **Write the tool** function: + * Process the parameters + * Return a string result (or raise an exception for errors) + +1. **Test locally** before deploying: + + ```bash + pip install -r requirements.txt + python server.py + ``` + +Example: + +```python +from fastmcp import FastMCP + +mcp = FastMCP("My Custom Server") + +@mcp.tool() +def my_custom_tool(param1: str, param2: int) -> str: + """Description of what this tool does. + + Args: + param1: Description of first parameter + param2: Description of second parameter + + Returns: + Result description + """ + # Your implementation here + return f"Processed {param1} with {param2}" + +if __name__ == "__main__": + mcp.run(transport="http", host="0.0.0.0", port=8080) +``` + +## Architecture + +The server uses FastMCP, a high-level framework for building MCP servers that features: + +* **HTTP Transport**: Built-in Streamable HTTP transport +* **Type-Safe**: Automatic JSON schema generation from Python type hints +* **Simple API**: Decorator-based tool definitions + +This provides: + +* Standard MCP protocol support +* Automatic tool schema generation +* Built-in HTTP server +* Easy deployment to any platform + +## Learn more + +* [MCP Documentation](https://modelcontextprotocol.io/) +* [FastMCP documentation](https://gofastmcp.com/getting-started/welcome) +* [Building MCP Servers](https://modelcontextprotocol.io/docs/building-servers) +* [Unikraft Cloud Documentation](https://unikraft.com/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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). diff --git a/mcp-server-simple-distroless/client.py b/mcp-server-simple-distroless/client.py new file mode 100644 index 00000000..cc49b578 --- /dev/null +++ b/mcp-server-simple-distroless/client.py @@ -0,0 +1,90 @@ +"""Simple MCP Server Python Client +------------------------------- + +This script demonstrates how to connect to the Simple MCP server deployed on Unikraft Cloud using Streamable HTTP transport. +It lists available tools and tests each one. + +Usage: + export MCP_SERVER_URL=https://your-server-url/mcp + uv run client.py +""" +# /// script +# requires-python = ">=3.11" +# dependencies = [ +# "fastmcp", +# ] +# /// + +import asyncio +import os +from fastmcp import Client + + +async def main(): + url = os.getenv("MCP_SERVER_URL", None) + if url is None: + print("Error: MCP_SERVER_URL environment variable is not set.") + print("Please set it to the server's MCP endpoint URL (e.g., https://your-server-url/mcp).") + return + + print(f"Connecting to {url}...") + + try: + async with Client(url) as client: + # List available tools + print("\n--- Listing Tools ---") + tools = await client.list_tools() + for tool in tools: + print(f"Name: {tool.name}") + print(f"Description: {tool.description}") + print() + + # Test weather tool + print("\n--- Testing Weather Tool ---") + try: + result = await client.call_tool( + "get_weather", + arguments={"city": "London"} + ) + for content in result.content: + if content.type == "text": + print(content.text) + + except Exception as e: + print(f"Error calling weather tool: {e}") + + # Test time tool + print("\n--- Testing Time Tool ---") + try: + result = await client.call_tool( + "get_time", + arguments={"timezone": "America/New_York"} + ) + for content in result.content: + if content.type == "text": + print(content.text) + + except Exception as e: + print(f"Error calling time tool: {e}") + + # Test calculate tool + print("\n--- Testing Calculate Tool ---") + try: + result = await client.call_tool( + "calculate", + arguments={"operation": "add", "a": 10, "b": 5} + ) + for content in result.content: + if content.type == "text": + print(content.text) + + except Exception as e: + print(f"Error calling calculate tool: {e}") + + except Exception as e: + print(f"Connection error: {e}") + print("Make sure the URL is correct and the server is running.") + + +if __name__ == "__main__": + asyncio.run(main()) diff --git a/mcp-server-simple-distroless/requirements.txt b/mcp-server-simple-distroless/requirements.txt new file mode 100644 index 00000000..ee766127 --- /dev/null +++ b/mcp-server-simple-distroless/requirements.txt @@ -0,0 +1,72 @@ +Authlib==1.7.2 +PyJWT==2.12.1 +PyYAML==6.0.3 +Pygments==2.20.0 +SecretStorage==3.5.0 +aiofile==3.9.0 +annotated-types==0.7.0 +anyio==4.13.0 +attrs==26.1.0 +backports.tarfile==1.2.0 +beartype==0.22.9 +cachetools==7.1.1 +caio==0.9.25 +certifi==2026.4.22 +cffi==2.0.0 +click==8.3.3 +cryptography==48.0.0 +cyclopts==4.11.2 +dnspython==2.8.0 +docstring_parser==0.18.0 +docutils==0.22.4 +email-validator==2.3.0 +exceptiongroup==1.3.1 +fastmcp==3.2.4 +griffelib==2.0.2 +h11==0.16.0 +httpcore==1.0.9 +httpx-sse==0.4.3 +httpx==0.28.1 +idna==3.15 +importlib_metadata==8.7.1 +jaraco.classes==3.4.0 +jaraco.context==6.1.2 +jaraco.functools==4.4.0 +jeepney==0.9.0 +joserfc==1.6.5 +jsonref==1.1.0 +jsonschema-path==0.4.6 +jsonschema-specifications==2025.9.1 +jsonschema==4.26.0 +keyring==25.7.0 +markdown-it-py==4.0.0 +mcp==1.27.0 +mdurl==0.1.2 +more-itertools==11.0.2 +openapi-pydantic==0.5.1 +opentelemetry-api==1.41.1 +packaging==26.2 +pathable==0.5.0 +platformdirs==4.9.6 +py-key-value-aio==0.4.4 +pycparser==3.0 +pydantic-settings==2.14.0 +pydantic==2.13.3 +pydantic_core==2.46.3 +pyperclip==1.11.0 +python-dotenv==1.2.2 +python-multipart==0.0.27 +pytz==2026.2 +referencing==0.37.0 +rich-rst==1.3.2 +rich==15.0.0 +rpds-py==0.30.0 +sse-starlette==3.4.1 +starlette==1.0.1 +typing-inspection==0.4.2 +typing_extensions==4.15.0 +uncalled-for==0.3.1 +uvicorn==0.46.0 +watchfiles==1.1.1 +websockets==16.0 +zipp==3.23.1 diff --git a/mcp-server-simple-distroless/server.py b/mcp-server-simple-distroless/server.py new file mode 100644 index 00000000..399c8483 --- /dev/null +++ b/mcp-server-simple-distroless/server.py @@ -0,0 +1,104 @@ +"""Simple Custom Weather MCP Server using FastMCP.""" + +from datetime import datetime +import random +import pytz +from fastmcp import FastMCP + +# Simulated weather data +WEATHER_DATA = { + 'London': {'temp_range': (10, 20), 'conditions': ['Rainy', 'Cloudy', 'Partly cloudy']}, + 'New York': {'temp_range': (15, 25), 'conditions': ['Sunny', 'Cloudy', 'Clear']}, + 'Tokyo': {'temp_range': (18, 28), 'conditions': ['Humid', 'Partly cloudy', 'Clear']}, + 'Paris': {'temp_range': (12, 22), 'conditions': ['Cloudy', 'Rainy', 'Partly cloudy']}, + 'Berlin': {'temp_range': (8, 18), 'conditions': ['Cloudy', 'Rainy', 'Clear']}, +} + +mcp = FastMCP("Simple Weather MCP Server") + + +@mcp.tool() +def get_weather(city: str) -> str: + """Get current weather for a city. + + Args: + city: Name of the city + + Returns: + Weather information including temperature, conditions, and humidity + """ + # Get city data or use default + city_data = WEATHER_DATA.get(city, { + 'temp_range': (15, 25), + 'conditions': ['Partly cloudy', 'Clear', 'Cloudy'] + }) + + # Generate random weather + temp = random.randint(*city_data['temp_range']) + conditions = random.choice(city_data['conditions']) + humidity = random.randint(40, 80) + + result = f"""Weather for {city}: +Temperature: {temp}Β°C +Conditions: {conditions} +Humidity: {humidity}% +Timestamp: {datetime.utcnow().isoformat()}""" + + return result + + +@mcp.tool() +def get_time(timezone: str) -> str: + """Get current time in a timezone. + + Args: + timezone: IANA timezone (e.g., America/New_York, Europe/London) + + Returns: + Current time, date, day, and UTC offset for the specified timezone + """ + try: + tz = pytz.timezone(timezone) + current_time = datetime.now(tz) + + result = f"""Time in {timezone}: +Time: {current_time.strftime('%H:%M:%S')} +Date: {current_time.strftime('%Y-%m-%d')} +Day: {current_time.strftime('%A')} +UTC Offset: {current_time.strftime('%z')}""" + + return result + except pytz.exceptions.UnknownTimeZoneError: + return f"Error: Unknown timezone {timezone}" + + +@mcp.tool() +def calculate(operation: str, a: float, b: float) -> str: + """Perform a simple calculation. + + Args: + operation: Mathematical operation (add, subtract, multiply, divide) + a: First operand + b: Second operand + + Returns: + Result of the calculation + """ + if operation == 'add': + result = a + b + elif operation == 'subtract': + result = a - b + elif operation == 'multiply': + result = a * b + elif operation == 'divide': + if b == 0: + return "Error: Division by zero" + result = a / b + else: + return f"Error: Unknown operation {operation}" + + return f"{a} {operation} {b} = {result}" + + +if __name__ == "__main__": + mcp.run(transport="http", host="0.0.0.0", port=8080, path="/mcp") diff --git a/mcp-server-simple-distroless/test_mcp-server-simple-distroless.py b/mcp-server-simple-distroless/test_mcp-server-simple-distroless.py new file mode 100644 index 00000000..4de12ac4 --- /dev/null +++ b/mcp-server-simple-distroless/test_mcp-server-simple-distroless.py @@ -0,0 +1,48 @@ +"""End-to-end test for the ``mcp-server-simple-distroless`` example. + +Mirrors the manual steps from ``mcp-server-simple-distroless/README.md``: + +1. ``unikraft build . --output /mcp-server-simple-distroless:`` +2. ``unikraft run --metro -p 443:8080/tls+http -m 512M --image ...`` +3. Connect to the MCP server endpoint and verify it responds. + +The MCP server exposes its HTTP transport at ``/mcp``. We verify the +server is reachable by issuing an MCP ``initialize`` request. +""" + +from __future__ import annotations + +from _testlib.unikraft import extract_instance_name, extract_instance_url + + +def test_mcp_server_simple_responds(build_image, run_instance, http_post, wait_instance): + image = build_image("mcp-server-simple-distroless", "mcp-server-simple-distroless") + + instance = run_instance( + image, + publish=["443:8080/tls+http"], + memory="512M", + ) + + url = extract_instance_url(instance) + assert url, f"could not determine instance URL from: {instance!r}" + + wait_instance(extract_instance_name(instance), "running") + + # Send an MCP initialize request to the server's HTTP transport endpoint. + resp = http_post( + f"{url}/mcp", + json={ + "jsonrpc": "2.0", + "id": 1, + "method": "initialize", + "params": { + "protocolVersion": "2024-11-05", + "capabilities": {}, + "clientInfo": {"name": "pytest", "version": "1.0.0"}, + }, + }, + headers={"Content-Type": "application/json", "Accept": "application/json, text/event-stream"}, + timeout=30, + ) + assert resp.status_code == 200 diff --git a/nginx-distroless/Dockerfile b/nginx-distroless/Dockerfile new file mode 100644 index 00000000..9f679732 --- /dev/null +++ b/nginx-distroless/Dockerfile @@ -0,0 +1,4 @@ +FROM distrolessdevops/nginx-distroless:1.25.3 + +# Custom configuration files, including using a single process for Nginx +COPY ./rootfs/ / diff --git a/nginx-distroless/Kraftfile b/nginx-distroless/Kraftfile new file mode 100644 index 00000000..2fc26dd9 --- /dev/null +++ b/nginx-distroless/Kraftfile @@ -0,0 +1,12 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: ["/usr/sbin/nginx", "-c", "/etc/nginx/nginx.conf"] diff --git a/nginx-distroless/README.md b/nginx-distroless/README.md new file mode 100644 index 00000000..3a110f1b --- /dev/null +++ b/nginx-distroless/README.md @@ -0,0 +1,215 @@ +# Distroless Nginx + +This example uses [`Nginx`](https://nginx.org), one of the most popular web servers. +Nginx can be used with Unikraft / Unikraft Cloud to serve static web content. + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](https://unikraft.com/docs/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/nginx-distroless/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/nginx-distroless/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +When done, invoke the following command to deploy this app on Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build . --output /nginx-distroless:latest +unikraft run --metro fra \ + -m 256M \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000 \ + --image /nginx-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud deploy \ + -M 256Mi \ + -p 443:8080/tls+http \ + --scale-to-zero on \ + --scale-to-zero-cooldown 1s \ + . +``` + +The output shows the instance address and other details: + +**Using the unikraft CLI (Recommended)** +```ansi title="unikraft" +metro: fra +name: nginx-distroless-67zbu +uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa +state: starting +image: /nginx-distroless +resources: + memory: 256MiB + vcpus: 1 +service: + uuid: a942b9b5-ad17-3ffe-dcd2-ef4331f9087a + name: nameless-fog-0tvh1uov + domains: + - fqdn: nameless-fog-0tvh1uov.fra.unikraft.app +networks: +- uuid: 62d9bbf0-aec8-61f6-7bdb-86edf63dd068 + private-ip: 10.0.3.3 + mac: 12:b0:c6:23:ed:15 +timestamps: + created: just now +scale-to-zero: + enabled: true + policy: on + cooldown-time: 1s +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: nginx-distroless-67zbu + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: 8a8bc1b9-0af6-420e-a426-190dc2da9eaa + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: https://nameless-fog-0tvh1uov.fra.unikraft.app + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//nginx-distroless@sha256:f51ecc121c9ca34abb88a2bc6a69765501304f7893f7e85af15fbec3dc86e2bd + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 256 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: nameless-fog-0tvh1uov + β”œβ”€ private fqdn: nginx-distroless-67zbu.internal + └─── private ip: 10.0.3.3 +``` + +In this case, the instance name is `nginx-distroless-67zbu` and the address is `https://nameless-fog-0tvh1uov.fra.unikraft.app`. +They're different for each run. + +Use `curl` to query the Unikraft Cloud instance of Nginx. + +```bash +curl https://nameless-fog-0tvh1uov.fra.unikraft.app +``` + +```text + + + +Welcome to nginx! +[...] +``` + +You can list information about the instance by running: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances list +``` + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra nginx-distroless-67zbu running /nginx-distroless 256MiB 1 nameless-fog-0tvh1uov.fra.unikraft.app 2 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance list +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE +MEMORY VCPUS ARGS BOOT TIME +nginx-distroless-67zbu nameless-fog-0tvh1uov.fra.unikraft.app running 5 minutes ago oci://unikraft.io//nginx-distroless@sha256:... 256 MiB 1 11.13 ms +``` + +When done, you can remove the instance: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances delete nginx-distroless-67zbu +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance remove nginx-distroless-67zbu +``` + +## Customize your app + +To customize the Nginx app, update the files in the repository, listed below: + +* `Kraftfile`: the Unikraft Cloud specification +* `rootfs/wwwroot/index.html`: the index page of the content served +* `rootfs/etc/nginx/nginx.conf`: the Nginx configuration file + +Update the contents of the `rootfs/wwwroot/` directory to serve different static web content. +For example, you could change the contents of `rootfs/wwwroot/index.html` to: + +```html + + + +Hello + + +

Hello, World!

+ + +``` + +After re-deploying the Nginx image on Unikraft Cloud, using `curl` or a browser to query it will present the new page contents. + +Tools like [`Jekyll`](https://jekyllrb.com/) or [`Hugo`](https://gohugo.io/) can generate the static web content located in the `rootfs/wwwroot/` offline. + +If required, you can also customize the configuration of Nginx in `rootfs/etc/nginx/nginx.conf`. +You can set a new webroot (different than `wwwroot`), or a different internal port, or a different index page, etc. + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). diff --git a/nginx-distroless/rootfs/etc/nginx/nginx.conf b/nginx-distroless/rootfs/etc/nginx/nginx.conf new file mode 100644 index 00000000..7ee052f1 --- /dev/null +++ b/nginx-distroless/rootfs/etc/nginx/nginx.conf @@ -0,0 +1,31 @@ +worker_processes 1; +daemon off; +master_process off; +user root root; + +events { + worker_connections 64; +} + +http { + include mime.types; + default_type application/octet-stream; + + open_file_cache max=10000 inactive=30s; + open_file_cache_min_uses 2; + open_file_cache_errors on; + + error_log stderr error; + access_log off; + + keepalive_timeout 10s; + keepalive_requests 10000; + send_timeout 10s; + + server { + listen 8080; + server_name localhost; + root /wwwroot; + index index.html; + } +} diff --git a/nginx-distroless/rootfs/wwwroot/index.html b/nginx-distroless/rootfs/wwwroot/index.html new file mode 100644 index 00000000..2ca3b954 --- /dev/null +++ b/nginx-distroless/rootfs/wwwroot/index.html @@ -0,0 +1,25 @@ + + + +Welcome to nginx! + + + +

Welcome to nginx!

+

If you see this page, the nginx web server is successfully installed and +working. Further configuration is required.

+ +

For online documentation and support please refer to +nginx.org.
+Commercial support is available at +nginx.com.

+ +

Thank you for using nginx.

+ + diff --git a/nginx-distroless/test_nginx-distroless.py b/nginx-distroless/test_nginx-distroless.py new file mode 100644 index 00000000..63a0649d --- /dev/null +++ b/nginx-distroless/test_nginx-distroless.py @@ -0,0 +1,31 @@ +"""End-to-end test for the ``nginx-distroless`` example. + +Mirrors the manual steps from ``nginx-distroless/README.md``: + +1. ``unikraft build . --output /nginx-distroless:`` +2. ``unikraft run --metro -p 443:8080/tls+http -m 256M --image ...`` +3. ``curl https://`` and assert the default Nginx welcome page. +""" + +from __future__ import annotations + +from _testlib.unikraft import extract_instance_name, extract_instance_url + + +def test_nginx_serves_welcome_page(build_image, run_instance, http, wait_instance): + image = build_image("nginx-distroless", "nginx-distroless") + + instance = run_instance( + image, + publish=["443:8080/tls+http"], + memory="256M", + ) + + url = extract_instance_url(instance) + assert url, f"could not determine instance URL from: {instance!r}" + + wait_instance(extract_instance_name(instance), "running") + + resp = http(url) + assert resp.status_code == 200 + assert "Welcome to nginx" in resp.text diff --git a/nginx-flask-mongo-distroless/README.md b/nginx-flask-mongo-distroless/README.md new file mode 100644 index 00000000..22695583 --- /dev/null +++ b/nginx-flask-mongo-distroless/README.md @@ -0,0 +1,408 @@ +# Distroless Flask with Distroless MongoDB + +[Flask](https://flask.palletsprojects.com/en/stable/) is a lightweight WSGI web application framework in Python, and [MongoDB](https://www.mongodb.com/) is a NoSQL database that stores data in JSON-like documents. +This example deploys three services on Unikraft Cloud: NGINX (reverse proxy), Flask (backend), and MongoDB (database). + +**Credits**: This example is based on this [Awesome Compose example](https://github.com/docker/awesome-compose/tree/master/nginx-flask-mongo). + +## Deployment + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](https://unikraft.com/docs/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/nginx-flask-mongo-distroless` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/nginx-flask-mongo-distroless/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +## MongoDB + +Create a volume for MongoDB data persistence: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft volume create --metro fra --name mongo-data --size 1G +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud volume create --name mongo-data --size 1Gi +``` + +You can list the created volume by running: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft volume list +``` + +```ansi title="unikraft" +METRO NAME STATE SIZE CREATED +fra mongo-data available 1GiB just now +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud volume list +``` + +```ansi title="kraft" +NAME CREATED AT SIZE ATTACHED TO MOUNTED BY STATE PERSISTENT +mongo-data now 1.0 GiB available true +``` + +First, deploy the MongoDB instance. +MongoDB is an internal service (not publicly accessible), reached via the `mongo.internal` domain: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build ./mongo --output /mongo-distroless:latest +unikraft run --metro fra \ + -m 1024M \ + --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ + --domain mongo.internal \ + --volume mongo-data:/data/db \ + --image /mongo-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud deploy \ + -M 1024Mi \ + --scale-to-zero idle \ + --scale-to-zero-stateful \ + --scale-to-zero-cooldown 1s \ + --domain mongo.internal \ + --volume mongo-data:/data/db \ + ./mongo +``` + +The output shows the MongoDB instance details: + +**Using the unikraft CLI (Recommended)** +```text title="unikraft" +metro: fra +name: mongo-distroless-o3qhq +uuid: 90158c53-6654-4e73-bad1-1d6ab4452001 +state: starting +image: /mongo-distroless +resources: + memory: 1GiB + vcpus: 1 +service: + name: restless-glade-l8pu2mf0 + uuid: 77a04441-2479-433a-b468-32f23e475f58 + domains: + - fqdn: mongo.internal +volumes: +- name: mongo-data + uuid: 9c7723f3-7e1f-4e06-afe6-c811240faf5a + at: /data/db +networks: +- uuid: 4f891227-d381-42f4-88a4-25a97b95a9e3 + private-ip: 10.0.15.21 + mac: 12:b0:0a:00:0f:15 +timestamps: + created: just now +scale-to-zero: + enabled: true + policy: idle + stateful: true + cooldown-time: 1s +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: mongo-distroless-o3qhq + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: 90158c53-6654-4e73-bad1-1d6ab4452001 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: mongo.internal + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//mongo-distroless@sha256:68894454735e0e5b07d61aad19b1c03355f415ec33c050daeaa419d931962657 + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 1024 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: restless-glade-l8pu2mf0 + β”œβ”€ private fqdn: mongo-distroless-o3qhq.internal + └─── private ip: 10.0.15.21 +``` + +## Flask + +Next, deploy the Flask backend. +It connects to MongoDB using the `MONGO_SERVER_URL` environment variable and is reached internally via `backend.internal`: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build ./flask --output /flask-distroless:latest +unikraft run --metro fra \ + -m 1024M \ + --scale-to-zero policy=on,cooldown-time=1000 \ + --domain backend.internal \ + --env FLASK_SERVER_PORT=9091 \ + --env MONGO_SERVER_URL=mongo.internal:27017 \ + --image /flask-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud deploy \ + -M 1024Mi \ + --scale-to-zero on \ + --scale-to-zero-cooldown 1s \ + --domain backend.internal \ + --env FLASK_SERVER_PORT=9091 \ + --env MONGO_SERVER_URL=mongo.internal:27017 \ + ./flask +``` + +The output shows the Flask instance details: + +**Using the unikraft CLI (Recommended)** +```text title="unikraft" +metro: fra +name: flask-distroless-9a68z +uuid: bb6d91f7-0714-45e5-b14a-ec82a5dac36e +state: starting +image: /flask-distroless +runtime: + env: + FLASK_SERVER_PORT: 9091 + MONGO_SERVER_URL: mongo.internal:27017 +resources: + memory: 1GiB + vcpus: 1 +service: + name: broken-bird-8isa6q21 + uuid: cd9fe757-784a-49d4-8936-1b6859b3a72d + domains: + - fqdn: backend.internal +networks: +- uuid: 2da7f679-7067-4fb5-908b-853607d383f2 + private-ip: 10.0.17.97 + mac: 12:b0:0a:00:11:61 +timestamps: + created: just now +scale-to-zero: + enabled: true + policy: on + cooldown-time: 1s +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: flask-distroless-9a68z + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: bb6d91f7-0714-45e5-b14a-ec82a5dac36e + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: backend.internal + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//flask-distroless@sha256:f23b3368cd777acae68ad8f35713a4cf55f901d6c266017bf6f0679ffc7a8172 + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 1024 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: broken-bird-8isa6q21 + β”œβ”€ private fqdn: flask-distroless-9a68z.internal + └─── private ip: 10.0.17.97 +``` + +## NGINX + +Finally, deploy NGINX as the public-facing reverse proxy. +It forwards requests to the Flask backend at `backend.internal:9091` by default. +To use a different backend domain, set the `BACKEND_HOST` environment variable to the same value you pass as the Flask domain. + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build ./nginx --output /nginx-distroless:latest +unikraft run --metro fra \ + -m 512M \ + -p 443:80/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000 \ + -e BACKEND_HOST=backend.internal \ + --image /nginx-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud deploy \ + -M 512Mi \ + -p 443:80/tls+http \ + --scale-to-zero on \ + --scale-to-zero-cooldown 1s \ + --env BACKEND_HOST=backend.internal \ + ./nginx +``` + +The output shows the NGINX instance details including its public FQDN: + +**Using the unikraft CLI (Recommended)** +```text title="unikraft" +metro: fra +name: nginx-distroless-jnpwi +uuid: 57f64e99-bd06-46fd-98f4-26b64751623e +state: starting +image: /nginx-distroless +resources: + memory: 512MiB + vcpus: 1 +service: + name: snowy-river-gotjeojl + uuid: 287ee3b8-43bc-47d1-a88e-4d6c72d2d682 + domains: + - fqdn: snowy-river-gotjeojl.fra.unikraft.app +networks: +- uuid: 107e4a03-e285-4d1d-84cb-24f86d7af875 + private-ip: 10.0.14.201 + mac: 12:b0:0a:00:0e:c9 +timestamps: + created: just now +scale-to-zero: + enabled: true + policy: on + cooldown-time: 1s +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: nginx-distroless-jnpwi + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: 57f64e99-bd06-46fd-98f4-26b64751623e + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: https://snowy-river-gotjeojl.fra.unikraft.app + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//nginx-distroless@sha256:8cff54392eeead80bafe33538866b04bfd076f2052d65cb3751a938a22368bc0 + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 512 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: snowy-river-gotjeojl + β”œβ”€ private fqdn: nginx-distroless-jnpwi.internal + └─── private ip: 10.0.14.201 +``` + +You can list all deployed instances with: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances list +``` + +```text title="unikraft" +METRO NAME STATE IMAGE MEMORY VCPUS FQDN CREATED +fra nginx-distroless-jnpwi standby /nginx-distroless 512MiB 1 snowy-river-gotjeojl.fra.unikraft.app 11 minutes ago +fra flask-distroless-9a68z standby /flask-distroless 1GiB 1 backend.internal 12 minutes ago +fra mongo-distroless-o3qhq standby /mongo-distroless 1GiB 1 mongo.internal 14 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance list +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE +MEMORY VCPUS ARGS BOOT TIME +nginx-distroless-jnpwi snowy-river-gotjeojl.fra.unikraft.app standby standby oci://unikraft.io//nginx-distroless@sha256:... 512 MiB 1 83.87 ms +flask-distroless-9a68z backend.internal running since 2mins oci://unikraft.io//flask-distroless@sha256:... 1.0 GiB 1 1916.54 ms +mongo-distroless-o3qhq mongo.internal running since 5mins oci://unikraft.io//mongo-distroless@sha256:... 1.0 GiB 1 2776.86 ms +``` + +## Test the deployment + +The FQDN of the NGINX instance can be found in the `FQDN` column of the `unikraft instances list` output above. +Use `curl` to query it (replace with your actual FQDN): + +```bash +curl https:// +``` + +```text +Hello from the MongoDB client! +``` + +## Clean up + +When done, remove the instances and volume: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances delete mongo-distroless-o3qhq flask-distroless-9a68z nginx-distroless-jnpwi +unikraft volume delete mongo-data +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance remove mongo-distroless-o3qhq flask-distroless-9a68z nginx-distroless-jnpwi +kraft cloud volume remove mongo-data +``` + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). + +- [Flask Documentation](https://flask.palletsprojects.com/en/stable/) +- [MongoDB Documentation](https://www.mongodb.com/docs/) +- [Nginx Documentation](https://nginx.org/en/docs/) +- [Awesome Compose](https://github.com/docker/awesome-compose) +- [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) +- [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) diff --git a/nginx-flask-mongo-distroless/flask/.dockerignore b/nginx-flask-mongo-distroless/flask/.dockerignore new file mode 100644 index 00000000..44563916 --- /dev/null +++ b/nginx-flask-mongo-distroless/flask/.dockerignore @@ -0,0 +1,6 @@ +Kraftfile +Dockerfile +.unikraft +initrd +rootfs +.dockerignore diff --git a/nginx-flask-mongo-distroless/flask/Dockerfile b/nginx-flask-mongo-distroless/flask/Dockerfile new file mode 100644 index 00000000..e685d068 --- /dev/null +++ b/nginx-flask-mongo-distroless/flask/Dockerfile @@ -0,0 +1,23 @@ +FROM python:3.12-bookworm AS builder + +WORKDIR /app + +COPY requirements.txt /app +RUN pip3 install -r requirements.txt --no-cache-dir + +COPY . /app + + +FROM gcr.io/distroless/cc-debian12 + +WORKDIR /app + +COPY --from=builder /usr/local/bin/python3.12 /usr/local/bin/python3.12 + +COPY --from=builder /usr/local/lib/libpython3.12.so.1.0 /usr/local/lib/libpython3.12.so.1.0 + +COPY --from=builder /usr/local/lib/python3.12 /usr/local/lib/python3.12 + +COPY --from=builder /usr/lib/x86_64-linux-gnu/libz.so.1 /usr/lib/x86_64-linux-gnu/libz.so.1 + +COPY --from=builder /app /app diff --git a/nginx-flask-mongo-distroless/flask/Kraftfile b/nginx-flask-mongo-distroless/flask/Kraftfile new file mode 100644 index 00000000..33db81b8 --- /dev/null +++ b/nginx-flask-mongo-distroless/flask/Kraftfile @@ -0,0 +1,17 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +labels: + cloud.unikraft.v1.instances/scale_to_zero.policy: "off" + cloud.unikraft.v1.instances/scale_to_zero.stateful: "false" + # cloud.unikraft.v1.instances/scale_to_zero.cooldown_time_ms: 1000 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: [ "/usr/local/bin/python3.12", "/app/server.py" ] diff --git a/nginx-flask-mongo-distroless/flask/requirements.txt b/nginx-flask-mongo-distroless/flask/requirements.txt new file mode 100644 index 00000000..d75be0c8 --- /dev/null +++ b/nginx-flask-mongo-distroless/flask/requirements.txt @@ -0,0 +1,2 @@ +pymongo +flask diff --git a/nginx-flask-mongo-distroless/flask/server.py b/nginx-flask-mongo-distroless/flask/server.py new file mode 100644 index 00000000..3669e36c --- /dev/null +++ b/nginx-flask-mongo-distroless/flask/server.py @@ -0,0 +1,32 @@ +#!/usr/bin/env python +import os + +from flask import Flask +from pymongo import MongoClient + +app = Flask(__name__) + +client_host = os.environ.get("MONGO_SERVER_URL", "mongo.internal:27017") +print(f"Connecting to MongoDB at {client_host}") + +try: + client = MongoClient(host=client_host, + directConnection=True, + serverSelectionTimeoutMS=2000, + appname="unikraft") +except Exception as e: + print(f"Error creating MongoDB client: {e}") + + +@app.route('/') +def todo(): + try: + client.admin.command('ismaster') + return "Hello from the MongoDB client!\n" + except Exception as e: + print(f"MongoDB connection error: {e}") + return "MongoDB server not available, but Flask is running!\n" + + +if __name__ == "__main__": + app.run(host='0.0.0.0', port=int(os.environ.get("FLASK_SERVER_PORT", 9090)), debug=False) diff --git a/nginx-flask-mongo-distroless/mongo/Dockerfile b/nginx-flask-mongo-distroless/mongo/Dockerfile new file mode 100644 index 00000000..14a4ed72 --- /dev/null +++ b/nginx-flask-mongo-distroless/mongo/Dockerfile @@ -0,0 +1,50 @@ +FROM mongo:6.0.13 AS build + +RUN mkdir /blank + + +FROM gcr.io/distroless/cc-debian12 + +# Binary executable +COPY --from=build /usr/bin/mongod /usr/bin/mongod + +# System libraries +COPY --from=build /lib/x86_64-linux-gnu/libcurl.so.4 /lib/x86_64-linux-gnu/libcurl.so.4 +COPY --from=build /lib/x86_64-linux-gnu/liblzma.so.5 /lib/x86_64-linux-gnu/liblzma.so.5 +COPY --from=build /lib/x86_64-linux-gnu/libresolv.so.2 /lib/x86_64-linux-gnu/libresolv.so.2 +COPY --from=build /lib/x86_64-linux-gnu/libnghttp2.so.14 /lib/x86_64-linux-gnu/libnghttp2.so.14 +COPY --from=build /lib/x86_64-linux-gnu/libidn2.so.0 /lib/x86_64-linux-gnu/libidn2.so.0 +COPY --from=build /lib/x86_64-linux-gnu/librtmp.so.1 /lib/x86_64-linux-gnu/librtmp.so.1 +COPY --from=build /lib/x86_64-linux-gnu/libssh.so.4 /lib/x86_64-linux-gnu/libssh.so.4 +COPY --from=build /lib/x86_64-linux-gnu/libpsl.so.5 /lib/x86_64-linux-gnu/libpsl.so.5 +COPY --from=build /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 +COPY --from=build /lib/x86_64-linux-gnu/libldap-2.5.so.0 /lib/x86_64-linux-gnu/libldap-2.5.so.0 +COPY --from=build /lib/x86_64-linux-gnu/liblber-2.5.so.0 /lib/x86_64-linux-gnu/liblber-2.5.so.0 +COPY --from=build /lib/x86_64-linux-gnu/libzstd.so.1 /lib/x86_64-linux-gnu/libzstd.so.1 +COPY --from=build /lib/x86_64-linux-gnu/libbrotlidec.so.1 /lib/x86_64-linux-gnu/libbrotlidec.so.1 +COPY --from=build /lib/x86_64-linux-gnu/libz.so.1 /lib/x86_64-linux-gnu/libz.so.1 +COPY --from=build /lib/x86_64-linux-gnu/libunistring.so.2 /lib/x86_64-linux-gnu/libunistring.so.2 +COPY --from=build /lib/x86_64-linux-gnu/libgnutls.so.30 /lib/x86_64-linux-gnu/libgnutls.so.30 +COPY --from=build /lib/x86_64-linux-gnu/libhogweed.so.6 /lib/x86_64-linux-gnu/libhogweed.so.6 +COPY --from=build /lib/x86_64-linux-gnu/libnettle.so.8 /lib/x86_64-linux-gnu/libnettle.so.8 +COPY --from=build /lib/x86_64-linux-gnu/libgmp.so.10 /lib/x86_64-linux-gnu/libgmp.so.10 +COPY --from=build /lib/x86_64-linux-gnu/libkrb5.so.3 /lib/x86_64-linux-gnu/libkrb5.so.3 +COPY --from=build /lib/x86_64-linux-gnu/libk5crypto.so.3 /lib/x86_64-linux-gnu/libk5crypto.so.3 +COPY --from=build /lib/x86_64-linux-gnu/libcom_err.so.2 /lib/x86_64-linux-gnu/libcom_err.so.2 +COPY --from=build /lib/x86_64-linux-gnu/libkrb5support.so.0 /lib/x86_64-linux-gnu/libkrb5support.so.0 +COPY --from=build /lib/x86_64-linux-gnu/libsasl2.so.2 /lib/x86_64-linux-gnu/libsasl2.so.2 +COPY --from=build /lib/x86_64-linux-gnu/libbrotlicommon.so.1 /lib/x86_64-linux-gnu/libbrotlicommon.so.1 +COPY --from=build /lib/x86_64-linux-gnu/libp11-kit.so.0 /lib/x86_64-linux-gnu/libp11-kit.so.0 +COPY --from=build /lib/x86_64-linux-gnu/libtasn1.so.6 /lib/x86_64-linux-gnu/libtasn1.so.6 +COPY --from=build /lib/x86_64-linux-gnu/libkeyutils.so.1 /lib/x86_64-linux-gnu/libkeyutils.so.1 +COPY --from=build /lib/x86_64-linux-gnu/libffi.so.8 /lib/x86_64-linux-gnu/libffi.so.8 + +# Configuration files +COPY --from=build /etc/lsb-release /etc/lsb-release +COPY --from=build /etc/localtime /etc/localtime + +# Required files +COPY --from=build /blank /tmp + +# Data files +COPY --from=build /data/db /data/db diff --git a/nginx-flask-mongo-distroless/mongo/Kraftfile b/nginx-flask-mongo-distroless/mongo/Kraftfile new file mode 100644 index 00000000..1f54f126 --- /dev/null +++ b/nginx-flask-mongo-distroless/mongo/Kraftfile @@ -0,0 +1,12 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: ["/usr/bin/mongod", "--bind_ip_all", "--nounixsocket"] diff --git a/nginx-flask-mongo-distroless/nginx/Dockerfile b/nginx-flask-mongo-distroless/nginx/Dockerfile new file mode 100644 index 00000000..6ca623fd --- /dev/null +++ b/nginx-flask-mongo-distroless/nginx/Dockerfile @@ -0,0 +1,16 @@ +FROM nginx:1.25 AS build + +RUN apt-get update && apt-get install -y --no-install-recommends busybox-static gettext-base + + +FROM distrolessdevops/nginx-distroless:1.25.3 + +# Shell for entrypoint.sh +COPY --from=build /bin/busybox /bin/sh + +# envsubst - present in entrypoint.sh +COPY --from=build /usr/bin/envsubst /usr/bin/envsubst +COPY --from=build /usr/bin/env /usr/bin/env + +COPY ./nginx.conf /etc/nginx/nginx.conf.template +COPY ./entrypoint.sh /entrypoint.sh diff --git a/nginx-flask-mongo-distroless/nginx/Kraftfile b/nginx-flask-mongo-distroless/nginx/Kraftfile new file mode 100644 index 00000000..6d046c9b --- /dev/null +++ b/nginx-flask-mongo-distroless/nginx/Kraftfile @@ -0,0 +1,17 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +labels: + cloud.unikraft.v1.instances/scale_to_zero.policy: "on" + cloud.unikraft.v1.instances/scale_to_zero.stateful: "false" + cloud.unikraft.v1.instances/scale_to_zero.cooldown_time_ms: 1000 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: [ "/entrypoint.sh" ] diff --git a/nginx-flask-mongo-distroless/nginx/entrypoint.sh b/nginx-flask-mongo-distroless/nginx/entrypoint.sh new file mode 100755 index 00000000..62df4c64 --- /dev/null +++ b/nginx-flask-mongo-distroless/nginx/entrypoint.sh @@ -0,0 +1,11 @@ +#!/usr/bin/env sh +set -e + +# BACKEND_HOST defaults to backend.internal if not set. +BACKEND_HOST="${BACKEND_HOST:-backend.internal}" +export BACKEND_HOST + +# Substitute environment variables into the NGINX template. +envsubst '${BACKEND_HOST}' < /etc/nginx/nginx.conf.template > /etc/nginx/nginx.conf + +exec /usr/sbin/nginx diff --git a/nginx-flask-mongo-distroless/nginx/nginx.conf b/nginx-flask-mongo-distroless/nginx/nginx.conf new file mode 100644 index 00000000..2da296ee --- /dev/null +++ b/nginx-flask-mongo-distroless/nginx/nginx.conf @@ -0,0 +1,33 @@ +worker_processes 1; +daemon off; +master_process off; +user root root; + +events { + worker_connections 64; +} + +http { + default_type application/octet-stream; + + open_file_cache max=10000 inactive=30s; + open_file_cache_min_uses 2; + open_file_cache_errors on; + + error_log stderr error; + access_log off; + + keepalive_timeout 10s; + keepalive_requests 10000; + send_timeout 10s; + + server { + listen 80; + + server_name 127.0.0.1; + + location / { + proxy_pass http://${BACKEND_HOST}:9091; + } + } +} diff --git a/nginx-flask-mongo-distroless/test_nginx-flask-mongo-distroless.py b/nginx-flask-mongo-distroless/test_nginx-flask-mongo-distroless.py new file mode 100644 index 00000000..97f5545a --- /dev/null +++ b/nginx-flask-mongo-distroless/test_nginx-flask-mongo-distroless.py @@ -0,0 +1,86 @@ +"""End-to-end test for the ``nginx-flask-mongo-distroless`` example. + +Mirrors the manual steps from ``nginx-flask-mongo-distroless/README.md``: + +1. Create a volume for MongoDB data persistence. +2. Build and deploy MongoDB (internal, on ``mongo-{test_run_id}.internal``). +3. Build and deploy Flask backend (internal, on ``backend-{test_run_id}.internal``). +4. Build and deploy Nginx reverse proxy (public, on port 443:80/tls+http). +5. ``curl https://`` and assert "Hello from the MongoDB client!". +""" + +from __future__ import annotations + +import logging + +from _testlib.unikraft import extract_instance_name, extract_instance_url + +log = logging.getLogger(__name__) + + +def test_nginx_flask_mongo(build_image, run_instance, http, unikraft, request, test_run_id, wait_instance): + volume_name = f"data-{test_run_id}" + + def _cleanup_volume(): + try: + unikraft.run(["volume", "delete", volume_name], check=False) + log.info("deleted volume %s", volume_name) + except Exception: + log.exception("error deleting volume %s", volume_name) + + request.addfinalizer(_cleanup_volume) + + # 1. Create the MongoDB volume. + unikraft.run([ + "volume", "create", + "--metro", unikraft.metro, + f"--name={volume_name}", + "--size=1G", + ]) + + mongo_domain = f"{test_run_id}-mongo.internal" + + # 2. Build and deploy MongoDB. + mongo_image = build_image("nginx-flask-mongo/mongo", "nfm-mongo") + + run_instance( + mongo_image, + memory="1024M", + domain=mongo_domain, + scale_to_zero={"policy": "idle", "cooldown-time": "1000", "stateful": "true"}, + volume=f"{volume_name}:/data/db", + name=f"mongo-distroless-{test_run_id}", + ) + + # 3. Build and deploy Flask backend. + backend_domain = f"backend-{test_run_id}.internal" + flask_image = build_image("nginx-flask-mongo/flask", "nfm-flask") + + run_instance( + flask_image, + memory="1024M", + domain=backend_domain, + env={"FLASK_SERVER_PORT": "9091", "MONGO_SERVER_URL": f"{mongo_domain}:27017"}, + name=f"flask-distroless-{test_run_id}", + ) + + # 4. Build and deploy Nginx reverse proxy. + nginx_image = build_image("nginx-flask-mongo/nginx", "nfm-nginx") + + nginx_instance = run_instance( + nginx_image, + publish=["443:80/tls+http"], + memory="512M", + name=f"nginx-distroless-{test_run_id}", + env={"BACKEND_HOST": backend_domain}, + ) + + url = extract_instance_url(nginx_instance) + assert url, f"could not determine instance URL from: {nginx_instance!r}" + + wait_instance(extract_instance_name(nginx_instance), "running") + + # 5. Verify the response from the full stack. + resp = http(url) + assert resp.status_code == 200 + assert "Hello from the MongoDB client!" in resp.text diff --git a/node-code-execution-distroless/Dockerfile b/node-code-execution-distroless/Dockerfile new file mode 100644 index 00000000..8bc915e4 --- /dev/null +++ b/node-code-execution-distroless/Dockerfile @@ -0,0 +1,14 @@ +FROM node:25-slim AS build + +WORKDIR /src + +COPY package*.json ./ +RUN npm install --omit=dev + + +FROM nvitaterna/nodejs-distroless:25 + +WORKDIR /home/node/app + +COPY --from=build /src /home/node/app +COPY ./server.ts ./ diff --git a/node-code-execution-distroless/Kraftfile b/node-code-execution-distroless/Kraftfile new file mode 100644 index 00000000..dd838a4f --- /dev/null +++ b/node-code-execution-distroless/Kraftfile @@ -0,0 +1,12 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: [ "/usr/bin/node", "--experimental-strip-types", "/home/node/app/server.ts" ] diff --git a/node-code-execution-distroless/README.md b/node-code-execution-distroless/README.md new file mode 100644 index 00000000..97ae15e3 --- /dev/null +++ b/node-code-execution-distroless/README.md @@ -0,0 +1,342 @@ +# Distroless Node.js Code Execution with ROMs + +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](https://unikraft.com/docs/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-distroless` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/node-code-execution-distroless/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```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): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build . --output /node-code-exec-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft pkg \ + --name index.unikraft.io//node-code-exec-distroless: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 https://unikraft.com/docs/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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft run --metro fra \ + --name node-exec \ + -m 512M \ + --image /node-code-exec-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance create \ + --start \ + --name node-exec \ + -M 512Mi \ + /node-code-exec-distroless:latest +``` + +The output shows the instance details: + +**Using the unikraft CLI (Recommended)** +```ansi title="unikraft" +metro: fra +name: node-exec +uuid: 96608ed2-45e0-4c8f-8269-5d8cd3e4b41a +state: starting +image: /node-code-exec-distroless +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 +``` + +or + +**Using the legacy kraft CLI** +```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-distroless@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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances templates list +``` + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED +fra node-exec template /node-code-exec-distroless 512MiB 1 5 seconds ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance template list +``` + +**Using the legacy kraft CLI** +```bash title="kraft" +NAME IMAGE ARGS +CREATED AT +node-exec oci://unikraft.io//node-code-exec-distroless@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`): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build rom1/ --output /node-rom1:latest +unikraft build rom2/ --output /node-rom2:latest +``` + +or + +**Using the legacy kraft CLI** +```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: + +**Using the unikraft CLI (Recommended)** +```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 +``` + +or + +**Using the legacy kraft CLI** +```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: + +**Using the unikraft CLI (Recommended)** +```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 +``` + +or + +**Using the legacy kraft CLI** +```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: + +**Using the unikraft CLI (Recommended)** +```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-distroless 512MiB 1 nameless-wood-gw7pbnls.fra.unikraft.app 2 minutes ago +fra node-exec-rom1 standby /node-code-exec-distroless 512MiB 1 sparkling-dawn-syowlbtj.fra.unikraft.app 3 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```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-distroless@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-distroless@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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). diff --git a/node-code-execution-distroless/package.json b/node-code-execution-distroless/package.json new file mode 100644 index 00000000..8ba7ce6c --- /dev/null +++ b/node-code-execution-distroless/package.json @@ -0,0 +1,22 @@ +{ + "name": "typescript-rom-server", + "version": "1.0.0", + "description": "Unikraft Cloud TypeScript ROM example - HTTP server with dynamically loaded JS functions from ROM", + "type": "module", + "main": "server.ts", + "scripts": { + "start": "node --experimental-strip-types server.ts", + "typecheck": "tsc --noEmit" + }, + "keywords": [ + "unikraft", + "rom", + "typescript" + ], + "author": "", + "license": "ISC", + "devDependencies": { + "typescript": "^5.8.0", + "@types/node": "^22.0.0" + } +} diff --git a/node-code-execution-distroless/rom1/Kraftfile b/node-code-execution-distroless/rom1/Kraftfile new file mode 100644 index 00000000..aff6a308 --- /dev/null +++ b/node-code-execution-distroless/rom1/Kraftfile @@ -0,0 +1,4 @@ +spec: v0.7 + +roms: +- ./fs diff --git a/node-code-execution-distroless/rom1/fs/rom.js b/node-code-execution-distroless/rom1/fs/rom.js new file mode 100644 index 00000000..4ae7adc3 --- /dev/null +++ b/node-code-execution-distroless/rom1/fs/rom.js @@ -0,0 +1,3 @@ +exports.handler = function () { + return "Bye, World!\n"; +}; diff --git a/node-code-execution-distroless/rom2/Kraftfile b/node-code-execution-distroless/rom2/Kraftfile new file mode 100644 index 00000000..aff6a308 --- /dev/null +++ b/node-code-execution-distroless/rom2/Kraftfile @@ -0,0 +1,4 @@ +spec: v0.7 + +roms: +- ./fs diff --git a/node-code-execution-distroless/rom2/fs/rom.ts b/node-code-execution-distroless/rom2/fs/rom.ts new file mode 100644 index 00000000..fd18ffcb --- /dev/null +++ b/node-code-execution-distroless/rom2/fs/rom.ts @@ -0,0 +1,3 @@ +export function handler(): string { + return "Auf Wiedersehen!\n"; +} diff --git a/node-code-execution-distroless/server.ts b/node-code-execution-distroless/server.ts new file mode 100644 index 00000000..7d55a9fe --- /dev/null +++ b/node-code-execution-distroless/server.ts @@ -0,0 +1,97 @@ +import http from "node:http"; +import fs from "node:fs"; +import { stripTypeScriptTypes } from "node:module"; +import { pathToFileURL } from "node:url"; + +const templatePath: string = "/uk/libukp/template_instance"; +const compiledRomPath: string = "/run/rom.compiled.mjs"; + +let romHandler: () => string; + +function resolveRomSource(): { path: string; isTypeScript: boolean } { + const tsPath = "/rom/rom.ts"; + if (fs.existsSync(tsPath)) { + return { path: tsPath, isTypeScript: true }; + } + + const jsPath = "/rom/rom.js"; + if (fs.existsSync(jsPath)) { + return { path: jsPath, isTypeScript: false }; + } + + throw new Error("No ROM module found at /rom/rom.ts or /rom/rom.js"); +} + +function compileTypeScript(sourcePath: string): string { + const tsSource: string = fs.readFileSync(sourcePath, "utf-8"); + const jsSource: string = stripTypeScriptTypes(tsSource, { + mode: "strip", + sourceUrl: sourcePath, + }); + fs.writeFileSync(compiledRomPath, jsSource, "utf-8"); + console.log(`compiled ${sourcePath} -> ${compiledRomPath}`); + return compiledRomPath; +} + +async function loadRomModule(): Promise { + const { path: romPath, isTypeScript } = resolveRomSource(); + + const modulePath: string = isTypeScript + ? compileTypeScript(romPath) + : romPath; + + const romModule = await import(pathToFileURL(modulePath).href); + romHandler = romModule.handler; + console.log(`loaded ROM module from ${romPath}`); +} + +function writeTemplateFlag(): void { + fs.writeFileSync(templatePath, "1", { encoding: "utf-8" }); +} + +function parseArgs(): { host: string; port: number } { + const args: string[] = process.argv.slice(2); + let host: string = "0.0.0.0"; + let port: number = 8080; + + for (let i = 0; i < args.length; i++) { + if (args[i] === "--host" && i + 1 < args.length) { + host = args[++i]; + } else if (args[i] === "--port" && i + 1 < args.length) { + port = parseInt(args[++i], 10); + } + } + + return { host, port }; +} + +async function main(): Promise { + const { host, port } = parseArgs(); + + const server = http.createServer((req, res) => { + if (req.method === "GET") { + res.writeHead(200, { "Content-Type": "text/plain" }); + const msg: string = romHandler(); + res.end(msg); + } else { + res.writeHead(405, { "Content-Type": "text/plain" }); + res.end("Method Not Allowed\n"); + } + }); + + // Initiate template creation right before loading the ROM module + console.log("writing template flag"); + writeTemplateFlag(); + + await loadRomModule(); + + server.listen(port, host, () => { + console.log(`starting server at ${host}:${port}`); + }); +} + +main().catch((error: unknown) => { + const message = error instanceof Error ? error.message : String(error); + console.error(`failed to start server: ${message}`); + process.exit(1); +}); diff --git a/node-code-execution-distroless/test_node-code-execution-distroless.py b/node-code-execution-distroless/test_node-code-execution-distroless.py new file mode 100644 index 00000000..223e4a73 --- /dev/null +++ b/node-code-execution-distroless/test_node-code-execution-distroless.py @@ -0,0 +1,140 @@ +"""End-to-end test for the ``node-code-execution-distroless`` example. + +Mirrors the manual steps from ``node-code-execution-distroless/README.md``: + +1. ``unikraft build . --output /node-code-exec-distroless:`` +2. ``unikraft run --metro --name -m 512M --image ...`` + (the instance auto-converts into a template) +3. ``unikraft build rom1/ --output /node-rom1:`` + ``unikraft build rom2/ --output /node-rom2:`` +4. ``unikraft run --metro -p 443:8080/tls+http + --rom image=/node-rom1:,at=/rom --template `` +5. ``curl https://`` β†’ "Bye, World!" +6. Repeat with rom2 β†’ "Auf Wiedersehen!" +""" + +from __future__ import annotations + +import time +import uuid + +import pytest + +from _testlib.unikraft import extract_instance_url + + +@pytest.fixture(scope="module") +def _template_and_roms(request, unikraft, repo_root, ukc_image_prefix, test_run_id): + """Build the base image, wait for template creation, and build ROMs. + + Module-scoped so the expensive template setup is shared across both ROM + tests in this file. + + Returns a tuple of (template_name, rom1_tag, rom2_tag). + """ + context = repo_root / "node-code-execution-distroless" + base_tag = f"{ukc_image_prefix}/node-code-exec-distroless:{test_run_id}" + template_name = f"{test_run_id}-tmpl" + rom1_tag = f"{ukc_image_prefix}/node-rom1:{test_run_id}" + rom2_tag = f"{ukc_image_prefix}/node-rom2:{test_run_id}" + + # Register cleanup in reverse order (LIFO). + def _cleanup_rom2(): + unikraft.delete_image(rom2_tag) + + def _cleanup_rom1(): + unikraft.delete_image(rom1_tag) + + def _cleanup_template(): + unikraft.run( + ["instance", "template", "delete", template_name], + check=False, + ) + + def _cleanup_base(): + unikraft.delete_image(base_tag) + + request.addfinalizer(_cleanup_rom2) + request.addfinalizer(_cleanup_rom1) + request.addfinalizer(_cleanup_template) + request.addfinalizer(_cleanup_base) + + # 1. Build the base image. + unikraft.build(context, base_tag) + + # 2. Run the base image; it auto-converts into a template. + unikraft.run_instance( + base_tag, + memory="512M", + name=template_name, + ) + + # 3. Wait for the template to be ready. + for _ in range(60): + time.sleep(5) + proc = unikraft.run( + ["instance", "template", "ls"], + check=False, + ) + if proc.returncode == 0 and template_name in proc.stdout: + break + else: + pytest.fail(f"template {template_name!r} did not become ready within timeout") + + # 4. Build ROM images. + unikraft.build(context / "rom1", rom1_tag) + unikraft.build(context / "rom2", rom2_tag) + + return template_name, rom1_tag, rom2_tag + + +def test_node_code_execution_rom1( + _template_and_roms, request, unikraft, test_run_id, http, wait_instance +): + """Run an instance from the template with ROM1 and verify response.""" + template_name, rom1_tag, _ = _template_and_roms + + instance_name = f"node-rom1-{test_run_id}" + request.addfinalizer(lambda: unikraft.delete_instance(instance_name)) + + instance = unikraft.run_instance( + publish=["443:8080/tls+http"], + name=instance_name, + template=template_name, + rom={"image": rom1_tag, "at": "/rom"}, + scale_to_zero={"policy": "on", "cooldown-time": "1000", "stateful": "true"}, + ) + + url = extract_instance_url(instance) + assert url, f"could not determine instance URL from: {instance!r}" + + wait_instance(instance_name, "standby") + resp = http(url) + assert resp.status_code == 200 + assert "Bye, World!" in resp.text + + +def test_node_code_execution_rom2( + _template_and_roms, request, unikraft, test_run_id, http, wait_instance +): + """Run an instance from the template with ROM2 and verify response.""" + template_name, _, rom2_tag = _template_and_roms + + instance_name = f"node-rom2-{test_run_id}" + request.addfinalizer(lambda: unikraft.delete_instance(instance_name)) + + instance = unikraft.run_instance( + publish=["443:8080/tls+http"], + name=instance_name, + template=template_name, + rom={"image": rom2_tag, "at": "/rom"}, + scale_to_zero={"policy": "on", "cooldown-time": "1000", "stateful": "true"}, + ) + + url = extract_instance_url(instance) + assert url, f"could not determine instance URL from: {instance!r}" + + wait_instance(instance_name, "standby") + resp = http(url) + assert resp.status_code == 200 + assert "Auf Wiedersehen!" in resp.text diff --git a/node-code-execution-distroless/tsconfig.json b/node-code-execution-distroless/tsconfig.json new file mode 100644 index 00000000..29354026 --- /dev/null +++ b/node-code-execution-distroless/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "nodenext", + "moduleResolution": "nodenext", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "noEmit": true + }, + "include": ["server.ts"], + "exclude": ["node_modules"] +} diff --git a/node-playwright-chromium-distroless/.dockerignore b/node-playwright-chromium-distroless/.dockerignore new file mode 100644 index 00000000..6690cae7 --- /dev/null +++ b/node-playwright-chromium-distroless/.dockerignore @@ -0,0 +1,3 @@ +/node_modules/ +/.unikraft/ +/*.png diff --git a/node-playwright-chromium-distroless/.gitignore b/node-playwright-chromium-distroless/.gitignore new file mode 100644 index 00000000..6690cae7 --- /dev/null +++ b/node-playwright-chromium-distroless/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/.unikraft/ +/*.png diff --git a/node-playwright-chromium-distroless/Dockerfile b/node-playwright-chromium-distroless/Dockerfile new file mode 100644 index 00000000..ea69cf09 --- /dev/null +++ b/node-playwright-chromium-distroless/Dockerfile @@ -0,0 +1,198 @@ +FROM debian:bookworm AS build + +ARG NODE_VERSION=22.8.0 + +RUN set -xe; \ + apt-get -yqq update; \ + apt-get -yqq install \ + libcups2 \ + libnss3 \ + libatk1.0-0 \ + libnspr4 \ + libpango1.0-0 \ + libasound2 \ + libatspi2.0-0 \ + libxdamage1 \ + libatk-bridge2.0-0 \ + libxkbcommon0 \ + libdrm2 \ + libxcomposite1 \ + libxfixes3 \ + libxrandr2 \ + libgbm1; \ + apt-get -yqq install \ + ca-certificates \ + curl \ + build-essential \ + libssl-dev \ + git \ + ; + +RUN set -xe; \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash; \ + . ~/.bashrc; \ + nvm install ${NODE_VERSION} \ + ; + +WORKDIR /app +COPY package* . + +RUN set -xe; \ + . ~/.bashrc; \ + npm install \ + ; + +RUN mkdir /home/tmp + + +FROM gcr.io/distroless/nodejs22-debian12 + +ARG NODE_VERSION=22.8.0 + +# Create required directories. +COPY --from=build /home/tmp /tmp + +# Chrome binary +COPY --from=build /root/.cache/ms-playwright /root/.cache/ms-playwright + +# Chrome libraries +COPY --from=build /lib/x86_64-linux-gnu/libdl.so.2 \ + /lib/x86_64-linux-gnu/libpthread.so.0 \ + /lib/x86_64-linux-gnu/libgobject-2.0.so.0 \ + /lib/x86_64-linux-gnu/libglib-2.0.so.0 \ + /lib/x86_64-linux-gnu/libnss3.so \ + /lib/x86_64-linux-gnu/libnssutil3.so \ + /lib/x86_64-linux-gnu/libsmime3.so \ + /lib/x86_64-linux-gnu/libnspr4.so \ + /lib/x86_64-linux-gnu/libatk-1.0.so.0 \ + /lib/x86_64-linux-gnu/libatk-bridge-2.0.so.0 \ + /lib/x86_64-linux-gnu/libcups.so.2 \ + /lib/x86_64-linux-gnu/libgio-2.0.so.0 \ + /lib/x86_64-linux-gnu/libdrm.so.2 \ + /lib/x86_64-linux-gnu/libdbus-1.so.3 \ + /lib/x86_64-linux-gnu/libexpat.so.1 \ + /lib/x86_64-linux-gnu/libxcb.so.1 \ + /lib/x86_64-linux-gnu/libxkbcommon.so.0 \ + /lib/x86_64-linux-gnu/libatspi.so.0 \ + /lib/x86_64-linux-gnu/libm.so.6 \ + /lib/x86_64-linux-gnu/libX11.so.6 \ + /lib/x86_64-linux-gnu/libXcomposite.so.1 \ + /lib/x86_64-linux-gnu/libXdamage.so.1 \ + /lib/x86_64-linux-gnu/libXext.so.6 \ + /lib/x86_64-linux-gnu/libXfixes.so.3 \ + /lib/x86_64-linux-gnu/libXrandr.so.2 \ + /lib/x86_64-linux-gnu/libgbm.so.1 \ + /lib/x86_64-linux-gnu/libpango-1.0.so.0 \ + /lib/x86_64-linux-gnu/libcairo.so.2 \ + /lib/x86_64-linux-gnu/libasound.so.2 \ + /lib/x86_64-linux-gnu/libgcc_s.so.1 \ + /lib/x86_64-linux-gnu/libc.so.6 \ + /lib/x86_64-linux-gnu/libffi.so.8 \ + /lib/x86_64-linux-gnu/libpcre2-8.so.0 \ + /lib/x86_64-linux-gnu/libplc4.so \ + /lib/x86_64-linux-gnu/libplds4.so \ + /lib/x86_64-linux-gnu/libgssapi_krb5.so.2 \ + /lib/x86_64-linux-gnu/libavahi-common.so.3 \ + /lib/x86_64-linux-gnu/libavahi-client.so.3 \ + /lib/x86_64-linux-gnu/libgnutls.so.30 \ + /lib/x86_64-linux-gnu/libz.so.1 \ + /lib/x86_64-linux-gnu/libgmodule-2.0.so.0 \ + /lib/x86_64-linux-gnu/libmount.so.1 \ + /lib/x86_64-linux-gnu/libselinux.so.1 \ + /lib/x86_64-linux-gnu/libsystemd.so.0 \ + /lib/x86_64-linux-gnu/libXau.so.6 \ + /lib/x86_64-linux-gnu/libXdmcp.so.6 \ + /lib/x86_64-linux-gnu/libXi.so.6 \ + /lib/x86_64-linux-gnu/libXrender.so.1 \ + /lib/x86_64-linux-gnu/libwayland-server.so.0 \ + /lib/x86_64-linux-gnu/libfribidi.so.0 \ + /lib/x86_64-linux-gnu/libthai.so.0 \ + /lib/x86_64-linux-gnu/libharfbuzz.so.0 \ + /lib/x86_64-linux-gnu/libpixman-1.so.0 \ + /lib/x86_64-linux-gnu/libfontconfig.so.1 \ + /lib/x86_64-linux-gnu/libfreetype.so.6 \ + /lib/x86_64-linux-gnu/libpng16.so.16 \ + /lib/x86_64-linux-gnu/libxcb-shm.so.0 \ + /lib/x86_64-linux-gnu/libxcb-render.so.0 \ + /lib/x86_64-linux-gnu/libkrb5.so.3 \ + /lib/x86_64-linux-gnu/libk5crypto.so.3 \ + /lib/x86_64-linux-gnu/libcom_err.so.2 \ + /lib/x86_64-linux-gnu/libkrb5support.so.0 \ + /lib/x86_64-linux-gnu/libp11-kit.so.0 \ + /lib/x86_64-linux-gnu/libidn2.so.0 \ + /lib/x86_64-linux-gnu/libunistring.so.2 \ + /lib/x86_64-linux-gnu/libtasn1.so.6 \ + /lib/x86_64-linux-gnu/libnettle.so.8 \ + /lib/x86_64-linux-gnu/libhogweed.so.6 \ + /lib/x86_64-linux-gnu/libgmp.so.10 \ + /lib/x86_64-linux-gnu/libblkid.so.1 \ + /lib/x86_64-linux-gnu/libcap.so.2 \ + /lib/x86_64-linux-gnu/libgcrypt.so.20 \ + /lib/x86_64-linux-gnu/liblzma.so.5 \ + /lib/x86_64-linux-gnu/libzstd.so.1 \ + /lib/x86_64-linux-gnu/liblz4.so.1 \ + /lib/x86_64-linux-gnu/libbsd.so.0 \ + /lib/x86_64-linux-gnu/libdatrie.so.1 \ + /lib/x86_64-linux-gnu/libgraphite2.so.3 \ + /lib/x86_64-linux-gnu/libbrotlidec.so.1 \ + /lib/x86_64-linux-gnu/libkeyutils.so.1 \ + /lib/x86_64-linux-gnu/libresolv.so.2 \ + /lib/x86_64-linux-gnu/libgpg-error.so.0 \ + /lib/x86_64-linux-gnu/libmd.so.0 \ + /lib/x86_64-linux-gnu/libbrotlicommon.so.1 \ + /lib/x86_64-linux-gnu/ + +# Other Chrome-related libraries +COPY --from=build /usr/lib/x86_64-linux-gnu/libsoftokn3.so \ + /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 \ + /usr/lib/x86_64-linux-gnu/libudev.so.1 \ + /usr/lib/x86_64-linux-gnu/libfreebl3.so \ + /usr/lib/x86_64-linux-gnu/libfreeblpriv3.so \ + /usr/lib/x86_64-linux-gnu/libudev.so.1.7.5 \ + /usr/lib/x86_64-linux-gnu/ + +# System libraries +COPY --from=build /lib/x86_64-linux-gnu/libbrotlienc.so.1 \ + /lib/x86_64-linux-gnu/libnghttp2.so.14 \ + /lib/x86_64-linux-gnu/libicui18n.so.72 \ + /lib/x86_64-linux-gnu/libicuuc.so.72 \ + /lib/x86_64-linux-gnu/libstdc++.so.6 \ + /lib/x86_64-linux-gnu/librt.so.1 \ + /lib/x86_64-linux-gnu/libtinfo.so.6 \ + /lib/x86_64-linux-gnu/libproc2.so.0 \ + /lib/x86_64-linux-gnu/libicudata.so.72 \ + /lib/x86_64-linux-gnu/ + +# Dbus and system files +COPY --from=build /usr/lib/dbus-1.0 /usr/lib/dbus-1.0 +COPY --from=build /usr/lib/systemd /usr/lib/systemd +COPY --from=build /usr/lib/tmpfiles.d /usr/lib/tmpfiles.d +COPY --from=build /usr/lib/sysusers.d /usr/lib/sysusers.d +COPY --from=build /usr/lib/sysctl.d /usr/lib/sysctl.d + +# Data files +COPY --from=build /usr/share/fonts /usr/share/fonts + +COPY --from=build /run /run + +# Distro definition +COPY --from=build /etc/os-release /etc/os-release +COPY --from=build /usr/lib/os-release /usr/lib/os-release + +# Configuration files +COPY --from=build /etc /etc + +# Node modules, including Puppeteer and application +COPY --from=build /app /app + +# Required by wrapper script +COPY --from=build /bin/sh /bin/sh + +# Required by Playwright / Chrome +COPY --from=build /usr/bin/ps /usr/bin/ps + +# Actual server implementation +COPY ./server.js /app/server.js + +# Wrapper script set environment +COPY ./wrapper.sh /usr/bin/wrapper.sh diff --git a/node-playwright-chromium-distroless/Kraftfile b/node-playwright-chromium-distroless/Kraftfile new file mode 100644 index 00000000..c6192e59 --- /dev/null +++ b/node-playwright-chromium-distroless/Kraftfile @@ -0,0 +1,12 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: ["/usr/bin/wrapper.sh", "/nodejs/bin/node", "/app/server.js"] diff --git a/node-playwright-chromium-distroless/README.md b/node-playwright-chromium-distroless/README.md new file mode 100644 index 00000000..5a4994bd --- /dev/null +++ b/node-playwright-chromium-distroless/README.md @@ -0,0 +1,184 @@ +# Distroless Playwright (Chromium) with Node.js + +[Playwright](https://playwright.dev/) is a framework for web testing and Automation. + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](https://unikraft.com/docs/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/node-playwright-chromium-distroless/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/node-playwright-chromium-distroless/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +When done, invoke the following command to deploy this app on Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build . --output /node-playwright-chromium-distroless:latest +unikraft run --metro fra \ + -m 4G \ + -p 443:8080/tls+http \ + --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ + --image /node-playwright-chromium-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```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 \ + . +``` + +The output shows the instance address and other details: + +**Using the unikraft CLI (Recommended)** +```ansi title="unikraft" +metro: fra +name: node-playwright-chromium-distroless-v5f8p +uuid: a1b2c3d4-e5f6-7a8b-9c0d-a1b2c3d4e5f6 +state: starting +image: /node-playwright-chromium-distroless +resources: + memory: 4096MiB + vcpus: 1 +service: + uuid: b2c3d4e5-f6a7-8b9c-0d1e-b2c3d4e5f6a7 + name: gentle-moon-cx2jh5wd + domains: + - fqdn: gentle-moon-cx2jh5wd.fra.unikraft.app +networks: +- uuid: c3d4e5f6-a7b8-9c0d-1e2f-c3d4e5f6a7b8 + private-ip: 10.0.4.3 + mac: 12:b0:8e:5a:cd:b7 +timestamps: + created: just now +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: node-playwright-chromium-distroless-v5f8p + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: a1b2c3d4-e5f6-7a8b-9c0d-a1b2c3d4e5f6 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: https://gentle-moon-cx2jh5wd.fra.unikraft.app + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//node-playwright-chromium-distroless@sha256:7c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 4096 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: gentle-moon-cx2jh5wd + β”œβ”€ private fqdn: node-playwright-chromium-distroless-v5f8p.internal + └─── private ip: 10.0.4.3 +``` + +In this case, the instance name is `node-playwright-chromium-distroless-v5f8p` and the address is `https://gentle-moon-cx2jh5wd.fra.unikraft.app`. +They're different for each run. + +The command will deploy the files in the current directory. +It results in the creation of a remote web-based service for creating PNG screenshots of remote pages. + +Use the `?page=` to point the service to the remote page to screenshot. +Query the service using commands such as: + +```console +curl "https://..unikraft.app/?page=https://google.com" -o ss-google.png +curl "https://..unikraft.app/?page=https://bing.com" -o ss-bing.png +``` + +You can list information about the instance by running: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances list +``` + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node-playwright-chromium-distroless-v5f8p running /node-playwright-chromium-distroless 4096MiB 1 gentle-moon-cx2jh5wd.fra.unikraft.app 2 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance list +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE + MEMORY VCPUS ARGS BOOT TIME +node-playwright-chromium-distroless-v5f8p gentle-moon-cx2jh5wd.fra.unikraft.app running 1 minute ago oci://unikraft.io//node-playwright-chromium-distroless@sha256:... 4 GiB 1 300.21 ms +``` + +When done, you can remove the instance: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances delete +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance remove +``` + + +## Learn more + +- [Playwright's Documentation](https://playwright.dev/docs/intro) +- [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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). diff --git a/node-playwright-chromium-distroless/package-lock.json b/node-playwright-chromium-distroless/package-lock.json new file mode 100644 index 00000000..98fa4a4d --- /dev/null +++ b/node-playwright-chromium-distroless/package-lock.json @@ -0,0 +1,73 @@ +{ + "name": "node-playwright", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "dependencies": { + "playwright": "^1.55.1", + "playwright-chromium": "^1.55.1" + } + }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/playwright": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright/-/playwright-1.59.1.tgz", + "integrity": "sha512-C8oWjPR3F81yljW9o5OxcWzfh6avkVwDD2VYdwIGqTkl+OGFISgypqzfu7dOe4QNLL2aqcWBmI3PMtLIK233lw==", + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "fsevents": "2.3.2" + } + }, + "node_modules/playwright-chromium": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-chromium/-/playwright-chromium-1.59.1.tgz", + "integrity": "sha512-aTsPenkxsr9np4vIHuMEND6comqepVvzbL0MwkozFNliwGZjTqrBUQ7TF6Ay1ZIU/e7rcUpGsCTUG+nqwxG2Xw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "playwright-core": "1.59.1" + }, + "bin": { + "playwright": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + } + } +} diff --git a/node-playwright-chromium-distroless/package.json b/node-playwright-chromium-distroless/package.json new file mode 100644 index 00000000..5ac6232c --- /dev/null +++ b/node-playwright-chromium-distroless/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "playwright": "^1.55.1", + "playwright-chromium": "^1.55.1" + } +} diff --git a/node-playwright-chromium-distroless/server.js b/node-playwright-chromium-distroless/server.js new file mode 100644 index 00000000..28a0637a --- /dev/null +++ b/node-playwright-chromium-distroless/server.js @@ -0,0 +1,64 @@ +const http = require('http'); // Loads the http module +const url = require('url'); // Parse the HTTP URL +const {chromium} = require('playwright'); // Get Chromium headless browser + +const port = 8080; // Use port 8080 by default + +const requestListener = function (request, response) { + + // Get information object of request URL + const parsedURL = url.parse(request.url, true); + + // Check for `page` GET variable + if (parsedURL.pathname === '/' && parsedURL.query.page) { + remote = parsedURL.query.page; + + // Anonymous function to call Playwright + (async () => { + try { + const browser = await chromium.launch({headless: true}); + const page = await browser.newPage(); + await page.goto(remote); + const buffer = await page.screenshot(); + + await browser.close(); + + // Tell the browser everything is OK (Status code 200), and the data is PNG + response.writeHead(200, { + 'Content-Type': 'image/png' + }); + + // Write image buffer to the body of the response + response.write(buffer, 'binary'); + response.end(); + } catch(err) { + // In case of error, send a text message + response.writeHead(200, { + 'Content-Type': 'text/plain' + }); + + // Write the error text to the body of the response + response.write('Error' + err + '.\n'); + response.end(); + } + })() + + return; + } + + // If no HTTP GET variable, tell the browser everything is OK (Status code 200), and the data is in plain text + response.writeHead(200, { + 'Content-Type': 'text/plain' + }); + + // Write a simple text to the body of the page + response.write('Nothing to do.\n'); + response.end(); + +} + +// Fire up server, show banner message and wait for connections +const server = http.createServer(requestListener); +server.listen(port, () => { + console.log('Server is running on port ' + port); +}); diff --git a/node-playwright-chromium-distroless/test_node-playwright-chromium-distroless.py b/node-playwright-chromium-distroless/test_node-playwright-chromium-distroless.py new file mode 100644 index 00000000..73eb69d2 --- /dev/null +++ b/node-playwright-chromium-distroless/test_node-playwright-chromium-distroless.py @@ -0,0 +1,37 @@ +"""End-to-end test for the ``node-playwright-chromium-distroless`` example. + +Mirrors the manual steps from ``node-playwright-chromium-distroless/README.md``: + +1. ``unikraft build . --output /node-playwright-chromium-distroless:`` +2. ``unikraft run --metro -p 443:8080/tls+http -m 4G --image ...`` +3. ``curl https:///?page=https://example.com`` and assert + the response is a valid PNG screenshot. +""" + +from __future__ import annotations + +from _testlib.unikraft import extract_instance_name, extract_instance_url + +_PNG_MAGIC = b"\x89PNG\r\n\x1a\n" + + +def test_node_playwright_chromium_screenshot(build_image, run_instance, http, wait_instance): + image = build_image( + "node-playwright-chromium-distroless", + "node-playwright-chromium-distroless", + ) + + instance = run_instance( + image, + publish=["443:8080/tls+http"], + memory="4G", + ) + + url = extract_instance_url(instance) + assert url, f"could not determine instance URL from: {instance!r}" + + wait_instance(extract_instance_name(instance), "running") + + resp = http(f"{url}/?page=https://example.com") + assert resp.status_code == 200 + assert resp.content[:8] == _PNG_MAGIC, "response is not a valid PNG image" diff --git a/node-playwright-chromium-distroless/wrapper.sh b/node-playwright-chromium-distroless/wrapper.sh new file mode 100755 index 00000000..7e046db5 --- /dev/null +++ b/node-playwright-chromium-distroless/wrapper.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +export HOME=/root +cd /app +exec "$@" diff --git a/node-playwright-firefox-distroless/.dockerignore b/node-playwright-firefox-distroless/.dockerignore new file mode 100644 index 00000000..6690cae7 --- /dev/null +++ b/node-playwright-firefox-distroless/.dockerignore @@ -0,0 +1,3 @@ +/node_modules/ +/.unikraft/ +/*.png diff --git a/node-playwright-firefox-distroless/.gitignore b/node-playwright-firefox-distroless/.gitignore new file mode 100644 index 00000000..6690cae7 --- /dev/null +++ b/node-playwright-firefox-distroless/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/.unikraft/ +/*.png diff --git a/node-playwright-firefox-distroless/Dockerfile b/node-playwright-firefox-distroless/Dockerfile new file mode 100644 index 00000000..cc0a3066 --- /dev/null +++ b/node-playwright-firefox-distroless/Dockerfile @@ -0,0 +1,157 @@ +FROM debian:bookworm AS build + +ARG NODE_VERSION=22.8.0 + +RUN set -xe; \ + apt-get -yqq update; \ + apt-get -yqq install \ + build-essential \ + ca-certificates \ + curl \ + libasound2 \ + libatk1.0-0 \ + libatk-bridge2.0-0 \ + libatspi2.0-0 \ + libgbm1 \ + libgtk-3-0 \ + libnspr4 \ + libnss3 \ + libpango1.0-0 \ + libssl-dev \ + libx11-xcb1 \ + libxcomposite1 \ + libxfixes3 \ + libxkbcommon0 \ + ; + +RUN set -xe; \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash; \ + . ~/.bashrc; \ + nvm install ${NODE_VERSION} \ + ; + +WORKDIR /app +COPY package* . + +RUN set -xe; \ + . ~/.bashrc; \ + npm install \ + ; + +RUN mkdir /home/tmp + + +FROM gcr.io/distroless/nodejs22-debian12 + +ARG NODE_VERSION=22.8.0 + +# Create required directories. +COPY --from=build /home/tmp /tmp + +# Browser binary +COPY --from=build /root/.cache/ms-playwright /root/.cache/ms-playwright + +# Browser libraries +COPY --from=build \ + /lib/x86_64-linux-gnu/libasound.so.2 \ + /lib/x86_64-linux-gnu/libatk-1.0.so.0 \ + /lib/x86_64-linux-gnu/libatk-bridge-2.0.so.0 \ + /lib/x86_64-linux-gnu/libatspi.so.0 \ + /lib/x86_64-linux-gnu/libblkid.so.1 \ + /lib/x86_64-linux-gnu/libbrotlicommon.so.1 \ + /lib/x86_64-linux-gnu/libbrotlidec.so.1 \ + /lib/x86_64-linux-gnu/libbsd.so.0 \ + /lib/x86_64-linux-gnu/libcairo-gobject.so.2 \ + /lib/x86_64-linux-gnu/libcairo.so.2 \ + /lib/x86_64-linux-gnu/libcap.so.2 \ + /lib/x86_64-linux-gnu/libdatrie.so.1 \ + /lib/x86_64-linux-gnu/libdbus-1.so.3 \ + /lib/x86_64-linux-gnu/libepoxy.so.0 \ + /lib/x86_64-linux-gnu/libexpat.so.1 \ + /lib/x86_64-linux-gnu/libffi.so.8 \ + /lib/x86_64-linux-gnu/libfontconfig.so.1 \ + /lib/x86_64-linux-gnu/libfreetype.so.6 \ + /lib/x86_64-linux-gnu/libfribidi.so.0 \ + /lib/x86_64-linux-gnu/libgcrypt.so.20 \ + /lib/x86_64-linux-gnu/libgdk-3.so.0 \ + /lib/x86_64-linux-gnu/libgdk_pixbuf-2.0.so.0 \ + /lib/x86_64-linux-gnu/libgio-2.0.so.0 \ + /lib/x86_64-linux-gnu/libglib-2.0.so.0 \ + /lib/x86_64-linux-gnu/libgmodule-2.0.so.0 \ + /lib/x86_64-linux-gnu/libgobject-2.0.so.0 \ + /lib/x86_64-linux-gnu/libgpg-error.so.0 \ + /lib/x86_64-linux-gnu/libgraphite2.so.3 \ + /lib/x86_64-linux-gnu/libgtk-3.so.0 \ + /lib/x86_64-linux-gnu/libharfbuzz.so.0 \ + /lib/x86_64-linux-gnu/libjpeg.so.62 \ + /lib/x86_64-linux-gnu/liblz4.so.1 \ + /lib/x86_64-linux-gnu/liblzma.so.5 \ + /lib/x86_64-linux-gnu/libmd.so.0 \ + /lib/x86_64-linux-gnu/libmount.so.1 \ + /lib/x86_64-linux-gnu/libnspr4.so \ + /lib/x86_64-linux-gnu/libnss3.so \ + /lib/x86_64-linux-gnu/libnssutil3.so \ + /lib/x86_64-linux-gnu/libpango-1.0.so.0 \ + /lib/x86_64-linux-gnu/libpangocairo-1.0.so.0 \ + /lib/x86_64-linux-gnu/libpangoft2-1.0.so.0 \ + /lib/x86_64-linux-gnu/libpcre2-8.so.0 \ + /lib/x86_64-linux-gnu/libpixman-1.so.0 \ + /lib/x86_64-linux-gnu/libplc4.so \ + /lib/x86_64-linux-gnu/libplds4.so \ + /lib/x86_64-linux-gnu/libpng16.so.16 \ + /lib/x86_64-linux-gnu/libresolv.so.2 \ + /lib/x86_64-linux-gnu/librt.so.1 \ + /lib/x86_64-linux-gnu/libselinux.so.1 \ + /lib/x86_64-linux-gnu/libsmime3.so \ + /lib/x86_64-linux-gnu/libssl3.so \ + /lib/x86_64-linux-gnu/libsystemd.so.0 \ + /lib/x86_64-linux-gnu/libthai.so.0 \ + /lib/x86_64-linux-gnu/libwayland-client.so.0 \ + /lib/x86_64-linux-gnu/libwayland-cursor.so.0 \ + /lib/x86_64-linux-gnu/libwayland-egl.so.1 \ + /lib/x86_64-linux-gnu/libX11.so.6 \ + /lib/x86_64-linux-gnu/libX11-xcb.so.1 \ + /lib/x86_64-linux-gnu/libXau.so.6 \ + /lib/x86_64-linux-gnu/libxcb-render.so.0 \ + /lib/x86_64-linux-gnu/libxcb-shm.so.0 \ + /lib/x86_64-linux-gnu/libxcb.so.1 \ + /lib/x86_64-linux-gnu/libXcomposite.so.1 \ + /lib/x86_64-linux-gnu/libXcursor.so.1 \ + /lib/x86_64-linux-gnu/libXdamage.so.1 \ + /lib/x86_64-linux-gnu/libXdmcp.so.6 \ + /lib/x86_64-linux-gnu/libXext.so.6 \ + /lib/x86_64-linux-gnu/libXfixes.so.3 \ + /lib/x86_64-linux-gnu/libXinerama.so.1 \ + /lib/x86_64-linux-gnu/libXi.so.6 \ + /lib/x86_64-linux-gnu/libxkbcommon.so.0 \ + /lib/x86_64-linux-gnu/libXrandr.so.2 \ + /lib/x86_64-linux-gnu/libXrender.so.1 \ + /lib/x86_64-linux-gnu/libzstd.so.1 \ + /lib/x86_64-linux-gnu/ + +# Dbus and system files +COPY --from=build /usr/lib/dbus-1.0 /usr/lib/dbus-1.0 +COPY --from=build /usr/lib/systemd /usr/lib/systemd +COPY --from=build /usr/lib/tmpfiles.d /usr/lib/tmpfiles.d +COPY --from=build /usr/lib/sysusers.d /usr/lib/sysusers.d +COPY --from=build /usr/lib/sysctl.d /usr/lib/sysctl.d + +# Data files +COPY --from=build /usr/share/fonts /usr/share/fonts + +# Run directory +COPY --from=build /run /run + +# Node modules, including Puppeteer and application +COPY --from=build /app /app + +# Required by wrapper script +COPY --from=build /bin/sh /bin/sh +COPY --from=build /usr/bin/chown /usr/bin/chown +COPY --from=build /usr/bin/ln /usr/bin/ln + +# Actual server implementation +COPY ./server.js /app/server.js + +# Wrapper script +COPY ./wrapper.sh /usr/bin/wrapper.sh diff --git a/node-playwright-firefox-distroless/Kraftfile b/node-playwright-firefox-distroless/Kraftfile new file mode 100644 index 00000000..c6192e59 --- /dev/null +++ b/node-playwright-firefox-distroless/Kraftfile @@ -0,0 +1,12 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: ["/usr/bin/wrapper.sh", "/nodejs/bin/node", "/app/server.js"] diff --git a/node-playwright-firefox-distroless/README.md b/node-playwright-firefox-distroless/README.md new file mode 100644 index 00000000..8e4f329a --- /dev/null +++ b/node-playwright-firefox-distroless/README.md @@ -0,0 +1,184 @@ +# Distroless Playwright (Firefox) with Node.js + +[Playwright](https://playwright.dev/) is a framework for web testing and Automation. + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](https://unikraft.com/docs/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/node-playwright-firefox-distroless/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/node-playwright-firefox-distroless/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +When done, invoke the following command to deploy this app on Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build . --output /node-playwright-firefox-distroless:latest +unikraft run --metro fra \ + -m 4G \ + -p 443:8080/tls+http \ + --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ + --image /node-playwright-firefox-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```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 \ + . +``` + +The output shows the instance address and other details: + +**Using the unikraft CLI (Recommended)** +```ansi title="unikraft" +metro: fra +name: node-playwright-firefox-distroless-q3m9k +uuid: d4e5f6a7-b8c9-0d1e-2f3a-d4e5f6a7b8c9 +state: starting +image: /node-playwright-firefox-distroless +resources: + memory: 4096MiB + vcpus: 1 +service: + uuid: e5f6a7b8-c9d0-1e2f-3a4b-e5f6a7b8c9d0 + name: bright-lake-dh6xp2sq + domains: + - fqdn: bright-lake-dh6xp2sq.fra.unikraft.app +networks: +- uuid: f6a7b8c9-d0e1-2f3a-4b5c-f6a7b8c9d0e1 + private-ip: 10.0.5.3 + mac: 12:b0:9f:6b:de:c8 +timestamps: + created: just now +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: node-playwright-firefox-distroless-q3m9k + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: d4e5f6a7-b8c9-0d1e-2f3a-d4e5f6a7b8c9 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: https://bright-lake-dh6xp2sq.fra.unikraft.app + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//node-playwright-firefox-distroless@sha256:8d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 4096 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: bright-lake-dh6xp2sq + β”œβ”€ private fqdn: node-playwright-firefox-distroless-q3m9k.internal + └─── private ip: 10.0.5.3 +``` + +In this case, the instance name is `node-playwright-firefox-distroless-q3m9k` and the address is `https://bright-lake-dh6xp2sq.fra.unikraft.app`. +They're different for each run. + +The command will deploy the files in the current directory. +It results in the creation of a remote web-based service for creating PNG screenshots of remote pages. + +Use the `?page=` to point the service to the remote page to screenshot. +Query the service using commands such as: + +```console +curl "https://..unikraft.app/?page=https://google.com" -o ss-google.png +curl "https://..unikraft.app/?page=https://bing.com" -o ss-bing.png +``` + +You can list information about the instance by running: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances list +``` + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS +FQDN CREATED +fra node-playwright-firefox-distroless-q3m9k running /node-playwright-firefox-distroless 4096MiB 1 bright-lake-dh6xp2sq.fra.unikraft.app 2 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance list +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +node-playwright-firefox-distroless-q3m9k bright-lake-dh6xp2sq.fra.unikraft.app running 1 minute ago oci://unikraft.io//node-playwright-firefox-distroless@sha256:... 4 GiB 1 350.87 ms +``` + +When done, you can remove the instance: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances delete +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance remove +``` + + +## Learn more + +- [Playwright's Documentation](https://playwright.dev/docs/intro) +- [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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). diff --git a/node-playwright-firefox-distroless/package.json b/node-playwright-firefox-distroless/package.json new file mode 100644 index 00000000..c570d7c9 --- /dev/null +++ b/node-playwright-firefox-distroless/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "playwright": "^1.55.1", + "playwright-firefox": "^1.55.1" + } +} diff --git a/node-playwright-firefox-distroless/server.js b/node-playwright-firefox-distroless/server.js new file mode 100644 index 00000000..1e0cdb47 --- /dev/null +++ b/node-playwright-firefox-distroless/server.js @@ -0,0 +1,64 @@ +const http = require('http'); // Loads the http module +const url = require('url'); // Parse the HTTP URL +const {firefox} = require('playwright'); // Get Chromium headless browser + +const port = 8080; // Use port 8080 by default + +const requestListener = function (request, response) { + + // Get information object of request URL + const parsedURL = url.parse(request.url, true); + + // Check for `page` GET variable + if (parsedURL.pathname === '/' && parsedURL.query.page) { + remote = parsedURL.query.page; + + // Anonymous function to call Playwright + (async () => { + try { + const browser = await firefox.launch({headless: true}); + const page = await browser.newPage(); + await page.goto(remote); + const buffer = await page.screenshot(); + + await browser.close(); + + // Tell the browser everything is OK (Status code 200), and the data is PNG + response.writeHead(200, { + 'Content-Type': 'image/png' + }); + + // Write image buffer to the body of the response + response.write(buffer, 'binary'); + response.end(); + } catch(err) { + // In case of error, send a text message + response.writeHead(200, { + 'Content-Type': 'text/plain' + }); + + // Write the error text to the body of the response + response.write('Error' + err + '.\n'); + response.end(); + } + })() + + return; + } + + // If no HTTP GET variable, tell the browser everything is OK (Status code 200), and the data is in plain text + response.writeHead(200, { + 'Content-Type': 'text/plain' + }); + + // Write a simple text to the body of the page + response.write('Nothing to do.\n'); + response.end(); + +} + +// Fire up server, show banner message and wait for connections +const server = http.createServer(requestListener); +server.listen(port, () => { + console.log('Server is running on port ' + port); +}); diff --git a/node-playwright-firefox-distroless/test_node-playwright-firefox-distroless.py b/node-playwright-firefox-distroless/test_node-playwright-firefox-distroless.py new file mode 100644 index 00000000..70464960 --- /dev/null +++ b/node-playwright-firefox-distroless/test_node-playwright-firefox-distroless.py @@ -0,0 +1,37 @@ +"""End-to-end test for the ``node-playwright-firefox-distroless`` example. + +Mirrors the manual steps from ``node-playwright-firefox-distroless/README.md``: + +1. ``unikraft build . --output /node-playwright-firefox-distroless:`` +2. ``unikraft run --metro -p 443:8080/tls+http -m 4G --image ...`` +3. ``curl https:///?page=https://example.com`` and assert + the response is a valid PNG screenshot. +""" + +from __future__ import annotations + +from _testlib.unikraft import extract_instance_name, extract_instance_url + +_PNG_MAGIC = b"\x89PNG\r\n\x1a\n" + + +def test_node_playwright_firefox_screenshot(build_image, run_instance, http, wait_instance): + image = build_image( + "node-playwright-firefox-distroless", + "node-playwright-firefox-distroless", + ) + + instance = run_instance( + image, + publish=["443:8080/tls+http"], + memory="4G", + ) + + url = extract_instance_url(instance) + assert url, f"could not determine instance URL from: {instance!r}" + + wait_instance(extract_instance_name(instance), "running") + + resp = http(f"{url}/?page=https://example.com") + assert resp.status_code == 200 + assert resp.content[:8] == _PNG_MAGIC, "response is not a valid PNG image" diff --git a/node-playwright-firefox-distroless/wrapper.sh b/node-playwright-firefox-distroless/wrapper.sh new file mode 100755 index 00000000..2053b6b3 --- /dev/null +++ b/node-playwright-firefox-distroless/wrapper.sh @@ -0,0 +1,10 @@ +#!/bin/sh + +set -e + +chown root:root /root + +export HOME=/root + +cd /app +exec "$@" diff --git a/node-playwright-webkit-distroless/.dockerignore b/node-playwright-webkit-distroless/.dockerignore new file mode 100644 index 00000000..6690cae7 --- /dev/null +++ b/node-playwright-webkit-distroless/.dockerignore @@ -0,0 +1,3 @@ +/node_modules/ +/.unikraft/ +/*.png diff --git a/node-playwright-webkit-distroless/.gitignore b/node-playwright-webkit-distroless/.gitignore new file mode 100644 index 00000000..6690cae7 --- /dev/null +++ b/node-playwright-webkit-distroless/.gitignore @@ -0,0 +1,3 @@ +/node_modules/ +/.unikraft/ +/*.png diff --git a/node-playwright-webkit-distroless/Dockerfile b/node-playwright-webkit-distroless/Dockerfile new file mode 100644 index 00000000..9972319f --- /dev/null +++ b/node-playwright-webkit-distroless/Dockerfile @@ -0,0 +1,325 @@ +FROM debian:bookworm AS build + +ARG NODE_VERSION=22.8.0 + +RUN set -xe; \ + apt-get -yqq update; \ + apt-get -yqq install \ + libatk1.0 \ + libatk-bridge2.0 \ + libatomic1 \ + libcairo-gobject2 \ + libenchant-2.2 \ + libepoxy0 \ + libevent-2.1 \ + libflite1 \ + libgdk-pixbuf-2.0 \ + libgles2 \ + libgstreamer1.0-0 \ + libgstreamer1.0-dev \ + libgstreamer-gl1.0-0 \ + libgstreamer-plugins-bad1.0-0 \ + libgstreamer-plugins-base1.0-0 \ + libgtk-3-0 \ + libharfbuzz-icu0 \ + libhyphen0 \ + liblcms2-2 \ + libmanette-0.2-0 \ + libopus0 \ + libpango-1.0 \ + libpangocairo-1.0 \ + libsecret-1-0 \ + libtinfo6 \ + libvpx7 \ + libwebpdemux2 \ + libwebpmux3 \ + libwoff1 \ + libx264-164 \ + libx264-dev \ + libxslt1.1 \ + ; \ + apt-get -yqq install \ + ca-certificates \ + curl \ + ; + +RUN set -xe; \ + curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/master/install.sh | bash; \ + . ~/.bashrc; \ + nvm install ${NODE_VERSION} \ + ; + +WORKDIR /app +COPY package* . + +RUN set -xe; \ + . ~/.bashrc; \ + npm install \ + ; + +RUN mkdir /home/tmp + + +FROM gcr.io/distroless/nodejs22-debian12 + +ARG NODE_VERSION=22.8.0 + +# Create required directories. +COPY --from=build /home/tmp /tmp + +# Browser binary +COPY --from=build /root/.cache/ms-playwright /root/.cache/ms-playwright + +# Browser libraries +COPY --from=build \ + /usr/lib/x86_64-linux-gnu/libabsl_base.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_debugging_internal.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_demangle_internal.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_graphcycles_internal.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_int128.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_malloc_internal.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_raw_logging_internal.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_spinlock_wait.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_stacktrace.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_strings_internal.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_strings.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_symbolize.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_synchronization.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_throw_delegate.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_time.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libabsl_time_zone.so.20220623 \ + /usr/lib/x86_64-linux-gnu/libaom.so.3 \ + /usr/lib/x86_64-linux-gnu/libasound.so.2 \ + /usr/lib/x86_64-linux-gnu/libatk-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libatk-bridge-2.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libatomic.so.1 \ + /usr/lib/x86_64-linux-gnu/libatspi.so.0 \ + /usr/lib/x86_64-linux-gnu/libavif.so.15 \ + /usr/lib/x86_64-linux-gnu/libblkid.so.1 \ + /usr/lib/x86_64-linux-gnu/libbrotlicommon.so.1 \ + /usr/lib/x86_64-linux-gnu/libbrotlidec.so.1 \ + /usr/lib/x86_64-linux-gnu/libbsd.so.0 \ + /usr/lib/x86_64-linux-gnu/libbz2.so.1.0 \ + /usr/lib/x86_64-linux-gnu/libcairo-gobject.so.2 \ + /usr/lib/x86_64-linux-gnu/libcairo.so.2 \ + /usr/lib/x86_64-linux-gnu/libcap.so.2 \ + /usr/lib/x86_64-linux-gnu/libcom_err.so.2 \ + /usr/lib/x86_64-linux-gnu/libdatrie.so.1 \ + /usr/lib/x86_64-linux-gnu/libdav1d.so.6 \ + /usr/lib/x86_64-linux-gnu/libdbus-1.so.3 \ + /usr/lib/x86_64-linux-gnu/libdrm_amdgpu.so.1 \ + /usr/lib/x86_64-linux-gnu/libdrm_intel.so.1 \ + /usr/lib/x86_64-linux-gnu/libdrm_nouveau.so.2 \ + /usr/lib/x86_64-linux-gnu/libdrm_radeon.so.1 \ + /usr/lib/x86_64-linux-gnu/libdrm.so.2 \ + /usr/lib/x86_64-linux-gnu/libdw.so.1 \ + /usr/lib/x86_64-linux-gnu/libedit.so.2 \ + /usr/lib/x86_64-linux-gnu/libEGL_mesa.so.0 \ + /usr/lib/x86_64-linux-gnu/libEGL.so.1 \ + /usr/lib/x86_64-linux-gnu/libelf.so.1 \ + /usr/lib/x86_64-linux-gnu/libenchant-2.so.2 \ + /usr/lib/x86_64-linux-gnu/libepoxy.so.0 \ + /usr/lib/x86_64-linux-gnu/libevdev.so.2 \ + /usr/lib/x86_64-linux-gnu/libevent-2.1.so.7 \ + /usr/lib/x86_64-linux-gnu/libexpat.so.1 \ + /usr/lib/x86_64-linux-gnu/libffi.so.8 \ + /usr/lib/x86_64-linux-gnu/libflite_cmu_grapheme_lang.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_cmu_grapheme_lex.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_cmu_indic_lang.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_cmu_indic_lex.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_cmulex.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_cmu_time_awb.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_cmu_us_awb.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_cmu_us_kal16.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_cmu_us_kal.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_cmu_us_rms.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_cmu_us_slt.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite.so.1 \ + /usr/lib/x86_64-linux-gnu/libflite_usenglish.so.1 \ + /usr/lib/x86_64-linux-gnu/libfontconfig.so.1 \ + /usr/lib/x86_64-linux-gnu/libfreetype.so.6 \ + /usr/lib/x86_64-linux-gnu/libfribidi.so.0 \ + /usr/lib/x86_64-linux-gnu/libgav1.so.1 \ + /usr/lib/x86_64-linux-gnu/libgbm.so.1 \ + /usr/lib/x86_64-linux-gnu/libgcrypt.so.20 \ + /usr/lib/x86_64-linux-gnu/libgdk-3.so.0 \ + /usr/lib/x86_64-linux-gnu/libgdk_pixbuf-2.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgio-2.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libglapi.so.0 \ + /usr/lib/x86_64-linux-gnu/libGLdispatch.so.0 \ + /usr/lib/x86_64-linux-gnu/libGLESv2.so.2 \ + /usr/lib/x86_64-linux-gnu/libglib-2.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libGL.so.1 \ + /usr/lib/x86_64-linux-gnu/libGLX.so.0 \ + /usr/lib/x86_64-linux-gnu/libgmodule-2.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgmp.so.10 \ + /usr/lib/x86_64-linux-gnu/libgnutls.so.30 \ + /usr/lib/x86_64-linux-gnu/libgobject-2.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgpg-error.so.0 \ + /usr/lib/x86_64-linux-gnu/libgraphite2.so.3 \ + /usr/lib/x86_64-linux-gnu/libgssapi_krb5.so.2 \ + /usr/lib/x86_64-linux-gnu/libgstallocators-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgstapp-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgstaudio-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgstbase-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgstcodecparsers-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgstfft-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgstgl-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgstpbutils-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgstreamer-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgsttag-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgstvideo-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libgtk-3.so.0 \ + /usr/lib/x86_64-linux-gnu/libgudev-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libharfbuzz-icu.so.0 \ + /usr/lib/x86_64-linux-gnu/libharfbuzz.so.0 \ + /usr/lib/x86_64-linux-gnu/libhogweed.so.6 \ + /usr/lib/x86_64-linux-gnu/libhyphen.so.0 \ + /usr/lib/x86_64-linux-gnu/libicudata.so.72 \ + /usr/lib/x86_64-linux-gnu/libicui18n.so.72 \ + /usr/lib/x86_64-linux-gnu/libicuuc.so.72 \ + /usr/lib/x86_64-linux-gnu/libidn2.so.0 \ + /usr/lib/x86_64-linux-gnu/libjpeg.so.62 \ + /usr/lib/x86_64-linux-gnu/libk5crypto.so.3 \ + /usr/lib/x86_64-linux-gnu/libkeyutils.so.1 \ + /usr/lib/x86_64-linux-gnu/libkrb5.so.3 \ + /usr/lib/x86_64-linux-gnu/libkrb5support.so.0 \ + /usr/lib/x86_64-linux-gnu/liblcms2.so.2 \ + /usr/lib/x86_64-linux-gnu/libLLVM-15.so.1 \ + /usr/lib/x86_64-linux-gnu/liblz4.so.1 \ + /usr/lib/x86_64-linux-gnu/liblzma.so.5 \ + /usr/lib/x86_64-linux-gnu/libmanette-0.2.so.0 \ + /usr/lib/x86_64-linux-gnu/libmd.so.0 \ + /usr/lib/x86_64-linux-gnu/libmount.so.1 \ + /usr/lib/x86_64-linux-gnu/libnettle.so.8 \ + /usr/lib/x86_64-linux-gnu/libnghttp2.so.14 \ + /usr/lib/x86_64-linux-gnu/libopus.so.0 \ + /usr/lib/x86_64-linux-gnu/liborc-0.4.so.0 \ + /usr/lib/x86_64-linux-gnu/libp11-kit.so.0 \ + /usr/lib/x86_64-linux-gnu/libpango-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libpangocairo-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libpangoft2-1.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libpciaccess.so.0 \ + /usr/lib/x86_64-linux-gnu/libpcre2-8.so.0 \ + /usr/lib/x86_64-linux-gnu/libpixman-1.so.0 \ + /usr/lib/x86_64-linux-gnu/libpng16.so.16 \ + /usr/lib/x86_64-linux-gnu/libproxy.so.1 \ + /usr/lib/x86_64-linux-gnu/libpsl.so.5 \ + /usr/lib/x86_64-linux-gnu/librav1e.so.0 \ + /usr/lib/x86_64-linux-gnu/libresolv.so.2 \ + /usr/lib/x86_64-linux-gnu/libsecret-1.so.0 \ + /usr/lib/x86_64-linux-gnu/libselinux.so.1 \ + /usr/lib/x86_64-linux-gnu/libsensors.so.5 \ + /usr/lib/x86_64-linux-gnu/libsoup-3.0.so.0 \ + /usr/lib/x86_64-linux-gnu/libsqlite3.so.0 \ + /usr/lib/x86_64-linux-gnu/libSvtAv1Enc.so.1 \ + /usr/lib/x86_64-linux-gnu/libsystemd.so.0 \ + /usr/lib/x86_64-linux-gnu/libtasn1.so.6 \ + /usr/lib/x86_64-linux-gnu/libthai.so.0 \ + /usr/lib/x86_64-linux-gnu/libtinfo.so.6 \ + /usr/lib/x86_64-linux-gnu/libudev.so.1 \ + /usr/lib/x86_64-linux-gnu/libunistring.so.2 \ + /usr/lib/x86_64-linux-gnu/libunwind.so.8 \ + /usr/lib/x86_64-linux-gnu/libvpx.so.7 \ + /usr/lib/x86_64-linux-gnu/libwayland-client.so.0 \ + /usr/lib/x86_64-linux-gnu/libwayland-cursor.so.0 \ + /usr/lib/x86_64-linux-gnu/libwayland-egl.so.1 \ + /usr/lib/x86_64-linux-gnu/libwayland-server.so.0 \ + /usr/lib/x86_64-linux-gnu/libwebpdemux.so.2 \ + /usr/lib/x86_64-linux-gnu/libwebpmux.so.3 \ + /usr/lib/x86_64-linux-gnu/libwebp.so.7 \ + /usr/lib/x86_64-linux-gnu/libwoff2common.so.1.0.2 \ + /usr/lib/x86_64-linux-gnu/libwoff2dec.so.1.0.2 \ + /usr/lib/x86_64-linux-gnu/libX11.so.6 \ + /usr/lib/x86_64-linux-gnu/libX11-xcb.so.1 \ + /usr/lib/x86_64-linux-gnu/libXau.so.6 \ + /usr/lib/x86_64-linux-gnu/libxcb-dri2.so.0 \ + /usr/lib/x86_64-linux-gnu/libxcb-dri3.so.0 \ + /usr/lib/x86_64-linux-gnu/libxcb-present.so.0 \ + /usr/lib/x86_64-linux-gnu/libxcb-randr.so.0 \ + /usr/lib/x86_64-linux-gnu/libxcb-render.so.0 \ + /usr/lib/x86_64-linux-gnu/libxcb-shm.so.0 \ + /usr/lib/x86_64-linux-gnu/libxcb.so.1 \ + /usr/lib/x86_64-linux-gnu/libxcb-sync.so.1 \ + /usr/lib/x86_64-linux-gnu/libxcb-xfixes.so.0 \ + /usr/lib/x86_64-linux-gnu/libXcomposite.so.1 \ + /usr/lib/x86_64-linux-gnu/libXcursor.so.1 \ + /usr/lib/x86_64-linux-gnu/libXdamage.so.1 \ + /usr/lib/x86_64-linux-gnu/libXdmcp.so.6 \ + /usr/lib/x86_64-linux-gnu/libXext.so.6 \ + /usr/lib/x86_64-linux-gnu/libXfixes.so.3 \ + /usr/lib/x86_64-linux-gnu/libXinerama.so.1 \ + /usr/lib/x86_64-linux-gnu/libXi.so.6 \ + /usr/lib/x86_64-linux-gnu/libxkbcommon.so.0 \ + /usr/lib/x86_64-linux-gnu/libxml2.so.2 \ + /usr/lib/x86_64-linux-gnu/libXrandr.so.2 \ + /usr/lib/x86_64-linux-gnu/libXrender.so.1 \ + /usr/lib/x86_64-linux-gnu/libxshmfence.so.1 \ + /usr/lib/x86_64-linux-gnu/libxslt.so.1 \ + /usr/lib/x86_64-linux-gnu/libyuv.so.0 \ + /usr/lib/x86_64-linux-gnu/libz3.so.4 \ + /usr/lib/x86_64-linux-gnu/libz.so.1 \ + /usr/lib/x86_64-linux-gnu/libzstd.so.1 \ + /usr/lib/x86_64-linux-gnu/ + +COPY --from=build \ + /usr/lib/x86_64-linux-gnu/dri \ + /usr/lib/x86_64-linux-gnu/dri + +COPY --from=build \ + /usr/lib/x86_64-linux-gnu/gstreamer-1.0 \ + /usr/lib/x86_64-linux-gnu/gstreamer-1.0 + +COPY --from=build \ + /usr/lib/x86_64-linux-gnu/gconv \ + /usr/lib/x86_64-linux-gnu/gconv + +COPY --from=build \ + /usr/lib/x86_64-linux-gnu/gio \ + /usr/lib/x86_64-linux-gnu/gio + +COPY --from=build \ + /usr/lib/locale \ + /usr/lib/locale + +COPY --from=build \ + /usr/lib/x86_64-linux-gnu/dri \ + /usr/lib/dri + +# Dbus and system files +COPY --from=build /usr/lib/dbus-1.0 /usr/lib/dbus-1.0 +COPY --from=build /usr/lib/systemd /usr/lib/systemd +COPY --from=build /usr/lib/tmpfiles.d /usr/lib/tmpfiles.d +COPY --from=build /usr/lib/sysusers.d /usr/lib/sysusers.d +COPY --from=build /usr/lib/sysctl.d /usr/lib/sysctl.d + +# Data files +COPY --from=build /usr/share/fonts /usr/share/fonts +COPY --from=build /usr/share/glib-2.0 /usr/share/glib-2.0 +COPY --from=build /usr/share/glvnd /usr/share/glvnd + +# Run directory +COPY --from=build /run /run + +# Node modules, including Puppeteer and application +COPY --from=build /app /app + +# Required by wrapper script +COPY --from=build /bin/sh /bin/sh + +# Actual server implementation +COPY ./server.js /app/server.js + +# Required by helper script pw_run.sh +COPY --from=build /bin/bash /bin/bash +COPY --from=build /usr/bin/env /usr/bin/env +COPY --from=build /usr/bin/uname /usr/bin/uname +COPY --from=build /usr/bin/dirname /usr/bin/dirname +COPY --from=build /usr/bin/readlink /usr/bin/readlink +COPY --from=build /usr/bin/ln /usr/bin/ln + +# Create symlinks for library directories. +RUN /usr/bin/ln -sfn /usr/lib /lib + +# Wrapper script +COPY ./wrapper.sh /usr/bin/wrapper.sh diff --git a/node-playwright-webkit-distroless/Kraftfile b/node-playwright-webkit-distroless/Kraftfile new file mode 100644 index 00000000..c6192e59 --- /dev/null +++ b/node-playwright-webkit-distroless/Kraftfile @@ -0,0 +1,12 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: ["/usr/bin/wrapper.sh", "/nodejs/bin/node", "/app/server.js"] diff --git a/node-playwright-webkit-distroless/README.md b/node-playwright-webkit-distroless/README.md new file mode 100644 index 00000000..557919b6 --- /dev/null +++ b/node-playwright-webkit-distroless/README.md @@ -0,0 +1,185 @@ +# Distroless Playwright (WebKit) with Node.js + +[Playwright](https://playwright.dev/) is a framework for web testing and Automation. + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](https://unikraft.com/docs/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/node-playwright-webkit-distroless/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/node-playwright-webkit-distroless/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +When done, invoke the following command to deploy this app on Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build . --output /node-playwright-webkit-distroless:latest +unikraft run --metro fra \ + -m 4G \ + -p 443:8080/tls+http \ + --scale-to-zero policy=idle,cooldown-time=1000,stateful=true \ + --image /node-playwright-webkit-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```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 \ + . +``` + +The output shows the instance address and other details: + +**Using the unikraft CLI (Recommended)** +```ansi title="unikraft" +metro: fra +name: node-playwright-webkit-distroless-t7r2j +uuid: a2b3c4d5-e6f7-8a9b-0c1d-a2b3c4d5e6f7 +state: starting +image: /node-playwright-webkit-distroless +resources: + memory: 4096MiB + vcpus: 1 +service: + uuid: b3c4d5e6-f7a8-9b0c-1d2e-b3c4d5e6f7a8 + name: silent-fog-er8np3fb + domains: + - fqdn: silent-fog-er8np3fb.fra.unikraft.app +networks: +- uuid: c4d5e6f7-a8b9-0c1d-2e3f-c4d5e6f7a8b9 + private-ip: 10.0.6.4 + mac: 12:b0:a0:7c:ef:d9 +timestamps: + created: just now +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: node-playwright-webkit-distroless-t7r2j + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: a2b3c4d5-e6f7-8a9b-0c1d-a2b3c4d5e6f7 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: https://silent-fog-er8np3fb.fra.unikraft.app + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//node-playwright-webkit-distroless@sha256:9e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 4096 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: silent-fog-er8np3fb + β”œβ”€ private fqdn: node-playwright-webkit-distroless-t7r2j.internal + └─── private ip: 10.0.6.4 +``` + +In this case, the instance name is `node-playwright-webkit-distroless-t7r2j` and the address is `https://silent-fog-er8np3fb.fra.unikraft.app`. +They're different for each run. + +The command will deploy the files in the current directory. +It results in the creation of a remote web-based service for creating PNG screenshots of remote pages. + +Use the `?page=` to point the service to the remote page to screenshot. +Query the service using commands such as: + +```console +curl "https://..unikraft.app/?page=https://google.com" -o ss-google.png +curl "https://..unikraft.app/?page=https://bing.com" -o ss-bing.png +``` + +You can list information about the instance by running: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances list +``` + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN + CREATED +fra node-playwright-webkit-distroless-t7r2j running /node-playwright-webkit-distroless 4096MiB 1 silent-fog-er8np3fb.fra.unikraft.app 2 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance list +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE + MEMORY VCPUS ARGS BOOT TIME +node-playwright-webkit-distroless-t7r2j silent-fog-er8np3fb.fra.unikraft.app running 1 minute ago oci://unikraft.io//node-playwright-webkit-distroless@sha256:... 4 GiB 1 2.94 s +``` + +When done, you can remove the instance: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances delete +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance remove +``` + + +## Learn more + +- [Playwright's Documentation](https://playwright.dev/docs/intro) +- [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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). diff --git a/node-playwright-webkit-distroless/package.json b/node-playwright-webkit-distroless/package.json new file mode 100644 index 00000000..f2a9230b --- /dev/null +++ b/node-playwright-webkit-distroless/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "playwright": "^1.55.1", + "playwright-webkit": "^1.55.1" + } +} diff --git a/node-playwright-webkit-distroless/server.js b/node-playwright-webkit-distroless/server.js new file mode 100644 index 00000000..e3b45f92 --- /dev/null +++ b/node-playwright-webkit-distroless/server.js @@ -0,0 +1,64 @@ +const http = require('http'); // Loads the http module +const url = require('url'); // Parse the HTTP URL +const {webkit} = require('playwright'); // Get Webkit headless browser + +const port = 8080; // Use port 8080 by default + +const requestListener = function (request, response) { + + // Get information object of request URL + const parsedURL = url.parse(request.url, true); + + // Check for `page` GET variable + if (parsedURL.pathname === '/' && parsedURL.query.page) { + remote = parsedURL.query.page; + + // Anonymous function to call Playwright + (async () => { + try { + const browser = await webkit.launch({headless: true}); + const page = await browser.newPage(); + await page.goto(remote); + const buffer = await page.screenshot(); + + await browser.close(); + + // Tell the browser everything is OK (Status code 200), and the data is PNG + response.writeHead(200, { + 'Content-Type': 'image/png' + }); + + // Write image buffer to the body of the response + response.write(buffer, 'binary'); + response.end(); + } catch(err) { + // In case of error, send a text message + response.writeHead(200, { + 'Content-Type': 'text/plain' + }); + + // Write the error text to the body of the response + response.write('Error' + err + '.\n'); + response.end(); + } + })() + + return; + } + + // If no HTTP GET variable, tell the browser everything is OK (Status code 200), and the data is in plain text + response.writeHead(200, { + 'Content-Type': 'text/plain' + }); + + // Write a simple text to the body of the page + response.write('Nothing to do.\n'); + response.end(); + +} + +// Fire up server, show banner message and wait for connections +const server = http.createServer(requestListener); +server.listen(port, () => { + console.log('Server is running on port ' + port); +}); diff --git a/node-playwright-webkit-distroless/test_node-playwright-webkit-distroless.py b/node-playwright-webkit-distroless/test_node-playwright-webkit-distroless.py new file mode 100644 index 00000000..e4bc6aa0 --- /dev/null +++ b/node-playwright-webkit-distroless/test_node-playwright-webkit-distroless.py @@ -0,0 +1,37 @@ +"""End-to-end test for the ``node-playwright-webkit-distroless`` example. + +Mirrors the manual steps from ``node-playwright-webkit-distroless/README.md``: + +1. ``unikraft build . --output /node-playwright-webkit-distroless:`` +2. ``unikraft run --metro -p 443:8080/tls+http -m 4G --image ...`` +3. ``curl https:///?page=https://example.com`` and assert + the response is a valid PNG screenshot. +""" + +from __future__ import annotations + +from _testlib.unikraft import extract_instance_name, extract_instance_url + +_PNG_MAGIC = b"\x89PNG\r\n\x1a\n" + + +def test_node_playwright_webkit_screenshot(build_image, run_instance, http, wait_instance): + image = build_image( + "node-playwright-webkit-distroless", + "node-playwright-webkit-distroless", + ) + + instance = run_instance( + image, + publish=["443:8080/tls+http"], + memory="4G", + ) + + url = extract_instance_url(instance) + assert url, f"could not determine instance URL from: {instance!r}" + + wait_instance(extract_instance_name(instance), "running") + + resp = http(f"{url}/?page=https://example.com") + assert resp.status_code == 200 + assert resp.content[:8] == _PNG_MAGIC, "response is not a valid PNG image" diff --git a/node-playwright-webkit-distroless/wrapper.sh b/node-playwright-webkit-distroless/wrapper.sh new file mode 100755 index 00000000..7e046db5 --- /dev/null +++ b/node-playwright-webkit-distroless/wrapper.sh @@ -0,0 +1,7 @@ +#!/bin/sh + +set -e + +export HOME=/root +cd /app +exec "$@" diff --git a/node18-agario-distroless/Dockerfile b/node18-agario-distroless/Dockerfile new file mode 100644 index 00000000..1fde193c --- /dev/null +++ b/node18-agario-distroless/Dockerfile @@ -0,0 +1,23 @@ +FROM node:18-bookworm-slim AS build + +RUN set -xe; \ + mkdir -p /usr/src/app; \ + mkdir -p /app-clone; \ + apt-get update; \ + apt-get install -y --no-install-recommends git ca-certificates; \ + git clone "https://github.com/owenashurst/agar.io-clone.git" /app-clone + +WORKDIR /usr/src/app + +RUN set -xe; \ + cp /app-clone/package.json /usr/src/app/package.json; \ + npm install; \ + npm cache clean --force; \ + cp -r /app-clone/. /usr/src/app; \ + rm -rf /app-clone; \ + npm run build + + +FROM gcr.io/distroless/nodejs18-debian12 + +COPY --from=build /usr/src/app /usr/src/app diff --git a/node18-agario-distroless/Kraftfile b/node18-agario-distroless/Kraftfile new file mode 100644 index 00000000..8a26af3c --- /dev/null +++ b/node18-agario-distroless/Kraftfile @@ -0,0 +1,13 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: ["/nodejs/bin/node", "/usr/src/app/bin/server/server.js"] + diff --git a/node18-agario-distroless/README.md b/node18-agario-distroless/README.md new file mode 100644 index 00000000..d99ce53e --- /dev/null +++ b/node18-agario-distroless/README.md @@ -0,0 +1,179 @@ +# Distroless Agar.io (Node) + +[Agar.io](https://agar.io/) is a popular multiplayer game where players control a cell and aim to grow by consuming smaller cells while avoiding being consumed by larger ones. +This guide deploys an implementation of the game using Node.js on Unikraft Cloud. + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](https://unikraft.com/docs/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/node18-agario-distroless/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/node18-agario-distroless/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +When done, invoke the following command to deploy this app on Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build . --output /node18-agario-distroless:latest +unikraft run --metro fra \ + -m 1G \ + -p 443:3000/tls+http \ + --scale-to-zero policy=on,cooldown-time=2000,stateful=true \ + --image /node18-agario-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud deploy --metro fra \ + -M 1Gi \ + -p 443:3000/tls+http \ + --scale-to-zero on \ + --scale-to-zero-stateful \ + --scale-to-zero-cooldown 2s \ + . +``` + +The output shows the instance address and other details: + +**Using the unikraft CLI (Recommended)** +```ansi title="unikraft" +metro: fra +name: node18-agario-distroless-5k2xp +uuid: b3c4d5e6-f7a8-9b0c-1d2e-b3c4d5e6f7a8 +state: starting +image: /node18-agario-distroless +resources: + memory: 1024MiB + vcpus: 1 +service: + uuid: c4d5e6f7-a8b9-0c1d-2e3f-c4d5e6f7a8b9 + name: dark-meadow-fj9tm6bq + domains: + - fqdn: dark-meadow-fj9tm6bq.fra.unikraft.app +networks: +- uuid: d5e6f7a8-b9c0-1d2e-3f4a-d5e6f7a8b9c0 + private-ip: 10.0.3.5 + mac: 12:b0:b1:8d:f0:ea +timestamps: + created: just now +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: node18-agario-distroless-5k2xp + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: b3c4d5e6-f7a8-9b0c-1d2e-b3c4d5e6f7a8 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: https://dark-meadow-fj9tm6bq.fra.unikraft.app + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//node18-agario-distroless@sha256:0f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 1024 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: dark-meadow-fj9tm6bq + β”œβ”€ private fqdn: node18-agario-distroless-5k2xp.internal + └─── private ip: 10.0.3.5 +``` + +In this case, the instance name is `node18-agario-distroless-5k2xp` and the address is `https://dark-meadow-fj9tm6bq.fra.unikraft.app`. +They're different for each run. + +The command will deploy an `agar.io` alternative called `https://github.com/owenashurst/agar.io-clone`. + +After deploying, you can query the service using the provided URL. + +You can list information about the instance by running: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances list +``` + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN +CREATED +fra node18-agario-distroless-5k2xp running /node18-agario-distroless 1024MiB 1 dark-meadow-fj9tm6bq.fra.unikraft.app 2 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance list +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE + MEMORY VCPUS ARGS BOOT TIME +node18-agario-distroless-5k2xp dark-meadow-fj9tm6bq.fra.unikraft.app running 1 minute ago oci://unikraft.io//node18-agario-distroless@sha256:... 1.0 GiB 1 78.95 ms +``` + +When done, you can remove the instance: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances delete +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance remove +``` + + +## Learn more + +- [Node.js's Documentation](https://nodejs.org/docs/latest/api/) +- [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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). diff --git a/node18-agario-distroless/test_node18-agario-distroless.py b/node18-agario-distroless/test_node18-agario-distroless.py new file mode 100644 index 00000000..bd546cc3 --- /dev/null +++ b/node18-agario-distroless/test_node18-agario-distroless.py @@ -0,0 +1,31 @@ +"""End-to-end test for the ``node18-agario-distroless`` example. + +Mirrors the manual steps from ``node18-agario-distroless/README.md``: + +1. ``unikraft build . --output /node18-agario-distroless:`` +2. ``unikraft run --metro -p 443:3000/tls+http -m 1G --image ...`` +3. Point browser at the instance URL and verify the game page loads. +""" + +from __future__ import annotations + +from _testlib.unikraft import extract_instance_name, extract_instance_url + + +def test_agario_serves_page(build_image, run_instance, http, wait_instance): + image = build_image("node18-agario-distroless", "node18-agario-distroless") + + instance = run_instance( + image, + publish=["443:3000/tls+http"], + memory="1G", + ) + + url = extract_instance_url(instance) + assert url, f"could not determine instance URL from: {instance!r}" + + wait_instance(extract_instance_name(instance), "running") + + resp = http(url) + assert resp.status_code == 200 + assert " **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/node18-wingsio-distroless/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/node18-wingsio-distroless/ + ``` +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +When done, invoke the following command to deploy this app on Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build . --output /node18-wingsio-distroless:latest +unikraft run --metro fra \ + -m 1G \ + -p 443:3000/tls+http \ + --scale-to-zero policy=on,cooldown-time=1500,stateful=true \ + --image /node18-wingsio-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud deploy --metro fra \ + -M 1Gi \ + -p 443:3000/tls+http \ + --scale-to-zero on \ + --scale-to-zero-stateful \ + --scale-to-zero-cooldown 1500ms \ + . +``` + +The output shows the instance address and other details: + +**Using the unikraft CLI (Recommended)** +```ansi title="unikraft" +metro: fra +name: node18-wingsio-distroless-h4n8m +uuid: c4d5e6f7-a8b9-0c1d-2e3f-c4d5e6f7a8b9 +state: starting +image: /node18-wingsio-distroless +resources: + memory: 1024MiB + vcpus: 1 +service: + uuid: d5e6f7a8-b9c0-1d2e-3f4a-d5e6f7a8b9c0 + name: swift-cloud-gk7us4cz + domains: + - fqdn: swift-cloud-gk7us4cz.fra.unikraft.app +networks: +- uuid: e6f7a8b9-c0d1-2e3f-4a5b-e6f7a8b9c0d1 + private-ip: 10.0.4.4 + mac: 12:b0:c2:9e:01:fb +timestamps: + created: just now +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: node18-wingsio-distroless-h4n8m + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: c4d5e6f7-a8b9-0c1d-2e3f-c4d5e6f7a8b9 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: https://swift-cloud-gk7us4cz.fra.unikraft.app + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//node18-wingsio-distroless@sha256:1a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 1024 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: swift-cloud-gk7us4cz + β”œβ”€ private fqdn: node18-wingsio-distroless-h4n8m.internal + └─── private ip: 10.0.4.4 +``` + +In this case, the instance name is `node18-wingsio-distroless-h4n8m` and the address is `https://swift-cloud-gk7us4cz.fra.unikraft.app`. +They're different for each run. + +The command will deploy an `wings.io` alternative called `https://github.com/Blendlight/wings.io-clone-io`. + +After deploying, you can query the service using the provided URL. + +You can list information about the instance by running: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances list +``` + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN + CREATED +fra node18-wingsio-distroless-h4n8m running /node18-wingsio-distroless 1024MiB 1 swift-cloud-gk7us4cz.fra.unikraft.app 2 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance list +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE + MEMORY VCPUS ARGS BOOT TIME +node18-wingsio-distroless-h4n8m swift-cloud-gk7us4cz.fra.unikraft.app running 1 minute ago oci://unikraft.io//node18-wingsio-distroless@sha256:... 1.0 GiB 1 82.16 ms +``` + +When done, you can remove the instance: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances delete +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance remove +``` + + +## Learn more + +- [Node.js's Documentation](https://nodejs.org/docs/latest/api/) +- [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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). diff --git a/node18-wingsio-distroless/test_node18-wingsio-distroless.py b/node18-wingsio-distroless/test_node18-wingsio-distroless.py new file mode 100644 index 00000000..93f4b989 --- /dev/null +++ b/node18-wingsio-distroless/test_node18-wingsio-distroless.py @@ -0,0 +1,31 @@ +"""End-to-end test for the ``node18-wingsio-distroless`` example. + +Mirrors the manual steps from ``node18-wingsio-distroless/README.md``: + +1. ``unikraft build . --output /node18-wingsio-distroless:`` +2. ``unikraft run --metro -p 443:3000/tls+http -m 1G --image ...`` +3. Point browser at the instance URL and verify the game page loads. +""" + +from __future__ import annotations + +from _testlib.unikraft import extract_instance_name, extract_instance_url + + +def test_wingsio_serves_page(build_image, run_instance, http, wait_instance): + image = build_image("node18-wingsio-distroless", "node18-wingsio-distroless") + + instance = run_instance( + image, + publish=["443:3000/tls+http"], + memory="1G", + ) + + url = extract_instance_url(instance) + assert url, f"could not determine instance URL from: {instance!r}" + + wait_instance(extract_instance_name(instance), "running") + + resp = http(url) + assert resp.status_code == 200 + assert " **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/node21-websocket-distroless/` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/node21-websocket-distroless/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +When done, invoke the following command to deploy this app on Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build . --output /node21-websocket-distroless:latest +unikraft run --metro fra \ + -m 1G \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ + --image /node21-websocket-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud deploy --metro fra \ + -M 1Gi \ + -p 443:8080/tls+http \ + --scale-to-zero on \ + --scale-to-zero-stateful \ + --scale-to-zero-cooldown 1s \ + . +``` + +The output shows the instance address and other details: + +**Using the unikraft CLI (Recommended)** +```ansi title="unikraft" +metro: fra +name: node21-websocket-distroless-j2x9r +uuid: d5e6f7a8-b9c0-1d2e-3f4a-d5e6f7a8b9c0 +state: starting +image: /node21-websocket-distroless +resources: + memory: 1024MiB + vcpus: 1 +service: + uuid: e6f7a8b9-c0d1-2e3f-4a5b-e6f7a8b9c0d1 + name: lively-breeze-hp3wx6yt + domains: + - fqdn: lively-breeze-hp3wx6yt.fra.unikraft.app +networks: +- uuid: f7a8b9c0-d1e2-3f4a-5b6c-f7a8b9c0d1e2 + private-ip: 10.0.5.4 + mac: 12:b0:d3:af:12:0c +timestamps: + created: just now +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: node21-websocket-distroless-j2x9r + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: d5e6f7a8-b9c0-1d2e-3f4a-d5e6f7a8b9c0 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: https://lively-breeze-hp3wx6yt.fra.unikraft.app + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//node21-websocket-distroless@sha256:2b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 1024 MiB + β”œβ”€β”€β”€β”€β”€β”€ service: lively-breeze-hp3wx6yt + β”œβ”€ private fqdn: node21-websocket-distroless-j2x9r.internal + └─── private ip: 10.0.5.4 +``` + +In this case, the instance name is `node21-websocket-distroless-j2x9r` and the address is `https://lively-breeze-hp3wx6yt.fra.unikraft.app`. +They're different for each run. + +The command will build the files in the current directory. + +After deploying, you can query the service with a WebSocket client, such as [`wscat`](https://github.com/websockets/wscat). +Install `wscat` with `npm`: + +```console +npm install -g wscat +``` + +Then query the WebSocket server deployed on Unikraft Cloud, using its URL: + +```console +wscat --connect wss://..unikraft.app +``` + +Then enter messages, that will be replied by the server. + +You can list information about the instance by running: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances list +``` + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN + CREATED +fra node21-websocket-distroless-j2x9r running /node21-websocket-distroless 1024MiB 1 lively-breeze-hp3wx6yt.fra.unikraft.app 2 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance list +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE + MEMORY VCPUS ARGS BOOT TIME +node21-websocket-distroless-j2x9r lively-breeze-hp3wx6yt.fra.unikraft.app running 1 minute ago oci://unikraft.io//node21-websocket-distroless@sha256:... 1.0 GiB 1 45.83 ms +``` + +When done, you can remove the instance: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances delete +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance remove +``` + + +## Learn more + +- [WebSocket documentation](https://nextjs.org/docs) +- [ws: A Node.js WebSocket library](https://github.com/websockets/ws) +- [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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). diff --git a/node21-websocket-distroless/package-lock.json b/node21-websocket-distroless/package-lock.json new file mode 100644 index 00000000..2d27633e --- /dev/null +++ b/node21-websocket-distroless/package-lock.json @@ -0,0 +1,36 @@ +{ + "name": "websocket", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "websocket", + "version": "0.1.0", + "dependencies": { + "ws": "^8.21" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + } + } +} diff --git a/node21-websocket-distroless/package.json b/node21-websocket-distroless/package.json new file mode 100644 index 00000000..1140d71c --- /dev/null +++ b/node21-websocket-distroless/package.json @@ -0,0 +1,8 @@ +{ + "name": "websocket", + "type": "module", + "version": "0.1.0", + "dependencies": { + "ws": "^8.21" + } +} diff --git a/node21-websocket-distroless/server.js b/node21-websocket-distroless/server.js new file mode 100644 index 00000000..5a3f1109 --- /dev/null +++ b/node21-websocket-distroless/server.js @@ -0,0 +1,24 @@ +import { WebSocketServer } from 'ws'; + +const port = 8080; +const wss = new WebSocketServer({ port: port }); + +wss.on('connection', function connection(ws, req) { + const ip = req.socket.remoteAddress; + + console.log('Client connected from %s', ip); + ws.send('Connection received. Waiting for messages.'); + + ws.on('error', console.error); + + ws.on('message', function message(data) { + console.log('received: %s', data); + ws.send(data); + }); + + ws.on('close', function close() { + console.log('Client disconnected from %s', ip); + }); +}); + +console.log('WebSocket server is running on ws://localhost:%d', port); diff --git a/node21-websocket-distroless/test_node21-websocket-distroless.py b/node21-websocket-distroless/test_node21-websocket-distroless.py new file mode 100644 index 00000000..ea76f289 --- /dev/null +++ b/node21-websocket-distroless/test_node21-websocket-distroless.py @@ -0,0 +1,69 @@ +"""End-to-end test for the ``node21-websocket-distroless`` example. + +Mirrors the manual steps from ``node21-websocket-distroless/README.md`` and the +existing CI workflow (example-node21-websocket-stable.yaml): + +1. ``unikraft build . --output /node21-websocket-distroless:`` +2. ``unikraft run --metro -p 443:8080/tls+http -m 1G --image ...`` +3. Connect via WebSocket (wss://) and verify echo behaviour. + +The CI workflow sends "hello" and asserts it is echoed back. We also +verify the initial greeting message and multiple echo round-trips. +""" + +from __future__ import annotations + +import asyncio + +import websockets + +from _testlib.unikraft import extract_instance_fqdn, extract_instance_name + + +async def _websocket_test(host: str) -> None: + """Connect to the WebSocket server and exercise its echo behaviour.""" + uri = f"wss://{host}" + + conn = await asyncio.wait_for( + websockets.connect(uri), + timeout=10, + ) + async with conn: + # ------------------------------------------------------------------ + # 1. Greeting β€” server sends a welcome message on connect. + # ------------------------------------------------------------------ + greeting = await asyncio.wait_for(conn.recv(), timeout=10) + assert "Connection received" in greeting + + # ------------------------------------------------------------------ + # 2. Echo β€” matches CI workflow assertion (send "hello", expect it back). + # ------------------------------------------------------------------ + await conn.send("hello") + reply = await asyncio.wait_for(conn.recv(), timeout=10) + assert reply.decode() == "hello" + + # ------------------------------------------------------------------ + # 3. Multiple echo round-trips β€” verify stateless echo behaviour. + # ------------------------------------------------------------------ + messages = ["foo", "bar baz", "πŸš€ unicode test", ""] + for msg in messages: + await conn.send(msg) + reply = await asyncio.wait_for(conn.recv(), timeout=10) + assert reply.decode() == msg, f"expected {msg!r}, got {reply!r}" + + +def test_node21_websocket(build_image, run_instance, wait_instance): + """Build, deploy, and exercise a Node.js WebSocket echo server.""" + image = build_image("node21-websocket-distroless", "node21-websocket-distroless") + + instance = run_instance( + image, + publish=["443:8080/tls+http"], + memory="1G", + ) + + host = extract_instance_fqdn(instance) + assert host, f"could not determine instance FQDN from: {instance!r}" + + wait_instance(extract_instance_name(instance), "running") + asyncio.run(_websocket_test(host)) diff --git a/node24-karaoke-distroless/.dockerignore b/node24-karaoke-distroless/.dockerignore new file mode 100644 index 00000000..9ac9448d --- /dev/null +++ b/node24-karaoke-distroless/.dockerignore @@ -0,0 +1 @@ +.unikraft diff --git a/node24-karaoke-distroless/Dockerfile b/node24-karaoke-distroless/Dockerfile new file mode 100644 index 00000000..1e0c8815 --- /dev/null +++ b/node24-karaoke-distroless/Dockerfile @@ -0,0 +1,36 @@ +FROM node:24-bookworm-slim AS build + +RUN set -xe; \ + apt-get update; \ + apt-get install -y --no-install-recommends \ + ca-certificates \ + git \ + ; \ + npm install --global corepack@latest; \ + corepack enable pnpm; \ + corepack use pnpm@latest-10 + +WORKDIR /allkaraoke + +RUN set -xe; \ + git clone --depth=1 https://github.com/Asvarox/allkaraoke.git /allkaraoke; \ + pnpm install; \ + pnpm exec playwright install-deps; \ + pnpm exec playwright install; \ + pnpm build + +# Install serve globally for use as static file server +RUN npm install -g serve + + +FROM gcr.io/distroless/nodejs24-debian12 + +# System binaries +COPY --from=build /bin/sh /bin/sh + +# serve (static file server) +COPY --from=build /usr/local/lib/node_modules/serve /usr/local/lib/node_modules/serve + +# Distribution configuration +COPY --from=build /allkaraoke/build/client /wwwroot +COPY ./entrypoint.sh /entrypoint.sh diff --git a/node24-karaoke-distroless/Kraftfile b/node24-karaoke-distroless/Kraftfile new file mode 100644 index 00000000..67fd48fa --- /dev/null +++ b/node24-karaoke-distroless/Kraftfile @@ -0,0 +1,12 @@ +spec: v0.7 + +runtime: base-compat:latest + +targets: + - kraftcloud/x86_64 + +rootfs: + source: ./Dockerfile + format: erofs + +cmd: ["/entrypoint.sh"] diff --git a/node24-karaoke-distroless/README.md b/node24-karaoke-distroless/README.md new file mode 100644 index 00000000..c7552d39 --- /dev/null +++ b/node24-karaoke-distroless/README.md @@ -0,0 +1,232 @@ +# Distroless Node AllKaraoke + +[Allkaraoke](https://github.com/Asvarox/allkaraoke) offers an ultrastar deluxe-like online platform for karaoke. + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](https://unikraft.com/docs/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/node24-karaoke-distroless` directory: + + ```bash + git clone https://github.com/unikraft-cloud/examples + cd examples/node24-karaoke-distroless/ + ``` + +Make sure to log into Unikraft Cloud and pick a [metro](https://unikraft.com/docs/platform/metros) close to you. +This guide uses `fra` (Frankfurt, πŸ‡©πŸ‡ͺ): + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft login +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +When done, invoke the following command to deploy this app on Unikraft Cloud: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft build . --output /node24-karaoke-distroless:latest +unikraft run --metro fra \ + -m 2G \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=2000,stateful=true \ + --image /node24-karaoke-distroless:latest +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud deploy \ + -M 2Gi \ + -p 443:8080/tls+http \ + --scale-to-zero on \ + --scale-to-zero-stateful \ + --scale-to-zero-cooldown 2s \ + . +``` + +The output shows the instance address and other details: + +**Using the unikraft CLI (Recommended)** +```ansi title="unikraft" +metro: fra +name: node24-karaoke-distroless-9lw5q +uuid: e5f6a7b8-c9d0-1234-efab-345678901234 +state: starting +image: /node24-karaoke-distroless +resources: + memory: 2GiB + vcpus: 1 +service: + uuid: ef4112f8-10fc-fe6e-f48c-43a6623ec878 + name: wild-song-p5q2nrwx + domains: + - fqdn: wild-song-p5q2nrwx.fra.unikraft.app +networks: +- uuid: cf5f3cbb-abf5-632e-3dd6-2de91885c6d9 + private-ip: 10.0.3.8 + mac: 12:b0:30:64:22:f9 +timestamps: + created: just now +``` + +or + +**Using the legacy kraft CLI** +```ansi title="kraft" +[●] Deployed successfully! + β”‚ + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ name: node24-karaoke-distroless-9lw5q + β”œβ”€β”€β”€β”€β”€β”€β”€β”€β”€ uuid: e5f6a7b8-c9d0-1234-efab-345678901234 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ metro: https://api.fra.unikraft.cloud/v1 + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ state: starting + β”œβ”€β”€β”€β”€β”€β”€β”€ domain: https://wild-song-p5q2nrwx.fra.unikraft.app + β”œβ”€β”€β”€β”€β”€β”€β”€β”€ image: oci://unikraft.io//node24-karaoke-distroless@sha256:1a3c5e7b9d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0b2c4e6f8a0b2d4f6a8c0e2b + β”œβ”€β”€β”€β”€β”€β”€β”€ memory: 2 GiB + β”œβ”€β”€β”€β”€β”€β”€ service: wild-song-p5q2nrwx + β”œβ”€ private fqdn: node24-karaoke-distroless-9lw5q.internal + └─── private ip: 10.0.3.8 +``` + +In this case, the instance name is `node24-karaoke-distroless-9lw5q` and the address is `https://wild-song-p5q2nrwx.fra.unikraft.app`. +They're different for each run. + +Use `curl` to query the Unikraft Cloud instance of the AllKaraoke instance: + +```bash +curl https://wild-song-p5q2nrwx.fra.unikraft.app +``` + +```text + + + + + + + AllKaraoke.Party - Free Online Karaoke + ... + + ... + +``` + +You can list information about the instance by running: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances list +``` + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node24-karaoke-distroless-9lw5q running /node24-karaoke-distroless 2GiB 1 wild-song-p5q2nrwx.fra.unikraft.app 2 minutes ago +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance list +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE + MEMORY VCPUS ARGS BOOT TIME +node24-karaoke-distroless-9lw5q wild-song-p5q2nrwx.fra.unikraft.app running since 3mins oci://unikraft.io//node24-karaoke-distroless@sha256:... 2 GiB 1 1.24 s +``` + +When done, you can remove the instance: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft instances delete node24-karaoke-distroless-9lw5q +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud instance remove node24-karaoke-distroless-9lw5q +``` + +## Customize your app + +To customize the app, update the files in the repository, listed below: + +* `Kraftfile`: the Unikraft Cloud specification +* `Dockerfile`: the Docker-specified app filesystem +* `entrypoint.sh`: the shell script used to start the AllKaraoke server + +Lines in the `Kraftfile` have the following roles: + +* `spec: v0.7`: The current `Kraftfile` specification version is `0.7`. + +* `runtime: base-compat:latest`: The kernel to use. + +* `rootfs`: Build the app root filesystem. + `source: ./Dockerfile` means the filesystem is built using the `Dockerfile`. + `format: erofs` means the filesystem type is [EROFS](https://erofs.docs.kernel.org/). + +* `cmd: ["/entrypoint.sh"]`: Use `/entrypoint.sh` as the starting command of the instance. + +Lines in the `Dockerfile` have the following roles: + +* `FROM node:24-bookworm-slim AS build`: Build the AllKaraoke project using the Node.js 24 Bookworm slim image. + +* `RUN git clone ...; pnpm install; pnpm build`: Clone the AllKaraoke repository, install dependencies, and build it for production. + +* `FROM gcr.io/distroless/nodejs24-debian12`: Use a fresh Node.js 24 Bookworm distroless image for the runtime. + +* `COPY ...`: Copy required files to the app filesystem: the built AllKaraoke artifacts, and the entrypoint script. + +The following options are available for customizing the app: + +* If you want to use a specific version of AllKaraoke, update the `git clone` command in the `Dockerfile` to pin a particular commit or tag. + +* If you want to add extra files, you need to copy them into the filesystem using the `COPY` command in the `Dockerfile`. + +* If you want to change the startup behavior, update the `entrypoint.sh` script. + +* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). + +## Learn more + +- [Allkaraoke official deployment](https://allkaraoke.party/) +- [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: + +**Using the unikraft CLI (Recommended)** +```bash title="unikraft" +unikraft --help +``` + +or + +**Using the legacy kraft CLI** +```bash title="kraft" +kraft cloud --help +``` + +Or visit the [CLI Reference](https://unikraft.com/docs/cli/unikraft) or the [legacy CLI Reference](https://unikraft.com/docs/cli/kraft/overview). diff --git a/node24-karaoke-distroless/entrypoint.sh b/node24-karaoke-distroless/entrypoint.sh new file mode 100755 index 00000000..eed10203 --- /dev/null +++ b/node24-karaoke-distroless/entrypoint.sh @@ -0,0 +1,2 @@ +#!/bin/sh +exec /nodejs/bin/node /usr/local/lib/node_modules/serve/build/main.js -s /wwwroot -p 8080 diff --git a/node24-karaoke-distroless/test_node24-karaoke-distroless.py b/node24-karaoke-distroless/test_node24-karaoke-distroless.py new file mode 100644 index 00000000..caa86b76 --- /dev/null +++ b/node24-karaoke-distroless/test_node24-karaoke-distroless.py @@ -0,0 +1,30 @@ +"""End-to-end test for the ``node24-karaoke-distroless`` example. + +Mirrors the manual steps from ``node24-karaoke-distroless/README.md``: + +1. ``unikraft build . --output /node24-karaoke-distroless:`` +2. ``unikraft run --metro -p 443:8080/tls+http -m 2G --image ...`` +3. ``curl https://`` and assert the karaoke app is served. +""" + +from __future__ import annotations + +from _testlib.unikraft import extract_instance_name, extract_instance_url + + +def test_karaoke_serves_page(build_image, run_instance, http, wait_instance): + image = build_image("node24-karaoke-distroless", "node24-karaoke-distroless") + + instance = run_instance( + image, + publish=["443:8080/tls+http"], + memory="2G", + ) + + url = extract_instance_url(instance) + assert url, f"could not determine instance URL from: {instance!r}" + + wait_instance(extract_instance_name(instance), "running") + + resp = http(url) + assert resp.status_code == 200