diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 00000000..df398fbd --- /dev/null +++ b/.dockerignore @@ -0,0 +1,45 @@ +# Vale packages +.vale/styles/* +!.vale/styles/Unikraft/* +!.vale/styles/config/vocabularies/Unikraft/accept.txt + +# Node/Zudoku +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# zudoku +/dist/ +/.zudoku/ + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# env files (can opt-in for commiting if needed) +.env* + +# typescript +*.tsbuildinfo + +# build outputs +dist/ + +# generated files +apis/platform.yaml +build.log +.examples/ diff --git a/.eslintrc.cjs b/.eslintrc.cjs new file mode 100644 index 00000000..bf021ee7 --- /dev/null +++ b/.eslintrc.cjs @@ -0,0 +1,35 @@ +module.exports = { + env: { + browser: true, + es2021: true, + node: true, + }, + extends: [ + 'eslint:recommended', + 'plugin:@typescript-eslint/recommended' + ], + overrides: [ + { + env: { + node: true + }, + files: [ + '.eslintrc.{js,cjs}', + 'scripts/**/*.js' + ], + parserOptions: { + sourceType: 'module' + } + } + ], + parser: '@typescript-eslint/parser', + parserOptions: { + ecmaVersion: 'latest', + sourceType: 'module' + }, + plugins: [ + '@typescript-eslint' + ], + rules: { + } +} diff --git a/.github/actions/cleanup-www/action.yaml b/.github/actions/cleanup-www/action.yaml new file mode 100644 index 00000000..142d6d14 --- /dev/null +++ b/.github/actions/cleanup-www/action.yaml @@ -0,0 +1,39 @@ +name: cleanup-www + +description: | + Destroys the Unikraft landing and docs website (www). + +inputs: + name: + required: true + description: Name of the service. + metro: + required: false + description: Unikraft Cloud Metro. + default: was1 + delete-image: + required: false + description: Also delete the image. + default: "false" + +runs: + using: composite + steps: + - name: Delete Service + if: always() + id: service + shell: bash + run: | + kraft cloud --metro "${{ inputs.metro }}" service remove \ + --drain \ + "${{ inputs.name }}" \ + ; + + - name: Delete Image + if: ${{ inputs.delete-image }} + id: image + shell: bash + run: | + kraft cloud --metro "${{ inputs.metro }}" image remove \ + "${{ inputs.name }}" \ + ; diff --git a/.github/actions/setup-kraftkit/action.yaml b/.github/actions/setup-kraftkit/action.yaml new file mode 100644 index 00000000..f4872f85 --- /dev/null +++ b/.github/actions/setup-kraftkit/action.yaml @@ -0,0 +1,26 @@ +name: setup-kraftkit + +description: Setup KraftKit. + +inputs: + version: + description: KraftKit version to install (use latest to auto-detect). + required: false + default: latest + +runs: + using: composite + steps: + - name: Install + shell: bash + run: | + if [[ -z $(command -v kraft) ]]; then + _VERSION="${{ inputs.version }}" + if [[ -z "${_VERSION}" || "${_VERSION}" == "latest" ]]; then + _VERSION=$(curl -fsSL https://get.kraftkit.sh/latest.txt) + fi + wget -nv -LO kraft.tar.gz https://github.com/unikraft/kraftkit/releases/download/v${_VERSION}/kraft_${_VERSION}_linux_amd64.tar.gz; + tar xzf kraft.tar.gz kraft; + sudo mv kraft /usr/bin/; + rm kraft.tar.gz; + fi diff --git a/.github/actions/setup-www/action.yaml b/.github/actions/setup-www/action.yaml new file mode 100644 index 00000000..7afcc293 --- /dev/null +++ b/.github/actions/setup-www/action.yaml @@ -0,0 +1,133 @@ +name: setup-www + +description: | + Deploys the Unikraft landing and docs website (www). + +inputs: + environment: + required: true + description: The environment to use. + name: + required: true + description: Name of the service. + domain: + required: false + description: The domain to use for the deployment. + metro: + required: false + description: Unikraft Cloud Metro. + default: http://api.dal2.unikraft.cloud/v1 + memory: + required: false + description: Set the memory size. + default: 1Gi + args: + required: false + description: Optional arguments you pass to kraft cloud deploy. + port: + required: false + description: The port the service listens on. + default: "2015" + log-level: + required: false + default: error + description: Set the logging level. + posthog-host: + required: false + description: The host address for the PostHog API. + posthog-key: + required: false + description: The key to authenticate with PostHog. + docs-channel: + required: false + default: prod-staging + description: The docs channel to use for the OpenAPI spec. + +outputs: + domain: + description: | + Either the input domain if set or the automatically generated value + otherwise. + value: ${{ steps.service.outputs.domain }} + +runs: + using: composite + steps: + - name: Create Deployment + uses: bobheadxi/deployments@v1 + id: init-deploy + with: + step: start + token: ${{ github.token }} + env: ${{ inputs.environment }} + + - name: Create Service Group + id: service + shell: bash + run: | + set -e; + + # Create the service if it does not exist. + _SVC=$(kraft cloud --metro "${{ inputs.metro }}" service get "${{ inputs.name }}" -o raw | jq -r '.data.service_groups[].error') + if [[ "${_SVC}" == "8" ]]; then + if [[ "${{ inputs.domain }}" == "" ]]; then + kraft cloud --metro "${{ inputs.metro }}" service create \ + --subdomain "${{ inputs.name }}" \ + --name "${{ inputs.name }}" \ + "443:${{ inputs.port }}" \ + ; + else + kraft cloud --metro "${{ inputs.metro }}" service create \ + --domain "${{ inputs.domain }}" \ + --name "${{ inputs.name }}" \ + "443:${{ inputs.port }}" \ + ; + fi + fi + + # Save the generated domain as an output variable + echo "domain=$(kraft cloud --metro ${{ inputs.metro }} service get ${{ inputs.name }} -o raw | jq -r '.data.service_groups[0].domains[0].fqdn')" >> $GITHUB_OUTPUT + + - name: Deploy + id: deploy + shell: bash + run: | + kraft cloud deploy \ + --rollout remove \ + --rollout-qualifier all \ + --build-arg API_URL="https://${{ steps.service.outputs.domain }}" \ + --build-arg PUBLIC_API_URL="https://${{ steps.service.outputs.domain }}" \ + --build-arg VITE_PUBLIC_SITE_URL="https://${{ steps.service.outputs.domain }}" \ + --build-arg PUBLIC_STATUS_PAGE_SUMMARY_URL="https://status.unikraft.cloud/api/v1/summary" \ + --build-arg PUBLIC_POSTHOG_HOST="${{ inputs.posthog-host }}" \ + --build-arg PUBLIC_POSTHOG_KEY="${{ inputs.posthog-key }}" \ + --build-arg DOCS_CHANNEL="${{ inputs.docs-channel }}" \ + --memory "${{ inputs.memory }}" \ + --service "${{ inputs.name }}" \ + --image "${{ inputs.name }}" \ + --as kraftfile-runtime \ + .; + + - name: Test liveliness + id: liveliness + continue-on-error: true + shell: bash + run: | + sleep 10; + curl -Lk --fail "https://${{ steps.service.outputs.domain }}/" + + - name: Dump instance logs on failure + if: ${{ steps.liveliness.outcome == 'failure' }} + shell: bash + run: | + kraft cloud service logs "${{ inputs.name }}" + exit 1 + + - name: Update Deployment + uses: bobheadxi/deployments@v1 + with: + step: finish + token: ${{ github.token }} + env: ${{ inputs.environment }} + deployment_id: ${{ steps.init-deploy.outputs.deployment_id }} + status: ${{ job.status }} diff --git a/.github/workflows/pr-preview.yaml b/.github/workflows/pr-preview.yaml new file mode 100644 index 00000000..ea7b74e3 --- /dev/null +++ b/.github/workflows/pr-preview.yaml @@ -0,0 +1,120 @@ +name: pr/preview + +on: + pull_request: + types: + - opened + - synchronize + - reopened + - closed + branches: + - prod-staging + +# Automatically cancel in-progress actions on the same branch +concurrency: + group: ${{ github.workflow }}-${{ github.event_name == 'pull_request_target' && github.head_ref || github.ref }} + cancel-in-progress: true + +env: + UKC_TOKEN: ${{ secrets.UKC_TOKEN }} + UKC_METRO: ${{ vars.UKC_METRO }} + KRAFTKIT_LOG_LEVEL: error + KRAFTKIT_NO_CHECK_UPDATES: "true" + +permissions: + deployments: write + contents: read + pull-requests: write + +jobs: + deploy: + environment: pull-requests + runs-on: arc-ubuntu24.04-big + if: ${{ github.event.action != 'closed' }} + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate required secrets + shell: bash + run: | + if [[ -z "${{ vars.UKC_METRO }}" ]]; then + echo "Missing UKC_METRO variable" >&2 + exit 1 + fi + + + - name: Setup KraftKit + uses: ./.github/actions/setup-kraftkit + + - name: Setup docs website + id: www + uses: ./.github/actions/setup-www + continue-on-error: true + with: + environment: pull-requests + name: com-docs-pr-${{ github.event.number }} + metro: ${{ vars.UKC_METRO }} + log-level: trace + docs-channel: prod-staging + + - uses: chrnorm/deployment-action@v2 + name: Create GitHub deployment + if: always() + id: pr-deploy-github + with: + token: '${{ github.token }}' + environment-url: 'https://${{ steps.www.outputs.domain }}' + environment: pull-requests + + - name: Update PR deployment status (success) + if: ${{ steps.www.outcome == 'success' }} + uses: chrnorm/deployment-status@v2 + with: + token: '${{ github.token }}' + environment-url: 'https://${{ steps.www.outputs.domain }}' + deployment-id: '${{ steps.pr-deploy-github.outputs.deployment_id }}' + state: 'success' + + - name: Update PR deployment status (failure) + if: ${{ steps.www.outcome == 'failure' }} + uses: chrnorm/deployment-status@v2 + with: + token: '${{ github.token }}' + environment-url: 'https://${{ steps.www.outputs.domain }}' + deployment-id: '${{ steps.pr-deploy-github.outputs.deployment_id }}' + state: 'failure' + + - name: Fail job if deploy failed + if: ${{ steps.www.outcome == 'failure' }} + shell: bash + run: | + exit 1 + + cleanup: + environment: pull-requests + runs-on: arc-ubuntu24.04-big + if: ${{ github.event.action == 'closed' }} + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate required secrets + shell: bash + run: | + if [[ -z "${{ vars.UKC_METRO }}" ]]; then + echo "Missing UKC_METRO variable" >&2 + exit 1 + fi + + - name: Setup KraftKit + uses: ./.github/actions/setup-kraftkit + + - name: Cleanup deployment + id: cleanup-www + uses: ./.github/actions/cleanup-www + with: + name: com-docs-pr-${{ github.event.number }} + metro: ${{ vars.UKC_METRO }} + delete-image: true diff --git a/.github/workflows/prod-stable.yaml b/.github/workflows/prod-stable.yaml new file mode 100644 index 00000000..67ba4a05 --- /dev/null +++ b/.github/workflows/prod-stable.yaml @@ -0,0 +1,85 @@ +name: prod-stable + +on: + push: + branches: [prod-stable] + paths: + - '**' + - '!.github/workflows/**/*' + - '.github/workflows/prod-stable.yaml' + - '!README.md' + +env: + UKC_TOKEN: ${{ secrets.UKC_TOKEN }} + UKC_METRO: ${{ vars.UKC_METRO }} + KRAFTKIT_LOG_LEVEL: debug + KRAFTKIT_NO_CHECK_UPDATES: "true" + +permissions: + deployments: write + contents: read + +jobs: + deploy: + environment: prod-stable + runs-on: arc-ubuntu24.04-big + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate required secrets + shell: bash + run: | + if [[ -z "${{ vars.UKC_METRO }}" ]]; then + echo "Missing UKC_METRO variable" >&2 + exit 1 + fi + + + - name: Setup KraftKit + uses: ./.github/actions/setup-kraftkit + + - name: Setup docs website + id: www + uses: ./.github/actions/setup-www + continue-on-error: true + with: + environment: prod-stable + name: com-docs-stable + metro: ${{ vars.UKC_METRO }} + log-level: trace + docs-channel: prod-stable + + - uses: chrnorm/deployment-action@v2 + name: Create GitHub deployment + if: always() + id: deploy-github + with: + token: '${{ github.token }}' + environment-url: 'https://com-docs-stable.ukp-stable.apw.unikraft.internal' + environment: prod-stable + + - name: Update deployment status (success) + if: ${{ steps.www.outcome == 'success' }} + uses: chrnorm/deployment-status@v2 + with: + token: '${{ github.token }}' + environment-url: 'https://com-docs-stable.ukp-stable.apw.unikraft.internal' + deployment-id: '${{ steps.deploy-github.outputs.deployment_id }}' + state: 'success' + + - name: Update deployment status (failure) + if: ${{ steps.www.outcome == 'failure' }} + uses: chrnorm/deployment-status@v2 + with: + token: '${{ github.token }}' + environment-url: 'https://com-docs-stable.ukp-stable.apw.unikraft.internal' + deployment-id: '${{ steps.deploy-github.outputs.deployment_id }}' + state: 'failure' + + - name: Fail job if deploy failed + if: ${{ steps.www.outcome == 'failure' }} + shell: bash + run: | + exit 1 diff --git a/.github/workflows/prod-staging.yaml b/.github/workflows/prod-staging.yaml new file mode 100644 index 00000000..49d353d7 --- /dev/null +++ b/.github/workflows/prod-staging.yaml @@ -0,0 +1,85 @@ +name: prod-staging + +on: + push: + branches: [prod-staging] + paths: + - '**' + - '!.github/workflows/**/*' + - '.github/workflows/prod-staging.yaml' + - '!README.md' + +env: + UKC_TOKEN: ${{ secrets.UKC_TOKEN }} + UKC_METRO: ${{ vars.UKC_METRO }} + KRAFTKIT_LOG_LEVEL: debug + KRAFTKIT_NO_CHECK_UPDATES: "true" + +permissions: + deployments: write + contents: read + +jobs: + deploy: + environment: prod-staging + runs-on: arc-ubuntu24.04-big + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Validate required secrets + shell: bash + run: | + if [[ -z "${{ vars.UKC_METRO }}" ]]; then + echo "Missing UKC_METRO variable" >&2 + exit 1 + fi + + + - name: Setup KraftKit + uses: ./.github/actions/setup-kraftkit + + - name: Setup docs website + id: www + uses: ./.github/actions/setup-www + continue-on-error: true + with: + environment: prod-staging + name: com-docs-staging + metro: ${{ vars.UKC_METRO }} + log-level: trace + docs-channel: prod-staging + + - uses: chrnorm/deployment-action@v2 + name: Create GitHub deployment + if: always() + id: deploy-github + with: + token: '${{ github.token }}' + environment-url: 'https://com-docs-staging.ukp-stable.apw.unikraft.internal' + environment: prod-staging + + - name: Update deployment status (success) + if: ${{ steps.www.outcome == 'success' }} + uses: chrnorm/deployment-status@v2 + with: + token: '${{ github.token }}' + environment-url: 'https://com-docs-staging.ukp-stable.apw.unikraft.internal' + deployment-id: '${{ steps.deploy-github.outputs.deployment_id }}' + state: 'success' + + - name: Update deployment status (failure) + if: ${{ steps.www.outcome == 'failure' }} + uses: chrnorm/deployment-status@v2 + with: + token: '${{ github.token }}' + environment-url: 'https://com-docs-staging.ukp-stable.apw.unikraft.internal' + deployment-id: '${{ steps.deploy-github.outputs.deployment_id }}' + state: 'failure' + + - name: Fail job if deploy failed + if: ${{ steps.www.outcome == 'failure' }} + shell: bash + run: | + exit 1 diff --git a/.github/workflows/sync.yaml b/.github/workflows/sync.yaml index 9ab85345..81e99651 100644 --- a/.github/workflows/sync.yaml +++ b/.github/workflows/sync.yaml @@ -1,6 +1,8 @@ name: sync on: + schedule: + - cron: '17 3 */2 * *' workflow_dispatch: inputs: examples_branch: @@ -9,10 +11,10 @@ on: The branch of the examples repository to sync from. Default: main docs_branch: - default: main + default: prod-staging description: | The base branch in docs repository to create PR against. - Default: main + Default: prod-staging # Automatically cancel in-progress actions on the same branch concurrency: @@ -21,6 +23,7 @@ concurrency: permissions: contents: read + pull-requests: read jobs: sync: @@ -28,7 +31,7 @@ jobs: steps: - uses: actions/checkout@v4 with: - ref: ${{ inputs.docs_branch || 'main' }} + ref: ${{ inputs.docs_branch || 'prod-staging' }} - name: Sync example READMEs shell: bash @@ -48,24 +51,34 @@ jobs: BRANCH="sync/examples-$(date +'%Y%m%d%H%M%S')" echo "name=${BRANCH}" >>"${GITHUB_OUTPUT}" + - name: Generate timestamp + id: timestamp + run: | + TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC") + echo "value=${TIMESTAMP}" >>"${GITHUB_OUTPUT}" + - name: Create pull request uses: peter-evans/create-pull-request@v6 with: token: ${{ secrets.GH_PAT }} - base: ${{ inputs.docs_branch || 'main' }} - commit-message: "sync: Update guides from examples repository" - title: "sync: Update guides from examples repository" + base: ${{ inputs.docs_branch || 'prod-staging' }} + commit-message: "chore: Update guides from examples repository" + title: "chore: Update guides from examples repository" body: | - Automated sync of example READMEs from [`unikraft/examples@${{ inputs.examples_branch }}`](https://github.com/unikraft/examples/tree/${{ inputs.examples_branch }}). + IMPORTANT: Please make sure to review the deployment of this PR and also update `pages/guides/overview.mdx` if necessary. + + Automated sync of example READMEs from [`unikraft-cloud/examples@${{ inputs.examples_branch || 'main' }}`](https://github.com/unikraft-cloud/examples/tree/${{ inputs.examples_branch || 'main' }}). This PR updates the guides in `pages/guides/` with the latest content from the examples repository. - **Branch:** `${{ inputs.examples_branch }}` - **Timestamp:** $(date -u +"%Y-%m-%d %H:%M:%S UTC") + **Branch:** `${{ inputs.examples_branch || 'main' }}` + **Timestamp:** ${{ steps.timestamp.outputs.value }} branch: ${{ steps.id.outputs.name }} committer: Unikraft Bot author: Unikraft Bot - add-paths: pages/guides + add-paths: | + pages/guides + zudoku.config.tsx signoff: true labels: | automated diff --git a/.gitignore b/.gitignore index 119089b5..df398fbd 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,44 @@ .vale/styles/* !.vale/styles/Unikraft/* !.vale/styles/config/vocabularies/Unikraft/accept.txt + +# Node/Zudoku +/node_modules +/.pnp +.pnp.* +.yarn/* +!.yarn/patches +!.yarn/plugins +!.yarn/releases +!.yarn/versions + +# testing +/coverage + +# zudoku +/dist/ +/.zudoku/ + +# misc +.DS_Store +*.pem + +# debug +npm-debug.log* +yarn-debug.log* +yarn-error.log* +pnpm-debug.log* + +# env files (can opt-in for commiting if needed) +.env* + +# typescript +*.tsbuildinfo + +# build outputs +dist/ + +# generated files +apis/platform.yaml +build.log +.examples/ diff --git a/.vale.ini b/.vale.ini index c60d6e2b..086e2968 100644 --- a/.vale.ini +++ b/.vale.ini @@ -21,7 +21,7 @@ TokenIgnores = (?sm)((?:import|export) .+?$), \ (`[^`]*`), \ (```[\s\S]*?```) -BlockIgnores = (?sm)```[\s\S]*?```$ +BlockIgnores = (?sm)```[\s\S]*?```$, (?s)\{/\*\s*vale off\s*\*/\}.*?\{/\*\s*vale on\s*\*/\} CommentDelimiters = {/*, */} diff --git a/.vale/styles/config/vocabularies/Unikraft/accept.txt b/.vale/styles/config/vocabularies/Unikraft/accept.txt index cdd951f3..2a4ed8df 100644 --- a/.vale/styles/config/vocabularies/Unikraft/accept.txt +++ b/.vale/styles/config/vocabularies/Unikraft/accept.txt @@ -6,12 +6,15 @@ (?i)autoscale (?i)autoscaling (?i)FQDN +FUSE (?i)just-in-time (?i)kraftkit (?i)maximum (?i)minimum (?i)namespaced? (?i)performant +(?i)PIE +(?i)ROM(s)? (?i)rootfs (?i)tokenizer (?i)unikraft @@ -21,12 +24,14 @@ (?i)webservers? ALIAS ANAME +ASCII CNAME COPY DCO EOF FROM HIPPA +HMR IATA ISO LLM @@ -49,6 +54,10 @@ Unikraft Cloud POSIX PV PVC +EROFS +CPIO +CDP +FTP # ============================================================================== # NAMES OF THINGS @@ -126,6 +135,7 @@ PVC (?i)GraphQL (?i)gui (?i)haproxy +(?i)HDD(s)? (?i)Heroku (?i)Hightouch (?i)href @@ -223,8 +233,10 @@ PVC (?i)Singleline (?i)Spring (?i)sql +(?i)SSD(s)? (?i)ssh (?i)ssl +(?i)SSR (?i)stderr (?i)stdin (?i)stdout @@ -269,6 +281,7 @@ PVC (?i)vite (?i)VS Code (?i)vscode +(?i)vsftpd (?i)Vue (?i)Vuex (?i)wagi diff --git a/Caddyfile b/Caddyfile new file mode 100644 index 00000000..b8b8fb10 --- /dev/null +++ b/Caddyfile @@ -0,0 +1,18 @@ +:2015 + +header { + Server "UnikraftCloud" +} + +redir / /docs + +handle /docs* { + root * /var/www + encode gzip + file_server + try_files {path}/index.html {path}.html {path} +} + +handle { + respond 404 +} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 00000000..0af48275 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,73 @@ +# syntax=docker/dockerfile:1.7 + +################################################################################ +# Build kraft docs +################################################################################ +FROM golang:1.26 AS build-kraft-docs + +WORKDIR /kraftkit +ADD https://github.com/unikraft/kraftkit.git /kraftkit + +RUN make docs + +################################################################################ +# Build unikraft docs +################################################################################ +FROM golang:1.26 AS build-cli-docs + +WORKDIR /cli +ADD https://github.com/unikraft/cli.git /cli + +RUN make docs + +################################################################################ +# Dependencies + build +################################################################################ +FROM node:24-alpine AS build +WORKDIR /docs + +ENV CI=true +ENV PNPM_HOME="/pnpm" + +RUN set -xe; \ + apk --no-cache add \ + ca-certificates \ + git \ + wget \ + ; + +RUN set -xe; \ + corepack enable; \ + corepack prepare pnpm@latest --activate + +COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./ + +RUN --mount=type=cache,id=pnpm,target=/pnpm/store \ + pnpm install --prefer-frozen-lockfile; \ + pnpm install --prod --prefer-frozen-lockfile + +COPY . . + +# Grab the latest OpenAPI spec based on the desired channel. +ARG DOCS_CHANNEL=prod-staging +ADD https://raw.githubusercontent.com/unikraft-cloud/openapi/refs/heads/${DOCS_CHANNEL}/platform.yaml apis/platform.yaml + +# Kraftfile v0.7 schema docs (from unikraft-cloud/x) +ADD https://raw.githubusercontent.com/unikraft-cloud/x/refs/heads/${DOCS_CHANNEL}/kraftfile/schema.md pages/kraftfile/v0.7.md + +# Kraft (old CLI) docs -> /cli/kraft/ +COPY --from=build-kraft-docs /kraftkit/docs/kraft/cloud /docs/pages/cli/kraft +COPY --from=build-kraft-docs /kraftkit/docs/kraft/cloud.mdx /docs/pages/cli/kraft/overview.mdx + +# Unikraft (new CLI) docs -> /cli/unikraft/ +COPY --from=build-cli-docs /cli/dist/docs/mdx/unikraft/ /docs/pages/cli/unikraft/ +COPY --from=build-cli-docs /cli/dist/docs/mdx/unikraft.mdx /docs/pages/cli/unikraft.mdx + +RUN pnpm run build + +################################################################################ +# Production filesystem +################################################################################ +FROM caddy:2.10.2-alpine AS prod +COPY Caddyfile /etc/caddy/Caddyfile +COPY --from=build /docs/dist/docs /var/www/docs diff --git a/Kraftfile b/Kraftfile new file mode 100644 index 00000000..6fd52cc6 --- /dev/null +++ b/Kraftfile @@ -0,0 +1,7 @@ +spec: v0.6 + +runtime: base-compat:latest + +rootfs: ./Dockerfile + +cmd: ["/usr/bin/caddy", "run", "--config", "/etc/caddy/Caddyfile"] diff --git a/Makefile b/Makefile index e8012171..2c16a4c2 100644 --- a/Makefile +++ b/Makefile @@ -45,14 +45,32 @@ sync: rm -rf "$$EXAMPLES_LOCAL"; \ git clone --depth 1 --branch $(EXAMPLES_BRANCH) https://github.com/$(EXAMPLES_REPO) "$$EXAMPLES_LOCAL"; \ fi; \ + updated_guides=""; \ for example in $$(find "$$EXAMPLES_LOCAL" -mindepth 1 -maxdepth 1 -type d -not -name '.*'); do \ name=$$(basename "$$example"); \ readme="$$example/README.md"; \ if [ -f "$$readme" ]; then \ - guide="$(GUIDES_DIR)/$$name.mdx"; \ - echo " 📄 $$name -> $$(basename $$guide)"; \ - $(WORKDIR)/scripts/transform-readme.sh "$$readme" "$$guide" "$$name"; \ + # TODO: remove - Only transform updated READMEs \ + if grep -q "# Set metro to Frankfurt, DE" "$$readme" ; then \ + guide="$(GUIDES_DIR)/$$name.mdx" ;\ + echo " 📄 $$name -> $$(basename $$guide)" ;\ + $(WORKDIR)/scripts/transform_readme.py "$$readme" "$$guide" "$$name" ;\ + updated_guides="$$updated_guides $$name.mdx" ;\ + else \ + echo "⏭ Skipping $$name (metro line not present)" ;\ + fi ;\ fi; \ - done + done; \ + for guide in $$(find "$(GUIDES_DIR)" -maxdepth 1 -name '*.mdx' -type f); do \ + gname=$$(basename "$$guide"); \ + if [ "$$gname" = "overview.mdx" ]; then continue; fi; \ + if ! echo "$$updated_guides" | grep -qw "$$gname"; then \ + echo " 🗑 Removing stale guide: $$gname"; \ + rm -f "$$guide"; \ + fi; \ + done; \ + $(WORKDIR)/scripts/update_zudoku_guides.py "$(GUIDES_DIR)" "$(WORKDIR)/zudoku.config.tsx" .PHONY: sync-list +sync-list: + $(WORKDIR)/scripts/update_zudoku_guides.py "$(GUIDES_DIR)" "$(WORKDIR)/zudoku.config.tsx" diff --git a/README.md b/README.md index 8091a6f1..a787c0f0 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,6 @@ This repository contains the markdown contents of the documentation site at http ## Contributing to the docs Contributions to the documentation are welcome! -If you're looking to file an issue or bug report for anything other than these documentations, please [visit the roadmap](https://roadmap.unikraft.com/). ### Table of contents diff --git a/apis/.gitkeep b/apis/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/docker-compose.yaml b/docker-compose.yaml new file mode 100644 index 00000000..3a62a2b7 --- /dev/null +++ b/docker-compose.yaml @@ -0,0 +1,7 @@ +services: + docs: + build: + context: . + target: prod + ports: + - "2015:2015" diff --git a/package.json b/package.json new file mode 100644 index 00000000..adf84137 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "docs", + "version": "0.1.0", + "type": "module", + "private": true, + "scripts": { + "dev": "zudoku dev --port 3131", + "build": "zudoku build", + "preview": "zudoku preview", + "lint": "eslint" + }, + "dependencies": { + "@shikijs/vscode-textmate": "^10.0.2", + "hast-util-to-html": "^9.0.5", + "oniguruma-parser": "^0.12.1", + "react": ">=19.0.0", + "react-dom": ">=19.0.0", + "regex-utilities": "^2.3.0", + "zudoku": "^0.77.0" + }, + "devDependencies": { + "@typescript-eslint/eslint-plugin": "^8.0.0", + "@typescript-eslint/parser": "^8.0.0", + "eslint": "^9.14.0" + } +} diff --git a/pages/cli/filters.mdx b/pages/cli/filters.mdx new file mode 100644 index 00000000..720860de --- /dev/null +++ b/pages/cli/filters.mdx @@ -0,0 +1,163 @@ +--- +title: Filters +description: Filter expression language for the --filter and --until flags +navigation_icon: filter +--- + +The `unikraft` CLI's `--filter` and `--until` flags accept filter expressions that match resources based on their field values. +These flags are available on `list`, `wait`, and `delete` subcommands throughout the `unikraft` CLI. +They aren't available in the legacy `kraft cloud` CLI. + +The syntax extends the [containerd filter grammar](https://github.com/containerd/containerd/blob/main/pkg/filters/filter.go) with negated regex matching, wildcards, and alternative quoting. + +## Grammar + +``` +expression := selector ( "," selector )* +selector := fieldpath [ operator value ] + | fieldpath? "*" ( "." selector | operator value )? +fieldpath := field ( "." field )* +field := quoted-string | identifier +identifier := [A-Za-z] [A-Za-z0-9_]+ +operator := "=" | "==" | "!=" | "!==" | "~=" | "!~=" +value := quoted-string | regex-string | unquoted-string +``` + +A **quoted-string** uses Go string syntax (double quotes with standard +backslash escapes: `\n`, `\t`, `\\`, `\"`, `\xNN`, `\uNNNN`, `\UNNNNNNNN`, +octal `\NNN`). + +A **regex-string** may only appear after `~=` or `!~=`. +You can delimit it with `/` or `|` instead of `"`, which is convenient when the pattern contains double-quotes or backslashes (for example, `name~=/[abc]{0,2}/` or `path~=|foo/bar|`). + +An **unquoted-string** is any run of non-whitespace, non-comma characters. + +## Operators + +| Operator | Alias | Description | Example | +| -------- | ----- | --------------------------------- | ----------------- | +| `==` | `=` | Equals | `state==running` | +| `!=` | `!==` | Not equals | `state!=stopped` | +| `~=` | | Matches regular expression | `name~="^web-.*"` | +| `!~=` | | Doesn't match regular expression | `name!~="^test-"` | +| _(none)_ | | Field is present and non-empty | `description` | + +Regular expressions use Go `regexp` syntax. + +## Field paths + +Fields use their output name (the name shown in `kv` or `table` output). +Nested fields use dot-separated paths. + +``` +name # top-level field +settings.bar # nested field +timestamps.created-at # hyphenated field name +labels."my complex key" # quoted path segment +``` + +### Wildcards + +A `*` in a field path iterates over all entries of a map or array field. +The wildcard can appear at any level, and you can chain wildcards. + +``` +labels.* # true if any label entry exists +labels.*==value # true if any label entry equals "value" +authors.*.email==a@b.com # true if any author's email matches +nested.*.*==true # double wildcard +``` + +**Negation semantics with wildcards:** positive operators (`==`, `~=`) match if +**any** entry meets the condition. +Negated operators (`!=`, `!~=`) match only if **all** entries meet the condition (that is, no entry matches the negated pattern). + +### Indexed access + +You can also access array elements by zero-based numeric index. + +``` +authors.0.name==Alice # first author's name +authors.1.email==b@b.com # second author's email +``` + +## Combining expressions + +### And (comma-separated within one flag) + +Comma-separated selectors within a single `--filter` value must **all** match for the filter to include a resource. + +```bash +--filter 'state==running,name~="^web"' +``` + +### Or (repeated flags) + +When you specify `--filter` several times, the filter includes a resource if it matches **any** of the expressions. + +```bash +--filter 'state==running' --filter 'state==stopped' +``` + +### Combining both operators + +The two mechanisms compose: + +```bash +--filter 'state==running,metro==fra0' --filter 'state==stopped,metro==was1' +``` + +This matches resources that are (running in fra0) **or** (stopped in was1). + +## The `--until` flag + +The `--until` flag uses the same expression syntax as `--filter`. +It's available on `wait` subcommands (and aliases to `--filter` there). +The command polls until every targeted resource matches the expression. + +```bash +unikraft instance wait my-instance --until state==running +``` + +## Special fields + +The `metro` field receives special handling: when present in a filter, it restricts which metros the CLI queries rather than filtering results after the fact. +This means `--filter metro==fra0` avoids fetching data from other metros entirely. + +## Examples + +```bash +# List only running instances +unikraft instance list --filter state==running + +# List instances NOT in the "stopped" state +unikraft instance list --filter 'state!=stopped' + +# List images whose ref contains "nginx" +unikraft image list --filter 'ref~="/nginx"' + +# Exclude images matching a pattern +unikraft image list --filter 'ref!~="test"' + +# Regex with slash delimiters (avoids escaping issues) +unikraft image list --filter 'ref~=/nginx[0-9]{1,3}/' + +# Wait for an instance to reach the running state +unikraft instance wait my-instance --until state==running + +# Delete all stopped instances (with confirmation) +unikraft instance remove --filter state==stopped + +# Combine filters: running instances in fra0, OR stopped in was1 +unikraft instance list --filter 'state==running,metro==fra0' \ + --filter 'state==stopped,metro==was1' + +# Filter on nested fields +unikraft instance list --filter 'settings.autoscale==true' + +# Filter using array wildcard +unikraft instance list --filter 'volumes.*.name==data' + +# Check that no label matches a pattern (all must satisfy !=) +unikraft instance list --filter 'labels.*!=temporary' +``` diff --git a/pages/cli/legacy-overview.mdx b/pages/cli/legacy-overview.mdx new file mode 100644 index 00000000..dd6f1be5 --- /dev/null +++ b/pages/cli/legacy-overview.mdx @@ -0,0 +1,96 @@ +--- +title: Kraft cloud CLI (legacy) +description: Legacy Kraft cloud CLI installation and quick start +--- + +The **kraft cloud** CLI is part of the [KraftKit](https://github.com/unikraft/kraftkit) toolchain. +While it remains fully supported with ongoing bug fixes, it won't receive any new functionality. +New users should use the unikraft CLI instead. + +## Installation + +For Windows, KraftKit currently requires Windows Subsystem for Linux 2 (WSL2). +Please ensure you set up WSL2 on your host (for example: run `wsl --install -d ubuntu` in PowerShell). + + + +```bash title="1-liner (macOS & Linux)" +curl --proto '=https' --tlsv1.2 -sSf https://get.kraftkit.sh | sh +``` + +```bash title="macOS" +brew install unikraft/tap/kraftkit +``` + +```bash title="Debian/Ubuntu" +# Update and install dependencies +sudo apt-get update +sudo apt-get install \ + ca-certificates \ + curl \ + gnupg \ + lsb-release + +# Add Unikraft's official GPG key: +sudo mkdir -p /etc/apt/keyrings +curl -fsSL https://deb.pkg.kraftkit.sh/gpg.key | \ + sudo gpg --dearmor -o /etc/apt/keyrings/unikraft.gpg + +# Use the following command to set up the APT repository: +echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/unikraft.gpg] https://deb.pkg.kraftkit.sh /" | \ + sudo tee /etc/apt/sources.list.d/unikraft.list > /dev/null + +# Update the APT package index, and install the latest version of kraftkit: +sudo apt-get update +sudo apt-get install kraftkit +``` + +```bash title="Fedora/RHEL/Rocky/Alma" +# Add the Kraftkit repository +sudo tee /etc/yum.repos.d/kraftkit.repo < + +## Quick start + +```sh +# Deploy an instance +kraft cloud deploy -p 443:8080/http+tls -M 512 . + +# List instances +kraft cloud instance list +``` + +## Choosing a CLI + +| Feature | unikraft | kraft cloud | +|---------|----------|-------------| +| Recommended for new users | Yes | No | +| Profile management | Yes | Limited | +| Scale-to-zero support | Yes | Yes | +| Compose support | No | Yes | + +If you are getting started with Unikraft Cloud, use the **unikraft** CLI. +If you need Docker Compose-style workflows use the **kraft cloud** CLI. + +[Back to CLI overview](/docs/cli/overview) diff --git a/pages/cli/overview.mdx b/pages/cli/overview.mdx new file mode 100644 index 00000000..a84cfd4a --- /dev/null +++ b/pages/cli/overview.mdx @@ -0,0 +1,91 @@ +--- +title: Overview +description: Command-line interfaces for Unikraft Cloud +navigation_icon: book-open +--- + +The **unikraft** CLI is the new, official command-line interface for [Unikraft Cloud](https://unikraft.cloud). +It provides a modern, streamlined experience for deploying and managing unikernels globally. + +## Features + +- **Deploy Instantly** - Run unikernel images as serverless instances with `unikraft run` +- **Global Infrastructure** - Deploy across supported metros with automatic multi-region support +- **Resource Management** - Create, read, update, and delete instances, volumes, services, certificates, and images +- **Scale-to-Zero** - Built-in support for scale-to-zero policies to optimize costs +- **Output Formats** - Machine-readable JSON or human-friendly text output +- **Profile Management** - Switch between accounts and configurations +- **Shell Completions** - Tab completion for Bash, Zsh, Fish, and PowerShell + +## Installation + + + +```bash title="1-liner (macOS & Linux)" +curl --proto '=https' --tlsv1.2 -fsSL https://unikraft.com/cli/install.sh | sh +``` + +```bash title="macOS" +brew install unikraft/cli/unikraft +``` + +```bash title="Debian/Ubuntu" +# Update and install dependencies +sudo apt update +sudo apt install ca-certificates curl + +# Download and add the GPG key +sudo install -d -m 0755 /etc/apt/keyrings + +sudo curl -fsSL \ + -o /etc/apt/keyrings/unikraft-cli.gpg \ + https://pkg.unikraft.com/debian/cli-apt/keys/cli-apt.gpg + +sudo tee /etc/apt/sources.list.d/unikraft-cli.sources < + +See [alternative installation instructions](https://github.com/unikraft/cli/?tab=readme-ov-file#installation) for other platforms. + +## Quick start + +```sh +# Login to Unikraft Cloud +unikraft login + +# Deploy an instance +unikraft run --metro=fra -p 443:8080/http+tls --image=nginx:latest + +# List instances +unikraft instances list +``` + +[View unikraft CLI documentation](/docs/cli/unikraft) + +Looking for the legacy `kraft cloud` CLI? +See the [Kraft cloud CLI (legacy)](/docs/cli/legacy-overview) page for installation instructions. diff --git a/pages/cli/registries.mdx b/pages/cli/registries.mdx new file mode 100644 index 00000000..d18df6fc --- /dev/null +++ b/pages/cli/registries.mdx @@ -0,0 +1,227 @@ +--- +title: Registries +navigation_icon: package +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +Unikraft Cloud uses a central OCI registry at `index.unikraft.io` to store images. +Every image you build and push lands in this registry under your account's namespace. +When you start an instance, the platform pulls the image from this registry automatically. + +Each node has a local registry also running for directly pushing to a specific node. +These registries have the address formatted as `index..unikraft.cloud`. +The main advantage of pushing to a node's registry is that it avoids the round trip to the central registry. +This speeds up deployments and reduces bandwidth usage. + +Directly pushing to a node's registry or using the central registry is interchangeable. +The platform handles synchronization between them. + +## Image naming + +The platform addresses images using the standard OCI reference format: + +``` +index.unikraft.io//: +index..unikraft.cloud//: +``` + +For example: + +``` +index.unikraft.io/alice/http-python312:latest +index.unikraft.io/alice/http-python312@sha256:278cb8b1... + +index.node1.unikraft.cloud/alice/http-python312:latest +index.node1.unikraft.cloud/alice/http-python312@sha256:278cb8b1... +``` + +The `` segment is your Unikraft Cloud organization, available as the `UKC_USER` environment variable in most guides. +Tags are arbitrary strings. +The platform defaults to `latest` when you don't specify a tag. +You can also refer to images by digest to pin to an exact version. + +## Authentication + +Registry authentication uses the same API token as the rest of the platform. +The new CLI uses profiles, while the legacy CLI reads `UKC_TOKEN` directly. + + + + ```bash title="unikraft" + unikraft login + ``` + + ```bash title="kraft" + export UKC_TOKEN= + ``` + + + +The `UKC_TOKEN` and `UKC_METRO` environment variables are only used by the legacy CLI. + +You will also need to use the legacy CLI login command for `kraft pkg` operations: + + + + ```bash title="central registry" + kraft login -u "$UKC_USER" -t "$UKC_TOKEN" index.unikraft.io + ``` + + ```bash title="local registry" + kraft login -u "$UKC_USER" -t "$UKC_TOKEN" index..unikraft.cloud + ``` + + + +## Pushing images + +### Via the CLI (recommended) + +The simplest way to push an image is to use the CLI to build, package, push, and start an instance in one flow: + + + ```bash title="central registry" + unikraft build . --output index.unikraft.io//:latest + unikraft run --metro=fra -p 443:8080/http+tls -m 256MiB --name --image=index.unikraft.io//:latest + ``` + ```bash title="local registry" + unikraft build . --output index..unikraft.cloud//:latest + unikraft run --metro=fra -p 443:8080/http+tls -m 256MiB --name --image=index..unikraft.cloud//:latest + ``` + + +During deployment you will see output confirming the push: + + + + ```ansi title="central registry" + [+] packaging index.unikraft.io/alice/my-app:latest... done! + [+] pushing index.unikraft.io/alice/my-app:latest (kraftcloud/x86_64)... done! + ``` + + ```ansi title="local registry" + [+] packaging index..unikraft.cloud/alice/my-app:latest... done! + [+] pushing index..unikraft.cloud/alice/my-app:latest (kraftcloud/x86_64)... done! + ``` + + +### Via `kraft pkg --push` + +For more control, for example when packaging [ROMs](/features/roms) or base images, use the legacy CLI with the `--push` flag: + + + + ```bash title="central registry" + kraft pkg \ + --plat kraftcloud \ + --arch x86_64 \ + --name index.unikraft.io/"$UKC_USER"/my-app:latest \ + --push \ + . + ``` + + ```bash title="local registry" + kraft pkg \ + --plat kraftcloud \ + --arch x86_64 \ + --name index..unikraft.cloud/"$UKC_USER"/my-app:latest \ + --push \ + . + ``` + + + +## Listing images + + + + ```bash title="unikraft" + unikraft images list + ``` + + ```bash title="kraft" + kraft cloud image ls + ``` + + + + + + ```text title="central registry" + NAME VERSION SIZE + alice/http-python312 latest 77 MB + alice/my-app latest 42 MB + ``` + + ```text title="local registry" + NAME VERSION SIZE + alice/http-python312 latest 77 MB + index..unikraft.cloud/alice/my-app latest 42 MB + ``` + + + +:::note +There may be a delay of a few minutes between pushing or removing an image and the image list reflecting the change. +::: + +## Removing images + + + + ```bash title="unikraft" + # Not supported yet for direct pushing to node registries. + # Remove images from the node itself periodically. + ``` + + ```bash title="kraft" + kraft cloud img rm my-app + ``` + + + +## Kernel layer deduplication + +An image has two OCI layers: a kernel layer and a rootfs layer. +When you push an update to an existing image, `kraft` checks whether the kernel layer is already present in the registry and skips re-uploading it if so. +Only the rootfs layer travels over the wire. +This makes iterative pushes faster and cheaper on bandwidth. + +[ROMs](/features/roms) follow the same packaging system but go out without a kernel layer. +They only have a rootfs. +They appear in the image list alongside regular images. + +## Image addresses in instance creation + +When creating an instance directly from an existing image, reference it by tag or digest: + + + + ```bash title="central registry" + # By tag + unikraft run --metro=fra -p 443:8080/http+tls -m 256MiB \ + --image=index.unikraft.io/alice/my-app:latest + + # By digest (exact version pinning) + unikraft run --metro=fra -p 443:8080/http+tls -m 256MiB \ + --image=index.unikraft.io/alice/my-app@sha256:278cb8b1... + ``` + + ```bash title="local registry" + # By tag + unikraft run --metro=fra -p 443:8080/http+tls -m 256MiB \ + --image=index..unikraft.cloud/alice/my-app:latest + + # By digest (exact version pinning) + unikraft run --metro=fra -p 443:8080/http+tls -m 256MiB \ + --image=index..unikraft.cloud/alice/my-app@sha256:278cb8b1... + ``` + + + +## Learn more + +* [Images](/platform/images): build, package, and deploy workflow. +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1). diff --git a/pages/faq.mdx b/pages/faq.mdx index d5926677..cb18b734 100644 --- a/pages/faq.mdx +++ b/pages/faq.mdx @@ -3,32 +3,33 @@ title: FAQ navigation_icon: circle-question-mark --- -#### What's Unikraft? +{/* vale off */} +#### What is Unikraft? +{/* vale on */} -Unikraft is the company behind Unikraft Cloud, a next-generation compute platform that leverages unikernel technology. +Unikraft is the company behind Unikraft Cloud, a next-generation compute platform that leverages microVM technology. It delivers millisecond cold boots and scale-to-zero autoscaling so you never pay for idle resources. -#### Can you do cloud-prem, or on-prem deployments? +{/* vale off */} +#### What is a microVM? -Yes. -Please [get in touch](https://unikraft.com/contact) to discuss dedicated hosting. +Definitionally, a microVM is any virtual machine launched with Firecracker, an open source Virtual Machine Monitor (VMM) from Amazon, originally done for AWS Lambda. +{/* vale on */} +The VM itself can still be chunky: as long as Firecracker launched it, it's considered a microVM. +And any microVM *is* a VM, so it has the same strong, hardware-level isolation as any VM does. -#### What's a unikernel? +#### Can you do cloud-prem, or on-prem deployments? -A unikernel is a specialized virtual machine. -Each target app gets a distro and kernel containing only the code it needs. -Everything builds at compile time, before deployment. -If an app doesn't need a line of code to run, it doesn't get deployed. -This packages as any other virtual machine, with strong, hardware-level isolation. -Unikraft packages unikernels as OCI images. +Yes. +Please [get in touch](https://unikraft.com/contact) to discuss dedicated hosting. #### Aren't virtual machines heavyweight? They need not be! -Unikraft unikernels are proof you can have strong, hardware-level isolation. +Unikraft microVMs are proof you can have strong, hardware-level isolation. This isolation is the workhorse of public cloud deployments. It combines with the lightweight characteristics of containers or processes, such as millisecond cold boot times. @@ -44,10 +45,10 @@ For example, an NGINX Unikraft image is under 2MBs in size. When you deploy a container on the public cloud, it gets deployed on top of a virtual machine to have strong, hardware-level isolation. The container thus adds yet another layer of overhead between your app and the hardware. At Unikraft, `Dockerfile`s create the filesystem at build time. -Then, a lean unikernel provides the best efficiency at deploy time. +Then, a lean microVM provides the best efficiency at deploy time. -#### Do Unikraft unikernels come with security benefits? +#### Do Unikraft microVMs come with security benefits? Yes, especially stemming from the fact that they have a minimal Trusted Computing Base (TCB), and everything is off by default (services, ports, etc). @@ -57,33 +58,28 @@ Yes, especially stemming from the fact that they have a minimal Trusted Computin Definitely. Every instance on Unikraft Cloud has a private IP and DNS name. You can plug instances together. -Follow this [guide](/docs/guides/features/idns/) for instructions. +Follow this [guide](/platform/services) for instructions. #### With an access token and an app, what should you do? You'll need a Dockerfile and Kraftfile. -See any of the apps/langs guides (here)[/docs/guides/] to see examples. +See any of the apps/langs guides [here](/guides/bun) to see examples. -#### What's `kraft`? +{/* vale off */} +#### What is `kraft`? +{/* vale on */} -`kraft` is Unikraft Cloud's [open source CLI tool written in Go](https://github.com/unikraft/kraftkit). +`kraft` is Unikraft Cloud's legacy [open source CLI tool written in Go](https://github.com/unikraft/kraftkit). You can control Unikraft Cloud with it as well as build unikernels and try things out locally. - - -#### How does Unikraft differ from Linux, and what about debugging? - -Unikraft implements the Linux API so apps and languages run unmodified. -A common concern is that unikernels are hard to debug. -Unikraft includes a full GDB server, tracing, and other facilities, so debugging feels like Linux (see [here](https://unikraft.org/docs/internals/debugging)). -For observability, Unikraft ships a Prometheus library so a unikernel can export metrics to Grafana dashboards. +If you want to interact with the latest features of Unikraft Cloud, you should use the new [open source `unikraft` CLI tool](https://github.com/unikraft/cli), written in Go as well. #### How's millisecond scale-to-zero achieved? Magic 🪄 -Kidding, it's the combination of using Unikraft unikernels to run workloads and a custom node ingress controller. +Kidding, it's the combination of using Unikraft microVMs to run workloads and a custom node ingress controller. This controller can be reactive in milliseconds and scale to thousands of instances. Other optimizations to the underlying host also help. When traffic dies, your app will stop, consume no resources, and get charged $0. @@ -115,7 +111,7 @@ Stateful scale-to-zero in milliseconds anyone? #### What do you need to get started? [Sign up](https://console.unikraft.cloud/signup) to get an access token. -If you're interested in an enterprise solution please [write to us](/contact). +If you're interested in an enterprise solution please [write to us](https://unikraft.com/contact). #### What about usability? @@ -145,7 +141,7 @@ Unikraft can provide cost savings in many ways: #### What's the relationship between the Unikraft open source project and Unikraft Cloud? Unikraft OSS allows you to build and run unikernels locally via `kraft run` (and even hack the Unikraft OS itself if you like tinkering!). -When you're ready to deploy, switch to `kraft cloud deploy`. +When you're ready to deploy to cloud, use the `unikraft` CLI (or the legacy `kraft cloud` CLI). #### Are you a replacement for Docker? @@ -153,14 +149,14 @@ When you're ready to deploy, switch to `kraft cloud deploy`. Definitely not! Docker is great for dev environments and for building images. In fact, Unikraft relies on `Dockerfile` to specify the filesystem of images on Unikraft Cloud. -Having said that, when it's time to deploy, Unikraft places the resulting filesystem on a lean unikernel. +Having said that, when it's time to deploy, Unikraft places the resulting filesystem on a lean microVM. It's ready to run with hardware-level isolation and extreme efficiency. #### Are you a competing technology to WASM? No. -You can read more about [how Unikraft and WASM complement each other in our dedicated blog post on the matter](/blog/unikernels-and-wasm/). +You can read more about [how Unikraft and WASM complement each other in our dedicated blog post on the matter](https://unikraft.com/blog/unikernels-and-wasm/). In short, WASM provides language-level isolation, whereas Unikraft provides hardware-level. When you deploy WASM to the public cloud, there will be a VM underneath for isolation. Maybe even a container runtime. @@ -171,7 +167,7 @@ The VM (the unikernel) itself has only the minimal code needed to run the WASM r #### Can you deploy within hyperscaler infra and connect to their services with Unikraft Cloud? Yes. -Unikraft supports [metros](/docs/metros) / regions within existing hyperscaler infra, and also connectivity to their services. +Unikraft supports [metros](/platform/metros) (regions) within existing hyperscaler infra, and also connectivity to their services. For example, you could run an API server on Unikraft Cloud and connect to S3 as a storage back end. Contact information is available for more details. @@ -179,13 +175,13 @@ Contact information is available for more details. #### Can you try Unikraft open source first? Yes. -Head on over to [unikraft.org](http://www.unikraft.org/), install the `kraft` tool with the one-liner there, and use `kraft run` to build, package and run your app locally. -When you're ready to deploy to cloud, set your Unikraft Cloud token and deploy via `kraft cloud deploy`. +Head on over to [unikraft.org](https://unikraft.org/docs/cli/install) to install the legacy `kraft` tool, and use `kraft run` to build, package and run your app locally. +When you're ready to deploy to cloud, use the [`unikraft`](https://github.com/unikraft/cli) CLI or the legacy `kraft cloud` CLI. #### Does Unikraft Cloud work on Mac/Windows/Linux? -Yes, the kraft tool can run on Mac, Windows or Linux. +Yes, the CLI can run on Mac, Windows or Linux. #### How can you track instances on Unikraft Cloud? @@ -194,16 +190,119 @@ Unikraft provides full log support for all users. Paying customers get access to Prometheus metrics. -#### Are there other unikernel cloud platforms? +{/* vale off */} +#### What is a unikernel? +{/* vale on */} + +A unikernel is a specialized virtual machine. +Each target app gets a distro and kernel containing only the code it needs. +Everything builds at compile time, before deployment. +If an app doesn't need a line of code to run, it doesn't get deployed. +This packages as any other virtual machine, with strong, hardware-level isolation. +Unikraft packages unikernels as OCI images. + + +#### Does Unikraft Cloud only support unikernels? + +No, Unikraft Cloud also supports minimalistic, Linux-based microVMs. + + +#### Are there other platforms supporting unikernels? Other unikernel projects exist, but most are research efforts, unmaintained, or limited to a single app or language and aren't POSIX (Portable Operating System Interface) compliant. #### Do you have a Kubernetes integration? -[Yes!](/docs/integrations/kubernetes/) +[Yes!](/integrations/kubernetes/) #### Do you have a Terraform integration? -[Yes!](/docs/integrations/terraform/) +[Yes!](https://github.com/unikraft-cloud/terraform-provider-ukc) + + +#### When should you use the CLI stable and/or staging? + +You should use stable versions when initially trying out the Unikraft Cloud Platform and its features. +Any time you encounter a problem, it's good to first try out the latest staging version before forwarding the problem as it might have been already fixed. +There's also a chance that staging contains changes available only on non-stable nodes. + + +#### What keywords does the CLI ignore from a Dockerfile? + +This is the current list of ignored keywords: `EXPOSE`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, `VOLUME`. +The `USER` keyword is relevant for running subsequent `RUN` instructions in the Dockerfile, but not for the `ENTRYPOINT` and `CMD` commands. +At runtime, Unikraft Cloud instances run as `root` user, unless explicitly switching to another user. + + +#### How does the image size and memory relate to boot times? + +When using `CPIO`, there is a [strong correlation](/tutorials/rootfs-formats) between the image size and the boot time. +This is because it needs to load the whole image into memory first. + +When using `EROFS`, there is no such correlation, as the operating system loads pages directly from disk, on demand. + +At the same time, there is a slight correlation between big memory allocations and higher boot times, though not significant. +This is irrespective of the file system used. + + +#### What's the key difference between cold boots and warm boots? + +End-result wise, cold and warm boots are no different, the instance still works. +The server handles the request in both cases if the app is stateless. +The main difference appears in [stateful](/features/snapshots) apps and when considering latency, hence the `--scale-to-zero-stateful` flag. + +A cold boot equates to starting the instance every time and then stopping it if no traffic hit it for a while. + +A warm boot resembles the suspend and resume operations in virtual machines. +The platform saves the system memory to disk and loads it when a request comes. + +Warm boots offer clear advantages over cold boots, having lower boot times, no setup time, and are better in general, with the trade off of using space on the disk. +Thus, for most use cases, using [stateful scale-to-zero](/features/scale-to-zero#stateful-scale-to-zero) is the better alternative. + +Cases exist where short lived VMs are small enough to have a small boot time, but they use a lot of memory. +In this case it might be better to use cold boots, to use disk efficiently. +This should be first experimented or discussed with a Unikraft Engineer to verify it. + + +#### What can you push as an image? + +An image is specifically a pair of a kernel and a rootfs bundled together as different layers of an OCI package. +To keep things light, certain optimizations are in place to not repush existing kernels and to reuse them from your account. +If these aren't automatically detected, the CLI pushes the kernel again. + +At the same time, things like ROMs use the same packaging systems, but push images with a missing kernel and only a rootfs. +These have specific metadata that identifies them as ROMs. + +Finally, kernels can also run without a rootfs. +They don't do anything, but support exists to run them. + + +#### How to tell if an app can run in the Unikraft kernel or TinyX? + +No exact suite can determine if your app can run on one or another. + +TinyX is a slimmed down Linux Kernel, which means that most cloud-compatible apps work out of the box as long as it has all files available. +Some virtual devices and virtual file system entries are missing, but you can contact a Unikraft staff member to look into adding them. + +If your app successfully runs on TinyX and Unikraft Cloud, you have the option to also try running it on the Unikraft kernel. +This kernel removes even more unused features so the chance that the app isn't compatible increases, but the performance and boot times are better. +For example, if your app uses extensive process cloning, it has a low chance to work on the Unikraft kernel, as it supports running a single process at a time. +Other examples of unsupported formats are 32bit executables and non-PIE (Position Independent Executable) executables, as the Unikraft kernel can't load them in memory at the moment. + +Other small differences exist, which you should report to the Unikraft Team for fixing, as the Unikraft kernel tries to follow Linux and the POSIX format. + + +#### How does the [rootfs format](/tutorials/rootfs-formats) and size equate to the memory used? + +When using the `CPIO` file system, the memory allocated to the instance needs to be at least twice as big as the image size. +This is because the system must load and unpack the `CPIO` rootfs format in memory before using it. +This is a bare minimum and to this you need to add also the memory your app needs to run. + +When using `EROFS`, there is no direct correlation between the image size and the memory needed. +Ideally, the memory should be at least the image size to make sure the system can load all files in memory before using. +The system loads files on demand and maps them directly from disk, ensuring lower memory usage and overhead. +If you have files that you know are never used, it's better to remove them from the image to slim it down. +Thus, in theory, `EROFS` uses only the allocated pages, but it's also highly dependent on the workload. +An instance with a 3GB image might use 128MB of RAM at one point, whilst an instance with a 300MB image might use 1GB of RAM at some point. diff --git a/pages/features/autokill.mdx b/pages/features/autokill.mdx new file mode 100644 index 00000000..e4fe5a9a --- /dev/null +++ b/pages/features/autokill.mdx @@ -0,0 +1,93 @@ +--- +title: Autokill +navigation_icon: timer-off +--- + +Autokill automatically terminates instances or service groups when a configured condition triggers. +It helps avoid unnecessary resource usage when instances or groups are no longer needed. + +You can configure autokill on: +- **Instances**: killed after a time, or after reaching a request limit +- **Instance templates**: killed if nobody clones them within a specified time +- **Service groups**: killed after the group has been empty for a specified time + +## Instance autokill + +### Kill after stopped duration + +Automatically kill an instance after it stops and remains stopped for a given number of milliseconds: + +```json title="POST /instances" +{ + "autokill": { + "time_ms": 3600000 + } +} +``` + +This example kills the instance 1 hour after it stops. + +### Kill after a request count + +Kill an instance after it serves a specified request count: + +```json title="POST /instances" +{ + "autokill": { + "num_requests": 1000 + } +} +``` + +### Updating autokill on a stopped instance + +```json title="PATCH /instances/{uuid}" +{ + "prop": "autokill", + "op": "set", + "value": { + "time_ms": 10000 + } +} +``` + +## Service group autokill + +Automatically kill a service group after it stays empty (no instances) for a given number of milliseconds: + +```json title="POST /services" +{ + "autokill": { + "time_ms": 300000 + } +} +``` + +This example kills the service group 5 minutes after its last instance leaves. + +## Instance template autokill + +Automatically kill an instance template if nobody clones it within a given number of milliseconds: + +```json title="POST /instances/templates" +{ + "autokill": { + "time_ms": 86400000 + } +} +``` + +This example removes the template after 24 hours without a clone. + +## Interaction with delete locks + +Autokill skips any instance that has a [delete lock](/platform/delete-locks) set. +The autokill timer doesn't fire while a delete lock is active. + +## Autokill on instance clone + +When you clone an instance from a template, the new instance inherits the autokill configuration. + +## Learn more + +* Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the sections on [instances](/api/platform/v1/instances) and [services](/api/platform/v1/service-groups) diff --git a/pages/features/autoscale.mdx b/pages/features/autoscale.mdx index 8ce3ae55..3effb5ed 100644 --- a/pages/features/autoscale.mdx +++ b/pages/features/autoscale.mdx @@ -3,8 +3,8 @@ title: Autoscale navigation_icon: square-stack --- -Autoscaling is [load balancing](/docs/features/load-balancing) where the *number of instances* used to handle your traffic is automatically adapted to match the current traffic load. -On Unikraft Cloud, scale out (the process of adding instances to cope with increased load) happens in milliseconds. +Autoscaling is [load balancing](/features/load-balancing) where the *number of instances* used to handle your traffic automatically adapts to match the current traffic load. +On Unikraft Cloud, scale-out (the process of adding instances to cope with increased load) happens in milliseconds. You can transparently and effortlessly handle load increase including traffic peaks. No more headaches due to slow autoscale like keeping hot instances around to deal with peaks, coming up with complex predictive algorithms, or other painful workarounds. You can set autoscale on and let Unikraft Cloud handle your traffic increases and peaks. @@ -12,7 +12,7 @@ You can set autoscale on and let Unikraft Cloud handle your traffic increases an ## The basics -As with [load balancing](/docs/features/load-balancing), autoscaling in Unikraft Cloud takes care of a *service*. +As with [load balancing](/features/load-balancing), autoscaling in Unikraft Cloud takes care of a *service*. Services allow you to load balance traffic for an Internet-facing service like a web server by creating many instances within the same service. While you can add or remove instances to a service to scale your service, doing this manually makes it hard to react to changes in traffic load. @@ -21,27 +21,37 @@ This is where autoscale comes into play. With autoscale enabled, Unikraft Cloud takes care of the heavy lifting for you by continuously monitoring the load of your service and automatically creating or deleting instances as needed. +{/* vale off */} :::caution[Limited Access] - -At the moment, autoscale **isn't** enabled by default on Unikraft Cloud accounts (you might get a "Autoscale not enabled for your account" error). -If you would like to enable it for you please reach out to the Unikraft Cloud Discord or send an email to `support@unikraft.com`. - +At the moment, autoscale **is not** enabled by default (you might get an "Autoscale not enabled for your account" error). +If you would like to enable it, please reach out to the [Unikraft Cloud Discord](https://kraft.cloud/discord) or send an email to `support@unikraft.com`. ::: +{/* vale on */} :::note -Autoscale as well as load balancing in general is currently only available for Internet-facing services. +Autoscale, as well as load balancing in general, currently supports only Internet-facing services. ::: ## Setting up autoscale -First, use the `kraft cloud deploy` command to create an instance, in this example using NGINX: +First, create an instance, in this example using NGINX: -```bash title="" + + +```bash title="unikraft" +git clone https://github.com/unikraft-cloud/examples +cd examples/nginx/ +unikraft run --metro=fra -p 443:8080/http+tls -m 256MiB --image=nginx:latest +``` + +```bash title="kraft" git clone https://github.com/unikraft-cloud/examples cd examples/nginx/ -kraft cloud deploy -p 443:8080 . +kraft cloud deploy -p 443:8080 -M 256 . ``` + + ```ansi title="" [●] Deployed successfully! │ @@ -51,24 +61,26 @@ kraft cloud deploy -p 443:8080 . ├─────────── url: https://small-leaf-rafirkw7.fra.unikraft.app ├───────── image: nginx@sha256:389bfa6be6455c92b61cfe429b50491373731dbdd8bd8dc79c08f985d6114758 ├───── boot time: 20.36 ms - ├──────── memory: 128 MiB + ├──────── memory: 256 MiB ├─────── service: small-leaf-rafirkw7 ├── private fqdn: nginx-4d7u3.internal ├──── private ip: 172.16.6.5 └────────── args: /usr/bin/nginx -c /etc/nginx/nginx.conf ``` -With this single `kraft cloud deploy` command, 3 things happen: +This single deploy or run flow does 3 things: 1. Creates an instance of NGINX which will serve as the **autoscale master** instance. 1. Creates a service via the `-p` flag (named `small-leaf-rafirkw7`). -1. Attaches the instance to the service (automatically done by the `-p` flag too). +1. Attaches the instance to the service (the `-p` flag also does this automatically). All that's left to do now to set up autoscale is to set an autoscale configuration *policy* and to set the instance as master. Unikraft Cloud then takes care of cloning this master instance whenever load increases. -To achieve this, use the `kraft cloud scale` command: +To achieve this, use the legacy CLI scale command: -```bash title="" + + +```bash title="kraft" kraft cloud scale init small-leaf-rafirkw7 \ --master nginx-4d7u3 \ --min-size 1 \ @@ -90,37 +102,42 @@ kraft cloud scale add small-leaf-rafirkw7 \ --step :50/-50 ``` + + Note the following: -* The first command sets the master to the instance created, and sets it to scale up to a maximum of 8 instances and a minimum of 1; the warm up and cool down time are also set to 1 second each, so it's not constantly fluctuating up and down. -* The second command sets the *scale out* policy based on CPU utilization (in millicores): between 60% and 80% utilization, an increase by 50% of instances occurs. +* The first command sets the master to the created instance, and configures it to scale up to a maximum of 8 instances and a minimum of 1; the command also sets the warm up and cool down time to 1 second each, so it doesn't constantly fluctuate up and down. +* The second command sets the *scale-out* policy based on CPU utilization (in millicores): between 60% and 80% utilization, the system increases instances by 50%. From 80% onward, the number of instances doubles. -* The third command sets the *scale in* policy: below 50% utilization, reduce the number of instances by half (note the `-` sign for scale in). +* The third command sets the *scale-in* policy: below 50% utilization, the system reduces the number of instances by half (note the `-` sign for scale-in). :::note -The intervals that autoscale uses for making scale-out and scale-in decisions use the `--warmup-time` and `--cooldown-time` parameters of `kraft cloud scale init`, in units of milliseconds. -Refer to the API autoscale [reference](/docs/api/v1/autoscale/#step) for more details. +The intervals that autoscale uses for making scale-out and scale-in decisions use the `--warmup-time` and `--cooldown-time` parameters of the scale init command, in units of milliseconds. +Refer to the API autoscale [reference](/api/platform/v1/autoscale) for more details. ::: :::note -Keep in mind that there are a few restrictions on how to define scale-in/scale-out steps. -The documentation is available [here](/docs/api/v1/autoscale/#step) at the bottom of the section. -General information about autoscale is available [here](/docs/api/v1/autoscale) +Keep in mind that a few restrictions apply to how you define scale-in/scale-out steps. +You can find the documentation [here](/api/platform/v1/autoscale) at the bottom of the section. ::: ## Testing it -To check it's working, you can use the `kraft cloud scale get` command to list the autoscale properties of the service: +To check it's working, you can use the legacy CLI scale get command to list the autoscale properties of the service: -```bash title="" + + +```bash title="kraft" kraft cloud scale get small-leaf-rafirkw7 ``` + + You should see output like: ```ansi title="" @@ -135,12 +152,16 @@ You should see output like: policies: scale-out-policy;scale-in-policy ``` -To list an individual policy, you can further use the `kraft cloud scale get` command as follows: +To list an individual policy, use the legacy CLI scale get command as follows: -```bash title="" + + +```bash title="kraft" kraft cloud scale get --policy scale-out-policy small-leaf-rafirkw7 ``` + + You should see output like: ```ansi title="" @@ -158,13 +179,21 @@ steps: type: step ``` -You can further check that the master instance is on `standby` (scaled to zero) assuming your service isn't receiving any traffic yet. -You can get the UUID of your master instance from the `kraft cloud scale get` command above. +You can further check that the master instance is on `standby` (scaled to zero), assuming your service hasn't received any traffic yet. +You can get the UUID of your master instance from the legacy CLI scale get command above. -```bash title="" + + +```bash title="unikraft" +unikraft instances get f840ac12-f485-4f02-9f33-6a0a7de46f1f -o list +``` + +```bash title="kraft" kraft cloud instance get f840ac12-f485-4f02-9f33-6a0a7de46f1f -o list ``` + + You should see output like: ```ansi title="" @@ -175,7 +204,7 @@ You should see output like: state: standby created at: 30 minutes ago image: nginx@sha256:d4325c1f1a472c511723148adc380d491029f4c98a2367fbeff628c6456d4180 - memory: 128 MiB + memory: 256 MiB args: /usr/bin/nginx -c /etc/nginx/nginx.conf env: volumes: @@ -186,18 +215,126 @@ You should see output like: Note the value of the `state` field. Now to make sure the service is up, `curl` the service address: -```bash +```bash title="" curl https://small-leaf-rafirkw7.fra.unikraft.app ``` You should get an immediate response, even though the instance was on `standby`. -You can use the `watch` tool to see if you manage to see the instance change state from `standby` to `running`: +You can use a watch command to see if you catch the instance changing state from `standby` to `running`: -```ansi title="" -watch --color -n 0.5 Unikraft Cloud instance list + + +```bash title="unikraft" +unikraft instances list --watch +``` + +```bash title="kraft" +watch --color -n 0.5 kraft cloud instance list +``` + + + +## Policy types + +Four autoscale policy types are available. +A service can have more than one policy active at the same time. + +### Step policy + +The `step` policy scales instances based on metric thresholds. +You define up to **4 steps**, each specifying a lower bound, upper bound, and the scaling change to apply when the metric falls in that range. + +You should order steps by lower bound with no gaps between them and no overlaps. + +```bash title="" +kraft cloud scale add \ + --name my-step-policy \ + --type step \ + --metric cpu \ + --adjustment-type change \ + --step 600:800/2 \ + --step 800:/4 +``` + +#### Metrics + +The following metrics can drive a step policy: + +| Metric | Description | +|--------|-------------| +| `cpu` | CPU utilization in millicores | +| `inflight_reqs` | Number of requests the platform is processing across all instances | +| `reqs_per_sec` | Request throughput in requests per second | + +#### Scale change types + +Step policies support three scaling change types: + +| Type | Description | +|------|-------------| +| `change` | Change the instance count by the specified value (positive to scale out, negative to scale in) | +| `exact` | Set the instance count to exactly the specified value | +| `percent` | Change the instance count by the specified percentage of the current count | + + +### On-demand policy + +The `on-demand` policy creates a new instance immediately when an incoming request finds no available instances. +This prevents request queuing but introduces cold start delays. + +```json title="POST /services" +{ + ... + "policies": [ + { + "name": "scale-out", + "type": "on_demand" + } + ] + ... +} +``` + +### Create policy + +The `create` policy provisions a new VM when an instance exceeds the `num_requests` threshold. +Setting `replace` to true deletes the original VM after the new one starts. + +```json title="POST /services" +{ + ... + "policies": [ + { + "name": "create", + "type": "create", + "replace": true, + "num_requests": 100 + } + ] + ... +} +``` + + +### Idle policy + +The `idle` policy scales in (removes instances) when the service has been idle—receiving no requests—for a configurable period. + +```json title="POST /services" +{ + ... + "policies": [ + { + "name": "scale-in", + "type": "idle", + "idle_time_ms": 1000 + } + ] + ... +} ``` ## Learn more -* The `kraft cloud` [command-line tool reference](/docs/cli/), and in particular the [scale](/docs/cli/scale) subcommand. -* Unikraft Cloud's [REST API reference](/docs/api/v1), and in particular the section on [autoscale](/docs/api/v1/autoscale). +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1), and in particular the section on [autoscale](/api/platform/v1/autoscale). diff --git a/pages/features/cron-jobs.mdx b/pages/features/cron-jobs.mdx new file mode 100644 index 00000000..0bf2f428 --- /dev/null +++ b/pages/features/cron-jobs.mdx @@ -0,0 +1,147 @@ +--- +title: Cron Jobs / Scheduled Wake-ups +navigation_icon: clock +--- + +Scheduled operations let you automatically start, stop, delete, or exec a command in instances on a calendar-based schedule. +Each scheduled operation specifies a name, a calendar expression, and an action (`start`, `stop`, `delete`, or `exec`). + +Each instance stores its own schedules, and cloning preserves them. + +## Calendar expression format + +Unikraft Cloud uses systemd calendar events (see [systemd.time(7)](https://www.man7.org/linux/man-pages/man7/systemd.time.7.html)). +Calendar expressions have the following fields: + +``` +[weekday] [[year-]month-day] [hour:minute[:second]] +``` + +The expression syntax supports ranges, steps, and comma-separated lists: + +| Syntax | Description | +|--------|-------------| +| `*` | Any value | +| `5` | Exact value | +| `1..5` | Range | +| `1..5/2` | Range with step | +| `1,2,5` | Comma-separated list | + +## Setting a schedule at instance creation + +Pass `schedules` in the create request. +For example, to start an instance every day at 09:00 UTC and stop it at 18:00 UTC: + +```json title="POST /instances" +{ + "image": "...", + "schedules": [ + { + "name": "morning-start", + "when": "*-*-* 09:00:00", + "action": "start" + }, + { + "name": "evening-stop", + "when": "*-*-* 18:00:00", + "action": "stop" + } + ] +} +``` + +To run a command inside the instance every night at midnight, use the `exec` action with an `args` array: + +```json title="POST /instances" +{ + "image": "...", + "schedules": [ + { + "name": "nightly-cleanup", + "when": "*-*-* 00:00:00", + "action": "exec", + "args": ["/bin/sh", "-c", "rm -rf /tmp/*"] + } + ] +} +``` + +## Updating schedules of an existing instance + +Add, set, or remove scheduled operations via `PATCH`: + +```json title="PATCH /instances/{uuid}" +{ + "prop": "schedules", + "op": "add", + "value": [ + { + "name": "weekend-stop", + "when": "Sat,Sun *-*-* 20:00:00", + "action": "stop" + } + ] +} +``` + +Add an `exec` schedule the same way, but include `args` with the command to run: + +```json title="PATCH /instances/{uuid}" +{ + "prop": "schedules", + "op": "add", + "value": [ + { + "name": "hourly-healthcheck", + "when": "*-*-* *:00:00", + "action": "exec", + "args": ["/usr/bin/healthcheck"] + } + ] +} +``` + +```json title="PATCH /instances/{uuid}" +{ + "prop": "schedules", + "op": "del", + "value": [ + "morning-start" + ] +} +``` + +## Actions + +| Action | Description | +|--------|-------------| +| `start` | Start the instance at the scheduled time | +| `stop` | Stop the instance at the scheduled time | +| `delete` | Delete the instance at the scheduled time | +| `exec` | Execute a command inside the instance at the scheduled time. Requires the `args` field. | + +### The `args` field + +The `args` field is an array of strings specifying the command to run when using the `exec` action. +The first element is the executable and the remaining elements are its arguments. + +```json +{ + "name": "daily-report", + "when": "*-*-* 06:00:00", + "action": "exec", + "args": ["/usr/bin/python3", "/app/report.py", "--daily"] +} +``` + +No need to specify it for all other actions. + +## Notes + +- Each schedule has a name that must be unique within an instance. +- An instance can have more than one scheduled operation. +- When you clone an instance from a template, the clone inherits its scheduled operations. + +## Learn more + +* Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). diff --git a/pages/features/forking.mdx b/pages/features/forking.mdx new file mode 100644 index 00000000..5fa51227 --- /dev/null +++ b/pages/features/forking.mdx @@ -0,0 +1,136 @@ +--- +title: Instance Forking +navigation_icon: git-fork +--- + +:::caution +Instance forking is a preview feature and isn't yet available on stable. +The interface described here reflects the current implementation and may change before general availability. +::: + +Instance forking lets a running instance create an independent copy of itself at a snapshot boundary. +Both the original instance (the **parent**) and the copy (the **child**) resume execution from the same memory state. +This works like `fork(2)` in a POSIX process. + +The app inside an instance triggers the fork through a filesystem interface. + +## Filesystem interface + +The fork interface uses a FUSE (Filesystem in Userspace) filesystem mounted at `/uk/libukp` inside every instance. +Apps trigger forks and track child instances by reading and writing (ASCII) files in this filesystem. + +The filesystem exposes the following top-level entries: + +| Path | Read | Write | Description | +|------|------|-------|-------------| +| `/uk/libukp/fork` | Returns the child UUID after a fork (for the parent), or empty (for the child). | Triggers a new fork operation. | Main entry point for forking. | +| `/uk/libukp/vm/` | Directory listing of all child instance UUIDs. | — | Each child gets a subdirectory named by its UUID. | + +## Triggering a fork + +To fork the current instance, write any value to the `fork` file: + +```bash +echo "1" > /uk/libukp/fork +``` + +The write blocks until the platform completes the fork. +When it returns, the parent can read the child's UUID from the same file: + +```bash +# Returns the UUID of the newly created child (parent only) +# To support parallel forks, only the thread initiating the fork can read the return value from the file. +cat /uk/libukp/fork +``` + +For the child instance, reading `/uk/libukp/fork` returns a zero UUID. +This is how an app determines whether it's the parent or the child after a fork. + +### Concurrent fork requests + +The `ukp-fuse` daemon serializes fork operations. +Only one fork can run at a time. +If a second fork request arrives while one runs, the daemon queues it. +If the queue fills up, the write returns `EBUSY`. + +The daemon processes queued requests in order once the current fork completes. + +## Tracking child instances + +After a fork, a new directory appears at `/uk/libukp/vm//` containing files that reflect the child's state: + +| File | Read | Write | Description | +|------|------|-------|-------------| +| `snapshot` | Snapshot UUID the child originates from. | — | Read-only. | +| `creation_time` | Unix timestamp (seconds since epoch) of child creation. | — | Read-only. | +| `status` | Current state: `Starting`, `Running`, `Draining`, `Stopping`, `Stopped`, or `Template`. | — | Read-only. | +| `wait` | Exit code of the child (available once stopped). | Write any value to begin waiting for the child to stop. | Supports `poll()` for non-blocking use. Returns `POLLIN` when the exit code becomes available. | + +### Waiting for a child to stop + +To block until a child instance exits: + +```bash +# Write to begin the wait, then read the exit code +echo "1" > /uk/libukp/vm//wait +cat /uk/libukp/vm//wait +``` + +The write to `wait` blocks until the child stops. +After the write completes, reading `wait` returns the numeric exit code. + +If the child has already stopped by the time you write to `wait`, the write completes immediately. + +Many processes can wait on the same child concurrently. +The daemon notifies each one when the child exits. + +For non-blocking usage, `poll()` on the `wait` file descriptor returns `POLLIN` when the exit status becomes available. + +### Checking child status + +To inspect a child's current state without blocking: + +```bash +cat /uk/libukp/vm//status +# Output: Running +``` + +The platform updates the status file in real time. + +Possible states and their numeric values: + +| State | Description | +|-------|-------------| +| `Stopped` | Instance has exited. | +| `Starting` | Instance is booting. | +| `Running` | Instance is running. | +| `Draining` | Instance is draining connections before stopping. | +| `Stopping` | Instance is shutting down. | +| `Template` | Instance has become a template. | + +## Child lifecycle + +- Children have `delete-on-stop` enabled automatically. +The platform removes them when they stop. +- The child's `/uk/libukp/vm/` directory starts empty. +The child doesn't inherit any children from the parent—it only sees children from forks it starts itself. +- A parent can have many children alive at the same time. +- A child can itself fork, becoming a parent to its own children. + +## Error handling + +| Error | Cause | +|-------|-------| +| `EBUSY` | Fork queue is full. | +| `EIO` | Hardware signal to the platform failed, or the request belonged to a parent before a fork occurred. | +| `ESTALE` | A child VM directory access occurred after the child's cleanup (for example, after the current instance became a child via fork). | + +## Constraints + +- The REST API and `kraft` can't trigger a fork. +You must start it from within the instance via the `/uk/libukp/fork` file. + +## Learn more + +- [Instance templates](/platform/instances#instance-templates): a related snapshot-based feature for creating instances from a stopped template +- [Instances](/platform/instances) diff --git a/pages/features/load-balancing.mdx b/pages/features/load-balancing.mdx index d81468da..110a758f 100644 --- a/pages/features/load-balancing.mdx +++ b/pages/features/load-balancing.mdx @@ -3,7 +3,7 @@ title: Load Balancing navigation_icon: split --- -Load balancing in Unikraft Cloud is easy: as soon as you attach more than one instance to a [service](/docs/guides/features/service), Unikraft Cloud will automatically start balancing traffic between many instances. +Load balancing in Unikraft Cloud is easy: as soon as you attach more than one instance to a [service](/platform/services), Unikraft Cloud will automatically start balancing traffic between many instances. The load balancing happens based on the number of connections (in TCP mode) or requests (in HTTP mode). More on that below. @@ -11,16 +11,16 @@ Because of load balancing, instances in a service must be of the same kind (for You can remove instances from a service at any time, and, when you do, Unikraft Cloud will immediately take the instance out of the load balancing service. -## Soft-/hard-limits +## Soft and hard limits -[Services](/docs/platform/services) have soft and hard limits for the number of concurrent requests and connections. +[Services](/platform/services) have soft and hard limits for the number of concurrent requests and connections. The limits apply **per instance**. -For HTTP services (that is, using the `http` [handler](#handlers)) the system checks each individual in-flight request against the limit, but not the underlying TCP connection. -For TCP services the individual open connections get counted. +For HTTP services (that is, using the `http` [handler](/platform/services#handlers)), the system checks each individual in-flight request against the limit, but not the underlying TCP connection. +For TCP services, the system counts the individual open connections. In the following, the term request refers to both requests and connections. -The load balancer uses the soft limit to decide when to wake up another [standby](/docs/api/v1/instances#states) instance. -For example, if you set the soft limit to 5 and the service consists of 2 standby instances, one of the instances receives up to 5 concurrent requests. +The load balancer uses the soft limit to decide when to wake up another [standby](/platform/instances#instance-states) instance. +For example, if you set the soft limit to 5 and the service consists of 2 standby instances, one of the instances can receive up to 5 concurrent requests. The 6th parallel request wakes up the second instance. If there are no more standby instances to wake up, the number of requests assigned to each instance will exceed the soft limit. The load balancer ensures that when the number of in-flight requests goes down again, instances go into standby as fast as possible. @@ -32,15 +32,26 @@ In case there are no other instances available, the load balancer blocks excess ## Setup -To set load balancing up, first use `kraft cloud deploy` with the `-p` flag to create the service as part of the instance creation. +To set load balancing up, first use the deploy or run flow with the publish flag to create the service as part of the instance creation. For example, use NGINX as the app: -```bash title="" + + +```bash title="unikraft" +git clone https://github.com/unikraft-cloud/examples +cd examples/nginx/ +unikraft build . --output /nginx:latest +unikraft run --metro=fra -p 443:8080/http+tls -m 256MiB --image=/nginx:latest +``` + +```bash title="kraft" git clone https://github.com/unikraft-cloud/examples cd examples/nginx/ -kraft cloud deploy -p 443:8080 . +kraft cloud deploy -p 443:8080 -M 256 . ``` + + This single command will (a) create a service via the `-p` flag and (b) start an NGINX instance: ```ansi title="" @@ -52,7 +63,7 @@ This single command will (a) create a service via the `-p` flag and (b) start an ├─────────── url: https://wandering-shape-n6mhimgn.fra.unikraft.app ├───────── image: nginx@sha256:269192f523dca7498423bc54676ab08e415e9c7442d1bd3d65f07ab5e50a43d ├───── boot time: 20.18 ms - ├──────── memory: 128 Mi + ├──────── memory: 256 MiB ├─────── service: wandering-shape-n6mhimgn ├── private fqdn: nginx-8ujeu.internal ├──── private ip: 172.16.6.7 @@ -61,20 +72,27 @@ This single command will (a) create a service via the `-p` flag and (b) start an With this in place, it's now time to start a second instance and attach it to the created service (in this case, named `wandering-shape-n6mhimgn`): -```bash title="" + + +```bash title="unikraft" cd examples/nginx/ -kraft cloud deploy --service wandering-shape-n6mhimgn . +unikraft run --metro=fra --service wandering-shape-n6mhimgn -m 256MiB --image=/nginx:latest ``` -:::tip +```bash title="kraft" +cd examples/nginx/ +kraft cloud deploy --service wandering-shape-n6mhimgn -M 256 . +``` -If you get a prompt saying "deployment already exists: what would you like to do with the 1 existing instances," choose `keep`. + +:::tip +If a prompt appears saying "deployment already exists: what would you like to do with the 1 existing instances," choose `keep`. ::: The command's output should look like this: -```ansi +```ansi title="" [●] Deployed successfully! │ ├────────── name: nginx-djta3 @@ -83,20 +101,28 @@ The command's output should look like this: ├─────────── url: https://wandering-shape-n6mhimgn.fra.unikraft.app ├───────── image: nginx@sha256:c00c11a5cbd6a3020dd4d9703fbeb2a2f2aab37f18f7a0ba9c66db5a71897c3a ├───── boot time: 20.46 ms - ├──────── memory: 128 MiB + ├──────── memory: 256 MiB ├______─ service: wandering-shape-n6mhimgn ├── private fqdn: nginx-djta3.internal ├──── private ip: 172.16.6.3 └────────── args: /usr/bin/nginx -c /etc/nginx/nginx.conf ``` -Both the `url` and `service` fields in the 2 instances are the same, as they should be. +Both the `url` and `service` fields in the 2 instances are the same. To check that it worked, run the following command: -```bash title="" + + +```bash title="unikraft" +unikraft services list +``` + +```bash title="kraft" kraft cloud service list -o list ``` + + You should see output such as: ```ansi title="" @@ -113,26 +139,30 @@ Note the two instances (their UUIDs) under the `instances` field. You're now load balancing across 2 NGINX instances! :::tip - -Currently it's impossible to set the service's name via `kraft cloud deploy`. -If you'd like to set the service's name, first use the `kraft cloud service` command to create the service. -You can use the `--service` flag of `kraft cloud deploy` to attach the instance to the service, as outlined in this [guide](/docs/guides/features/service) - +Currently it's impossible to set the service's name via the deploy or run flow. +If you'd like to set the service's name, first use the CLI to create the service. +You can use the `--service` flag to attach the instance to the service, as outlined in this [guide](/platform/services). ::: :::tip +To ensure that stopping an instance doesn't drop existing connections, use Unikraft Cloud's draining feature by stopping an instance with a drain timeout. -If you want to ensure that no existing connections get dropped when stopping an instance, use Unikraft Cloud's draining feature by stopping an instance through `kraft cloud instance stop -w ` + + +```bash title="kraft" +kraft cloud instance stop -w +``` + ::: ## Load balancing algorithm The load balancing algorithm is a variant of `least_conn`. -For every instance, the number of current in-flight TCP connections (if in `tcp` [mode](/docs/cli/service/create)) or requests (if in `http` [mode](/docs/cli/service/create)). +For every instance, the system tracks the number of current in-flight TCP connections (if in `tcp` [mode](/platform/services#handlers)) or requests (if in `http` [mode](/platform/services#handlers)). To select an instance, the system goes over all instances in the service. -It finds the set of instances that have the least amount of in-flight requests/connections, and picks from that set. +It finds the set of instances with the least amount of in-flight requests/connections, and picks from that set. To illustrate, imagine this scenario: @@ -143,12 +173,12 @@ To illustrate, imagine this scenario: | `i-2` | 2 | | `i-3` | 1 | -In this case, the algorithm would first choose instances `i-1` and `i-3`, since they both have the least number of connections at the moment (only 1 each). -After that, the algorithm would choose between these 2 instances and assign the new connection to it. -For example, if it chose `i-1`, the next new connection would go to `i-3`. -This is because it would now be the only instance with only 1 connection (assuming none of the connections that the other instances handle close). +In this case, the algorithm first chooses instances `i-1` and `i-3`, since they both have the least number of connections at the moment (only 1 each). +After that, it chooses between these 2 instances and assigns the new connection to one of them. +For example, if it chose `i-1`, the next new connection goes to `i-3`. +This is because `i-3` now becomes the only instance with only 1 connection (assuming none of the connections that the other instances handle close). ## Learn more -* The `kraft cloud` [command-line tool reference](/docs/cli/), and in particular the services subcommand. -* Unikraft Cloud's [REST API reference](/docs/api/v1), and in particular the section on services. +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1), and in particular the section on services. diff --git a/pages/features/roms.mdx b/pages/features/roms.mdx new file mode 100644 index 00000000..16761b45 --- /dev/null +++ b/pages/features/roms.mdx @@ -0,0 +1,563 @@ +--- +title: ROMs +navigation_icon: hard-drive +--- + +:::caution[**DISCLAIMER**] +The ROM feature isn't currently enabled for the public Unikraft Cloud offering. +As such, the CLI can't entirely leverage this feature. +For boxes where it's enabled, use it via the [Unikraft Cloud API](/api/platform/v1). +::: + +Unikraft Cloud supports the ability to attach **Read-Only Memory (ROM)** blobs to instances. +It allows you to create a general-purpose base image and then customize individual instances by attaching code or data as separate ROM blobs. +This enables quick deployment of custom functionality to preexisting language environments without rebuilding the entire image. + +## Overview + +With ROMs, you can: + +- Deploy variations of an app from a single base image. +- Update app code without rebuilding the base image. +- Reduce image size and deployment time. + +The ROM workflow consists of two main components: + +1. **Base Image**: A general-purpose image containing the runtime environment (for example, Python interpreter, Node.js, etc.). +1. **ROM Blobs**: Separate, lightweight images containing your app code or any other data you want to customize. + +This separation allows you to maintain one base image while deploying many different instances with different data. + +:::note +By default, the app running inside the base image needs to mount the attached ROM device. +ROM blobs appear as block devices at paths like `/dev/ukp_rom_`, where `` is the ROM name you specify when creating the instance. + +For convenience, you can use **automounting** by setting the `at` field on a ROM entry. +When `at` is present, the platform automatically mounts the ROM at the given path before the instance starts, so your app can read files from it directly without any manual mount step. +::: + +## Setup + +This example shows how to deploy Python functions using ROMs on Unikraft Cloud. +Ensure you have the `kraft` CLI installed and configured with your Unikraft Cloud account. +Set the following environment variables: + +```bash title="" +export UKC_USER="" +export UKC_TOKEN="" +export UKC_METRO="fra" # or your preferred metro +``` + +### Base Image + +First, create a base image with a Python HTTP server that loads and executes custom Python programs from a ROM. + + + +```dockerfile title="Dockerfile" +FROM python:3.12 AS build + +RUN set -xe; \ + /usr/sbin/ldconfig /usr/local/lib + +FROM scratch + +# Python binary +COPY --from=build /usr/local/bin/python3 /usr/bin/python3 + +# System binaries +COPY --from=build /usr/bin/mount /usr/bin/mount +COPY --from=build /usr/bin/sh /usr/bin/sh +COPY --from=build /usr/bin/mkdir /usr/bin/mkdir + +# System Libraries +COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 +COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 +COPY --from=build /lib/x86_64-linux-gnu/libmount.so.1 /lib/x86_64-linux-gnu/libmount.so.1 +COPY --from=build /lib/x86_64-linux-gnu/libselinux.so.1 /lib/x86_64-linux-gnu/libselinux.so.1 +COPY --from=build /lib/x86_64-linux-gnu/libblkid.so.1 /lib/x86_64-linux-gnu/libblkid.so.1 +COPY --from=build /lib/x86_64-linux-gnu/libpcre2-8.so.0 /lib/x86_64-linux-gnu/libpcre2-8.so.0 +COPY --from=build /usr/lib/x86_64-linux-gnu/libz.so.1 /usr/lib/x86_64-linux-gnu/libz.so.1 +COPY --from=build /usr/lib/x86_64-linux-gnu/libcrypto.so.3 /usr/lib/x86_64-linux-gnu/libcrypto.so.3 + +# Dynamic linker / loader +COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=build /etc/ld.so.cache /etc/ld.so.cache + +# Python (and other) libraries +COPY --from=build /usr/local/lib /usr/local/lib + +# Python app +COPY ./wrapper.sh /wrapper.sh +COPY ./server.py /src/server.py +``` +```yaml title="Kraftfile" +spec: v0.6 + +runtime: base-compat:latest + +rootfs: ./Dockerfile + +cmd: [ "/wrapper.sh", "/usr/bin/python3", "/src/server.py" ] +``` + +```python title="server.py" +import argparse +import importlib.util +import sys +from http.server import HTTPServer, BaseHTTPRequestHandler + +rom_module = None + +def load_rom_module(): + global rom_module + module_name = 'rom' + module_path = '/tmp/rom.py' + + spec = importlib.util.spec_from_file_location(module_name, module_path) + rom_module = importlib.util.module_from_spec(spec) + sys.modules[module_name] = rom_module + spec.loader.exec_module(rom_module) + +class MyServer(BaseHTTPRequestHandler): + global rom_module + def do_GET(self): + self.send_response(200) + self.send_header("Content-type", "text/html") + self.end_headers() + msg = rom_module.function() + self.wfile.write(bytes(msg, "utf-8")) + +def main(args): + load_rom_module() + server = HTTPServer((args.host, args.port), MyServer) + + print("starting server at %s:%s" % (args.host, args.port)) + + try: + server.serve_forever() + + except KeyboardInterrupt: + pass + + print("server stopped") + +def parse_args(): + parser = argparse.ArgumentParser() + parser.add_argument("--host", type=str, default="0.0.0.0") + parser.add_argument("--port", type=int, default=8080) + return parser.parse_args() + +if __name__ == "__main__": + main(parse_args()) +``` + +```bash title="wrapper.sh" +#!/usr/bin/sh + +/usr/bin/mkdir -p /tmp/ +/usr/bin/mount -t erofs /dev/ukp_rom_python_function.py /tmp/ +exec "$@" +``` + + +:::tip +If you use the `at` field when attaching the ROM (see [Automounting](#automounting)), the platform handles the mount for you and you can remove `wrapper.sh` and the `mount`/`mkdir` calls entirely. +::: + +Ensure that the `wrapper.sh` script is executable: + +```bash title="" +chmod a+x wrapper.sh +``` + +Package and push the base image: + +```bash title="" +kraft pkg \ + --plat kraftcloud \ + --arch x86_64 \ + --name "$UKC_USER"/http-python:latest \ + --rootfs-type erofs \ + --push . +``` + +Wait a few seconds for propagation and check that the image is present: + + + +```bash title="unikraft" +unikraft images list +``` + +```bash title="kraft" +kraft cloud image ls +``` + + + +{/* vale off */} +### ROM files +{/* vale on */} + +To showcase the benefits of using ROMs, create two files, each containing a Python function that the base image will load and execute. +Create a separate directory for each of the ROMs: + +```bash title="" +mkdir -p rom1/fs rom2/fs +``` + + + +```python title="rom1/fs/rom.py" +def function(): + return "Hi from 1st ROM!\n" +``` + +```yaml title="rom1/Kraftfile" +spec: v0.6 + +roms: + - ./fs +``` + +```python title="rom2/fs/rom.py" +def function(): + return "Hi from 2nd ROM!\n" +``` + +```yaml title="rom2/Kraftfile" +spec: v0.6 + +roms: + - ./fs +``` + + +Package and push the ROMs: + +```bash title="" +cd rom1/ +kraft pkg \ + --no-kernel \ + --plat kraftcloud \ + --arch x86_64 \ + --name "$UKC_USER"/my-rom1:latest \ + --rootfs-type erofs \ + --push . + +cd ../rom2/ +kraft pkg \ + --no-kernel \ + --plat kraftcloud \ + --arch x86_64 \ + --name "$UKC_USER"/my-rom2:latest \ + --rootfs-type erofs \ + --push . +``` + +Wait a few seconds for propagation and check that the ROMs are present: + + + +```bash title="unikraft" +unikraft images list +``` + +```bash title="kraft" +kraft cloud image ls +``` + + + +:::tip +The `--no-kernel` flag tells `kraft` to package only the ROM files, without the kernel, since this is data that will get attached to another image. +::: + +:::caution +This example packages the ROMs as EROFS filesystems. +If packaging the ROMs as a cpio archives (that is, `--rootfs-type cpio`), or as standalone files, you must align their size to 4096 bytes. +::: + +### Instances + +Create two instances with the same base image and attach different ROMs: + +```bash title="" +curl -X POST \ + -H "Authorization: Bearer ${UKC_TOKEN}" \ + -H "Content-Type: application/json" \ + "${UKC_METRO}/instances" \ + -d "[ + { + 'name': 'test-http-python-rom1', + 'image': '${UKC_USER}/http-python:latest', + 'service_group': { + 'services': [ + { + 'port': 443, + 'destination_port': 8080, + 'handlers': ['tls', 'http'] + } + ], + 'domains': [ + { + 'name': 'test-http-python-rom1' + } + ] + }, + 'memory_mb': 512, + 'scale_to_zero': { + 'policy': 'on', + 'stateful': false, + 'cooldown_time_ms': 1000 + }, + 'autostart': true, + 'roms': [ + { + 'name': 'python_function.py', + 'image': '$UKC_USER/my-rom1:latest' + } + ] + }, + { + 'name': 'test-http-python-rom2', + 'image': '${UKC_USER}/http-python:latest', + 'service_group': { + 'services': [ + { + 'port': 443, + 'destination_port': 8080, + 'handlers': ['tls', 'http'] + } + ], + 'domains': [ + { + 'name': 'test-http-python-rom2' + } + ] + }, + 'memory_mb': 512, + 'scale_to_zero': { + 'policy': 'on', + 'stateful': false, + 'cooldown_time_ms': 1000 + }, + 'autostart': true, + 'roms': [ + { + 'name': 'python_function.py', + 'image': '$UKC_USER/my-rom2:latest' + } + ] + } + ]" +``` + +Check that the instances are up: + + + + ```bash title="unikraft" + unikraft instances get test-http-python-rom1 + unikraft instances get test-http-python-rom2 + ``` + + ```bash title="kraft" + kraft cloud instance get test-http-python-rom1 + kraft cloud instance get test-http-python-rom2 + ``` + + + +Note the `roms` array in the instances configurations. +Each ROM is available as a readable device at `/dev/ukp_rom_python_function.py` (in the form of an EROFS filesystem in this case), which the server program mounts at `/tmp/rom.py` and executes. + +## Inline ROMs + +Instead of pre-packaging and pushing a ROM image to a registry, you can provide file contents directly in the instance creation request. +This is useful when you create instances from a template where the snapshot locks ENV and ARGS, but you still need to inject instance-specific data. + +### API format + +Add a `files` array to a ROM entry instead of an `image` field: + +```json title="POST /instances" +{ + "image": "myuser/my-base:latest", + "roms": [ + { + "name": "my-rom", + "files": [ + { + "path": "config/settings.json", + "encoding": "text", + "data": "{\"debug\": false, \"port\": 8080}" + }, + { + "path": "certs/client.pem", + "encoding": "base64", + "data": "LS0tLS1CRUdJTi..." + } + ], + "at": "/roms/my-rom" + } + ] +} +``` + +Each file in the `files` array has three fields: + +| Field | Description | +|-------|-------------| +| `path` | File path inside the ROM filesystem. The platform creates directories automatically. | +| `encoding` | Either `text` (UTF-8 string) or `base64` (base64-encoded binary data). | +| `data` | The file contents in the specified encoding. | + +The platform packages the provided files into an EROFS filesystem and attaches it as a ROM device, the same way image-based ROMs work. +The `at` field is optional and works the same as for image-based ROMs—when present, the platform mounts the ROM at the given path automatically. + +### Combining with image-based ROMs + +You can mix inline ROMs and image-based ROMs in the same request: + +```json title="POST /instances" +{ + "image": "myuser/my-base:latest", + "roms": [ + { + "name": "app-code", + "image": "myuser/my-rom:latest", + "at": "/app" + }, + { + "name": "instance-config", + "files": [ + { + "path": "config.json", + "encoding": "text", + "data": "{\"instance_id\": \"abc-123\"}" + } + ], + "at": "/config" + } + ] +} +``` + +### Limits + +The platform enforces the following default limits on inline ROMs: + +| Limit | Default | +|-------|---------| +| ROMs per instance | 8 (inline and image-based combined) | +| Files per inline ROM | 16 | +| Total size per inline ROM | 1 MB (across all files) | +| Max API request body | 8 MB | + +No per-file size limit exists. +The total size limit applies to the combined size of all files in a single inline ROM. +Since inline ROM data is base64-encoded in the JSON request body, the 8 MB request body limit is the practical upper bound on total data per API call. + +## Automounting + +Instead of mounting the ROM device manually inside the guest, you can set the optional `at` field on any ROM entry. +The platform will then mount the ROM at the specified path before the instance starts. + +```json title="POST /instances" +{ + "image": "...", + "roms": [ + { + "name": "python_function.py", + "image": "myuser/my-rom1:latest", + "at": "/tmp" + } + ] +} +``` + +{/* vale off */} +With this configuration the EROFS filesystem is mounted at `/tmp` automatically, so `/tmp/rom.py` is available to the app without any `mount` call in a wrapper script. +In this case you can simplify or remove the `wrapper.sh` from the base image, since the platform handles the mount for you. +{/* vale on */} + +The `at` field is optional--omitting it leaves the ROM as a raw block device at `/dev/ukp_rom_` and the guest remains responsible for mounting it. + +## Inline ROMs + +Instead of referencing a pre-built image, you can supply file contents directly in the instance creation request using **inline ROMs**. +You specify an inline ROM by a `files` array instead of an `image` field. +Each entry in the array specifies the file `path` inside the ROM filesystem, the `encoding` of the provided data (`"base64"` or `"text"`), and the raw `data` string. + +```json title="POST /instances" +{ + "image": "...", + "roms": [ + { + "name": "my-rom", + "files": [ + { + "path": "test/file.txt", + "encoding": "base64", + "data": "VGhpcyBpcyBhIHRlc3QgZmlsZS4=" + }, + { + "path": "another-file.txt", + "encoding": "text", + "data": "This is another test file." + } + ], + "at": "/roms/my-rom" + } + ] +} +``` + +Inline ROMs are particularly useful when working with **instances created from templates**, where you can't change the environment variables and startup arguments of the original template. +By attaching an inline ROM you can inject configuration files, scripts, or other data into the instance at creation time without modifying the base image or the template. + +{/* vale off */} +:::note +An inline ROM is ephemeral: it's automatically deleted once the last reference to it is closed. +::: +{/* vale on */} + +### Testing + +Query the instances to call the Python function from the ROM. +You should see different responses: + +```console title="" +$ curl https://test-http-python-rom1.fra.kraft.cloud +Hi from 1st ROM! +``` + +```console title="" +$ curl https://test-http-python-rom2.fra.kraft.cloud +Hi from 2nd ROM! +``` + +### Cleanup + +When done, remove the instances: + + + +```bash title="unikraft" +unikraft instances delete test-http-python-rom1 test-http-python-rom2 +``` + +```bash title="kraft" +kraft cloud instance rm test-http-python-rom1 test-http-python-rom2 +``` + + + +## Learn more + +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1), and in particular the [instances API](/api/platform/v1/instances). +* The `kraft pkg` [command reference](https://unikraft.org/docs/cli/reference/kraft/pkg) for packaging images and ROMs. +* The [systemd `mount` man page](https://www.man7.org/linux/man-pages/man8/mount.8.html) for filesystem mount options relevant to manual mounting scenarios. diff --git a/pages/features/scale-to-zero.mdx b/pages/features/scale-to-zero.mdx index 013e2b98..3632cbcc 100644 --- a/pages/features/scale-to-zero.mdx +++ b/pages/features/scale-to-zero.mdx @@ -11,17 +11,17 @@ Instances on Unikraft Cloud can cold boot within milliseconds, while providing t Millisecond cold boots allow performing low-latency scale-to-zero. As long as no traffic is flowing through your instance, it consumes **no** resources. -When the next connection arrives, Unikraft Cloud takes care of transparently cold booting (it's called cold booting if it's milliseconds?) your instance and replying. -All within a negligible amount of time for Internet round-trip times and so unbeknownst to your end users. +When the next connection arrives, Unikraft Cloud takes care of transparently cold booting (is it cold booting if it's milliseconds?) your instance and replying. +All within a negligible amount of time compared to Internet round-trip times, and so unbeknownst to your end users. -By default, Unikraft Cloud reduces network and cloud stack cold start time to a min time. +By default, Unikraft Cloud reduces network and cloud stack cold start time to a minimum. Some apps take a while to initialize (for example, Spring Boot, Puppeteer, etc). -If you need to deploy such an app and still want millisecond cold starts, Unikraft Cloud provides a *stateful* feature. -Please check out [this guide](/docs/guides/features/stateful/) for more information on how to set this up. +If you need to deploy such an app and still want millisecond cold starts, Unikraft Cloud offers a *stateful* feature. +Please check out [this guide](/features/scale-to-zero#stateful-scale-to-zero) for more information on how to set this up. :::tip -If you add instances to a [service](/docs/guides/features/service), the service will load balance traffic across all them and Unikraft Cloud will ensure each instance gets woken up as needed. -This differs from [autoscale](/docs/guides/features/autoscale), in which you *don't* specify the number of instances. +If you add instances to a [service](/platform/services), the service will load balance traffic across them and Unikraft Cloud will wake up each instance as needed. +This differs from [autoscale](/features/autoscale), in which you *don't* specify the number of instances. The platform does this for you based on traffic load. ::: @@ -34,9 +34,9 @@ Unikraft Cloud currently supports the following scale-to-zero policies: | Policy | Description | |--------|-------------| -| `off` | Scale-to-zero isn't enabled. The instance keeps on running until manually stopped. | -| `on` | Enables scale-to-zero. When there are no TCP connections or HTTP requests during the cool-down time, the instance goes into standby. | -| `idle` | Same as `on`, but also puts the instance into standby when there are TCP connections established that have been inactive during the cool-down time. The connections remain established and incoming packets wake up the instance. Scale-to-zero doesn't happen while there are active HTTP requests (that is, traffic on ports, which use the `http` [handler](/docs/api/v1/services#handlers)). | +| `off` | Disables scale-to-zero. The instance keeps on running until manually stopped. | +| `on` | Enables scale-to-zero. When there are no TCP connections or HTTP requests during the cooldown time, the instance goes into standby. | +| `idle` | Same as `on`, but also puts the instance into standby when there are TCP connections established that have been inactive during the cooldown time. The connections remain established and incoming packets wake up the instance. Scale-to-zero doesn't happen while there are active HTTP requests (that is, traffic on ports which use the `http` [handler](/platform/services#handlers)). | :::note Unikraft Cloud only considers network traffic that's going through its proxy to control scale-to-zero. @@ -52,18 +52,17 @@ Cases exist where making your app aware of scale-to-zero makes sense. - **Background Jobs**: For example, you want to run background jobs after your app has responded to a request (for example, send trace information to a logging server). In this case, you may want to temporarily disable scale-to-zero to ensure your instance isn't put to sleep while still performing work. - Cases exist where making your app aware of scale-to-zero makes sense. - **Long Request Processing**: The same is true if your app can have long request processing times. - Consider a setup where you use the `idle` policy with plain TCP connections and configure a cool-down time of 10s. + Consider a setup where you use the `idle` policy with plain TCP connections and configure a cooldown time of 10s. Sometimes if it takes your app 15s to process a request until the first response data gets sent, Unikraft Cloud will prematurely scale your instance to zero[^1]. -Configuring a longer cool-down time can be a simple solution sometimes. +Configuring a longer cooldown time can be a simple solution sometimes. But this isn't possible if the max duration of background jobs or request processing phases is unknown. It also means that you have to compromise between cost efficiency and reliability of your service. Unikraft Cloud allows your app to temporarily disable scale-to-zero. -You can have both a short cool-down phase and reliable operation no matter how long things may take. +You can have both a short cooldown phase and reliable operation no matter how long things may take. To control scale-to-zero from within your app, instances on Unikraft Cloud provide a special file-based interface: | Path | Description | @@ -71,13 +70,13 @@ To control scale-to-zero from within your app, instances on Unikraft Cloud provi |`/uk/libukp/scale_to_zero_disable` | Allows to temporarily disable scale-to-zero. | The `scale_to_zero_disable` pseudo file keeps track of the count of concurrent disable requests. -If the count is `0`, scale-to-zero remains active[^1]. +If the count reaches `0`, scale-to-zero remains active[^2]. Any number larger than `0` means scale-to-zero is temporarily inactive. -Using a count instead of a boolean value helps many independent workers. +Using a count instead of a boolean value is helpful when you have many independent workers. Your app workers can disable scale-to-zero individually by incrementing and decrementing the count without having to synchronize. Reading the file returns the current count. -The value gets a prefix with an equals sign (that is, `=X` with `X` being the current count). +The system prefixes the value with an equals sign (that is, `=X` with `X` being the current count). Writing to the file modifies the count. You can use the following strings: @@ -95,9 +94,40 @@ Any attempt to set a count less than `0` or larger than `2^64` returns an `ERANG :::caution Writing to the file to disable scale-to-zero doesn't atomically disable scale-to-zero. There can be a short delay (a few milliseconds) until the Unikraft Cloud controller receives the changed value and makes scale-to-zero decisions accordingly. -Ensure you configure a cool-down time large enough to accommodate for potential signal delay. +Ensure you configure a cooldown time large enough to accommodate for potential signal delay. ::: +## Notification signal + +Before the platform suspends an instance, it can send a notification signal to the guest, giving it time to finish in-flight work before suspension begins. + +The platform delivers the notification over [vsock](https://man7.org/linux/man-pages/man7/vsock.7.html) on port `138`. +The platform sends the literal string `scaletozero` to the guest at the configured lead time before suspension. + +Your app can listen on vsock port `138` and use the signal to: +- Drain in-flight requests +- Flush write buffers +- Update any external state + +### Configuring the notification lead time + +Use the `notify_time_ms` field in the `scale_to_zero` configuration when creating or updating an instance to specify the number of milliseconds before the scaletozero event, when the platform should send the notification signal: + +```json title="POST /instances" +{ + "scale_to_zero": { + "cooldown_time_ms": 5000, + "notify_time_ms": 2000 + } +} +``` + +This sends the notification 2 seconds before the cooldown expires and suspension begins. + +:::note +`notify_time_ms` must be less than `cooldown_time_ms`. +If `notify_time_ms` is `0` (the default), the platform skips the notification and suspends the instance immediately when the cooldown expires, without informing the guest. +::: ## Stateful scale-to-zero @@ -105,16 +135,17 @@ Some apps have a long cold boot phase or experience large first response latency For example, to run just-in-time compilation and fill caches. Using stateful scale-to-zero can dramatically reduce the response time of your service. With stateful scale-to-zero Unikraft Cloud takes a snapshot of the instance state before putting it into standby. -When incoming network traffic triggers a wake-up, the snapshot gets loaded. -The instance resumes execution where it left off - with caches already warm. +When incoming network traffic triggers a wake-up, Unikraft Cloud loads the snapshot. +The instance resumes execution where it left off—with caches already warm. Stateful scale-to-zero can also enable scale-to-zero for apps that need to keep state for functional correctness. This works even if cold boot times are no concern. +If your app doesn't require maintaining state and has a satisfactorily short boot time, you might prefer to disable stateful scale-to-zero to save disk space. :::note The time to load an instance snapshot is constant and in the order of a few milliseconds. -This is what gets reported in the boot time metrics. -The actual memory contents gets loaded from the snapshot only at first access. +This is what the boot time metrics report. +The system loads the actual memory contents from the snapshot only at first access. This reduces response latency by loading only what's necessary to process the request. This means that the first few requests might take longer until all required memory comes back. @@ -122,9 +153,44 @@ The time to take an instance snapshot during scale-to-zero depends on the amount ::: +## Best practices + +### Cooldown configuration + +Choosing the optimal cooldown period depends on your app's start-up time and traffic patterns. +The default cooldown is 1 second, which works well for simple apps. +For larger apps with longer boot times, start with a conservative cooldown (for example, 5-10 seconds) and gradually lower it while monitoring behavior. + +If your app takes a long time to initialize, you might encounter issues where it scales down before serving the first request if the cooldown is too short. +Strategies to mitigate this include: +1. **Stateful Scale-to-Zero**: [Enabling stateful mode](#stateful-scale-to-zero) saves memory to disk, reducing wake-up time for complex apps. +2. **Startup Protection**: Use the [manual disable mechanism](#application-support-for-scale-to-zero) to temporarily turn off scale-to-zero during your app's boot sequence, then re-enable it once ready to serve. + +### Managing connectivity and state + +#### Continuous connections + +Apps using long-lived connections with frequent keep-alive messages (for example, WebSockets, HTTP/2, HMR) may prevent the instance from scaling to zero. +To allow the instance to sleep: +* Increase the interval between keep-alive messages. +* Use a more aggressive cooldown period if appropriate. +* If requiring continuous connectivity, consider disabling scale-to-zero for these workloads. + +#### Internal communication + +:::info +This feature is currently under active development and may change in future releases. +::: + +Currently, internal traffic via Private FQDNs doesn't trigger the scale-to-zero wake-up mechanism. +Services attempting to reach a standby instance via its Private FQDN will fail to wake it. +To ensure wake-on-request behavior for creating service-to-service communication, use the instance's **Public FQDN**. +Note that this also applies to the legacy [CLI tunnel](/docs/cli/kraft/tunnel) command, which relies on internal connectivity. + + ## Getting started -Millisecond scale-to-zero applies via a **label** in each of +You can enable millisecond scale-to-zero via a **label** in each of the subdirectories' `Kraftfile` or with the `--scale-to-zero` flag in relevant command-line tool subcommands. @@ -135,8 +201,8 @@ labels: ``` :::tip -The `cool-down` flag tells Unikraft Cloud how long the instance must be idle before scaling to zero. -In the examples, values that work for each of them appear so you don't have to worry about this label if you don't want to. +The `--scale-to-zero-cooldown` flag tells Unikraft Cloud how long the instance must be idle before scaling to zero. +The examples include appropriate values so you don't have to worry about this label if you don't want to. ::: :::tip @@ -145,12 +211,23 @@ You can disable scale-to-zero either by setting the label to `false`, or with th Since Unikraft Cloud has scale-to-zero on by default, all you need to do is to start an instance normally: -```bash title="" -git clone https://github.com/kraft-cloud/examples + + +```bash title="unikraft" +git clone https://github.com/unikraft-cloud/examples +cd examples/nginx/ +unikraft build . --output /nginx:latest +unikraft run --metro=fra -p 443:8080/http+tls -m 128MiB --image=/nginx:latest +``` + +```bash title="kraft" +git clone https://github.com/unikraft-cloud/examples cd examples/nginx/ -kraft cloud deploy -p 443:8080 . +kraft cloud deploy -p 443:8080 -M 256 . ``` + + This command will create the NGINX instance with scale-to-zero enabled: ```ansi title="" @@ -162,20 +239,28 @@ This command will create the NGINX instance with scale-to-zero enabled: ├─────────── url: https://twilight-gorilla-ui5b6kwt.fra.unikraft.app ├───────── image: nginx@sha256:19854a12fe97f138313cb9b4806828cae9cecf2d050077a0268d98129863f954 ├───── boot time: 19.81 ms - ├──────── memory: 128 MiB + ├──────── memory: 256 MiB ├─────── service: twilight-gorilla-ui5b6kwt ├── private fqdn: nginx-1a747.internal ├──── private ip: 172.16.6.1 └────────── args: /usr/bin/nginx -c /etc/nginx/nginx.conf ``` -Note that at first the system lists the status as `running` in the output of the `kraft cloud deploy` command. +Note that at first the system lists the status as `running` in the output of the deploy or run flow. Check the instance's status: -```bash title="" + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" kraft cloud instances list ``` + + You should see output like: ```ansi title="" @@ -184,14 +269,19 @@ nginx-1a747 twilight-gorilla-ui5b6kwt.fra.unikraft.app standby ``` Notice the state is now set to `standby`? -At first `kraft cloud deploy` sets the state to `running`, but then Unikraft Cloud puts the instance immediately to sleep (more accurately, it stopped it, but it keeps state to start it again when needed). +At first the deploy or run flow sets the state to `running`. +Unikraft Cloud then puts the instance to sleep (it stops it, but keeps state to start it again when needed). -You can also check that scale-to-zero gets enabled through the `kraft cloud scale` command: +You can also check that scale-to-zero gets enabled through the legacy CLI scale command: -```bash title="" + + +```bash title="kraft" kraft cloud scale get twilight-gorilla-ui5b6kw ``` + + which outputs: ```ansi title="" @@ -206,32 +296,32 @@ which outputs: policies: ``` -Note the `min size` (0) and `max size` (1) fields -- these mean that the service can scale from max 1 instance to min 0 instances, meaning that the system enables scale-to-zero. - - -### Testing scale-to-zero - -Now take this out for a spin. -``` - -Note the `min size` (0) and `max size` (1) fields -- these mean that the service can scale from max 1 instance to min 0 instances, meaning that scale-to-zero is enabled. +Note the `min size` (0) and `max size` (1) fields -- these mean that the service can scale between 0 and 1 instances, which enables scale-to-zero. ### Testing scale-to-zero -Try using `curl` or your browser to see scale-to-zero (well, scale to 1 in this case!) in action: +Try using `curl` or your browser to see scale-to-zero (well, scale-to-1 in this case!) in action: ```bash title="" curl https://twilight-gorilla-ui5b6kwt.fra.unikraft.app ``` You should get an NGINX response with no noticeable delay. -For fun, try to use the following command to see if you can catch the instance's `STATE` field changing from `standby` to `running` +For fun, try to use the following command to see if you can catch the instance's `STATE` field changing from `standby` to `running`: -```bash title="" + + +```bash title="unikraft" +unikraft instances list --watch +``` + +```bash title="kraft" watch --color -n 0.5 kraft cloud instance list ``` + + If you `curl` enough, you should see the `STATE` turn to a green `running` from time to time: @@ -243,17 +333,17 @@ nginx-1a747 twilight-gorilla-ui5b6kwt.fra.unikraft.app running :::note You can use scale-to-zero *in conjunction with* autoscale. This ensures that as you scale back down, if traffic dies, your last instance gets removed. -You're not charged for the service. -For more on autoscale please see the autoscale [guide](/docs/guides/features/autoscale) +You're not charged for the service in this state. +For more on autoscale please see the autoscale [guide](/features/autoscale). ::: ## Learn more -* The `kraft cloud` [command-line tool reference](/docs/cli/), and in particular the services and scale subcommands -* Unikraft Cloud's [REST API reference](/docs/api/v1), and in particular the section [on scale-to-zero](/docs/api/v1/instances/#scaletozero). +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1), and in particular the [scale-to-zero schema](/api/platform/v1/~schemas#instance-scale-to-zero). -[^1]: This is never the case for ports of your service that have the `http` handler set. -[^2]: If it's actually enabled depends on the instance configuration. +[^1]: This is never the case for ports of your service that have the `http` [handler](/platform/services#handlers) set. +[^2]: If it's actually enabled, depends on the instance configuration. diff --git a/pages/features/snapshots.mdx b/pages/features/snapshots.mdx index 706d37f1..1e7256e8 100644 --- a/pages/features/snapshots.mdx +++ b/pages/features/snapshots.mdx @@ -4,32 +4,45 @@ navigation_icon: pause --- Unikraft Cloud supports the ability to deploy **stateful** apps. -These apps can keep state across different [scale-to-zero](/docs/guides/features/scaletozero) incarnations. +These apps can keep state across different [scale-to-zero](/features/scale-to-zero) incarnations. This mechanism also reduces app init time. Heavy-weight apps take a while to get started when cold started (for example, Spring Boot, Ruby on Rails, etc.). -Taking a snapshot of the app memory and restoring it when the app starts again does this. +You can reduce this initialization time by taking a snapshot of the app memory and restoring it when the app starts again. ## Setting it up -The quickest way to set this up is via the `--scale-to-zero-stateful` flag of -the `kraft cloud deploy` or `kraft cloud instance create` commands, for example: +The quickest way to set this up is via the legacy CLI `--scale-to-zero-stateful` flag: -```bash + + +```bash title="kraft" kraft cloud deploy --metro fra --scale-to-zero-stateful . ``` + + As an alternative, you can add a `label` to your example's `Kraftfile` as follows: -```yaml +```yaml title="" labels: cloud.unikraft.v1.instances/scale_to_zero.stateful: "true" ``` -You can see an example of such a `Kraftfile` and label in the [Spring Boot -example](/docs/guides/springboot). +You can see an example of such a `Kraftfile` and label in the [Spring Boot example](/guides/springboot). + +Once deployed, you can check whether this mechanism works for your app via the CLI: + + + +```bash title="unikraft" +unikraft instances get +``` + +```bash title="kraft" +kraft cloud instance get +``` -Once deployed, you can check that this mechanism is actually enabled for your -app via the `kraft cloud instance get ` command: + ```ansi stateful: enabled @@ -38,19 +51,18 @@ stateful: enabled ## How it works -With stateful enabled, when you first deploy your app Unikraft Cloud will bring it up -normally and let it fully initialize (which may be fast, or take seconds or -minutes, depending on your app's init time). Unikraft Cloud will wait until the app's port -gets up, and then start the scale-to-zero process (assuming no traffic is coming -to that port). At this point, Unikraft Cloud will save the state of your app, and set the -app to standby (consuming no resources). +When you enable stateful mode and first deploy your app, Unikraft Cloud brings it up normally and lets it fully initialize. +This may be fast or take seconds or minutes, depending on your app's init time. +Unikraft Cloud waits until the app's exposed port becomes available, and then starts the scale-to-zero process. +This assumes no traffic is coming to that port. +At this point, Unikraft Cloud saves the state of your app and sets the app to standby (consuming no resources). -Next, when traffic arrives, Unikraft Cloud will bring up the app including its saved state, -ensuring statefulness across different scale-to-zero and scale to one cycles, -but also eliminating any long initialization times from heavyweight apps. +Next, when traffic arrives, Unikraft Cloud brings up the app including its saved state. +This ensures statefulness across scale-to-zero and scale-to-one cycles. +It also eliminates long initialization times from heavyweight apps. -## Learn More +## Learn more -* The `kraft cloud` [command-line tool reference](/docs/cli/), and in particular the `deploy` and `instance` subcommands. -* Unikraft Cloud's [REST API reference](/docs/api/v1), and in particular the section on [stateful scale-to-zero](/docs/api/v1/instances/#scaletozero_stateful). +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1), and in particular the [scale-to-zero schema](/api/platform/v1/~schemas#instance-scale-to-zero). diff --git a/pages/guides/bun.mdx b/pages/guides/bun.mdx deleted file mode 100644 index 18d18636..00000000 --- a/pages/guides/bun.mdx +++ /dev/null @@ -1,206 +0,0 @@ ---- -title: "Bun" ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a Bun app. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/bun` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/bun/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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 the app on Unikraft Cloud: - -```bash -kraft cloud deploy -p 443:3000 -M 512 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: bun-700mp - ├────────── uuid: e467a880-075c-41e0-97ac-88e3e938523e - ├───────── state: starting - ├──────── domain: https://quiet-pond-ao44imcg.fra.unikraft.app - ├───────── image: bun@sha256:dfcbee1efe0d8a1d43ab2dab70cf1cc5066bb1353aa1c528c745533d2cc33276 - ├──────── memory: 512 MiB - ├─────── service: quiet-pond-ao44imcg - ├── private fqdn: bun-700mp.internal - ├──── private ip: 172.16.3.3 - └────────── args: /usr/bin/bun run /usr/src/server.ts -``` - -In this case, the instance name is `bun-700mp` and the address is `https://quiet-pond-ao44imcg.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the Bun instance: - -```bash -curl https://quiet-pond-ao44imcg.fra.unikraft.app -``` - -```text -Hello, World! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` - -```text -NAME FQDN STATE STATUS IMAGE MEMORY ARGS BOOT TIME -bun-700mp quiet-pond-ao44imcg.fra.unikraft.app running since 3mins bun@sha256:dfcbee1efe0d8a1d43ab... 512 MiB /usr/bin/bun run /usr/src/server.ts 289.03 ms -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove bun-700mp -``` - -## 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 -* `server.ts`: the Bun server implementation - - - - Kratfile - Dockerfile - server.ts - - - ```yaml -spec: v0.6 - -name: bun - -runtime: base-compat:latest - -rootfs: ./Dockerfile - -cmd: ["/usr/bin/bun," "run," "/usr/src/server.ts"] - ``` - - - ```dockerfile -FROM oven/bun:1 AS bun - -RUN set -xe; \ - mv /usr/local/bin/bun /usr/bin/bun; \ - mkdir /self; \ - ln -sfn /usr/bin/bun /self/exe - -FROM alpine:3 AS sys - -RUN set -xe; \ - mkdir -p /target/etc; \ - mkdir -p /blank; \ - apk --no-cache add \ - ca-certificates \ - tzdata \ - ; \ - update-ca-certificates; \ - ln -sf /usr/share/zoneinfo/Etc/UTC /target/etc/localtime; \ - echo "Etc/UTC" > /target/etc/timezone; - -FROM scratch - -COPY --from=bun /self /proc/self -COPY --from=bun /usr/bin/bun /usr/bin/bun - -COPY --from=bun /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=bun /lib/x86_64-linux-gnu/libdl.so.2 /lib/x86_64-linux-gnu/libdl.so.2 -COPY --from=bun /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=bun /lib/x86_64-linux-gnu/libpthread.so.0 /lib/x86_64-linux-gnu/libpthread.so.0 -COPY --from=bun /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 - -COPY --from=sys /target/etc /etc -COPY --from=sys /usr/share/zoneinfo/UTC /usr/share/zoneinfo/UTC -COPY --from=sys /usr/share/zoneinfo/Etc/UTC /usr/share/zoneinfo/Etc/UTC -COPY --from=sys /etc/ssl/certs/ca-certificates.crt /etc/ssl/certs/ca-certificates.crt -COPY --from=sys /blank /tmp - -COPY ./server.ts /usr/src/server.ts - ``` - - - ```dockerfile -const port = process.env.PORT || 3000; - -console.log( - `Launching Bun HTTP server on port: ${port}, url: http://0.0.0.0:${port} 🚀` -); - -Bun.serve({ - port: port, - fetch(_request) { - return new Response("Hello, World!\n"); - }, -}); - ``` - - - -Lines in the `Kraftfile` have the following roles: - -* `spec: v0.6`: The current `Kraftfile` specification version is `0.6`. - -* `runtime: base-compat:latest`: The kernel to use. - -* `rootfs: ./Dockerfile`: Build the app root filesystem using the `Dockerfile`. - -* `cmd: ["/usr/bin/bun", "run", "/usr/src/server.ts"]`: Use `/usr/bin/bun run /usr/src/server.ts` as the starting command of the instance. - -Lines in the `Dockerfile` have the following roles: - -* `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). - -* `COPY ...`: Copy required files to the app filesystem: the `bun` binary executable, libraries, configuration files, the `/usr/src/server.ts` implementation. - -* `RUN ...`: Run specific commands to generate or to prepare the filesystem contents. - -The following options are available for customizing the app: - -* If you only update the implementation in the `server.ts` source file, you don't need to make any other changes. - -* 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 replace `server.ts` with a different source file, update the `cmd` line in the `Kraftfile` and replace `/usr/src/server.ts` with the path to your new source file. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/caddy.mdx b/pages/guides/caddy.mdx deleted file mode 100644 index 69f1dac3..00000000 --- a/pages/guides/caddy.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Caddy ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This example uses [`Caddy`](https://caddyserver.com/), one of the most popular web servers. -Caddy can be used with Unikraft / Unikraft Cloud to serve static web content. - -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/caddy/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/caddy/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:2015 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: caddy-vhf4m - ├────────── uuid: db624eff-4739-4500-873c-f7c58e4eefd7 - ├───────── state: running - ├─────────── url: https://frosty-sky-vz8kwsmb.fra.unikraft.app - ├───────── image: caddy@sha256:25df97e3c43147c683f31dd062d0fa75122358b596de5804ca246c4e8613dd56 - ├───── boot time: 20.18ms - ├──────── memory: 128 MiB - ├─────── service: frosty-sky-vz8kwsm - ├── private fqdn: caddy-vhf4m.internal - ├──── private ip: 172.16.6.2 - └────────── args: /usr/bin/caddy run --config /etc/caddy/Caddyfile -``` - -In this case, the instance name is `caddy-vhf4m` and the address is `https://frosty-sky-vz8kwsmb.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of Caddy. - -```bash -curl https://frosty-sky-vz8kwsmb.fra.unikraft.app -``` -```text -Hello World! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -caddy-vhf4m frosty-sky-vz8kwsmb.fra.unikraft.app running 1 minute ago caddy@sha25:25df97e3c43147c683f... 128 MiB /usr/bin/caddy run --config /e... 20180us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove caddy-vhf4m -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `Kraftfile`: the Unikraft Cloud specification -* `rootfs/var/www/index.html`: the index page of the content served -* `rootfs/etc/caddy/Caddyfile`: the Caddy configuration file - - - - ```yaml - spec: v0.6 - - runtime: caddy:latest - - rootfs: ./rootfs - ``` - - - ```html -Hello, world! - ``` - - - ```text -:2015 - -root * /var/www - -encode gzip - -templates - -file_server - ``` - - - -Update the contents of the `rootfs/var/www` directory to serve different static web content. -For example, you could change the contents of `rootfs/var/www/index.html` to: - -```html - - - -Hello - - -

Hello, World!

- - -``` - -After re-deploying the Caddy image on Unikraft Cloud, using `curl` or a browser to query it will present the new page contents. - -You can generate the static web content in `rootfs/var/www/` offline with tools such as [`Jekyll`](https://jekyllrb.com/) or [`Hugo`](https://gohugo.io/). - -If required, you can also customize the configuration of Caddy in `rootfs/etc/caddy/Caddyfile`. -You can set a new webroot (different than `rootfs`), or a different internal port, or a different index page, etc. - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/caddy2.7-go1.21.mdx b/pages/guides/caddy2.7-go1.21.mdx new file mode 100644 index 00000000..d7b581e3 --- /dev/null +++ b/pages/guides/caddy2.7-go1.21.mdx @@ -0,0 +1,211 @@ +--- +title: "Caddy" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This example uses [`Caddy`](https://caddyserver.com/), one of the most popular web servers. +Caddy 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](/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/caddy2.7-go1.21/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/caddy2.7-go1.21/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /caddy27-go121:latest +unikraft run --scale-to-zero policy=on,cooldown-time=1000 --metro fra -p 443:2015/http+tls -m 256M --image /caddy27-go121:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero on --scale-to-zero-cooldown 1s -p 443:2015/http+tls -M 256Mi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: caddy27-go121-vhf4m +uuid: db624eff-4739-4500-873c-f7c58e4eefd7 +state: starting +image: /caddy27-go121 +resources: + memory: 256MiB + vcpus: 1 +service: + uuid: 085cfc91-34d2-0e4d-5f19-fe67832f16a8 + name: frosty-sky-vz8kwsmb + domains: + - fqdn: frosty-sky-vz8kwsmb.fra.unikraft.app +networks: +- uuid: a72ea1ab-9686-c5b5-a8c2-fd58922f30f3 + private-ip: 10.0.6.2 + mac: 12:b0:a9:b2:fd:13 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: caddy27-go121-vhf4m + ├───────── uuid: db624eff-4739-4500-873c-f7c58e4eefd7 + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://frosty-sky-vz8kwsmb.fra.unikraft.app + ├──────── image: oci://unikraft.io//caddy27-go121@sha256:25df97e3c43147c683f31dd062d0fa75122358b596de5804ca246c4e8613dd56 + ├─────── memory: 256 MiB + ├────── service: frosty-sky-vz8kwsmb + ├─ private fqdn: caddy27-go121-vhf4m.internal + └─── private ip: 10.0.6.2 +``` + + + +In this case, the instance name is `caddy27-go121-vhf4m` and the address is `https://frosty-sky-vz8kwsmb.fra.unikraft.app`. +They're different for each run. + +Use `curl` to query the Unikraft Cloud instance of Caddy. + +```bash +curl https://frosty-sky-vz8kwsmb.fra.unikraft.app +``` + +```text +Hello World! +``` + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra caddy27-go121-vhf4m running /caddy27-go121 256MiB 1 frosty-sky-vz8kwsmb.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +caddy27-go121-vhf4m frosty-sky-vz8kwsmb.fra.unikraft.app running 1 minute ago oci://unikraft.io//caddy27-go121@sha256:... 256 MiB 1 20.18 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete caddy27-go121-vhf4m +``` + +```bash title="kraft" +kraft cloud instance remove caddy27-go121-vhf4m +``` + + + +## Customize your app + +To customize the app, update the files in the repository, listed below: + +* `Kraftfile`: the Unikraft Cloud specification +* `rootfs/var/www/index.html`: the index page of the content served +* `rootfs/etc/caddy/Caddyfile`: the Caddy configuration file + +Update the contents of the `rootfs/var/www` directory to serve different static web content. +For example, you could change the contents of `rootfs/var/www/index.html` to: + +```html + + + +Hello + + +

Hello, World!

+ + +``` + +After re-deploying the Caddy image on Unikraft Cloud, using `curl` or a browser to query it will present the new page contents. + +You can generate the static web content in `rootfs/var/www/` offline with tools such as [`Jekyll`](https://jekyllrb.com/) or [`Hugo`](https://gohugo.io/). + +If required, you can also customize the configuration of Caddy in `rootfs/etc/caddy/Caddyfile`. +You can set a new webroot (different than `rootfs`), or a different internal port, or a different index page, etc. + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/cpp-boost.mdx b/pages/guides/cpp-boost.mdx deleted file mode 100644 index 02da7c26..00000000 --- a/pages/guides/cpp-boost.mdx +++ /dev/null @@ -1,286 +0,0 @@ ---- -title: C++ Boost ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a C++-based HTTP web server using the [Boost](https://www.boost.org/) libraries. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -2. Clone the [example repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-cpp-boost/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-cpp-boost/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# Set Unikraft Cloud access token -export UKC_TOKEN=token -# Set metro to Frankfurt, DE -export UKC_METRO=fra -``` -x -When done, invoke the following command to deploy this app on Unikraft Cloud: - -```bash -kraft cloud deploy -p 443:8080 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-cpp-boost-rae7s - ├────────── uuid: 5a9886fa-f8a3-4860-afcf-d5eb13fdc38d - ├───────── state: running - ├─────────── url: https://red-snow-3bn7bzc8.fra.unikraft.app - ├───────── image: http-cpp-boost@sha256:61cf86b89fed46351af53689e27189315e466576475f61c7240bf17644613489 - ├───── boot time: 15.00 ms - ├──────── memory: 128 MiB - ├─────── service: red-snow-3bn7bzc8 - ├── private fqdn: http-cpp-boost-rae7s.internal - ├──── private ip: 172.16.6.4 - └────────── args: /http_server -``` - -In this case, the instance name is `http-cpp-boost-rae7s` and the address is `https://red-snow-3bn7bzc8.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the C++ Boost HTTP web server: - -```bash -curl https://red-snow-3bn7bzc8.fra.unikraft.app -``` -```text -Hello, World! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-cpp-boost-rae7s red-snow-3bn7bzc8.fra.unikraft.app running 1 minute ago http-cpp-boost@sha256:61cf86b89fed46351af53689e27189315e... 128 MiB /http_server 15000us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove http-cpp-boost-rae7s -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `http_server.cpp`: the C++ HTTP server -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```cpp -/** - * Compile with: g++ async_http_server.cpp -o async_http_server -lboost_system -lboost_thread -lpthread - * - * https://gist.github.com/danilogr/990efa4ab3b5bce29b883b931ac55507 - */ - -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -using boost::asio::ip::tcp; - -class HttpServer; // forward declaration - -class Request : public boost::enable_shared_from_this -{ - // member variables - HttpServer& server; - boost::asio::streambuf request; - boost::asio::streambuf response; - - void afterRead(const boost::system::error_code& ec, std::size_t bytes_transferred) - { - // done reading, writes answer (yes, we ignore the request); - std::ostream res_stream(&response); - - boost::system::error_code err; - res_stream << "HTTP/1.0 200 OK\r\n" - << "Content-Type: text/html; charset=UTF-8\r\n" - << "Content-Length: 14\r\n" - << "\r\n" - << "Hello, World!\n" << "\r\n"; - boost::asio::async_write(*socket, response, boost::bind(&Request::afterWrite, shared_from_this(), err, bytes_transferred)); - } - - void afterWrite(const boost::system::error_code& ec, std::size_t bytes_transferred) - { - // done writing, closing connection - socket->close(); - } - - public: - - boost::shared_ptr socket; - Request(HttpServer& server); - void answer() - { - if (!socket) return; - - // reads request till the end - boost::system::error_code err; - boost::asio::async_read_until(*socket, request, "\r\n\r\n", - boost::bind(&Request::afterRead, shared_from_this(), err, 0)); - } -}; - - -class HttpServer -{ - public: - - HttpServer(unsigned int port) : acceptor(io_service, tcp::endpoint(tcp::v4(), port)) {} - ~HttpServer() { if (sThread) sThread->join(); } - - void Run() - { - sThread.reset(new boost::thread(boost::bind(&HttpServer::thread_main, this))); - } - - boost::asio::io_service io_service; - - private: - tcp::acceptor acceptor; - boost::shared_ptr sThread; - - void thread_main() - { - // adds some work to the io_service - start_accept(); - io_service.run(); - } - void start_accept() - { - boost::system::error_code err; - boost::shared_ptr req (new Request(*this)); - acceptor.async_accept(*req->socket, - boost::bind(&HttpServer::handle_accept, this, req, err)); - } - - void handle_accept(boost::shared_ptr req, const boost::system::error_code& error) - { - if (!error) { req->answer(); } - start_accept(); - } -}; - -Request::Request(HttpServer& server): server(server) -{ - socket.reset(new tcp::socket(server.io_service)); -} - - -int main() -{ - HttpServer server(8080); - server.Run(); -} -} - ``` - - - ```yaml -spec: v0.6 - -runtime: base:latest - -rootfs: ./Dockerfile - -cmd: ["/http_server"] - ``` - - - ```dockerfile -FROM --platform=linux/x86_64 debian:bookworm AS build - -RUN set -xe ; \ - apt -yqq update ; \ - apt -yqq install build-essential ; \ - apt -yqq install libboost-all-dev - -WORKDIR /src - -COPY ./http_server.cpp /src/http_server.cpp - -RUN set -xe; \ - g++ \ - -Wall -Wextra \ - -fPIC -pie \ - -o /http_server http_server.cpp \ - -lboost_system -lboost_thread -lpthread - -FROM scratch - -# System/C++ libraries -COPY --from=build /lib/x86_64-linux-gnu/libboost_thread.so.1.74.0 /lib/x86_64-linux-gnu/libboost_thread.so.1.74.0 -COPY --from=build /lib/x86_64-linux-gnu/libstdc++.so.6 /lib/x86_64-linux-gnu/libstdc++.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 - -# C++ HTTP server -COPY --from=build /http_server /http_server - ``` - - - -Lines in the `Kraftfile` have the following roles: - -* `spec: v0.6`: The current `Kraftfile` specification version is `0.6`. - -* `runtime: base`: The Unikraft runtime kernel to use is its base one. - -* `rootfs: ./Dockerfile`: Build the app root filesystem using the `Dockerfile`. - -* `cmd: ["/http_server"]`: Use `/http_server` as the starting command of the instance. - -Lines in the `Dockerfile` have the following roles: - -* `COPY ./http_server.cpp /src/http_server.cpp`: Copy the server implementation file (`http_server.cpp`) in the Docker filesystem (in `/src/http_server.cpp`). - -The following options are available for customizing the app: - -* If you only update the implementation in the `http_server.cpp` source file, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/cpp.mdx b/pages/guides/cpp.mdx deleted file mode 100644 index ac03cf39..00000000 --- a/pages/guides/cpp.mdx +++ /dev/null @@ -1,247 +0,0 @@ ---- -title: C++ ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a simple C++-based HTTP web server. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -2. Clone the [example repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-cpp/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-cpp/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:8080 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-cpp-jzbuo - ├────────── uuid: b8e015fd-d006-49d5-849e-3fd497c9159a - ├───────── state: running - ├─────────── url: https://throbbing-wave-grxjih4t.fra.unikraft.app - ├───────── image: http-cpp@sha256:a58873987104b52c13b79168a2e2f1a81876ba6efacd6dbc98e996afe5c09699 - ├───── boot time: 15.61 ms - ├──────── memory: 128 MiB - ├─────── service: throbbing-wave-grxjih4t - ├── private fqdn: http-cpp-jzbuo.internal - ├──── private ip: 172.16.6.5 - └────────── args: /http_server -``` - -In this case, the instance name is ` http-cpp-jzbuo` and the address is `https://throbbing-wave-grxjih4t.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the C++ HTTP web server: - -```bash -curl https://throbbing-wave-grxjih4t.fra.unikraft.app -``` -```text -Hello, World! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-cpp-jzbuo throbbing-wave-grxjih4t.fra.unikraft.app running 1 minute ago http-cpp@sha256:a58873987104b52c13b79168a2e2f1a81876ba6efac... 128 MiB /http_server 15614us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove http-cpp-jzbuo -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `http_server.cpp`: the actual C++ HTTP server -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```cpp - /* SPDX-License-Identifier: BSD-3-Clause */ -/* - * Copyright (c) 2023, Unikraft GmbH and the Unikraft Authors. - */ - -#include -#include -#include -#include -#include -#include - -#define LISTEN_PORT 8080 - -static std::string reply = "HTTP/1.1 200 OK\r\n" \ - "Content-Type: text/html\r\n" \ - "Content-Length: 14\r\n" \ - "Connection: close\r\n" \ - "\r\n" \ - "Hello, World!\n"; - -#define BUFLEN 2048 -static char recvbuf[BUFLEN]; - -int main(int argc __attribute__((unused)), - char *argv[] __attribute__((unused))) -{ - int rc = 0; - int srv, client; - ssize_t n; - struct sockaddr_in srv_addr; - - srv = socket(AF_INET, SOCK_STREAM, 0); - if (srv < 0) { - std::cerr << "Failed to create socket: " << errno << std::endl; - goto out; - } - - srv_addr.sin_family = AF_INET; - srv_addr.sin_addr.s_addr = INADDR_ANY; - srv_addr.sin_port = htons(LISTEN_PORT); - - rc = bind(srv, (struct sockaddr *) &srv_addr, sizeof(srv_addr)); - if (rc < 0) { - std::cerr << "Failed to bind socket: " << errno << std::endl; - goto out; - } - - /* Accept one simultaneous connection */ - rc = listen(srv, 1); - if (rc < 0) { - std::cerr << "Failed to listen on socket: " << errno << std::endl; - goto out; - } - - std::cout << "Listening on port " << LISTEN_PORT << std::endl; - while (1) { - client = accept(srv, NULL, 0); - if (client < 0) { - std::cerr << "Failed to accept incoming connection: " << errno << std::endl; - goto out; - } - - /* Receive some bytes (ignore errors) */ - read(client, recvbuf, BUFLEN); - - /* Send reply */ - n = write(client, reply.c_str(), reply.length()); - if (n < 0) - std::cerr << "Failed to send reply" << std::endl; - else - std::cout << "Sent a reply" << std::endl; - - /* Close connection */ - close(client); - } - -out: - return rc; -} - ``` - - - ```yaml - spec: v0.6 - - runtime: base:latest - - rootfs: ./Dockerfile - - cmd: ["/http_server"] - ``` - - - ```dockerfile - FROM --platform=linux/x86_64 gcc:13.2.0-bookworm AS build - -WORKDIR /src - -COPY ./http_server.cpp /src/http_server.cpp - -RUN set -xe; \ - g++ \ - -Wall -Wextra \ - -fPIC -pie \ - -o /http_server http_server.cpp - -FROM scratch - -# System/C++ libraries -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/ -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /usr/local/lib64/libgcc_s.so.1 /usr/local/lib64/libgcc_s.so.1 -COPY --from=build /usr/local/lib64/libstdc++.so.6 /usr/local/lib64/libstdc++.so.6 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 -COPY --from=build /etc/ld.so.cache /etc/ld.so.cache - -# C++ HTTP server -COPY --from=build /http_server /http_server - ``` - - - -Lines in the `Kraftfile` have the following roles: - -* `spec: v0.6`: The current `Kraftfile` specification version is `0.6`. - -* `runtime: base`: The Unikraft runtime kernel to use is its base one. - -* `rootfs: ./Dockerfile`: Build the app root filesystem using the `Dockerfile`. - -* `cmd: ["/http_server"]`: Use `/http_server` as the starting command of the instance. - -Lines in the `Dockerfile` have the following roles: - -* `FROM --platform=linux/x86_64 gcc:13.2.0-bookworm AS build`: Build the filesystem from the `bookworm gcc` container image, to [create a base image](https://docs.docker.com/build/building/base-images/). - -* `COPY ./http_server.cpp /src/http_server.cpp`: Copy the server implementation file (`http_server.cpp`) in the Docker filesystem (in `/src/http_server.cpp`). - -The following options are available for customizing the app: - -* If you only update the implementation in the `http_server.cpp` source file, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/debian-ssh.mdx b/pages/guides/debian-ssh.mdx new file mode 100644 index 00000000..85ac063b --- /dev/null +++ b/pages/guides/debian-ssh.mdx @@ -0,0 +1,185 @@ +--- +title: "Debian SSH server" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This guide explains how to create and deploy a Debian app with SSH enabled. +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/debian-ssh` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/debian-ssh/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /debian-ssh:latest +unikraft run --scale-to-zero policy=off --metro fra -p 2222:2222/tls -m 1G -e PUBKEY="...." --image /debian-ssh:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero off -p 2222:2222/tls -M 1Gi -e PUBKEY="...." . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: debian-ssh-2uwg5 +uuid: b3d158c5-fb52-4685-a76b-2497973308dc +state: starting +image: /debian-ssh +resources: + memory: 1024MiB + vcpus: 1 +service: + uuid: 5771bead-c045-52fa-1f89-5bc4f1cf3c38 + name: nameless-cherry-sw2e9ul2 + domains: + - fqdn: nameless-cherry-sw2e9ul2.fra.unikraft.app +networks: +- uuid: 9d76ad3f-2149-f0af-d77f-76daba253d33 + private-ip: 10.0.0.109 + mac: 12:b0:1d:bd:54:f6 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: debian-ssh-2uwg5 + ├───────── uuid: b3d158c5-fb52-4685-a76b-2497973308dc + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://nameless-cherry-sw2e9ul2.fra.unikraft.app + ├──────── image: oci://unikraft.io//debian-ssh@sha256:2442b4d5e078e7bc9ccd887fac65623511551592315d341a219f34a2c6628949 + ├─────── memory: 1024 MiB + ├────── service: nameless-cherry-sw2e9ul2 + ├─ private fqdn: debian-ssh-2uwg5.internal + └─── private ip: 10.0.0.109 +``` + + + +In this case, the instance name is `debian-ssh-2uwg5` and the address is `nameless-cherry-sw2e9ul2.fra.unikraft.app`. +They're different for each run. + +You need to set up a tunnel that handles the TLS connection to the Unikraft Cloud instance. +This way, you have a non-TLS port that your SSH client can connect to: + +```bash +socat TCP-LISTEN:2222,reuseaddr,fork OPENSSL:nameless-cherry-sw2e9ul2.fra.unikraft.app:2222,verify=0 +``` + +Then connect to the instance via SSH using: + +```bash +ssh -l root localhost -p 2222 +``` + +You might see warnings like `REMOTE HOST IDENTIFICATION HAS CHANGED`. +This is normal if you have set up tunnels to connect with SSH on `localhost`, so don't worry. + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra debian-ssh-2uwg5 running /debian-ssh 1.0GiB 1 nameless-cherry-sw2e9ul2.fra.unikraft… 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +debian-ssh-2uwg5 nameless-cherry-sw2e9ul2.fra.unikraft.app running since 5mins oci://unikraft.io//debian-ssh@sha256:... 1.0 GiB 1 217.26 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete debian-ssh-2uwg5 +``` + +```bash title="kraft" +kraft cloud instance remove debian-ssh-2uwg5 +``` + + + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/django.mdx b/pages/guides/django.mdx deleted file mode 100644 index 2b3fba56..00000000 --- a/pages/guides/django.mdx +++ /dev/null @@ -1,302 +0,0 @@ ---- -title: Django ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a Python Django web app. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-python3.12-django5.0/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-python3.12-django5.0/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:80 -M 512 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-python312-django50-vt56c - ├────────── uuid: d8469447-fdf6-4caf-9fea-494218ca6f72 - ├───────── state: running - ├─────────── url: https://dawn-sound-n5wrkxi2.fra.unikraft.app - ├───────── image: http-python312-django50@sha256:221666d414299aff54dbf10020b3d540270ee0c5907c1c6a728ca254ce8b0e50 - ├───── boot time: 80.32 ms - ├──────── memory: 512 MiB - ├─────── service: dawn-sound-n5wrkxi2 - ├── private fqdn: http-python312-django50-vt56c.internal - ├──── private ip: 172.16.6.5 - └────────── args: /usr/bin/python3 /app/main.py -``` - -In this case, the instance name is `http-python312-django50-vt56c` and the address is `https://dawn-sound-n5wrkxi2.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the Django web app server: - -```bash -curl https://dawn-sound-n5wrkxi2.fra.unikraft.app -``` -```html - - - - - - - The install worked successfully! Congratulations! - - - - -

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.

- - - ``` - - - ```text - worker_processes 1; - daemon off; - master_process off; - user root root; - - events { - worker_connections 64; - } - - http { - include mime.types; - default_type app/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; - } - } - ``` - - - 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: @@ -194,8 +199,17 @@ You can set a new webroot (different than `wwwroot`), or a different internal po Use the `--help` option for detailed information on using Unikraft Cloud: -```bash + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" kraft cloud --help ``` -Or visit the [CLI Reference](/docs/cli). + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/node-playwright-chromium.mdx b/pages/guides/node-playwright-chromium.mdx new file mode 100644 index 00000000..e18ec561 --- /dev/null +++ b/pages/guides/node-playwright-chromium.mdx @@ -0,0 +1,187 @@ +--- +title: "Playwright (Chromium) with Node.js" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +[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](/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/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/node-playwright-chromium/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /node-playwright-chromium:latest +unikraft run --scale-to-zero policy=idle,cooldown-time=1000,stateful=true --metro fra -p 443:8080/tls+http -m 4G --image /node-playwright-chromium:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero idle --scale-to-zero-stateful --scale-to-zero-cooldown 1s -p 443:8080/tls+http -M 4Gi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: node-playwright-chromium-v5f8p +uuid: a1b2c3d4-e5f6-7a8b-9c0d-a1b2c3d4e5f6 +state: starting +image: /node-playwright-chromium +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 +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: node-playwright-chromium-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@sha256:7c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d + ├─────── memory: 4096 MiB + ├────── service: gentle-moon-cx2jh5wd + ├─ private fqdn: node-playwright-chromium-v5f8p.internal + └─── private ip: 10.0.4.3 +``` + + + +In this case, the instance name is `node-playwright-chromium-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: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node-playwright-chromium-v5f8p running /node-playwright-chromium 4096MiB 1 gentle-moon-cx2jh5wd.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +node-playwright-chromium-v5f8p gentle-moon-cx2jh5wd.fra.unikraft.app running 1 minute ago oci://unikraft.io//node-playwright-chromium@sha256:... 4 GiB 1 300.21 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete +``` + +```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: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/node-playwright-firefox.mdx b/pages/guides/node-playwright-firefox.mdx new file mode 100644 index 00000000..9d1ffd0a --- /dev/null +++ b/pages/guides/node-playwright-firefox.mdx @@ -0,0 +1,187 @@ +--- +title: "Playwright (Firefox) with Node.js" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +[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](/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/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/node-playwright-firefox/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /node-playwright-firefox:latest +unikraft run --scale-to-zero policy=idle,cooldown-time=1000,stateful=true --metro fra -p 443:8080/tls+http -m 4G --image /node-playwright-firefox:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero idle --scale-to-zero-stateful --scale-to-zero-cooldown 1s -p 443:8080/tls+http -M 4Gi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: node-playwright-firefox-q3m9k +uuid: d4e5f6a7-b8c9-0d1e-2f3a-d4e5f6a7b8c9 +state: starting +image: /node-playwright-firefox +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 +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: node-playwright-firefox-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@sha256:8d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e + ├─────── memory: 4096 MiB + ├────── service: bright-lake-dh6xp2sq + ├─ private fqdn: node-playwright-firefox-q3m9k.internal + └─── private ip: 10.0.5.3 +``` + + + +In this case, the instance name is `node-playwright-firefox-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: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node-playwright-firefox-q3m9k running /node-playwright-firefox 4096MiB 1 bright-lake-dh6xp2sq.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +node-playwright-firefox-q3m9k bright-lake-dh6xp2sq.fra.unikraft.app running 1 minute ago oci://unikraft.io//node-playwright-firefox@sha256:... 4 GiB 1 350.87 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete +``` + +```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: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/node-playwright-webkit.mdx b/pages/guides/node-playwright-webkit.mdx new file mode 100644 index 00000000..0cc74003 --- /dev/null +++ b/pages/guides/node-playwright-webkit.mdx @@ -0,0 +1,187 @@ +--- +title: "Playwright (WebKit) with Node.js" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +[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](/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/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/node-playwright-webkit/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /node-playwright-webkit:latest +unikraft run --scale-to-zero policy=idle,cooldown-time=1000,stateful=true --metro fra -p 443:8080/tls+http -m 4G --image /node-playwright-webkit:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero idle --scale-to-zero-stateful --scale-to-zero-cooldown 1s -p 443:8080/tls+http -M 4Gi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: node-playwright-webkit-t7r2j +uuid: a2b3c4d5-e6f7-8a9b-0c1d-a2b3c4d5e6f7 +state: starting +image: /node-playwright-webkit +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 +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: node-playwright-webkit-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@sha256:9e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f + ├─────── memory: 4096 MiB + ├────── service: silent-fog-er8np3fb + ├─ private fqdn: node-playwright-webkit-t7r2j.internal + └─── private ip: 10.0.6.4 +``` + + + +In this case, the instance name is `node-playwright-webkit-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: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node-playwright-webkit-t7r2j running /node-playwright-webkit 4096MiB 1 silent-fog-er8np3fb.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +node-playwright-webkit-t7r2j silent-fog-er8np3fb.fra.unikraft.app running 1 minute ago oci://unikraft.io//node-playwright-webkit@sha256:... 4 GiB 1 2.94 s +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete +``` + +```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: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/node.mdx b/pages/guides/node.mdx deleted file mode 100644 index 8f9107af..00000000 --- a/pages/guides/node.mdx +++ /dev/null @@ -1,288 +0,0 @@ ---- -title: Node ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a simple Node-based HTTP web server. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-node21/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-node21/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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 the app on Unikraft Cloud: - -```bash -kraft cloud deploy -p 443:8080 -M 256 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-node21-ubl8g - ├────────── uuid: 6985f8e7-72c2-4726-941c-3c568a83722e - ├───────── state: running - ├─────────── url: https://ancient-haze-sd3wwi0x.fra.unikraft.app - ├───────── image: http-node21@sha256:de174e3703c79a048f0af52344c373296b55f3ca2b96cd29e16c1f014cefd232 - ├───── boot time: 41.65 ms - ├──────── memory: 256 MiB - ├─────── service: ancient-haze-sd3wwi0x - ├── private fqdn: http-node21-ubl8g.internal - ├──── private ip: 172.16.3.3 - └────────── args: /usr/bin/node /usr/src/server.js -``` - -In this case, the instance name is `http-node21-ubl8g` and the address is `https://ancient-haze-sd3wwi0x.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the Node-based HTTP web server: - -```bash -curl https://ancient-haze-sd3wwi0x.fra.unikraft.app -``` -```text -Hello, World! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-node21-ubl8g ancient-haze-sd3wwi0x.fra.unikraft.app running 50 seconds ago http-node21@sha256:de174e3703c79a04... 256 MiB /usr/bin/node /usr/src/server.js 31654us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove http-node21-ubl8g -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `server.js`: the actual Node HTTP server -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```js - const http = require('http'); // Loads the http module - - http.createServer((request, response) => { - - // 1. Tell the browser everything is OK (Status code 200), and the data is in plain text - response.writeHead(200, { - 'Content-Type': 'text/plain' - }); - - // 2. Write the announced text to the body of the page - response.write('Hello, World!\n'); - - // 3. Tell the server that all of the response headers and body have been sent - response.end(); - - }).listen(8080); // 4. Tells the server what port to be on - ``` - - - ```yaml - spec: v0.6 - - runtime: node:21 - - rootfs: ./Dockerfile - - cmd: ["/usr/bin/node", "/usr/src/server.js"] - ``` - - - ```dockerfile - FROM scratch - - # Simple Node HTTP server - COPY ./server.js /usr/src/server.js - ``` - - - -Lines in the `Kraftfile` have the following roles: - -* `spec: v0.6`: The current `Kraftfile` specification version is `0.6`. - -* `runtime: node:21`: The Unikraft runtime kernel to use is Node 21. - -* `rootfs: ./Dockerfile`: Build the app root filesystem using the `Dockerfile`. - -* `cmd: ["/usr/bin/node", "/usr/src/server.js"]`: Use `/usr/bin/node /usr/src/server.js` as the starting command of the instance. - -Lines in the `Dockerfile` have the following roles: - -* `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). - -* `COPY ./server.js /usr/src/server.js`: Copy the server implementation file (`server.js`) in the Docker filesystem (in `/usr/src/server.js`). - -The following options are available for customizing the app: - -* If you only update the implementation in the `server.js` source file, you don't need to make any other changes. - -* 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 replace `server.js` with a different source file, update the `cmd` line in the `Kraftfile` and replace `/usr/src/server.js` with the path to your new source file. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - This includes the use of Node frameworks and the use of [`npm`](https://www.npmjs.com/), as shown in the next section. - -## Using `npm` - -[`npm`](https://www.npmjs.com/) is a package manager for Node. -It's used to install dependencies for Node apps. -`npm` uses a `package.json` file to list required dependencies (with versions). - -The [`node21-expressjs`](https://github.com/kraftcloud/examples/tree/main/node21-expressjs) example in the [`examples`](https://github.com/kraftcloud/examples) repository details the use of `npm` to deploy an app using the [ExpressJS](https://expressjs.com/) framework on Unikraft Cloud. -Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `node21-expressjs` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/node21-expressjs/ -``` - -Run the command below to deploy the app on Unikraft Cloud: - -```bash -kraft cloud deploy -p 443:3000 -M 256 . -``` - -Differences from the `http-node21` app are also the steps required to create an `npm`-based app: - -1. Add the `package.json` file used by `npm`. - -1. Add framework-specific source files. - In this case, this means `app/index.js`. - -1. Update the `Dockerfile` to: - - 1. `COPY` the local files. - - 1. `RUN` the `npm install` command to install dependencies. - - 1. `COPY` of the resulting and required files (`node_modules/` and `app/index.js`) in the app filesystem, using the [`scratch` container](https://hub.docker.com/_/scratch/). - -The following lists the files: - - - - ```js - const express = require('express') - const app = express() - const port = 3000 - - app.get('/', (req, res) => { - res.send('Hello, World!\n') - }) - - app.listen(port, () => { - console.log(`Example app listening on port ${port}`) - }) - ``` - - - ```json - { - "dependencies": { - "express": "^4.18.2" - } - } - ``` - - - ```yaml - spec: v0.6 - - runtime: node:21 - - rootfs: ./Dockerfile - - cmd: ["/usr/bin/node", "/usr/src/server.js"] - ``` - - - ```dockerfile - FROM node:21-alpine AS build - - WORKDIR /usr/src - - COPY . /usr/src/ - - RUN npm install - - FROM scratch - - # Distribution configuration - COPY --from=build /etc/os-release /etc/os-release - - # Express.js - COPY --from=build /usr/src/app/index.js /usr/src/server.js - COPY --from=build /usr/src/node_modules /usr/src/node_modules - ``` - - - -The `package.json` file lists the `express` [dependency](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#dependencies). - -The `Kraftfile` is the same one used for `http-node21`. - -For `Dockerfile` newly added lines have the following roles: - -* `FROM node:21-alpine AS build`: Use the base image of the `node:21-alpine` container. - This provides the `npm` binary and other Node-related components. - Name the current image `build`. - -* `WORKDIR /usr/src`: Use `/usr/src` as working directory. - All other commands in the `Dockerfile` run inside this directory. - -* `COPY . /usr/src/`: Copy the contents of the local current directory to the Docker filesystem. - Note that paths in the `.dockerignore` file aren't copied. - This means that `package.json` and `app/index.js` are copied. - -* `RUN npm install`: Install `npm` components listed in `packages.json`. - -* `COPY --from=build ...`: Copy existing files in the new `build` image in the `scratch`-based image. - `/etc/os-release` must copy to provide the distribution information required by node. - `/usr/src/node_modules` are the `npm`-generated files. - `/usrc/src/app/index.js` is the original `ExpressJS` source code file. - -Similar actions apply to other `npm`-based apps. -See also other Node examples: [`node18-prisma-rest-express`](https://github.com/kraftcloud/examples/tree/main/node18-prisma-rest-express) and [`node21-nextjs`](https://github.com/kraftcloud/examples/tree/main/node21-nextjs). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/node18-agario.mdx b/pages/guides/node18-agario.mdx new file mode 100644 index 00000000..fddd7b64 --- /dev/null +++ b/pages/guides/node18-agario.mdx @@ -0,0 +1,181 @@ +--- +title: "Agar.io (Node)" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +[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](/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/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/node18-agario/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /node18-agario:latest +unikraft run --scale-to-zero policy=on,cooldown-time=2000,stateful=true --metro fra -p 443:3000/tls+http -m 1G --image /node18-agario:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero on --scale-to-zero-stateful --scale-to-zero-cooldown 2s --metro fra -p 443:3000/tls+http -M 1Gi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: node18-agario-5k2xp +uuid: b3c4d5e6-f7a8-9b0c-1d2e-b3c4d5e6f7a8 +state: starting +image: /node18-agario +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 +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: node18-agario-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@sha256:0f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a + ├─────── memory: 1024 MiB + ├────── service: dark-meadow-fj9tm6bq + ├─ private fqdn: node18-agario-5k2xp.internal + └─── private ip: 10.0.3.5 +``` + + + +In this case, the instance name is `node18-agario-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: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node18-agario-5k2xp running /node18-agario 1024MiB 1 dark-meadow-fj9tm6bq.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +node18-agario-5k2xp dark-meadow-fj9tm6bq.fra.unikraft.app running 1 minute ago oci://unikraft.io//node18-agario@sha256:... 1.0 GiB 1 78.95 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete +``` + +```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: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/node18-wingsio.mdx b/pages/guides/node18-wingsio.mdx new file mode 100644 index 00000000..1ec38e98 --- /dev/null +++ b/pages/guides/node18-wingsio.mdx @@ -0,0 +1,180 @@ +--- +title: "Wings.io (Node)" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +[Wings.io](https://wings.io/) is a multiplayer .io game where players control a plane and try to shoot down other players while avoiding being shot themselves. +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](/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-wingsio/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/node18-wingsio/ +``` +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /node18-wingsio:latest +unikraft run --scale-to-zero policy=on,cooldown-time=1500,stateful=true --metro fra -p 443:3000/tls+http -m 1G --image /node18-wingsio:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero on --scale-to-zero-stateful --scale-to-zero-cooldown 1500ms --metro fra -p 443:3000/tls+http -M 1Gi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: node18-wingsio-h4n8m +uuid: c4d5e6f7-a8b9-0c1d-2e3f-c4d5e6f7a8b9 +state: starting +image: /node18-wingsio +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 +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: node18-wingsio-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@sha256:1a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b + ├─────── memory: 1024 MiB + ├────── service: swift-cloud-gk7us4cz + ├─ private fqdn: node18-wingsio-h4n8m.internal + └─── private ip: 10.0.4.4 +``` + + + +In this case, the instance name is `node18-wingsio-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: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node18-wingsio-h4n8m running /node18-wingsio 1024MiB 1 swift-cloud-gk7us4cz.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +node18-wingsio-h4n8m swift-cloud-gk7us4cz.fra.unikraft.app running 1 minute ago oci://unikraft.io//node18-wingsio@sha256:... 1.0 GiB 1 82.16 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete +``` + +```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: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/node21-websocket.mdx b/pages/guides/node21-websocket.mdx new file mode 100644 index 00000000..cac2444a --- /dev/null +++ b/pages/guides/node21-websocket.mdx @@ -0,0 +1,195 @@ +--- +title: "Node WebSocket Server" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +[WebSocket](https://en.wikipedia.org/wiki/WebSocket) is a bidirectional communication protocol over TCP, compatible with HTTP. +This example builds an echo-reply WebSocket server in [Node](https://nodejs.org/en). + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node21-websocket/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/node21-websocket/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /node21-websocket:latest +unikraft run --scale-to-zero policy=on,cooldown-time=1000,stateful=true --metro fra -p 443:8080/tls+http -m 1G --image /node21-websocket:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero on --scale-to-zero-stateful --scale-to-zero-cooldown 1s --metro fra -p 443:8080/tls+http -M 1Gi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: node21-websocket-j2x9r +uuid: d5e6f7a8-b9c0-1d2e-3f4a-d5e6f7a8b9c0 +state: starting +image: /node21-websocket +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 +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: node21-websocket-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@sha256:2b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c + ├─────── memory: 1024 MiB + ├────── service: lively-breeze-hp3wx6yt + ├─ private fqdn: node21-websocket-j2x9r.internal + └─── private ip: 10.0.5.4 +``` + + + +In this case, the instance name is `node21-websocket-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: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node21-websocket-j2x9r running /node21-websocket 1024MiB 1 lively-breeze-hp3wx6yt.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +node21-websocket-j2x9r lively-breeze-hp3wx6yt.fra.unikraft.app running 1 minute ago oci://unikraft.io//node21-websocket@sha256:... 1.0 GiB 1 45.83 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete +``` + +```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: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/node24-karaoke.mdx b/pages/guides/node24-karaoke.mdx new file mode 100644 index 00000000..d79b6108 --- /dev/null +++ b/pages/guides/node24-karaoke.mdx @@ -0,0 +1,235 @@ +--- +title: "Node AllKaraoke" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +[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](/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` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/node24-karaoke/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /node24-karaoke:latest +unikraft run --scale-to-zero policy=on,cooldown-time=2000,stateful=true --metro fra -p 443:8080/tls+http -m 2G --image /node24-karaoke:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero on --scale-to-zero-stateful --scale-to-zero-cooldown 2s -p 443:8080/tls+http -M 2Gi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: node24-karaoke-9lw5q +uuid: e5f6a7b8-c9d0-1234-efab-345678901234 +state: starting +image: /node24-karaoke +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 +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: node24-karaoke-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@sha256:1a3c5e7b9d2f4a6c8e0b2d4f6a8c0e2b4d6f8a0b2c4e6f8a0b2d4f6a8c0e2b + ├─────── memory: 2 GiB + ├────── service: wild-song-p5q2nrwx + ├─ private fqdn: node24-karaoke-9lw5q.internal + └─── private ip: 10.0.3.8 +``` + + + +In this case, the instance name is `node24-karaoke-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: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node24-karaoke-9lw5q running /node24-karaoke 2GiB 1 wild-song-p5q2nrwx.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +node24-karaoke-9lw5q wild-song-p5q2nrwx.fra.unikraft.app running since 3mins oci://unikraft.io//node24-karaoke@sha256:... 2 GiB 1 1.24 s +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete node24-karaoke-9lw5q +``` + +```bash title="kraft" +kraft cloud instance remove node24-karaoke-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 node:24-bookworm-slim AS prod`: Use a fresh Node.js 24 Bookworm slim image for the runtime. + +* `COPY ...`: Copy required files to the app filesystem: the `node` binary executable, system libraries, 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: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/novnc-browser.mdx b/pages/guides/novnc-browser.mdx new file mode 100644 index 00000000..77dc3d6d --- /dev/null +++ b/pages/guides/novnc-browser.mdx @@ -0,0 +1,191 @@ +--- +title: "noVNC" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This guide explains how to create and deploy a [noVNC](https://novnc.com/info.html) app, allowing you to access remote desktops through +a web interface inside a modern browser. + +**Note**: Anthropic's [Computer Use Demo](https://github.com/anthropics/claude-quickstarts/tree/main/computer-use-demo) inspired this example. + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/novnc-browser` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/novnc-browser/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /novnc-browser:latest +unikraft run --scale-to-zero policy=on,cooldown-time=4000,stateful=true --metro fra -p 443:6080/tls+http -m 4G --image /novnc-browser:latest +``` + +```bash title="kraft" +kraft cloud deploy \ + --scale-to-zero on \ + --scale-to-zero-stateful \ + --scale-to-zero-cooldown 4s \ + -p 443:6080/tls+http \ + -M 4Gi \ + -n vnc-browser \ + . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: vnc-browser +uuid: 90a59b05-0ae1-4ca6-8383-79c5115355ee +state: starting +image: /novnc-browser +resources: + memory: 4096MiB + vcpus: 1 +service: + uuid: aaf03f7c-65e6-5624-d5f4-84e87450beee + name: weathered-fog-y5jjmwfd + domains: + - fqdn: weathered-fog-y5jjmwfd.fra.unikraft.app +networks: +- uuid: 61708609-d291-572d-4a4c-399413238199 + private-ip: 10.0.0.49 + mac: 12:b0:1e:47:6c:59 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: vnc-browser + ├───────── uuid: 90a59b05-0ae1-4ca6-8383-79c5115355ee + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://weathered-fog-y5jjmwfd.fra.unikraft.app + ├──────── image: oci://unikraft.io//novnc-browser@sha256:fdb4887e84362ebbaf54c713e0d85f547e8ee173fe63a6ab39e94b7e612a9892 + ├─────── memory: 4096 MiB + ├────── service: weathered-fog-y5jjmwfd + ├─ private fqdn: vnc-browser.internal + └─── private ip: 10.0.0.49 +``` + + + +In this case, the instance name is `vnc-browser` and the address is `https://weathered-fog-y5jjmwfd.fra.unikraft.app`. +The name was preset, but the address is different for each run. +Enter the provided address into your browser of choice to access the remote desktop interface. + +Use `curl` to query the Unikraft Cloud instance: + +```bash +curl https://weathered-fog-y5jjmwfd.fra.unikraft.app +``` + +```text +Hello, World! +``` + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra vnc-browser standby /novnc-browser 4.0GiB 1 weathered-fog-y5jjmwfd.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +vnc-browser weathered-fog-y5jjmwfd.fra.unikraft.app standby standby oci://unikraft.io//novnc-browser@sha256:... 4.0 GiB 1 7.17 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete vnc-browser +``` + +```bash title="kraft" +kraft cloud instance remove vnc-browser +``` + + + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/opentelemetry-collector.mdx b/pages/guides/opentelemetry-collector.mdx index 01d201fe..6a4a8c57 100644 --- a/pages/guides/opentelemetry-collector.mdx +++ b/pages/guides/opentelemetry-collector.mdx @@ -4,140 +4,157 @@ title: "OpenTelemetry Collector" import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" -This example uses [OpenTelemetry -Collector](https://opentelemetry.io/docs/collector/), a vendor-agnostic +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This example uses [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/), a vendor-agnostic implementation of how to receive, process and export telemetry data. -OpenTelemetry Collector works with Unikraft / Unikraft Cloud to process -telemetry data. +OpenTelemetry Collector works with Unikraft / Unikraft Cloud to process telemetry data. To run this example, follow these steps: -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/opentelemetry-collector/` directory: +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/opentelemetry-collector/` directory: ```bash -git clone https://github.com/kraftcloud/examples +git clone https://github.com/unikraft-cloud/examples cd examples/opentelemetry-collector/ ``` -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): -```bash + + +```bash title="unikraft" +unikraft login +``` + +```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: -```bash -kraft cloud deploy -M 1024M . + + +```bash title="unikraft" +unikraft build . --output /opentelemetry-collector:latest +unikraft run --metro fra -m 1536M --image /opentelemetry-collector:latest ``` -The output shows the instance address and other details: +```bash title="kraft" +kraft cloud deploy -M 1536Mi . +``` -```ansi + + +The output shows the instance details: + + + +```ansi title="unikraft" +metro: fra +name: opentelemetry-collector-bvtnh +uuid: 40e8b154-b3b6-4312-ae69-2cdb794b15e4 +state: starting +image: /opentelemetry-collector +resources: + memory: 1536MiB + vcpus: 1 +networks: +- uuid: e74ba590-cbec-404b-d076-16aca1b52404 + private-ip: 10.0.3.3 + mac: 12:b0:aa:f7:b9:26 +timestamps: + created: just now +``` + +```ansi title="kraft" [●] Deployed successfully! │ - ├────────── name: opentelemetry-collector-bvtnh - ├────────── uuid: 40e8b154-b3b6-4312-ae69-2cdb794b15e4 - ├───────── state: starting - ├───────── image: opentelemetry-collector@sha256:64f73ea5fe208f54e5212f57979f24bebcf36276495462c52b380d15dd539ced - ├──────── memory: 976 MiB - ├── private fqdn: opentelemetry-collector-bvtnh.internal - ├──── private ip: 172.16.3.3 - └────────── args: /usr/bin/otelcontribcol --config /etc/otel/config.yaml + ├───────── name: opentelemetry-collector-bvtnh + ├───────── uuid: 40e8b154-b3b6-4312-ae69-2cdb794b15e4 + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├──────── image: oci://unikraft.io//opentelemetry-collector@sha256:64f73ea5fe208f54e5212f57979f24bebcf36276495462c52b380d15dd539ced + ├─────── memory: 1536 MiB + ├─ private fqdn: opentelemetry-collector-bvtnh.internal + └─── private ip: 10.0.3.3 ``` + + In this case, the instance name is `opentelemetry-collector-bvtnh`. They're different for each run. Note that the instance doesn't export a service. The default configuration can receive telemetry data from other instances by specifying the private IP or internal DNS as destination. +Use port 4317 for gRPC and port 4318 for HTTP. The only configured exporter is the debug exporter. Feel free to change and redeploy! You can list information about the instance by running: -```bash + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" kraft cloud instance list ``` -```text -razvand@mantis:~/.../examples/opentelemetry-collector$ kraft cloud inst ls -NAME FQDN STATE STATUS IMAGE MEMORY ARGS BOOT TIME -opentelemetry-collector-bvtnh running since 11mins opentelemetry... 976 MiB /usr/bin/otelcontribcol --config... 177.62 ms + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra opentelemetry-collector-bvtnh running /opentelemetry 1536MiB 1 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +opentelemetry-collector-bvtnh running since 11mins oci://unikraft.io//opentelemetry... 1536 MiB 1 177.62 ms ``` + + When done, you can remove the instance: -```bash + + +```bash title="unikraft" +unikraft instances delete opentelemetry-collector-bvtnh +``` + +```bash title="kraft" kraft cloud instance remove opentelemetry-collector-bvtnh ``` + + ## Customize your app To customize the OpenTelemetry Collector app, update `Kraftfile` or, more likely, the `rootfs/etc/otel/config.yaml` files: - - - Kratfile - rootfs/etc/otel/config.yaml - - - ```yaml -spec: v0.6 - -runtime: opentelemetry-collector:latest - -rootfs: ./rootfs - ``` - - - ```yaml title="" -receivers: - otlp: - protocols: - grpc: - endpoint: "0.0.0.0:4317" - http: - endpoint: "0.0.0.0:4318" - -exporters: - debug: - verbosity: detailed - -processors: - batch: - -extensions: - health_check: - -service: - extensions: [health_check] - # Disable the self telemetry which requires process-level data - telemetry: - metrics: - level: none - pipelines: - traces: - receivers: [otlp] - processors: [batch] - exporters: [debug] - metrics: - receivers: [otlp] - processors: [batch] - exporters: [debug] - logs: - receivers: [otlp] - processors: [batch] - exporters: [debug] - ``` - - - You can update the `rootfs/etc/otel/config.yaml` file as detailed in the [documentation](https://opentelemetry.io/docs/collector/configuration/). Such as adding another export, apart from the debug exporter. @@ -145,8 +162,17 @@ Such as adding another export, apart from the debug exporter. Use the `--help` option for detailed information on using Unikraft Cloud: -```bash + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" kraft cloud --help ``` -Or visit the [CLI Reference](/docs/cli). + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/overview.mdx b/pages/guides/overview.mdx new file mode 100644 index 00000000..795c6a5c --- /dev/null +++ b/pages/guides/overview.mdx @@ -0,0 +1,183 @@ +--- +title: "Guides Overview" +--- + +This page lists all available guides for deploying applications and services on Unikraft Cloud, grouped by category. +These guides mimic the [examples](https://github.com/unikraft-cloud/examples) repository, and are constantly updated with new content. + +:::note +Unikraft Cloud can run any workload—define it in a Dockerfile and it will run it. +These guides are here to make that journey as fast as possible for you. +::: + +{/* vale off */} + +## HTTP Servers + +Deploy HTTP servers written in your language of choice on Unikraft Cloud. + +### C / C++ + +- [C HTTP Server](/guides/httpserver-gcc13.2) +- [C++ HTTP Server](/guides/httpserver-gpp13.2) +- [C++ Boost HTTP Server](/guides/httpserver-boost1.74-gpp13.2) +- [SSH and HTTP Server with C and Debugging Tools](/guides/httpserver-c-debug) + +### .NET + +- [.NET HTTP Server](/guides/httpserver-dotnet10.0) + +### Elixir / Erlang + +- [Elixir HTTP Server](/guides/httpserver-elixir1.16) +- [Erlang HTTP Server](/guides/httpserver-erlang26.2) + +### Go + +- [Go HTTP Server](/guides/httpserver-go1.21) + +### Java + +- [Java HTTP Server](/guides/httpserver-java21) +- [Spring Boot HTTP Server](/guides/httpserver-java17-springboot3.5.x) +- [Spring Petclinic](/guides/httpserver-java17-spring-petclinic) + +### JavaScript / TypeScript + +- [Node HTTP Server](/guides/httpserver-node25) +- [Bun HTTP Server](/guides/httpserver-bun) +- [Express HTTP Server](/guides/httpserver-expressjs4.18-node21) +- [Puppeteer HTTP Server](/guides/httpserver-node-express-puppeteer) +- [Prisma HTTP Server](/guides/httpserver-prisma-expressjs4.19-node18) +- [Next.js HTTP Server](/guides/httpserver-node21-nextjs) +- [Remix HTTP Server](/guides/httpserver-node21-remix) +- [SolidJS HTTP Server](/guides/httpserver-node21-solid-start) +- [SvelteKit HTTP Server](/guides/httpserver-node21-sveltekit) +- [Vite HTTP Server (Nginx)](/guides/httpserver-nginx-vite-vanilla) +- [Vite HTTP Server (Node)](/guides/httpserver-node-vite-vanilla) +- [Vite SSR HTTP Server (Node)](/guides/httpserver-node-vite-ssr-vanilla) + +### Lua + +- [Lua HTTP Server](/guides/httpserver-lua5.1) + +### Perl + +- [Perl HTTP Server](/guides/httpserver-perl5.42) + +### PHP + +- [PHP HTTP Server](/guides/httpserver-php8.2) + +### Python + +- [Python HTTP Server](/guides/httpserver-python3.12) +- [Django HTTP Server](/guides/httpserver-python3.12-django5.0) +- [FastAPI HTTP Server](/guides/httpserver-python3.12-fastapi-0.121.3) +- [Flask HTTP Server](/guides/httpserver-python3.12-flask3.0) +- [Flask and SQLite HTTP Server](/guides/httpserver-python3.12-flask3.0-sqlite) + +### Ruby + +- [Ruby HTTP Server](/guides/httpserver-ruby3.2) + +### Rust + +- [Rust HTTP Server](/guides/httpserver-rust1.91) +- [Rust (Actix Web) HTTP Server](/guides/httpserver-rust1.87-actix-web4) +- [Rust (Leptos + Trunk) HTTP Server](/guides/httpserver-rust-trunkrs-leptos) +- [Rust (Rocket) HTTP Server](/guides/httpserver-rust1.81-rocket0.5) +- [Rust (Tokio) HTTP Server](/guides/httpserver-rust1.75-tokio) + +## Web Servers & Reverse Proxies + +Serve static content or route traffic with popular web server and proxy tools. + +- [Nginx](/guides/nginx) +- [Caddy](/guides/caddy2.7-go1.21) +- [HAProxy](/guides/haproxy) +- [Skipper](/guides/skipper0.18) +- [Traefik](/guides/traefik) + +## Databases & Caching + +Run databases and caching layers on Unikraft Cloud. + +- [DragonflyDB](/guides/dragonflydb) +- [DuckDB with Go](/guides/duckdb-go1.21) +- [MariaDB](/guides/mariadb) +- [Memcached](/guides/memcached1.6) +- [MongoDB](/guides/mongodb) +- [PostgreSQL](/guides/postgres) +- [Redis](/guides/redis7.2) + +## Storage & Media + +Host object storage and image processing services. + +- [MinIO](/guides/minio) +- [Imaginary](/guides/imaginary) + +## WebAssembly Runtimes + +Run WebAssembly workloads on Unikraft Cloud. + +- [Spin](/guides/spin-wagi-http) +- [Wazero](/guides/wazero-import-go) + +## AI & MCP Servers + +Deploy Model Context Protocol (MCP) servers and AI-enabled services. + +- [Simple MCP Server](/guides/mcp-server-simple) +- [ArXiv MCP Server](/guides/mcp-server-arxiv) + +## Observability & Monitoring + +Collect metrics and visualise them with standard tools. + +- [Grafana](/guides/grafana) +- [OpenTelemetry Collector](/guides/opentelemetry-collector) + +## Developer Tools & Remote Access + +Access remote development environments and file transfer services. + +- [Visual Studio Code Server](/guides/visual-studio-code-server) +- [noVNC](/guides/novnc-browser) +- [Debian SSH Server](/guides/debian-ssh) +- [vsftpd](/guides/vsftpd) + +## Full-Stack Applications & CMS + +Deploy complete web applications and content management systems. + +- [WordPress](/guides/wordpress-all-in-one) +- [Ruby on Rails](/guides/ruby3.2-rails) +- [Hugo](/guides/hugo0.122) + +## Browser Automation + +Run browser automation and end-to-end testing workloads on Unikraft Cloud. + +- [Playwright Chromium (Node)](/guides/node-playwright-chromium) +- [Playwright Firefox (Node)](/guides/node-playwright-firefox) +- [Playwright WebKit (Node)](/guides/node-playwright-webkit) +- [Playwright Chromium (Python)](/guides/python-playwright-chromium) + +## Real-time & Gaming + +Multiplayer and WebSocket-based applications. + +- [Node WebSocket Server](/guides/node21-websocket) +- [Agar.io Clone](/guides/node18-agario) +- [Wings.io Clone](/guides/node18-wingsio) +- [Node AllKaraoke](/guides/node24-karaoke) + +## Integration & Automation + +Examples for webhooks, background jobs, and automation. + +- [GitHub Webhook Receiver](/guides/github-webhook-node) + +{/* vale on */} diff --git a/pages/guides/perl.mdx b/pages/guides/perl.mdx deleted file mode 100644 index 3bde5b7c..00000000 --- a/pages/guides/perl.mdx +++ /dev/null @@ -1,151 +0,0 @@ ---- -title: Perl ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a simple Perl-based HTTP web server. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-perl5.38/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-perl5.38/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:8080 -M 512 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-perl538-1p4ml - ├────────── uuid: eabb0080-0065-40ff-81a8-f790f1b218ee - ├───────── state: running - ├─────────── url: https://cold-brook-ba71jc16.fra.unikraft.app - ├───────── image: http-perl538@sha256:8c2c1f536b349c24e04ab4fec508b69f7f2349302d42a02855318ee55c12e37c - ├───── boot time: 64.56 ms - ├──────── memory: 512 MiB - ├─────── service: cold-brook-ba71jc16 - ├── private fqdn: http-perl538-1p4ml.internal - ├──── private ip: 172.16.3.3 - └────────── args: /usr/bin/perl /usr/src/server.pl -``` - -In this case, the instance name is `http-perl538-1p4ml` and the address is `https://cold-brook-ba71jc16.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the Perl-based HTTP web server: - -```bash -curl https://cold-brook-ba71jc16.fra.unikraft.app -``` -```text -Hello, World! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-perl538-1p4ml cold-brook-ba71jc16.fra.unikraft.app stopped 37 seconds ago http-perl538@sha256:8c2c1f536b349c... 512 MiB /usr/bin/perl /usr/src/server.pl 64556us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove http-perl538-1p4ml -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `server.pl`: the actual Perl HTTP server -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```pl - #!/usr/bin/perl - - use warnings; - use strict; - - use HTTP::Daemon; - use HTTP::Status; - use HTTP::Response; - - my $daemon = HTTP::Daemon->new( - LocalAddr => '0.0.0.0', - LocalPort => 8080, - ) or die; - - while (my $client_connection = $daemon->accept) { - my $request = $client_connection->get_request; - my $response = HTTP::Response->new(200); - $response->content("Hello, World!\n"); - $client_connection->send_response($response); - } - ``` - - - ```yaml - spec: v0.6 - - runtime: perl:5.38 - - rootfs: ./Dockerfile - - cmd: ["/usr/bin/perl", "/usr/src/server.pl"] - ``` - - - ```dockerfile - FROM scratch - - # Simple Perl HTTP server - COPY ./server.pl /usr/src/server.pl - ``` - - - -The following options are available for customizing the app: - -* If you only update the implementation in the `server.pl` source file, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/php.mdx b/pages/guides/php.mdx deleted file mode 100644 index 893b6dc7..00000000 --- a/pages/guides/php.mdx +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: PHP ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a simple PHP-based HTTP web server. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-php8.2/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-php8.2/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:8080 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-php82-g00si - ├────────── uuid: 033b2f4b-72ff-414d-b0de-63571477c657 - ├───────── state: running - ├─────────── url: https://aged-fire-rh0oi0tj.fra.unikraft.app - ├───────── image: http-php82@sha256:dccaac053982673765b8f00497a9736c31458ab23ad59a550b09aa8dedfabb34 - ├───── boot time: 32.80 ms - ├──────── memory: 128 MiB - ├─────── service: aged-fire-rh0oi0tj - ├── private fqdn: http-php82-g00si.internal - ├──── private ip: 172.16.3.3 - └────────── args: /usr/local/bin/php /usr/src/server.php -``` - -In this case, the instance name is `http-php82-g00si` and the address is `https://aged-fire-rh0oi0tj.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the PHP-based HTTP web server: - -```bash -curl https://aged-fire-rh0oi0tj.fra.unikraft.app -``` -```text -Hello, World! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-php82-g00si aged-fire-rh0oi0tj.fra.unikraft.app running 50 seconds ago http-php82@sha256:dccaac05398267376... 256 MiB /usr/local/bin/php /usr/src/server.php 32801us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove http-php82-g00si -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `server.php`: the actual PHP HTTP server -* `php.ini`: the PHP configuration -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```php - #!/usr/local/bin/php -q - - ``` - - - ```ini - [PHP] - - extension=sockets - ``` - - - ```yaml - spec: v0.6 - - runtime: php:8.2 - - rootfs: ./Dockerfile - - cmd: ["/usr/local/bin/php", "/usr/src/server.php"] - ``` - - - ```dockerfile - FROM scratch - - # PHP configuration - COPY ./php.ini /usr/local/etc/php/php.ini - - # Simple PHP HTTP server - COPY ./server.php /usr/src/server.php - ``` - - - -The following options are available for customizing the app: - -* If you only update the implementation in the `server.php` source file, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - If you need new extensions, that may require updating the `php.ini` file. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/postgres.mdx b/pages/guides/postgres.mdx index 69ed1569..677b2753 100644 --- a/pages/guides/postgres.mdx +++ b/pages/guides/postgres.mdx @@ -1,55 +1,110 @@ --- -title: PostgreSQL +title: "PostgreSQL" --- import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + This guide shows you how to use [PostgreSQL](https://www.postgresql.org/), a powerful, open source object-relational database system. To run it, follow these steps: -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/postgres/` directory: +:::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/postgres/` directory: ```bash -git clone https://github.com/kraftcloud/examples +git clone https://github.com/unikraft-cloud/examples cd examples/postgres/ ``` -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): -```bash + + +```bash title="unikraft" +unikraft login +``` + +```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: -```bash -kraft cloud deploy -e POSTGRES_PASSWORD=unikraft -p 5432:5432/tls -M 1024 . + + +```bash title="unikraft" +unikraft build . --output /postgres:latest +unikraft run --metro fra --scale-to-zero policy=idle,cooldown-time=1000,stateful=true -p 5432:5432/tls -m 1G -e POSTGRES_PASSWORD=unikraft --image /postgres:latest ``` +```bash title="kraft" +kraft cloud deploy --scale-to-zero idle --scale-to-zero-stateful --scale-to-zero-cooldown 1s -p 5432:5432/tls -M 1Gi -e POSTGRES_PASSWORD=unikraft . +``` + + + The output shows the instance address and other details: -```ansi + + +```ansi title="unikraft" +metro: fra +name: postgres-saan9 +uuid: 3a1371f2-68c6-4187-84f8-c080f2b028ca +state: starting +image: /postgres +resources: + memory: 1024MiB + vcpus: 1 +service: + uuid: 8e9d810b-b1da-a30b-fd42-5c30c1900cb5 + name: young-thunder-fbafrsxj + domains: + - fqdn: young-thunder-fbafrsxj.fra.unikraft.app +networks: +- uuid: f1fab4c9-7951-75e3-ea1c-d87e47b4c9e2 + private-ip: 10.0.3.1 + mac: 12:b0:31:34:b1:96 +timestamps: + created: just now +``` + +```ansi title="kraft" [●] Deployed successfully! │ - ├────────── name: postgres-saan9 - ├────────── uuid: 3a1371f2-68c6-4187-84f8-c080f2b028ca - ├───────── state: starting - ├────────── fqdn: young-thunder-fbafrsxj.fra.unikraft.app - ├───────── image: postgres@sha256:2476c0373d663d7604def7c35ffcb4ed4de8ab231309b4f20104b84f31570766 - ├──────── memory: 1024 MiB - ├─────── service: young-thunder-fbafrsxj - ├── private fqdn: postgres-saan9.internal - ├──── private ip: 172.16.3.1 - └────────── args: wrapper.sh docker-entrypoint.sh postgres -c shared_preload_libraries='pg_ukc_scaletozero' + ├───────── name: postgres-saan9 + ├───────── uuid: 3a1371f2-68c6-4187-84f8-c080f2b028ca + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://young-thunder-fbafrsxj.fra.unikraft.app + ├──────── image: oci://unikraft.io//postgres@sha256:2476c0373d663d7604def7c35ffcb4ed4de8ab231309b4f20104b84f31570766 + ├─────── memory: 1024 MiB + ├────── service: young-thunder-fbafrsxj + ├─ private fqdn: postgres-saan9.internal + └─── private ip: 10.0.3.1 ``` + + In this case, the instance name is `postgres-saan9` and the service `young-thunder-fbafrsxj`. They're different for each run. @@ -74,55 +129,96 @@ postgres=# Use SQL and `psql` commands for your work. -:::tip[Idle Scale-to-Zero] -This example uses the [`idle` scale-to-zero policy](/docs/api/v1/instances#scaletozero_policy) by default (see the `labels` section in the `Kraftfile`). +:::tip +This example uses the [`idle` scale-to-zero policy](/api/platform/v1/instances#scaletozero_policy) by default (see the `labels` section in the `Kraftfile`). It means that the instance will scale-to-zero even in the presence of `psql` connections. To ensure that the instance isn't put into standby even for long running queries (during which the connections are also idle). The PostgreSQL example makes use of scale-to-zero app support. -To this end, the example loads the [`pg_ukc_scaletozero`](https://github.com/kraftcloud/pg_ukc_scaletozero) module into PostgreSQL, which suspends scale-to-zero during query processing. +To this end, the example loads the [`pg_ukc_scaletozero`](https://github.com/unikraft-cloud/pg_ukc_scaletozero) module into PostgreSQL, which suspends scale-to-zero during query processing. You can see this in action by running `SELECT pg_sleep(10);` and verifying that the instance keeps on running. ::: :::note -If you'd like to use a port other than `5432/tls` you'll need to use the `kraft cloud tunnel` command to connect to PostgreSQL. -See [the tunneling guide](/docs/guides/features/tunnel) for more information. +If you'd like to use a port other than `5432/tls` you'll need to use the `socat` command to connect to PostgreSQL. +See the [MariaDB](https://github.com/unikraft-cloud/examples/tree/main/mariadb) example for a guide on how to use it. Additionally, you need to explicitly disable scale-to-zero by either changing the label in the `Kraftfile` or use `--scale-to-zero off` in the deploy command. ::: You can list information about the instance by running: -```bash + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" kraft cloud instance list ``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -postgres-saan9 young-thunder-fbafrsxj.fra.unikraft.app running 6 minutes ago postgres@sha256:2476c0373d663d7604d... 1.0 GiB wrapper.sh docker-entrypoint.sh postgres 603.42 ms + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra postgres-saan9 running /postgres 1.0GiB 1 young-thunder-fbafrsxj.fra.unikraft.app 2 minutes ago ``` +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +postgres-saan9 young-thunder-fbafrsxj.fra.unikraft.app running 6 minutes ago oci://unikraft.io//postgres@sha256:... 1.0 GiB 1 603.42 ms +``` + + + When done, you can remove the instance: -```bash + + +```bash title="unikraft" +unikraft instance remove postgres-saan9 +``` + +```bash title="kraft" kraft cloud instance remove postgres-saan9 ``` -## Using volumes + -You can use [volumes](/docs/guides/features/volumes) for data persistence for you PostgreSQL instance. +## Using volumes +You can use [volumes](/platform/volumes) for data persistence for your PostgreSQL instance. For that you would first create a volume: -```console -kraft cloud volume create --name postgres --size 200 + + +```bash title="unikraft" +unikraft volume create --set metro=fra --set name=postgres --set size=200M +``` + +```bash title="kraft" +kraft cloud volume create --name postgres --size 200Mi ``` + + Then start the PostgreSQL instance and mount that volume: -```console -kraft cloud deploy -e POSTGRES_PASSWORD=unikraft -e PGDATA=/volume/postgres -v postgres:/volume -p 5432:5432/tls -M 1024 . + + +```bash title="unikraft" +unikraft build . --output /postgres:latest +unikraft run --metro fra --scale-to-zero policy=idle,cooldown-time=1000,stateful=true -p 5432:5432/tls -m 1G -e POSTGRES_PASSWORD=unikraft -e PGDATA=/volume/postgres --volume postgres:/volume --image /postgres:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero idle --scale-to-zero-stateful --scale-to-zero-cooldown 1s -p 5432:5432/tls -M 1Gi -e POSTGRES_PASSWORD=unikraft -e PGDATA=/volume/postgres -v postgres:/volume . ``` + + ## Customize your deployment Your deployment is a standard PostgreSQL installation. @@ -133,7 +229,7 @@ For that you use a different `POSTGRES_PASSWORD` environment variable when start You could also a different location to mount your volume or set extra configuration options. -You can use the PostgreSQL instance in conjunction with a frontend service, [see the guide here](/docs/guides/features/idns). +You can use the PostgreSQL instance in conjunction with a frontend service, [see the guide here](/platform/services). But in that case make sure to disable scale-to-zero if you plan to use the DB internally. :::note @@ -144,8 +240,17 @@ Support for scale-to-zero for internal instances is coming soon. Use the `--help` option for detailed information on using Unikraft Cloud: -```bash + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" kraft cloud --help ``` -Or visit the [CLI Reference](/docs/cli). + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/puppeteer.mdx b/pages/guides/puppeteer.mdx deleted file mode 100644 index 263e63ad..00000000 --- a/pages/guides/puppeteer.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: Puppeteer ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide shows you how to use [Puppeteer](https://pptr.dev/), a Node.js library which provides a high-level API to control browsers, including the option to run them headless (no UI). - -To run it, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/node-express-puppeteer/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/node-express-puppeteer/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# Set Unikraft Cloud access token -export UKC_TOKEN=token -# Set metro to Frankfurt, DE -export UKC_METRO=fra -``` - -:::note -A Puppeteer instance on Unikraft Cloud requires 4GB to run. -Request an increase in the instance memory quota when you need more memory. -::: - -When done, invoke the following command to deploy this app on Unikraft Cloud: - -```bash -kraft cloud deploy -p 443:3000 -M 4096 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: node-express-puppeteer-7afg3 - ├────────── uuid: 7bb479d7-5b3e-444f-b07c-eae4da6f57cc - ├───────── state: starting - ├──────── domain: https://nameless-fog-0tvh1uov.fra.unikraft.app - ├───────── image: node-express-puppeteer@sha256:78d0b180161c876f17d05116b93011ddcd44c76758d6fa0359f05938e67cea65 - ├──────── memory: 4096 MiB - ├─────── service: little-snow-7qwu6vv5 - ├── private fqdn: node-express-puppeteer-7afg3.internal - ├──── private ip: 172.16.3.1 - └────────── args: /usr/bin/wrapper.sh /usr/bin/node /app/bin/www -``` - -In this case, the instance name is `node-express-puppeteer-7afg3`. -They're different for each run. - -Use a browser to access the landing page of the Puppeteer (that uses [ExpressJS](https://expressjs.com/)). -The app and the landing page are part of [this repository](https://github.com/christopher-talke/node-express-puppeteer-pdf-example). - -In the example run above the landing page is at https://nameless-fog-0tvh1uov.fra.unikraft.app -You can use the landing page to generate the PDF version of a remote page. - -You can list information about the instance by running: - -```bash -kraft cloud instance list node-express-puppeteer-7afg3 -``` - -```text -NAME FQDN STATE STATUS IMAGE MEMORY ARGS BOOT TIME -node-express-puppeteer-7afg3 node-express-puppeteer-7afg3.fra.unikraft.app running since 6mins node-express-puppeteer-7afg3@s... 4.0 GiB /usr/bin/wrapper.sh /usr/bin/n... 15.27 ms -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove node-express-puppeteer-7afg3 -``` - -## Customize your deployment - -The current deployment uses an ExpressJS service that uses the [PDF generating functionality of Puppeteer](https://devdocs.io/puppeteer/). -Customizing the deployment means updating the service, such as adding new functionalities provided by Puppeteer. -You can update the service to provide a REST-like interface. - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/python-playwright-chromium.mdx b/pages/guides/python-playwright-chromium.mdx new file mode 100644 index 00000000..939632f8 --- /dev/null +++ b/pages/guides/python-playwright-chromium.mdx @@ -0,0 +1,188 @@ +--- +title: "Playwright (Chromium) with Python FastAPI" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +[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](/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/python-playwright-chromium/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/python-playwright-chromium/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /python-playwright-chromium:latest +unikraft run --scale-to-zero policy=idle,cooldown-time=1000,stateful=true --metro fra -p 443:8080/tls+http -m 4G --image /python-playwright-chromium:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero idle --scale-to-zero-stateful --scale-to-zero-cooldown 1s -p 443:8080/tls+http -M 4Gi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: python-playwright-chromium-m6k3p +uuid: e6f7a8b9-c0d1-2e3f-4a5b-e6f7a8b9c0d1 +state: starting +image: /python-playwright-chromium +resources: + memory: 4096MiB + vcpus: 1 +service: + uuid: f7a8b9c0-d1e2-3f4a-5b6c-f7a8b9c0d1e2 + name: young-night-kq8bv2mx + domains: + - fqdn: young-night-kq8bv2mx.fra.unikraft.app +networks: +- uuid: a8b9c0d1-e2f3-4a5b-6c7d-a8b9c0d1e2f3 + private-ip: 10.0.6.5 + mac: 12:b0:e4:b0:23:1d +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: python-playwright-chromium-m6k3p + ├───────── uuid: e6f7a8b9-c0d1-2e3f-4a5b-e6f7a8b9c0d1 + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://young-night-kq8bv2mx.fra.unikraft.app + ├──────── image: oci://unikraft.io//python-playwright-chromium@sha256:3c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1b2c3d4e5f6a7b8c9d + ├─────── memory: 4096 MiB + ├────── service: young-night-kq8bv2mx + ├─ private fqdn: python-playwright-chromium-m6k3p.internal + └─── private ip: 10.0.6.5 +``` + + + +In this case, the instance name is `python-playwright-chromium-m6k3p` and the address is `https://young-night-kq8bv2mx.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://github.com" -o ss-github.png +``` + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra python-playwright-chromium-m6k3p running /python-playwright-chromium 4096MiB 1 young-night-kq8bv2mx.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +python-playwright-chromium-m6k3p young-night-kq8bv2mx.fra.unikraft.app running 1 minute ago oci://unikraft.io//python-playwright-chromium@sha256:... 4 GiB 1 3.47 s +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete +``` + +```bash title="kraft" +kraft cloud instance remove +``` + + + + +## Learn more + +- [Playwright's Documentation](https://playwright.dev/docs/intro) +- [FastAPI's Tutorial](https://fastapi.tiangolo.com/tutorial/) +- [Unikraft Cloud's Documentation](https://unikraft.cloud/docs/) +- [Building `Dockerfile` Images with `Buildkit`](https://unikraft.org/guides/building-dockerfile-images-with-buildkit) + + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/python.mdx b/pages/guides/python.mdx deleted file mode 100644 index 03686062..00000000 --- a/pages/guides/python.mdx +++ /dev/null @@ -1,280 +0,0 @@ ---- -title: Python ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a simple Python-based HTTP web server. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-python3.12/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-python3.12/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:8080 -M 512 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-python312-ma2i9 - ├────────── uuid: e7389eee-9808-4152-b2ec-1f3c0541fd05 - ├───────── state: running - ├─────────── url: https://young-night-5fpf0jj8.fra.unikraft.app - ├───────── image: http-python312@sha256:278cb8b14f9faf9c2702dddd8bfb6124912d82c11b4a2c6590b6e32fc4049472 - ├───── boot time: 15.09 ms - ├──────── memory: 512 MiB - ├─────── service: young-night-5fpf0jj8 - ├── private fqdn: http-python312-ma2i9.internal - ├──── private ip: 172.16.3.3 - └────────── args: /usr/bin/python /src/server.py -``` - -In this case, the instance name is `http-python312-ma2i9` and the address is `https://young-night-5fpf0jj8.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the Python-based HTTP web server: - -```bash -curl https://young-night-5fpf0jj8.fra.unikraft.app -``` -```text -Hello, World! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-python312-ma2i9 young-night-5fpf0jj8.fra.unikraft.app running 1 minute ago http-python312@sha256:278cb8b14f9faf9c27... 512 MiB /usr/bin/python /src/server.py 15094us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove http-python312-ma2i9 -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `server.py`: the actual Python HTTP server -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```py - import argparse - from http.server import HTTPServer, BaseHTTPRequestHandler - - class MyServer(BaseHTTPRequestHandler): - def do_GET(self): - self.send_response(200) - self.send_header("Content-type", "text/html") - self.end_headers() - self.wfile.write(bytes("Hello, World!\n", "utf-8")) - - def main(args): - server = HTTPServer((args.host, args.port), MyServer) - - print("starting server at %s:%s" % (args.host, args.port)) - - try: - server.serve_forever() - - except KeyboardInterrupt: - pass - - print("server stopped") - - def parse_args(): - parser = argparse.ArgumentParser() - parser.add_argument("--host", type=str, default="0.0.0.0") - parser.add_argument("--port", type=int, default=8080) - return parser.parse_args() - - if __name__ == "__main__": - main(parse_args()) - ``` - - - ```yaml -spec: v0.6 - -runtime: python:3.12 - -rootfs: ./Dockerfile - -cmd: ["/usr/bin/python3", "/src/server.py"] - ``` - - - ```dockerfile -FROM scratch - -# Python HTTP server -COPY ./server.py /src/server.py - ``` - - - -Lines in the `Kraftfile` have the following roles: - -* `spec: v0.6`: The current `Kraftfile` specification version is `0.6`. - -* `runtime: python:3.12`: The Unikraft runtime kernel to use is Python 3.12. - -* `rootfs: ./Dockerfile`: Build the app root filesystem using the `Dockerfile`. - -* `cmd: ["/usr/bin/python3", "/src/server.py"]`: Use `/usr/bin/python3 /src/server.py` as the starting command of the instance. - -Lines in the `Dockerfile` have the following roles: - -* `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). - -* `COPY ./server.py /src/server.py`: Copy the server implementation file (`server.py`) in the Docker filesystem (in `/src/server.py`). - -The following options are available for customizing the app: - -* If you only update the implementation in the `server.py` source file, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - This includes the use of Python frameworks and the use of [`pip`](), as shown in the next section. - -## Using `pip` - -[`pip`]() is a package manager for Python. -It's used to install dependencies for Python apps. -`pip` uses the `requirements.txt` file to list required dependencies (with versions). - -The [`http-python3.12-flask3.0`](/docs/guides/flask-sqlite/) guide details the use of `pip` to deploy an app using the [`Flask`](https://flask.palletsprojects.com/en/3.0.x/) framework on Unikraft Cloud. - -Run the command below to deploy the app on Unikraft Cloud: - -```bash -kraft cloud deploy -p 443:8080 -M 512 . -``` - -Differences from the `http-python3.12` app are also the steps required to create an `pip`-based app: - -1. Add the `requirements.txt` file used by `pip`. - -1. Add framework-specific source files. - In this case, this means the `server.py` file. - -1. Update the `Dockerfile` to: - - 1. `COPY` the local files. - - 1. `RUN` the `pip3 install` command to install dependencies. - - 1. `COPY` of the resulting and required files (`/usr/local/lib/pyhon3.12` and `server.py`) in the app filesystem, using the [`scratch` container](https://hub.docker.com/_/scratch/). - -The following lists the files: - - - - ```py - from flask import Flask - app = Flask(__name__) - - @app.route('/') - def hello(): - return "Hello, World!\n" - - if __name__ == '__main__': - app.run(host='0.0.0.0', port=8080) - ``` - - - ```text - flask>=3.0,<3.1 - ``` - - - ```yaml - spec: v0.6 - - runtime: python:3.12 - - rootfs: ./Dockerfile - - cmd: ["/usr/bin/python3", "/app/server.py"] - ``` - - - ```dockerfile - FROM python:3.12-bookworm AS base - - WORKDIR /app - - COPY requirements.txt /app - - RUN pip3 install -r requirements.txt --no-cache-dir - - FROM scratch - - COPY --from=base /usr/local/lib/python3.12 /usr/local/lib/python3.12 - COPY ./server.py /app/server.py - ``` - - - -The `requirements.txt` file lists the `flask` dependency. - -The `Kraftfile` is the same one used for `http-python3.12`. - -For `Dockerfile` newly added lines have the following roles: - -* `FROM python:3.12-bookworm AS base`: Use the base image of the `python:3.12-bookworm` container. - This provides the `pip3` binary and other Python-related components. - Name the current image `base`. - -* `WORKDIR /app`: Use `/app` as working directory. - All other commands in the `Dockerfile` run inside this directory. - -* `COPY requirements.txt /app`: Copy the package configuration file to the Docker filesystem. - -* `RUN pip3 install ...`: Install `pip` components listed in `requirements.txt`. - -* `COPY --from=base ...`: Copy generated Python files in the new `base` image in the `scratch`-based image. - -Similar actions apply to other `pip3`-based apps. -See also the [`http-python3.12-django5.0`](https://github.com/kraftcloud/examples/tree/main/http-python3.12-django5.0) example. - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/rails.mdx b/pages/guides/rails.mdx deleted file mode 100644 index 0d6218ad..00000000 --- a/pages/guides/rails.mdx +++ /dev/null @@ -1,191 +0,0 @@ ---- -title: Ruby on Rails ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a [Ruby on Rails](https://rubyonrails.org/) app. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/ruby3.2-rails/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/ruby3.2-rails/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -M 1024 -p 443:3000 -e GEM_HOME=/usr/local/bundle -e BUNDLE_APP_CONFIG=/usr/local/bundle . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: ruby32-rails-apa93 - ├────────── uuid: 2f85b9db-94f8-45d2-8e38-ed9b56cb8695 - ├───────── state: running - ├─────────── url: https://aged-waterfall-qraz0s7d.fra.unikraft.app - ├───────── image: ruby32-rails@sha256:fdd46011408fdee05644665ad59b24115737e3fdb352169ec2f3f16a45d4f31d - ├───── boot time: 577.34 ms - ├──────── memory: 1024 MiB - ├─────── service: aged-waterfall-qraz0s7d - ├── private fqdn: ruby32-rails-apa93.internal - ├──── private ip: 172.16.3.3 - └────────── args: /usr/bin/ruby /app/bin/rails server -b 0.0.0.0 -``` - -In this case, the instance name is `ruby32-rails-apa93` and the address is `https://aged-waterfall-qraz0s7d.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the Python-based HTTP web server: - -```bash -curl https://aged-waterfall-qraz0s7d.fra.unikraft.app/hello -``` -```text -[...] - -

Hello World

-Hello, World! - - -[...] -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -ruby32-rails-apa93 aged-waterfall-qraz0s7d.fra.unikraft.app running 2 minutes ago ruby32-rails@sha256:fdd46011408fdee... 1.0 GiB /usr/bin/ruby /app/bin/rails server -b 0.0.0.0 577.34 ms -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove ruby32-rails-apa93 -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `app/` and `config/`: the contents to update the Rails setup -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```yaml - spec: v0.6 - - runtime: ruby:3.2 - - rootfs: ./Dockerfile - - cmd: ["/usr/bin/ruby", "/app/bin/rails", "server", "-b", "0.0.0.0"] - ``` - - - ```dockerfile - FROM ruby:3.2.2-bookworm AS build - - RUN gem install rails - RUN rails new app - - WORKDIR /app - RUN rails generate controller hello - - COPY . /app/ - - FROM scratch - - # Ruby Gems & Rails contents - COPY --from=build /usr/local/bundle /usr/local/bundle - - # System libraries - [...] - - COPY --from=build /app /app - ``` - - - -The `app/` and `config/` directories contain files that are to overwrite generated Rails files: - -```bash -tree app/ config/ -``` -```text -app/ -|-- controllers/ -| `-- hello_controller.rb -`-- views/ - `-- hello/ - `-- index.html.erb -config/ -|-- environments/ -| `-- development.rb -`-- routes.rb -``` - -These files add the configuration (controller, view, route) to print the `Hello, World!` message in Rails. -They overwrite the generated Rails configuration to provide the app setup. -Update these files, and other files, with required contents for your own app. - -Lines in the `Kraftfile` have the following roles: - -* `spec: v0.6`: The current `Kraftfile` specification version is `0.6`. - -* `runtime: ruby:3.2`: The Unikraft runtime kernel to use is Ruby 3.2. - -* `rootfs: ./Dockerfile`: Build the app root filesystem using the `Dockerfile`. - -* `cmd: ["/usr/bin/ruby", "/app/bin/rails", "server", "-b", "0.0.0.0"]`: Use `/usr/bin/ruby /app/bin/rails server -b 0.0.0.0` as the starting command of the instance. - -Lines in the `Dockerfile` have the following roles: - -* The `RUN` lines install Rails and generate the default app setup for a `hello` app. - -* `COPY . /app/`: Copies the local files (from `app/` and `config/`) to the app, overwriting generated contents to provide the user-specified app. - -* `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). - -* `COPY --from=build /app /app`: Copy the app directory to the filesystem. - -The following options are available for customizing the app: - -* If you only update the implementation in the `app/` or `config/` directories, or adding new directories or files to overwrite Rails generated files, you don't need to make any other changes. - -* If changing the app name, change the `hello` name in `RUN rails generate controller hello` to a new one. - The file names in the `app/` and `config/` directories have to be similarly updated. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/redis.mdx b/pages/guides/redis.mdx deleted file mode 100644 index 2a47688b..00000000 --- a/pages/guides/redis.mdx +++ /dev/null @@ -1,160 +0,0 @@ ---- -title: Redis ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guides shows you how to use [Redis](https://redis.io), an open source in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability. - -To run it example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/redis/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/redis/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 6379:6379/tls -M 512 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: redis-alb4r - ├────────── uuid: d3c3141b-97b2-4e1d-87ae-39e4f14ab49e - ├───────── state: running - ├────────── fqdn: rough-wind-8vxrd1ms.fra.unikraft.app - ├───────── image: redis@sha256:9665c51faf7deb538cf7907b012b55700cad08cd391f5ba099d95d018c8da7d - ├───── boot time: 26.13 ms - ├──────── memory: 512 MiB - ├─────── service: rough-wind-8vxrd1ms - ├── private fqdn: redis-alb4r.internal - ├──── private ip: 172.16.6.2 - └────────── args: /usr/bin/redis-server /etc/redis/redis.conf -``` - -In this case, the instance name is `redis-alb4r` which is different for every run. - -To test the deployment, first forward the port with the `kraft cloud tunnel` command: - -```bash -kraft cloud tunnel 6379:memcached-arkv7:6379 -``` - -Then, from another console, you can now use the `redis-benchmark` client to connect to Redis, for example: -```console -redis-benchmark -t ping,set,get -n 10000 -``` - -You should see output like: - -```ansi -====== PING_INLINE ====== - 10000 requests completed in 32.03 seconds - 50 parallel clients - 3 bytes payload - keep alive: 1 - host configuration "save": - host configuration "appendonly": no - multi-thread: no - -0.01% <= 138 milliseconds -0.05% <= 139 milliseconds -2.34% <= 140 milliseconds -4.49% <= 141 milliseconds -8.57% <= 142 milliseconds -16.06% <= 143 milliseconds -21.83% <= 144 milliseconds -26.25% <= 145 milliseconds -34.54% <= 146 milliseconds -... -``` - -To disconnect, kill the `tunnel` command with ctrl-C. - -:::note -This guide uses `kraft cloud tunnel` only when a service doesn't support TLS and isn't HTTP-based (TLS/SNI determines the correct instance to send traffic to). -Also note that the `tunnel` command isn't needed when connecting via an instance's private IP/FQDN. -For example, when a Redis instance serves as a cache server to -another instance that acts as a frontend and which **does** support TLS. -::: - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -redis-alb4r rough-wind-8vxrd1ms.fra.unikraft.app running 1 minute ago redis@sha256:9665c5... 512 MiB /usr/bin/redis-server /etc/red... 26131us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove redis-alb4r -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `Kraftfile`: the Unikraft Cloud specification, including command-line arguments -* `Dockerfile`: In case you need to add files to your instance's rootfs - - - - ```yaml -spec: v0.6 - -runtime: redis:latest - -rootfs: ./Dockerfile - -cmd: ["/usr/bin/redis-server", "/etc/redis/redis.conf"] - ``` - - -```Dockerfile -FROM debian:bookworm AS build - -RUN set -xe; \ - echo "Your code here"; - -FROM scratch - -# Copy your files here -# COPY --from=build / / -``` - - - - - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/redis7.2.mdx b/pages/guides/redis7.2.mdx new file mode 100644 index 00000000..a218fe94 --- /dev/null +++ b/pages/guides/redis7.2.mdx @@ -0,0 +1,221 @@ +--- +title: "Redis" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This guide shows you how to use [Redis](https://redis.io), an open source in-memory storage, used as a distributed, in-memory key–value database, cache and message broker, with optional durability. + +To run it, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/redis7.2/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/redis7.2/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /redis72:latest +unikraft run --scale-to-zero policy=off --metro fra -p 6379:6379/tls -m 512M --image /redis72:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero off -p 6379:6379/tls -M 512Mi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: redis72-alb4r +uuid: d3c3141b-97b2-4e1d-87ae-39e4f14ab49e +state: starting +image: /redis72 +resources: + memory: 512MiB + vcpus: 1 +service: + uuid: 7a4f2b3c-1d8e-4a92-b3f5-e6c1d2a3b4e5 + name: rough-wind-8vxrd1ms + domains: + - fqdn: rough-wind-8vxrd1ms.fra.unikraft.app +networks: +- uuid: 9b5e1f8d-3c2a-7b46-d1e9-f2a3b4c5d6e7 + private-ip: 10.0.3.2 + mac: 12:b0:4e:20:b3:e7 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: redis72-alb4r + ├───────── uuid: d3c3141b-97b2-4e1d-87ae-39e4f14ab49e + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://rough-wind-8vxrd1ms.fra.unikraft.app + ├──────── image: oci://unikraft.io//redis72@sha256:9665c51faf7deb538cf7907b012b55700cad08cd391f5ba099d95d018c8da7d + ├─────── memory: 512 MiB + ├────── service: rough-wind-8vxrd1ms + ├─ private fqdn: redis72-alb4r.internal + └─── private ip: 10.0.3.2 +``` + + + +In this case, the instance name is `redis72-alb4r` which is different for every run. + +To test the deployment, first forward the port using `socat`: + +```bash +socat TCP-LISTEN:6379,fork OPENSSL:rough-wind-8vxrd1ms.fra.unikraft.app:6379,verify=0 +``` + +Then, from another console, you can now use the `redis-benchmark` client to connect to Redis, for example: + +```console +redis-benchmark -t ping,set,get -n 10000 +``` + +You should see output like: + +```ansi +====== PING_INLINE ====== + 10000 requests completed in 32.03 seconds + 50 parallel clients + 3 bytes payload + keep alive: 1 + host configuration "save": + host configuration "appendonly": no + multi-thread: no + +0.01% <= 138 milliseconds +0.05% <= 139 milliseconds +2.34% <= 140 milliseconds +4.49% <= 141 milliseconds +8.57% <= 142 milliseconds +16.06% <= 143 milliseconds +21.83% <= 144 milliseconds +26.25% <= 145 milliseconds +34.54% <= 146 milliseconds +... +``` + +To disconnect, kill the `socat` command with ctrl-C. + +:::note +This guide uses `socat` for port forwarding only when a service doesn't support TLS and isn't HTTP-based (TLS/SNI determines the correct instance to send traffic to). +Also note that port forwarding isn't needed when connecting via an instance's private IP/FQDN. +For example, when a Redis instance serves as a cache server to +another instance that acts as a frontend and which **does** support TLS. +::: + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra redis72-alb4r running /redis72 512MiB 1 rough-wind-8vxrd1ms.fra.unikraft.app 1 minute ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +redis72-alb4r rough-wind-8vxrd1ms.fra.unikraft.app running 1 minute ago oci://unikraft.io//redis72@sha256:... 512 MiB 1 26.13 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete redis72-alb4r +``` + +```bash title="kraft" +kraft cloud instance remove redis72-alb4r +``` + + + +## Customize your app + +To customize the app, update the files in the repository, listed below: + +* `Kraftfile`: the Unikraft Cloud specification, including command-line arguments +* `Dockerfile`: In case you need to add files to your instance's rootfs + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/remix.mdx b/pages/guides/remix.mdx deleted file mode 100644 index 51c4cd13..00000000 --- a/pages/guides/remix.mdx +++ /dev/null @@ -1,131 +0,0 @@ ---- -title: Remix ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide shows how to deploy a [Remix](https://remix.run/) app. - -To do so, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/node21-remix/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/node21-remix/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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 the app on Unikraft Cloud: - -```bash -kraft cloud deploy -p 443:3000 -M 512 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: node21-remix-jvj6b - ├────────── uuid: 4e6ccb1f-0533-4dc1-be67-eca8dfc1f8c6 - ├───────── state: running - ├─────────── url: https://long-star-1tms9h1z.fra.unikraft.app - ├───────── image: node21-remix@sha256:300eefce3de136ad9c782f010b69da01100ae5f0ca17f038f92321d735d6675f - ├───── boot time: 153.47 ms - ├──────── memory: 512 MiB - ├─────── service: long-star-1tms9h1z - ├── private fqdn: node21-remix-jvj6b.internal - ├──── private ip: 172.16.6.8 - └────────── args: /usr/bin/node /usr/src/server.js -``` - -In this case, the instance name is `node21-remix-jvj6b` and the address is `https://long-star-1tms9h1z.fra.unikraft.app`. -They're different for each run. -You can now point your browser at the address to see your deployed instance. - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -node21-remix-jvj6b long-star-1tms9h1z.fra.unikraft.app running 1 minutes ago node21-rem... 256 MiB /usr/bin/node /usr/src/server... 67.65 ms -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove node21-remix-jvj6b -``` - -## 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 -* `server.js`: the server itself - - - ```yaml -spec: v0.6 - -runtime: node:21 - -rootfs: ./Dockerfile - -cmd: ["/usr/bin/node", "/usr/src/server.js"] - ``` - - - ```dockerfile -FROM node:21-alpine AS base - -WORKDIR /usr/src - -FROM base AS build - -COPY package.json /usr/src/package.json -RUN npm install - -COPY . /usr/src/ -RUN npm run build - -FROM base AS runtime - -COPY package.json /usr/src/package.json -RUN npm install --omit dev - -FROM scratch - -COPY --from=runtime /usr/src/node_modules /usr/src/node_modules -COPY --from=build /usr/src/build/ /usr/src/build/ -COPY --from=build /usr/src/public /usr/src/public -COPY --from=build /usr/src/server.js /usr/src/server.js -COPY --from=build /usr/src/package.json /usr/src/package.json - ``` - - - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/ruby.mdx b/pages/guides/ruby.mdx deleted file mode 100644 index 2a82a45e..00000000 --- a/pages/guides/ruby.mdx +++ /dev/null @@ -1,149 +0,0 @@ ---- -title: Ruby ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a simple Ruby-based HTTP web server. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-ruby3.2/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-ruby3.2/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:8080 -M 256 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-ruby32-s6l8n - ├────────── uuid: b1ebbbc0-5efa-476c-adb6-99866773245c - ├───────── state: running - ├─────────── url: https://silent-resonance-1jtz5c66.fra.unikraft.app - ├───────── image: http-ruby32@sha256:4cf3b341898e6ebff18ff2b68353ef872dded650c9d16a6f005a8fbe8a7cbb3d - ├───── boot time: 71.19 ms - ├──────── memory: 256 MiB - ├─────── service: silent-resonance-1jtz5c66 - ├── private fqdn: http-ruby32-s6l8n.internal - ├──── private ip: 172.16.3.3 - └────────── args: /usr/bin/ruby /src/server.rb -``` - -In this case, the instance name is `http-ruby32-s6l8n` and the address is `https://silent-resonance-1jtz5c66.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the Ruby-based HTTP web server: - -```bash -curl https://silent-resonance-1jtz5c66.fra.unikraft.app -``` -```text -Hello, World! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-ruby32-s6l8n silent-resonance-1jtz5c66.fra.unikraft.app running 12 minutes ago http-ruby32@sha256:4cf3b341898e6ebff18ff2b68353ef... 256 MiB /usr/bin/ruby /src/server.rb 71191us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove http-ruby32-s6l8n -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `server.rb`: the actual Ruby HTTP server -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```rb - require 'socket' - - socket = TCPServer.new(8080) - - loop do - client = socket.accept - - request = client.gets - - response = "HTTP/1.1 200 OK\r\n" \ - "Content-type: text/html\r\n" \ - "Connection: close\r\n" \ - "\r\n" \ - "Hello, World!" - client.puts(response) - - client.close - end - ``` - - - ```yaml - spec: v0.6 - - runtime: ruby:3.2 - - rootfs: ./Dockerfile - - cmd: ["/usr/bin/ruby", "/src/server.rb"] - ``` - - - ```dockerfile - FROM scratch - - # Simple Ruby HTTP server - COPY ./server.rb /src/server.rb - ``` - - - -The following options are available for customizing the app: - -* If you only update the implementation in the `server.rb` source file, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/ruby3.2-rails.mdx b/pages/guides/ruby3.2-rails.mdx new file mode 100644 index 00000000..32554df6 --- /dev/null +++ b/pages/guides/ruby3.2-rails.mdx @@ -0,0 +1,247 @@ +--- +title: "Ruby on Rails" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This guide explains how to create and deploy a [Ruby on Rails](https://rubyonrails.org/) app. +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/ruby3.2-rails/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/ruby3.2-rails/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /ruby32-rails:latest +unikraft run --scale-to-zero policy=idle,cooldown-time=1000,stateful=true --metro fra -p 443:3000/tls+http -m 1G -e GEM_HOME=/usr/local/bundle -e BUNDLE_APP_CONFIG=/usr/local/bundle --image /ruby32-rails:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero idle --scale-to-zero-stateful --scale-to-zero-cooldown 1s -p 443:3000/tls+http -M 1Gi -e GEM_HOME=/usr/local/bundle -e BUNDLE_APP_CONFIG=/usr/local/bundle . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: ruby32-rails-apa93 +uuid: 2f85b9db-94f8-45d2-8e38-ed9b56cb8695 +state: starting +image: /ruby32-rails +resources: + memory: 1024MiB + vcpus: 1 +service: + uuid: 42265dab-df44-6d66-0075-fc07455178c8 + name: aged-waterfall-qraz0s7d + domains: + - fqdn: aged-waterfall-qraz0s7d.fra.unikraft.app +networks: +- uuid: 7ae3414b-1d87-c831-d26a-506a4cf9ac71 + private-ip: 10.0.3.3 + mac: 12:b0:39:3e:b5:36 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: ruby32-rails-apa93 + ├───────── uuid: 2f85b9db-94f8-45d2-8e38-ed9b56cb8695 + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://aged-waterfall-qraz0s7d.fra.unikraft.app + ├──────── image: oci://unikraft.io//ruby32-rails@sha256:fdd46011408fdee05644665ad59b24115737e3fdb352169ec2f3f16a45d4f31d + ├─────── memory: 1024 MiB + ├────── service: aged-waterfall-qraz0s7d + ├─ private fqdn: ruby32-rails-apa93.internal + └─── private ip: 10.0.3.3 +``` + + + +In this case, the instance name is `ruby32-rails-apa93` and the address is `https://aged-waterfall-qraz0s7d.fra.unikraft.app`. +They're different for each run. + +Use `curl` to query the Unikraft Cloud instance of the Python-based HTTP web server: + +```bash +curl https://aged-waterfall-qraz0s7d.fra.unikraft.app/hello +``` + +```text +[...] + +

Hello World

+Hello, World! + + +[...] +``` + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra ruby32-rails-apa93 running /ruby32-rails 1.0GiB 1 aged-waterfall-qraz0s7d.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +ruby32-rails-apa93 aged-waterfall-qraz0s7d.fra.unikraft.app running 2 minutes ago oci://unikraft.io//ruby32-rails@sha256:... 1.0 GiB 1 577.34 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete ruby32-rails-apa93 +``` + +```bash title="kraft" +kraft cloud instance remove ruby32-rails-apa93 +``` + + + +## Customize your app + +To customize the app, update the files in the repository, listed below: + +* `app/` and `config/`: the contents to update the Rails setup +* `Kraftfile`: the Unikraft Cloud specification +* `Dockerfile`: the Docker-specified app filesystem + +The `app/` and `config/` directories contain files that are to overwrite generated Rails files: + +```bash +tree app/ config/ +``` + +```text +app/ +|-- controllers/ +| `-- hello_controller.rb +`-- views/ + `-- hello/ + `-- index.html.erb +config/ +|-- environments/ +| `-- development.rb +`-- routes.rb +``` + +These files add the configuration (controller, view, route) to print the `Hello, World!` message in Rails. +They overwrite the generated Rails configuration to provide the app setup. +Update these files, and other files, with required contents for your own app. + +Lines in the `Kraftfile` have the following roles: + +* `spec: v0.7`: The current `Kraftfile` specification version is `0.7`. + +* `runtime: base-compat:latest`: The runtime kernel to use is the base compatibility kernel. + +* `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: ["/usr/bin/ruby", "/app/bin/rails", "server", "-b", "0.0.0.0"]`: Use `/usr/bin/ruby /app/bin/rails server -b 0.0.0.0` as the starting command of the instance. + +Lines in the `Dockerfile` have the following roles: + +* The `RUN` lines install Rails and generate the default app setup for a `hello` app. + +* `COPY . /app/`: Copies the local files (from `app/` and `config/`) to the app, overwriting generated contents to provide the user-specified app. + +* `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). + +* `COPY --from=build /app /app`: Copy the app directory to the filesystem. + +The following options are available for customizing the app: + +* If you only update the implementation in the `app/` or `config/` directories, or adding new directories or files to overwrite Rails generated files, you don't need to make any other changes. + +* If changing the app name, change the `hello` name in `RUN rails generate controller hello` to a new one. + The file names in the `app/` and `config/` directories have to be similarly updated. + +* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/rust-actix.mdx b/pages/guides/rust-actix.mdx deleted file mode 100644 index b5872d2e..00000000 --- a/pages/guides/rust-actix.mdx +++ /dev/null @@ -1,188 +0,0 @@ ---- -title: Rust (Actix Web) ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This example uses [`actix-web`](https://actix.rs), a popular Rust web framework. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-rust1.75-actix-web4/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-rust1.75-actix-web4/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:8080 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-rust175-actix-web4-3pj27 - ├────────── uuid:33e9b4ba-58d7-4b3a-b9dd-4c406c0e7b07 - ├───────── state: running - ├─────────── url: https://autumn-silence-wupu2nus.fra.unikraft.app - ├───────── image: http-rust175-actix-web4@sha256:11723705230f0f4545d2be7e4867dc67b396870769e91f05e2fa6d9da94f9b59 - ├───── boot time: 11.67 ms - ├──────── memory: 128 MiB - ├─────── service: autumn-silence-wupu2nus - ├── private fqdn: http-rust175-actix-web4-3pj27.internal - ├──── private ip: 172.16.3.3 - └────────── args: /server -``` - -In this case, the instance name is `http-rust175-actix-web4-3pj27` and the address is `https://autumn-silence-wupu2nus.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the Rust-based HTTP web server: - -```bash -curl https://autumn-silence-wupu2nus.fra.unikraft.app -curl https://autumn-silence-wupu2nus.fra.unikraft.app/hey -``` -```text -Hello world! -Hey there! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-rust175-actix-web4-3pj27 autumn-silence-wupu2nus.fra.unikraft.app running 10 minutes ago http-rust175-actix-web4@sha256:11723705230f0f4545d2be7... 128 MiB /server 11672us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove http-rust175-actix-web4-3pj27 -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `src/main.rs`: the actual implementation -* `Cargo.toml`: the Cargo package manager configuration file -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```yaml - spec: v0.6 - - runtime: base:latest - - rootfs: ./Dockerfile - - cmd: ["/server"] - ``` - - - ```rust - use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder}; - - #[get("/")] - async fn hello() -> impl Responder { - HttpResponse::Ok().body("Hello world!") - } - - #[post("/echo")] - async fn echo(req_body: String) -> impl Responder { - HttpResponse::Ok().body(req_body) - } - - async fn manual_hello() -> impl Responder { - HttpResponse::Ok().body("Hey there!") - } - - #[actix_web::main] - async fn main() -> std::io::Result<()> { - HttpServer::new(|| { - App::new() - .service(hello) - .service(echo) - .route("/hey", web::get().to(manual_hello)) - }) - .bind(("0.0.0.0", 8080))? - .run() - .await - } - ``` - - - ```toml - [package] - name = "hello" - version = "0.0.0" - edition = "2021" - publish = false - - [dependencies] - actix-web = "4" - ``` - - - ```dockerfile - FROM rust:1.75.0-bookworm AS build - - RUN cargo new --bin app - WORKDIR /app - COPY Cargo.toml ./ - COPY src ./src - RUN cargo build --release - - # This stage is optional but reduces overall image size - FROM scratch - COPY --from=build /app/target/release/hello /server - COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/ - COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/ - COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/ - COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ - ``` - - - -The following options are available for customizing the app: - -* If you only update the implementation in the `src/main.rs` source file, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - If you add new Rust source code files, be sure to configure required dependencies in the `Cargo.toml` file. - -* If you build a new executable, update the `cmd` line in the `Kraftfile` and replace `/server` with the path to the new executable. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/rust-rocket.mdx b/pages/guides/rust-rocket.mdx deleted file mode 100644 index b0520a77..00000000 --- a/pages/guides/rust-rocket.mdx +++ /dev/null @@ -1,241 +0,0 @@ ---- -title: Rust (Rocket) ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This example uses [`Rocket`](https://rocket.rs/), a popular Rust web framework. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-rust1.75-rocket0.5/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-rust1.75-rocket0.5 -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:8080 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-rust175-rocket05-tuwq3 - ├────────── uuid: b6fe13e4-93b7-402b-bdec-1bc4d81bc275 - ├───────── state: running - ├─────────── url: https://empty-bobo-n3htmpye.fra.unikraft.app - ├───────── image: http-rust175-rocket05@sha256:23a7a6e155758e6e8f75e9570f0aec5fb744f08c1bad2454d7386367c5ea45d6 - ├───── boot time: 17.41 ms - ├──────── memory: 128 MiB - ├─────── service: empty-bobo-n3htmpye - ├── private fqdn: http-rust175-rocket05-tuwq3.internal - ├──── private ip: 172.16.6.6 - └────────── args: /server -``` - -In this case, the instance name is `http-rust175-rocket05-tuwq3` and the address is `https://empty-bobo-n3htmpye.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query any of the Rocket server's paths, for example: - -```bash -curl https://empty-bobo-n3htmpye.fra.unikraft.app/wave/Rocketeer/100 -``` -```text -👋 Hello, 100 year old named Rocketeer! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-rust175-rocket05-tuwq3 empty-bobo-n3htmpye.fra.unikraft.app running 1 minute ago http-rust175-rocket05@sha256:23a7a6... 128 MiB /server 17412us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove http-rust175-rocket05-tuwq3 -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `src/main.rs`: the actual server -* `Cargo.toml`: the Cargo package manager configuration file -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```yaml -spec: v0.6 - -runtime: base:latest - -rootfs: ./Dockerfile - -cmd: ["/server"] - ``` - - - ```rust -#[macro_use] extern crate rocket; - -#[cfg(test)] mod tests; - -#[derive(FromFormField)] -enum Lang { - #[field(value = "en")] - English, - #[field(value = "ru")] - #[field(value = "ру")] - Russian -} - -#[derive(FromForm)] -struct Options<'r> { - emoji: bool, - name: Option<&'r str>, -} - -// Try visiting: -// http://127.0.0.1:8000/hello/world -#[get("/world")] -fn world() -> &'static str { - "Hello, world!" -} - -// Try visiting: -// http://127.0.0.1:8000/hello/мир -#[get("/мир")] -fn mir() -> &'static str { - "Привет, мир!" -} - -// Try visiting: -// http://127.0.0.1:8000/wave/Rocketeer/100 -#[get("//")] -fn wave(name: &str, age: u8) -> String { - format!("👋 Hello, {} year old named {}!", age, name) -} - -// Note: without the `..` in `opt..`, we'd need to pass `opt.emoji`, `opt.name`. -// -// Try visiting: -// http://127.0.0.1:8000/?emoji -// http://127.0.0.1:8000/?name=Rocketeer -// http://127.0.0.1:8000/?lang=ру -// http://127.0.0.1:8000/?lang=ру&emoji -// http://127.0.0.1:8000/?emoji&lang=en -// http://127.0.0.1:8000/?name=Rocketeer&lang=en -// http://127.0.0.1:8000/?emoji&name=Rocketeer -// http://127.0.0.1:8000/?name=Rocketeer&lang=en&emoji -// http://127.0.0.1:8000/?lang=ru&emoji&name=Rocketeer -#[get("/?&")] -fn hello(lang: Option, opt: Options<'_>) -> String { - let mut greeting = String::new(); - if opt.emoji { - greeting.push_str("👋 "); - } - - match lang { - Some(Lang::Russian) => greeting.push_str("Привет"), - Some(Lang::English) => greeting.push_str("Hello"), - None => greeting.push_str("Hi"), - } - - if let Some(name) = opt.name { - greeting.push_str(", "); - greeting.push_str(name); - } - - greeting.push('!'); - greeting -} - -#[launch] -fn rocket() -> _ { - rocket::build() - .mount("/", routes![hello]) - .mount("/hello", routes![world, mir]) - .mount("/wave", routes![wave]) -} - ``` - - - ```toml -[package] -name = "hello" -version = "0.0.0" -edition = "2021" -publish = false - -[dependencies] -rocket = "0.5.0" - ``` - - - ```dockerfile -FROM rust:1.75.0-bookworm AS build - -RUN cargo new --bin app -WORKDIR /app -COPY Cargo.toml ./ -COPY Rocket.toml ./ -COPY src ./src -RUN cargo build --release - -FROM scratch - -COPY ./Rocket.toml /Rocket.toml -COPY --from=build /app/target/release/hello /server -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/ -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/ -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/ -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ - ``` - - - -The following options are available for customizing the app: - -* If you only update the implementation in the `src/main.rs` source file, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - If you add new Rust source code files, be sure to configure required dependencies in the `Cargo.toml` file. - -* If you build a new executable, update the `cmd` line in the `Kraftfile` and replace `/server` with the path to the new executable. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/rust-tokio.mdx b/pages/guides/rust-tokio.mdx deleted file mode 100644 index 3c29b6fa..00000000 --- a/pages/guides/rust-tokio.mdx +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: Rust (Tokio) ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This example uses [`Tokio`](https://tokio.rs/), a popular Rust asynchronous runtime. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/http-rust1.75-tokio/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/http-rust1.75-tokio/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:8080 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: http-rust175-tokio-6gxsp - ├────────── uuid: d5719f64-0653-42d7-b2de-aa6dee0ce467 - ├───────── state: running - ├─────────── url: https://empty-dawn-3coedrce.fra.unikraft.app - ├───────── image: http-rust175-tokio@sha256:0ce75912711aa2329232a2ca6c3ccb7a244b6d546fafc081f815c2fde8224856 - ├───── boot time: 21.41 ms - ├──────── memory: 128 Mi - ├─────── service: empty-dawn-3coedrce - ├── private fqdn: http-rust175-tokio-6gxsp.internal - ├──── private ip: 172.16.6.3 - └────────── args: /server -``` - -In this case, the instance name is `http-rust175-tokio-6gxsp` and the address is `https://empty-dawn-3coedrce.fra.unikraft.app. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the Tokio-based HTTP web server: - -```bash -curl https://empty-dawn-3coedrce.fra.unikraft.app -``` -```text -Hello, World! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-rust175-tokio-6gxsp empty-dawn-3coedrce.fra.unikraft.app running 1 minute ago http-rust175-tokio@sha256:0ce75912... 128 MiB /server 21412us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove http-rust175-tokio-6gxsp -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `src/main.rs`: the actual server -* `Cargo.toml`: the Cargo package manager configuration file -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```yaml -spec: v0.6 - -runtime: base:latest - -rootfs: ./Dockerfile - -cmd: ["/server"] - ``` - - - ```rust -use std::net::SocketAddr; -use tokio::net::TcpListener; -use tokio::io::{AsyncReadExt, AsyncWriteExt}; - -#[tokio::main] -async fn main() -> Result<(), Box> { - let addr = SocketAddr::from(([0, 0, 0, 0], 8080)); - let listener = TcpListener::bind(&addr).await?; - - println!("Listening on: http://{}", addr); - - loop { - let (mut stream, _) = listener.accept().await?; - - tokio::spawn(async move { - loop { - let mut buffer = [0; 1024]; - let _ = stream.read(&mut buffer).await; - - let contents = "Hello, World!\r\n"; - let content_length = contents.len(); - let response = format!("HTTP/1.1 200 OK\r\nContent-Length: {content_length}\r\n\r\n{contents}"); - let _ = stream.write_all(response.as_bytes()).await; - } - }); - } -} - ``` - - - ```toml -[package] -name = "http-tokio" -version = "0.1.0" -edition = "2021" - - -[dependencies] -tokio = {version = "1", features = ["rt-multi-thread", "net", "time", "macros", "io-util"] } - ``` - - - ```dockerfile -FROM rust:1.75.0-bookworm AS build - -WORKDIR /src - -COPY ./src /src/src -COPY ./Cargo.toml /src/Cargo.toml - -RUN cargo build - -FROM scratch - -COPY --from=build src/target/debug/http-tokio /server -COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 -COPY --from=build /lib/x86_64-linux-gnu/libgcc_s.so.1 /lib/x86_64-linux-gnu/libgcc_s.so.1 -COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 - ``` - - - -The following options are available for customizing the app: - -* If you only update the implementation in the `src/main.rs` source file, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - If you add new Rust source code files, be sure to configure required dependencies in the `Cargo.toml` file. - -* If you build a new executable, update the `cmd` line in the `Kraftfile` and replace `/server` with the path to the new executable. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/skipper.mdx b/pages/guides/skipper.mdx deleted file mode 100644 index 63acd702..00000000 --- a/pages/guides/skipper.mdx +++ /dev/null @@ -1,94 +0,0 @@ ---- -title: Skipper ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This example uses [`Skipper`](https://opensource.zalando.com/skipper/), an HTTP router and reverse proxy for service composition - -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/skipper/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/skipper/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:9090 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: skipper-mx4ai - ├────────── uuid: 34e3d740-c2b0-4644-b7e1-647350f688dc - ├───────── state: running - ├─────────── url: https://aged-sea-o7d3c42s.fra.unikraft.app - ├───────── image: skipper@sha256:5483eaf3612cca2116ceaab9be42557686324f1d30337ae15d0495eef63d0386 - ├───── boot time: 43.71 ms - ├──────── memory: 128 MiB - ├─────── service: aged-sea-o7d3c42s - ├── private fqdn: skipper-mx4ai.internal - ├──── private ip: 172.16.6.4 - └────────── args: /usr/bin/skipper -address :9090 -routes-file /etc/skipper/example.eskip -``` - -In this case, the instance name is `skipper-mx4ai` and the address is `https://aged-sea-o7d3c42s.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of Skipper. - -```bash -curl https://aged-sea-o7d3c42s.fra.unikraft.app -``` -```text -Hello, world from Skipper on Unikraft! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -skipper-mx4ai aged-sea-o7d3c42s.fra.unikraft.app running 1 minute ago skipper@sha256:5483eaf... 128 MiB /usr/bin/skipper -address :9090 -routes-f... 43709us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove skipper-mx4ai -``` - -## Customize your app - -To customize Skipper you can change the `example.eskip` configuration file. - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/skipper0.18.mdx b/pages/guides/skipper0.18.mdx new file mode 100644 index 00000000..2f62b833 --- /dev/null +++ b/pages/guides/skipper0.18.mdx @@ -0,0 +1,184 @@ +--- +title: "Skipper" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This example uses [`Skipper`](https://opensource.zalando.com/skipper/), an HTTP router and reverse proxy for service composition + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/skipper0.18/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/skipper0.18/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /skipper018:latest +unikraft run --scale-to-zero policy=on,cooldown-time=1000,stateful=true --metro fra -p 443:9090/tls+http -m 256M --image /skipper018:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero on --scale-to-zero-stateful --scale-to-zero-cooldown 1s -p 443:9090/tls+http -M 256Mi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: skipper018-mx4ai +uuid: 34e3d740-c2b0-4644-b7e1-647350f688dc +state: starting +image: /skipper018 +resources: + memory: 256MiB + vcpus: 1 +service: + uuid: b32c9035-d669-79fa-9955-9ad52cd1fcb4 + name: aged-sea-o7d3c42s + domains: + - fqdn: aged-sea-o7d3c42s.fra.unikraft.app +networks: +- uuid: 70cfb329-9ab3-fc8c-aff9-a3bbbbeb70f3 + private-ip: 10.0.6.4 + mac: 12:b0:32:1b:02:7b +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: skipper018-mx4ai + ├───────── uuid: 34e3d740-c2b0-4644-b7e1-647350f688dc + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://aged-sea-o7d3c42s.fra.unikraft.app + ├──────── image: oci://unikraft.io//skipper018@sha256:5483eaf3612cca2116ceaab9be42557686324f1d30337ae15d0495eef63d0386 + ├─────── memory: 256 MiB + ├────── service: aged-sea-o7d3c42s + ├─ private fqdn: skipper018-mx4ai.internal + └─── private ip: 10.0.6.4 +``` + + + +In this case, the instance name is `skipper018-mx4ai` and the address is `https://aged-sea-o7d3c42s.fra.unikraft.app`. +They're different for each run. + +Use `curl` to query the Unikraft Cloud instance of Skipper. + +```bash +curl https://aged-sea-o7d3c42s.fra.unikraft.app +``` + +```text +Hello, world from Skipper on Unikraft! +``` + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra skipper018-mx4ai running /skipper018 256MiB 1 aged-sea-o7d3c42s.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +skipper018-mx4ai aged-sea-o7d3c42s.fra.unikraft.app running 1 minute ago oci://unikraft.io//skipper018@sha256:... 256 MiB 1 43.71 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete skipper018-mx4ai +``` + +```bash title="kraft" +kraft cloud instance remove skipper018-mx4ai +``` + + + +## Customize your app + +To customize Skipper you can change the `example.eskip` configuration file. + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/solidjs.mdx b/pages/guides/solidjs.mdx deleted file mode 100644 index 1027146b..00000000 --- a/pages/guides/solidjs.mdx +++ /dev/null @@ -1,120 +0,0 @@ ---- -title: SolidJS ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide shows how to deploy a [Solid Start](https://start.solidjs.com) app. -To do so, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/node21-solid-start/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/node21-solid-start/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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 the app on Unikraft Cloud: - -```bash -kraft cloud deploy -p 443:3000 -M 256 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: node21-solid-start-lvoa2 - ├────────── uuid: 4e6ccb1f-0533-4dc1-be67-eca8dfc1f8c6 - ├───────── state: running - ├─────────── url: https://long-star-1tms9h1z.fra.unikraft.app - ├───────── image: node21-solid-start@sha256:eb2e79b2fc5c28bb43923a1fc4931db94ebc3f939a6fbe00d06189c0ae2e02fd - ├───── boot time: 67.65 ms - ├──────── memory: 256 MiB - ├─────── service: long-star-1tms9h1z - ├── private fqdn: node21-solid-start-lvoa2.internal - ├──── private ip: 172.16.6.8 - └────────── args: /usr/bin/node /usr/src/server/index.mjs -``` - -In this case, the instance name is `node21-solid-start-lvoa2` and the address is `https://long-star-1tms9h1z.fra.unikraft.app`. -They're different for each run. -You can now point your browser at the address to see your deployed instance. - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -node21-solid-start-lvoa2 long-star-1tms9h1z.fra.unikraft.app running 1 minutes ago node21-sol... 256 MiB /usr/bin/node /usr/src/server... 67.65 ms -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove node21-solid-start-lvoa2 -``` - -## 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 -* `src/`: server source files - - - ```yaml - spec: v0.6 - - runtime: node:21 - - rootfs: ./Dockerfile - - cmd: ["/usr/bin/node", "/usr/src/server.js"] - ``` - - - ```dockerfile -FROM node:21-alpine AS build - -WORKDIR /usr/src - -COPY . /usr/src/ - -RUN set -xe && \ - npm instal -g pnpm && \ - pnpm install && \ - pnpm build - -FROM scratch - -COPY --from=build /usr/src/.output/ /usr/src/ - ``` - - - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/spin-wagi-http.mdx b/pages/guides/spin-wagi-http.mdx new file mode 100644 index 00000000..14b24851 --- /dev/null +++ b/pages/guides/spin-wagi-http.mdx @@ -0,0 +1,218 @@ +--- +title: "Spin" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This guide explains how to create and deploy a simple Spin HTTP app. +This guide comes from [Spin's `spin-wagi-http` example](https://github.com/fermyon/spin/tree/v2.1.0/examples/spin-wagi-http). +It shows how to run a Spin app serving routes from two programs written in different languages (Rust and C++). + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/spin-wagi-http/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/spin-wagi-http/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /spin-wagi-http:latest +unikraft run --scale-to-zero policy=on,cooldown-time=1000,stateful=true --metro fra -p 443:3000/tls+http -m 4G --image /spin-wagi-http:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero on --scale-to-zero-stateful --scale-to-zero-cooldown 1s -p 443:3000/tls+http -M 4Gi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: spin-wagi-http-is72r +uuid: 045c1bda-0f2e-4f8b-98c7-a208bfa7d143 +state: starting +image: /spin-wagi-http +resources: + memory: 4096MiB + vcpus: 1 +service: + uuid: 9b5b88fd-16ce-3db6-a828-ee885647d820 + name: damp-bobo-wg43p36e + domains: + - fqdn: damp-bobo-wg43p36e.fra.unikraft.app +networks: +- uuid: db3851f6-ace1-8601-b6fa-925b7fdf8390 + private-ip: 10.0.28.16 + mac: 12:b0:fc:f5:09:d5 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: spin-wagi-http-is72r + ├───────── uuid: 045c1bda-0f2e-4f8b-98c7-a208bfa7d143 + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://damp-bobo-wg43p36e.fra.unikraft.app + ├──────── image: oci://unikraft.io//spin-wagi-http@sha256:57a5151996d83332af6da521e1cd92271a8c3ac7ae26bc44a7c0dbbc0a30e577 + ├─────── memory: 4096 MiB + ├────── service: damp-bobo-wg43p36e + ├─ private fqdn: spin-wagi-http-is72r.internal + └─── private ip: 10.0.28.16 +``` + + + +In this case, the instance name is `spin-wagi-http-is72r` and the address is `https://damp-bobo-wg43p36e.fra.unikraft.app`. +They're different for each run. + +Then `curl` the hello route: + +```bash +curl -i https://damp-bobo-wg43p36e.fra.unikraft.app/hello + +Hello, Fermyon! +``` + +And `curl` the goodbye route: + +```bash +curl -i https://damp-bobo-wg43p36e.fra.unikraft.app/goodbye + +Goodbye, Fermyon! +``` + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra spin-wagi-http-is72r running /spin-wagi-http 4.0GiB 1 damp-bobo-wg43p36e.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +spin-wagi-http-is72r damp-bobo-wg43p36e.fra.unikraft.app running 1 minute ago oci://unikraft.io//spin-wagi-http@sha2... 4.0 GiB 1 300.06 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete spin-wagi-http-is72r +``` + +```bash title="kraft" +kraft cloud instance remove spin-wagi-http-is72r +``` + + + +## Customize your app + +To customize the app, update the files in the repository, listed below: + +* `wagi-http-cpp`: C++ server handling the hello route +* `http-rust`: Rust server handling the goodbye route +* `Kraftfile`: the Unikraft Cloud specification +* `Dockerfile`: the Docker-specified app filesystem +* `spin.toml`: The Spin TOML configuration file + +Lines in the `Kraftfile` have the following roles: + +* `spec: v0.7`: The current `Kraftfile` specification version is `0.7`. + +* `runtime: base-compat:latest`: The runtime kernel to use is the base compatibility kernel. + +* `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: ["/usr/bin/spin", "up", "--from", "/app/spin.toml", "--listen", "0.0.0.0:3000"]`: Use `spin` as the command to start the app, with the given parameters. + +The following options are available for customizing the app: + +* If only updating the existing files under the `wagi-http-cpp` and `http-rust` directories, you don't need to make any other changes. + +* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. + +* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/spin.mdx b/pages/guides/spin.mdx deleted file mode 100644 index d1e2cc44..00000000 --- a/pages/guides/spin.mdx +++ /dev/null @@ -1,220 +0,0 @@ ---- -title: Spin ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a simple Spin HTTP app. -This guide comes from [Spin's `spin-wagi-http` example](https://github.com/fermyon/spin/tree/v2.1.0/examples/spin-wagi-http). -It shows how to run a Spin app serving routes from two programs written in different languages (Rust and C++). -Both the Spin executor and the Wagi executor on Unikraft Cloud. -To run it, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/spin-wagi-http/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/spin-wagi-http/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:3000 -M 2048 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: spin-wagi-http-is72r - ├────────── uuid: 045c1bda-0f2e-4f8b-98c7-a208bfa7d143 - ├───────── state: running - ├─────────── url: https://damp-bobo-wg43p36e.fra.unikraft.app - ├───────── image: spin-wagi-http@sha256:57a5151996d83332af6da521e1cd92271a8c3ac7ae26bc44a7c0dbbc0a30e577 - ├───── boot time: 300.06 ms - ├──────── memory: 2048 MiB - ├─────── service: damp-bobo-wg43p36e - ├── private fqdn: spin-wagi-http-is72r.internal - ├──── private ip: 172.16.28.16 - └────────── args: /usr/bin/spin up --from /app/spin.toml --listen 0.0.0.0:3000 -``` - -In this case, the instance name is `spin-wagi-http-is72r` and the address is `https://damp-bobo-wg43p36e.fra.unikraft.app`. -They're different for each run. - -Then `curl` the hello route: - -```bash -curl -i https://damp-bobo-wg43p36e.fra.unikraft.app/hello - -Hello, Fermyon! -``` - -And `curl` the goodbye route: - -```bash -curl -i https://damp-bobo-wg43p36e.fra.unikraft.app/goodbye - -Goodbye, Fermyon! -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -spin-wagi-http-is72r damp-bobo-wg43p36e.fra.unikraft.app running 1 minute ago spin-wagi-http@sha2... 2.0 GiB /usr/bin/spin up --from /app/spin.tom... 300064us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove spin-wagi-http-is72r -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `wagi-http-cpp`: C++ server handling the hello route -* `http-rust`: Rust server handling the goodbye route -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem -* `spin.toml`: The Spin TOML configuration file - - - - ```yaml -spec: v0.6 - -runtime: spin:latest - -rootfs: ./Dockerfile - -cmd: ["/usr/bin/spin", "up", "--from", "/app/spin.toml", "--listen", "0.0.0.0:3000"] - ``` - - - ```dockerfile -FROM rust:1.75.0-bookworm AS spin - -ARG SPIN_VERSION=2.1.0 - -WORKDIR /spin - -RUN set -xe; \ - apt-get update; \ - apt-get install -y --no-install-recommends \ - ca-certificates \ - make \ - wget; - -RUN set -xe; \ - wget -q -O spin.tar.gz "https://github.com/fermyon/spin/releases/download/v${SPIN_VERSION}/spin-v${SPIN_VERSION}-linux-amd64.tar.gz"; \ - tar xzvf ./spin.tar.gz; \ - mv ./spin /usr/bin/spin; \ - rm -rf spin.tar.gz; \ - rustup target add wasm32-wasi - -ARG WASI_SDK_VERSION=21 - -WORKDIR /wasi - -RUN set -xe; \ - wget -q -O wasi-sdk.tar.gz "https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-${WASI_SDK_VERSION}/wasi-sdk-${WASI_SDK_VERSION}.0-linux.tar.gz"; \ - mkdir -p /opt/wasi-sdk; \ - tar xzvf ./wasi-sdk.tar.gz --strip-components 1 -C /opt/wasi-sdk/; \ - rm -rf ./wasi-sdk.tar.gz; - -FROM spin AS build - -COPY ./http-rust /app/http-rust -WORKDIR /app/http-rust -RUN cargo build --release - -COPY ./wagi-http-cpp /app/wagi-http-cpp -WORKDIR /app/wagi-http-cpp -RUN make build - -FROM scratch - -COPY --from=build /app /app -COPY ./spin.toml /app/spin.toml - ``` - - - ```toml - spin_manifest_version = 2 - -[app] -description = "A hello world app that serves content from a C++ program and a Rust program" -name = "spin-wagi-hello" -version = "1.0.0" - -[[trigger.http]] -route = "/hello" -component = "hello" -executor = { type = "wagi" } # _start (the default entrypoint) is automatically mapped to main() - -[[trigger.http]] -route = "/goodbye" -component = "goodbye" -executor = { type = "http" } - -[component.hello] -source = "wagi-http-cpp/main.wasm" -[component.hello.build] -command = "make build -C wagi-http-cpp" - -[component.goodbye] -source = "http-rust/target/wasm32-wasi/release/goodbyerust.wasm" -[component.goodbye.build] -command = "cargo build --target wasm32-wasi --release --manifest-path http-rust/Cargo.toml" - ``` - - - -Lines in the `Kraftfile` have the following roles: - -* `spec: v0.6`: The current `Kraftfile` specification version is `0.6`. - -* `runtime: spin:latest`: The Unikraft runtime kernel to use is Spin. - -* `rootfs: ./Dockerfile`: Build the app root filesystem using the `Dockerfile`. - -* `cmd: ["/usr/bin/spin", "up", "--from", "/app/spin.toml", "--listen", "0.0.0.0:3000"]`: Use `spin` as the command to start the app, with the given parameters. - -The following options are available for customizing the app: - -* If only updating the existing files under the `wagi-http-cpp` and `http-rust` directories, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/springboot.mdx b/pages/guides/springboot.mdx deleted file mode 100644 index 2d466241..00000000 --- a/pages/guides/springboot.mdx +++ /dev/null @@ -1,192 +0,0 @@ ---- -title: "Run a Spring Boot app" ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a Spring Boot web server. -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/java17-springboot3.2.x/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/java17-springboot3.2.x/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -M 1024 -p 443:8080 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: java17-springboot32x-qseeo - ├────────── uuid: b081166d-a2a0-43af-982d-1aa17f06b5c4 - ├───────── state: running - ├─────────── url: https://long-dust-si7xsngk.fra.unikraft.app - ├───────── image: java17-springboot32x@sha256:cc2f2ad18ce8e36b8e8f4debee096fef7b0bb8b47762575a2ba5a9de8199c64a - ├───── boot time: 153.97 ms - ├──────── memory: 1024 MiB - ├─────── service: long-dust-si7xsngk - ├── private fqdn: java17-springboot32x-qseeo.internal - ├──── private ip: 172.16.6.2 - └────────── args: /usr/lib/jvm/java-17-openjdk-amd64/bin/java -jar /usr/src/demo-0.0.1-SNAPSHOT.jar -``` - -In this case, the instance name is `java17-springboot32x-qseeo` and the address is `https://long-dust-si7xsngk.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Spring Boot server's `hello` endpoint: - -```bash -curl https://long-dust-si7xsngk.fra.unikraft.app/hello -``` -```text -Hello World! -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove -``` - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `DemoApplication.java`: the server -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - - - - ```java -package com.example.demo; -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -@SpringBootApplication -@RestController -public class DemoApplication { - public static void main(String[] args) { - SpringApplication.run(DemoApplication.class, args); - } - @GetMapping("/hello") - public String hello(@RequestParam(value = "name", defaultValue = "World") String name) { - return String.format("Hello %s!", name); - } -} - ``` - - - ```yaml -spec: v0.6 - -runtime: java:17 - -rootfs: ./Dockerfile - -cmd: ["/usr/lib/jvm/java-17-openjdk-amd64/bin/java", "-jar", "/usr/src/demo-0.0.1-SNAPSHOT.jar"] - ``` - - - ```dockerfile -FROM --platform=linux/x86_64 debian:bookworm AS build - -RUN set -xe ; \ - apt -yqq update ; \ - apt -yqq install default-jre default-jdk curl unzip ; - -RUN ldconfig /usr/lib/jvm/java-17-openjdk-amd64/lib/ - -WORKDIR /src - -RUN set -xe ; \ - curl -G https://start.spring.io/starter.zip \ - -d appName=DemoApplication \ - -d artifactId=demo \ - -d bootVersion=3.2.2 \ - -d dependencies=web \ - -d description=com.example \ - -d javaVersion=17 \ - -d language=java \ - -d name=demo \ - -d packageName=com.example.demo \ - -d packaging=jar \ - -d type=maven-project \ - -d version=0.0.1-SNAPSHOT \ - -o demo.zip ; \ - unzip demo.zip \ - ; - -COPY DemoApplication.java src/main/java/com/example/demo/ - -RUN set -xe ; \ - ./mvnw compile package install - -RUN set -xe ; \ - mkdir /tmpdir - -FROM scratch - -COPY --from=build /tmpdir /tmp -COPY --from=build /src/target/demo-0.0.1-SNAPSHOT.jar /usr/src/demo-0.0.1-SNAPSHOT.jar - ``` - - - -Lines in the `Kraftfile` have the following roles: - -* `spec: v0.6`: The current `Kraftfile` specification version is `0.6`. - -* `runtime: java:17`: The Unikraft runtime kernel to use is Java. - -* `rootfs: ./Dockerfile`: Build the app root filesystem using the `Dockerfile`. - -* `cmd: ["/usr/lib/jvm/java-17-openjdk-amd64/bin/java", "-jar", "/usr/src/demo-0.0.1-SNAPSHOT.jar"]`: Use as the starting command of the instance. - -Lines in the `Dockerfile` have the following roles: - -* `FROM scratch`: Build the filesystem from the [`scratch` container image](https://hub.docker.com/_/scratch/), to [create a base image](https://docs.docker.com/build/building/base-images/). - -* `COPY DemoApplication.java src/main/java/com/example/demo/`: Copy the server implementation file in the Docker filesystem. - - -The following options are available for customizing the app: - -* If you only update the implementation in the `DemoApplication.java` source file, you don't need to make any other changes. - -* If you create any new source files, copy them into the app filesystem by using the `COPY` command in the `Dockerfile`. - -* More extensive changes may require extending the `Dockerfile` ([see `Dockerfile` syntax reference](https://docs.docker.com/engine/reference/builder/)). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/sveltekit.mdx b/pages/guides/sveltekit.mdx deleted file mode 100644 index 1cad997e..00000000 --- a/pages/guides/sveltekit.mdx +++ /dev/null @@ -1,236 +0,0 @@ ---- -title: SvelteKit ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide explains how to create and deploy a [SvelteKit](https://kit.svelte.dev/) app. -[SvelteKit](https://kit.svelte.dev/) builds on Svelte, a UI framework that uses a compiler to let you write breathtakingly concise components that do minimal work in the browser. - -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/node21-sveltekit/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/node21-sveltekit/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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 the app on Unikraft Cloud: - -```bash -kraft cloud deploy -p 443:3000 -M 256 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: node21-sveltekit-zmt39 - ├────────── uuid: cd5071b0-5605-4771-b75d-4789393e60de - ├───────── state: running - ├─────────── url: https://dark-fog-z18n0ej1.fra.unikraft.app - ├───────── image: node21-sveltekit@sha256:4cea210aef3513bd68490640b511ebcff2b867e9222028b9938faccffc21cb83 - ├───── boot time: 72.86ms - ├──────── memory: 256 MiB - ├─────── service: dark-fog-z18n0ej1 - ├── private fqdn: node21-sveltekit-zmt39.internal - ├──── private ip: 172.16.3.3 - └────────── args: /usr/bin/node /app/build/index.js -``` - -In this case, the instance name is `node21-sveltekit-zmt39` and the address is `https://dark-fog-z18n0ej1.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance: - -```bash -curl https://dark-fog-z18n0ej1.fra.unikraft.app -``` - -```text - - -[...] - -[...] -``` - -Or even better, point a browser at it 😀 -This will get you to play with the SvelteKit demo app. - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` - -```text -NAME FQDN STATE CREATED AT MEMORY ARGS BOOT TIME -node21-sveltekit-zmt39 dark-fog-z18n0ej1.fra.unikraft.app running 5 minutes ago node21-sveltekit@sha256:4cea210ae... 256 MiB /usr/bin/node /app/build/index.js 72.86 ms -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove node21-sveltekit-zmt39 -``` - - -## Customize your app - -To customize the app, update the files in the repository. -That is, you update the SvelteKit / Node / `npm`-specific files, or the Unikraft Cloud-specific files. -This section details each below. - - -### Updating the app - -Updating the SvelteKit app is reliant on making `npm`-related updates. - -[`npm`](https://www.npmjs.com/) is a package manager for Node. -It's used to install dependencies for Node apps. -`npm` uses a `package.json` file to list required dependencies (with versions). - -[`npm create svelte@latest`](https://kit.svelte.dev/docs/creating-a-project) generated the app files, using the [`@sveltejs/adapter-node`](https://kit.svelte.dev/docs/adapter-node) adapter. - -The `src/routes/sverdle/` directory contains the core implementation. -Detailed information on updating the implementation is part of the [official SvelteKit documentation](https://kit.svelte.dev/docs/introduction). - -Updates to the implementation will probably require updates to the `package.json` file. -The `package.json` file lists [`npm` app dependencies](https://docs.npmjs.com/cli/v8/configuring-npm/package-json#dependencies). - -#### Deploying locally - -Before deploying the SvelteKit app on Unikraft Cloud, you can deploy locally. -Assuming `npm` is installed, use the following commands: - -```bash -npm install -npm run dev -``` - -```text - VITE v5.2.6 ready in 807 ms - - ➜ Local: http://localhost:5173/ - ➜ Network: use --host to expose - ➜ press h + enter to show help -``` - -You can test the app locally by pointing your browser to `http://localhost:5173/`. - -### Sample specification - - - - ```yaml -spec: v0.6 - -runtime: node:21 - -rootfs: ./Dockerfile - -cmd: ["/usr/bin/node", "/app/build/index.js"] - ``` - - - ```dockerfile -FROM node:21-alpine AS deps - -WORKDIR /app - -COPY package.json /app - -RUN npm install - -FROM deps AS build - -COPY . /app - -RUN npm run build; \ - npm prune --production - -RUN <> /app/build/package.json -{"type": "module"} -EOF - -FROM scratch AS prod - -COPY --from=build /app/build /app/build -COPY --from=build /app/node_modules /app/node_modules - ``` - - - -Lines in the `Kraftfile` have the following roles: - -* `spec: v0.6`: The current `Kraftfile` specification version is `0.6`. - -* `runtime: node:21`: The Unikraft runtime kernel to use is Node 21. - -* `rootfs: ./Dockerfile`: Build the app root filesystem using the `Dockerfile`. - -* `cmd: [["/usr/bin/node", "/app/build/index.js"]]`: Use `/usr/bin/node /app/build/index.js` as the starting command of the instance. - -Lines in the `Dockerfile` have the following roles: - -* `FROM node:21-alpine AS deps`: Use the base image of the `node:21-alpine` container. - This provides the `npm` binary and other Node-related components. - Name the current image `deps`. - -* `WORKDIR /app`: Use `/app` as working directory. - All other commands in the `Dockerfile` run inside this directory. - -* `COPY package.json /app`: Copy the `npm` configuration file to be able to install dependencies. - -* `RUN npm install`: Install `npm` components listed in `packages.json`. - -* `FROM deps AS build`: Create a fresh checkpoint of the base image for the build phase. - -* `COPY . /app`: Copy contents of the current directory (the actual app files) in the Docker filesystem. - Note that paths in the `.dockerignore` file aren't copied. - -* `RUN npm run build; ...`: Build the files required for running the app standalone. - The app entry point is the resulting `/app/build/index.js` file that's used as the start command in the `Kraftfile`. - To run the app, configure it as a module. - -* `FROM scratch as prod`: Use [the `scratch` empty container`]() as the actual runtime environment. - It will only contain the required files from the build. - -* `COPY --from=build ...`: Copy existing files from new `build` image to the `scratch`-based image. - This means the app build directory (`/app/build`) and the required dependencies (`/app/node_modules`). - -#### Customization options - -It's unlikely you will have to update the `Kraftfile`. -The only potential update is the `cmd` option. -But since SvelteKit / `npm` generates the `/app/build/index.js` file, you're unlikely to update it. - -You also don't need to change the `Dockerfile`. -Updates to the app would be part of the `package.json` file or the `src/` directory. -And updating these won't affect the contents of the `Dockerfile`. -Still, if required, apps may require extending the `Dockerfile` with extra [`Dockerfile` commands](https://docs.docker.com/engine/reference/builder/). - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/traefik.mdx b/pages/guides/traefik.mdx index 389abc67..f117b26e 100644 --- a/pages/guides/traefik.mdx +++ b/pages/guides/traefik.mdx @@ -1,56 +1,109 @@ --- -title: Traefik +title: "Traefik" --- import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + This example uses the [`Traefik`](https://traefik.io/traefik/) cloud native app proxy. +To run it, follow these steps: -To run this example, follow these steps: +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). +:::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. +::: -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/traefik/` directory: +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/traefik/` directory: ```bash -git clone https://github.com/kraftcloud/examples +git clone https://github.com/unikraft-cloud/examples cd examples/traefik/ ``` -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): -```bash + + +```bash title="unikraft" +unikraft login +``` + +```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: -```bash -kraft cloud deploy -M 512 -p 443:80/tls+http -p 8080:8080/tls . + + +```bash title="unikraft" +unikraft build . --output /traefik:latest +unikraft run --scale-to-zero policy=on,cooldown-time=1000,stateful=true --metro fra -p 443:80/tls+http -p 8080:8080/tls -m 1G --image /traefik:latest ``` +```bash title="kraft" +kraft cloud deploy --scale-to-zero on --scale-to-zero-stateful --scale-to-zero-cooldown 1s -p 443:80/tls+http -p 8080:8080/tls -M 1Gi . +``` + + + The output shows the instance address and other details: -```ansi + + +```ansi title="unikraft" +metro: fra +name: traefik-wqe7e +uuid: 69d25b0b-1813-4a3f-88e6-64abbc78b359 +state: starting +image: /traefik +resources: + memory: 1024MiB + vcpus: 1 +service: + uuid: ec7570da-5700-01b5-aaa1-0734498c11eb + name: holy-cherry-rye39b1x + domains: + - fqdn: holy-cherry-rye39b1x.fra.unikraft.app +networks: +- uuid: b59f4362-dc72-7efe-477b-1efe227e1b08 + private-ip: 10.0.28.16 + mac: 12:b0:31:58:d7:d0 +timestamps: + created: just now +``` + +```ansi title="kraft" [●] Deployed successfully! │ - ├────────── name: traefik-wqe7e - ├────────── uuid: 69d25b0b-1813-4a3f-88e6-64abbc78b359 - ├───────── state: running - ├─────────── url: https://holy-cherry-rye39b1x.fra.unikraft.app - ├───────── image: traefik@sha256:f6dd913a81f6a057ceb9db7844222d7287b2a83f668cca88c73c2e85554cb526 - ├───── boot time: 53.66 ms - ├──────── memory: 512 MiB - ├─────── service: holy-cherry-rye39b1x - ├── private fqdn: traefik-wqe7e.internal - ├──── private ip: 172.16.28.16 - └────────── args: /usr/bin/traefik -configFile /etc/traefik/default.toml + ├───────── name: traefik-wqe7e + ├───────── uuid: 69d25b0b-1813-4a3f-88e6-64abbc78b359 + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://holy-cherry-rye39b1x.fra.unikraft.app + ├──────── image: oci://unikraft.io//traefik@sha256:f6dd913a81f6a057ceb9db7844222d7287b2a83f668cca88c73c2e85554cb526 + ├─────── memory: 1024 MiB + ├────── service: holy-cherry-rye39b1x + ├─ private fqdn: traefik-wqe7e.internal + └─── private ip: 10.0.28.16 ``` + + In this case, the instance name is `traefik-wqe7e` and the address is `https://holy-cherry-rye39b1x.fra.unikraft.app`. They're different for each run. @@ -59,6 +112,7 @@ Use `curl` to query the Unikraft Cloud instance of Traefik. ```bash curl https://holy-cherry-rye39b1x.fra.unikraft.app:8080/dashboard ``` + ```text Traefik ... ``` @@ -72,20 +126,46 @@ Please change default.toml as needed. You can list information about the instance by running: -```bash + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" kraft cloud instance list ``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -traefik-wqe7e holy-cherry-rye39b1x.fra.unikraft.app running 8 minutes ago traefik@sha256:f6dd913a8... 512 MiB /usr/bin/traefik -configFile /etc/traefik/... 53661us + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra traefik-wqe7e running /traefik 1024MiB 1 holy-cherry-rye39b1x.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +traefik-wqe7e holy-cherry-rye39b1x.fra.unikraft.app running 8 minutes ago oci://unikraft.io//traefik@sha256:... 1024 MiB 1 53.66 ms ``` + + When done, you can remove the instance: -```bash + + +```bash title="unikraft" +unikraft instances delete traefik-wqe7e +``` + +```bash title="kraft" kraft cloud instance remove traefik-wqe7e ``` + + ## Customize your app To customize Traefik app you can change the `default.toml` configuration file. @@ -94,8 +174,17 @@ To customize Traefik app you can change the `default.toml` configuration file. Use the `--help` option for detailed information on using Unikraft Cloud: -```bash + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" kraft cloud --help ``` -Or visit the [CLI Reference](/docs/cli). + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/tyk.mdx b/pages/guides/tyk.mdx deleted file mode 100644 index 4f59bc2c..00000000 --- a/pages/guides/tyk.mdx +++ /dev/null @@ -1,186 +0,0 @@ ---- -title: Tyk ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This example uses [`Tyk`](https://tyk.io/), an API gateway and management platform. -Tyk is used together with Redis to store API tokens and OAuth clients. - -To run this example, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/tyk/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/tyk/ -``` - -Make sure to log into KraftCloud by setting your token and a [metro](/docs/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# Set KraftCloud 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 KraftCloud: - -```bash -kraft cloud compose up -``` - -The output shows the [Compose](/docs/guides/features/compose) output: - -```text - i building service=tyk - i packaging service=tyk -[+] building rootfs... done! -[+] packaging index.unikraft.io/tyk-tyk... done! -[+] pushing index.unikraft.io/tyk-tyk:latest (kraftcloud/x86_64)... done! - i creating instance image=redis:latest - i no ports or service specified, disabling scale to zero - i creating instance image=index.unikraft.io/tyk-tyk@sha256:06f8ba3f2350e57717bd947f43f04a1ac44ab65010c8994488223eb042c30feb - i no ports or service specified, disabling scale to zero - i starting 2 instance(s) -``` - -To list information about the instances, use: - -```bash -kraft cloud compose ps -``` -```text -NAME FQDN STATE STATUS IMAGE MEMORY ARGS BOOT TIME -tyk-tyk funky-pond-45usfkxx.fra.unikraft.app running since 45secs tyk-tyk@sha256:06f8ba3... 128 MiB /usr/bin/tyk start --conf /etc/tyk.conf 38.12 ms -tyk-redis running since 45secs redis@sha256:d4604c6d80e7d57590f2c46659f2... 512 MiB /usr/bin/redis-server /etc/redis/redis.conf 18.69 ms -``` - -The Tyk and Redis instances are named `tyk-tyk` and `tyk-redis` (as defined in the `compose.yaml` file). -Only the Tyk instance is available as a public service. -Its address is `https://funky-pond-45usfkxx.fra.unikraft.app`. -It's different for each run. - -Use `curl` to query the Tyk instance on KraftCloud on the available address: - -```bash -curl https://funky-pond-45usfkxx.fra-test.unikraft.app/hello -``` -```text -{"status":"pass","version":"v5.3.0-dev","description":"Tyk GW","details":{"redis":{"status":"pass","componentType":"datastore","time":"2024-07-12T05:57:44Z"}}} -``` - -When done, you can bring down the instances: - -```bash -kraft cloud compose down -``` - -## Customize your app - -To customize the Tyk app, you can update: - -* `Kraftfile`: the KraftCloud specification -* `Dockerfile` / `rootfs/`: the Tyk filesystem (in this case the configuration file `/etc/tyk.conf`) -* `compose.yaml`: the Compose specification - - - - ```yaml - spec: v0.6 - - runtime: tyk:latest - - rootfs: ./Dockerfile - - cmd: ["/usr/bin/tyk", "start", "--conf", "/etc/tyk.conf"] - ``` - - - ```dockerfile - FROM scratch - - COPY ./rootfs / - ``` - - - ```text - { - "listen_address": "", - "listen_port": 8080, - "secret": "352d20ee67be67f6340b4c0605b044b7", - "template_path": "/tyk/templates", - "use_db_app_configs": false, - "app_path": "/opt/tyk-gateway/apps", - "middleware_path": "/opt/tyk-gateway/middleware", - "storage": { - "type": "redis", - "host": "tyk-redis.internal", - "port": 6379, - "username": "", - "password": "", - "database": 0, - "optimisation_max_idle": 2000, - "optimisation_max_active": 4000 - }, - "enable_analytics": false, - "analytics_config": { - "type": "", - "ignored_ips": [] - }, - "dns_cache": { - "enabled": false, - "ttl": 3600, - "check_interval": 60 - }, - "allow_master_keys": false, - "policies": { - "policy_source": "file" - }, - "hash_keys": true, - "hash_key_function": "murmur64", - "suppress_redis_signal_reload": false, - "force_global_session_lifetime": false, - "max_idle_connections_per_host": 500 - } - ``` - - - ```yaml - services: - - tyk: - build: . - ports: - - 443:8080 - - redis: - image: redis:latest - mem_reservation: 512M - networks: - tyk-network: - ``` - - - -It's unlikely you will have to update the `Kraftfile` specification. - -Update the contents of the `rootfs/etc/tyk.conf` file for a different configuration. - -You can also update the `Dockerfile` in order to extend the Tyk filesystem with extra data files or configuration files. - -The `compose.yaml` file can be update to assign different names, ports, network names or other [Compose](/docs/guides/features/compose)-specific configurations. - -## Learn more - -Use the `--help` option for detailed information on using KraftCloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/visual-studio-code-server.mdx b/pages/guides/visual-studio-code-server.mdx new file mode 100644 index 00000000..518bd44e --- /dev/null +++ b/pages/guides/visual-studio-code-server.mdx @@ -0,0 +1,223 @@ +--- +title: "Visual Studio Code Server" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +[Visual Studio Code](https://code.visualstudio.com/) is a source-code editor developed by Microsoft. +It includes support for debugging, syntax highlighting, intelligent code completion, snippets, code refactoring, and embedded Git. +It features a [Code server](https://code.visualstudio.com/docs/remote/vscode-server), which allows you to run Visual Studio Code remotely and access it through a web browser or your local Visual Studio Code client. + +This guide explains how to create and deploy a Visual Studio Code server app. +To run it, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/visual-studio-code-server` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/visual-studio-code-server/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft volume create --set metro=fra --set name=code-workspace --set size=1G + +unikraft build . --output /visual-studio-code-server:latest +unikraft run --metro fra -p 443:8443/tls+http -m 2G --volume code-workspace:/workspace --scale-to-zero policy=on,cooldown-time=4000,stateful=true -e PGUID=0 -e PGID=0 -e PASSWORD=unikraft -e SUDO_PASSWORD=unikraft -e DEFAULT_WORKSPACE="/workspace" --image /visual-studio-code-server:latest +``` + +```bash title="kraft" +kraft cloud volume create --name code-workspace --size 1Gi + +kraft cloud deploy --scale-to-zero on --scale-to-zero-stateful --scale-to-zero-cooldown 4s --name code-server -p 443:8443/tls+http -M 2Gi -v code-workspace:/workspace -e PGUID=0 -e PGID=0 -e PASSWORD=unikraft -e SUDO_PASSWORD=unikraft -e DEFAULT_WORKSPACE="/workspace" . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: code-server +uuid: c1a619a0-e222-4042-94b8-ba4b39353417 +state: starting +image: /visual-studio-code-server +resources: + memory: 2048MiB + vcpus: 1 +service: + uuid: 4a0866e4-3bec-3666-c4aa-61672de542e2 + name: blue-shape-chmxf1g4 + domains: + - fqdn: blue-shape-chmxf1g4.fra.unikraft.app +networks: +- uuid: dcec209f-31a6-b355-a88e-f5ac3edfa20e + private-ip: 10.0.0.49 + mac: 12:b0:ec:d2:df:1c +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: code-server + ├───────── uuid: c1a619a0-e222-4042-94b8-ba4b39353417 + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://blue-shape-chmxf1g4.fra.unikraft.app + ├──────── image: oci://unikraft.io//visual-studio-code-server@sha256:633ec8a8dcb342b093c6f055f84fc056ee1abe40ff56e98bd612c4b9d4ddffcb + ├─────── memory: 2048 MiB + ├────── service: blue-shape-chmxf1g4 + ├─ private fqdn: code-server.internal + └─── private ip: 10.0.0.49 +``` + + + +This will create a volume for data persistence, and mount it at `/workspace` inside the VM. + +In this case, the instance name is `code-server` and the address is `https://blue-shape-chmxf1g4.fra.unikraft.app`. +The name was preset, but the address is different for each run. +Enter the provided address into your browser of choice to access the Code server instance. + +You can list information about the volume by running: + + + +```bash title="unikraft" +unikraft volumes list +``` + +```bash title="kraft" +kraft cloud volume list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE SIZE CREATED +fra code-workspace mounted 1.0GiB 13 minutes ago +``` + +```ansi title="kraft" +NAME CREATED AT SIZE ATTACHED TO MOUNTED BY STATE PERSISTENT +code-workspace 13 minutes ago 1.0 GiB code-server code-server mounted true +``` + + + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra code-server standby /visual-studio-code-server 2.0GiB 1 blue-shape-chmxf1g4.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +code-server blue-shape-chmxf1g4.fra.unikraft.app standby standby oci://unikraft.io//visual-studio-code-server@sha256:... 2.0 GiB 1 8.45 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete code-server +``` + +```bash title="kraft" +kraft cloud instance remove code-server +``` + + + +The volume isn't removed by default, so you can recreate the instance and still have access to your old data. +Remove it using: + + + +```bash title="unikraft" +unikraft volume delete code-workspace +``` + +```bash title="kraft" +kraft cloud volume remove code-workspace +``` + + + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/vsftpd.mdx b/pages/guides/vsftpd.mdx new file mode 100644 index 00000000..a0b42e1e --- /dev/null +++ b/pages/guides/vsftpd.mdx @@ -0,0 +1,228 @@ +--- +title: "vsftpd" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This guide explains how to create and deploy a [vsftpd](https://security.appspot.com/vsftpd.html) app, to secure access to the files of your VM. +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/vsftpd` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/vsftpd/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft volume create --set metro=fra --set name=vsftpd-workspace --set size=1G + +unikraft build . --output /vsftpd:latest +unikraft run --metro fra --scale-to-zero policy=on,cooldown-time=40000,stateful=true -p 20:20/tls -p 21:21/tls -p 222:22/tls -p 990:990/tls -p 10100:10100/tls -m 1G --volume vsftpd-workspace:/root --image /vsftpd:latest +``` + +```bash title="kraft" +kraft cloud volume create --name vsftpd-workspace --size 1Gi + +kraft cloud deploy --scale-to-zero on --scale-to-zero-stateful --scale-to-zero-cooldown 3s --name vsftpd -p 20:20/tls -p 21:21/tls -p 222:22/tls -p 990:990/tls -p 10100:10100/tls -M 1Gi -v vsftpd-workspace:/root . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: vsftpd +uuid: 186a46a0-7c89-4bfd-83a8-649bcc60a96e +state: starting +image: /vsftpd +resources: + memory: 1024MiB + vcpus: 1 +service: + uuid: 4814b43a-c1d3-48f0-ef3e-9dba8bcaba25 + name: broken-orangutan-jypu2z53 + domains: + - fqdn: broken-orangutan-jypu2z53.fra.unikraft.app +networks: +- uuid: 6adc6c29-5c9b-e472-70ff-fc3f3816d5a2 + private-ip: 10.0.0.109 + mac: 12:b0:17:ff:e4:c7 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: vsftpd + ├───────── uuid: 186a46a0-7c89-4bfd-83a8-649bcc60a96e + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://broken-orangutan-jypu2z53.fra.unikraft.app + ├──────── image: oci://unikraft.io//vsftpd@sha256:31aad1619c31f499b11f1bef8fead6e6df76f235a57add011e5e414a3f51ee64 + ├─────── memory: 1024 MiB + ├────── service: broken-orangutan-jypu2z53 + ├─ private fqdn: vsftpd.internal + └─── private ip: 10.0.0.109 +``` + + + +This will create a volume for data persistence, and mount it at `/root` inside the VM. + +In this case, the instance name is `vsftpd` and the address is `https://broken-orangutan-jypu2z53.fra.unikraft.app`. +The name was preset, but the address is different for each run. + +**Note**: The `root` password defaults to `rootpass`. +Don't forget to change it inside the `Dockerfile` and update the commands below. + +You can access the FTP server using a client like `lftp`: + +```bash +lftp -u root,rootpass ftps://broken-orangutan-jypu2z53.fra.unikraft.app:21 +lftp root@broken-orangutan-jypu2z53.fra.unikraft.app:~> ls +``` + +You can list information about the volume by running: + + + +```bash title="unikraft" +unikraft volumes list +``` + +```bash title="kraft" +kraft cloud volume list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE SIZE CREATED +fra vsftpd-workspace mounted 1.0GiB 9 minutes ago +``` + +```ansi title="kraft" +NAME CREATED AT SIZE ATTACHED TO MOUNTED BY STATE PERSISTENT +vsftpd-workspace 9 minutes ago 1.0 GiB vsftpd vsftpd mounted true +``` + + + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra vsftpd standby /vsftpd 1.0GiB 1 broken-orangutan-jypu2z53.fra.unikraf… 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +vsftpd broken-orangutan-jypu2z53.fra.unikraft.app standby standby oci://unikraft.io//vsftpd@sha256:... 1.0 GiB 1 7.19 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete vsftpd +``` + +```bash title="kraft" +kraft cloud instance remove vsftpd +``` + + + +The volume isn't removed by default, so you can recreate the instance and still have access to your old data. +Remove it using: + + + +```bash title="unikraft" +unikraft volume delete vsftpd-workspace +``` + +```bash title="kraft" +kraft cloud volume remove vsftpd-workspace +``` + + + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/wazero-import-go.mdx b/pages/guides/wazero-import-go.mdx new file mode 100644 index 00000000..f46c6ac9 --- /dev/null +++ b/pages/guides/wazero-import-go.mdx @@ -0,0 +1,207 @@ +--- +title: "Wazero" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This example comes from [Wazero's "import go" example](https://github.com/tetratelabs/wazero/tree/main/examples/import-go) +and shows how to define, import and call a wasm blob from Go and run it on Unikraft Cloud. + +To run this example, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/wazero-import-go/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/wazero-import-go/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /wazero-import-go:latest +unikraft run --scale-to-zero policy=on,cooldown-time=1000 --metro fra -p 443:8080/tls+http -m 512M --image /wazero-import-go:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero on --scale-to-zero-cooldown 1s -p 443:8080/tls+http -M 512Mi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: wazero-import-go-r4dx8 +uuid: a763e1c3-bb38-475f-95b6-1e78d8ca74fc +state: starting +image: /wazero-import-go +resources: + memory: 512MiB + vcpus: 1 +service: + uuid: f38a171d-e283-24ca-1158-c7907948071e + name: cool-morning-camrrhsa + domains: + - fqdn: cool-morning-camrrhsa.fra.unikraft.app +networks: +- uuid: bc4bb64c-8185-3b83-8693-b1464ab4723e + private-ip: 10.0.6.7 + mac: 12:b0:05:55:02:fe +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: wazero-import-go-r4dx8 + ├───────── uuid: a763e1c3-bb38-475f-95b6-1e78d8ca74fc + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://cool-morning-camrrhsa.fra.unikraft.app + ├──────── image: oci://unikraft.io//wazero-import-go@sha256:865700d358ffb2751888798ec8f302d23310b1fcf84f4d3f17f79fc25ff71153 + ├─────── memory: 512 MiB + ├────── service: cool-morning-camrrhsa + ├─ private fqdn: wazero-import-go-r4dx8.internal + └─── private ip: 10.0.6.7 +``` + + + +In this case, the instance name is `wazero-import-go-r4dx8` and the address is `https://cool-morning-camrrhsa.fra.unikraft.app`. +They're different for each run. + +Use `curl` to query the Unikraft Cloud instance of the Go/wazero server: + +```bash +curl https://cool-morning-camrrhsa.fra.unikraft.app +``` + +```text +println >> 24 +log_i32 >> 24 +``` + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra wazero-import-go-r4dx8 running /wazero-import-go 512MiB 1 cool-morning-camrrhsa.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +wazero-import-go-r4dx8 cool-morning-camrrhsa.fra.unikraft.app running 1 minutes ago oci://unikraft.io//wazero-import-go@s... 512 MiB 1 20.04 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete wazero-import-go-r4dx8 +``` + +```bash title="kraft" +kraft cloud instance remove wazero-import-go-r4dx8 +``` + + + +## Background + +WebAssembly has neither a mechanism to get the current year, nor one to print to the console, so this example defines these in Go. +Like Go, WebAssembly functions are namespaced into modules instead of packages. +With Go only exported functions can import into another module. +`age-calculator.go` shows how to export functions using [HostModuleBuilder](https://pkg.go.dev/github.com/tetratelabs/wazero#HostModuleBuilder) and how a WebAssembly module defined in its [text format](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-format%E2%91%A0) imports it. +This only uses the text format for demonstration purposes, to show you what's going on. +It's likely, you will use another language to compile a Wasm (WebAssembly Module) binary, such as TinyGo. +Regardless of how wasm produces, the export/import mechanics are the same! + +### Using WASI + +WebAssembly System Interface (WASI) is a modular system interface for WebAssembly. +This uses an ad-hoc Go-defined function to print to the console. +An emerging specification standardizes system calls (like Go's [x/sys](https://pkg.go.dev/golang.org/x/sys/unix)) called WebAssembly System Interface [(WASI)](https://github.com/WebAssembly/WASI). +While this isn't yet a W3C standard, wazero includes a [wasi package](https://pkg.go.dev/github.com/tetratelabs/wazero/wasi). + +## Customize your app + +To customize the app, update the files in the repository, listed below: + +* `agecalculator.go`: The Go web server that calls the WASM/Wazero blog for the age calculation +* `Kraftfile`: the Unikraft Cloud specification +* `Dockerfile`: the Docker-specified app filesystem + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/wazero.mdx b/pages/guides/wazero.mdx deleted file mode 100644 index add6fb17..00000000 --- a/pages/guides/wazero.mdx +++ /dev/null @@ -1,119 +0,0 @@ ---- -title: Wazero ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This example comes from [Wazero's "import go" example](https://github.com/tetratelabs/wazero/tree/main/examples/import-go) -and shows how to define, import and call a wasm blob from Go and run it on Unikraft Cloud. -To run this it, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/wazero-import-go/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/wazero-import-go/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# 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: - -```bash -kraft cloud deploy -p 443:8080 . /age-calculator 2000 -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: wazero-import-go-r4dx8 - ├────────── uuid: a763e1c3-bb38-475f-95b6-1e78d8ca74fc - ├───────── state: running - ├─────────── url: https://cool-morning-camrrhsa.fra.unikraft.app - ├───────── image: wazero-import-go@sha256:865700d358ffb2751888798ec8f302d23310b1fcf84f4d3f17f79fc25ff71153 - ├───── boot time: 20.04 m - ├──────── memory: 512 MiB - ├─────── service: cool-morning-camrrhsa - ├── private fqdn: wazero-import-go-r4dx8.internal - ├──── private ip: 172.16.6.7 - └────────── args: /age-calculator 2000 -``` - -In this case, the instance name is `wazero-import-go-r4dx8` and the address is `https://cool-morning-camrrhsa.fra.unikraft.app`. -They're different for each run. - -Use `curl` to query the Unikraft Cloud instance of the Go/wazero server: - -```bash -curl https://cool-morning-camrrhsa.fra.unikraft.app -``` -```text -println >> 24 -log_i32 >> 24 -``` - -You can list information about the instance by running: - -```bash -kraft cloud instance list -``` -```text -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -wazero-import-go-r4dx8 cool-morning-camrrhsa.fra.unikraft.app running 1 minutes ag wazero-import-go@s... 512 MiB /age-calculator 2000 20040us -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove wazero-import-go-r4dx8 -``` - - -## Background - -WebAssembly has neither a mechanism to get the current year, nor one to print to the console, so this example defines these in Go. -Like Go, WebAssembly functions are namespaced into modules instead of packages. -With Go only exported functions can import into another module. -`age-calculator.go` shows how to export functions using [HostModuleBuilder](https://pkg.go.dev/github.com/tetratelabs/wazero#HostModuleBuilder) and how a WebAssembly module defined in its [text format](https://www.w3.org/TR/2019/REC-wasm-core-1-20191205/#text-format%E2%91%A0) imports it. -This only uses the text format for demonstration purposes, to show you what's going on. -It's likely, you will use another language to compile a Wasm (WebAssembly Module) binary, such as TinyGo. -Regardless of how wasm produces, the export/import mechanics are the same! - - -### Using WASI - -WebAssembly System Interface (WASI) is a modular system interface for WebAssembly. -This uses an ad-hoc Go-defined function to print to the console. -An emerging specification standardizes system calls (like Go's [x/sys](https://pkg.go.dev/golang.org/x/sys/unix)) called WebAssembly System Interface [(WASI)](https://github.com/WebAssembly/WASI). -While this isn't yet a W3C standard, wazero includes a [wasi package](https://pkg.go.dev/github.com/tetratelabs/wazero/wasi). - - -## Customize your app - -To customize the app, update the files in the repository, listed below: - -* `agecalculator.go`: The Go web server that calls the WASM/Wazero blog for the age calculation -* `Kraftfile`: the Unikraft Cloud specification -* `Dockerfile`: the Docker-specified app filesystem - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/guides/wordpress-all-in-one.mdx b/pages/guides/wordpress-all-in-one.mdx new file mode 100644 index 00000000..bbea8ef4 --- /dev/null +++ b/pages/guides/wordpress-all-in-one.mdx @@ -0,0 +1,174 @@ +--- +title: "Wordpress" +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +{/* vale off */} +{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */} + + +This guide shows you how to use [Wordpress](https://wordpress.com/), a web content management system. + +To run it, follow these steps: + +1. Install the CLI. + Use the [unikraft CLI](/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + Alternatively, you can also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +:::note +The unikraft CLI is the current standard, while kraft is the legacy version. +Choose one of the CLIs below and only run the commands associated with it for the rest of this guide. +::: + + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/wordpress-all-in-one/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/wordpress-all-in-one/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /wordpress-all-in-one:latest +unikraft run --scale-to-zero policy=on,cooldown-time=3000,stateful=true --metro fra -p 443:3000/tls+http -m 4G --image /wordpress-all-in-one:latest +``` + +```bash title="kraft" +kraft cloud deploy --scale-to-zero on --scale-to-zero-stateful --scale-to-zero-cooldown 3s -p 443:3000/tls+http -M 4Gi . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: wordpress-fx5rb +uuid: bfb9d151-1604-452a-b2e0-f737486744df +state: starting +image: /wordpress +resources: + memory: 4096MiB + vcpus: 1 +service: + uuid: 398fe5bb-e172-465e-8f74-56ffcfb24a3d + name: cool-silence-h5c1es4z + domains: + - fqdn: cool-silence-h5c1es4z.fra.unikraft.app +networks: +- uuid: 26c200e0-43eb-dd46-e4be-e9505ff677d1 + private-ip: 10.0.3.1 + mac: 12:b0:4e:20:b3:e7 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: wordpress-fx5rb + ├───────── uuid: bfb9d151-1604-452a-b2e0-f737486744df + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://cool-silence-h5c1es4z.fra.unikraft.app + ├──────── image: oci://unikraft.io//wordpress@sha256:3e116e6c74dd04e19d4062a14f8173974ba625179ace3c10a2c96546638c4cd8 + ├─────── memory: 4096 MiB + ├────── service: cool-silence-h5c1es4z + ├─ private fqdn: wordpress-fx5rb.internal + └─── private ip: 10.0.3.1 +``` + + + +In this case, the instance name is `wordpress-fx5rb`. +They're different for each run. + +Use a browser to access the install page of Wordpress. +Fill out the form and complete the Wordpress install. + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra wordpress-fx5rb running /wordpress-all-in-one 4096MiB 1 cool-silence-h5c1es4z.fra.unikraft.app 1 minute ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +wordpress-fx5rb cool-silence-h5c1es4z.fra.unikraft.app running 1 minute ago oci://unikraft.io//wordpress-all-in-one@sha256:... 4096 MiB 1 245.32 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete wordpress-fx5rb +``` + +```bash title="kraft" +kraft cloud instance remove wordpress-fx5rb +``` + + + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/cli/unikraft) or the [legacy CLI Reference](/cli/kraft/overview). +{/* vale on */} diff --git a/pages/guides/wordpress.mdx b/pages/guides/wordpress.mdx deleted file mode 100644 index c4d38dd6..00000000 --- a/pages/guides/wordpress.mdx +++ /dev/null @@ -1,98 +0,0 @@ ---- -title: Wordpress ---- - -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - -This guide shows you how to use [Wordpress](https://wordpress.com/), a web content management system. - -To run it, follow these steps: - -1. Install the [`kraft` CLI tool](/install) and a container runtime engine, for example [Docker](https://docs.docker.com/engine/install/). - -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/wordpress-all-in-one/` directory: - -```bash -git clone https://github.com/kraftcloud/examples -cd examples/wordpress-all-in-one/ -``` - -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): - -```bash -# Set Unikraft Cloud access token -export UKC_TOKEN=token -# Set metro to Frankfurt, DE -export UKC_METRO=fra -``` - -:::note -A Wordpress instance on Unikraft Cloud requires 3GB to run. -Request an increase in the instance memory quota when you need more memory. -::: - -When done, invoke the following command to deploy this app on Unikraft Cloud: - -```bash -kraft cloud deploy -p 443:3000 -M 3072 . -``` - -The output shows the instance address and other details: - -```ansi -[●] Deployed successfully! - │ - ├────────── name: wordpress-fx5rb - ├────────── uuid: bfb9d151-1604-452a-b2e0-f737486744df - ├───────── state: starting - ├──────── domain: https://cool-silence-h5c1es4z.fra.unikraft.app - ├───────── image: wordpress@sha256:3e116e6c74dd04e19d4062a14f8173974ba625179ace3c10a2c96546638c4cd8 - ├──────── memory: 3072 MiB - ├─────── service: cool-silence-h5c1es4z - ├── private fqdn: wordpress-fx5rb.internal - ├──── private ip: 172.16.3.1 - └────────── args: /usr/local/bin/wrapper.sh -``` - -In this case, the instance name is `wordpress-fx5rb`. -They're different for each run. - -Use a browser to access the install page of Wordpress. -Fill out the form and complete the Wordpress install. - -You can list information about the instance by running: - -```bash -kraft cloud inst list -``` - -```text -NAME FQDN STATE STATUS IMAGE MEMORY ARGS BOOT TIME -wordpress-fx5rb cool-silence-h5c1es4z.fra.unikraft.app running since 2mins wordpress@sha256:... 3.0 GiB /usr/local/bin/wrapper.sh 1708.17 ms -``` - -When done, you can remove the instance: - -```bash -kraft cloud instance remove wordpress-fx5rb -``` - -## Customize your deployment - -The current deployment uses the current stable version of Wordpress (6.5.5). -It also uses hard-coded values for the database name, user name, passwords. -You can update the `Dockerfile` with other names. -Or expand the configuration to feature non-hard-coded values. - -You can deploy WordPress modules in the WordPress instance without affecting the build. - -## Learn more - -Use the `--help` option for detailed information on using Unikraft Cloud: - -```bash -kraft cloud --help -``` - -Or visit the [CLI Reference](/docs/cli). diff --git a/pages/integrations/kubernetes.mdx b/pages/integrations/kubernetes.mdx index fecc9f37..e1c9d13e 100644 --- a/pages/integrations/kubernetes.mdx +++ b/pages/integrations/kubernetes.mdx @@ -5,16 +5,16 @@ navigation_icon: ship-wheel Unikraft Cloud integrates seamlessly with any Kubernetes cluster through a virtual [kubelet](https://kubernetes.io/docs/reference/command-line-tools-reference/kubelet/) known as **Kraftlet**. This is a lightweight Kubernetes node implementation which connects your cluster to Unikraft Cloud's high-performance compute instead of running real pods locally. -This enables developers to deploy and manage Unikraft unikernels as if they were native Kubernetes pods. +This enables developers to deploy and manage Unikraft microVMs as if they were native Kubernetes pods. This integration extends Kubernetes' scheduling and orchestration capabilities to the Unikraft Cloud platform. -This allows workloads to take advantage of unikernel-level I/O performance, security, cold start and transparent scale-to-zero efficiency while retaining full compatibility with existing Kubernetes tooling. +This allows workloads to take advantage of microVM-level I/O performance, security, cold start and transparent scale-to-zero efficiency while retaining full compatibility with existing Kubernetes tooling. Upon startup, Kraftlet will register itself as a worker node with the Kubernetes API. Once Kraftlet registers itself as a node, Kubernetes can schedule Pods onto it. Any Pod scheduled to the Kraftlet node won't run as a container within the cluster. -Instead, it will run a highly optimized, ultra lightweight unikernel VM instance on Unikraft Cloud. +Instead, it will run a highly optimized microVM on Unikraft Cloud. Kraftlet will manage the Pod lifecycle to make sure the apps are up and running. @@ -29,6 +29,8 @@ $ helm install kraftlet \ --set ukc.token=$UKC_TOKEN \ oci://ghcr.io/unikraft-cloud/helm-charts/kraftlet ``` +The `UKC_METRO` and `UKC_TOKEN` variables are only for these values and the legacy CLI. +The `unikraft` CLI uses profiles instead. You can check if Kraftlet is running by checking its pods: ```bash title="" @@ -127,10 +129,18 @@ Your app is now managed from the Kubernetes cluster, but is actually running on To check the instances, run: -``` shell + + +```shell title="unikraft" +$ unikraft instances list +``` + +```shell title="kraft" $ kraft cloud instance list ``` + + Which will return a list of instances created from pods above: ``` @@ -145,12 +155,20 @@ As you can see, all instances have the same FQDN. This is because Kraftlet created a corresponding Unikraft Cloud service for the Kubernetes service defined in YAML above. You can check the created service with the following command: + + +```shell title="unikraft" +$ unikraft services list ``` + +```shell title="kraft" $ kraft cloud service list NAME FQDN SERVICES INSTANCES CREATED AT PERSISTENT my-service my-service-orjlyrac.fra.unikraft.app 443:8080/tls+http my-app-78c766fb67-k2dgk-nginx my-app-78c766fb67-tkkxq-nginx my-a... 6 minutes ago true ``` + + You can now manage your app running in Unikraft Cloud via Kubernetes resources! ### Stateful apps @@ -185,12 +203,20 @@ my-claim Bound pv-7ab06383-ac03-4a81-968a-1b0cff03c23a 10Mi RWO Also, you can check the volumes on the Unikraft Cloud: + + +```shell title="unikraft" +$ unikraft volumes list ``` + +```shell title="kraft" $ kraft cloud volume list NAME CREATED AT SIZE ATTACHED TO MOUNTED BY STATE PERSISTENT my-claim 5 minutes ago 10 MiB available true ``` + + At the moment, the volume isn't attached or mounted by an instance. To create an instance that would use the volume, you can create a Kubernetes Pod that would reference the PVC: @@ -223,39 +249,121 @@ spec: If you check the instances again, you will see a new instance created from the Pod: + + +```shell title="unikraft" +$ unikraft instances list ``` + +```shell title="kraft" $ kraft cloud instance list NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME nginx-pod-nginx fragrant-breeze-llesxdta.fra.unikraft.app running since 1min nginx@sha256:49d8fb7a9934a87e93f9eb326... 128 MiB 1 /usr/bin/nginx -c /etc/nginx/nginx.conf 15.14 ms ``` + + And if you check the volume now, you will see it's attached and mounted by the created instance: + + +```shell title="unikraft" +$ unikraft volumes list ``` + +```shell title="kraft" $ kraft cloud volume list NAME CREATED AT SIZE ATTACHED TO MOUNTED BY STATE PERSISTENT my-claim 8 minutes ago 10 MiB nginx-pod-nginx nginx-pod-nginx mounted true ``` + + +## Kraftlet internals + +This section describes how Kraftlet translates Kubernetes objects into Unikraft Cloud resources. + +### Ports and handlers + +When Kraftlet maps a Kubernetes Service port to a Unikraft Cloud service, it derives the [handler](/platform/services#handlers) from the port number automatically: + +| Port | Handler applied | +|------|-----------------| +| `80` | `http` | +| `443` | `tls + http` | +| Any other port | `tls` | + +This is why the example above produces `443:8080/tls+http` in the service list. +Kraftlet infers `tls+http` from port 443. + +### Multi-container pods + +Kraftlet maps each container in a pod to a **separate Unikraft Cloud instance**. +When a Pod has a single container, the Unikraft Cloud service takes the Kubernetes Service name directly. +When a Pod has more than one container, each container gets its own Unikraft Cloud service, named `-` (for example, `my-svc-app` and `my-svc-sidecar`). + +Kraftlet supports init containers. +Kraftlet schedules both regular containers and init containers as Unikraft Cloud instances, and deletes them together when you delete the Pod. + +### Resource lifecycle + +When you delete a Kubernetes object, Kraftlet deletes the corresponding Unikraft Cloud resource: + +| Kubernetes object deleted | Unikraft Cloud resource deleted | +|---|---| +| Pod / Deployment replica | Instance (and service group if no other Pods are backing it) | +| PersistentVolumeClaim | Unikraft Cloud volume | + ## Notes -* [**Instances**](https://unikraft.cloud/docs/api/v1/instances/) +* [**Instances**](/platform/instances) For each Pod scheduled on Kraftlet, Kraftlet runs its containers as separate Unikraft Cloud instances rather than running them as containers. Kraftlet ensures it assigns instances to the correct Unikraft Cloud services and attaches them to the corresponding Unikraft Cloud volumes. -* [**Services**](https://unikraft.cloud/docs/api/v1/services/) +* [**Services**](/platform/services) When a Pod gets scheduled on the Kraftlet node, Kraftlet fetches the existing Kubernetes service that the given Pod is backing and **creates a corresponding Unikraft Cloud Service**. Kraftlet allows cluster admins to manage Unikraft Cloud Services by defining a Kubernetes service backed by Pods running on Kraftlet. -* [**Volumes**](https://unikraft.cloud/docs/api/v1/volumes/) +* [**Volumes**](/platform/volumes) Kraftlet listens for changes on [PersistentVolumeClaim objects](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) with storageClass `ukc-volume`. For each creation of such Persistent Volume Claim (PVC) object, the Kraftlet will create a corresponding Unikraft Cloud volume and a `PersistentVolume` object to bind the PVC object to. Kraftlet allows for volume management through Kubernetes clusters. +## Platform features + +Kraftlet supports the following Unikraft Cloud platform features on Kraftlet-managed resources: + +* [**Instance templates**](/platform/instances#instance-templates) + + Frequently deployed workloads can go into instance templates. + Templates pre-warm the snapshot, reducing cold-start latency for every new instance created from the template. + When Kraftlet creates an instance from a Pod spec, you can pre-position an instance template to speed up scheduling. + +* [**Scale-to-zero**](/features/scale-to-zero) + + Instances attached to a Unikraft Cloud service suspend automatically when idle. + Scale-to-zero runs by default for service-backed pods and you can configure it via Pod annotations (see [Annotations](#annotations) below). + +## Annotations + +Kraftlet reads the following annotations from Pod and Service objects to configure Unikraft Cloud resources. + +### Pod annotations + +| Annotation | Type | Default | Description | +|---|---|---|---| +| `cloud.unikraft.v1.instances/autostart` | boolean | `true` | Whether the instance starts automatically when Kraftlet schedules the Pod. | +| `cloud.unikraft.v1.instances/template` | string | — | Name of a pre-existing Unikraft Cloud instance template to use instead of the container image. | +| `cloud.unikraft.v1.instances/scale_to_zero.policy` | `on` \| `off` \| `idle` | `on` | Enables or disables scale-to-zero for service-backed instances. | +| `cloud.unikraft.v1.instances/scale_to_zero.stateful` | boolean | `false` | When `true`, Kraftlet retains the instance state when scaling to zero. | +| `cloud.unikraft.v1.instances/scale_to_zero.cooldown_time_ms` | integer | `1000` | Idle time in milliseconds before Kraftlet suspends the instance. | + +### Service annotations -## Resources +| Annotation | Type | Default | Description | +|---|---|---|---| +| `cloud.unikraft.v1.services/domain` | string | — | Custom domain for the Unikraft Cloud service. For multi-container pods, prefix with the container name (`cloud.unikraft.v1.services/domain.`) to set a per-container domain, or use the global annotation to derive `-` automatically. | -- See [Unikraft public roadmap](https://roadmap.unikraft.com) for planned features or to suggest use cases and ideas. diff --git a/pages/integrations/sdks/go.mdx b/pages/integrations/sdks/go.mdx new file mode 100644 index 00000000..837fd5ac --- /dev/null +++ b/pages/integrations/sdks/go.mdx @@ -0,0 +1,314 @@ +--- +title: Go SDK +navigation_icon: code +--- + +The Unikraft Cloud Go SDK is an autogenerated client library that interfaces +with the Unikraft Cloud platform API, based on the public OpenAPI +specification at https://github.com/unikraft-cloud/openapi. + +## Installation + +```bash title="" +go get unikraft.com/cloud/sdk +``` + +Requires Go 1.25 or later. + +## Quickstart + +```go title="main.go" +package main + +import ( + "context" + "fmt" + + "unikraft.com/cloud/sdk/platform" +) + +func main() { + ctx := context.Background() + + // Create a new client. The token and default metro are automatically + // read from environment variables (UKC_TOKEN and UKC_METRO). + client := platform.NewClient() + + // List all instances + resp, err := client.GetInstances(ctx, nil, nil) + if err != nil { + panic(err) + } + + // Check for API-level errors + if resp.Status != "success" { + panic(resp.Message) + } + + for _, inst := range resp.Data.Instances { + fmt.Printf("Name: %s\n", *inst.Name) + fmt.Printf("UUID: %s\n", *inst.Uuid) + fmt.Printf("State: %s\n", *inst.State) + fmt.Printf("Image: %s\n", *inst.Image) + fmt.Println("---") + } +} +``` + +## Authentication + +Pass your token via `platform.WithToken()`: + +```go title="" +client := platform.NewClient( + platform.WithToken("your-api-token"), +) +``` + +Or set one of the following environment variables. +`NewClient` reads them automatically in order of preference: + +1. `UKC_TOKEN` +2. `UNIKRAFT_CLOUD_TOKEN` +3. `KRAFTCLOUD_TOKEN` (legacy name) + +You can also set the default metro via the `UKC_METRO` environment variable. +If not specified, the SDK defaults to `fra`. + +```go title="" +// No WithToken/WithDefaultMetro needed, NewClient reads the environment automatically +client := platform.NewClient() +``` + +## Client options + +| Option | Description | +| ----------------------------- | -------------------------------------------------------------- | +| `WithDefaultEndpoint(string)` | Full API endpoint address (for example, `https://api.fra.unikraft.cloud`) | +| `WithDefaultMetro(string)` | Metro shorthand—converted to endpoint address automatically | +| `WithToken(string)` | API token | +| `WithHTTPClient(...)` | Custom `http.Client` | +| `WithUserAgent(string)` | Override the `User-Agent` header | +| `WithAllowInsecure(bool)` | Skip TLS verification (development only) | + +`WithDefaultEndpoint` and `WithDefaultMetro` both set the API endpoint. +Use `WithDefaultMetro("fra")` as a shorthand for `WithDefaultEndpoint("https://api.fra.unikraft.cloud")`. + +The base address is `https://api..unikraft.cloud`. + +## Resources + +The `platform.Client` interface covers the full Unikraft Cloud REST API surface. + +### Instances + +```go +// Create +client.CreateInstance(ctx, req) +``` + +```go +// Read +client.GetInstances(ctx, ids, details) +client.GetInstanceByUUID(ctx, uuid, details) +``` + +```go +// Start / Stop +client.StartInstanceByUUID(ctx, uuid) +client.StartInstances(ctx, ids) +client.StopInstanceByUUID(ctx, uuid, req) +client.StopInstances(ctx, req) +``` + +```go +// Update +client.UpdateInstanceByUUID(ctx, uuid, req) +client.UpdateInstances(ctx, req) +``` + +```go +// Delete +client.DeleteInstanceByUUID(ctx, uuid) +client.DeleteInstances(ctx, ids) +``` + +```go +// Logs and metrics +client.GetInstanceLogsByUUID(ctx, uuid, req) +client.GetInstanceLogs(ctx, req) +client.GetInstanceMetricsByUUID(ctx, uuid) +client.GetInstanceMetrics(ctx, ids) +``` + +```go +// Wait for a target state +client.WaitInstanceByUUID(ctx, uuid, req) +client.WaitInstances(ctx, req) +``` + +### Instance templates + +```go +// Create +client.CreateTemplateInstances(ctx, req) +``` + +```go +// Read +client.GetTemplateInstances(ctx, ids, details, count, tags) +client.GetTemplateInstanceByUUID(ctx, uuid, details) +``` + +```go +// Update +client.UpdateTemplateInstanceByUUID(ctx, uuid, req) +client.UpdateTemplateInstances(ctx, req) +``` + +```go +// Delete +client.DeleteTemplateInstanceByUUID(ctx, uuid) +client.DeleteTemplateInstances(ctx, ids) +``` + +### Service groups + +```go +// Create +client.CreateServiceGroup(ctx, req) +``` + +```go +// Read +client.GetServiceGroups(ctx, ids, details) +client.GetServiceGroupByUUID(ctx, uuid, details) +``` + +```go +// Update +client.UpdateServiceGroupByUUID(ctx, uuid, req) +client.UpdateServiceGroups(ctx, req) +``` + +```go +// Delete +client.DeleteServiceGroupByUUID(ctx, uuid) +client.DeleteServiceGroups(ctx, ids) +``` + +### Autoscale + +```go +// Configuration (by service group UUID) +client.CreateAutoscaleConfigurationByServiceGroupUUID(ctx, uuid, req) +client.GetAutoscaleConfigurationsByServiceGroupUUID(ctx, uuid) +client.DeleteAutoscaleConfigurationsByServiceGroupUUID(ctx, uuid) +``` + +```go +// Configuration (batch) +client.CreateAutoscaleConfigurations(ctx, req) +client.GetAutoscaleConfigurations(ctx, ids) +client.DeleteAutoscaleConfigurations(ctx, ids) +``` + +```go +// Policies +client.CreateAutoscaleConfigurationPolicy(ctx, uuid, req) +client.GetAutoscaleConfigurationPolicies(ctx, uuid, req) +client.GetAutoscaleConfigurationPolicyByName(ctx, uuid, name) +client.DeleteAutoscaleConfigurationPolicies(ctx, uuid, req) +client.DeleteAutoscaleConfigurationPolicyByName(ctx, uuid, name) +``` + +### Volumes + +```go +// Create +client.CreateVolume(ctx, req) +``` + +```go +// Read +client.GetVolumes(ctx, ids, details) +client.GetVolumeByUUID(ctx, uuid, details) +``` + +```go +// Attach / Detach +client.AttachVolumeByUUID(ctx, uuid, req) +client.AttachVolumes(ctx, req) +client.DetachVolumeByUUID(ctx, uuid, req) +client.DetachVolumes(ctx, req) +``` + +```go +// Clone +client.CloneVolumeByUUID(ctx, uuid, req) +client.CloneVolumes(ctx, req) +``` + +```go +// Update +client.UpdateVolumeByUUID(ctx, uuid, req) +client.UpdateVolumes(ctx, req) +``` + +```go +// Delete +client.DeleteVolumeByUUID(ctx, uuid) +client.DeleteVolumes(ctx, ids) +``` + +### Certificates + +```go +// Create +client.CreateCertificate(ctx, req) +``` + +```go +// Read +client.GetCertificates(ctx, ids, details) +client.GetCertificateByUUID(ctx, uuid) +``` + +```go +// Delete +client.DeleteCertificateByUUID(ctx, uuid) +client.DeleteCertificates(ctx, ids) +``` + +### Images + +```go +// Read +client.GetImages(ctx, req, namespace) +``` + +### Users and quotas + +```go +// Read quotas +client.GetUser(ctx) +client.GetUserByUUID(ctx, uuid) +``` + +```go +// Create users +client.AddUsers(ctx) +``` + +### Health + +```go +// Check node health +client.Healthz(ctx) +``` + +## Source + +The SDK source and full API reference are available at +[github.com/unikraft-cloud/go-sdk](https://github.com/unikraft-cloud/go-sdk) +and [pkg.go.dev/unikraft.com/cloud/sdk](https://pkg.go.dev/unikraft.com/cloud/sdk). diff --git a/pages/introduction.mdx b/pages/introduction.mdx index 4c2d5c59..52b1ffc5 100644 --- a/pages/introduction.mdx +++ b/pages/introduction.mdx @@ -3,11 +3,10 @@ title: Introduction navigation_icon: book-check --- -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" -Unikraft is a from-first-principles virtualization solution and cloud platform based on **unikernels** which provides best-in-class performance, security, and scalability. +Unikraft Cloud is a from-first-principles virtualization solution and cloud platform based on **MicroVMs** which provides best-in-class performance, security, and scalability. -Running your app on Unikraft is like running a container but with hardware isolation, millisecond cold starts, and stateful scale-to-zero. +Running your app on Unikraft Cloud is like running a container but with hardware isolation, millisecond cold starts, and stateful scale-to-zero. Expect higher server density, cost-savings, I/O performance boosts, active vCPU pricing and _much more_. @@ -17,68 +16,71 @@ Expect higher server density, cost-savings, I/O performance boosts, active vCPU 1. [Create a free account](https://console.unikraft.cloud/signup). -1. Install the `kraft` CLI. - - - Shell 1-liner - macOS - Debian/Ubuntu - - - ```bash title="Run on any *NIX shell" - curl -sSfL https://get.kraftkit.sh | sh - ``` - - - ```bash title="Shell" - brew install unikraft/cli/kraftkit - ``` - - - ```bash - sudo apt-get update - sudo apt-get install \ - ca-certificates \ - curl \ - gnupg \ - lsb-release - ``` - - Add Unikraft's official GPG key: - - ```bash - sudo mkdir -p /etc/apt/keyrings - curl -fsSL https://deb.pkg.kraftkit.sh/gpg.key | \ - sudo gpg --dearmor -o /etc/apt/keyrings/unikraft.gpg - ``` - - Use the following command to set up the APT repository: - - ```bash - echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/unikraft.gpg] https://deb.pkg.kraftkit.sh /" | \ - sudo tee /etc/apt/sources.list.d/unikraft.list > /dev/null - ``` - - Update the APT package index, and install the latest version of `kraftkit`: - - ```bash - sudo apt-get update - sudo apt-get install kraftkit - ``` - - - - See [alternative installation instructions](/docs/cli/install) for other - platforms. - -1. Login to your account by setting your access token (received via email): +1. Install the Unikraft CLI. + + + + ```bash title="1-liner (macOS & Linux)" + curl --proto '=https' --tlsv1.2 -fsSL https://unikraft.com/cli/install.sh | sh + ``` + + ```bash title="macOS" + brew install unikraft/cli/unikraft + ``` + + ```bash title="Debian/Ubuntu" + # Update and install dependencies + sudo apt update + sudo apt install ca-certificates curl + + # Download and add the GPG key + sudo install -d -m 0755 /etc/apt/keyrings + + sudo curl -fsSL \ + -o /etc/apt/keyrings/unikraft-cli.gpg \ + https://pkg.unikraft.com/debian/cli-apt/keys/cli-apt.gpg + + sudo tee /etc/apt/sources.list.d/unikraft-cli.sources < + + See [alternative installation instructions](https://github.com/unikraft/cli/?tab=readme-ov-file#installation) for other platforms. + +1. Authenticate with your account. + ```bash - export UKC_TOKEN= + unikraft login ``` -1. Deploy your first scale-to-zero, serverless app with Unikraft Cloud: +1. Deploy your first scale-to-zero, serverless app with Unikraft Cloud. + ```bash - kraft cloud deploy -p 443:8080 nginx:latest + unikraft run --metro=fra -p 443:8080/http+tls --image=nginx:latest ``` diff --git a/pages/platform/certificates.mdx b/pages/platform/certificates.mdx index 680ef001..8ccf9393 100644 --- a/pages/platform/certificates.mdx +++ b/pages/platform/certificates.mdx @@ -3,9 +3,19 @@ title: Certificates navigation_icon: shield-check --- -To use a custom certificate instead of the automatically generated one from the default public certificate authority, run the `create` command: +To use a custom certificate instead of the automatically generated one from the default public certificate authority, create a certificate with the CLI: -```ansi title="" + + +```ansi title="unikraft" +unikraft certificates create \ + --cn *.mydomain.com \ + --name mydomain-cert \ + --pkey /path/to/private.key \ + --chain /path/to/chain.pem +``` + +```ansi title="kraft" kraft cloud cert create \ --cn *.mydomain.com \ --name mydomain-cert \ @@ -13,16 +23,26 @@ kraft cloud cert create \ --chain /path/to/chain.pem ``` + + The provided common name (CN) must match the one for which Unikraft Cloud issued the certificate. It can also be a wildcard domain. -Use the `kraft cloud cert` command to view and manage certificates. -For example, to list certificates, run: +Use the CLI to view and manage certificates. +For example, to list certificates: -```bash title="" + + +```bash title="unikraft" +unikraft certificates list +``` + +```bash title="kraft" kraft cloud cert ls ``` + + You should see output like: ```text title="" @@ -33,10 +53,18 @@ mydomain-cert valid *.mydomain.com 2 days ago Retrieve full information about a certificate with: -```bash title="" + + +```bash title="unikraft" +unikraft certificates get mydomain.com-sa4x9 +``` + +```bash title="kraft" kraft cloud cert get mydomain.com-sa4x9 ``` + + You should see output like: ```ansi title="" @@ -56,12 +84,20 @@ serial number: 0455BBAEC140EACBA5FEEAE6D817E73EF266 To remove a certificate, first remove any instances from the relevant service and then remove the service. Remove the certificate with: -```bash title="" + + +```bash title="unikraft" +unikraft certificates delete mydomain.com-sa4x9 +``` + +```bash title="kraft" kraft cloud cert rm mydomain.com-sa4x9 ``` + + ## Learn more -* The `kraft cloud` [CLI reference](/docs/cli/) -* Unikraft Cloud's [REST API reference](/docs/api/v1) -* Many more guides [here](/docs/guides) +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1). +* Many more guides [here](/guides/bun). diff --git a/pages/platform/delete-locks.mdx b/pages/platform/delete-locks.mdx new file mode 100644 index 00000000..da8964ad --- /dev/null +++ b/pages/platform/delete-locks.mdx @@ -0,0 +1,170 @@ +--- +title: Delete Locks +navigation_icon: lock +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +:::caution +The [KraftKit CLI tool](/cli/) doesn't expose this feature to users yet. +Use it via the [Unikraft Cloud API](/api/platform/v1). +::: + +Delete locks prevent accidental deletion of instances and volumes. +When you set a delete lock on a resource, any attempt to delete it returns an error until you remove it. + +Delete locks apply to: +- Instances +- Instance templates +- Volumes +- Volume templates + +## Enabling a delete lock + +You can set a delete lock on a resource via a `PATCH` operation. +Set `UKC_TOKEN` and `UKC_METRO` for the API calls: + + + + Instance + Instance Template + Volume + Volume Template + + +```bash title="Locking an instance" +curl -X PATCH \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.$UKC_METRO.unikraft.cloud/v1/instances" \ + -d "[{ + 'name': '', + 'prop': 'delete_lock', + 'op': 'set', + 'value': true + }]" +``` + + +```bash title="Locking an instance template" +curl -X PATCH \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.$UKC_METRO.unikraft.cloud/v1/instance/templates" \ + -d "[{ + 'name': '', + 'prop': 'delete_lock', + 'op': 'set', + 'value': true + }]" +``` + + +```bash title="Locking a volume" +curl -X PATCH \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.$UKC_METRO.unikraft.cloud/v1/volumes" \ + -d "[{ + 'name': '', + 'prop': 'delete_lock', + 'op': 'set', + 'value': true + }]" +``` + + +```bash title="Locking a volume template" +curl -X PATCH \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.$UKC_METRO.unikraft.cloud/v1/volume/templates" \ + -d "[{ + 'name': '', + 'prop': 'delete_lock', + 'op': 'set', + 'value': true + }]" +``` + + + +## Removing a delete lock + +Before you can delete a resource, you need to remove the delete lock by setting its value to `false`: + + + + Instance + Instance Template + Volume + Volume Template + + +```bash title="Unlocking an instance" +curl -X PATCH \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.$UKC_METRO.unikraft.cloud/v1/instances" \ + -d "[{ + 'name': '', + 'prop': 'delete_lock', + 'op': 'set', + 'value': false + }]" +``` + + +```bash title="Unlocking an instance template" +curl -X PATCH \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.$UKC_METRO.unikraft.cloud/v1/instance/templates" \ + -d "[{ + 'name': '', + 'prop': 'delete_lock', + 'op': 'set', + 'value': false + }]" +``` + + +```bash title="Unlocking a volume" +curl -X PATCH \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.$UKC_METRO.unikraft.cloud/v1/volumes" \ + -d "[{ + 'name': '', + 'prop': 'delete_lock', + 'op': 'set', + 'value': false + }]" +``` + + +```bash title="Unlocking a volume template" +curl -X PATCH \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.$UKC_METRO.unikraft.cloud/v1/volume/templates" \ + -d "[{ + 'name': '', + 'prop': 'delete_lock', + 'op': 'set', + 'value': false + }]" +``` + + + +## Behaviour + +- **Deletion blocked**: Any `DELETE` request on a locked resource returns an error. +- **Autokill skipped**: The [autokill](/features/autokill) subsystem skips instances and service groups that have a delete lock set. +- **Templates**: Delete locks are independently configurable on instance templates and volume templates via their respective `PATCH` endpoints. + +## Learn more + +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the sections on [instances](/api/platform/v1/instances) and [volumes](/api/platform/v1/volumes). diff --git a/pages/platform/domains.mdx b/pages/platform/domains.mdx index c1faee13..875f718f 100644 --- a/pages/platform/domains.mdx +++ b/pages/platform/domains.mdx @@ -8,7 +8,7 @@ This guide shows how to deploy an app and link it to a domain you own (for examp :::note Unikraft Cloud can take a few seconds before issuing the certificate. If there are misconfigurations or DNS changes that your external DNS provider hasn't propagated yet, this can take much longer. -You can check the validation status of the certificate with `kraft cloud cert get` (more on this command below). +You can check the validation status of the certificate with the CLI (more on this below). The controller retries at these intervals: [1,5,10,30] minutes, then [1,6,12,24] hours, then it fails. ::: @@ -31,13 +31,25 @@ ALIAS (apex alias) and ANAME (authoritative alias) are DNS record types some pro If the provider doesn't support these record types, add an A record pointing to the metro IP address. :::tip -You can find information about the Unikraft Cloud metros available to you, as well as their IP addresses, via the cmd `kraft cloud metro ls`. +You can find information about the Unikraft Cloud metros available to you, as well as their IP addresses, via the CLI. + + + +```bash title="unikraft" +unikraft metros list +``` + +```bash title="kraft" +kraft cloud metro ls +``` + + ::: :::note You can have many domains for the same service. Unikraft Cloud also supports wildcard domains. -See the [certificates API](/docs/api/v1/certificates)) for details. +See the [certificates API](/api/platform/v1/certificates) for details. ::: ## Launching your app @@ -49,24 +61,43 @@ git clone https://github.com/unikraft-cloud/examples cd examples/nginx ``` -Log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. +Log into Unikraft Cloud and select a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪). Set the following: -```bash title="" + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" # Set Unikraft Cloud access token export UKC_TOKEN=token # Set metro to Frankfurt, DE export UKC_METRO=fra ``` -With this in place, use the `kraft cloud deploy` command to create an instance of the web server and to link it to a custom name. -Unikraft Cloud does this through the `-d` flag: + -```bash title="" +The `UKC_TOKEN` and `UKC_METRO` environment variables are only supported by the legacy CLI. + +With this in place, use the CLI to create an instance of the web server and link it to a custom name. +Unikraft Cloud does this through the domain flag: + + + +```bash title="unikraft" +unikraft build . --output /nginx:latest +unikraft run --metro=fra -p 443:8080/http+tls --domain mydomain.com --image=/nginx:latest +``` + +```bash title="kraft" kraft cloud deploy -p 443:8080 -d mydomain.com . ``` + + The resulting output of the `deploy` command should be like: ```ansi title="" @@ -85,13 +116,13 @@ The resulting output of the `deploy` command should be like: └────────── args: /usr/bin/nginx -c /etc/nginx/nginx.conf ``` -When you issue the `-d` flag, Unikraft Cloud requests a +When you issue the domain flag, Unikraft Cloud requests a new certificate from the public certificate authority. :::note Issuing the certificate can take a few seconds. If there are misconfigurations or pending DNS changes, it can take longer. -Check the validation status with `kraft cloud cert get` (more on this command below). +Check the validation status with the CLI (more on this command below). The Unikraft Cloud controller retries at these intervals: [1,5,10,30] minutes, then [1,6,12,24] hours, then it fails. ::: @@ -101,3 +132,8 @@ Test the deployment with `curl`: ```bash curl https://mydomain.com ``` + +## Learn more + +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the sections on [instances](/api/platform/v1/instances) and [service groups](/api/platform/v1/service-groups). diff --git a/pages/platform/images.mdx b/pages/platform/images.mdx index 3a1389bf..4d2cdc8d 100644 --- a/pages/platform/images.mdx +++ b/pages/platform/images.mdx @@ -4,18 +4,31 @@ navigation_icon: package --- Unikraft Cloud uses a **registry** to store images used to instantiate apps. -At a high level, you use `kraft` to build and push an image to the registry, and then ask the controller to start an app from it. +At a high level, you use the CLI to build and push an image to the registry, and then ask the controller to start an app from it. -1. **build**: `kraft` builds the app locally according to a `Dockerfile`. -1. **pkg**: `kraft` packages the app locally into an OCI image. -1. **push**: `kraft` pushes the OCI image to the Unikraft Cloud registry. -1. **create**: The controller instantiates your app from the image according to a `Kraftfile`. +This process utilizes two types of registries: central and local. +Built images default to the central registry (`index.unikraft.io`). +The platform pulls from this registry automatically upon starting an instance for the first time. -The simplest way to run this workflow is to use `kraft cloud deploy`, which combines all steps into one command. -Internally, the deploy command calls other subcommands. -The service in the diagram is the mechanism to connect apps to the Internet. -Read more in the [services guide](/docs/guides/features/service). +You can choose to push an image directly to a node's local registry instead. +This bypasses the central registry entirely. +Skipping this extra network round trip speeds up deployments. +It also reduces bandwidth consumption. +Read more about this in the [Registries documentation](/cli/registries). +:::caution[DISCLAIMER] +The local registry isn't currently available for the public Unikraft Cloud offering. +::: + +1. **build**: You execute `unikraft build` with the +`--output` flag. This single command reads your Dockerfile or Kraftfile, compiles +the app locally, packages the resulting files into an OCI image, and +uploads that image directly to the Unikraft Cloud registry. + +1. **run**: You execute unikraft run and provide the uploaded image +name. The Unikraft Cloud controller pulls the image from the registry, +allocates the necessary system resources, and starts your app as a +running instance. ## `Dockerfiles`, `Kraftfiles` and runtimes @@ -25,38 +38,70 @@ Use the Python running example: ```bash title="" git clone https://github.com/unikraft-cloud/examples -cd examples/http-python3.12 +cd examples/httpserver-python3.12 ``` The directory contains the following `Kraftfile`: ```yaml title="Kraftfile" -spec: v0.6 +spec: v0.7 -runtime: python:3.12 +runtime: base-compat:latest -rootfs: ./Dockerfile +rootfs: + source: ./Dockerfile + format: erofs cmd: ["/usr/bin/python3", "/src/server.py"] ``` -The file is simple: it defines the start `cmd`, instructs `kraft` to build the root filesystem with a `Dockerfile`, and specifies the `python3.12` **runtime**. -On Unikraft Cloud, a runtime is a base image that contains the (minimal) code needed for the app (in this case the Python interpreter) to run. -Unikraft Cloud then overlays your app code on top of it during the packaging step. +The file is simple. +It defines the start command, instructs the unikraft CLI to build the root filesystem, and specifies the base-compat:latest runtime. +On Unikraft Cloud, a runtime provides a base image containing the minimal code your app needs to execute. +The unikraft CLI combines your app files with this base image during the build step. +It uses the Dockerfile as the source to generate the directory structure. +When packaging these files into a root filesystem, Unikraft offers two main options: cpio and erofs. +The file specifies the erofs format to create a read-only disk image. +The erofs format decompresses data block-by-block only when the system accesses it. +This selective approach saves RAM and improves boot times. +In contrast, the cpio format must remove its entire contents into memory on startup. +Check out this [tutorial](/tutorials/rootfs-formats) comparing rootfs types to understand why Unikraft favors erofs. +For a more in depth explanation of the Kraftfile you can check the [reference](/kraftfile/v0.7) The `Dockerfile` itself for the app looks as follows: ```dockerfile title="Dockerfile" +FROM python:3.12 AS build + +RUN set -xe; \ + /usr/sbin/ldconfig /usr/local/lib + FROM scratch -# Python HTTP server +# copy the dependencies from the base image +COPY --from=build /usr/local/lib /usr/local/lib +COPY --from=build /usr/local/bin/python3 /usr/bin/python3 +COPY --from=build /lib/x86_64-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6 +COPY --from=build /lib/x86_64-linux-gnu/libm.so.6 /lib/x86_64-linux-gnu/libm.so.6 +COPY --from=build /usr/lib/x86_64-linux-gnu/libz.so.1 /usr/lib/x86_64-linux-gnu/libz.so.1 +COPY --from=build /usr/lib/x86_64-linux-gnu/libcrypto.so.3 /usr/lib/x86_64-linux-gnu/libcrypto.so.3 +COPY --from=build /lib64/ld-linux-x86-64.so.2 /lib64/ld-linux-x86-64.so.2 +COPY --from=build /etc/ld.so.cache /etc/ld.so.cache + +# copy the python image COPY ./server.py /src/server.py ``` -If you're familiar with `Dockerfiles` there is nothing unusual here, other than that by default Unikraft Cloud uses `FROM scratch` to keep images lean. +If you're familiar with `Dockerfiles` there is nothing unusual here, other than that Unikraft Cloud prefers usage of `FROM scratch` to keep images lean. +This multi-stage build creates a minimal container. +First, the process loads a standard Python 3.12 image to configure system dependencies. +Next, it starts an empty filesystem called scratch. +The builder extracts the compiled Python binary and essential shared system libraries from the first stage. +It moves these exact files into the empty environment. +Finally, the builder copies your app script into the container. To add your app's code to the build, change the `COPY` commands as needed. -All [guides](/docs/guides/) on Unikraft Cloud, and the [examples](https://github.com/unikraft-cloud/examples) they rely on underneath come with `Kraftfile`s and `Dockerfile`s for you to get started. +All [guides](/guides/overview) on Unikraft Cloud, and the [examples](https://github.com/unikraft-cloud/examples) they rely on underneath come with `Kraftfile`s and `Dockerfile`s for you to get started. :::tip You can also try a standard base image (for example, `FROM python:alpine`). @@ -66,126 +111,322 @@ This choice may increase image size, memory use, and boot time. ## Example workflows -This guide uses a Python [app](/docs/guides/python) as an example to show three workflows: +This guide uses a Python [app](/guides/httpserver-python3.12) as an example to show three workflows: 1. How to create an image and launch an instance from it. -1. How to create an image and launch many instances from it. -1. How to launch instances from an existing image. +2. How to create an image and launch many instances from it. +3. How to launch instances from an existing image. ### Create an image and an instance from it -Start with the simplest workflow: create an image from a Python app (following the Python app [guide](/docs/guides/python)) and start an instance from it with a single `kraft cloud deploy` command: +Start with the simplest workflow: create an image from a Python app and start an instance from it: -```bash title="" -git clone https://github.com/unikraft-cloud/examples -cd examples/http-python3.12 -kraft cloud deploy -p 443:8080 -M 512 . + + +```bash title="unikraft" +unikraft build . --output /httpserver-python312:latest +unikraft run --metro=fra -p 443:8080/http+tls -m 512M --image /httpserver-python312:latest ``` +```bash title="kraft" +kraft cloud deploy -p 443:8080 -M 512Mi . +``` + + + The output should look like: -```ansi title="" + + +```ansi title="unikraft" +metro: fra +name: httpserver-python312-ma2i9 +uuid: e7389eee-9808-4152-b2ec-1f3c0541fd05 +state: running +image: /httpserver-python312 +resources: + memory: 512MiB + vcpus: 1 +service: + uuid: 51a41f63-7e88-c443-b9bf-83cd7c04d975 + name: young-night-5fpf0jj8 + domains: + - fqdn: young-night-5fpf0jj8.fra.unikraft.app +networks: +- uuid: 53da3490-c6f5-3718-25f1-219a65163c73 + private-ip: 10.0.3.3 + mac: 12:b0:18:0c:cb:aa +timestamps: + created: just now +``` + +```ansi title="kraft" [●] Deployed successfully! │ - ├────────── name: http-python312-ma2i9 - ├────────── uuid: e7389eee-9808-4152-b2ec-1f3c0541fd05 - ├───────── state: running - ├─────────── url: https://young-night-5fpf0jj8.fra.unikraft.app - ├───────── image: [username]]/http-python312@sha256:278cb8b14f9faf9c2702dddd8bfb6124912d82c11b4a2c6590b6e32fc4049472 - ├───── boot time: 15.09 ms - ├──────── memory: 512 MiB - ├─────── service: young-night-5fpf0jj8 - ├── private fqdn: http-python312-ma2i9.internal - ├──── private ip: 172.16.3.3 - └────────── args: /usr/bin/python /src/server.py -``` - -This command builds an image named `http-python312@sha256:278cb8b1...` using the `Kraftfile` and `Dockerfile`. -It then packages it, pushes it to the registry, and starts an instance named `http-python312-ma2i9` from it. + ├───────── name: httpserver-python312-ma2i9 + ├───────── uuid: e7389eee-9808-4152-b2ec-1f3c0541fd05 + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├─────── domain: https://young-night-5fpf0jj8.fra.unikraft.app + ├──────── image: oci://unikraft.io//httpserver-python312@sha256:278cb8b14f9faf9c2702dddd8bfb6124912d82c11b4a2c6590b6e32fc4049472 + ├─────── memory: 512 MiB + ├────── service: young-night-5fpf0jj8 + ├─ private fqdn: httpserver-python312-ma2i9.internal + └─── private ip: 10.0.3.3 +``` + + + + +This command builds an image named `httpserver-python312@sha256:278cb8b1...` using the `Kraftfile` and `Dockerfile`. +It then packages it, pushes it to the registry, and starts an instance named `httpserver-python312-ma2i9` from it. The controller fetches the image from the registry to start the instance. You can see your images by running the following command: -```bash title="" + + +```bash title="unikraft" +unikraft images list +``` + +```bash title="kraft" kraft cloud image ls ``` + + You should see output like: -```text title="" -IMAGE TAG SIZE -[username]]/http-python312 latest 77 MB + +```text title="unikraft" +REF DIGEST +/httpserver-python312 sha256:efcf60c1ab4677464f50f5fc7c0011efef7ea66d371586a22d1960bbeb53e2ee ``` -And you can remove an image from the registry via: +```bash title="kraft" +NAME VERSION SIZE +/http-python latest 120 MB +``` + -```bash title="" -kraft cloud img rm http-python312 +And you can remove an image from the central registry with the following command: + + +```bash title="unikraft" +unikraft img rm httpserver-python312 ``` +```bash title="kraft" +kraft cloud img rm httpserver-python312 +``` + + + :::note -There may be a delay of a few minutes between removing an image from the registry and `kraft cloud image ls` reflecting the change. +There may be a delay of a few minutes between removing an image from the registry and the image list reflecting the change. ::: - ### Create an image and many instances from it -For the next workflow, use `kraft cloud deploy` again, this time with the `-R` flag to start many instances: +For the next workflow, start many instances: -```bash title="" -git clone https://github.com/unikraft-cloud/examples -cd examples/http-python3.12 -kraft cloud deploy -p 443:8080 -M 512 -R 2 . + + +```bash title="unikraft" +unikraft build . --output /httpserver-python312:latest +unikraft run --metro=fra -p 443:8080/http+tls -m 512M --replicas 2 --image /httpserver-python312:latest +``` + +```bash title="kraft" +kraft cloud deploy -p 443:8080 -M 512Mi --replicas 2 . ``` -```ansi title="" + + + +```ansi title="unikraft" +metro: fra +name: httpserver-python312-lscmn +uuid: 1121225d-a678-45a2-a8ce-335a9a6a2683 +state: running +image: dragosgheorghioiu/httpserver-python312 +resources: + memory: 512MiB + vcpus: 1 +service: + name: restless-orangutan-1lklj6z5 + uuid: 16f4b86d-df8f-452c-b189-41ec34250e58 + domains: + - fqdn: restless-orangutan-1lklj6z5.fra.unikraft.app +networks: +- uuid: da9445a3-c7b7-4327-8499-3bd7001cd829 + private-ip: 10.0.6.37 + mac: 12:b0:0a:00:06:25 +timestamps: + created: just now + +metro: fra +name: httpserver-python312-xfgvc +uuid: d021379d-9e03-4d0b-92a6-6fdf038e076a +state: running +image: dragosgheorghioiu/httpserver-python312 +resources: + memory: 512MiB + vcpus: 1 +service: + name: icy-violet-hce1qwm5 + uuid: 4cd63083-d538-44df-92b9-74bd6eb54ec0 + domains: + - fqdn: icy-violet-hce1qwm5.fra.unikraft.app +networks: +- uuid: 51c147cc-8f8d-4347-b628-d2e0f4f9b3be + private-ip: 10.0.0.121 + mac: 12:b0:0a:00:00:79 +timestamps: + created: just now + +metro: fra +name: httpserver-python312-7lha2 +uuid: 0706c648-0c56-4c74-8dda-5a1aeff7d8b8 +state: running +image: dragosgheorghioiu/httpserver-python312 +resources: + memory: 512MiB + vcpus: 1 +service: + name: hidden-silence-l3g520dw + uuid: 38703973-ccbd-47ae-b4c0-f9f59c1751e7 + domains: + - fqdn: hidden-silence-l3g520dw.fra.unikraft.app +networks: +- uuid: 6c20f9b2-dc39-41d1-a4bd-250a52a75b19 + private-ip: 10.0.9.185 + mac: 12:b0:0a:00:09:b9 +timestamps: + created: just now +``` +```ansi title="kraft" [●] Deployed successfully! │ - ├────────── name: http-python312-8mxq5 - ├────────── uuid: 37f5b23c-0996-45fa-8d7f-e6b2942eb6fb - ├───────── state: running - ├─────────── url: https://small-darkness-4t9y8n5s.fra.unikraft.app - ├───────── image: http-python312@sha256:5b922dfa1632af38c476b98fdd9f4314fb9c5e587d3d31255e6479108c057e88 - ├───── boot time: 153.65 ms - ├──────── memory: 512 MiB - ├─────── service: small-darkness-4t9y8n5s - ├── private fqdn: http-python312-8mxq5.internal - ├──── private ip: 172.16.6.7 - └────────── args: /usr/bin/python3 /src/server.py + ├───────── name: httpserver-python312-lscmn + ├───────── uuid: 1121225d-a678-45a2-a8ce-335a9a6a2683 + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: running + ├─────── domain: https://restless-orangutan-1lklj6z5.fra.unikraft.app + ├──────── image: oci://unikraft.io//httpserver-python312@sha256:5b922dfa1632af38c476b98fdd9f4314fb9c5e587d3d31255e6479108c057e88 + ├─────── memory: 512 MiB + ├────── service: restless-orangutan-1lklj6z5 + ├─ private fqdn: httpserver-python312-8mxq5.internal + └─── private ip: 10.0.6.37 ``` + Check that it worked by listing all instances with: -```bash title="" + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" kraft cloud instance list ``` -```text title="" -NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME -http-python312-w4bcp sparkling-surf-qphxdk0j.fra.unikraft.app running 45 seconds ago http-python312@sha256... 512 MiB /usr/bin/python3 /src/server.py 153794us -http-python312-juvv4 old-brook-v3bf7h7z.fra.unikraft.app running 45 seconds ago http-python312@sha256... 512 MiB /usr/bin/python3 /src/server.py 154744us -http-python312-8mxq5 small-darkness-4t9y8n5s.fra.unikraft.app running 45 seconds ago http-python312@sha256... 512 MiB /usr/bin/python3 /src/server.py 153646us + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra httpserver-python312-7lha2 running /httpserver-python312 512MiB 1 hidden-silence-l3g520dw.fra.unikraft.app just now +fra httpserver-python312-xfgvc running /httpserver-python312 512MiB 1 icy-violet-hce1qwm5.fra.unikraft.app just now +fra httpserver-python312-lscmn running /httpserver-python312 512MiB 1 restless-orangutan-1lklj6z5.fra.unikraft.app just now +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +httpserver-python312-7lha2 hidden-silence-l3g520dw.fra.unikraft.app running since 7mins dragosgheorghioiu/httpserver-python312@sh... 512 MiB 1 82.28 ms +httpserver-python312-xfgvc icy-violet-hce1qwm5.fra.unikraft.app running since 7mins dragosgheorghioiu/httpserver-python312@sh... 512 MiB 1 81.27 ms +httpserver-python312-lscmn restless-orangutan-1lklj6z5.fra.unikraft.app running since 7mins dragosgheorghioiu/httpserver-python312@sh... 512 MiB 1 84.28 ms ``` + + Three instances run: the original plus two replicas. ### Create instances from an existing image -In this final workflow, take the existing image and start new instances using the `kraft cloud instance` command: +In this final workflow, take the existing image and start new instances from it: -```bash title="" + + +```bash title="unikraft" +unikraft run --metro=fra -p 443:8080/http+tls -m 512M --image /httpserver-python312@sha256:1b815914eb568a06ca4bbfdfb7d6cf484a9e9a0947ba8e0e0f1664d972a25bca +``` + +```bash title="kraft" kraft cloud instance create \ --start \ --port 443:8080 \ - -M 512 \ - [username]/http-python312@sha256:1b815914eb568a06ca4bbfdfb7d6cf484a9e9a0947ba8e0e0f1664d972a25bca + -M 512Mi \ + /httpserver-python312@sha256:1b815914eb568a06ca4bbfdfb7d6cf484a9e9a0947ba8e0e0f1664d972a25bca ``` +:::note +Pinning the digest is optional. +Providing the exact hash guarantees you deploy the exact same immutable image every time. +This prevents unexpected behavior if a developer overwrites a standard tag like latest. +If you omit the digest, the CLI automatically pulls the newest version of your image. +::: + + + You now have a new instance created from the existing image. +## REST API reference + +### List images from local registry + +Base address: `https://api..unikraft.cloud/v1` + +``` +GET /image-store +``` + +Returns all images accessible for your account and known by the platform from the local registry. +Each image object includes: + +| Field | Type | Description | +|-------|------|-------------| +| `created_at` | timestamp | Creation time. | +| `owner` | string | Owner of the image (omitted if not root account). | +| `url` | string | Full image address (includes digest). | +| `tags` | array of strings | List of shortened tags. | +| `initrd_or_rom` | bool | Whether the image includes an initrd or ROM component. | +| `size_in_bytes` | int64 | Total image size in bytes. | +| `args` | array of strings | Default arguments (omitted if none). | +| `env` | map of strings | Default environmental variables (omitted if none). | +| `users` | array of strings | Users with access to the image (omitted if not root account). | + + +### List images from central registry + +Base address `https://controlplane.unikraft.cloud/v1` + +``` +GET /images +``` + +Returns all images accessible for your account and known by the platform from the central registry. + +| Field | Type | Description | +|-------|------|-------------| +| `name` | string | Image reference (`{owner}/{registry}/{image}`). Excludes digest and tag. | + ## Learn more -* The `kraft cloud` [CLI reference](/docs/cli/) -* Unikraft Cloud's [REST API reference](/docs/api/v1) +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1). diff --git a/pages/platform/instances.mdx b/pages/platform/instances.mdx index b63f6fea..c4fd097e 100644 --- a/pages/platform/instances.mdx +++ b/pages/platform/instances.mdx @@ -6,9 +6,9 @@ description: | --- This document describes the Unikraft Cloud Instances API (v1) for managing Unikraft instances. -An *instance* is a Unikraft virtual machine running a single instance of your app. +An *instance* is a MicroVM running a single instance of your app. -### Instance states +## Instance states An instance can be in one of the following states: @@ -16,21 +16,20 @@ An instance can be in one of the following states: |------------|-------------| | `stopped` | The instance isn't running and doesn't count against live resource quotas. Connections can't establish. | | `starting` | The instance is booting up. This typically takes just a few milliseconds. | -| `running` | Your app reaches its main entry point. | +| `running` | Your app reached its main entry point. | | `draining` | The instance is draining connections before shutting down. No new connections can establish. | | `stopping` | The instance is shutting down. | | `standby` | The instance has scaled-to-zero. The instance isn't running, but will be automatically started when there are incoming requests. | Unikraft Cloud reports these as instance `state` values via the endpoints. -LSB (least significant bit) is the lowest order bit in a binary number. -### Stop reason +## Stop reason To understand why Unikraft Cloud stopped an instance or is shutting it down, it provides information about the stop reason. -You can retrieve this information via the [`GET /v1/instances`](#status) endpoint when an instance is in the `draining`, `stopping`, `stopped` or `standby` state. +You can retrieve this information via the [`GET /instances`](/api/platform/v1/instances#list-instances) endpoint when an instance is in the `draining`, `stopping`, `stopped` or `standby` state. The `stop_reason` contains a bitmask that tells you the origin of the shutdown: -| Bit | 4 [F] | 3 [U] | 2 [P] | 1 [A] | 0 [K] (least significant bit, LSB) | +| Bit | 4 [F] | 3 [U] | 2 [P] | 1 [A] | 0 [K] | |------|---------------------------|----------------------------|----------------------------|------------------------------------|---------------------------------------| |Desc. | This was a force stop[^1] | Stop initiated by user[^2] | Stop initiated by platform | App exited - `exit_code` available | Kernel exited - `stop_code` available | @@ -41,7 +40,7 @@ For example, the `stop_reason` has the following values in these scenarios: | `28` | `11100`/`FUP--` | Forced user-initiated shutdown. | | `15` | `01111`/`-UPAK` | Regular user-initiated shutdown. The app and kernel have exited. The `exit_code` and `stop_code` show if the app and kernel shut down cleanly. | | `13` | `01101`/`-UP-K` | The user initiated a shutdown but the app was forcefully killed by the kernel during shutdown. This can be the case if the image doesn't support a clean app exit or the app crashed after receiving a termination signal. Unikraft Cloud ignores the `exit_code` in this scenario. | -| `7` | `00111`/`--PAK` | Unikraft Cloud initiated the shutdown (for example, due to [scale-to-zero](/docs/api/v1/autoscale#scaletozero)). The app and kernel have exited. The `exit_code` and `stop_code` show if the app and kernel shut down cleanly. | +| `7` | `00111`/`--PAK` | Unikraft Cloud initiated the shutdown (for example, due to [scale-to-zero](/features/scale-to-zero)). The app and kernel have exited. The `exit_code` and `stop_code` show if the app and kernel shut down cleanly. | | `3` | `00011`/`---AK` | The app exited. The `exit_code` and `stop_code` show if the app and kernel shut down cleanly. | | `1` | `00001`/`----K` | The instance likely experienced a fatal crash and the `stop_code` contains more information about the cause of the crash. | | `0` | `00000`/`-----` | The stop reason is unknown. | @@ -65,7 +64,7 @@ Unikraft Cloud defines the `stop_code` by the kernel and has the following encod |------|------------------|------------------|----------------|-----------------|----------------| | Desc.| Reserved[^3] | `errno` | `shutdown_bit` | `initlvl` | `reason` | -### Reason +#### Reason The `reason` can be any of the following values: @@ -98,7 +97,7 @@ A level of `127` means the instance was executing the app. :::note For example, an out-of-memory (OOM) situation triggers a page fault `PGFAULT(4)` with `errno` set to `ENOMEM(12)`. -In that case the `stop_code` is `0x000C7F04=818948` and the `stop_reason` is `----K(1)` if the stop occurred during app execution. +In that case the `stop_code` is `0x000C7F04=818948` and the `stop_reason` is `----K (1)` if the stop occurred during app execution. ::: @@ -117,7 +116,8 @@ When an instance stops, Unikraft Cloud evaluates the stop reason and the restart It uses an exponential back-off delay (immediate, 5s, 10s, 20s, 40s, 5m) to slow down restarts in tight crash loops. If an instance runs without problems for 10s, Unikraft Cloud resets the back-off delay and the restart sequence ends. -The `restart.attempt` value in [`GET /v1/instances`](#status) counts restarts in the current sequence. +The `restart.attempt` value in [`GET /instances`](/api/platform/v1/instances#list-instances) counts restarts in the current sequence. +The number of completed restarts is `restart_count`. The `restart.next_at` field indicates when the next restart occurs if a back-off delay is in effect. A manual start or stop of the instance aborts the restart sequence and resets the back-off delay. @@ -129,3 +129,165 @@ A manual start or stop of the instance aborts the restart sequence and resets th [^2]: A stop command originating from the user travels through the platform controller. This is why bit 2 [P] will also always occur for user-initiated stops. [^3]: The system sets reserved bits to 0. Ignore them. + +## Instance templates + +An instance template is a snapshotted instance that acts as a source for cloning new instances. +Cloning a template creates a new instance that resumes from the exact original system state. +It preserves memory contents, open files, and populated caches to bypass the standard boot sequence. +Once you convert an instance into a template, you can't reverse the process. + +To transition an actively running instance into a template, from the guest write the value 1 to `/uk/libukp/template_instance`: + +```bash title="Convert instance to template" +echo 1 > /uk/libukp/template_instance +``` + +This action instructs the controller to freeze running processes and save the instance as a reusable template. + +Templates support [delete locks](/platform/delete-locks), [tags](/platform/tagging), and [autokill](/features/autokill). +The platform measures autokill on a template from the time of last clone, not from instance stop time. + +### Nested templates + +You can clone a template from another template, creating a hierarchy. +Each clone inherits the parent's full state at clone time and can independently accumulate further state before you convert it into its own template. +This enables layered configurations, for example a base template with a warm database connection pool, and child templates specialized for different query workloads. + +No depth limit applies to nesting, and circular references aren't possible because the template state is immutable once set. + +For more information, check out the API reference for instance templates [here](/api/platform/v1/instances#create-template-instances-from-instances). + +## Creating instances + +### Replicas + +The [`POST /instances`](/api/platform/v1/instances#create-instance) request accepts a `replicas` field (default 0) that creates more copies of the instance alongside the base one. + +| Value | Instances created | +|-------|-------------------| +| `0` | 1 (the base instance) | +| `1` | 2 (base + 1 replica) | +| `N` | N + 1 | + +All instances share the same image, memory, arguments, and service group configuration. +Replicas receive independent names and UUIDs. + +### Wait for running + +By default [`POST /instances`](/api/platform/v1/instances#create-instance) returns as soon as the platform queues the instance. +Set `timeout_s` non-zero to block until the instance reaches the `running` state or the timeout expires. +For example: + +```json title="POST /instances" +{ + ... + "image": "nginx:latest", + "memory_mb": 256, + "timeout_s": 10, + ... +} +``` + +:::note +`wait_timeout_ms` is a deprecated compatibility field. +When set, the platform rounds the value up to the next full second. +Use `timeout_s` instead. +::: + +### Delete on stop + +Pass `delete-on-stop` in the features array to automatically delete the instance when it stops: + +```json title="POST /instances" +{ + ... + "image": "nginx:latest", + "memory_mb": 256, + "features": ["delete-on-stop"], + "restart_policy": "never", + ... +} +``` + +This is useful for ephemeral workloads—batch jobs, one-shot tasks—where you don't need to keep the stopped instance. + +:::note +To use the "delete-on-stop" feature, set the `restart_policy` to `never`. +::: + +## Stopping instances + +### Drain timeout + +By default [`PUT /instances/stop`](/api/platform/v1/instances#stop-instances) stops the instance immediately. +Set `drain_timeout_ms` to allow the instance to finish serving in-flight connections before it stops. + +| Value | Behaviour | +|-------|-----------| +| `0` | Stop immediately (default). | +| `-1` | Use the platform's maximum drain timeout. | +| `N` | Drain for up to N milliseconds, then stop. | + +```json title="PUT /instances/stop" +{ + ... + "name": "my-instance", + "drain_timeout_ms": 5000, + ... +} +``` + +While draining, the instance enters the `draining` state. +The platform accepts no new connections. +The instance stops once all connections close or the timeout elapses. + +:::note +You can't combine `drain_timeout_ms` with `force: true`. +Setting both returns a `400` error. +::: + +## Instance metrics + +Use the [`GET /instances/metrics`](/api/platform/v1/instances#get-instances-metrics) endpoint to retrieve runtime statistics for one or more instances. +With no identifiers, the platform returns metrics for all instances in your account. + +### Response format + +Include the header `Accept: application/json` to receive a JSON response. +Otherwise, you will receive metrics in Prometheus text format, that you can parse with Prometheus client libraries. + +**JSON response**: each instance object contains: + +| Field | Type | Description | +|-------|------|-------------| +| `state` | string | Current instance state. | +| `start_count` | int | Number of times the instance has started. | +| `restart_count` | int | Number of restarts (omitted if 0). | +| `started_at` | timestamp | Time of last start (omitted if unset). | +| `stopped_at` | timestamp | Time of last stop (omitted if unset). | +| `uptime_ms` | int64 | Current uptime in milliseconds. | +| `boot_time_us` | int | Boot time in microseconds (omitted if 0). | +| `net_time_us` | int | Network setup time in microseconds (omitted if 0). | +| `rss_bytes` | int64 | Resident set size in bytes. | +| `cpu_time_ms` | int64 | Consumed CPU time in milliseconds. | +| `rx_bytes` | int64 | Bytes received over the network. | +| `rx_packets` | int64 | Packets received from the network. | +| `tx_bytes` | int64 | Bytes transmitted over the network. | +| `tx_packets` | int64 | Packets transmitted over the network. | +| `nconns` | int | Number of active connections. | +| `nreqs` | int | Number of active HTTP requests. | +| `nqueued` | int | Number of queued connections or requests. | +| `ntotal` | int64 | Total connections or requests processed since start. | +| `wakeup_latency` | array | Histogram buckets. Each bucket has `bucket_ms` (upper bound, or `null` for overflow) and `count`. | +| `wakeup_latency_sum` | uint64 | Sum of all recorded wakeup latencies. | + +## Instance logs + +Use the [`GET /instances/logs`](/api/platform/v1/instances#get-instances-logs) endpoint to retrieve logs for one or more instances. +The logs capture the instance's `stdout` and `stderr` output, and they're preserved across restarts and stops. + +## Learn more + +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [instances](/api/platform/v1/instances). diff --git a/pages/platform/metrics.mdx b/pages/platform/metrics.mdx new file mode 100644 index 00000000..3094acac --- /dev/null +++ b/pages/platform/metrics.mdx @@ -0,0 +1,164 @@ +--- +title: Metrics +navigation_icon: chart-bar +description: | + Retrieve runtime statistics for your Unikraft Cloud instances. +--- + +Unikraft Cloud exposes runtime metrics for each instance: CPU time, memory usage, network I/O, connection counts, and scale-to-zero wakeup latencies. + +## Endpoints + +``` +GET /instances/{uuid}/metrics +GET /instances/metrics?uuid=[,…] +POST /instances/metrics (UUIDs or names in request body) +``` + +With no identifiers, the platform returns metrics for all instances in your account. +You can also pass a list of names or UUIDs in the `POST` body. + +## Response formats + +Set the `Accept` header to control the response format: + +| `Accept` header | Format | +|-----------------|--------| +| `application/json` | JSON object with an `instances` array. | +| Any other value (or omitted) | Prometheus text exposition format. | + +## Response fields + +Each entry in the `instances` array contains the following fields: + +### Identity + +| Field | Type | Description | +|-------|------|-------------| +| `uuid` | string | Instance UUID. | +| `name` | string | Instance name. | + +### Lifecycle + +| Field | Type | Description | +|-------|------|-------------| +| `state` | string | Current instance state. | +| `start_count` | int | Number of times the instance has started. | +| `restart_count` | int | Number of automatic restarts. Omitted if `0`. | +| `started_at` | timestamp | Time of last start. Omitted if unset. | +| `stopped_at` | timestamp | Time of last stop. Omitted if unset. | +| `uptime_ms` | int64 | Current uptime in milliseconds. | + +### Boot timing + +| Field | Type | Description | +|-------|------|-------------| +| `boot_time_us` | int | Time from boot trigger to app entry point, in microseconds. Omitted if `0`. | +| `net_time_us` | int | Time to set up the network interface, in microseconds. Omitted if `0`. | + +### Resource usage + +| Field | Type | Description | +|-------|------|-------------| +| `rss_bytes` | int64 | Resident set size: physical memory in use, in bytes. | +| `cpu_time_ms` | int64 | Total CPU time consumed since last start, in milliseconds. | + +### Network throughput + +| Field | Type | Description | +|-------|------|-------------| +| `rx_bytes` | int64 | Bytes received since last start. | +| `rx_packets` | int64 | Packets received since last start. | +| `tx_bytes` | int64 | Bytes transmitted since last start. | +| `tx_packets` | int64 | Packets transmitted since last start. | + +### Connection activity + +These fields reflect the view from the Unikraft Cloud load balancer, not the instance itself. + +| Field | Type | Description | +|-------|------|-------------| +| `nconns` | int | Number of active TCP connections. | +| `nreqs` | int | Number of active HTTP requests (non-zero only for HTTP-mode services). | +| `nqueued` | int | Number of connections or requests waiting for acceptance. | +| `ntotal` | int64 | Total connections or requests processed since last start. | + +### Wakeup latency histogram + +For instances with [scale-to-zero](/features/scale-to-zero) enabled, the `wakeup_latency` field contains a histogram of the time between an incoming connection and the instance resuming execution. + +| Field | Type | Description | +|-------|------|-------------| +| `wakeup_latency` | array | Histogram buckets. Each bucket has `bucket_ms` (upper bound in ms, or `null` for the overflow bucket) and `count` (number of wakeups in that bucket). | +| `wakeup_latency_sum` | uint64 | Sum of all recorded wakeup latencies in milliseconds. | + +## Prometheus metrics + +When not requesting JSON, the endpoint returns standard Prometheus text format. +The available metric names are: + +``` +instance_state +instance_start_count +instance_restart_count +instance_started_at +instance_stopped_at +instance_uptime_s +instance_boot_time_s +instance_net_time_s +instance_rss_bytes +instance_cpu_time_s +instance_rx_bytes +instance_tx_bytes +instance_rx_packets +instance_tx_packets +instance_nconns +instance_nreqs +instance_nqueued +instance_ntotal +instance_wakeup_latency_seconds (histogram) +``` + +The platform labels all metrics with `uuid` and `name`. + +## Example + +Set `UKC_TOKEN` and `UKC_METRO` for the API call: + +```bash title="" +curl -X GET \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.$UKC_METRO.unikraft.cloud/v1/instances/metrics?uuid=66d05e09-1436-4d1f-bbe6-6dc03ae48d7a" +``` + +```json title="" +{ + "instances": [ + { + "uuid": "66d05e09-1436-4d1f-bbe6-6dc03ae48d7a", + "name": "nginx-1a747", + "state": "running", + "start_count": 3, + "uptime_ms": 42310, + "boot_time_us": 19810, + "rss_bytes": 12582912, + "cpu_time_ms": 14, + "rx_bytes": 4096, + "rx_packets": 8, + "tx_bytes": 8192, + "tx_packets": 12, + "nconns": 1, + "nreqs": 0, + "nqueued": 0, + "ntotal": 7 + } + ] +} +``` + +## Learn more + +* Unikraft Cloud's [REST API reference](/api/platform/v1) +* [Instance states](/platform/instances#instance-states) +* [Scale-to-zero](/features/scale-to-zero) diff --git a/pages/platform/metros.mdx b/pages/platform/metros.mdx index 0062d560..7fe18e8e 100644 --- a/pages/platform/metros.mdx +++ b/pages/platform/metros.mdx @@ -15,7 +15,19 @@ By default, at present, Unikraft Cloud supports the following metros: :::tip -You can list the metros available to your account via the `kraft cloud metro ls` command. +You can list the metros available to your account via the CLI. + + + +```bash title="unikraft" +unikraft metros list +``` + +```bash title="kraft" +kraft cloud metro ls +``` + + ::: @@ -67,18 +79,26 @@ If you require running solutions in-house or on-premise, either for SOC/ISO/HIPP ## Usage -When using `kraft`, you can set the metro you wish to use by either setting the CLI flag `--metro` in any `kraft cloud` subcommand or by setting the environmental variable `UKC_METRO`. +You can set the metro you wish to use by setting the `--metro` flag. +The legacy CLI also supports the `UKC_METRO` environment variable. Use the IATA code descriptor to target the specific metro. For example with `fra`: -```bash title="" -# Via CLI flag -kraft cloud --metro fra run -p 443:8080 nginx:latest - -# or via environmental variable -export UKC_METRO=fra -kraft cloud run -p 443:8080 nginx:latest -``` + + ```bash title="unikraft" + unikraft run --metro=fra -p 443:8080/http+tls --image=nginx:latest + ``` + + ```bash title="kraft" + # Via CLI flag + kraft cloud --metro fra run -p 443:8080 nginx:latest + + # or via environmental variable + export UKC_METRO=fra + kraft cloud run -p 443:8080 nginx:latest + ``` + The `UKC_METRO` environment variable is only supported by the legacy CLI. + For on-premise or enterprise users, set the FQDN of the API endpoint as the metro argument, e.g.: diff --git a/pages/platform/quotas.mdx b/pages/platform/quotas.mdx new file mode 100644 index 00000000..68e27fc7 --- /dev/null +++ b/pages/platform/quotas.mdx @@ -0,0 +1,127 @@ +--- +title: Quotas +navigation_icon: gauge +description: | + Understand and track your Unikraft Cloud resource quotas. +--- + +Unikraft Cloud enforces per-account resource quotas to govern how many instances, service groups, volumes, and compute resources you can provision. +Use the quotas endpoint to inspect your current usage and limits. + +## Endpoints + +``` +GET /users/quotas +GET /users/{uuid}/quotas +``` + +`GET /users/quotas` (no UUID) returns the quota for the authenticated user. + +## Response structure + +The response contains a top-level `quotas` array. +Each element has three sections: `used`, `hard`, and `limits`. + +### `used`: Current resource usage + +| Field | Type | Description | +|-------|------|-------------| +| `instances` | int | Total number of instances (stopped + running). | +| `live_instances` | int | Number of currently running instances. | +| `live_vcpus` | int | Number of vCPUs in use across running instances. | +| `live_memory_mb` | int | Memory (MB) in use across running instances. | +| `service_groups` | int | Number of allocated service groups. | +| `services` | int | Number of allocated services (published ports). | +| `volumes` | int | Number of allocated volumes. | +| `total_volume_mb` | int | Total volume storage in use (MB). | + +### `hard`: Maximum allowed allocations + +| Field | Type | Description | +|-------|------|-------------| +| `instances` | int | Maximum number of instances. | +| `live_vcpus` | int | Maximum vCPUs running simultaneously. | +| `live_memory_mb` | int | Maximum memory (MB) running simultaneously. | +| `service_groups` | int | Maximum number of service groups. | +| `services` | int | Maximum number of services. | +| `volumes` | int | Maximum number of volumes. | +| `total_volume_mb` | int | Maximum total volume storage (MB). | + +### `limits`: Per-resource range constraints + +These define the minimum and maximum values you can specify when creating individual resources. + +| Field | Type | Description | +|-------|------|-------------| +| `min_memory_mb` | int | Minimum memory (MB) per instance. | +| `max_memory_mb` | int | Maximum memory (MB) per instance. | +| `min_vcpus` | int | Minimum vCPUs per instance. | +| `max_vcpus` | int | Maximum vCPUs per instance. | +| `min_volume_mb` | int | Minimum volume size (MB). | +| `max_volume_mb` | int | Maximum volume size (MB). | +| `min_autoscale_size` | int | Minimum autoscale group size. | +| `max_autoscale_size` | int | Maximum autoscale group size. | + +## Example + +Set `UKC_TOKEN` and `UKC_METRO` for the API call: + +```bash title="" +curl -X GET \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + "https://api.$UKC_METRO.unikraft.cloud/v1/users/quotas" +``` + +```json title="" +{ + "quotas": [ + { + "status": "ok", + "uuid": "a1b2c3d4-...", + "used": { + "instances": 3, + "live_instances": 2, + "live_vcpus": 2, + "live_memory_mb": 512, + "service_groups": 2, + "services": 4, + "volumes": 1, + "total_volume_mb": 512 + }, + "hard": { + "instances": 50, + "live_vcpus": 16, + "live_memory_mb": 4096, + "service_groups": 20, + "services": 40, + "volumes": 20, + "total_volume_mb": 20480 + }, + "limits": { + "min_memory_mb": 16, + "max_memory_mb": 4096, + "min_vcpus": 1, + "max_vcpus": 8, + "min_volume_mb": 128, + "max_volume_mb": 10240, + "min_autoscale_size": 0, + "max_autoscale_size": 10 + } + } + ] +} +``` + +:::note +The `hard.live_instances` field is a compatibility alias for `hard.live_vcpus`. +It's kept for backward compatibility and the platform may drop it in a future API version. +Prefer `hard.live_vcpus`. +::: + +## Learn more + +* Unikraft Cloud's [REST API reference](/api/platform/v1) +* [Instances](/platform/instances) +* [Volumes](/platform/volumes) +* [Services](/platform/services) diff --git a/pages/platform/services.mdx b/pages/platform/services.mdx index 908a6e14..57c241c7 100644 --- a/pages/platform/services.mdx +++ b/pages/platform/services.mdx @@ -3,6 +3,8 @@ title: Services navigation_icon: group --- +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + On creation, Unikraft Cloud assigns each instance a private IP address and private FQDN for internal connectivity. It connects them to the Internet through a *service*: create a service and attach instances to it. @@ -10,39 +12,61 @@ It connects them to the Internet through a *service*: create a service and attac A service defines how to reach a group of instances from the outside world. It maps an external FQDN port to an internal port common to all instances. -The platform [load balances](/docs/guides/features/lb/) incoming connections across instances. +The platform [load balances](/features/load-balancing) incoming connections across instances. Avoid placing apps that expose different ports in the same service. ::: -If you use the `kraft cloud deploy` command, part of the output lists a service, for example: +If you use the deploy or run flow, part of the output lists a service, for example: ```ansi title="" ├─ service: frosty-sky-vz8kwsm ``` -Because `kraft cloud deploy` is a single command to deploy a service, it automatically creates a service and attaches the new instance to it. -The rest of this guide shows how to create a service first, then use `kraft cloud deploy` to create and attach instances to it. +Because the deploy or run flow is a single command to deploy a service, it automatically creates a service and attaches the new instance to it. +The rest of this guide shows how to create a service first, then use the CLI to create and attach instances to it. -First, create a new service with the `kraft cloud service` command: +First, create a new service with the CLI: -```bash title="" + + +```bash title="unikraft" +unikraft services create \ + --set name=my-service \ + --set metro=fra \ + --set services=443:8080/tls+http +``` + +```bash title="kraft" kraft cloud service create --name my-service 443:8080/http+tls ``` + + This creates a new service named `my-service` listening on port 443. Unikraft Cloud terminates TLS and sends HTTP to port 8080. This example assumes that the app opens port 8080. -Now use `kraft cloud deploy` with the `--service` flag to attach the instance to the `my-service` service. -For example, from the [Go web server guide](/docs/guides/go) (see all guides [here](/docs/guides/)): +Now use the CLI with the service flag to attach the instance to the `my-service` service. +For example, from the [Go web server guide](/guides/go): -```bash title="" + + +```bash title="unikraft" git clone https://github.com/unikraft-cloud/examples cd examples/http-go1.21/ -kraft cloud deploy --service my-service . +unikraft build . --output /http-go121:latest +unikraft run --metro=fra --service my-service -m 256MiB --image=/http-go121:latest ``` +```bash title="kraft" +git clone https://github.com/unikraft-cloud/examples +cd examples/http-go1.21/ +kraft cloud deploy --service my-service -M 256 . +``` + + + This creates a new Go web server instance and immediately attaches it to the `my-service` service. The output shows the instance address and other details: @@ -55,7 +79,7 @@ The output shows the instance address and other details: ├─────────── url: https://my-service-rrtckyyi.fra.unikraft.app ├───────── image: http-go121@sha256:4d536236d226781874c3ad930dbc936c4f407aa3483a1f8e961ba63a7a72c78d ├───── boot time: 17.14 ms - ├──────── memory: 128 MiB + ├──────── memory: 256 MiB ├─────── service: my-service ├── private fqdn: http-go121-fkt1x.internal ├──── private ip: 172.16.6.6 @@ -78,7 +102,7 @@ hello, world! :::tip -If you specify a port with the `-p` option when using `kraft cloud deploy`, the command creates a service automatically. +If you specify a port with the publish flag when using the deploy or run flow, the command creates a service automatically. In that case the platform deletes the service when the instance ends, and you can't define the service name. ::: @@ -87,11 +111,161 @@ That's it. In the end, if you want to remove a service, use: -```bash title="" + + +```bash title="unikraft" +unikraft services delete my-service +``` + +```bash title="kraft" kraft cloud service remove my-service ``` + + +## Handlers + +Handlers define how the service will handle incoming connections and forward traffic from the Internet to your app. +For example, you can configure a service to end TLS connections, redirect HTTP traffic, or enable HTTP mode for load balancing. +You configure the handlers for every published service port individually. + +Currently, there are 3 supported handlers: + + + + tls + http + redirect + + + Terminate the TLS connection at the Unikraft Cloud gateway using the wildcard certificate issued for the `unikraft.cloud` domain. + The gateway forwards the unencrypted traffic to your app. + + + Enable HTTP mode on the load balancer to load balance on the level of individual HTTP requests. + In this mode, the load balancer accepts only HTTP connections. + If you don't set this option, the load balancer works in TCP mode and distributes TCP connections. + + + Redirect traffic from the source port to the destination port. + + + +:::note +The following set of constraints apply when publishing ports: +- Port 80: **must** have `http` and **must not** have `tls` set. +- Port 443: **must** have `http` and `tls` set. +- You can only set the `redirect` handler on port 80 (HTTP) to redirect to + port 443 (HTTPS). +- All other ports **must** have `tls` and **must not** have `http` set. +::: + +For example, the following creates the service `my-service` with three published ports: +- Port 443 with both `http` and `tls` handlers (HTTP mode). +- Port 80 with the `http` and `redirect` handlers (HTTP mode). +- Port 10000 with only the `tls` handler (TCP mode). + + + +```bash title="unikraft" +unikraft services create \ + --set name=my-service \ + --set metro=fra \ + --set services=443:8080/tls+http \ + --set services=80:8080/http+redirect \ + --set services=10000:10000/tls +``` + +```bash title="kraft" +kraft cloud service create --name my-service 443:8080/http+tls 80:8080/http+redirect 10000:10000/tls +``` + + + +## `UDP` Support + +`UDP` is a defined service protocol alongside `TCP`. +Unikraft Cloud stores and reports it in the `protocol` field of each published port. + +You can use `UDP` for both internal VM-to-VM traffic and external traffic. + +### External `UDP` requirements + +To expose `UDP` externally for an user, the box must have an assigned IP address (primary or another assigned IP). +That IP must appear in the `addresses` array in `users.json` for that user: + +```json title="users.json" +{ + [ + "name": "user", + "addresses": [ + { + "ip": "a.b.c.d", + "internal_ip": "x.y.z.w", + "host": "host_name" + } + ] + ] +} +``` + +The `internal_ip` field is optional and used when the box routes traffic through an internal IP. + +You must also list the `UDP` IPs in the `additional_ip_addresses` directive in `nginx.conf.template` (space-separated). + +### Creating a `UDP` service + +To create a `UDP` service, set both `protocol` and `ip`: + +```json title="POST /services" +{ + "services": [ + { + "port": 7777, + "destination_port": 8080, + "protocol": "udp", + "ip": "a.b.c.d" + } + ] +} +``` + +The service `ip` must match an `addresses[].ip` value in `users.json` for the user. + +## Connection limits + +Every service group has a soft and a hard connection limit that the load balancer enforces. + +| Field | Default | Range | Description | +|-------|---------|-------|-------------| +| `soft_limit` | `1` | 1–65535 | The load balancer starts queuing new connections once the number of active connections reaches this value. | +| `hard_limit` | `65535` | 1–65535 | The load balancer rejects new connections once the number of active connections reaches this value. | + +`soft_limit` must be less than or equal to `hard_limit`. +Setting `soft_limit > hard_limit` returns a `400` error. + +You can set both fields at service group creation time and update them later with `PATCH /services/{name}`. + +```json title="POST /services" +{ + "name": "my-service", + "soft_limit": 100, + "hard_limit": 500 +} +``` + +## Persistent service groups + +A service group is **persistent** when your user account owns it rather than a specific instance. + +- A service group created with the CLI (or `POST /services`) is persistent. + It survives independently of any instances attached to it. +- A service group created implicitly via the publish flag on instance creation belongs to the instance. + The platform deletes it when you delete that instance. + +The `GET /services/{name}` response includes a `persistent` field (`bool`) that indicates which case applies. + ## Learn more -* The `kraft cloud` [CLI reference](/docs/cli/), in particular the services subcommand -* Unikraft Cloud's [REST API reference](/docs/api/v1), in particular the section on services +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [service groups](/api/platform/v1/service-groups). diff --git a/pages/platform/tagging.mdx b/pages/platform/tagging.mdx new file mode 100644 index 00000000..1c6fb9f8 --- /dev/null +++ b/pages/platform/tagging.mdx @@ -0,0 +1,91 @@ +--- +title: Tagging +navigation_icon: tag +--- + +Tags are labels that you can attach to instances, volumes, instance templates, and volume templates to organize and filter your resources. + +Don't confuse them with image tags which serve a different purpose (see [images](/api/platform/v1/images)). + +## Tag format + +Tags can be up to 256 characters long and may contain alphanumeric characters plus `-`, `+`, `_`, `.`, `:`, and `=`. +Each resource (instance, volume, instance template, volume template) can have up to 16 tags. +You can currently only manage tags via the API. +Kraftkit neither displays them nor allows you to edit them. + +## Adding tags + +Add tags to a resource at creation time: + +```json title="POST /instances" +{ + "image": "...", + "memory_mb": ..., + "tags": [ "production", "customer_A" ] +} +``` + +Add tags to an existing resource (`"op": "set"` will replace all existing tags): + +```json title="PATCH /instances" +{ + "name": "example-instance", + "prop": "tags", + "op": "set", + "value": ["testsystem", "customer_B"] +} +``` + +:::note +Use `/volumes` for volumes, `/instances/templates` for instance templates, and `/volumes/templates` for volume templates. +::: + +Add tags to a resource without replacing existing ones (`"op": "add"`): + +```json title="PATCH /instances" +{ + "name": "example-instance", + "prop": "tags", + "op": "add", + "value": ["legacy"] +} +``` + +:::note +Tags are automatically deduplicated. +Repeating the same tag adds it only once. +Adding a tag already attached to a resource has no effect. +::: + +## Removing tags + +Remove specific tags from a resource (`"op": "del"`): + +```json title="PATCH /instances" +{ + "name": "example-instance", + "prop": "tags", + "op": "del", + "value": ["legacy"] +} +``` + +## Filtering by tag + +Filter the instance list, volume list, instance template list, or volume template list by one or more tags. +Only resources that have _all_ the specified tags appear in the results. + +``` +GET /instances?tags=production,customer_A +``` + +Separate tags by commas (`,`). + +## Tags in API responses + +Each instance, volume, instance template, or volume template status response includes a `tags` array if at least one tag is present. + +## Learn more + +* Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the sections on [instances](/api/platform/v1/instances) and [volumes](/api/platform/v1/volumes) diff --git a/pages/platform/troubleshooting.mdx b/pages/platform/troubleshooting.mdx index 60c44394..151094fb 100644 --- a/pages/platform/troubleshooting.mdx +++ b/pages/platform/troubleshooting.mdx @@ -13,12 +13,22 @@ If you need help, reach out to Unikraft Cloud Support. An app may crash, freeze, or misbehave. To inspect it, try either: -```bash title="" + + +```bash title="unikraft" +unikraft instances logs +# or +unikraft instances get +``` + +```bash title="kraft" kraft cloud inst logs # or kraft cloud inst get ``` + + This may, in certain times, provide insufficient information. @@ -72,7 +82,7 @@ There is no service on this URL. ``` This happens when you connect with an HTTPS client (port `443`) and the app doesn't expose that port. -Database services such as [MongoDB](/docs/guides/mongodb) or [MariaDB](/docs/guides/mariadb) use different ports (for example, `27017`, `3306`). +Database services such as [MongoDB](/guides/mongodb) or [MariaDB](/guides/mariadb) use different ports (for example, `27017`, `3306`). Use the correct exposed port. You may need a TLS tunnel (see below). @@ -81,9 +91,9 @@ You may need a TLS tunnel (see below). ## Connect to a non-TLS app Unikraft Cloud uses TLS to expose services to the outside world. -Some apps (such as [MongoDB](/docs/guides/mongodb) or [MariaDB](/docs/guides/mariadb)) don't use TLS. -Create a TLS tunnel via `kraft cloud tunnel`, which opens a local endpoint (`localhost` / `127.0.0.1`) and forwards traffic over TLS. -See the [MariaDB guide](/docs/guides/mariadb) for an example. +Some apps (such as [MongoDB](/guides/mongodb) or [MariaDB](/guides/mariadb)) don't use TLS. +Create a TLS tunnel via the legacy CLI, which opens a local endpoint (`localhost` / `127.0.0.1`) and forwards traffic over TLS. +See the [MariaDB guide](/guides/mariadb) for an example. ## "No such file or directory" when building or deploying an image @@ -96,33 +106,42 @@ When building / deploying an image, you may get the error below: This often means the local `kraft` cache is in an inconsistent state. -To solve this, remove the local `kraft` cache and local packages: +To solve this, remove the local legacy CLI cache and local packages: -```bash title="" + + +```bash title="kraft" kraft pkg rm --all rm -fr ~/.local/share/kraftkit ``` + + ## Launched app not visible in list The most common reason is that you deployed an app to one metro but listed a different one. -Set the metro for a session with an env variable: +Use the `--metro` flag per command, or use the legacy CLI environment variable for a session: -```bash title="" -export UKC_METRO= -``` + -or for each indvidual command via the `--metro` flag, for example: +```bash title="unikraft" +unikraft instances list --metro +``` -```bash title="" +```bash title="kraft" +export UKC_METRO= kraft cloud instance list --metro ``` + + +> **Note:** The `UKC_METRO` environment variable is only supported by the legacy CLI. + ## How can you cache the app's filesystem for faster builds When using a `Dockerfile` for the app filesystem, Unikraft Cloud passes the commands to [BuildKit](https://docs.docker.com/build/buildkit/). -By default, each `kraft cloud deploy` command starts an ephemeral BuildKit container. +By default, each legacy deploy command starts an ephemeral BuildKit container. The platform then removes the filesystem app data, so each deploy starts from zero. To prevent this, follow the instructions [here](https://unikraft.org/guides/building-dockerfile-images-with-buildkit): @@ -144,7 +163,7 @@ Here `$HOME/.buildkit-cache` is a local path on your machine where BuildKit stor ## What's a `Kraftfile` -A `Kraftfile` is used by the `kraft` CLI tool to understand how to build and deploy your instance. +A `Kraftfile` is used by the CLI toolchain to understand how to build and deploy your instance. Typically you can use the default `Kraftfile` found in each Unikraft Cloud example. Below is a sample `Kraftfile` with a brief explanation: @@ -163,28 +182,30 @@ labels: cloud.unikraft.v1.instances/scale_to_zero.cooldown_time_ms: 1000 ``` -The `runtime` specifies one of the Unikraft Cloud runtimes (unikernels) built to run different languages and apps. +The `runtime` specifies one of the Unikraft Cloud runtimes (microVMs) built to run different languages and apps. Here it specifies a Python runtime. -The `rootfs` parameter tells `kraft` to use a `Dockerfile` in the same directory to build the root filesystem, and the `cmd` parameter which command to run when deployed (although you can also specify this in the `Dockerfile`). +The `rootfs` parameter tells the CLI to use a `Dockerfile` in the same directory to build the root filesystem, and the `cmd` parameter which command to run when deployed (although you can also specify this in the `Dockerfile`). Finally the `labels` specify runtime options. In the example above, the configuration enables stateful scale-to-zero and the platform waits 1 second with no requests before putting the instance to sleep. ## Debugging the build, packaging and pushing steps -The `kraft cloud deploy` command performs steps on your local device before actually deploying your app to Unikraft Cloud: +The legacy deploy command performs steps on your local device before actually deploying your app to Unikraft Cloud: 1. Downloads the runtime image (defined in the `Kraftfile`) from the Unikraft Cloud registry. 1. Builds the app filesystem using the `Dockerfile` via [BuildKit](https://unikraft.org/guides/building-dockerfile-images-with-buildkit). 1. Packages the app filesystem and the runtime in an Open Container Initiative (OCI) image. 1. Pushes the OCI image to the to the Unikraft Cloud registry under your username's namespace. -If any of these steps fail, enable `kraft` debugging with the `--log-level` and `--log-type` flags: +If any of these steps fail, enable legacy CLI debugging with the `--log-level` and `--log-type` flags: -```bash title="" -kraft cloud deploy --log-level debug --log-type basic [...] -``` + + ```bash title="kraft" + kraft cloud deploy --log-level debug --log-type basic [...] + ``` + You should then see debug output for the 4 steps above. @@ -213,14 +234,23 @@ If not, report it with the output on the [Discord server](/discord). ## Debugging running apps The most direct way to debug an app is to use the app console output, which may include kernel output. -To see it, after starting the Unikraft Cloud instance (via `kraft cloud deploy` or `kraft cloud inst create`), use: +To see it, after starting the Unikraft Cloud instance, use: -```bash title="" + + +```bash title="unikraft" +unikraft instances logs +``` + +```bash title="kraft" kraft cloud inst logs ``` + + In case of a crash, you'll see a full crash output: +{/* vale off */} ```text title="" Powered by Unikraft Telesto (0.16.2~9c264902) [ 0.066949] CRIT: [libukvmem] Cannot handle write page fault at 0x1000bb8024 (ec: 0x2): -12 @@ -248,10 +278,12 @@ Powered by Unikraft Telesto (0.16.2~9c264902) [ 0.080871] CRIT: [appelfloader] [0x000000100041fdcb] [ 0.081311] CRIT: [appelfloader] Bad frame pointer -It looks like the instance exited fatally. To see more details about why, run: +It looks like the instance exited fatally. +Run this for more details: - kraft cloud instance get http-python312-hb7ij + unikraft instances get http-python312-hb7ij ``` +{/* vale on */} Use the recommended command for detailed output. This yields output like: @@ -289,7 +321,8 @@ Here the cause is insufficient memory. If the stop reason lacks detail, enable debug tracing for the instance. -To do that, in your app of choice in the [examples repository](https://github.com/unikraft-cloud/examples/), or in an app directory you created, update the `runtime` entry in the `Kraftfile` to reference the debug build of the image you use by adding `-dbg` to the name of the runtime. +To do that, pick an app from the [examples repository](https://github.com/unikraft-cloud/examples/) or an app directory you created. +Update the `runtime` entry in the `Kraftfile` to reference the debug build by adding `-dbg` to the runtime name. For example, if you want to run the [http-go1.21 example](https://github.com/unikraft-cloud/examples/tree/main/http-go1.21) with debug output, update its `Kraftfile` as follows: ```yaml title="Kraftfile" @@ -305,16 +338,33 @@ cmd: ["/server"] That is, change `base:latest` to `base:latest-dbg`. Now Unikraft Cloud is ready to re-deploy: -```bash title="" + + +```bash title="unikraft" +unikraft build . --output /http-go-strace:latest +unikraft run --metro=fra --name http-go-strace -p 443:8080/http+tls --image=/http-go-strace:latest +``` + +```bash title="kraft" kraft cloud deploy --name http-go-strace -p 443:8080 . ``` + + You can now inspect the logs as before and view system call tracing: -```bash title="" + + +```bash title="unikraft" +unikraft instances logs http-go-strace +``` + +```bash title="kraft" kraft cloud inst logs http-go-strace ``` + + ```ansi title="" [ 0.000000] Info: [libkvmplat] Unikraft Telesto (0.16.2~5b96d531) [ 0.000000] Info: [libkvmplat] Architecture: x86_64 @@ -349,14 +399,27 @@ Contact Unikraft Cloud on the [Discord server](/discord) and include this output :::tip While you debug an issue you can mitigate crashes by setting a restart policy. -For example, use `kraft cloud deploy --restart on-failure` to have the platform restart the app if it crashes. -Find more info on [restart policies here](/docs/api/v1/instances/#restart-policy). +For example, use the CLI to set `--restart on-failure` so the platform restarts the app if it crashes. + + + +```bash title="unikraft" +unikraft run --metro=fra --restart=on-failure --image=my-app:latest +``` + +```bash title="kraft" +kraft cloud deploy --restart on-failure . +``` + + + +Find more info in the [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). ::: ## Learn more -* The `kraft cloud` [CLI reference](/docs/cli/) -* Unikraft Cloud's [REST API reference](/docs/api/v1) -* Many more guides [here](/docs/guides) +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1). +* Many more guides [here](/guides/bun). diff --git a/pages/platform/volumes.mdx b/pages/platform/volumes.mdx index 00f6ba94..e2b5071e 100644 --- a/pages/platform/volumes.mdx +++ b/pages/platform/volumes.mdx @@ -3,33 +3,56 @@ title: Volumes navigation_icon: cylinder --- -A volume is a persistent storage device that keeps data across restarts. +A volume is a persistent storage device that keeps data across restarts and even redeployments. -In this guide you will create a volume, attach it to a web server instance, and write to it. +This documents presents a guide where you will create a volume, attach it to a web server instance, and write to it. Then you will detach it, remove that instance, attach it to a new instance, and confirm the data persisted. -Finally, the guide covers how to share volumes across instances (read-only at present). -:::note +Then, it presents some features of volumes. -You can also share a volume across many instances as a read-only mount (read-write sharing is in progress). +## Volume workflow +:::note +You can create an instance with attached volumes in the same call - see the `volumes` field for the [`POST /instances`](/api/platform/v1/instances#create-instance) endpoint. +In this case, the volume's lifetime is tied to the instance - when you delete the instance, the volume disappears. ::: -## Setting up the volume +### Setting up the volume -To start, create the volume via `kraft cloud volume`, with size in MBs and name `my-volume`: +To start, create the volume with the CLI, with size in MBs and name `my-volume`: -```bash title="" -kraft cloud volume create --size 100 --name my-volume + + +```bash title="unikraft" +unikraft volumes create \ + --set name=my-volume \ + --set size=100 \ + --set metro=fra ``` +```bash title="kraft" +kraft cloud volume create \ + --size 100 \ + --name my-volume +``` + + + The command should return the volume's UUID, and you can check the operation worked via: -```bash title="" + + +```bash title="unikraft" +unikraft volumes list +``` + +```bash title="kraft" kraft cloud volume list ``` + + which should output something like: ```ansi title="" @@ -39,15 +62,19 @@ my-volume 15 seconds ago 100 MiB available true The `ATTACHED TO` field is empty because the platform hasn't attached it to any instance yet. -## Populating the volume with local data (Optional) +### Populating the volume with local data (optional) -If you'd like to populate your empty volume with local data, you can use the `kraft cloud volume import` command. +To populate an empty volume with local data, use the legacy CLI volume import command. For example, assuming the volume's name is `my-volume` and that the data you want to import are in your `my-data` directory, you would run: -```bash title="" + + +```bash title="kraft" kraft cloud volume import --volume my-volume --source my-data ``` + + You should see output like: ```ansi title="" @@ -55,11 +82,11 @@ You should see output like: [+] Spawning temporary volume data import instance... done! [0.1s] [+] Importing data (256 B) ••••••••••••••••••••••••••••••••••••••••••• 100% [0.1s] -[●] Import complete - │ - ├─── volume: my-volume - ├─ imported: 256 B - └─ capacity: 100 MiB +[●] Import complete + │ + ├─── volume: my-volume + ├─ imported: 256 B + └─ capacity: 100 MiB ``` ## Setting up the web server @@ -111,12 +138,21 @@ On every request, this simple server will write a timestamp to a file on the mounted persistent volume and print out the current contents of the file. Start the Flask web server, create a -[service](/docs/platform/services) for it via the `-p` flag, and mount the `my-volume` volume at `/mnt`: +[service](/platform/services) for it via the publish flag, and mount the `my-volume` volume at `/mnt`: -```bash title="" + + +```bash title="unikraft" +unikraft build . --output /http-python312-flask30:latest +unikraft run --metro=fra -m 512MiB -p 443:8080/http+tls -v my-volume:/mnt --image=/http-python312-flask30:latest +``` + +```bash title="kraft" kraft cloud deploy -M 512 -p 443:8080 --volume my-volume:/mnt . ``` + + You should see output like: ```ansi title="" @@ -137,10 +173,18 @@ You should see output like: To confirm that the platform attached the volume, run: -```bash title="" + + +```bash title="unikraft" +unikraft volumes get my-volume +``` + +```bash title="kraft" kraft cloud volume get my-volume ``` + + You should see output like: ```text title="" @@ -148,7 +192,7 @@ NAME CREATED AT SIZE ATTACHED TO STATE PERSI my-volume 34 minutes ago 100 MiB http-python312-flask30-2h608 mounted true ``` -## Testing it +## Testing the server The Flask server writes the time and date to `/mnt/log.txt` for each request. Test it by running `curl` several times. @@ -173,18 +217,38 @@ Log file created. To test data persistence, first stop the instance, detach the volume, and remove the instance: -```bash title="" + + +```bash title="unikraft" +unikraft instances stop http-python312-flask30-2h608 +unikraft instances delete http-python312-flask30-2h608 +``` + +```bash title="kraft" kraft cloud instance stop http-python312-flask30-2h608 kraft cloud volumes detach my-volume kraft cloud instance rm http-python312-flask30-2h608 ``` + + +The explicit volume detach command is only available in the legacy CLI. + Now start another instance and, like before, mount the same volume: -```bash title="" + + +```bash title="unikraft" +unikraft build . --output /http-python312-flask30:latest +unikraft run --metro=fra -m 512MiB -p 443:8080/http+tls -v my-volume:/mnt --image=/http-python312-flask30:latest +``` + +```bash title="kraft" kraft cloud deploy -M 512 -p 443:8080 --volume my-volume:/mnt . ``` + + This should output something like: ```ansi title="" @@ -207,7 +271,7 @@ Run one final `curl` to this new address. You should see the previous contents plus a new entry at the bottom: ```bash title="" -$ curl https://holy-bonobo-px345skl.fra.unikraft.app +$ curl https://winter-field-v6m37jgs.fra.unikraft.app Log file created. 2024-02-24 08:15:32 2024-02-24 08:15:34 @@ -219,12 +283,105 @@ Log file created. To clean up, first detach the volume from all instances and then remove it: -```bash title="" + + +```bash title="unikraft" +unikraft volumes delete +``` + +```bash title="kraft" kraft cloud volume detach kraft cloud volume remove ``` + + +The explicit volume detach command is only available in the legacy CLI. + +## Volume templates + +A volume template is a volume in the `TEMPLATE` state. +Once a volume transitions to template state, it's immutable: you can clone it but not write to it or delete it while active clones exist. + +The platform creates volume templates automatically when you convert an instance into an [instance template](/platform/instances#instance-templates): the platform converts all volumes attached to that instance into volume templates as an atomic operation. + +Volume templates have their own create, read, update, and delete endpoints at [`/volumes/templates`](/api/v1/volumes#create-template-volumes). +They support [delete locks](/platform/delete-locks) and [tags](/platform/tagging). + +## Volume cloning + +A volume clone is an independent copy of an existing volume. +The clone operation is **asynchronous**: the platform creates the new volume immediately in a pending state, and the data copy completes in the background. +To clone one or more volumes, you can use the [`POST /volumes/clone`](/api/v1/volumes#clone-volumes) endpoint. + +The platform also clones volumes implicitly when you clone an instance or create one from a template. +It clones each attached volume and attaches the clone to the new instance. + +You can't delete a source volume or template while it has active clones. + +## Shared volumes + +More than one instance can mount the same volume simultaneously as long as all mounts are in *read-only* mode. +Read-only sharing is always permitted regardless of how many other instances have the volume mounted. + +A *read-write* mount is **exclusive**. +You can't add one while any other mount exists on that volume, and a volume can't have two *read-write* mounts at once. + +Specify `readonly: true` when attaching a volume at instance creation or via the attach endpoint: + +```json title="POST /instances" +{ + ... + "volumes": [ + { + "name": "my-shared-volume", + "at": "/data", + "readonly": true + } + ], + ... +} +``` + +## Custom filesystems + +By default, volumes use a platform-configured filesystem (typically `ext4` for block volumes and `virtiofs` for hosted volumes). +If the platform operator has registered more named filesystems, you can select one by passing its name in the `filesystem` field when creating a volume: + +```json title="POST /volumes" +{ + "name": "my-volume", + "size_mb": 512, + "filesystem": "virtiofs" +} +``` + +### Managed volumes + +:::caution +This is an advanced feature that's only available on the self-hosted platform. +Quotas aren't yet enforced by the platform for managed volumes, so use them with caution to avoid filling up your host's disk. +::: + +A managed volume points to an existing directory on the host rather than a platform-allocated storage file. + +Create a managed volume with a `host_path` field instead of `size_mb`: + +```json title="POST /volumes" +{ + "name": "my-managed-volume", + "host_path": "/srv/data/mydir", + "uid": 1000, + "gid": 1000 +} +``` + +- `host_path` must be an absolute, normalised path to an existing directory on the host (no `.`, `..`, or `:` components). +- `uid` and `gid` set the ownership used when accessing the volume from the guest. Both default to `0` if not specified. +- `size_mb` and `template` are mutually exclusive with `host_path`. +- The platform doesn't create, format, resize, or delete the backing directory for managed volumes. + ## Learn more -* The `kraft cloud` [CLI reference](/docs/cli/), in particular the [deploy](/docs/cli/deploy) and [volumes](/docs/cli/volume) subcommands -* Unikraft Cloud's [REST API reference](/docs/api/v1), in particular the section on [volumes](/docs/api/v1/volumes) +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* Unikraft Cloud's [REST API reference](/api/platform/v1), in particular the section on [volumes](/api/platform/v1/volumes). diff --git a/pages/tutorials/docker-to-ukc.mdx b/pages/tutorials/docker-to-ukc.mdx new file mode 100644 index 00000000..accb3129 --- /dev/null +++ b/pages/tutorials/docker-to-ukc.mdx @@ -0,0 +1,389 @@ +--- +title: Docker To Unikraft Cloud +navigation_icon: package +--- + +This tutorial shows how to take an existing container image and turn it into a Unikraft Cloud deployment. +It starts with a safe first pass that keeps the upstream filesystem intact. +It then shows how to slim the image down to a bare minimum deployment. + +The examples use `FROM image:latest` as a placeholder for the upstream OCI image. +They follow the same general pattern used in the [Unikraft Cloud example repository](https://github.com/unikraft-cloud/examples). + +## Prerequisites + +Make sure you have the CLI installed. +Use the [unikraft CLI](/docs/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). +You also need a container runtime such as Docker because the `Dockerfile` builds the root filesystem. + +Set your Unikraft Cloud credentials and preferred metro before deploying: + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +The `UKC_TOKEN` and `UKC_METRO` environment variables are only supported by the legacy CLI. + +## Use the upstream image + +The fastest way to get an existing container running on Unikraft Cloud is to keep its root filesystem as-is. +For that, BuildKit fetches the upstream image through a `FROM image:latest` line and the `Kraftfile` reproduces the original start command. + +### Inspect the upstream image + +Before writing the `Kraftfile`, inspect the original image and record its effective startup command. +In Docker terms, the final process arguments are usually `ENTRYPOINT` followed by `CMD`. + +```bash +docker pull image:latest +docker image inspect image:latest --format '{{json .Config.Entrypoint}}' +docker image inspect image:latest --format '{{json .Config.Cmd}}' +docker image inspect image:latest --format '{{json .Config.WorkingDir}}' +docker image inspect image:latest --format '{{json .Config.ExposedPorts}}' +``` + +Use the values you get as follows: + +* If `Entrypoint` is empty, use `Cmd` as the full `cmd` value in the `Kraftfile`. +* If both are present, concatenate them in the same order and use the result as the `cmd` value. +* If the image starts through a wrapper such as `docker-entrypoint.sh`, keep that wrapper in the command line. +* Prefer absolute paths in `cmd` so the deployment doesn't depend on a specific working directory. + +### Create the initial files + +Start with the smallest possible `Dockerfile`: + +```dockerfile title="Dockerfile" +FROM image:latest +``` + +Then create a matching `Kraftfile`: + +```yaml title="Kraftfile" +spec: v0.6 + +runtime: base-compat:latest + +rootfs: ./Dockerfile + +cmd: ["/path/from-entrypoint", "arg1", "arg2"] +``` + +In this first-pass `Kraftfile`: + +* `runtime: base-compat:latest` selects the generic compatibility runtime. +* `rootfs: ./Dockerfile` tells the CLI to build the root filesystem from the `Dockerfile`. +* `cmd: [...]` explicitly sets the process arguments that should run inside the instance. + +Replace the placeholder command with the exact values extracted from `docker image inspect`. +For example, if the image reports `Entrypoint=["/usr/local/bin/docker-entrypoint.sh"]` and `Cmd=["postgres"]`, then use `cmd: ["/usr/local/bin/docker-entrypoint.sh", "postgres"]`. + +:::caution +These Dockerfile keywords aren't deployment controls on Unikraft Cloud. +In particular, `EXPOSE`, `HEALTHCHECK`, `ONBUILD`, `SHELL`, `STOPSIGNAL`, and `VOLUME` are ignored as Docker deployment semantics. +Also note that instances run as `root` unless you explicitly switch users at runtime. +::: + +### Deploy the first pass + +Run an initial deployment before optimizing anything. +This confirms that the runtime, filesystem, and command line are valid. + + + +```bash title="unikraft" +unikraft build . --output /docker-port:latest +unikraft run --metro=fra -p 443: -m 512MiB --image=/docker-port:latest +``` + +```bash title="kraft" +kraft cloud deploy \ + -p 443: \ + -M 512Mi \ + . +``` + + + +Replace [``](/platform/services) with the app's listen port. +Take this value from the image documentation or from the `ExposedPorts` field you inspected earlier. + +If the instance fails to boot, inspect the logs and fix the command line first. +In most broken first attempts, the issue is either a wrong executable path, a missing wrapper script, or insufficient memory. + +:::note +If the upstream image depends on a specific `WORKDIR`, verify whether absolute paths are enough. +If not, add a small wrapper script that changes directory and then `exec`s the original command. +::: + +## Slim the root filesystem + +Once the first pass works, the next step is to stop shipping the entire upstream image. +The goal is to copy only the files needed at runtime into a minimal final stage. + +In practice, that often means removing: + +* package manager caches; +* compilers and build tools; +* test data and documentation; +* shells you no longer need; +* temporary build artifacts. + +### Find the files you actually need + +For native or mixed-language workloads, the difficult part is identifying the runtime dependencies that must stay in the image. +The exact list depends on the upstream image. + +Use the upstream container to inspect the binary and its dependencies: + +```bash +docker run --rm --entrypoint sh image:latest -lc ' + command -v your-app || true + ldd /path/to/executable || true +' +``` + +If the image doesn't contain a shell, use a temporary debug image or inspect the filesystem with `docker create` and `docker cp`. +The goal is the same in both cases: copy only what the final process needs at runtime. + +:::tip +Verify whether the upstream image is glibc-based or musl-based before copying the loader and shared libraries. +The sample paths above use musl-flavoured locations because that's a common pattern in the examples, but your image may differ. +::: + +### Strip binaries and libraries + +Once you know which files the app needs, the next step is to reduce the size of the native executables and shared libraries themselves. +This helps most with native apps and compiled dependencies. + +The common tool for this is `strip` from `binutils`. +It removes symbol and debug information that the app doesn't need at runtime. + +:::note +Use stripping only after the app already boots correctly. +::: + +Start with the main executable and the libraries you identified with `ldd`: + +```dockerfile title="Dockerfile" +FROM image:latest AS upstream + +RUN strip --strip-unneeded /path/to/executable && \ + strip --strip-unneeded /usr/lib/libexample.so.1 +``` + +If the upstream image doesn't include `strip`, install the required tooling only in the build stage. +Don't copy that tooling into the final `scratch` stage. + +For example, on Debian-based images: + +```dockerfile title="Dockerfile" +FROM image:latest AS upstream + +RUN apt-get update && \ + apt-get install -y --no-install-recommends binutils + +RUN strip --strip-unneeded /path/to/executable && \ + strip --strip-unneeded /usr/lib/libexample.so.1 +``` + +As a rule of thumb, strip these files only when they're part of your runtime path: + +* the main executable; +* helper executables started by wrapper scripts; +* shared libraries reported by `ldd`. + +:::warning +Don't strip files if you will need them for debugging later. +::: + +### Convert the Dockerfile to a multi-stage build + +Start from the upstream image in a build stage. +Then copy only the final executable, its direct shared libraries, and the app files into a `scratch` stage. + +```dockerfile title="Dockerfile" +FROM image:latest AS upstream + +# Add framework-specific build steps here if the image needs a compile step. +# Remove caches here if the upstream image leaves package manager data behind. + +FROM scratch + +# Copy the main executable or wrapper script used by the app. +COPY --from=upstream /path/to/executable /path/to/executable + +# Copy the dynamic loader and shared libraries required by the executable. +COPY --from=upstream /lib/ld-musl-x86_64.so.1 /lib/ld-musl-x86_64.so.1 +COPY --from=upstream /usr/lib/libgcc_s.so.1 /usr/lib/libgcc_s.so.1 +COPY --from=upstream /usr/lib/libstdc++.so.6 /usr/lib/libstdc++.so.6 + +# Distribution metadata is often useful for runtime compatibility checks. +COPY --from=upstream /etc/os-release /etc/os-release + +# Copy only the app files that are needed at runtime. +COPY --from=upstream /app /app +``` + +This is the same shape used by examples such as [`node21-nextjs`](https://github.com/unikraft-cloud/examples/blob/main/node21-nextjs/Dockerfile), where the final image keeps only the Node binary, its required libraries, `/etc/os-release`, and the built app output. + +## Recommended deployment settings for the slim image + +After reducing the filesystem, deploy it with [`EROFS`](/tutorials/rootfs-formats) when possible. +This often gives better cold-boot behaviour and lower memory pressure than `CPIO` for larger images. + + + +```bash title="unikraft" +unikraft build . --output /docker-port:latest +unikraft run --metro=fra -p 443: -m 256MiB --image=/docker-port:latest +``` + +```bash title="kraft" +kraft cloud deploy \ + -p 443: \ + -M 256Mi \ + --rootfs-type erofs \ + . +``` + + + +Treat the memory value as a starting point. +Tune it after the first successful deployment by looking at the actual boot behaviour and logs. + +## Bare minimum checklist + +Before calling the image minimized, check the following: + +* the `cmd` line uses the final executable path; +* copy only runtime files into the final stage; +* no package manager cache or compiler output remains; +* expose the instance port through the CLI `-p` flag; +* use [`EROFS`](/tutorials/rootfs-formats) unless you have a reason to stay on `CPIO`. + +## More configurations + +### Environment variables + +If the upstream image depends on environment variables, you can optionally carry them over explicitly. +The most reliable method at deployment time is to pass them through the CLI with `--env` or `-e`. + + + +```bash title="unikraft" +unikraft build . --output /docker-port:latest +unikraft run --metro=fra -p 443: -m 256MiB \ + -e APP_ENV=production \ + -e LOG_LEVEL=info \ + --image=/docker-port:latest +``` + +```bash title="kraft" +kraft cloud deploy \ + -p 443: \ + -M 256Mi \ + --rootfs-type erofs \ + -e APP_ENV=production \ + -e LOG_LEVEL=info \ + . +``` + + + +If the image expects a wrapper entrypoint that prepares environment variables before starting the main process, keep that wrapper in your `cmd` line or replace it with your own script. +The [Environment Variables](/tutorials/environment-variables) tutorial describes the wrapper pattern in more detail. + +:::note +Dockerfile `ENV` instructions carry over into the image, but you might want to set them explicitly at deployment time for better visibility and control. +::: + +### Users and permissions + +Container images often include a `USER` directive to avoid running as `root`. +On Unikraft Cloud, that directive only affects subsequent `RUN` steps while building the root filesystem. +It doesn't automatically change the user that runs `ENTRYPOINT` or `CMD` at instance boot. + +In practice, this means the deployed process runs as `root` unless you switch users explicitly inside your startup command or wrapper script. + +When slimming the image, check permission-sensitive paths such as: + +* runtime directories under `/var` or `/run`; +* app state directories under `/app`, `/srv`, or `/data`; +* Unix sockets or process ID files created by the upstream entrypoint. + +If the original image relied on a specific unprivileged user, you may need to preserve passwd and group metadata files as well: + +```dockerfile +COPY --from=upstream /etc/passwd /etc/passwd +COPY --from=upstream /etc/group /etc/group +``` + +If the app still assumes a particular user context, add a small wrapper that prepares the filesystem and then starts the original process. + +### Scale to zero + +After the image boots correctly and the slimmed filesystem works, scale-to-zero is a good final validation step. +It tests whether the app can suspend cleanly and resume on the next request. + +The simplest way to test scale-to-zero is to deploy with an explicit scale-to-zero policy and a short cooldown: + + + +```bash title="unikraft" +unikraft build . --output /docker-port:latest +unikraft run --metro=fra -p 443: -m 256MiB \ + --scale-to-zero \ + --scale-to-zero-cooldown-time-ms=5000 \ + --image=/docker-port:latest +``` + +```bash title="kraft" +kraft cloud deploy \ + -p 443: \ + -M 256Mi \ + --rootfs-type erofs \ + --scale-to-zero on \ + --scale-to-zero-stateful \ + --scale-to-zero-cooldown 5s \ + . +``` + + + +After deployment, wait a few seconds and then list the instances: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + +If everything is working, the instance should transition to `standby` when idle and wake up again on the next request. + +Apps with long initialization phases, background work, or long-lived connections may need extra adjustments before scale-to-zero behaves well. +In those cases, review the [Scale-to-Zero](/features/scale-to-zero) feature page and consider a longer cooldown, stateful mode, or temporarily disabling scale-to-zero from inside the app during startup. + +## Learn more + +* [Images](/platform/images) and how `Dockerfile`s, `Kraftfile`s, and runtimes fit together. +* [Rootfs Formats](/tutorials/rootfs-formats) for understanding the `CPIO` and `EROFS` tradeoffs. +* The [Kraftfile reference](https://unikraft.org/docs/cli/reference/kraftfile) for all supported top-level fields. +* The [Next.js guide](/guides/nextjs) as a concrete example of a multi-stage Docker build trimmed down for deployment. diff --git a/pages/tutorials/environment-variables.mdx b/pages/tutorials/environment-variables.mdx new file mode 100644 index 00000000..bdefc26d --- /dev/null +++ b/pages/tutorials/environment-variables.mdx @@ -0,0 +1,130 @@ +--- +title: Environment Variables +navigation_icon: cog +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +Environment variables are a common way to configure applications at runtime. +Unikraft Cloud supports passing environment variables to instances during deployment through many methods. +This tutorial discusses each below and orders them by superseeding precedence (from most powerful to weakest). + +## Setting environment variables + +Below are all the ways you can set environment variables for your Unikraft Cloud instances. +The order they're presented is from the least precedence to the highest precedence. + +### Dockerfile ENV + +:::caution +At the moment environment variables from the `Dockerfile` are passed in the image but not read by the Unikraft Cloud platform. +Please use the other methods described below in the meantime. +::: + +You can set environment variables in your `Dockerfile` using the `ENV` instruction in your target `FROM` stage. +The CLI automatically reads them when building the image and sets them in the resulting packaged image. + +```dockerfile +# ... +FROM ubuntu:latest +ENV MY_ENV_VAR=my_value +# ... +``` + +### Kraftfile env + +:::caution +At the moment environment variables from the `Kraftfile` are passed in the image but not read by the Unikraft Cloud platform. +Nonetheless, the legacy deploy command can still read them and set them in the instance. +For other use cases, please use the other methods described below in the meantime. +::: + +You can set environment variables in your `Kraftfile` using the `env` field. +`kraft` automatically reads them when building the image and sets them in the resulting packaged image. + +```yaml +# ... +env: + MY_ENV_VAR: my_value +``` + +:::tip +This method takes precedence over the `Dockerfile`. +It will override environment variables with the same name set in the `Dockerfile`. +::: + +### Kraft flags + +When deploying an instance, you can pass environment variables using the `--env` (or `-e`) flag. +You can specify many environment variables by using the flag many times. +The flag works similarly to the `docker run -e` flag. + + + +```bash title="unikraft" +unikraft run --metro=fra -e MY_ENV_VAR=my_value --image=my-app:latest +``` + +```bash title="kraft" +kraft cloud deploy my-app --env MY_ENV_VAR=my_value +``` + + + +:::tip +This method takes precedence over the `Dockerfile` and `Kraftfile`. +It will override environment variables with the same name set in the previous two methods. +::: + +### Wrapper script + +Create a wrapper script that sets the environment variables before executing the main app. +Check what the original entrypoint of your app is and call it after setting the variables. +For example, if the original entrypoint is `/entrypoint.sh`, you can create a new script `wrapper.sh` like this: + +```bash +#!/bin/sh + +export MY_ENV_VAR="my_value" + +exec /entrypoint.sh "$@" +``` + +Then you change either your `Dockerfile` or `Kraftfile` to use this wrapper script as the entrypoint: + + + + Dockerfile + Kraftfile + + + ```Dockerfile + # ... + COPY wrapper.sh /wrapper.sh + RUN chmod +x /wrapper.sh + ENTRYPOINT ["/wrapper.sh"] + ``` + + + ```yaml + # ... + cmd: "/wrapper.sh" + ``` + + + +:::tip +This method has the highest precedence and will override environment variables set in the cli, `Dockerfile`, or `Kraftfile` as it hardcodes them in the script. +Thus it's recommended to do this only for variables that you set and don't change. +::: + +## Conclusion + +Out of the four ways to set environment variables in Unikraft Cloud instances you should pick the ones that work best for you. +Consider that the `Dockerfile` and `Kraftfile` can set variables for the image itself, while the CLI and wrapper script set them at runtime. + +## Learn more + +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* The [Dockerfile reference](https://docs.docker.com/reference/dockerfile/) for understanding how to set environment variables in Dockerfiles. +* The [Kraftfile reference](https://unikraft.org/docs/cli/reference/kraftfile) for understanding how to set environment variables in Kraftfiles. diff --git a/pages/tutorials/instance-metrics.mdx b/pages/tutorials/instance-metrics.mdx new file mode 100644 index 00000000..f5537372 --- /dev/null +++ b/pages/tutorials/instance-metrics.mdx @@ -0,0 +1,412 @@ +--- +title: Instance Metrics +navigation_icon: bar-chart +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +Unikraft Cloud provides an endpoint to retrieve real-time hardware and network metrics for your running instances. +These metrics are useful for monitoring the performance, memory usage, and network traffic handled by your app. + +## Prerequisites + +To access the instance metrics, you must have the **`developer`** permission role for your user. + +## Retrieving metrics + +You can retrieve the metrics of one or more instances by making a `GET` request to the `/instances/metrics` endpoint. +The request body must contain an array of instance UUIDs or names. +Use a tool like `curl` for ad-hoc queries, or configure your monitoring system to consume the endpoint's Prometheus-formatted output. + +### Example + +The following example creates an instance and retrieves its metrics using `curl`. + +First, provision a new instance: + + + +```bash title="unikraft" +git clone https://github.com/unikraft-cloud/examples +cd examples/nginx/ +unikraft run --metro=fra -p 443:8080/http+tls -m 256MiB --image=nginx:latest +``` + +```bash title="kraft" +git clone https://github.com/unikraft-cloud/examples +cd examples/nginx/ +kraft cloud deploy -p 443:8080 -M 256 . +``` + + + +This command will create the NGINX instance with scale-to-zero enabled: + +```ansi title="" +[●] Deployed successfully! + │ + ├────────── name: nginx-26g86 + ├────────── uuid: 3605978e-5feb-4209-8f9e-de45f00a7d66 + ├───────── state: running + ├─────────── url: https://black-snowflake-iy7509ap.fra.unikraft.app + ├───────── image: nginx@sha256:19854a12fe97f138313cb9b4806828cae9cecf2d050077a0268d98129863f954 + ├───── boot time: 7.77 ms + ├──────── memory: 256 MiB + ├─────── service: black-snowflake-iy7509ap + ├── private fqdn: nginx-26g86.internal + ├──── private ip: 172.16.6.1 + └────────── args: /usr/bin/nginx -c /etc/nginx/nginx.conf +``` + +Now, request the metrics for that instance. +Set `UKC_TOKEN` and `UKC_METRO` for the API calls: + + + + JSON + Prometheus + + +```bash title="" +curl -X GET "https://api.$UKC_METRO.unikraft.cloud/v1/instances/metrics" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -H "Accept: application/json" \ + -d '[{"name":"nginx-26g86"}]' +``` + + +```bash title="" +curl -X GET "https://api.$UKC_METRO.unikraft.cloud/v1/instances/metrics" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -d '[{"name":"nginx-26g86"}]' +``` + + + + + + JSON + Prometheus + + +```json +{ + "status": "success", + "data": { + "instances": [ + { + "status": "success", + "uuid": "3605978e-5feb-4209-8f9e-de45f00a7d66", + "name": "nginx-26g86", + "state": "standby", + "start_count": 2, + "started_at": "2026-03-18T12:31:49Z", + "stopped_at": "2026-03-18T12:31:50Z", + "uptime_s": 7.817000, + "boot_time_s": 0.005900, + "net_time_s": 0.025256, + "rss_bytes": 0, + "cpu_time_s": 0, + "rx_bytes": 1614, + "rx_packets": 10, + "tx_bytes": 537, + "tx_packets": 5, + "nconns": 0, + "nreqs": 0, + "nqueued": 0, + "ntotal": 1, + "wakeup_latency_seconds": [ + { + "bucket_s": 0.001000, + "count": 0 + }, + { + "bucket_s": 0.002000, + "count": 0 + }, + { + "bucket_s": 0.004000, + "count": 0 + }, + { + "bucket_s": 0.008000, + "count": 0 + }, + { + "bucket_s": 0.016000, + "count": 0 + }, + { + "bucket_s": 0.032000, + "count": 1 + }, + { + "bucket_s": 0.064000, + "count": 0 + }, + { + "bucket_s": 0.128000, + "count": 0 + }, + { + "bucket_s": 0.256000, + "count": 0 + }, + { + "bucket_s": 0.512000, + "count": 0 + }, + { + "bucket_s": 1.024000, + "count": 0 + }, + { + "bucket_s": 2.048000, + "count": 0 + }, + { + "bucket_s": 4.096000, + "count": 0 + }, + { + "bucket_s": null, + "count": 0 + } + ], + "wakeup_latency_seconds_sum": 0.023 + } + ] + }, + "op_time_us": 125 +} +``` + + +```prom +# HELP instance_state 0=stopped,1=starting,2=running,3=draining,4=stopping,5=standby,6=template +# TYPE instance_state gauge +instance_state{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 5 + +# HELP instance_start_count Number of times the instance has been started +# TYPE instance_start_count counter +instance_start_count{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 2 + +# HELP instance_restart_count Number of times the instance has been restarted +# TYPE instance_restart_count counter +instance_restart_count{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 0 + +# HELP instance_started_at Time when the instance started +# TYPE instance_started_at gauge +instance_started_at{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 1773840567 + +# HELP instance_stopped_at Time when the instance stopped +# TYPE instance_stopped_at gauge +instance_stopped_at{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 1773840567 + +# HELP instance_uptime_s Uptime in seconds +# TYPE instance_uptime_s gauge +instance_uptime_s{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 7.817000 + +# HELP instance_boot_time_s Boot time in seconds +# TYPE instance_boot_time_s gauge +instance_boot_time_s{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 0.005900 + +# HELP instance_net_time_s Net time in seconds +# TYPE instance_net_time_s gauge +instance_net_time_s{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 0.025256 + +# HELP instance_rss_bytes Resident set size in bytes +# TYPE instance_rss_bytes gauge +instance_rss_bytes{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 0 + +# HELP instance_cpu_time_s Consumed CPU time in seconds +# TYPE instance_cpu_time_s counter +instance_cpu_time_s{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 0.000000 + +# HELP instance_rx_bytes Amount of bytes received over network +# TYPE instance_rx_bytes counter +instance_rx_bytes{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 1614 + +# HELP instance_tx_bytes Amount of bytes transmitted over network +# TYPE instance_tx_bytes counter +instance_tx_bytes{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 543 + +# HELP instance_rx_packets Count of packets received from network +# TYPE instance_rx_packets counter +instance_rx_packets{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 10 + +# HELP instance_tx_packets Count of packets transmitted over network +# TYPE instance_tx_packets counter +instance_tx_packets{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 5 + +# HELP instance_nconns Number of active connections +# TYPE instance_nconns gauge +instance_nconns{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 0 + +# HELP instance_nreqs Number of active requests +# TYPE instance_nreqs gauge +instance_nreqs{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 0 + +# HELP instance_nqueued Number of queued connections/requests +# TYPE instance_nqueued gauge +instance_nqueued{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 0 + +# HELP instance_ntotal Number of processed connections/requests +# TYPE instance_ntotal counter +instance_ntotal{instance_uuid="3605978e-5feb-4209-8f9e-de45f00a7d66"} 1 + +# HELP instance_wakeup_latency_seconds Wakeup latencies in seconds +# TYPE instance_wakeup_latency_seconds histogram +instance_wakeup_latency_seconds{le="0.001000"} 0 +instance_wakeup_latency_seconds{le="0.002000"} 0 +instance_wakeup_latency_seconds{le="0.004000"} 0 +instance_wakeup_latency_seconds{le="0.008000"} 0 +instance_wakeup_latency_seconds{le="0.016000"} 0 +instance_wakeup_latency_seconds{le="0.032000"} 1 +instance_wakeup_latency_seconds{le="0.064000"} 1 +instance_wakeup_latency_seconds{le="0.128000"} 1 +instance_wakeup_latency_seconds{le="0.256000"} 1 +instance_wakeup_latency_seconds{le="0.512000"} 1 +instance_wakeup_latency_seconds{le="1.024000"} 1 +instance_wakeup_latency_seconds{le="2.048000"} 1 +instance_wakeup_latency_seconds{le="4.096000"} 1 +instance_wakeup_latency_seconds{le="+Inf"} 1 +instance_wakeup_latency_seconds_sum 23 +instance_wakeup_latency_seconds_count 1 +``` + + + +If the request is successful, you will receive a response reporting the instance's performance and network data. + +## Understanding the Response + +The metrics response contains fields for CPU, memory, boot time, and networking. +The Prometheus output comments contain details about each metric. +Below is a detailed breakdown of each field returned in the metrics object. + +### Instance Info + +#### **`uuid`** + +The UUID of the instance. + +#### **`name`** + +The name of the instance. + +#### **`state`** + +The current state of the instance. +Possible values: `stopped`, `starting`, `running`, `draining`, `stopping`, `standby`, `template`. + +### Lifecycle and uptime + +#### **`start_count`** + +Number of times the instance started, including scale-to-zero wakeups. + +#### **`started_at`** + +Timestamp of the most recent instance start, in [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) format. + +#### **`stopped_at`** + +Timestamp of the most recent instance stop, in [RFC 3339](https://www.rfc-editor.org/rfc/rfc3339) format. + +#### **`uptime_s`** + +Total accumulated uptime of the instance in seconds across all starts. + +### Memory & CPU + +#### **`rss_bytes`** + +Resident set size in bytes. +This is the amount of physical memory that the instance has touched and is currently reserved for it. +It grows as the instance uses more memory up to the configured limit. +This metric drops to `0` when the instance is in standby. + +#### **`cpu_time_s`** + +Consumed CPU time in seconds for the last second. +This metric drops to `0` when the instance is in standby. + +### Boot and network initialization times + +#### **`boot_time_s`** + +Boot time in seconds. +Calculated as the time between the virtualization toolstack responding to a boot request and the moment the guest OS starts executing user code. + +#### **`net_time_s`** + +Network initialization time in seconds. +This is the time from when the instance started until the user-level app starts listening on a non-localhost port. + +### Network traffic + +#### **`rx_bytes`** + +Total amount of network bytes received. + +#### **`rx_packets`** + +Total count of network packets received. + +#### **`tx_bytes`** + +Total amount of network bytes transmitted. + +#### **`tx_packets`** + +Total count of network packets transmitted. + +### Connections & Requests + +#### **`nconns`** + +Number of currently established inbound connections (non-HTTP). +This metric drops to `0` when the instance is in standby. + +#### **`nreqs`** + +Number of in-flight HTTP requests. +This metric drops to `0` when the instance is in standby. + +#### **`nqueued`** + +Number of queued inbound connections and HTTP requests. +This metric drops to `0` when the instance is in standby. + +#### **`ntotal`** + +Total number of inbound connections and HTTP requests handled. + +### Wakeup latency + +#### **`wakeup_latency_seconds`** + +A histogram of scale-to-zero wakeup latencies. +Each entry contains a `bucket_s` threshold (in seconds) and the `count` of wakeups that fell within that bucket. +The final bucket has `bucket_s: null`, representing the `+Inf` overflow bucket for wakeups exceeding all defined thresholds. + +#### **`wakeup_latency_seconds_sum`** + +The sum of all wakeup latencies in seconds. +Together with the histogram buckets this allows computing a mean wakeup latency: `wakeup_latency_seconds_sum / ntotal`. + +## Conclusion + +Instance metrics give you a real-time view into the performance and health of your Unikraft Cloud instances. +By monitoring memory usage, CPU time, boot and network initialization times, and connection statistics, you can understand how your app behaves under load. +The Prometheus format lets you plug these metrics directly into monitoring tools such as Grafana or any other Prometheus-compatible system. + +## Learn more + +* [Scale to zero](/features/scale-to-zero) and how it affects instance lifecycle. +* [Scale to zero triggers](/tutorials/scale-to-zero-triggers) for controlling when instances wake up. +* [Platform instances](/platform/instances) for managing and inspecting your instances. +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). diff --git a/pages/tutorials/network-communication.mdx b/pages/tutorials/network-communication.mdx new file mode 100644 index 00000000..a4c28f67 --- /dev/null +++ b/pages/tutorials/network-communication.mdx @@ -0,0 +1,55 @@ +--- +title: Network Communication +navigation_icon: network +--- + +:::caution +As of writing this, [scale-to-zero triggers](/tutorials/scale-to-zero-triggers) are unavailable on internal networking and, as such you must use external networking. +Moreover, there is no load balancing on internal networks, this being up to the user. +::: + +## Communication types + +The platform separates Unikraft Cloud instances on a per-user level. +This means that all instances part of the scope of a user can "see" each other in the private internal network and they can communicate with each other. +This communication is private and unencrypted, and doesn't reach the outside world. +You can communicate internally by using the Private IP or Private FQDN. + +At the same time, when using the Public FQDN the connection goes through the external load balancer applied to every public instance. +This means that an entirely different code path resolves the connection, and also the connection is always TLS encrypted. + +### External communication + +External communication refers to every request going over the public FQDN of an instance. +This means that the request goes through the external load balancer, and as such TLS encryption is mandatory. +This also means that the instance will be load balanced with all other public instances behind the same [service](/features/load-balancing). +The request will do a round trip from the client through the load balancer and to the instance. +This communication is best suited for frontend-to-service communication, public APIs, web services, and more. + +### Internal communication + +Internal communication refers to every request going over the private network of a user. +Every instance, by default, receives a private IP and a private FQDN (of the format `.internal`). +This means that the request stays within the user internal network, and doesn't go through the external load balancer. +This also means that the system won't encrypt the connection by default. +This communication is best suited for service-to-service communication, microservices, databases, caches, and more. +For example, you wouldn't want to expose your database publicly, but you would want your app to be able to connect to it internally. + +:::note +When using the legacy CLI tunnel command to connect to your instances you are using the internal network to reach your instance. +The command creates a public-facing instance that tunnels your requests to the private instance over the internal network. +This is a good way to keep your instances private while still being able to access them from your local machine. +::: + +## Conclusion + +When building your app architecture on Unikraft Cloud, it's important to understand the differences between internal and external communication. +External communication is best suited for public-facing services, while internal communication is ideal for private interactions between services within your app. +By leveraging both types of communication, you can deploy a robust and efficient app architecture on Unikraft Cloud. + +## Learn more + +* [Load balancing](/features/load-balancing) of services. +* [Platform services](/platform/services) and how they work. +* [Scale to zero triggers](/tutorials/scale-to-zero-triggers) for automatic instance scaling. +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). diff --git a/pages/tutorials/rootfs-compression.mdx b/pages/tutorials/rootfs-compression.mdx new file mode 100644 index 00000000..c008bb25 --- /dev/null +++ b/pages/tutorials/rootfs-compression.mdx @@ -0,0 +1,186 @@ +--- +title: Rootfs Compression +navigation_icon: file-archive +--- + +Compressing the root file system represents using an algorithm like `gzip`, `zstd`, etc. to reduce the size of the rootfs on disk. +This has the added benefit of reducing the time it takes to push the image to the Unikraft Cloud platform, as you transfer less data. +It also means that the size on disk on the host side reduces, which can be beneficial when dealing with many images or large images. +This size can drop depending on the contents of the rootfs and the compression algorithm used. +For example, text files often compress by up to 90% and more, whilst already compressed files like images or videos don't compress well at all. + +## Getting started + +The Unikraft Platform supports both compressed and uncompressed rootfses, but it's highly recommended to use uncompressed ones if size allows. +Decompressing is a step done by the kernel at boot time, resulting in slower access times across the board. +That's why the first attempt at using the platform should always be with uncompressed rootfses. + +Both formats, [`CPIO` and `EROFS`](/tutorials/rootfs-formats), support compression. +`CPIO` does full file system compression (like running `zip` on the file). +`EROFS` does per-block compression meaning that the system decompresses only the accessed blocks on-the-fly. +Both of them support most compression algorithms, but the recommended ones are those that have fast decompression speeds like `lz4hc`. + +As presented in the [rootfs formats tutorial](/tutorials/rootfs-formats) and in the [FAQ](/faq), the performance characteristics of the two formats differ. +When using compression, the system amplifies these differences further. +For `CPIO`, the system must decompress the entire archive first before using it. +This means that boot times increase further, and memory usage during boot is also higher negating the benefit of using compression in the first place. +`EROFS` is better, as the system decompresses only the accessed blocks on-the-fly, meaning that boot times are less affected, and memory usage is also kept lower. +This has the added downside that Direct Access (`DAX`) isn't possible when using compression, as the host side can't map a compressed image into guest memory. +This caveat results in having to load the entire compressed rootfs into guest memory, increasing memory usage by at least the size of the compressed rootfs. +Still, if many files aren't accessed in boot, the cold boot time might decrease compared to an uncompressed `EROFS` rootfs. + +## Packaging a Compressed rootfs + +Packaging a compressed rootfs is different depending on the format used. +The legacy [`kraft` CLI](https://unikraft.org/docs/cli/install) implements `CPIO` compression directly, while `EROFS` compression must happen externally using the `mkfs.erofs` tool. + +### CPIO Compression + +To package a compressed `CPIO` rootfs using the legacy CLI, you can use the `--compress` flag when running the `kraft pkg` or `kraft cloud deploy` command. +This uses `gzip` compression to compress the initrd file before packaging it into a `OCI` image and uploading it to the Unikraft Cloud platform. + + + +```bash title="kraft" +kraft cloud deploy \ + --rootfs-type cpio \ + --compress \ + ... +``` + + + +### EROFS Compression + +Currently, the legacy CLI has no direct support for compressing `EROFS` rootfses. +To package a compressed `EROFS` rootfs, you need to use the `mkfs.erofs` tool before packaging it with `kraft`. + +To do this you will also have to first export the image built from the `Dockerfile` into a `tar` archive: + +```bash +docker build -o type=tar,dest=rootfs -t my-docker-image -f Dockerfile . +``` + +You can then use the resulting `tar` archive to create a compressed `EROFS` rootfs using the following command. +Note that you can change the compression algorithm and level by modifying the `-z` option. +In this example the level is `9` (maximum) and the algorithm is `lz4hc` (high compression lz4): + +```bash +mkfs.erofs --all-root -b 4096 -d2 -E noinline_data -z lz4hc,9 rootfs.erofs --tar=rootfs rootfs.tar +``` + +Finally, you can package and deploy the compressed EROFS rootfs using the legacy CLI: + + + +```bash title="kraft" +kraft cloud deploy \ + --rootfs-type erofs \ + --rootfs ./rootfs.erofs \ + ... +``` + + +## Performance of Compressed rootfs vs. Uncompressed rootfs Images + +To illustrate the performance differences between compressed and uncompressed rootfses, below you can see the same [`node-22` image](https://github.com/unikraft-cloud/examples/tree/main/node-playwright-chromium) packaged and deployed four times: + +1. Uncompressed `CPIO` + ```ansi title="Uncompressed CPIO" + [●] Deployed successfully! + │ + ├─────── name: node-playwright-chromium-gjv31 + ├─────── uuid: 7ef181a3-8264-4418-82e9-4690ed75a030 + ├────── metro: fra + ├────── state: running + ├───── domain: https://dark-feather-7drf2jvp.fra.unikraft.app + ├────── image: demo/node-playwright-chromium@sha256:eafb34e104efe139234f5aa753f3d380afea2c5b78a3011ccd2017a25a5172c0 + ├── boot time: 621.28 ms + ├───── memory: 2400 MiB + ├──── service: dark-feather-7drf2jvp + ├─ private ip: 10.0.1.233 + └─────── args: /usr/bin/wrapper.sh /usr/bin/node /app/server.js + ``` + +1. Compressed `CPIO` + + ```ansi title="Compressed CPIO" + [●] Deployed successfully! + │ + ├─────── name: node-playwright-chromium-bt6r9 + ├─────── uuid: 1ebc4ee0-2fb1-4272-81d5-7adc76bfb566 + ├────── metro: fra + ├────── state: running + ├───── domain: https://old-cloud-em09h6ue.fra.unikraft.app + ├────── image: demo/node-playwright-chromium@sha256:ac6cde1c766162a3c1bc376985c2442eafacd5b18b26b599a25d3095c0a5f5ed + ├── boot time: 2079.74 ms + ├───── memory: 2400 MiB + ├──── service: old-cloud-em09h6ue + ├─ private ip: 10.0.1.233 + └─────── args: /usr/bin/wrapper.sh /usr/bin/node /app/server.js + ``` + +1. Uncompressed `EROFS` + + ```ansi title="Uncompressed EROFS" + [●] Deployed successfully! + │ + ├─────── name: node-playwright-chromium-o4ds4 + ├─────── uuid: 967b66fd-f71c-4b2b-8982-88e4c0373d02 + ├────── metro: fra + ├────── state: running + ├───── domain: https://damp-water-8ubacykw.fra.unikraft.app + ├────── image: cezar.unikraft.io/node-playwright-chromium@sha256:65bf231a557a0dd938592d0fd67f4d5f5e83c0df1ca27b8f8d8675253138ae4b + ├── boot time: 121.75 ms + ├───── memory: 900 MiB + ├──── service: damp-water-8ubacykw + ├─ private ip: 10.0.4.1 + └─────── args: /usr/bin/wrapper.sh /usr/bin/node /app/server.js + ``` + +1. Compressed `EROFS` + + ```ansi title="Compressed EROFS" + [●] Deployed successfully! + │ + ├────── name: node-playwright-chromium-9yb9y + ├────── uuid: d7255b2c-394c-4578-a21b-2f879f823cd9 + ├───── metro: fra + ├───── state: running + ├──── domain: https://damp-dream-rpmyh0h0.fra.unikraft.app + ├───── image: cezar.unikraft.io/node-playwright-chromium@sha256:7a93fbfa414eb958cd4f61b9cbe4874eb4fe6836e74fb292bca3e2927df71a88 + ├─ boot time: 92.36 ms + ├──── memory: 900 MiB + ├─── service: damp-dream-rpmyh0h0 + ├ private ip: 10.0.3.253 + └────── args: /usr/bin/wrapper.sh /usr/bin/node /app/server.js + ``` + +### Results + +| rootfs Type | Compression | Image Size | Boot Time | Instance Memory Usage | +|---------------|-------------|------------|------------|-----------------------| +| CPIO | No | 824 MiB | 621.28 ms | 2400 MiB | +| CPIO | Yes (gzip) | 299 MiB | 2079.74 ms | 2400 MiB | +| EROFS | No | 830 MiB | 121.75 ms | 900 MiB | +| EROFS | Yes (lz4hc) | 410 MiB | 92.36 ms | 900 MiB | + +As expected from the previous assumptions, the compressed CPIO rootfs boots slower than the uncompressed one. +This happens due to the need to decompress the entire archive before use. +EROFS performs better in both compressed and uncompressed forms, compared to CPIO. +The compressed EROFS rootfs boots even faster than the uncompressed one. +This is likely due to the reduced amount of data that the system reads from disk, resulting in faster access times despite the decompression overhead. +This only happens in the cold boot scenario. +In [warm boots](/features/snapshots), the uncompressed EROFS rootfs tops the charts as the system needs no decompression at all on demand. + +## Conclusion + +As a rule of thumb, use uncompressed rootfses whenever possible and try to always use `EROFS` over `CPIO` for better performance. +If limited by size constraints, try using compressed `EROFS` rootfses with a fast decompression algorithm like `lz4hc`. + +## Learn more + +* The [rootfs formats tutorial](/tutorials/rootfs-formats) for understanding the differences between `CPIO` and `EROFS`. +* The [`mkfs.erofs` documentation](https://man.archlinux.org/man/extra/erofs-utils/mkfs.erofs.1.en) for more information on creating compressed `EROFS` file systems. +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* The `kraft pkg` [command reference](https://unikraft.org/docs/cli/reference/kraft/pkg) for packaging images. diff --git a/pages/tutorials/rootfs-formats.mdx b/pages/tutorials/rootfs-formats.mdx new file mode 100644 index 00000000..2b51ac47 --- /dev/null +++ b/pages/tutorials/rootfs-formats.mdx @@ -0,0 +1,136 @@ +--- +title: Rootfs Formats +navigation_icon: files +--- + +Every virtual machine consists of at least two components: a kernel, and a root file system. +This tutorial focuses on the latter, the **rootfs** (also known as initrd). + +Currently, on the Unikraft Cloud platform, two types of file system archives are usable: `CPIO` (Copy In, Copy Out) and `EROFS` (Enhanced Read-Only File System). +They're packaging formats used for loading the root file system in the instance. +They consist mainly of metadata and file content without any advanced formatting to keep them as simple as possible. +The kernel loads and mounts the data from them at boot time. + +## Getting started + +The main difference between the rootfs formats is performance-wise. + +### CPIO + +`CPIO` is a simple format which translates in the need to copy and unpack the entire archive into guest memory when starting the instance. +This incurs a double memory usage during boot time, as the archive is first loaded into memory, then unpacked into a ramfs for the kernel to load. +At runtime, the guest can reclaim the initial memory allocated for the archive, and will only use the unpacked ramfs. +The boot times will still proportionally increase with the size of the `CPIO` archive. +Another consequence of using a `CPIO` initrd is that [snapshots](/features/snapshots) must include them, since Unikraft Cloud can't track where they're stored in guest memory. + +### EROFS + +`EROFS` wins on startup efficiency by not needing to unpack the archive at all into guest memory. +This means that you don't need to accomodate for double the memory usage during boot time, and boot times are faster as well. + +Another significant benefit of `EROFS` (although not available yet on public metros) is that Unikraft Cloud can enable `DAX` (Direct Access) on instances using `EROFS` rootfs. +This means that the host side loads the initrd, and the guest gets a map (**not a copy**) of it into its memory. +Thus, boot times aren't affected by the size of the rootfs at all. +Memory usage is also drastically reduced at boot, and only increases as the guest accesses files. +This also means that snapshots can exclude the original initrd. +Writing to the rootfs is still possible, because Unikraft Cloud uses **overlayfs** to mount the ramfs. +Any file changes are present in the snapshot. +Finally, `DAX` enables sharing the same rootfs image between many instances, increasing scalability. + +## Packaging the Rootfs + +The legacy `kraft` CLI supports both formats out of the box. +With it, you can package a rootfs from these sources: +- a simple file +- a directory +- a Dockerfile +- a remote OCI image + +If provided as a file, the CLI tool passes the file as-is, without any extra packaging. +Otherwise, you need to package the rootfs into a supported format (either `CPIO` or `EROFS`). +Already packaged archives with external tools (for example `mkfs.erofs`, `cpio`, `bsdcpio`) are also usable. +Right now the default format for the CLI tool is `CPIO`, with `EROFS` being an alternative, as the former is simpler, widely adoped, and better tested. +It's worth mentioning that after unpacking, there is no difference in the produced file contents between the formats. +They may only differ metadata-wise, depending on the implementation. +You may observe that a `CPIO` archive is typically smaller on disk than an `EROFS` one. +The only effect this has is on the time it takes to push the image. + +Below are two attempts at creating and starting an instance from the same [image](https://github.com/unikraft-cloud/examples/tree/main/node-playwright-chromium), packaged using both formats. + +### EROFS deployment + + + +```bash title="kraft" +kraft cloud deploy \ + -p 443:8080 \ + -M 900Mi \ + --rootfs-type erofs \ + --keep-file-owners \ + demo/node-playwright-chromium +``` + + + +```ansi title="Create Instance EROFS" +[●] Deployed successfully! + │ + ├────────── name: node-playwright-chromium-q9ynl + ├────────── uuid: cb7a1770-6d6a-4f53-a98a-f021ae796313 + ├───────── metro: fra + ├───────── state: running + ├──────── domain: https://proud-dream-udqhbav3.fra.unikraft.app + ├───────── image: demo/node-playwright-chromium@sha256:056d5daa3e0f8a91ac18930a9a7714203515a6fae1244b1ecea831f64f593e79 + ├───── boot time: 191.55 ms + ├──────── memory: 900 MiB + ├─────── service: proud-dream-udqhbav3 + ├──── private ip: 10.0.3.133 + └────────── args: /usr/bin/wrapper.sh /usr/bin/node /app/server.js +``` + +### CPIO deployment + + + +```bash title="kraft" +kraft cloud deploy \ + -p 443:8080 \ + -M 900Mi \ + --rootfs-type cpio \ + demo/node-playwright-chromium +``` + + + +```ansi title="Create Instance CPIO" +[●] Deployed successfully! + │ + ├────────── name: node-playwright-chromium-4rrxr + ├────────── uuid: aeff2d28-d718-4068-9e24-4e2bb203022c + ├───────── metro: fra + ├───────── state: running + ├──────── domain: https://dry-fire-vhmh9z8u.fra.unikraft.app + ├───────── image: demo/node-playwright-chromium@sha256:57d1773398bc6d4ff94f063d6e9b7400dca66171d04ce78cd8a45789ba0a7d93 + ├───── boot time: 607.34 ms + ├──────── memory: 2400 MiB + ├─────── service: dry-fire-vhmh9z8u + ├──── private ip: 10.0.3.133 + └────────── args: /usr/bin/wrapper.sh /usr/bin/node /app/server.js +``` + +The packaged archives have a size of around 750MiB each. +The `EROFS`-packaged instance requires at least 900MiB to work, and the `CPIO` one requires 2400MiB. +Simple arithmetic backs this. +In the `CPIO` case, the kernel gets a copy of the archive in memory and needs to remove it into ramfs. +This extraction process requires double the memory of the archive size. +This means the instance needs at least an extra 750x2=1500MiB of memory compared to `EROFS` to boot. +Finally, related to boot times, both cases have similar [warm boot](/features/snapshots) times, but the extra copies mentioned increase the cold boot time in the `CPIO` case. + +## Conclusion + +Bottom-line is, using `EROFS` is recommended in every case where possible, as the upsides definitely outweigh the small downsides. + +## Learn more + +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* The `kraft pkg` [command reference](https://unikraft.org/docs/cli/reference/kraft/pkg) for packaging images. diff --git a/pages/tutorials/rootfs-volumes-roms.mdx b/pages/tutorials/rootfs-volumes-roms.mdx new file mode 100644 index 00000000..f09a49b1 --- /dev/null +++ b/pages/tutorials/rootfs-volumes-roms.mdx @@ -0,0 +1,203 @@ +--- +title: Rootfses, Volumes and ROMs +navigation_icon: square-stack +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +Rootfses, volumes and ROMs (Read-Only Memory) are storage types that share many similarities, but have different use cases and results. +Below, this tutorial discusses each one of them and how to use them, and then does a quick comparison between the three. + +## Storage types + +### Rootfses + +The rootfs, also known as the initrd, is the main component in any instance running on Unikraft Cloud, and any UNIX-based operating system. +It's used for configuring its default behaviour and contains all files needed for it to boot. +On Unikraft Cloud it's used to also start a default app (without any specific configuration). +When booting, the system loads rootfs pages in memory, which incurs a small access time characteristic to RAM read/write operations. + +At the same time, because the rootfs resides in volatile memory, changes aren't persistent across reboots. +Because of this it's best to put only essential information in there to keep the size down (1-2GB maximum), to save on boot times and on used RAM. +For persistent storage, consider using [volumes](/platform/volumes) instead. + +A rootfs can be of two types: `CPIO` or `EROFS`. +Each type has its own advantages and disadvantages, but overall `EROFS` is better suited for Unikraft Cloud. +The [rootfs formats](/tutorials/rootfs-formats) documentation page details both types. +You can create a rootfs with the legacy [`kraft` CLI](https://unikraft.org/docs/cli/install) for both types, for example (legacy CLI only): + + + + CPIO + EROFS + + + ```bash + kraft cloud deploy \ + -p 443:8080 \ + -M 900Mi \ + --rootfs-type cpio \ + demo/node-playwright-chromium + ``` + + + ```bash + kraft cloud deploy \ + -p 443:8080 \ + -M 900Mi \ + --rootfs-type erofs \ + --keep-file-owners \ + demo/node-playwright-chromium + ``` + + + +### Volumes + +[Volumes](/platform/volumes) are a non-essential, persistent, large storage component on Unikraft Cloud. +You attach volumes to stopped instances or before you create them through the [API](/api/platform/v1/volumes#create-volume). +They're automatically mounted in the instance at the specified path either `RW` (Read-Write) or `RO` (Read-Only). +They're backed up on SSD (Solid State Drive) disks and offer redundancy without compromising on access latency. +As such you can use them for persistent data, secrets, storage, logs, and more, and you can share them between instances. + +To create and use a volume, you can use the CLI volume commands. +For example, you need to first create a volume: + + + +```bash title="unikraft" +unikraft volumes create \ + --set name=my-volume \ + --set size=100 \ + --set metro=fra +``` + +```bash title="kraft" +kraft cloud volume create --size 100 --name my-volume +``` + + + +Then you can optionally populate it with local data: + + + +```bash title="kraft" +kraft cloud volume import --volume my-volume --source my-data/ +``` + + + +Finally, when you deploy an instance you can attach the volume to it: + + + +```bash title="unikraft" +unikraft run --metro=fra -m 512MiB -p 443:8080/http+tls -v my-volume:/mnt --image=my-app:latest +``` + +```bash title="kraft" +kraft cloud deploy \ + -M 512 \ + -p 443:8080 \ + --volume my-volume:/mnt \ + . +``` + + + +### ROMs + +[ROMs](/features/roms) are an extension of the rootfs concept and a good way to customize generic rootfses for many instances. +They're attached on stopped instances, but aren't mounted automatically on boot as they can have any format (`ext4`, `cpio`, `erofs`, `xfs`, `ntfs`, etc). +They're not persistent and are `RO` (Read-Only), and as such should contain only custom rootfs code/data. +They're intended to be small, fast, and flexible as opposed to volumes. + +For example, you can use ROMs in the context of a web server that offers different index pages based on the ROM attached for localization. +By using ROMs, you can have a single slim image to which you can attach different ROMs depending on the language you want to serve. +Finally, it's considered good practice to keep ROMs small and focused, and the instance shouldn't depend on them to boot correctly. + +You need to first package ROMs as an OCI image and push them to the registry. +They show up as any other image in the registry, but don't have a kernel. +To package them you can either use `kraft`, or package it yourself and pass it to `kraft` for pushing: + +```bash title="Kraft Packaging and Pushing" +kraft pkg \ + --plat kraftcloud \ + --arch x86_64 \ + --name function-hello:latest \ + --rootfs-type erofs \ + --push \ + . +``` + +You can then specify ROMs through the API when creating or updating an instance: + + + + Update Instance + Create Instance + + + ```bash title="Updating Instance with ROMs" + curl -X PATCH "$UKC_METRO/instances/$id" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -d '{ + "prop": "roms", + "op": "add", + "value": [ + { + "name": "function-hello", + "image": "demo/function-hello:latest" + } + ] + }' + ``` + + + ```json title="Creating Instance with ROMs" + ... + "roms": [ + { + "name": "function-hello", + "image": "demo/function-hello:latest" + } + ], + ... + ``` + + + +Finally, in the instance you need to mount them like any other disk and you can find them under `/dev/` with their specified name: + +```bash title="Mounting ROMs in Instance" +mkdir /mnt/rom +mount -t erofs /dev/function-hello /mnt/rom +``` + +## Comparison + +| Type / Metric | Rootfses | Volumes | ROMs | +|---------------|----------|---------|------| +| I/O Speed | Fast | Decent | Fast | +| I/O Latency | Small | Noticeable | Small | +| Storage Medium | RAM | SSD/HDD | RAM | +| Intended Size | Medium | (up to) Large | Small | +| File System | `CPIO`/`EROFS` | `ext4`/`virtio-fs` | Any | +| Mounting | Automatic | Automatic | Manual | +| Persistence | No | Yes | No | +| Mandatory | Yes | No | No | + +## Conclusion + +As expected, each storage type has its own advantages and disadvantages. +Rootfses are mandatory and fast, but you should keep them small and only contain essential information. +Volumes are persistent and large, but have noticeable access latency. +ROMs are a flexible way to customize rootfses, but you should also keep them small and not be essential for booting. + +## Learn more + +* [Volumes](/platform/volumes) and how to use them. +* [Rootfs formats](/tutorials/rootfs-formats) and how to create them. +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). +* The `kraft pkg` [command reference](https://unikraft.org/docs/cli/reference/kraft/pkg) for packaging images. diff --git a/pages/tutorials/scale-to-zero-triggers.mdx b/pages/tutorials/scale-to-zero-triggers.mdx new file mode 100644 index 00000000..54d52632 --- /dev/null +++ b/pages/tutorials/scale-to-zero-triggers.mdx @@ -0,0 +1,143 @@ +--- +title: Scale To Zero Triggers +navigation_icon: arrow-down-1-0 +--- + +import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" + +Unikraft Cloud supports scaling instances to zero based on different triggers. +This allows you to optimize resource usage and costs by automatically setting instances to standby when they're not needed. +They will automatically resume when required with minimal delay. +The platform can scale down instances automatically, or you can scale them down manually. +You can read more about Scale To Zero [here](/features/scale-to-zero). + +## Automatic triggers + +Automatic triggers are the most common and easiest way to use scale-to-zero with your instances on Unikraft Cloud. +By default, instances scale down automatically when there is no traffic coming to them for a specified period. +You can also configure scale-to-zero to consider idle active connections when determining whether to scale down an instance. + +### No traffic + +When you enable this trigger, Unikraft Cloud will track incoming connections to your instance. +If there is no traffic for the configured duration, the instance scales down to zero. +You can use the legacy CLI tool or the [API](/api/platform/v1/instances#create-instance) to enable this trigger: + + + + Kraftkit + API + + +```bash +kraft cloud instance create \ + --scale-to-zero on \ + --scale-to-zero-cooldown 5s \ + ... +``` + + +```json +{ + ... + "scale_to_zero": { + "policy": "on", + "cooldown_time_ms": "5000 + } +} +``` + + + +### Idle connections + +When enabling this trigger, the platform will consider all active connections to your instance, so all traffic going to them. +The instance will scale down if you make no new network connections and all existing connections have been idle for the configured duration. + +You can enable this trigger using the legacy CLI tool or the [API](/api/platform/v1/instances#create-instance) as follows: + + + + Kraftkit + API + + +```bash +kraft cloud instance create \ + --scale-to-zero idle \ + --scale-to-zero-cooldown 5s \ + --scale-to-zero-stateful \ + ... +``` + + +```json +{ + ... + "scale_to_zero": { + "policy": "idle", + "cooldown_time_ms": "5000", + "stateful": true + } +} +``` + + + +:::tip +If your app uses [keep-alive](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Keep-Alive) messages over this active connection, your instance won't scale down. +You will have to either disable keep-alive or configure them with an interval high enough to allow scaling down. +For example, with a 5 seconds cooldown, you can set keep-alive to once per minute. +::: + +## Manual triggers + +For specific use cases, you might want to manually scale your instances to zero, or to manually disable scale-to-zero. +Unikraft Cloud supports two ways to manually trigger scale-to-zero. + +### API + +You can use the Unikraft Cloud API to manually scale down an instance. +You will have to use the `/instances/suspend` endpoint to scale down an instance. +This happens instantly, regardless of the configured automatic triggers. + +First, set the required environment variables: + +```bash +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +Then invoke the following `curl` command to scale down an instance named `my-instance`: + +```bash +curl -X POST https://api.$UKC_METRO.unikraft.cloud/v1/instances/suspend \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "my-instance" + }' +``` + +### Device file + +You can also manually scale down an instance by writing '+'/'-' to a special device file available inside the instance. +Every instance has this file at `/uk/libukp/scale_to_zero_disable`. +By writing '+' to this file, you disable automatic scale-to-zero for the instance until the number of '-' written matches the number of '+' written. +For example, to disable automatic scale-to-zero, run the following command inside the instance: + +```bash +echo '+' > /uk/libukp/scale_to_zero_disable +``` + +:::tip +If your app starts listening for incoming connections before being ready to serve them, you can use this mechanism to disable scale-to-zero during startup. +Once your app is ready to serve requests, you can write '-' to the file to re-enable automatic scale-to-zero. +::: + +## Learn more + +* The [Scale To Zero](/features/scale-to-zero) feature page. +* The [CLI reference](/docs/cli/unikraft) and the [legacy CLI reference](/docs/cli/kraft/overview). diff --git a/pages/use-cases/api-gateways.mdx b/pages/use-cases/api-gateways.mdx index d82d8f40..11a82edc 100644 --- a/pages/use-cases/api-gateways.mdx +++ b/pages/use-cases/api-gateways.mdx @@ -7,16 +7,16 @@ API Gateways are the critical entry point to your services. They manage authentication, routing, rate-limiting, observability, and security at the edge of your infrastructure. In modern architectures—especially microservices and serverless platforms—the API Gateway is the first line of defence and performance optimisation. -With **Unikraft Cloud**, API Gateways gain advantage from a unikernel-based, scale-to-zero infrastructure that delivers the performance of specialised systems with the flexibility of serverless deployment. +With **Unikraft Cloud**, API Gateways gain advantage from a microVM-based, scale-to-zero infrastructure that delivers the performance of specialised systems with the flexibility of serverless deployment. ## Why run an API gateway on Unikraft Cloud ### ⚡ Ultra-low latency -Unikernels remove the overhead of general-purpose operating systems by compiling only the required OS components alongside the API Gateway app. +MicroVMs remove the overhead of general-purpose operating systems by compiling only the required OS components alongside the API Gateway app. This results in: -* Sub-millisecond cold-start times (perfect for bursty API traffic). +* Single-digit-millisecond cold-start times (perfect for bursty API traffic). * Minimal memory footprint, allowing more requests per node. * Consistent, predictable response latency at scale. @@ -25,7 +25,7 @@ This results in: The open internet exposes API Gateways, making them often the most attacked component. Unikraft Cloud isolates each instance at the virtual machine level while keeping footprint minimal: -* Immutable, single-purpose unikernel images reduce attack surface. +* Immutable, single-purpose microVM images reduce attack surface. * Strong VM-level isolation without container overhead. * Built-in memory safety and image signing to ensure trust at deployment. @@ -35,8 +35,8 @@ Traffic to APIs often fluctuates—peaking during business hours, quiet overnigh Traditional gateway clusters waste resources when idle. With Unikraft Cloud: -* API Gateways use scale-to-zero when not in use, reducing idle costs to **zero**. -* Instant cold-starts ensure gateways are ready in microseconds when traffic resumes. +* API Gateways scale-to-zero when not in use, reducing idle costs to **zero**. +* Instant cold-starts ensure gateways are ready in milliseconds when traffic resumes. * Fine-grained billing: pay only for requests and runtime, not for idle VMs. ### 🔄 Serverless-ready @@ -59,36 +59,44 @@ It offers open standards, scalability, governance, and AI-ready features for API To run it, follow these steps: -1. Install the [`kraft` CLI tool](/install) and a container runtime engine (for example, Docker). +1. Install the [`kraft` CLI tool](https://unikraft.org/docs/cli/install) and a container runtime engine (for example, [Docker](https://docs.docker.com/engine/install/)). -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/tyk/` directory: +1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/tyk/` directory: ```bash title="" -git clone https://github.com/kraftcloud/examples +git clone https://github.com/unikraft-cloud/examples cd examples/tyk/ ``` -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): -```bash title="" + + +```bash title="kraft" # Set Unikraft Cloud access token export UKC_TOKEN=token # Set metro to Frankfurt, DE export UKC_METRO=fra ``` + + Then invoke: -```bash title="" + + +```bash title="kraft" kraft cloud compose up ``` + + After deploying, you can query the service using the provided address. Use the `/hello` path after the address, such as below: ```bash title="" -curl https://..kraft.host/hello | jq +curl https:///hello | jq ``` ```json title="" { @@ -105,3 +113,16 @@ curl https://..kraft.host/hello | jq } ``` +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/kraft/overview). diff --git a/pages/use-cases/build-test-environments.mdx b/pages/use-cases/build-test-environments.mdx new file mode 100644 index 00000000..8c6357be --- /dev/null +++ b/pages/use-cases/build-test-environments.mdx @@ -0,0 +1,451 @@ +--- +title: Build and Test Environments +navigation_icon: tool-case +--- + +Build and test workloads often need dynamic code execution in a controlled runtime. +You want to swap test logic, keep startup latency low, and avoid rebuilding full images for every test change. + +With **Unikraft Cloud**, you can run a reusable Go runtime image and inject test logic via ROMs. +At startup, the instance compiles ROM code into a Go plugin and executes it, giving you fast iteration with strong isolation. + +## Why run build and test environments on Unikraft Cloud + +### Fast test iteration + +Build and test logic changes frequently. +With ROM-based deployment, you package and push a small ROM instead of rebuilding the full runtime image: + +* Keep the runtime stable. +* Update only test payloads. +* Roll forward and back by switching ROM image tags. + +### Runtime compilation in isolated microVMs + +Each environment runs in its own microVM. +The runtime compiles ROM code inside the instance (`go build -buildmode=plugin`) and loads it as a plugin. +This isolates compilation and execution while keeping the runtime reusable. + +### Template-based cold start optimization + +The base image can create an [instance template](/platform/instances#instance-templates). +New environments launch from that template, reducing startup time for bursty CI-style workloads. + +### Scale-to-zero economics + +Test environments are often idle between runs. +With scale-to-zero enabled, idle instances consume no active resources while still waking when needed. + +## Getting started + +This guide uses the [`build-environments`](https://github.com/unikraft-cloud/examples/tree/main/build-environments) example. +The base image contains a Go server (`server.go`) that: + +1. Writes to `/uk/libukp/template_instance` to create a template. +2. Reads ROM source from `/rom/rom.go`. +3. Compiles it to `/run/rom.so` using `go build -buildmode=plugin`. +4. Loads the `Handler()` function from the plugin and serves its output over HTTP. + +### Prerequisites + +1. Install the CLI: + Use the [unikraft CLI](/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/). + +1. Clone the examples repository and enter the example directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/build-environments/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +export UKC_METRO=fra +``` + + + +### Step 1: Package and push the base runtime image + +Package and push the base Go runtime image (see `server.go` for the runtime implementation): + + + +```bash title="unikraft" +unikraft build . --output /go-build-env:latest +``` + +```bash title="kraft" +kraft pkg \ + --name index.unikraft.io//go-build-env:latest \ + --plat kraftcloud \ + --arch x86_64 \ + --rootfs-type erofs \ + --push \ + . +``` + + + +The server in `server.go` loads `/rom/rom.go`, compiles it to `/run/rom.so` using `go build -buildmode=plugin`, and invokes `Handler()` from the plugin. + +### Step 2: Create a template from the base image + +Create a short-lived instance from the base image (without ROM attached). +The server writes to `/uk/libukp/template_instance` and turns the instance into a [template](/platform/instances#instance-templates) before serving requests: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name go-build-env \ + -m 512M \ + --image /go-build-env:latest +``` + +```bash title="kraft" +kraft cloud instance create \ + --start \ + --name go-build-env \ + -M 512Mi \ + /go-build-env:latest +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: go-build-env +uuid: 650dbbe7-3949-4c93-88e7-6619a9216e0c +state: starting +image: /go-build-env +resources: + memory: 512MiB + vcpus: 1 +networks: +- uuid: 6f7a8b9c-0d1e-2f3a-4b5c-f6a7b8c9d0e1 + private-ip: 10.0.5.4 + mac: 12:b0:6c:3e:ab:95 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: go-build-env + ├───────── uuid: 650dbbe7-3949-4c93-88e7-6619a9216e0c + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├──────── image: oci://unikraft.io//go-build-env@sha256:1f57e9bb8702d031743acf43164b24cf182158c398f1eda8c5583208ccc9c300 + ├─────── memory: 512 MiB + ├─ private fqdn: go-build-env.internal + └─── private ip: 10.0.5.4 +``` + + + +This instance is short-lived, since right before the server starts, it triggers a conversion into a template. +To check that the template is ready, run: + + + +```bash title="unikraft" +unikraft instances templates list +``` + +```bash title="kraft" +kraft cloud instance template list +``` + + + +
+ + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED +fra go-build-env template /go-build-env 512MiB 1 just now +``` + +```ansi title="kraft" +NAME IMAGE ARGS CREATED AT +go-build-env oci://unikraft.io//go-build-env@sha256:1cbd6474386c9df546820cd0522030a1bd8702c59490548f10ae10fb8b0a6e14 20 seconds ago +``` + + + +### Step 3: Package ROM payloads + +Each ROM contains a Go function implementation. + + + +```bash title="unikraft" +unikraft build rom1/ --output /go-rom1:latest +unikraft build rom2/ --output /go-rom2:latest +``` + +```bash title="kraft" +kraft pkg \ + --rom ./fs \ + --rom-type erofs \ + --plat kraftcloud \ + --arch x86_64 \ + --name index.unikraft.io//go-rom1:latest \ + --push \ + rom1/ +kraft pkg \ + --rom ./fs \ + --rom-type erofs \ + --plat kraftcloud \ + --arch x86_64 \ + --name index.unikraft.io//go-rom2:latest \ + --push \ + rom2/ +``` + + + +### Step 4: Launch test environments from template + +Create an instance with the first ROM: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name go-build-env-rom1 \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ + --rom image=/go-rom1:latest,at=/rom \ + --template go-build-env +``` + +```bash title="kraft" +# kraft does not support creating instances with attached ROMs, but you can use the API directly +curl -X POST "$UKC_METRO/instances" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "go-build-env-rom1", + "template": { + "name": "go-build-env" + }, + "autostart": true, + "service_group": { + "services": [ + { + "port": 443, + "destination_port": 8080, + "handlers": ["tls", "http"] + } + ] + }, + "scale_to_zero": { + "policy": "on", + "stateful": true, + "cooldown_time_ms": 1000 + }, + "roms": [ + { + "name": "go_function", + "image": "index.unikraft.io//go-rom1:latest", + "at": "/rom" + } + ] +}' +``` + + + +The instance will compile the ROM into a plugin on first start, which may take a few seconds. +To check the progress, you can view the instance logs: + + + +```bash title="unikraft" +unikraft instances logs go-build-env-rom1 -f +``` + +```bash title="kraft" +kraft cloud instance logs go-build-env-rom1 -f +``` + + + +Create another instance with the second ROM: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name go-build-env-rom2 \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ + --rom image=/go-rom2:latest,at=/rom \ + --template go-build-env +``` + +```bash title="kraft" +# kraft does not support creating instances with attached ROMs, but you can use the API directly +curl -X POST "$UKC_METRO/instances" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "go-build-env-rom2", + "template": { + "name": "go-build-env" + }, + "autostart": true, + "service_group": { + "services": [ + { + "port": 443, + "destination_port": 8080, + "handlers": ["tls", "http"] + } + ] + }, + "scale_to_zero": { + "policy": "on", + "stateful": true, + "cooldown_time_ms": 1000 + }, + "roms": [ + { + "name": "go_function", + "image": "index.unikraft.io//go-rom2:latest", + "at": "/rom" + } + ] +}' +``` + + + +The instance will compile the ROM into a plugin on first start, which may take a few seconds. +To check the progress, you can view the instance logs: + + + +```bash title="unikraft" +unikraft instances logs go-build-env-rom2 -f +``` + +```bash title="kraft" +kraft cloud instance logs go-build-env-rom2 -f +``` + + + +Both instances run the same base Go runtime, but execute different ROM payloads, demonstrating fast iteration with reusable layers. + +List the instances and note their FQDN values: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + +
+ + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra go-build-env-rom2 standby /go-build-env 512MiB 1 nameless-wood-gw7pbnls.fra.unikraft.app 2 minutes ago +fra go-build-env-rom1 standby /go-build-env 512MiB 1 sparkling-dawn-syowlbtj.fra.unikraft.app 3 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +go-build-env-rom2 nameless-wood-gw7pbnls.fra.unikraft.app standby standby oci://unikraft.io//go-build-env@sha256:1cbd64... 512 MiB 1 6.98 ms +go-build-env-rom1 sparkling-dawn-syowlbtj.fra.unikraft.app standby standby oci://unikraft.io//go-build-env@sha256:1cbd64... 512 MiB 1 7.86 ms +``` + + + +Test both instances using the FQDNs from the listing: + +```bash +curl https://sparkling-dawn-syowlbtj.fra.unikraft.app +curl https://nameless-wood-gw7pbnls.fra.unikraft.app +``` + +```text +Bye, World! +Auf Wiedersehen! +``` + +## Why this pattern works + +This build/test environment uses three reusable layers: + +1. **Base runtime image**: +A Go server plus toolchain. +Build once, reuse many times. + +1. **ROM payload**: +A source-only image (`rom.go`). +Update per test case. + +1. **Template snapshot**: +A pre-initialized runtime used to speed up new instance startup. + +## Cleanup + + + +```bash title="unikraft" +unikraft instances delete go-build-env-rom1 go-build-env-rom2 +unikraft instances template delete go-build-env +``` + +```bash title="kraft" +kraft cloud instance remove go-build-env-rom1 go-build-env-rom2 +kraft cloud instance template remove go-build-env +``` + + + +## Learn more + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/kraft/overview). diff --git a/pages/use-cases/game-servers.mdx b/pages/use-cases/game-servers.mdx new file mode 100644 index 00000000..733d6884 --- /dev/null +++ b/pages/use-cases/game-servers.mdx @@ -0,0 +1,288 @@ +--- +title: Game Servers +navigation_icon: gamepad-2 +--- + +Running multiplayer game servers often means keeping long-lived VMs online, pre-warming for peak hours, and paying for idle time between sessions. +On Unikraft Cloud, you can run game servers in microVMs with [stateful scale-to-zero](/features/scale-to-zero#stateful-scale-to-zero), so instances pause when idle and resume when players reconnect. + +Minecraft is a strong example of this pattern. +JVM startup and world initialization are expensive, but template snapshots and stateful resume keep player experience smooth without running infrastructure every day. + +## Why use Unikraft Cloud for game servers + +### Fast restarts from template snapshots + +Game servers often have expensive startup paths: loading assets, initializing runtime state, and binding network services. +Unikraft Cloud's [instance templates](/platform/instances#instance-templates) snapshot the server after initialization, so later starts resume from a warm state instead of repeating full initialization. +For Minecraft specifically, this avoids repeated JVM warm-up and world initialization work. + +### Stateful scale-to-zero + +When all players disconnect, the instance enters standby and consumes no active resources. +On the next connection it wakes from the same in-memory snapshot, preserving server state without a full restart. +For Minecraft, this means your world and server process resume from snapshot instead of cold booting. + +### Isolation per server + +Each server runs in its own microVM: no shared kernel with other tenants, no container escape surface, and no noisy-neighbour risk. + +### Quick iteration with ROM + +Server settings can ship as auxiliary ROMs at instance creation time. +You can run distinct game-server variants by swapping ROM configuration rather than rebuilding the base image. + +## Getting started + +This guide uses the [`minecraft`](https://github.com/unikraft-cloud/examples/tree/main/minecraft) example. +The base image is built from [itzg/minecraft-server](https://hub.docker.com/r/itzg/minecraft-server) (Java 25) and includes: + +- A `wrapper.sh` entrypoint that starts SSH, loads ROM configuration, and disables scale-to-zero during initialization. +- Patched startup scripts that snapshot the instance into a template before full server warm-up. + +### Prerequisites + +1. Install the [unikraft CLI](/docs/cli/unikraft). + You need a [BuildKit](https://github.com/moby/buildkit) builder. The easiest way to get one is via [Docker](https://docs.docker.com/engine/install/). + +2. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/minecraft/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/minecraft/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + +```bash title="unikraft" +unikraft login +``` + +3. Review and adjust the base server settings in [`base/.env`](https://github.com/unikraft-cloud/examples/tree/main/minecraft/base/.env). + You can check out [this documentation](https://docker-minecraft-server.readthedocs.io/en/latest/) for available configuration options. + Make sure to also set your `PUBKEY` for SSH access, and optionally set `TEMPLATE_WITH_WORLD` if you want the template to include the world (see below). + Optionally, create per-config overrides in `/.env`. + All `.env` files are packaged as [auxiliary ROMs](/features/roms) and mounted at `/rom/`. + +### Step 1: Package and push the base image + +First, package and push the base Minecraft server image: + +```bash title="unikraft" +unikraft build . --output /minecraft:latest +``` + +The image contains the files from the Docker image [itzg/minecraft-server](https://hub.docker.com/r/itzg/minecraft-server) and includes a few tweaks: + +- The entrypoint uses a custom `wrapper.sh` script that: + - Starts an SSH server + - Loads the environment configuration from the attached ROMs + - Disables scale-to-zero before executing the original entrypoint +- The server configuration scripts from the original image have patches to trigger the template snapshot before the full warm-up of the server, which allows faster instance creation from the template. + Scale-to-zero is then re-enabled after server initialization. + +### Step 2: Create a template from the base image + +Boot a short-lived instance from the base image. +The startup script snapshots it into a template before world generation, then the instance exits. + +Pass your server settings as a ROM: + +```bash title="unikraft" +unikraft run --metro fra \ + --name minecraft-tpl \ + -m 4096M \ + --vcpus 4 \ + --image /minecraft:latest \ + --rom dir=base,at=/rom/base +``` + +The output shows the instance details: + +```ansi title="unikraft" +metro: fra +name: minecraft-tpl +uuid: d9d7be54-5495-45d0-b5af-48be7f30d1d8 +state: starting +image: /minecraft +resources: + memory: 4GiB + vcpus: 4 +roms: +- name: rom + image: c99125f6-6b8c-4f94-b94c-e2d9551b253b + at: /rom +networks: +- uuid: 123b0ed0-9f2f-417c-9307-34424d80e4cb + private-ip: 10.0.0.29 + mac: 12:b0:0a:00:00:1d +timestamps: + created: just now +``` + +If you also have per-config overrides (for example in `bingo/.env`), create the instance with more ROMs: + +```bash title="unikraft" +unikraft run --metro fra \ + --name minecraft-tpl \ + -m 4096M \ + --vcpus 4 \ + --image /minecraft:latest \ + --rom dir=base,at=/rom/base \ + --rom dir=bingo,at=/rom/bingo +``` + +The instance runs until initialization completes, then the platform snapshots it as a template and immediately deletes it. +You can configure the exact moment of snapshotting in [`patches/start-finalExec`](https://github.com/unikraft-cloud/examples/tree/main/minecraft/patches/start-finalExec) or with the following environment variable: + +- If `TEMPLATE_WITH_WORLD=true`, then the snapshot will trigger **after** world generation +- Otherwise, the snapshot will trigger after extracting the jar files and writing configuration files, **before** world generation + +Follow the template instance logs until they stop: + +```bash title="unikraft" +unikraft instances logs minecraft-tpl -f +``` + +Confirm the template is ready: + +```bash title="unikraft" +unikraft instances templates list +``` + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED +fra minecraft-tpl template /minecraft 4GiB 4 5 seconds ago +``` + +### Step 3: Launch a server from the template + +```bash title="unikraft" +unikraft run --metro fra \ + --name minecraft \ + -p 2222:2222/tls \ + -p 25565:25565/tls \ + --scale-to-zero policy=on,cooldown-time=5000,stateful=true \ + --template minecraft-tpl +``` + +The output includes the service address: + +```ansi title="unikraft" +metro: fra +name: minecraft +uuid: 56787b5d-14fb-4908-88d0-e9609d1ac1e0 +state: running +image: /minecraft +resources: + memory: 4GiB + vcpus: 4 +service: hidden-water-ewr8l9sp +roms: +- name: rom + image: c99125f6-6b8c-4f94-b94c-e2d9551b253b + at: /rom +networks: +- uuid: b6c7d615-7e74-49d6-a8b6-14c39323233b + private-ip: 10.0.0.29 + mac: 12:b0:0a:00:00:1d +timestamps: + created: just now +scale-to-zero: policy=on,stateful=true,cooldown-time=5s +``` + +The instance address is `https://hidden-water-ewr8l9sp.fra.unikraft.app`. +Follow the logs until the server is ready: + +```bash title="unikraft" +unikraft instances logs minecraft -f +``` + +```text +[12:59:45] [Server thread/INFO]: Preparing spawn area: 100% +[12:59:45] [Server thread/INFO]: Time elapsed: 3641 ms +[12:59:45] [Server thread/INFO]: Done (3.850s)! For help, type "help" +[12:59:45] [Server thread/INFO]: Starting remote control listener +[12:59:45] [Server thread/INFO]: Thread RCON Listener started +[12:59:45] [Server thread/INFO]: RCON running on 0.0.0.0:25575 +[12:59:45] [Server thread/INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:overworld +[12:59:45] [Server thread/INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:the_end +[12:59:45] [Server thread/INFO]: Saving chunks for level 'ServerLevel[world]'/minecraft:the_nether +[12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage (world): All chunks are saved +[12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM1): All chunks are saved +[12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage (DIM-1): All chunks are saved +[12:59:45] [Server thread/INFO]: ThreadedAnvilChunkStorage: All dimensions are saved +``` + +## Connect to the server + +:::caution[**DISCLAIMER**] +At the moment, Unikraft Cloud exposes services over TLS. +Minecraft and SSH clients speak plain TCP, so use `socat` to end TLS locally. +::: + +### Minecraft + +```bash +socat TCP-LISTEN:25565,reuseaddr,fork OPENSSL:hidden-water-ewr8l9sp.fra.unikraft.app:25565,verify=0 +``` + +Connect your Minecraft client to `localhost:25565`. + +### SSH + +```bash +socat TCP-LISTEN:2222,reuseaddr,fork OPENSSL:hidden-water-ewr8l9sp.fra.unikraft.app:2222,verify=0 +``` + +Connect to `localhost:2222` with username `root` and the SSH key set in `.env`. + +## Administration + +### Remote console + +SSH into the instance, then: + +```bash +rcon-cli --host 127.0.0.1 +``` + +Example commands: + +```text +say Hello from Unikraft Cloud! +/op +``` + +### Whitelist in offline mode + +Offline mode generates UUIDs that differ from online UUIDs. +Compute the correct UUID per username and add it to `/data/whitelist.json`: + +```python +import hashlib, uuid; h = hashlib.md5(b'OfflinePlayer:').digest(); u = uuid.UUID(bytes=h); print(u) +``` + +## Cleanup + +```bash title="unikraft" +unikraft instances delete minecraft +unikraft instances template delete minecraft-tpl +``` + +## Learn more + +```bash title="unikraft" +unikraft --help +``` + +Or visit the [CLI Reference](/docs/cli/unikraft). + +For more information on the features used in this use case and how to use them, check out the following documentation: + +* [Instance templates](/platform/instances#instance-templates) +* [Scale-to-zero](/features/scale-to-zero) +* [ROMs](/features/roms) +* [`minecraft` example](https://github.com/unikraft-cloud/examples/tree/main/minecraft) +* [itzg/minecraft-server documentation](https://docker-minecraft-server.readthedocs.io/en/latest/) diff --git a/pages/use-cases/headless-browsers.mdx b/pages/use-cases/headless-browsers.mdx index e54e8f9e..e1b8a847 100644 --- a/pages/use-cases/headless-browsers.mdx +++ b/pages/use-cases/headless-browsers.mdx @@ -3,20 +3,18 @@ title: Headless Browsers navigation_icon: app-window --- -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - Headless browsers power web scraping, automated testing, Search Engine Optimization (SEO) rendering, and synthetic monitoring. -They're resource-intensive, security-sensitive, and often run in short bursts—making them ideal candidates for unikernel-based, scale-to-zero infrastructure. +They're resource-intensive, security-sensitive, and often run in short bursts—making them ideal candidates for microVM-based, scale-to-zero infrastructure. ## Why headless browsers on Unikraft Cloud ### ⚡ Instant startups -Instances on Unikraft Cloud boot in microseconds, so browsers spin up only when needed: +Instances on Unikraft Cloud boot in milliseconds, so browsers spin up only when needed: * Perfect for on-demand scraping and CI/CD testing. -* No idle costs when browsers aren’t running. +* No idle costs when browsers aren't running. ### 🔒 Strong security @@ -25,13 +23,12 @@ With Unikraft Cloud: * Each browser runs in its own VM, isolated by design. * Minimal OS footprint drastically reduces attack surface. -* Immutable unikernel images prevent tampering. ### 💸 Cost-efficient scale-to-zero Scraping jobs or test suites often run in short, irregular bursts: -* Browsers use scale-to-zero when idle, cutting infrastructure waste. +* Browsers scale-to-zero when idle, cutting infrastructure waste. * Pay only for the seconds of actual execution. ### 🌍 Serverless-ready @@ -41,6 +38,7 @@ Integrate with pipelines and workloads that demand ephemeral execution: * Trigger browsers via APIs or serverless functions. * Distribute workloads globally to run close to data sources. + ## Getting started Headless browsers on **Unikraft Cloud** run faster, safer, and cheaper. @@ -52,25 +50,36 @@ option to run them headless (no UI). To run it, follow these steps: -1. Install the [`kraft` CLI tool](/install) and a container runtime engine (for example, Docker). +1. Install the CLI and a container runtime engine (for example, [Docker](https://docs.docker.com/engine/install/)). + Use the [unikraft CLI](/docs/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/node-express-puppeteer/` directory: +1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node-express-puppeteer/` directory: ```bash -git clone https://github.com/kraftcloud/examples +git clone https://github.com/unikraft-cloud/examples cd examples/node-express-puppeteer/ ``` -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): -```bash + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" # Set Unikraft Cloud access token export UKC_TOKEN=token # Set metro to Frankfurt, DE export UKC_METRO=fra ``` + + +The `UKC_TOKEN` and `UKC_METRO` environment variables are only supported by the legacy CLI. + :::note A Puppeteer instance on Unikraft Cloud requires 4GB to run. @@ -80,10 +89,19 @@ Request an increase in the instance memory quota when you need more memory. When done, invoke the following command to deploy this app on Unikraft Cloud: -```bash -kraft cloud deploy -p 443:3000 -M 4096 . + + +```bash title="unikraft" +unikraft build . --output /node-express-puppeteer:latest +unikraft run --metro=fra -p 443:3000/http+tls -m 4Gi --image=/node-express-puppeteer:latest ``` +```bash title="kraft" +kraft cloud deploy -p 443:3000 -M 4Gi . +``` + + + The output shows the instance address and other details: ```ansi @@ -107,15 +125,23 @@ They're different for each run. Use a browser to access the landing page of the Puppeteer (that uses [ExpressJS](https://expressjs.com/)). The app and the landing page are part of [this repository](https://github.com/christopher-talke/node-express-puppeteer-pdf-example). -In the example run above, the landing page is at the address https://nameless-fog-0tvh1uov.fra.unikraft.app +In the example run above, the landing page is at the address `https://nameless-fog-0tvh1uov.fra.unikraft.app`. You can use the landing page to generate the PDF version of a remote page. At any time, you can list information about the instance: -```bash + + +```bash title="unikraft" +unikraft instances list node-express-puppeteer-7afg3 +``` + +```bash title="kraft" kraft cloud instance list node-express-puppeteer-7afg3 ``` + + ```text NAME FQDN STATE STATUS IMAGE MEMORY ARGS BOOT TIME node-express-puppeteer-7afg3 node-express-puppeteer-7afg3.fra.unikraft.app running since 6mins node-express-puppeteer-7afg3@s... 4.0 GiB /usr/bin/wrapper.sh /usr/bin/n... 15.27 ms @@ -123,10 +149,18 @@ node-express-puppeteer-7afg3 node-express-puppeteer-7afg3.fra.unikraft.app run When done, you can remove the instance: -```bash + + +```bash title="unikraft" +unikraft instances delete node-express-puppeteer-7afg3 +``` + +```bash title="kraft" kraft cloud instance remove node-express-puppeteer-7afg3 ``` + + ### Customize your deployment The current deployment uses an ExpressJS service that uses the [PDF generating functionality of Puppeteer](https://devdocs.io/puppeteer/). @@ -137,8 +171,16 @@ You can update the service itself to provide a Representational State Transfer ( Use the `--help` option for detailed information on using Unikraft Cloud: -```bash + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" kraft cloud --help ``` -Or visit the [CLI Reference](/docs/cli). + + +Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/kraft/overview). diff --git a/pages/use-cases/mcp-servers.mdx b/pages/use-cases/mcp-servers.mdx new file mode 100644 index 00000000..e4db862e --- /dev/null +++ b/pages/use-cases/mcp-servers.mdx @@ -0,0 +1,212 @@ +--- +title: MCP Servers +navigation_icon: tool-case +--- + +[MCP (Model Context Protocol)](https://modelcontextprotocol.io/docs/getting-started/intro) servers enable AI assistants and LLMs to access external tools, data sources, and capabilities in a standardized way. +They act as bridges between AI models and real-world resources—file systems, databases, APIs, search engines, and custom business logic. +As AI assistants become more capable, MCP servers are becoming critical infrastructure for grounding AI in reality. + +With **Unikraft Cloud**, you are enhancing your MCP servers with instant deployment, scale-to-zero economics, and millisecond-level cold starts—perfect for the on-demand nature of AI interactions. + +## Why run MCP servers on Unikraft Cloud + +### ⚡ Zero-latency context injection + +AI assistants need instant access to tools and data. +Every millisecond of latency impacts the user experience. +Unikraft Cloud's microVM architecture reduces OS overhead: + +* Single-digit-millisecond cold starts mean MCP servers are ready instantly. +* Minimal memory footprint allows dense packing of specialized servers. + +### 🔒 Isolated execution + +MCP servers often access sensitive data—file systems, databases, proprietary APIs. +Unikraft Cloud provides VM-level isolation for each server instance: + +* Strong isolation prevents cross-contamination between different MCP contexts. +* Minimal attack surface with only essential components included. + +### 💸 Pay-per-query economics + +AI interactions are inherently bursty—periods of intense activity followed by silence. +Traditional server architectures waste resources during idle periods. +With Unikraft Cloud: + +* MCP servers scale-to-zero when not in use, costing nothing. +* Instant wake-up ensures your queries have no noticeable delay. +* Fine-grained billing means you only pay for actual AI interactions. + +### 🌍 Edge-ready deployment + +Unikraft Cloud help you reduce the latency of your applications: + +* Choose to deploy to your closest [metro](/platform/metros). +* Lightweight footprint enables cost-effective multi-region deployment. +* Fast startup times ensure consistent performance across all locations. + + +## Getting started + +MCP servers on Unikraft Cloud provide the perfect runtime for AI tool integration, being fast, secure, and cost-efficient. +Scale-to-zero economics meet instant availability, ensuring your AI assistants always have the tools they need, exactly when they need them. + +This guide shows you how to deploy two types of MCP servers: + +### Example 1: ArXiv MCP server + +The [arXiv MCP Server](https://github.com/blazickjp/arxiv-mcp-server) is a community-made MCP server that connects AI tools directly to the [arXiv](https://arxiv.org/) archive of scientific articles and publications. +It allows MCP clients to browse and analyze papers. + +To run an example based on it, follow these steps: + +1. Install the [`kraft` CLI tool](https://unikraft.org/docs/cli/install) and a container runtime engine (for example, [Docker](https://docs.docker.com/engine/install/)). + +1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/mcp-server-arxiv/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/mcp-server-arxiv/ +``` + +Make sure to log into Unikraft Cloud by setting your token and a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + +```bash +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +Follow the instructions in the example's README to set it up and grab the server's FQDN. + +You can use the included example client to test the server, or connect to it using your favourite MCP client. +For example, using [Visual Studio Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers#_add-an-mcp-server), you can add the following to your `.vscode/mcp.json` file: + +```json +{ + "servers": { + "arxiv-mcp": { + "type": "http", + "url": "https:///mcp" + } + } +} +``` + +The tool will then show up in Copilot Chat. +You can find similar instructions for [Claude Code](https://code.claude.com/docs/en/mcp#option-1:-add-a-remote-http-server) and other MCP-compatible clients. + +Example query that will trigger a tool call: + +{/* vale off */} + +> **Q:** *Could you download the original Unikraft paper for me?* +> +> **A:** The original Unikraft paper (“Unikraft: Fast, Specialized Unikernels the Easy Way”) is being downloaded and converted. I will notify you as soon as it is ready for reading or further actions. + +> **Q:** *Dump the whole first section of the paper here.* +> +> **A:** Here is the full first section (“Introduction”) from the Unikraft paper: +> +> **1 Introduction** +> +> Specialization is arguably the most effective way to achieve outstanding performance, whether it is for achieving high throughput in network-bound applications, making language runtime environments more efficient, or providing efficient container environments, to give some examples. +> Even in the hardware domain, and especially with the demise of Moore’s law, manufacturers are increasingly leaning towards hardware specialization to achieve ever better performance; the machine learning field is a primary exponent of this. +> ... + +{/* vale on */} + +### Example 2: Custom MCP server + +This example demonstrates how to build a minimal custom MCP server from scratch. +It provides weather information and time utilities. +This shows the core patterns for building your own MCP tools. + +To run it, follow these steps: + +1. Install the [`kraft` CLI tool](https://unikraft.org/docs/cli/install) and a container runtime engine (for example, [Docker](https://docs.docker.com/engine/install/)). + +1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/mcp-server-simple/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/mcp-server-simple/ +``` + +Make sure to set your Unikraft Cloud token and metro: + +```bash +export UKC_TOKEN=token +export UKC_METRO=fra +``` + +Follow the instructions in the example's README to set it up and grab the server's FQDN. + +You can use the included example client to test the server, or connect to it using your favourite MCP client. +For example, using [Visual Studio Code](https://code.visualstudio.com/docs/copilot/customization/mcp-servers#_add-an-mcp-server), you can add the following to your `.vscode/mcp.json` file: + +```json +{ + "servers": { + "custom-mcp": { + "type": "http", + "url": "https:///mcp" + } + } +} +``` + +The tool will then show up in Copilot Chat. +You can find similar instructions for [Claude Code](https://code.claude.com/docs/en/mcp#option-1:-add-a-remote-http-server) and other MCP-compatible clients. + +Example query that will trigger a tool call: + +{/* vale off */} + +> **Q**: What is the current weather in Paris, and what time is it there? +> +> **A**: The current weather in Paris is 21°C, partly cloudy, with 62% humidity. +> The local time in Paris is 23:21 (11:21 PM), Tuesday, December 2, 2025 (UTC+1). + +{/* vale on */} + +## Building your own MCP server + +MCP servers follow a simple pattern: + +1. **Define tools**: Specify what capabilities your server provides (functions, APIs, data sources). +1. **Handle requests**: Process MCP protocol requests from AI assistants. +1. **Return results**: Format responses in the MCP protocol format. + +Common use cases for custom MCP servers: + +* **Database access**: Query internal databases, data warehouses, or analytics platforms. +* **API integration**: Connect AI assistants to third-party services (customer relationship management, ticketing, monitoring). +* **Custom business logic**: Integrate company-specific calculations, validations, or workflows. +* **Document retrieval**: Search and retrieve from internal knowledge bases or document stores. + +Unikraft Cloud's instant deployment and scale-to-zero model makes it ideal for hosting dozens of specialized MCP servers. + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/kraft/overview). + +For more information about the Model Context Protocol: + +* [MCP Documentation](https://modelcontextprotocol.io/). +* [Reference MCP Servers](https://github.com/modelcontextprotocol/servers). +* [Building MCP Servers](https://modelcontextprotocol.io/docs/develop/build-server). +* [Building MCP Clients](https://modelcontextprotocol.io/docs/develop/build-client). diff --git a/pages/use-cases/remote-desktops.mdx b/pages/use-cases/remote-desktops.mdx new file mode 100644 index 00000000..c2633190 --- /dev/null +++ b/pages/use-cases/remote-desktops.mdx @@ -0,0 +1,224 @@ +--- +title: Remote Desktops +navigation_icon: screen-share +--- + +Full remote desktop environments power browser-based access to Linux GUIs, agentic computer-use workloads, secure browsing sessions, and disposable workstations. +They're memory-hungry, security-sensitive, and often run in short, interactive bursts—making them a strong fit for microVM-based, scale-to-zero infrastructure. + + +## Why run remote desktops on Unikraft Cloud + +### ⚡ Instant session startups + +MicroVMs on Unikraft Cloud boot in milliseconds, so a full desktop is ready the moment a user opens the link: + +* No waiting for heavy VMs or container images to warm up. +* Sessions resume from standby in milliseconds when traffic returns. + +### 🔒 Strong session isolation + +Remote desktops run untrusted browsers, untrusted documents, and arbitrary user input. +With Unikraft Cloud: + +* Each desktop runs in its own VM, isolated by design. +* A minimal, single-purpose image reduces attack surface. + +### 💸 Cost-efficient scale-to-zero + +Interactive sessions are bursty—active for minutes, idle for hours: + +* Desktops scale-to-zero between sessions, removing idle costs. +* Pay only for the seconds users actively spend in the session. + +### 🤖 Built for automation + +Computer-use workloads drive a real desktop the same way a human does: + +* Expose a noVNC endpoint over HTTPS for browser or programmatic access. +* Spin up a fresh, disposable environment per task or per session. + + +## Getting started + +Remote desktops on **Unikraft Cloud** run faster, safer, and cheaper. +From disposable browsers to a Linux GUI in a tab, you get instant scale-out and zero idle costs—without sacrificing isolation. + +This guide shows you how to use [noVNC](https://novnc.com/info.html), an open source Virtual Network Computing (VNC) client that runs in the browser, paired with a minimal Linux desktop and Firefox. + +To run it, follow these steps: + +1. Install the CLI and a container runtime engine (for example, [Docker](https://docs.docker.com/engine/install/)). + Use the [unikraft CLI](/docs/cli/unikraft) or the legacy [kraft CLI](https://unikraft.org/docs/cli/install). + +1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/novnc-browser/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/novnc-browser/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +The `UKC_TOKEN` and `UKC_METRO` environment variables are only supported by the legacy CLI. + +:::note + +A noVNC desktop instance on Unikraft Cloud requires 4GB to run. +Request an increase in the instance memory quota when you need more memory. + +::: + +When done, invoke the following command to deploy this app on Unikraft Cloud: + + + +```bash title="unikraft" +unikraft build . --output /novnc-browser:latest +unikraft run --scale-to-zero policy=on,cooldown-time=4000,stateful=true --metro fra -p 443:6080/tls+http -m 4G --name vnc-browser --image /novnc-browser:latest +``` + +```bash title="kraft" +kraft cloud deploy \ + --scale-to-zero on \ + --scale-to-zero-stateful \ + --scale-to-zero-cooldown 4s \ + -p 443:6080/tls+http \ + -M 4Gi \ + -n vnc-browser \ + . +``` + + + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: vnc-browser +uuid: 90a59b05-0ae1-4ca6-8383-79c5115355ee +state: starting +image: /novnc-browser +resources: + memory: 4096MiB + vcpus: 1 +service: + uuid: aaf03f7c-65e6-5624-d5f4-84e87450beee + name: weathered-fog-y5jjmwfd + domains: + - fqdn: weathered-fog-y5jjmwfd.fra.unikraft.app +networks: +- uuid: 61708609-d291-572d-4a4c-399413238199 + private-ip: 10.0.0.49 + mac: 12:b0:1e:47:6c:59 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├────────── name: vnc-browser + ├────────── uuid: 90a59b05-0ae1-4ca6-8383-79c5115355ee + ├───────── state: starting + ├──────── domain: https://weathered-fog-y5jjmwfd.fra.unikraft.app + ├───────── image: oci://unikraft.io//novnc-browser@sha256:fdb4887e84362ebbaf54c713e0d85f547e8ee173fe63a6ab39e94b7e612a9892 + ├──────── memory: 4096 MiB + ├─────── service: weathered-fog-y5jjmwfd + ├── private fqdn: vnc-browser.internal + └──── private ip: 10.0.0.49 +``` + + + +In this case, the instance name is `vnc-browser`. +The address is different for each run. + +Open the provided address in a browser to reach the noVNC landing page. +From there, click **Connect** to open the remote desktop, which ships with Firefox and a minimal Linux GUI. + +At any time, you can list information about the instance: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra vnc-browser standby /novnc-browser 4.0GiB 1 weathered-fog-y5jjmwfd.fra.unikraft.app 2 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +vnc-browser weathered-fog-y5jjmwfd.fra.unikraft.app standby standby oci://unikraft.io//novnc-browser@sha256:... 4.0 GiB 1 7.17 ms +``` + + + +When done, you can remove the instance: + + + +```bash title="unikraft" +unikraft instances delete vnc-browser +``` + +```bash title="kraft" +kraft cloud instance remove vnc-browser +``` + + + +### Customize your deployment + +The current deployment ships with Firefox, a tiling panel, and a small set of X11 utilities on top of Ubuntu. +Customizing the deployment means editing the `Dockerfile` to add the apps your users need, such as a different browser, an editor, or developer tools. +You can also adjust the screen geometry through the `WIDTH`, `HEIGHT`, and `DISPLAY_NUM` environment variables in `wrapper.sh`. + +[Anthropic's Computer Use Demo](https://github.com/anthropics/claude-quickstarts/tree/main/computer-use-demo) inspired this example, which works well as a sandbox for computer-use automation that needs to drive a real desktop. + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/kraft/overview). diff --git a/pages/use-cases/remote-ides.mdx b/pages/use-cases/remote-ides.mdx index ae1fb757..9300ee91 100644 --- a/pages/use-cases/remote-ides.mdx +++ b/pages/use-cases/remote-ides.mdx @@ -5,19 +5,19 @@ navigation_icon: square-chevron-right Remote Integrated Development Environments (IDEs) have become essential for cloud-native development, collaboration, and education. But traditional setups often suffer from **slow startup times, high idle costs, and complex resource management**. -With **Unikraft Cloud**, remote IDEs gain the advantages of **unikernels**, making them **fast, secure, and truly serverless**. +With **Unikraft Cloud**, remote IDEs gain the advantages of **microVMs**, making them **fast, secure, and truly serverless**. ## Why run remote IDEs on Unikraft Cloud ### ⚡ Instant developer environments -Unikraft unikernels boot in milliseconds, enabling **instant spin-up of full-featured IDEs** on demand. +Unikraft microVMs boot in milliseconds, enabling **instant spin-up of full-featured IDEs** on demand. Developers start coding immediately without waiting for heavy VMs or containers to initialise. ### 🔒 Secure, isolated workspaces -Each IDE runs as a **dedicated unikernel instance**, ensuring strong workload isolation and reducing the risk of cross-tenant data leaks. +Each IDE runs as a **dedicated microVM instance**, ensuring strong workload isolation and reducing the risk of cross-tenant data leaks. Sensitive code stays protected in a **minimal, hardened environment**. @@ -34,7 +34,7 @@ Perfect for **pair programming, CI/CD pipelines, or education platforms**. ### 🚀 High performance with low overhead -Unikernels run closer to bare metal than containers, providing developers with +MicroVMs run closer to bare metal than containers, providing developers with **snappy, low-latency environments** that feel as fast as local coding—without sacrificing portability or scalability. @@ -48,28 +48,47 @@ When not in use, the instance will [scale-to-zero](/docs/features/scale-to-zero) To run it, follow these steps: -1. Install the [`kraft` CLI tool](/install) and a container runtime engine (for example, Docker). +1. Install the [`kraft` CLI tool](https://unikraft.org/docs/cli/install) and a container runtime engine (for example, [Docker](https://docs.docker.com/engine/install/)). -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and `cd` into the `examples/code-server/` directory: +1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/code-server/` directory: ```bash -git clone https://github.com/kraftcloud/examples +git clone https://github.com/unikraft-cloud/examples cd examples/code-server/ ``` -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. -This guide uses `fra` (Frankfurt, 🇩🇪): +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪). -```bash + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" # Set Unikraft Cloud access token export UKC_TOKEN=token # Set metro to Frankfurt, DE export UKC_METRO=fra ``` + + +The `UKC_TOKEN` and `UKC_METRO` environment variables are only supported by the legacy CLI. + When done, invoke the following command to deploy this app on Unikraft Cloud: -```bash + + +```bash title="unikraft" +unikraft volumes create \ + --set name=code-workspace \ + --set size=1024 \ + --set metro=fra +``` + +```bash title="kraft" kraft cloud volume create \ --name code-workspace \ --size 1Gi @@ -78,9 +97,9 @@ kraft cloud deploy \ --scale-to-zero on \ --scale-to-zero-stateful \ --scale-to-zero-cooldown 4s \ - -M 2048 \ --name code-server \ -p 443:8443 \ + -M 2Gi \ -v code-workspace:/workspace \ -e PGUID=0 \ -e PGID=0 \ @@ -90,14 +109,42 @@ kraft cloud deploy \ . ``` + + Now, you can access the Code Server in the browser, at the provided address. ### Volume This deployment creates a volume for data persistence: `code-workspace`. -Upon deleting the instance (for example, `kraft cloud instance rm code-server`), this volume will persist, allowing you to create another instance without losing data. +Upon deleting the instance, this volume will persist, allowing you to create another instance without losing data. To remove the volume, you can use: -```bash + + +```bash title="unikraft" +unikraft volumes delete code-workspace +``` + +```bash title="kraft" kraft cloud volume rm code-workspace ``` + + + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/kraft/overview). diff --git a/pages/use-cases/sandboxes.mdx b/pages/use-cases/sandboxes.mdx new file mode 100644 index 00000000..8680a239 --- /dev/null +++ b/pages/use-cases/sandboxes.mdx @@ -0,0 +1,316 @@ +--- +title: Sandboxes +navigation_icon: box +--- + +Sandboxes give you on-demand, isolated execution environments for any workload. +Spin up a microVM in milliseconds to run short-lived tasks such as executing code, processing data, or responding to an event. +Then let it vanish automatically. + +Or keep a sandbox running in the background for as long as a job requires. +Either way, every sandbox runs in its own dedicated microVM: hardware-isolated, minimal, and ready for the job. + +With **Unikraft Cloud**, you get the instant startup speed of containers together with the strong isolation guarantees of hardware virtualization—not one or the other. + +## Why run sandboxes on Unikraft Cloud + +### ⚡ Instant startup, every time + +Sandboxes need to be ready the moment a task arrives. +Unikraft Cloud instances boot in milliseconds: + +* Ephemeral sandboxes spin up and complete tasks before cold starts are even noticeable. +* Long-running sandboxes resume instantly from stateful scale-to-zero snapshots. +* No pre-warming pools or reserved capacity needed. + +### 🔒 True hardware isolation + +{/* vale off */} + +Running arbitrary or untrusted workloads such as user-submitted code, AI agent tool calls, third-party plugins, test payloads, etc. demands more than container-level isolation. + +{/* vale on */} + +Every sandbox on Unikraft Cloud runs in its own microVM: + +* No shared kernel between tenants. Each sandbox has its own. +* Hardware-enforced boundaries reduce the risk of container escape. +* A minimal OS footprint reduces the attack surface for every run. + +### 💸 Pay only for execution + +Sandboxes are inherently bursty. +Some complete in milliseconds while others run for hours. +Unikraft Cloud's pricing model fits: + +* You pay only for the actual execution time of ephemeral sandboxes. +* Long-running sandboxes scale-to-zero when idle and resume instantly when needed. +* No idle infrastructure, no wasted capacity between runs. + +## Getting started + +{/* vale off */} + +A great example of sandboxes in action is [OpenClaw](https://openclaw.ai/), an autonomous AI agent framework that runs in an isolated microVM on Unikraft Cloud. + +{/* vale on */} + +This guide explains how to create and deploy your own OpenClaw gateway on Unikraft Cloud. +To run this example, follow these steps: + +1. Install the CLI and a container runtime engine (for example, [Docker](https://docs.docker.com/engine/install/)). + Use the [unikraft CLI](/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/). + You could also directly set up and use BuildKit, see the [quick start](https://github.com/moby/buildkit#quick-start). + +1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/openclaw` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/openclaw/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +The `UKC_TOKEN` and `UKC_METRO` environment variables are only supported by the legacy CLI. + +When done, you may create the OpenClaw Unikraft Cloud image and deploy an instance from it like so: + + + +```bash title="unikraft" +unikraft build . --output /openclaw:latest +unikraft run --scale-to-zero policy=on,cooldown-time=10000 --metro fra -p 18789:18789/tls -p 2222:2222/tls -m 4G -e PUBKEY="...." --image /openclaw:latest +``` + +```bash title="kraft" +kraft cloud deploy \ + --scale-to-zero on \ + --scale-to-zero-cooldown 10s \ + -p 18789:18789/tls \ + -p 2222:2222/tls \ + -M 4Gi \ + -e PUBKEY="..." \ + . +``` + + + +Make sure to replace `` with your username / org-name and to set your SSH public key as the `PUBKEY` environment variable above. + +The output shows the instance address and other details: + + + +```ansi title="unikraft" +metro: fra +name: openclaw-8tosm +uuid: e2a6183a-721b-4145-bfaf-37a5f859bbc1 +state: starting +image: demo/openclaw +runtime: + env: + PUBKEY: * +resources: + memory: 4GiB + vcpus: 1 +service: + uuid: 7ab20338-b04d-4869-947b-9433e21677b1 + name: divine-flower-bxsaapup + domains: + - fqdn: divine-flower-bxsaapup.fra0-demo.unikraft.app +networks: +- uuid: 2b0b120b-6ce5-4b19-ac4c-04ee8f11526e + private-ip: 10.0.12.97 + mac: 12:b0:0a:00:0c:61 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: openclaw-8tosm + ├───────── uuid: e2a6183a-721b-4145-bfaf-37a5f859bbc1 + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├──────── image: demo/openclaw@sha256:7a3d9f2b5e8c1a4d7f0e3b6c9a2d5f8b1e4a7d0c3f6b9e2a5d8c1f4b7e0a3d6c9 + ├─────── memory: 4096 MiB + ├─ private fqdn: openclaw-8tosm.internal + └─── private ip: 10.0.12.97 +``` + + + +In this case, the instance name is `openclaw-8tosm` and the address is `divine-flower-bxsaapup.fra0-demo.unikraft.app`. +These will be different for each run. + +You can now SSH into this instance and run the OpenClaw onboarding process. + +To SSH, you need to set up a tunnel that handles the TLS connection to the Unikraft Cloud instance. +This way, you have a non-TLS port that your SSH client can connect to: + +```bash +socat TCP-LISTEN:2222,reuseaddr,fork OPENSSL:divine-flower-bxsaapup.fra0-demo.unikraft.app:2222,verify=0 +``` + +Then connect to the instance via SSH using: + +```bash +ssh -l root localhost -p 2222 +``` + +You can list information about the instance by running: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED +fra openclaw-8tosm starting demo/openclaw 4096MiB 1 just now +``` + +```ansi title="kraft" +NAME IMAGE ARGS CREATED AT +openclaw-8tosm demo/openclaw 20 seconds ago +``` + + + +When done, you can remove the instance using: + + + +```bash title="unikraft" +unikraft instances delete openclaw-8tosm +``` + +```bash title="kraft" +kraft cloud instance remove openclaw-8tosm +``` + + + +{/* vale off */} + +## OpenClaw Setup + +{/* vale on */} + +Once you have SSH'd into your instance, you may run: + +```bash +openclaw onboard +``` + +This will set up your OpenClaw gateway on the instance. +You will have to provide your LLM's API key here. + +Once done, make note of your `gateway.auth.token` (henceforth referenced as ``) from `~/.openclaw/openclaw.json` + +```bash +cat ~/.openclaw/openclaw.json +``` + +Set `gateway.controlUi.allowedOrigins` in `~/.openclaw/openclaw.json`: + +```json +... + "gateway": { + ... + "controlUi": { + "allowedOrigins": [ + "https://proud-smoke-cjf0wro8.fra0-demo.unikraft.app:18789" + ] + }, + ... + }, +... +``` + +Replace the address above with the address of your instance (noted earlier). + +Run the gateway: + +```bash +openclaw gateway run --bind lan +``` + +You may now access the web dashboard at the following address: + +```ansi +https://
:18789?token= +``` + +Where `
` is your above noted address and `` is your above noted token. + +For security reasons, you will have to manually approve your web "device" to start using the web dashboard. +Create a new SSH connection to your OpenClaw instance: + +```bash +ssh -l root localhost -p 2222 +``` + +First, find your device ID: + +```bash +openclaw devices list +``` + +Look under the `Request` column. +Device IDs look like `cabd915e-137a-4bc4-b640-d0e507684d65` + +Finally, approve your device with: + +```bash +openclaw devices approve +``` + +Once you approve your device, refresh your OpenClaw web dashboard. + +You now have full access to your own OpenClaw deployment on Unikraft Cloud! + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/overview). diff --git a/pages/use-cases/serverless-databases.mdx b/pages/use-cases/serverless-databases.mdx index a368b225..74c0be50 100644 --- a/pages/use-cases/serverless-databases.mdx +++ b/pages/use-cases/serverless-databases.mdx @@ -3,27 +3,25 @@ title: Serverless Databases navigation_icon: database-zap --- -import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs" - Databases are central to modern apps, but they're notoriously resource-hungry, slow to scale, and costly to operate at low utilisation. Traditional VM- or container-based solutions keep idle resources running, resulting in waste and inefficiency. -With **Unikraft Cloud**, serverless databases gain the advantages of **unikernels**, combining **extreme performance** with **true scale-to-zero efficiency**. +With **Unikraft Cloud**, serverless databases gain the advantages of **microVMs**, combining **extreme performance** with **true scale-to-zero efficiency**. ## Why run serverless databases on Unikraft Cloud ### ⚡ Instant boot, millisecond latency -Unikraft unikernels start in milliseconds, enabling databases to **scale up instantly** when requests arrive and **shut down to zero** when idle. +Unikraft microVMs start in milliseconds, enabling databases to **scale up instantly** when requests arrive and **shut down to zero** when idle. This ensures the system uses resources **only when needed**, without cold-start penalties. ### 🔒 Stronger security by design -Each database instance runs as a **specialised, single-process unikernel**, drastically reducing the attack surface compared to general-purpose OSes. +Each database instance runs as a **specialised, single-process microVM**, drastically reducing the attack surface compared to general-purpose OSes. This isolation protects customer data with **built-in, lightweight security**. ### 💰 Cost-efficient scale-to-zero -Unikraft's unikernel architecture allows serverless databases to **consume no resources when idle** and scale to thousands of instances under load. +Unikraft's microVM architecture allows serverless databases to **consume no resources when idle** and scale to thousands of instances under load. You pay only for active queries, making it ideal for **spiky workloads, dev/test environments, and SaaS platforms**. ### 🛠 Seamless serverless integration @@ -33,54 +31,69 @@ Databases can integrate with API gateways, event triggers, and headless services ### 📈 Performance without trade-offs -Unlike traditional serverless platforms where databases often suffer from high overhead, unikernel-based instances deliver **bare-metal-like throughput and predictable latency**. +Unlike traditional serverless platforms where databases often suffer from high overhead, microVM-based instances deliver **bare-metal-like throughput and predictable latency**. These instances optimise both Online Transaction Processing (OLTP) and Online Analytical Processing (OLAP) workloads. +### 📸 Checkpoints for point-in-time recovery + +**Checkpointing** saves the complete state of a running microVM at any time, **without interrupting execution**. +Checkpoints are incremental, fast, and keep a history, so you can restore the database to any previous checkpoint to recover from a failed migration or an erroneous write. + +### 🔀 Instance branching for instant clones + +Unikraft Cloud lets you **branch** a running instance, creating an identical, **independent copy in milliseconds** while leaving the original untouched. +Test your migrations or destructive changes on a byte-for-byte copy of production before applying them, so you can proceed with confidence. ## Getting started This guide shows you how to use [PostgreSQL](https://www.postgresql.org/), a powerful, open source object-relational database system, in a serverless fashion. -With Unikraft Cloud, you can run PostgreSQL in a lightweight virtual machine, with minimal overhead and millisecond fast startup times. -The instance will [scale-to-zero](/docs/features/scale-to-zero) when not in use. +With Unikraft Cloud, you can run PostgreSQL in a lightweight virtual machine, with minimal overhead and millisecond-fast startup times. +The instance will [scale-to-zero](/features/scale-to-zero) when not in use. To run it, follow these steps: -1. Install the [`kraft` CLI tool](/install) and a container runtime engine (for example, Docker). +1. Install the [`kraft` CLI tool](https://unikraft.org/docs/cli/install) and a container runtime engine (for example, [Docker](https://docs.docker.com/engine/install/)). -1. Clone the [`examples` repository](https://github.com/kraftcloud/examples) and +1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/postgres/` directory: ```bash -git clone https://github.com/kraftcloud/examples +git clone https://github.com/unikraft-cloud/examples cd examples/postgres/ ``` -Make sure to log into Unikraft Cloud by setting your token and a [metro](/metros#available) close to you. +Make sure to log into Unikraft Cloud by setting your token and a [metro](/platform/metros) close to you. This guide uses `fra` (Frankfurt, 🇩🇪): -```bash + + +```bash title="unikraft" +unikraft login +unikraft build . --output /postgres:latest +unikraft run --metro=fra -e POSTGRES_PASSWORD=unikraft -p 5432:5432/tls -m 1Gi --image=/postgres:latest +``` + +```bash title="kraft" # Set Unikraft Cloud access token export UKC_TOKEN=token # Set metro to Frankfurt, DE export UKC_METRO=fra +kraft cloud deploy -e POSTGRES_PASSWORD=unikraft -p 5432:5432/tls -M 1Gi . ``` -When done, invoke the following command to deploy this app on Unikraft -Cloud: + -```bash -kraft cloud deploy -e POSTGRES_PASSWORD=unikraft -p 5432:5432/tls -M 1024 . -``` +The `UKC_TOKEN` and `UKC_METRO` environment variables are only supported by the legacy CLI. The output shows the instance address and other details: ```ansi -[90m[[0m[92m●[0m[90m][0m Deployed successfully! - [90m│[0m - [90m├[0m[90m──────────[0m [90mname[0m: postgres-saan9 - [90m├[0m[90m──────────[0m [90muuid[0m: 3a1371f2-68c6-4187-84f8-c080f2b028ca - [90m├[0m[90m─────────[0m [90mstate[0m: [92mstarting[0m - [90m├[0m[90m──────────[0m [90mfqdn[0m: young-thunder-fbafrsxj.fra.unikraft.app +[●] Deployed successfully! + │ + ├────────── name: postgres-saan9 + ├────────── uuid: 3a1371f2-68c6-4187-84f8-c080f2b028ca + ├───────── state: starting + ├────────── fqdn: young-thunder-fbafrsxj.fra.unikraft.app ├───────── image: postgres@sha256:2476c0373d663d7604def7c35ffcb4ed4de8ab231309b4f20104b84f31570766 ├──────── memory: 1024 MiB ├─────── service: young-thunder-fbafrsxj @@ -92,7 +105,7 @@ The output shows the instance address and other details: In this case, the instance name is `postgres-saan9` and the service `young-thunder-fbafrsxj`. They're different for each run. -If you use port 5432/tls according to the example above you can now directly connect to postgres: +If you use port `5432/tls`, according to the example above, you can now directly connect to postgres: ```console psql -U postgres -h young-thunder-fbafrsxj.fra.unikraft.app @@ -114,19 +127,18 @@ postgres=# Use SQL and `psql` commands for your work. :::tip[Idle scale-to-zero] -This example uses the [`idle` scale-to-zero policy](/docs/api/v1/instances#scaletozero_policy) by default (see the `labels` section in the `Kraftfile`). +This example uses the [`idle` scale-to-zero policy](/features/scale-to-zero) by default (see the `labels` section in the `Kraftfile`). This means that the instance will use scale-to-zero even in the presence of `psql` connections. The PostgreSQL example makes use of scale-to-zero app support. This ensures that the instance isn't put into standby even for long running queries (during which the connections are also idle). -To this end, the [`pg_ukc_scaletozero`](https://github.com/kraftcloud/pg_ukc_scaletozero) module is loaded into Postgres. +To this end, the [`pg_ukc_scaletozero`](https://github.com/unikraft-cloud/pg_ukc_scaletozero) module is loaded into Postgres. This module suspends scale-to-zero during query processing. You can see this in action by running `SELECT pg_sleep(10);` and verifying that the instance keeps on running. ::: :::note -If you'd like to use a port other than `5432/tls` you'll need to use the `kraft cloud tunnel` command to connect to Postgres. -See [the tunneling guide](/docs/guides/features/tunnel) for more information. +If you'd like to use a port other than `5432/tls`, you'll need to use the legacy [CLI tunnel](/docs/cli/kraft/tunnel) command to connect to Postgres. You need to explicitly disable scale-to-zero. You can do this by either changing the label in the `Kraftfile` or by using `--scale-to-zero off` in the deploy command. @@ -134,10 +146,18 @@ You can do this by either changing the label in the `Kraftfile` or by using `--s At any time, you can list information about the instance: -```bash + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" kraft cloud instance list ``` + + ```text NAME FQDN STATE CREATED AT IMAGE MEMORY ARGS BOOT TIME postgres-saan9 young-thunder-fbafrsxj.fra.unikraft.app running 6 minutes ago postgres@sha256:2476c0373d663d7604d... 1.0 GiB wrapper.sh docker-entrypoint.sh postgres 603.42 ms @@ -145,26 +165,91 @@ postgres-saan9 young-thunder-fbafrsxj.fra.unikraft.app running 6 minutes ag When done, you can remove the instance: -```bash + + +```bash title="unikraft" +unikraft instances delete postgres-saan9 +``` + +```bash title="kraft" kraft cloud instance remove postgres-saan9 ``` + + ### Using volumes -You can use [volumes](/docs/guides/features/volumes) for data persistence for your PostgreSQL instance. +You can use [volumes](/platform/volumes) for data persistence for your PostgreSQL instance. For that you would first create a volume: -```console + + +```console title="unikraft" +unikraft volumes create \ + --set name=postgres \ + --set size=200 \ + --set metro=fra +``` + +```console title="kraft" kraft cloud volume create --name postgres --size 200 ``` + + Then start the PostgreSQL instance and mount that volume: -```console -kraft cloud deploy -e POSTGRES_PASSWORD=unikraft -e PGDATA=/volume/postgres -v postgres:/volume -p 5432:5432/tls -M 1024 . + + +```console title="unikraft" +unikraft build . --output /postgres:latest +unikraft run --metro=fra -p 5432:5432/tls -m 1Gi -e POSTGRES_PASSWORD=unikraft -e PGDATA=/volume/postgres -v postgres:/volume --image=/postgres:latest +``` + +```console title="kraft" +kraft cloud deploy -p 5432:5432/tls -M 1Gi -e POSTGRES_PASSWORD=unikraft -e PGDATA=/volume/postgres -v postgres:/volume . ``` + + +### Using checkpoints + +:::note +Checkpointing is coming soon. +Full documentation will follow once the interface is available. +::: + +### Using instance branching + +:::note +Instance branching is a preview feature and isn't yet available on stable. +The interface described here reflects the current implementation and may change before general availability. +::: + +You can branch a running PostgreSQL instance to create an independent copy with the exact same state. +The source instance keeps running unaffected. + +To create a branch, pass `--branch` with the source instance name to `unikraft run`: + + +```console title="unikraft" +unikraft run --metro=fra -p 5432:5432/tls --branch +``` + + +The branch boots immediately from the same state as the source. +Connect to it like any other instance. +Whatever you do, the source instance remains unaffected. + +When you're done, remove the branch: + + +```console title="unikraft" +unikraft instances delete +``` + + ### Customize your deployment Your deployment is a standard PostgreSQL installation. @@ -175,7 +260,7 @@ For that you use a different `POSTGRES_PASSWORD` environment variable when start You could also use a different location to mount your volume or set more configuration options. -And, you can use the PostgreSQL instance in conjunction with a frontend service, [see the guide here](/docs/guides/features/idns). +And, you can use the PostgreSQL instance in conjunction with a frontend service, [see the guide here](/platform/services). But, in that case make sure to disable scale-to-zero if you plan to use the DB internally. :::note @@ -189,8 +274,16 @@ Support for scale-to-zero for internal instances is coming soon. Use the `--help` option for detailed information on using Unikraft Cloud: -```bash + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" kraft cloud --help ``` -Or visit the [CLI Reference](/docs/cli). + + +Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/kraft/overview). diff --git a/pages/use-cases/serverless-functions.mdx b/pages/use-cases/serverless-functions.mdx new file mode 100644 index 00000000..65fa470d --- /dev/null +++ b/pages/use-cases/serverless-functions.mdx @@ -0,0 +1,431 @@ +--- +title: Serverless Functions +navigation_icon: function-square +--- + +Serverless functions let you deploy small pieces of business logic without managing servers or runtimes. +They're ideal for event-driven pipelines, API backends, and dynamic code execution—but traditional function-as-a-service platforms often suffer from cold-start penalties and opaque sandboxing. + +With **Unikraft Cloud**, you can run serverless functions inside microVMs: instant availability, VM-level isolation, and true scale-to-zero economics. +The ROM (Read-Only Memory) model goes one step further—package your function code independently of the runtime image, and swap it in at instance creation time. + +## Why run serverless functions on Unikraft Cloud + +### ⚡ Millisecond cold starts with instance templates + +Unikraft Cloud's [instance templates](/platform/instances#instance-templates) pre-initialize a runtime once, then stamp out new instances from that snapshot. +Combined with stateful scale-to-zero, instances resume from a warm snapshot rather than booting from scratch: + +* Single-digit-millisecond wake-ups, even under burst traffic. +* No re-execution of startup logic between requests. + +### 🔒 Strong isolation per function + +Every function instance runs in its own microVM. +No shared kernel, no noisy-neighbour risk, and no container escape surface. +Sensitive business logic stays protected even when other tenants share the same platform. + +### 💸 Pay only for execution + +Functions are inherently bursty. +Scale-to-zero means an idle function costs nothing. +The ROM model makes this even leaner: you package the base runtime image once, and only the lightweight function ROM changes per deployment. + +### 🔄 Decouple runtime from function code + +ROMs separate the runtime image (Node.js, Python, etc.) from the function payload: + +* Update function code by pushing a new ROM—no runtime rebuild required. +* Run many specialised functions from a single base image. +* Instant rollback by pointing an instance at a previous ROM version. + +## Getting started + +This guide uses the [`node-code-execution`](https://github.com/unikraft-cloud/examples/tree/main/node-code-execution) example. +It deploys a Node.js runtime as the base image, then attaches JavaScript or TypeScript function ROMs to instances of that runtime. + +### Prerequisites + +1. Install the CLI: + Use the [unikraft CLI](/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/). + +1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/node-code-execution` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/node-code-execution/ +``` + +Make sure to log into Unikraft Cloud and pick a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + + + +```bash title="unikraft" +unikraft login +``` + +```bash title="kraft" +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + + + +### Step 1: Package and push the base runtime image + +The base image contains a Node.js server (`server.ts`) that loads a function from the attached ROM and executes it on each HTTP request. +Right before starting the server, it writes `1` to `/uk/libukp/template_instance`, which triggers Unikraft Cloud to convert the running instance into a [template](/platform/instances#instance-templates). + + + +```bash title="unikraft" +unikraft build . --output /node-code-exec:latest +``` + +```bash title="kraft" +kraft pkg \ + --name index.unikraft.io//node-code-exec:latest \ + --plat kraftcloud \ + --arch x86_64 \ + --rootfs-type erofs \ + --push \ + . +``` + + + +### Step 2: Create a template from the base image + +Boot a short-lived instance from the base image without any ROM attached. +The instance self-converts into a template and exits: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name node-exec \ + -m 512M \ + --image /node-code-exec:latest +``` + +```bash title="kraft" +kraft cloud instance create \ + --start \ + --name node-exec \ + -M 512Mi \ + /node-code-exec:latest +``` + + + +The output shows the instance details: + + + +```ansi title="unikraft" +metro: fra +name: node-exec +uuid: 96608ed2-45e0-4c8f-8269-5d8cd3e4b41a +state: starting +image: /node-code-exec +resources: + memory: 512MiB + vcpus: 1 +networks: +- uuid: 6f7a8b9c-0d1e-2f3a-4b5c-f6a7b8c9d0e1 + private-ip: 10.0.5.4 + mac: 12:b0:6c:3e:ab:95 +timestamps: + created: just now +``` + +```ansi title="kraft" +[●] Deployed successfully! + │ + ├───────── name: node-exec + ├───────── uuid: 96608ed2-45e0-4c8f-8269-5d8cd3e4b41a + ├──────── metro: https://api.fra.unikraft.cloud/v1 + ├──────── state: starting + ├──────── image: oci://unikraft.io//node-code-exec@sha256:71487fd6196987cf65fb89eb84405cb796677aba177dabacf391f09618313328 + ├─────── memory: 512 MiB + ├─ private fqdn: node-exec.internal + └─── private ip: 10.0.5.4 +``` + + + +This instance is short-lived, since right before the server starts, it triggers a conversion into a template. +To confirm the template is ready: + + + +```bash title="unikraft" +unikraft instances templates list +``` + +```bash title="kraft" +kraft cloud instance template list +``` + + + +
+ + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS CREATED +fra node-exec template /node-code-exec 512MiB 1 just now +``` + +```ansi title="kraft" +NAME IMAGE ARGS CREATED AT +node-exec oci://unikraft.io//node-code-exec@sha256:71487fd6196987cf65fb89eb84405cb796677aba177dabacf391f09618313328 20 seconds ago +``` + + + + +### Step 3: Package and push the function ROMs + +ROMs package only the function source files—no runtime needed. +Package both example functions: + + + +```bash title="unikraft" +unikraft build rom1/ --output /node-rom1:latest +unikraft build rom2/ --output /node-rom2:latest +``` + +```bash title="kraft" +kraft pkg \ + --rom ./fs \ + --rom-type erofs \ + --plat kraftcloud \ + --arch x86_64 \ + --name index.unikraft.io//node-rom1:latest \ + --push \ + rom1/ +kraft pkg \ + --rom ./fs \ + --rom-type erofs \ + --plat kraftcloud \ + --arch x86_64 \ + --name index.unikraft.io//node-rom2:latest \ + --push \ + rom2/ +``` + + + +### Step 4: Create instances from the template with ROMs attached + +Stamp out instances from the template, each with a different ROM. +New instances skip the cold-start initialization phase because they restore from the template snapshot. + +Create the first instance with the JavaScript ROM attached: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name node-exec-rom1 \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ + --rom image=/node-rom1:latest,at=/rom \ + --template node-exec +``` + +```bash title="kraft" +# kraft does not support creating instances with attached ROMs, but you can use the API directly +curl -X POST "$UKC_METRO/instances" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "node-exec-rom1", + "template": { + "name": "node-exec" + }, + "autostart": true, + "service_group": { + "services": [ + { + "port": 443, + "destination_port": 8080, + "handlers": ["tls", "http"] + } + ] + }, + "scale_to_zero": { + "policy": "on", + "stateful": true, + "cooldown_time_ms": 1000 + }, + "roms": [ + { + "name": "js_function", + "image": "index.unikraft.io//node-rom1:latest", + "at": "/rom" + } + ] +}' +``` + + + +Create the second instance with the TypeScript ROM attached: + + + +```bash title="unikraft" +unikraft run --metro fra \ + --name node-exec-rom2 \ + -p 443:8080/tls+http \ + --scale-to-zero policy=on,cooldown-time=1000,stateful=true \ + --rom image=/node-rom2:latest,at=/rom \ + --template node-exec +``` + +```bash title="kraft" +# kraft does not support creating instances with attached ROMs, but you can use the API directly +curl -X POST "$UKC_METRO/instances" \ + -H "Accept: application/json" \ + -H "Authorization: Bearer $UKC_TOKEN" \ + -H "Content-Type: application/json" \ + -d '{ + "name": "node-exec-rom2", + "template": { + "name": "node-exec" + }, + "autostart": true, + "service_group": { + "services": [ + { + "port": 443, + "destination_port": 8080, + "handlers": ["tls", "http"] + } + ] + }, + "scale_to_zero": { + "policy": "on", + "stateful": true, + "cooldown_time_ms": 1000 + }, + "roms": [ + { + "name": "ts_function", + "image": "index.unikraft.io//node-rom2:latest", + "at": "/rom" + } + ] +}' +``` + + + +Both instances run the same base Node.js runtime but execute different function code—without any rebuild of the runtime image. + +List the instances and note their FQDN values: + + + +```bash title="unikraft" +unikraft instances list +``` + +```bash title="kraft" +kraft cloud instance list +``` + + + +
+ + + +```ansi title="unikraft" +METRO NAME STATE IMAGE ARGS MEMORY VCPUS FQDN CREATED +fra node-exec-rom2 standby /node-code-exec 512MiB 1 nameless-wood-gw7pbnls.fra.unikraft.app 2 minutes ago +fra node-exec-rom1 standby /node-code-exec 512MiB 1 sparkling-dawn-syowlbtj.fra.unikraft.app 3 minutes ago +``` + +```ansi title="kraft" +NAME FQDN STATE STATUS IMAGE MEMORY VCPUS ARGS BOOT TIME +node-exec-rom2 nameless-wood-gw7pbnls.fra.unikraft.app standby standby oci://unikraft.io//node-code-exec@sha256:71487f... 512 MiB 1 6.98 ms +node-exec-rom1 sparkling-dawn-syowlbtj.fra.unikraft.app standby standby oci://unikraft.io//node-code-exec@sha256:71487f... 512 MiB 1 7.86 ms +``` + + + +Test both instances using the FQDNs from the listing: + +```bash +curl https://sparkling-dawn-syowlbtj.fra.unikraft.app +curl https://nameless-wood-gw7pbnls.fra.unikraft.app +``` + +```text +Bye, World! +Auf Wiedersehen! +``` + +## How it works + +The ROM deployment model has three layers: + +1. **Base runtime image**: A generic Node.js server that loads and executes code from a well-known path (`/rom/rom.js` or `/rom/rom.ts`). + Push it once; it changes infrequently. + +1. **Function ROM**: A read-only filesystem image containing only the function source file. + Lightweight, fast to build and push. + Attached to an instance at creation time and mounted at `/rom`. + +1. **Instance template**: A pre-initialized snapshot of the runtime, created automatically when the base image boots without a ROM. + New instances boot from this snapshot, skipping initialization and reducing cold-start latency to milliseconds. + +## Cleanup + + + +```bash title="unikraft" +unikraft instances delete node-exec-rom1 node-exec-rom2 +unikraft instances template delete node-exec +``` + +```bash title="kraft" +kraft cloud instance remove node-exec-rom1 node-exec-rom2 +kraft cloud instance template remove node-exec +``` + + + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="unikraft" +unikraft --help +``` + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/kraft/overview). + +For more on the ROM and template features: + +* [Instance templates](/platform/instances#instance-templates) +* [ROMs](/platform/instances#roms) +* [`node-code-execution` example](https://github.com/unikraft-cloud/examples/tree/main/node-code-execution) diff --git a/pages/use-cases/webhooks.mdx b/pages/use-cases/webhooks.mdx new file mode 100644 index 00000000..92234015 --- /dev/null +++ b/pages/use-cases/webhooks.mdx @@ -0,0 +1,111 @@ +--- +title: Webhooks +navigation_icon: plug +--- + +Webhooks, also known as reverse APIs, enable real-time, event-driven communication between services. +Instead of constantly polling for updates, webhooks allow services to push data to your app the moment something happens. +This could be when someone pushes a commit, processes a payment, or creates an issue. +This makes webhooks essential for building reactive, event-driven architectures. + +With **Unikraft Cloud**, you can deploy webhook receivers that combine instant availability with scale-to-zero economics—paying only when events arrive, while maintaining millisecond-level response times. + +## Why run webhook receivers on Unikraft Cloud + +### ⚡ Instant wake-up for bursty events + +Webhooks are inherently sporadic—events can arrive in bursts or be silent for hours. +Traditional servers waste resources running every day, waiting for events. +Unikraft Cloud's microVM architecture provides: + +* Single-digit-millisecond cold starts ensure instant response when events arrive. +* No noticeable delay for webhook senders, maintaining reliable integrations. +* Minimal memory footprint allows running many specialized webhook receivers. + +### 🔒 Isolated and secure + +Webhook receivers often process sensitive data from external services. +Unikraft Cloud provides VM-level isolation for each instance: + +* Strong isolation prevents cross-contamination between different webhook handlers. +* Built-in HTTPS endpoints with automatic TLS certificate management. + +### 💸 Pay only for events + +Why pay for idle servers when webhooks only fire occasionally? +With Unikraft Cloud: + +* Webhook receivers scale-to-zero when not processing events, costing nothing. +* Instant wake-up ensures the system processes events immediately upon arrival. +* Fine-grained billing means you only pay for actual event processing. + + +## Getting started + +This guide shows you how to deploy a GitHub webhook receiver that listens for repository events. +The example uses Node.js with Express and the official [Octokit webhooks library](https://www.npmjs.com/package/@octokit/webhooks). +To run it, follow these steps: + +1. Install the [`kraft` CLI tool](https://unikraft.org/docs/cli/install) and a container runtime engine (for example, [Docker](https://docs.docker.com/engine/install/)). + +1. Clone the [`examples` repository](https://github.com/unikraft-cloud/examples) and `cd` into the `examples/webhook-github-node/` directory: + +```bash +git clone https://github.com/unikraft-cloud/examples +cd examples/webhook-github-node/ +``` + +Make sure to log into Unikraft Cloud by setting your token and a [metro](/platform/metros) close to you. +This guide uses `fra` (Frankfurt, 🇩🇪): + +```bash +# Set Unikraft Cloud access token +export UKC_TOKEN=token +# Set metro to Frankfurt, DE +export UKC_METRO=fra +``` + +Follow the instructions in the example's README to set it up and grab the server's FQDN. +After that, check that the service is up: + +```bash +curl https:///health +``` + +```text +{"status":"healthy","timestamp":"2025-12-17T14:55:20.953Z","uptime":0.063799807} +``` + +To create a webhook in GitHub, follow the [official documentation](https://docs.github.com/en/webhooks/using-webhooks/creating-webhooks) and the indications in the example's README. + +The example includes: + +* **Signature validation** using GitHub's webhook secret (via Octokit library). +* **Event logging** with timestamps for debugging and auditing. +* **Health check** endpoint for monitoring and load balancer integration. +* **Scale-to-zero configuration** in the `Kraftfile`. + +To customize for other webhook sources ([Stripe](https://docs.stripe.com/webhooks), [Slack](https://docs.slack.dev/tools/node-slack-sdk/webhook/), [GitLab](https://docs.gitlab.com/user/project/integrations/webhooks/), etc.), it's best to refer to their specific libraries and signature validation methods. +Webhooks aren't a standardized protocol, so each service has its own conventions. +Also follow [Webhooks.fyi](https://webhooks.fyi/) for best practices. + +## Learn more + +Use the `--help` option for detailed information on using Unikraft Cloud: + + + +```bash title="kraft" +kraft cloud --help +``` + + + +Or visit the [CLI Reference](/docs/cli/unikraft) or the [legacy CLI reference](/docs/cli/kraft/overview). + +For more information, check out these resources: + +* [Express Documentation](https://expressjs.com/). +* [GitHub Webhooks Documentation](https://docs.github.com/en/webhooks). +* [Securing GitHub Webhooks](https://docs.github.com/en/webhooks/using-webhooks/validating-webhook-deliveries). +* [Webhook Best Practices](https://webhooks.fyi/). diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml new file mode 100644 index 00000000..cf306af4 --- /dev/null +++ b/pnpm-lock.yaml @@ -0,0 +1,8010 @@ +lockfileVersion: '9.0' + +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + +importers: + + .: + dependencies: + '@shikijs/vscode-textmate': + specifier: ^10.0.2 + version: 10.0.2 + hast-util-to-html: + specifier: ^9.0.5 + version: 9.0.5 + oniguruma-parser: + specifier: ^0.12.1 + version: 0.12.2 + react: + specifier: '>=19.0.0' + version: 19.2.5 + react-dom: + specifier: '>=19.0.0' + version: 19.2.5(react@19.2.5) + regex-utilities: + specifier: ^2.3.0 + version: 2.3.0 + zudoku: + specifier: ^0.77.0 + version: 0.77.0(@types/json-schema@7.0.15)(@types/node@25.6.0)(jiti@2.7.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.3)(use-sync-external-store@1.6.0(react@19.2.5)) + devDependencies: + '@typescript-eslint/eslint-plugin': + specifier: ^8.0.0 + version: 8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/parser': + specifier: ^8.0.0 + version: 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + eslint: + specifier: ^9.14.0 + version: 9.39.4(jiti@2.7.0) + +packages: + + '@apidevtools/json-schema-ref-parser@14.2.1': + resolution: {integrity: sha512-HmdFw9CDYqM6B25pqGBpNeLCKvGPlIx1EbLrVL0zPvj50CJQUHyBNBw45Muk0kEIkogo1VZvOKHajdMuAzSxRg==} + engines: {node: '>= 20'} + peerDependencies: + '@types/json-schema': ^7.0.15 + + '@apidevtools/json-schema-ref-parser@15.3.5': + resolution: {integrity: sha512-orNOYXw3hYXxxisXMldjzjBzqqTLBPbwOtHg7ovBPvfBHDue1qM9YJENZ3W2BQuS+7z4ThogMbEzEsov57Itkg==} + engines: {node: '>=20'} + peerDependencies: + '@types/json-schema': ^7.0.15 + + '@babel/runtime@7.29.2': + resolution: {integrity: sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==} + engines: {node: '>=6.9.0'} + + '@base-ui/react@1.4.1': + resolution: {integrity: sha512-Ab5/LIhcmL8BQcsBUYiOfkSDRdLpvgUBzMK30cu684JPcLclYlztharvCZyNNgzJtbAiREzI9q0pI5erHCMgCw==} + engines: {node: '>=14.0.0'} + peerDependencies: + '@date-fns/tz': ^1.2.0 + '@types/react': ^17 || ^18 || ^19 + date-fns: ^4.0.0 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@date-fns/tz': + optional: true + '@types/react': + optional: true + date-fns: + optional: true + + '@base-ui/utils@0.2.8': + resolution: {integrity: sha512-jvOi+c+ftGlGotNcKnzPVg2IhCaDTB6/6R3JeqdjdXktuAJi3wKH9T7+svuaKh1mmfVU11UWzUZVH74JDfi/wQ==} + peerDependencies: + '@types/react': ^17 || ^18 || ^19 + react: ^17 || ^18 || ^19 + react-dom: ^17 || ^18 || ^19 + peerDependenciesMeta: + '@types/react': + optional: true + + '@emnapi/core@1.9.2': + resolution: {integrity: sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==} + + '@emnapi/runtime@1.9.2': + resolution: {integrity: sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==} + + '@emnapi/wasi-threads@1.2.1': + resolution: {integrity: sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==} + + '@envelop/core@5.5.1': + resolution: {integrity: sha512-3DQg8sFskDo386TkL5j12jyRAdip/8yzK3x7YGbZBgobZ4aKXrvDU0GppU0SnmrpQnNaiTUsxBs9LKkwQ/eyvw==} + engines: {node: '>=18.0.0'} + + '@envelop/instrumentation@1.0.0': + resolution: {integrity: sha512-cxgkB66RQB95H3X27jlnxCRNTmPuSTgmBAq6/4n2Dtv4hsk4yz8FadA1ggmd0uZzvKqWD6CR+WFgTjhDqg7eyw==} + engines: {node: '>=18.0.0'} + + '@envelop/types@5.2.1': + resolution: {integrity: sha512-CsFmA3u3c2QoLDTfEpGr4t25fjMU31nyvse7IzWTvb0ZycuPjMjb0fjlheh+PbhBYb9YLugnT2uY6Mwcg1o+Zg==} + engines: {node: '>=18.0.0'} + + '@esbuild/aix-ppc64@0.28.0': + resolution: {integrity: sha512-lhRUCeuOyJQURhTxl4WkpFTjIsbDayJHih5kZC1giwE+MhIzAb7mEsQMqMf18rHLsrb5qI1tafG20mLxEWcWlA==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [aix] + + '@esbuild/android-arm64@0.28.0': + resolution: {integrity: sha512-+WzIXQOSaGs33tLEgYPYe/yQHf0WTU0X42Jca3y8NWMbUVhp7rUnw+vAsRC/QiDrdD31IszMrZy+qwPOPjd+rw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [android] + + '@esbuild/android-arm@0.28.0': + resolution: {integrity: sha512-wqh0ByljabXLKHeWXYLqoJ5jKC4XBaw6Hk08OfMrCRd2nP2ZQ5eleDZC41XHyCNgktBGYMbqnrJKq/K/lzPMSQ==} + engines: {node: '>=18'} + cpu: [arm] + os: [android] + + '@esbuild/android-x64@0.28.0': + resolution: {integrity: sha512-+VJggoaKhk2VNNqVL7f6S189UzShHC/mR9EE8rDdSkdpN0KflSwWY/gWjDrNxxisg8Fp1ZCD9jLMo4m0OUfeUA==} + engines: {node: '>=18'} + cpu: [x64] + os: [android] + + '@esbuild/darwin-arm64@0.28.0': + resolution: {integrity: sha512-0T+A9WZm+bZ84nZBtk1ckYsOvyA3x7e2Acj1KdVfV4/2tdG4fzUp91YHx+GArWLtwqp77pBXVCPn2We7Letr0Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [darwin] + + '@esbuild/darwin-x64@0.28.0': + resolution: {integrity: sha512-fyzLm/DLDl/84OCfp2f/XQ4flmORsjU7VKt8HLjvIXChJoFFOIL6pLJPH4Yhd1n1gGFF9mPwtlN5Wf82DZs+LQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [darwin] + + '@esbuild/freebsd-arm64@0.28.0': + resolution: {integrity: sha512-l9GeW5UZBT9k9brBYI+0WDffcRxgHQD8ShN2Ur4xWq/NFzUKm3k5lsH4PdaRgb2w7mI9u61nr2gI2mLI27Nh3Q==} + engines: {node: '>=18'} + cpu: [arm64] + os: [freebsd] + + '@esbuild/freebsd-x64@0.28.0': + resolution: {integrity: sha512-BXoQai/A0wPO6Es3yFJ7APCiKGc1tdAEOgeTNy3SsB491S3aHn4S4r3e976eUnPdU+NbdtmBuLncYir2tMU9Nw==} + engines: {node: '>=18'} + cpu: [x64] + os: [freebsd] + + '@esbuild/linux-arm64@0.28.0': + resolution: {integrity: sha512-RVyzfb3FWsGA55n6WY0MEIEPURL1FcbhFE6BffZEMEekfCzCIMtB5yyDcFnVbTnwk+CLAgTujmV/Lgvih56W+A==} + engines: {node: '>=18'} + cpu: [arm64] + os: [linux] + + '@esbuild/linux-arm@0.28.0': + resolution: {integrity: sha512-CjaaREJagqJp7iTaNQjjidaNbCKYcd4IDkzbwwxtSvjI7NZm79qiHc8HqciMddQ6CKvJT6aBd8lO9kN/ZudLlw==} + engines: {node: '>=18'} + cpu: [arm] + os: [linux] + + '@esbuild/linux-ia32@0.28.0': + resolution: {integrity: sha512-KBnSTt1kxl9x70q+ydterVdl+Cn0H18ngRMRCEQfrbqdUuntQQ0LoMZv47uB97NljZFzY6HcfqEZ2SAyIUTQBQ==} + engines: {node: '>=18'} + cpu: [ia32] + os: [linux] + + '@esbuild/linux-loong64@0.28.0': + resolution: {integrity: sha512-zpSlUce1mnxzgBADvxKXX5sl8aYQHo2ezvMNI8I0lbblJtp8V4odlm3Yzlj7gPyt3T8ReksE6bK+pT3WD+aJRg==} + engines: {node: '>=18'} + cpu: [loong64] + os: [linux] + + '@esbuild/linux-mips64el@0.28.0': + resolution: {integrity: sha512-2jIfP6mmjkdmeTlsX/9vmdmhBmKADrWqN7zcdtHIeNSCH1SqIoNI63cYsjQR8J+wGa4Y5izRcSHSm8K3QWmk3w==} + engines: {node: '>=18'} + cpu: [mips64el] + os: [linux] + + '@esbuild/linux-ppc64@0.28.0': + resolution: {integrity: sha512-bc0FE9wWeC0WBm49IQMPSPILRocGTQt3j5KPCA8os6VprfuJ7KD+5PzESSrJ6GmPIPJK965ZJHTUlSA6GNYEhg==} + engines: {node: '>=18'} + cpu: [ppc64] + os: [linux] + + '@esbuild/linux-riscv64@0.28.0': + resolution: {integrity: sha512-SQPZOwoTTT/HXFXQJG/vBX8sOFagGqvZyXcgLA3NhIqcBv1BJU1d46c0rGcrij2B56Z2rNiSLaZOYW5cUk7yLQ==} + engines: {node: '>=18'} + cpu: [riscv64] + os: [linux] + + '@esbuild/linux-s390x@0.28.0': + resolution: {integrity: sha512-SCfR0HN8CEEjnYnySJTd2cw0k9OHB/YFzt5zgJEwa+wL/T/raGWYMBqwDNAC6dqFKmJYZoQBRfHjgwLHGSrn3Q==} + engines: {node: '>=18'} + cpu: [s390x] + os: [linux] + + '@esbuild/linux-x64@0.28.0': + resolution: {integrity: sha512-us0dSb9iFxIi8srnpl931Nvs65it/Jd2a2K3qs7fz2WfGPHqzfzZTfec7oxZJRNPXPnNYZtanmRc4AL/JwVzHQ==} + engines: {node: '>=18'} + cpu: [x64] + os: [linux] + + '@esbuild/netbsd-arm64@0.28.0': + resolution: {integrity: sha512-CR/RYotgtCKwtftMwJlUU7xCVNg3lMYZ0RzTmAHSfLCXw3NtZtNpswLEj/Kkf6kEL3Gw+BpOekRX0BYCtklhUw==} + engines: {node: '>=18'} + cpu: [arm64] + os: [netbsd] + + '@esbuild/netbsd-x64@0.28.0': + resolution: {integrity: sha512-nU1yhmYutL+fQ71Kxnhg8uEOdC0pwEW9entHykTgEbna2pw2dkbFSMeqjjyHZoCmt8SBkOSvV+yNmm94aUrrqw==} + engines: {node: '>=18'} + cpu: [x64] + os: [netbsd] + + '@esbuild/openbsd-arm64@0.28.0': + resolution: {integrity: sha512-cXb5vApOsRsxsEl4mcZ1XY3D4DzcoMxR/nnc4IyqYs0rTI8ZKmW6kyyg+11Z8yvgMfAEldKzP7AdP64HnSC/6g==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openbsd] + + '@esbuild/openbsd-x64@0.28.0': + resolution: {integrity: sha512-8wZM2qqtv9UP3mzy7HiGYNH/zjTA355mpeuA+859TyR+e+Tc08IHYpLJuMsfpDJwoLo1ikIJI8jC3GFjnRClzA==} + engines: {node: '>=18'} + cpu: [x64] + os: [openbsd] + + '@esbuild/openharmony-arm64@0.28.0': + resolution: {integrity: sha512-FLGfyizszcef5C3YtoyQDACyg95+dndv79i2EekILBofh5wpCa1KuBqOWKrEHZg3zrL3t5ouE5jgr94vA+Wb2w==} + engines: {node: '>=18'} + cpu: [arm64] + os: [openharmony] + + '@esbuild/sunos-x64@0.28.0': + resolution: {integrity: sha512-1ZgjUoEdHZZl/YlV76TSCz9Hqj9h9YmMGAgAPYd+q4SicWNX3G5GCyx9uhQWSLcbvPW8Ni7lj4gDa1T40akdlw==} + engines: {node: '>=18'} + cpu: [x64] + os: [sunos] + + '@esbuild/win32-arm64@0.28.0': + resolution: {integrity: sha512-Q9StnDmQ/enxnpxCCLSg0oo4+34B9TdXpuyPeTedN/6+iXBJ4J+zwfQI28u/Jl40nOYAxGoNi7mFP40RUtkmUA==} + engines: {node: '>=18'} + cpu: [arm64] + os: [win32] + + '@esbuild/win32-ia32@0.28.0': + resolution: {integrity: sha512-zF3ag/gfiCe6U2iczcRzSYJKH1DCI+ByzSENHlM2FcDbEeo5Zd2C86Aq0tKUYAJJ1obRP84ymxIAksZUcdztHA==} + engines: {node: '>=18'} + cpu: [ia32] + os: [win32] + + '@esbuild/win32-x64@0.28.0': + resolution: {integrity: sha512-pEl1bO9mfAmIC+tW5btTmrKaujg3zGtUmWNdCw/xs70FBjwAL3o9OEKNHvNmnyylD6ubxUERiEhdsL0xBQ9efw==} + engines: {node: '>=18'} + cpu: [x64] + os: [win32] + + '@eslint-community/eslint-utils@4.9.1': + resolution: {integrity: sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + peerDependencies: + eslint: ^6.0.0 || ^7.0.0 || >=8.0.0 + + '@eslint-community/regexpp@4.12.2': + resolution: {integrity: sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==} + engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} + + '@eslint/config-array@0.21.2': + resolution: {integrity: sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/config-helpers@0.4.2': + resolution: {integrity: sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/core@0.17.0': + resolution: {integrity: sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/eslintrc@3.3.5': + resolution: {integrity: sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/js@9.39.4': + resolution: {integrity: sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/object-schema@2.1.7': + resolution: {integrity: sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@eslint/plugin-kit@0.4.1': + resolution: {integrity: sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@fastify/busboy@3.2.0': + resolution: {integrity: sha512-m9FVDXU3GT2ITSe0UaMA5rU3QkfC/UXtCU8y0gSN/GugTqtVldOBWIB5V6V3sbmenVZUIpU6f+mPEO2+m5iTaA==} + + '@fastify/otel@0.18.0': + resolution: {integrity: sha512-3TASCATfw+ctICSb4ymrv7iCm0qJ0N9CarB+CZ7zIJ7KqNbwI5JjyDL1/sxoC0ccTO1Zyd1iQ+oqncPg5FJXaA==} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@floating-ui/core@1.7.5': + resolution: {integrity: sha512-1Ih4WTWyw0+lKyFMcBHGbb5U5FtuHJuujoyyr5zTaWS5EYMeT6Jb2AuDeftsCsEuchO+mM2ij5+q9crhydzLhQ==} + + '@floating-ui/dom@1.7.6': + resolution: {integrity: sha512-9gZSAI5XM36880PPMm//9dfiEngYoC6Am2izES1FF406YFsjvyBMmeJ2g4SAju3xWwtuynNRFL2s9hgxpLI5SQ==} + + '@floating-ui/react-dom@2.1.8': + resolution: {integrity: sha512-cC52bHwM/n/CxS87FH0yWdngEZrjdtLW/qVruo68qg+prK7ZQ4YGdut2GyDVpoGeAYe/h899rVeOVm6Oi40k2A==} + peerDependencies: + react: '>=16.8.0' + react-dom: '>=16.8.0' + + '@floating-ui/utils@0.2.11': + resolution: {integrity: sha512-RiB/yIh78pcIxl6lLMG0CgBXAZ2Y0eVHqMPYugu+9U0AeT6YBeiJpf7lbdJNIugFP5SIjwNRgo4DhR1Qxi26Gg==} + + '@graphql-tools/executor@1.5.3': + resolution: {integrity: sha512-mgBFC0bsrZPZLu9EnydpMnAuQ8Iiq0CEbUcsmvXsm2/iYektGHDN/+bmb7hicA6dWZtdPfklYJmr21WD0GnOfA==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/merge@9.1.9': + resolution: {integrity: sha512-iHUWNjRHeQRYdgIMIuChThOwoKzA9vrzYeslgfBo5eUYEyHGZCoDPjAavssoYXLwstYt1dZj2J22jSzc2DrN0Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/schema@10.0.33': + resolution: {integrity: sha512-O6P3RIftO0jafnSsFAqpjurUuUxJ43s/AdPVLQsBkI6y4Ic/tKm4C1Qm1KKQsCDTOxXPJClh/v3g7k7yLKCFBQ==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@10.11.0': + resolution: {integrity: sha512-iBFR9GXIs0gCD+yc3hoNswViL1O5josI33dUqiNStFI/MHLCEPduasceAcazRH77YONKNiviHBV8f7OgcT4o2Q==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-tools/utils@11.1.0': + resolution: {integrity: sha512-PtFVG4r8Z2LEBSaPYQMusBiB3o6kjLVJyjCLbnWem/SpSuM21v6LTmgpkXfYU1qpBV2UGsFyuEnSJInl8fR1Ag==} + engines: {node: '>=16.0.0'} + peerDependencies: + graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-typed-document-node/core@3.2.0': + resolution: {integrity: sha512-mB9oAsNCm9aM3/SOv4YtBMqZbYj10R7dkq8byBqxGY/ncFwhf2oQzMV+LCRlWoDSEBJ3COiR1yeDvMtsoOsuFQ==} + peerDependencies: + graphql: ^0.8.0 || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 + + '@graphql-yoga/logger@2.0.1': + resolution: {integrity: sha512-Nv0BoDGLMg9QBKy9cIswQ3/6aKaKjlTh87x3GiBg2Z4RrjyrM48DvOOK0pJh1C1At+b0mUIM67cwZcFTDLN4sA==} + engines: {node: '>=18.0.0'} + + '@graphql-yoga/subscription@5.0.5': + resolution: {integrity: sha512-oCMWOqFs6QV96/NZRt/ZhTQvzjkGB4YohBOpKM4jH/lDT4qb7Lex/aGCxpi/JD9njw3zBBtMqxbaC22+tFHVvw==} + engines: {node: '>=18.0.0'} + + '@graphql-yoga/typed-event-target@3.0.2': + resolution: {integrity: sha512-ZpJxMqB+Qfe3rp6uszCQoag4nSw42icURnBRfFYSOmTgEeOe4rD0vYlbA8spvCu2TlCesNTlEN9BLWtQqLxabA==} + engines: {node: '>=18.0.0'} + + '@hono/node-server@1.19.13': + resolution: {integrity: sha512-TsQLe4i2gvoTtrHje625ngThGBySOgSK3Xo2XRYOdqGN1teR8+I7vchQC46uLJi8OF62YTYA3AhSpumtkhsaKQ==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + + '@humanfs/core@0.19.2': + resolution: {integrity: sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==} + engines: {node: '>=18.18.0'} + + '@humanfs/node@0.16.8': + resolution: {integrity: sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==} + engines: {node: '>=18.18.0'} + + '@humanfs/types@0.15.0': + resolution: {integrity: sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==} + engines: {node: '>=18.18.0'} + + '@humanwhocodes/module-importer@1.0.1': + resolution: {integrity: sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==} + engines: {node: '>=12.22'} + + '@humanwhocodes/retry@0.4.3': + resolution: {integrity: sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==} + engines: {node: '>=18.18'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/remapping@2.3.5': + resolution: {integrity: sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@lekoarts/rehype-meta-as-attributes@3.0.3': + resolution: {integrity: sha512-DyG740U2y8ZWU5ZsAGzKfdVKOENg9UZsCBUJ3DVaWT7ZMHhDG5U0CjLj40CsEsJJ1gauaQPlrU9QUg5uSa4RFg==} + engines: {node: '>=18.0.0'} + + '@mdx-js/mdx@3.1.1': + resolution: {integrity: sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==} + + '@mdx-js/react@3.1.1': + resolution: {integrity: sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==} + peerDependencies: + '@types/react': '>=16' + react: '>=16' + + '@mdx-js/rollup@3.1.1': + resolution: {integrity: sha512-v8satFmBB+DqDzYohnm1u2JOvxx6Hl3pUvqzJvfs2Zk/ngZ1aRUhsWpXvwPkNeGN9c2NCm/38H29ZqXQUjf8dw==} + peerDependencies: + rollup: '>=2' + + '@napi-rs/nice-android-arm-eabi@1.1.1': + resolution: {integrity: sha512-kjirL3N6TnRPv5iuHw36wnucNqXAO46dzK9oPb0wj076R5Xm8PfUVA9nAFB5ZNMmfJQJVKACAPd/Z2KYMppthw==} + engines: {node: '>= 10'} + cpu: [arm] + os: [android] + + '@napi-rs/nice-android-arm64@1.1.1': + resolution: {integrity: sha512-blG0i7dXgbInN5urONoUCNf+DUEAavRffrO7fZSeoRMJc5qD+BJeNcpr54msPF6qfDD6kzs9AQJogZvT2KD5nw==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [android] + + '@napi-rs/nice-darwin-arm64@1.1.1': + resolution: {integrity: sha512-s/E7w45NaLqTGuOjC2p96pct4jRfo61xb9bU1unM/MJ/RFkKlJyJDx7OJI/O0ll/hrfpqKopuAFDV8yo0hfT7A==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [darwin] + + '@napi-rs/nice-darwin-x64@1.1.1': + resolution: {integrity: sha512-dGoEBnVpsdcC+oHHmW1LRK5eiyzLwdgNQq3BmZIav+9/5WTZwBYX7r5ZkQC07Nxd3KHOCkgbHSh4wPkH1N1LiQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [darwin] + + '@napi-rs/nice-freebsd-x64@1.1.1': + resolution: {integrity: sha512-kHv4kEHAylMYmlNwcQcDtXjklYp4FCf0b05E+0h6nDHsZ+F0bDe04U/tXNOqrx5CmIAth4vwfkjjUmp4c4JktQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [freebsd] + + '@napi-rs/nice-linux-arm-gnueabihf@1.1.1': + resolution: {integrity: sha512-E1t7K0efyKXZDoZg1LzCOLxgolxV58HCkaEkEvIYQx12ht2pa8hoBo+4OB3qh7e+QiBlp1SRf+voWUZFxyhyqg==} + engines: {node: '>= 10'} + cpu: [arm] + os: [linux] + + '@napi-rs/nice-linux-arm64-gnu@1.1.1': + resolution: {integrity: sha512-CIKLA12DTIZlmTaaKhQP88R3Xao+gyJxNWEn04wZwC2wmRapNnxCUZkVwggInMJvtVElA+D4ZzOU5sX4jV+SmQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-linux-arm64-musl@1.1.1': + resolution: {integrity: sha512-+2Rzdb3nTIYZ0YJF43qf2twhqOCkiSrHx2Pg6DJaCPYhhaxbLcdlV8hCRMHghQ+EtZQWGNcS2xF4KxBhSGeutg==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@napi-rs/nice-linux-ppc64-gnu@1.1.1': + resolution: {integrity: sha512-4FS8oc0GeHpwvv4tKciKkw3Y4jKsL7FRhaOeiPei0X9T4Jd619wHNe4xCLmN2EMgZoeGg+Q7GY7BsvwKpL22Tg==} + engines: {node: '>= 10'} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-linux-riscv64-gnu@1.1.1': + resolution: {integrity: sha512-HU0nw9uD4FO/oGCCk409tCi5IzIZpH2agE6nN4fqpwVlCn5BOq0MS1dXGjXaG17JaAvrlpV5ZeyZwSon10XOXw==} + engines: {node: '>= 10'} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-linux-s390x-gnu@1.1.1': + resolution: {integrity: sha512-2YqKJWWl24EwrX0DzCQgPLKQBxYDdBxOHot1KWEq7aY2uYeX+Uvtv4I8xFVVygJDgf6/92h9N3Y43WPx8+PAgQ==} + engines: {node: '>= 10'} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-linux-x64-gnu@1.1.1': + resolution: {integrity: sha512-/gaNz3R92t+dcrfCw/96pDopcmec7oCcAQ3l/M+Zxr82KT4DljD37CpgrnXV+pJC263JkW572pdbP3hP+KjcIg==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@napi-rs/nice-linux-x64-musl@1.1.1': + resolution: {integrity: sha512-xScCGnyj/oppsNPMnevsBe3pvNaoK7FGvMjT35riz9YdhB2WtTG47ZlbxtOLpjeO9SqqQ2J2igCmz6IJOD5JYw==} + engines: {node: '>= 10'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@napi-rs/nice-openharmony-arm64@1.1.1': + resolution: {integrity: sha512-6uJPRVwVCLDeoOaNyeiW0gp2kFIM4r7PL2MczdZQHkFi9gVlgm+Vn+V6nTWRcu856mJ2WjYJiumEajfSm7arPQ==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [openharmony] + + '@napi-rs/nice-win32-arm64-msvc@1.1.1': + resolution: {integrity: sha512-uoTb4eAvM5B2aj/z8j+Nv8OttPf2m+HVx3UjA5jcFxASvNhQriyCQF1OB1lHL43ZhW+VwZlgvjmP5qF3+59atA==} + engines: {node: '>= 10'} + cpu: [arm64] + os: [win32] + + '@napi-rs/nice-win32-ia32-msvc@1.1.1': + resolution: {integrity: sha512-CNQqlQT9MwuCsg1Vd/oKXiuH+TcsSPJmlAFc5frFyX/KkOh0UpBLEj7aoY656d5UKZQMQFP7vJNa1DNUNORvug==} + engines: {node: '>= 10'} + cpu: [ia32] + os: [win32] + + '@napi-rs/nice-win32-x64-msvc@1.1.1': + resolution: {integrity: sha512-vB+4G/jBQCAh0jelMTY3+kgFy00Hlx2f2/1zjMoH821IbplbWZOkLiTYXQkygNTzQJTq5cvwBDgn2ppHD+bglQ==} + engines: {node: '>= 10'} + cpu: [x64] + os: [win32] + + '@napi-rs/nice@1.1.1': + resolution: {integrity: sha512-xJIPs+bYuc9ASBl+cvGsKbGrJmS6fAKaSZCnT0lhahT5rhA2VVy9/EcIgd2JhtEuFOJNx7UHNn/qiTPTY4nrQw==} + engines: {node: '>= 10'} + + '@napi-rs/wasm-runtime@1.1.4': + resolution: {integrity: sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==} + peerDependencies: + '@emnapi/core': ^1.7.1 + '@emnapi/runtime': ^1.7.1 + + '@opentelemetry/api-logs@0.207.0': + resolution: {integrity: sha512-lAb0jQRVyleQQGiuuvCOTDVspc14nx6XJjP4FspJ1sNARo3Regq4ZZbrc3rN4b1TYSuUCvgH+UXUPug4SLOqEQ==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.212.0': + resolution: {integrity: sha512-TEEVrLbNROUkYY51sBJGk7lO/OLjuepch8+hmpM6ffMJQ2z/KVCjdHuCFX6fJj8OkJP2zckPjrJzQtXU3IAsFg==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api-logs@0.214.0': + resolution: {integrity: sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/api@1.9.1': + resolution: {integrity: sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==} + engines: {node: '>=8.0.0'} + + '@opentelemetry/core@2.6.1': + resolution: {integrity: sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/core@2.7.1': + resolution: {integrity: sha512-QAqIj32AtK6+pEVNG7EOVxHdE06RP+FM5qpiEJ4RtDcFIqKUZHYhl7/7UY5efhwmwNAg7j8QbJVBLxMerc0+gw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.0.0 <1.10.0' + + '@opentelemetry/instrumentation-amqplib@0.61.0': + resolution: {integrity: sha512-mCKoyTGfRNisge4br0NpOFSy2Z1NnEW8hbCJdUDdJFHrPqVzc4IIBPA/vX0U+LUcQqrQvJX+HMIU0dbDRe0i0Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-connect@0.57.0': + resolution: {integrity: sha512-FMEBChnI4FLN5TE9DHwfH7QpNir1JzXno1uz/TAucVdLCyrG0jTrKIcNHt/i30A0M2AunNBCkcd8Ei26dIPKdg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-dataloader@0.31.0': + resolution: {integrity: sha512-f654tZFQXS5YeLDNb9KySrwtg7SnqZN119FauD7acBoTzuLduaiGTNz88ixcVSOOMGZ+EjJu/RFtx5klObC95g==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-fs@0.33.0': + resolution: {integrity: sha512-sCZWXGalQ01wr3tAhSR9ucqFJ0phidpAle6/17HVjD6gN8FLmZMK/8sKxdXYHy3PbnlV1P4zeiSVFNKpbFMNLA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-generic-pool@0.57.0': + resolution: {integrity: sha512-orhmlaK+ZIW9hKU+nHTbXrCSXZcH83AescTqmpamHRobRmYSQwRbD0a1odc0yAzuzOtxYiHiXAnpnIpaSSY7Ow==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-graphql@0.62.0': + resolution: {integrity: sha512-3YNuLVPUxafXkH1jBAbGsKNsP3XVzcFDhCDCE3OqBwCwShlqQbLMRMFh1T/d5jaVZiGVmSsfof+ICKD2iOV8xg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-hapi@0.60.0': + resolution: {integrity: sha512-aNljZKYrEa7obLAxd1bCEDxF7kzCLGXTuTJZ8lMR9rIVEjmuKBXN1gfqpm/OB//Zc2zP4iIve1jBp7sr3mQV6w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-http@0.214.0': + resolution: {integrity: sha512-FlkDhZDRjDJDcO2LcSCtjRpkal1NJ8y0fBqBhTvfAR3JSYY2jAIj1kSS5IjmEBt4c3aWv+u/lqLuoCDrrKCSKg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-ioredis@0.62.0': + resolution: {integrity: sha512-ZYt//zcPve8qklaZX+5Z4MkU7UpEkFRrxsf2cnaKYBitqDnsCN69CPAuuMOX6NYdW2rG9sFy7V/QWtBlP5XiNQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-kafkajs@0.23.0': + resolution: {integrity: sha512-4K+nVo+zI+aDz0Z85SObwbdixIbzS9moIuKJaYsdlzcHYnKOPtB7ya8r8Ezivy/GVIBHiKJVq4tv+BEkgOMLaQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-knex@0.58.0': + resolution: {integrity: sha512-Hc/o8fSsaWxZ8r1Yw4rNDLwTpUopTf4X32y4W6UhlHmW8Wizz8wfhgOKIelSeqFVTKBBPIDUOsQWuIMxBmu8Bw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-koa@0.62.0': + resolution: {integrity: sha512-uVip0VuGUQXZ+vFxkKxAUNq8qNl+VFlyHDh/U6IQ8COOEDfbEchdaHnpFrMYF3psZRUuoSIgb7xOeXj00RdwDA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + + '@opentelemetry/instrumentation-lru-memoizer@0.58.0': + resolution: {integrity: sha512-6grM3TdMyHzlGY1cUA+mwoPueB1F3dYKgKtZIH6jOFXqfHAByyLTc+6PFjGM9tKh52CFBJaDwodNlL/Td39z7Q==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongodb@0.67.0': + resolution: {integrity: sha512-1WJp5N1lYfHq2IhECOTewFs5Tf2NfUOwQRqs/rZdXKTezArMlucxgzAaqcgp3A3YREXopXTpXHsxZTGHjNhMdQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mongoose@0.60.0': + resolution: {integrity: sha512-8BahAZpKsOoc+lrZGb7Ofn4g3z8qtp5IxDfvAVpKXsEheQN7ONMH5djT5ihy6yf8yyeQJGS0gXFfpEAEeEHqQg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql2@0.60.0': + resolution: {integrity: sha512-m/5d3bxQALllCzezYDk/6vajh0tj5OijMMvOZGr+qN1NMXm1dzMNwyJ0gNZW7Fo3YFRyj/jJMxIw+W7d525dlw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-mysql@0.60.0': + resolution: {integrity: sha512-08pO8GFPEIz2zquKDGteBZDNmwketdgH8hTe9rVYgW9kCJXq1Psj3wPQGx+VaX4ZJKCfPeoLMYup9+cxHvZyVQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-pg@0.66.0': + resolution: {integrity: sha512-KxfLGXBb7k2ueaPJfq2GXBDXBly8P+SpR/4Mj410hhNgmQF3sCqwXvUBQxZQkDAmsdBAoenM+yV1LhtsMRamcA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-redis@0.62.0': + resolution: {integrity: sha512-y3pPpot7WzR/8JtHcYlTYsyY8g+pbFhAqbwAuG5bLPnR6v6pt1rQc0DpH0OlGP/9CZbWBP+Zhwp9yFoygf/ZXQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-tedious@0.33.0': + resolution: {integrity: sha512-Q6WQwAD01MMTub31GlejoiFACYNw26J426wyjvU7by7fDIr2nZXNW4vhTGs7i7F0TnXBO3xN688g1tdUgYwJ5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation-undici@0.24.0': + resolution: {integrity: sha512-oKzZ3uvqP17sV0EsoQcJgjEfIp0kiZRbYu/eD8p13Cbahumf8lb/xpYeNr/hfAJ4owzEtIDcGIjprfLcYbIKBQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.7.0 + + '@opentelemetry/instrumentation@0.207.0': + resolution: {integrity: sha512-y6eeli9+TLKnznrR8AZlQMSJT7wILpXH+6EYq5Vf/4Ao+huI7EedxQHwRgVUOMLFbe7VFDvHJrX9/f4lcwnJsA==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.212.0': + resolution: {integrity: sha512-IyXmpNnifNouMOe0I/gX7ENfv2ZCNdYTF0FpCsoBcpbIHzk81Ww9rQTYTnvghszCg7qGrIhNvWC8dhEifgX9Jg==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/instrumentation@0.214.0': + resolution: {integrity: sha512-MHqEX5Dk59cqVah5LiARMACku7jXSVk9iVDWOea4x3cr7VfdByeDCURK6o1lntT1JS/Tsovw01UJrBhN3/uC5w==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.3.0 + + '@opentelemetry/redis-common@0.38.3': + resolution: {integrity: sha512-VCghU1JYs/4gP6Gqf/xro9MEsZ7LrMv2uONVsaESKL38ZOB9BqnI98FfS23wjMnHlpuE+TTaWSoAVNpTwYXzjw==} + engines: {node: ^18.19.0 || >=20.6.0} + + '@opentelemetry/resources@2.7.1': + resolution: {integrity: sha512-DeT6KKolmC4e/dRQvMQ/RwlnzhaqeiFOXY5ngoOPJ07GgVVKxZOg9EcrNZb5aTzUn+iCrJldAgOfQm1O/QfPAQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/sdk-trace-base@2.7.1': + resolution: {integrity: sha512-NAYIlsF8MPUsKqJMiDQJTMPOmlbawC1Iz/omMLygZ1C9am8fTKYjTaI+OZM+WTY3t3Glo0wnOg/6/pac6RGPPw==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': '>=1.3.0 <1.10.0' + + '@opentelemetry/semantic-conventions@1.40.0': + resolution: {integrity: sha512-cifvXDhcqMwwTlTK04GBNeIe7yyo28Mfby85QXFe1Yk8nmi36Ab/5UQwptOx84SsoGNRg+EVSjwzfSZMy6pmlw==} + engines: {node: '>=14'} + + '@opentelemetry/sql-common@0.41.2': + resolution: {integrity: sha512-4mhWm3Z8z+i508zQJ7r6Xi7y4mmoJpdvH0fZPFRkWrdp5fq7hhZ2HhYokEOLkfqSMgPR4Z9EyB3DBkbKGOqZiQ==} + engines: {node: ^18.19.0 || >=20.6.0} + peerDependencies: + '@opentelemetry/api': ^1.1.0 + + '@oxc-project/types@0.126.0': + resolution: {integrity: sha512-oGfVtjAgwQVVpfBrbtk4e1XDyWHRFta6BS3GWVzrF8xYBT2VGQAk39yJS/wFSMrZqoiCU4oghT3Ch0HaHGIHcQ==} + + '@pagefind/darwin-arm64@1.5.2': + resolution: {integrity: sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==} + cpu: [arm64] + os: [darwin] + + '@pagefind/darwin-x64@1.5.2': + resolution: {integrity: sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==} + cpu: [x64] + os: [darwin] + + '@pagefind/freebsd-x64@1.5.2': + resolution: {integrity: sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==} + cpu: [x64] + os: [freebsd] + + '@pagefind/linux-arm64@1.5.2': + resolution: {integrity: sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==} + cpu: [arm64] + os: [linux] + + '@pagefind/linux-x64@1.5.2': + resolution: {integrity: sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==} + cpu: [x64] + os: [linux] + + '@pagefind/windows-arm64@1.5.2': + resolution: {integrity: sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==} + cpu: [arm64] + os: [win32] + + '@pagefind/windows-x64@1.5.2': + resolution: {integrity: sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==} + cpu: [x64] + os: [win32] + + '@posthog/core@1.23.1': + resolution: {integrity: sha512-GViD5mOv/mcbZcyzz3z9CS0R79JzxVaqEz4sP5Dsea178M/j3ZWe6gaHDZB9yuyGfcmIMQ/8K14yv+7QrK4sQQ==} + + '@pothos/core@4.12.0': + resolution: {integrity: sha512-PeiODrj3GjQ7Nbs/5p65DEyBWZTSGGjgGO/BgaMEqS1jBNX/2zJTEQJA9zM5uPmCHUCDjE7Qn2U7lOi0ALp/8A==} + peerDependencies: + graphql: ^16.10.0 + + '@prisma/instrumentation@7.6.0': + resolution: {integrity: sha512-ZPW2gRiwpPzEfgeZgaekhqXrbW+Y2RJKHVqUmlhZhKzRNCcvR6DykzylDrynpArKKRQtLxoZy36fK7U0p3pdgQ==} + peerDependencies: + '@opentelemetry/api': ^1.8 + + '@radix-ui/number@1.1.1': + resolution: {integrity: sha512-MkKCwxlXTgz6CFoJx3pCwn07GKp36+aZyu/u2Ln2VrA5DcdyCZkASEDBTd8x5whTQQL5CiYf4prXKLcgQdv29g==} + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-accordion@1.2.12': + resolution: {integrity: sha512-T4nygeh9YE9dLRPhAHSeOZi7HBXo+0kYIPJXayZfvWOWA0+n3dESrZbjfDPUABkUNym6Hd+f2IR113To8D2GPA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-alert-dialog@1.1.15': + resolution: {integrity: sha512-oTVLkEw5GpdRe29BqJ0LSDFWI3qu0vR1M0mUkOQWDIUnY/QIkLpgDMWuKxP94c2NAC2LGcgVhG1ImF3jkZ5wXw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-arrow@1.1.7': + resolution: {integrity: sha512-F+M1tLhO+mlQaOWspE8Wstg+z6PwxwRd8oQ8IXceWz92kfAmalTRf0EjrouQeo7QssEPfCn05B4Ihs1K9WQ/7w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-aspect-ratio@1.1.8': + resolution: {integrity: sha512-5nZrJTF7gH+e0nZS7/QxFz6tJV4VimhQb1avEgtsJxvvIp5JilL+c58HICsKzPxghdwaDt48hEfPM1au4zGy+w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-checkbox@1.3.3': + resolution: {integrity: sha512-wBbpv+NQftHDdG86Qc0pIyXk5IR3tM8Vd0nWLKDcX8nNn4nXFOFwsKuqw2okA/1D/mpaAkmuyndrPJTYDNZtFw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collapsible@1.1.12': + resolution: {integrity: sha512-Uu+mSh4agx2ib1uIGPP4/CKNULyajb3p92LsVXmH2EHVMTfZWpll88XJ0j4W0z3f8NK1eYl1+Mf/szHPmcHzyA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-collection@1.1.7': + resolution: {integrity: sha512-Fh9rGN0MoI4ZFUNyfFVNU4y9LUz93u9/0K+yLgA2bwRojxM8JU1DyvvMBabnZPBgMWREAJvU2jjVzq+LrFUglw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.2': + resolution: {integrity: sha512-jCi/QKUM2r1Ju5a3J64TH2A5SpKAgh0LpknyqdQ4m6DCV0xJ2HG1xARRwNGPQfi1SLdLWZ1OJz6F4OMBBNiGJA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-context@1.1.3': + resolution: {integrity: sha512-ieIFACdMpYfMEjF0rEf5KLvfVyIkOz6PDGyNnP+u+4xQ6jny3VCgA4OgXOwNx2aUkxn8zx9fiVcM8CfFYv9Lxw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dialog@1.1.15': + resolution: {integrity: sha512-TCglVRtzlffRNxRMEyR36DGBLJpeusFcgMVD9PZEzAKnUs1lKCgX5u9BmC2Yg+LL9MgZDugFFs1Vl+Jp4t/PGw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-direction@1.1.1': + resolution: {integrity: sha512-1UEWRX6jnOA2y4H5WczZ44gOOjTEmlqv1uNW4GAJEO5+bauCBhv8snY65Iw5/VOS/ghKN9gr2KjnLKxrsvoMVw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-dismissable-layer@1.1.11': + resolution: {integrity: sha512-Nqcp+t5cTB8BinFkZgXiMJniQH0PsUt2k51FUhbdfeKvc4ACcG2uQniY/8+h1Yv6Kza4Q7lD7PQV0z0oicE0Mg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-dropdown-menu@2.1.16': + resolution: {integrity: sha512-1PLGQEynI/3OX/ftV54COn+3Sud/Mn8vALg2rWnBLnRaGtJDduNW/22XjlGgPdpcIbiQxjKtb7BkcjP00nqfJw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-focus-guards@1.1.3': + resolution: {integrity: sha512-0rFg/Rj2Q62NCm62jZw0QX7a3sz6QCQU0LpZdNrJX8byRGaGVTqbrW9jAoIAHyMQqsNpeZ81YgSizOt5WXq0Pw==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-focus-scope@1.1.7': + resolution: {integrity: sha512-t2ODlkXBQyn7jkl6TNaw/MtVEVvIGelJDCG41Okq/KwUsJBwQ4XVZsHAVUkK4mBv3ewiAS3PGuUWuY2BoK4ZUw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-hover-card@1.1.15': + resolution: {integrity: sha512-qgTkjNT1CfKMoP0rcasmlH2r1DAiYicWsDsufxl940sT2wHNEWWv6FMWIQXWhVdmC1d/HYfbhQx60KYyAtKxjg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-id@1.1.1': + resolution: {integrity: sha512-kGkGegYIdQsOb4XjsfM97rXsiHaBwco+hFI66oO4s9LU+PLAC5oJ7khdOVFxkhsmlbpUqDAvXw11CluXP+jkHg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-label@2.1.8': + resolution: {integrity: sha512-FmXs37I6hSBVDlO4y764TNz1rLgKwjJMQ0EGte6F3Cb3f4bIuHB/iLa/8I9VKkmOy+gNHq8rql3j686ACVV21A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-menu@2.1.16': + resolution: {integrity: sha512-72F2T+PLlphrqLcAotYPp0uJMr5SjP5SL01wfEspJbru5Zs5vQaSHb4VB3ZMJPimgHHCHG7gMOeOB9H3Hdmtxg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-navigation-menu@1.2.14': + resolution: {integrity: sha512-YB9mTFQvCOAQMHU+C/jVl96WmuWeltyUEpRJJky51huhds5W2FQr1J8D/16sQlf0ozxkPK8uF3niQMdUwZPv5w==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popover@1.1.15': + resolution: {integrity: sha512-kr0X2+6Yy/vJzLYJUPCZEc8SfQcf+1COFoAqauJm74umQhta9M7lNJHP7QQS3vkvcGLQUbWpMzwrXYwrYztHKA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-popper@1.2.8': + resolution: {integrity: sha512-0NJQ4LFFUuWkE7Oxf0htBKS6zLkkjBH+hM1uk7Ng705ReR8m/uelduy1DBo0PyBXPKVnBA6YBlU94MBGXrSBCw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-portal@1.1.9': + resolution: {integrity: sha512-bpIxvq03if6UNwXZ+HTK71JLh4APvnXntDc6XOX8UVq4XQOVl7lwok0AvIl+b8zgCw3fSaVTZMpAPPagXbKmHQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-presence@1.1.5': + resolution: {integrity: sha512-/jfEwNDdQVBCNvjkGit4h6pMOzq8bHkopq458dPt2lMjx+eBQUohZNG9A7DtO/O5ukSbxuaNGXMjHicgwy6rQQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-primitive@2.1.4': + resolution: {integrity: sha512-9hQc4+GNVtJAIEPEqlYqW5RiYdrr8ea5XQ0ZOnD6fgru+83kqT15mq2OCcbe8KnjRZl5vF3ks69AKz3kh1jrhg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-progress@1.1.8': + resolution: {integrity: sha512-+gISHcSPUJ7ktBy9RnTqbdKW78bcGke3t6taawyZ71pio1JewwGSJizycs7rLhGTvMJYCQB1DBK4KQsxs7U8dA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-radio-group@1.3.8': + resolution: {integrity: sha512-VBKYIYImA5zsxACdisNQ3BjCBfmbGH3kQlnFVqlWU4tXwjy7cGX8ta80BcrO+WJXIn5iBylEH3K6ZTlee//lgQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-roving-focus@1.1.11': + resolution: {integrity: sha512-7A6S9jSgm/S+7MdtNDSb+IU859vQqJ/QAtcYQcfFC6W8RS4IxIZDldLR0xqCFZ6DCyrQLjLPsxtTNch5jVA4lA==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-scroll-area@1.2.10': + resolution: {integrity: sha512-tAXIa1g3sM5CGpVT0uIbUx/U3Gs5N8T52IICuCtObaos1S8fzsrPXG5WObkQN3S6NVl6wKgPhAIiBGbWnvc97A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-select@2.2.6': + resolution: {integrity: sha512-I30RydO+bnn2PQztvo25tswPH+wFBjehVGtmagkU78yMdwTwVf12wnAOF+AeP8S2N8xD+5UPbGhkUfPyvT+mwQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-separator@1.1.8': + resolution: {integrity: sha512-sDvqVY4itsKwwSMEe0jtKgfTh+72Sy3gPmQpjqcQneqQ4PFmr/1I0YA+2/puilhggCe2gJcx5EBAYFkWkdpa5g==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slider@1.3.6': + resolution: {integrity: sha512-JPYb1GuM1bxfjMRlNLE+BcmBC8onfCi60Blk7OBqi2MLTFdS+8401U4uFjnwkOr49BLmXxLC6JHkvAsx5OJvHw==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-slot@1.2.4': + resolution: {integrity: sha512-Jl+bCv8HxKnlTLVrcDE8zTMJ09R9/ukw4qBs/oZClOfoQk/cOTbDn+NceXfV7j09YPVQUryJPHurafcSg6EVKA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-switch@1.2.6': + resolution: {integrity: sha512-bByzr1+ep1zk4VubeEVViV592vu2lHE2BZY5OnzehZqOOgogN80+mNtCqPkhn2gklJqOpxWgPoYTSnhBCqpOXQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tabs@1.1.13': + resolution: {integrity: sha512-7xdcatg7/U+7+Udyoj2zodtI9H/IIopqo+YOIcZOq1nJwXWBZ9p8xiu5llXlekDbZkca79a/fozEYQXIA4sW6A==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle-group@1.1.11': + resolution: {integrity: sha512-5umnS0T8JQzQT6HbPyO7Hh9dgd82NmS36DQr+X/YJ9ctFNCiiQd6IJAYYZ33LUwm8M+taCz5t2ui29fHZc4Y6Q==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-tooltip@1.2.8': + resolution: {integrity: sha512-tY7sVt1yL9ozIxvmbtN5qtmH2krXcBCfjEiCgKGLqunJHvgvZG2Pcl2oQ3kbcZARb1BGEHdkLzcYGO8ynVlieg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-callback-ref@1.1.1': + resolution: {integrity: sha512-FkBMwD+qbGQeMu1cOHnuGB6x4yzPjho8ap5WtbEJ26umhgqVXbhekKUQO+hZEL1vU92a3wHwdp0HAcqAUF5iDg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-escape-keydown@1.1.1': + resolution: {integrity: sha512-Il0+boE7w/XebUHyBjroE+DbByORGR9KKmITzbR7MyQ4akpORYP/ZmbhAr0DG7RmmBqoOnZdy2QlvajJ2QA59g==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-previous@1.1.1': + resolution: {integrity: sha512-2dHfToCj/pzca2Ck724OZ5L0EVrr3eHRNsG/b3xQJLA2hZpVCS99bLAX+hm1IHXDEnzU6by5z/5MIY794/a8NQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-rect@1.1.1': + resolution: {integrity: sha512-QTYuDesS0VtuHNNvMh+CjlKJ4LJickCMUAqjlE3+j8w+RlRpwyX3apEQKGFzbZGdo7XNG1tXa+bQqIE7HIXT2w==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-size@1.1.1': + resolution: {integrity: sha512-ewrXRDTAqAXlkl6t/fkXWNAhFX9I+CkKlw6zjEwk86RSPKwZr3xpBRso655aqYafwtnbpHLj6toFzmd6xdVptQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-visually-hidden@1.2.3': + resolution: {integrity: sha512-pzJq12tEaaIhqjbzpCuv/OypJY/BPavOofm+dbab+MHLajy277+1lLm6JFcGgF5eskJ6mquGirhXY2GD/8u8Ug==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-visually-hidden@1.2.4': + resolution: {integrity: sha512-kaeiyGCe844dkb9AVF+rb4yTyb1LiLN/e3es3nLiRyN4dC8AduBYPMnnNlDjX2VDOcvDEiPnRNMJeWCfsX0txg==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/rect@1.1.1': + resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + + '@repeaterjs/repeater@3.0.6': + resolution: {integrity: sha512-Javneu5lsuhwNCryN+pXH93VPQ8g0dBX7wItHFgYiwQmzE1sVdg5tWHiOgHywzL2W21XQopa7IwIEnNbmeUJYA==} + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rhY3k7Bsae9qQfOtph2Pm2jZEA+s8Gmjoz4hhmx70K9iMQ/ddeae+xhRQcM5IuVx5ry1+bGfkvMn7D6MJggVSA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [android] + + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-rNz0yK078yrNn3DrdgN+PKiMOW8HfQ92jQiXxwX8yW899ayV00MLVdaCNeVBhG/TbH3ouYVObo8/yrkiectkcQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [darwin] + + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + resolution: {integrity: sha512-r/OmdR00HmD4i79Z//xO06uEPOq5hRXdhw7nzkxQxwSavs3PSHa1ijntdpOiZ2mzOQ3fVVu8C1M19FoNM+dMUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [darwin] + + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + resolution: {integrity: sha512-KcRE5w8h0OnjUatG8pldyD14/CQ5Phs1oxfR+3pKDjboHRo9+MkqQaiIZlZRpsxC15paeXme/I127tUa9TXJ6g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [freebsd] + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + resolution: {integrity: sha512-bT0guA1bpxEJ/ZhTRniQf7rNF8ybvXOuWbNIeLABaV5NGjx4EtOWBTSRGWFU9ZWVkPOZ+HNFP8RMcBokBiZ0Kg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm] + os: [linux] + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-+tHktCHWV8BDQSjemUqm/Jl/TPk3QObCTIjmdDy/nlupcujZghmKK2962LYrqFpWu+ai01AN/REOH3NEpqvYQg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-3fPzdREH806oRLxpTWW1Gt4tQHs0TitZFOECB2xzCFLPKnSOy90gwA7P29cksYilFO6XVRY1kzga0cL2nRjKPg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-EKwI1tSrLs7YVw+JPJT/G2dJQ1jl9qlTTTEG0V2Ok/RdOenRfBw2PQdLPyjhIu58ocdBfP7vIRN/pvMsPxs/AQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-Uknladnb3Sxqu6SEcqBldQyJUpk8NleooZEc0MbRBJ4inEhRYWZX0NJu12vNf2mqAq7gsofAxHrGghiUYjhaLQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + resolution: {integrity: sha512-FIb8+uG49sZBtLTn+zt1AJ20TqVcqWeSIyoVt0or7uAWesgKaHbiBh6OpA/k9v0LTt+PTrb1Lao133kP4uVxkg==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + resolution: {integrity: sha512-RuERhF9/EgWxZEXYWCOaViUWHIboceK4/ivdtQ3R0T44NjLkIIlGIAVAuCddFxsZ7vnRHtNQUrt2vR2n2slB2w==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + resolution: {integrity: sha512-mXcXnvd9GpazCxeUCCnZ2+YF7nut+ZOEbE4GtaiPtyY6AkhZWbK70y1KK3j+RDhjVq5+U8FySkKRb/+w0EeUwA==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [openharmony] + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + resolution: {integrity: sha512-3Q2KQxnC8IJOLqXmUMoYwyIPZU9hzRbnHaoV3Euz+VVnjZKcY8ktnNP8T9R4/GGQtb27C/UYKABxesKWb8lsvQ==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [wasm32] + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-tj7XRemQcOcFwv7qhpUxMTBbI5mWMlE4c1Omhg5+h8GuLXzyj8HviYgR+bB2DMDgRqUE+jiDleqSCRjx4aYk/Q==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [arm64] + os: [win32] + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + resolution: {integrity: sha512-PH5DRZT+F4f2PTXRXR8uJxnBq2po/xFtddyabTJVJs/ZYVHqXPEgNIr35IHTEa6bpa0Q8Awg+ymkTaGnKITw4g==} + engines: {node: ^20.19.0 || >=22.12.0} + cpu: [x64] + os: [win32] + + '@rolldown/pluginutils@1.0.0-rc.16': + resolution: {integrity: sha512-45+YtqxLYKDWQouLKCrpIZhke+nXxhsw+qAHVzHDVwttyBlHNBVs2K25rDXrZzhpTp9w1FlAlvweV1H++fdZoA==} + + '@rolldown/pluginutils@1.0.0-rc.7': + resolution: {integrity: sha512-qujRfC8sFVInYSPPMLQByRh7zhwkGFS4+tyMQ83srV1qrxL4g8E2tyxVVyxd0+8QeBM1mIk9KbWxkegRr76XzA==} + + '@rollup/pluginutils@5.3.0': + resolution: {integrity: sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==} + engines: {node: '>=14.0.0'} + peerDependencies: + rollup: ^1.20.0||^2.0.0||^3.0.0||^4.0.0 + peerDependenciesMeta: + rollup: + optional: true + + '@rollup/rollup-android-arm-eabi@4.60.3': + resolution: {integrity: sha512-x35CNW/ANXG3hE/EZpRU8MXX1JDN86hBb2wMGAtltkz7pc6cxgjpy1OMMfDosOQ+2hWqIkag/fGok1Yady9nGw==} + cpu: [arm] + os: [android] + + '@rollup/rollup-android-arm64@4.60.3': + resolution: {integrity: sha512-xw3xtkDApIOGayehp2+Rz4zimfkaX65r4t47iy+ymQB2G4iJCBBfj0ogVg5jpvjpn8UWn/+q9tprxleYeNp3Hw==} + cpu: [arm64] + os: [android] + + '@rollup/rollup-darwin-arm64@4.60.3': + resolution: {integrity: sha512-vo6Y5Qfpx7/5EaamIwi0WqW2+zfiusVihKatLvtN1VFVy3D13uERk/6gZLU1UiHRL6fDXqj/ELIeVRGnvcTE1g==} + cpu: [arm64] + os: [darwin] + + '@rollup/rollup-darwin-x64@4.60.3': + resolution: {integrity: sha512-D+0QGcZhBzTN82weOnsSlY7V7+RMmPuF1CkbxyMAGE8+ZHeUjyb76ZiWmBlCu//AQQONvxcqRbwZTajZKqjuOw==} + cpu: [x64] + os: [darwin] + + '@rollup/rollup-freebsd-arm64@4.60.3': + resolution: {integrity: sha512-6HnvHCT7fDyj6R0Ph7A6x8dQS/S38MClRWeDLqc0MdfWkxjiu1HSDYrdPhqSILzjTIC/pnXbbJbo+ft+gy/9hQ==} + cpu: [arm64] + os: [freebsd] + + '@rollup/rollup-freebsd-x64@4.60.3': + resolution: {integrity: sha512-KHLgC3WKlUYW3ShFKnnosZDOJ0xjg9zp7au3sIm2bs/tGBeC2ipmvRh/N7JKi0t9Ue20C0dpEshi8WUubg+cnA==} + cpu: [x64] + os: [freebsd] + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + resolution: {integrity: sha512-DV6fJoxEYWJOvaZIsok7KrYl0tPvga5OZ2yvKHNNYyk/2roMLqQAbGhr78EQ5YhHpnhLKJD3S1WFusAkmUuV5g==} + cpu: [arm] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + resolution: {integrity: sha512-mQKoJAzvuOs6F+TZybQO4GOTSMUu7v0WdxEk24krQ/uUxXoPTtHjuaUuPmFhtBcM4K0ons8nrE3JyhTuCFtT/w==} + cpu: [arm] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + resolution: {integrity: sha512-Whjj2qoiJ6+OOJMGptTYazaJvjOJm+iKHpXQM1P3LzGjt7Ff++Tp7nH4N8J/BUA7R9IHfDyx4DJIflifwnbmIA==} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-arm64-musl@4.60.3': + resolution: {integrity: sha512-4YTNHKqGng5+yiZt3mg77nmyuCfmNfX4fPmyUapBcIk+BdwSwmCWGXOUxhXbBEkFHtoN5boLj/5NON+u5QC9tg==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + resolution: {integrity: sha512-SU3kNlhkpI4UqlUc2VXPGK9o886ZsSeGfMAX2ba2b8DKmMXq4AL7KUrkSWVbb7koVqx41Yczx6dx5PNargIrEA==} + cpu: [loong64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-loong64-musl@4.60.3': + resolution: {integrity: sha512-6lDLl5h4TXpB1mTf2rQWnAk/LcXrx9vBfu/DT5TIPhvMhRWaZ5MxkIc8u4lJAmBo6klTe1ywXIUHFjylW505sg==} + cpu: [loong64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + resolution: {integrity: sha512-BMo8bOw8evlup/8G+cj5xWtPyp93xPdyoSN16Zy90Q2QZ0ZYRhCt6ZJSwbrRzG9HApFabjwj2p25TUPDWrhzqQ==} + cpu: [ppc64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + resolution: {integrity: sha512-E0L8X1dZN1/Rph+5VPF6Xj2G7JJvMACVXtamTJIDrVI44Y3K+G8gQaMEAavbqCGTa16InptiVrX6eM6pmJ+7qA==} + cpu: [ppc64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + resolution: {integrity: sha512-oZJ/WHaVfHUiRAtmTAeo3DcevNsVvH8mbvodjZy7D5QKvCefO371SiKRpxoDcCxB3PTRTLayWBkvmDQKTcX/sw==} + cpu: [riscv64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + resolution: {integrity: sha512-Dhbyh7j9FybM3YaTgaHmVALwA8AkUwTPccyCQ79TG9AJUsMQqgN1DDEZNr4+QUfwiWvLDumW5vdwzoeUF+TNxQ==} + cpu: [riscv64] + os: [linux] + libc: [musl] + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + resolution: {integrity: sha512-cJd1X5XhHHlltkaypz1UcWLA8AcoIi1aWhsvaWDskD1oz2eKCypnqvTQ8ykMNI0RSmm7NkTdSqSSD7zM0xa6Ig==} + cpu: [s390x] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-gnu@4.60.3': + resolution: {integrity: sha512-DAZDBHQfG2oQuhY7mc6I3/qB4LU2fQCjRvxbDwd/Jdvb9fypP4IJ4qmtu6lNjes6B531AI8cg1aKC2di97bUxA==} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@rollup/rollup-linux-x64-musl@4.60.3': + resolution: {integrity: sha512-cRxsE8c13mZOh3vP+wLDxpQBRrOHDIGOWyDL93Sy0Ga8y515fBcC2pjUfFwUe5T7tqvTvWbCpg1URM/AXdWIXA==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@rollup/rollup-openbsd-x64@4.60.3': + resolution: {integrity: sha512-QaWcIgRxqEdQdhJqW4DJctsH6HCmo5vHxY0krHSX4jMtOqfzC+dqDGuHM87bu4H8JBeibWx7jFz+h6/4C8wA5Q==} + cpu: [x64] + os: [openbsd] + + '@rollup/rollup-openharmony-arm64@4.60.3': + resolution: {integrity: sha512-AaXwSvUi3QIPtroAUw1t5yHGIyqKEXwH54WUocFolZhpGDruJcs8c+xPNDRn4XiQsS7MEwnYsHW2l0MBLDMkWg==} + cpu: [arm64] + os: [openharmony] + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + resolution: {integrity: sha512-65LAKM/bAWDqKNEelHlcHvm2V+Vfb8C6INFxQXRHCvaVN1rJfwr4NvdP4FyzUaLqWfaCGaadf6UbTm8xJeYfEg==} + cpu: [arm64] + os: [win32] + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + resolution: {integrity: sha512-EEM2gyhBF5MFnI6vMKdX1LAosE627RGBzIoGMdLloPZkXrUN0Ckqgr2Qi8+J3zip/8NVVro3/FjB+tjhZUgUHA==} + cpu: [ia32] + os: [win32] + + '@rollup/rollup-win32-x64-gnu@4.60.3': + resolution: {integrity: sha512-E5Eb5H/DpxaoXH++Qkv28RcUJboMopmdDUALBczvHMf7hNIxaDZqwY5lK12UK1BHacSmvupoEWGu+n993Z0y1A==} + cpu: [x64] + os: [win32] + + '@rollup/rollup-win32-x64-msvc@4.60.3': + resolution: {integrity: sha512-hPt/bgL5cE+Qp+/TPHBqptcAgPzgj46mPcg/16zNUmbQk0j+mOEQV/+Lqu8QRtDV3Ek95Q6FeFITpuhl6OTsAA==} + cpu: [x64] + os: [win32] + + '@scalar/helpers@0.2.7': + resolution: {integrity: sha512-uFTcdi3XYDDuaJLWiMuM3ijQit1OBw7AkuOuujReY8L9UmUQHY56erYg0+Db3llTsinuIYFh+eS/WX/sYuevYQ==} + engines: {node: '>=20'} + + '@scalar/json-magic@0.9.0': + resolution: {integrity: sha512-aSWd8rd3O73Ak9Ylson2TywvOuTjjOYiXydl9Cn8Ip/r7fi+h0QqAGom5gqo/WewrhySF9v+H/sW/Qmd05T/Kg==} + engines: {node: '>=20'} + + '@scalar/openapi-parser@0.23.13': + resolution: {integrity: sha512-YsljPOKOgQgZL/kBcEouwz2CUa+2hFfThlUZRWC2DFI2Fnw5Ur8F1IvGgPqCAHr9p2XMH+Z/Pag2jZUfLcxcww==} + engines: {node: '>=20'} + + '@scalar/openapi-types@0.5.3': + resolution: {integrity: sha512-m4n/Su3K01d15dmdWO1LlqecdSPKuNjuokrJLdiQ485kW/hRHbXW1QP6tJL75myhw/XhX5YhYAR+jrwnGjXiMw==} + engines: {node: '>=20'} + + '@scalar/openapi-upgrader@0.1.7': + resolution: {integrity: sha512-065froUtqvaHjyeJtyitf8tb+k7oh7nU0OinAHYbj1Bqgwb1s2+uKMqHYHEES5CNpp+2xtL4lxup6Aq29yW+sQ==} + engines: {node: '>=20'} + + '@sentry/core@10.49.0': + resolution: {integrity: sha512-UaFeum3LUM1mB0d67jvKnqId1yWQjyqmaDV6kWngG03x+jqXb08tJdGpSoxjXZe13jFBbiBL/wKDDYIK7rCK4g==} + engines: {node: '>=18'} + + '@sentry/node-core@10.49.0': + resolution: {integrity: sha512-7WO0KuCDPSq3G54TVUSI1CKFJwB67LasG+n/gDMBqbrarzs/Yh/s34OOMU5gfVQpncxQAmQsy4nEboQms8iNqA==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/exporter-trace-otlp-http': '>=0.57.0 <1' + '@opentelemetry/instrumentation': '>=0.57.1 <1' + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/semantic-conventions': ^1.39.0 + peerDependenciesMeta: + '@opentelemetry/api': + optional: true + '@opentelemetry/core': + optional: true + '@opentelemetry/exporter-trace-otlp-http': + optional: true + '@opentelemetry/instrumentation': + optional: true + '@opentelemetry/sdk-trace-base': + optional: true + '@opentelemetry/semantic-conventions': + optional: true + + '@sentry/node@10.49.0': + resolution: {integrity: sha512-xr+HXABCiO5mgAJRQxsXRdNOLO0+Ee6CvXAAIqovL2A1GlhxNWc5ooPWeIrrLDJ/KGyT8zI91O5scpVXdXs0uQ==} + engines: {node: '>=18'} + + '@sentry/opentelemetry@10.49.0': + resolution: {integrity: sha512-XNLm4dXmtegXQf+EEE2Cs84Ymlo/f5wMx+lg2S2XS4qLbXaPN/HttjhwKftd8D+8iUNfmH+xNMCSshx4s1B/1w==} + engines: {node: '>=18'} + peerDependencies: + '@opentelemetry/api': ^1.9.0 + '@opentelemetry/core': ^1.30.1 || ^2.1.0 + '@opentelemetry/sdk-trace-base': ^1.30.1 || ^2.1.0 + '@opentelemetry/semantic-conventions': ^1.39.0 + + '@shikijs/core@4.0.2': + resolution: {integrity: sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==} + engines: {node: '>=20'} + + '@shikijs/engine-javascript@4.0.2': + resolution: {integrity: sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==} + engines: {node: '>=20'} + + '@shikijs/engine-oniguruma@4.0.2': + resolution: {integrity: sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==} + engines: {node: '>=20'} + + '@shikijs/langs@4.0.2': + resolution: {integrity: sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==} + engines: {node: '>=20'} + + '@shikijs/primitive@4.0.2': + resolution: {integrity: sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==} + engines: {node: '>=20'} + + '@shikijs/rehype@4.0.2': + resolution: {integrity: sha512-cmPlKLD8JeojasNFoY64162ScpEdEdQUMuVodPCrv1nx1z3bjmGwoKWDruQWa/ejSznImlaeB0Ty6Q3zPaVQAA==} + engines: {node: '>=20'} + + '@shikijs/themes@4.0.2': + resolution: {integrity: sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==} + engines: {node: '>=20'} + + '@shikijs/transformers@4.0.2': + resolution: {integrity: sha512-1+L0gf9v+SdDXs08vjaLb3mBFa8U7u37cwcBQIv/HCocLwX69Tt6LpUCjtB+UUTvQxI7BnjZKhN/wMjhHBcJGg==} + engines: {node: '>=20'} + + '@shikijs/types@4.0.2': + resolution: {integrity: sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==} + engines: {node: '>=20'} + + '@shikijs/vscode-textmate@10.0.2': + resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + + '@tailwindcss/node@4.2.2': + resolution: {integrity: sha512-pXS+wJ2gZpVXqFaUEjojq7jzMpTGf8rU6ipJz5ovJV6PUGmlJ+jvIwGrzdHdQ80Sg+wmQxUFuoW1UAAwHNEdFA==} + + '@tailwindcss/oxide-android-arm64@4.2.2': + resolution: {integrity: sha512-dXGR1n+P3B6748jZO/SvHZq7qBOqqzQ+yFrXpoOWWALWndF9MoSKAT3Q0fYgAzYzGhxNYOoysRvYlpixRBBoDg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [android] + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + resolution: {integrity: sha512-iq9Qjr6knfMpZHj55/37ouZeykwbDqF21gPFtfnhCCKGDcPI/21FKC9XdMO/XyBM7qKORx6UIhGgg6jLl7BZlg==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [darwin] + + '@tailwindcss/oxide-darwin-x64@4.2.2': + resolution: {integrity: sha512-BlR+2c3nzc8f2G639LpL89YY4bdcIdUmiOOkv2GQv4/4M0vJlpXEa0JXNHhCHU7VWOKWT/CjqHdTP8aUuDJkuw==} + engines: {node: '>= 20'} + cpu: [x64] + os: [darwin] + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + resolution: {integrity: sha512-YUqUgrGMSu2CDO82hzlQ5qSb5xmx3RUrke/QgnoEx7KvmRJHQuZHZmZTLSuuHwFf0DJPybFMXMYf+WJdxHy/nQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [freebsd] + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + resolution: {integrity: sha512-FPdhvsW6g06T9BWT0qTwiVZYE2WIFo2dY5aCSpjG/S/u1tby+wXoslXS0kl3/KXnULlLr1E3NPRRw0g7t2kgaQ==} + engines: {node: '>= 20'} + cpu: [arm] + os: [linux] + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [glibc] + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==} + engines: {node: '>= 20'} + cpu: [x64] + os: [linux] + libc: [musl] + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + resolution: {integrity: sha512-eKSztKsmEsn1O5lJ4ZAfyn41NfG7vzCg496YiGtMDV86jz1q/irhms5O0VrY6ZwTUkFy/EKG3RfWgxSI3VbZ8Q==} + engines: {node: '>=14.0.0'} + cpu: [wasm32] + bundledDependencies: + - '@napi-rs/wasm-runtime' + - '@emnapi/core' + - '@emnapi/runtime' + - '@tybys/wasm-util' + - '@emnapi/wasi-threads' + - tslib + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + resolution: {integrity: sha512-qPmaQM4iKu5mxpsrWZMOZRgZv1tOZpUm+zdhhQP0VhJfyGGO3aUKdbh3gDZc/dPLQwW4eSqWGrrcWNBZWUWaXQ==} + engines: {node: '>= 20'} + cpu: [arm64] + os: [win32] + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + resolution: {integrity: sha512-1T/37VvI7WyH66b+vqHj/cLwnCxt7Qt3WFu5Q8hk65aOvlwAhs7rAp1VkulBJw/N4tMirXjVnylTR72uI0HGcA==} + engines: {node: '>= 20'} + cpu: [x64] + os: [win32] + + '@tailwindcss/oxide@4.2.2': + resolution: {integrity: sha512-qEUA07+E5kehxYp9BVMpq9E8vnJuBHfJEC0vPC5e7iL/hw7HR61aDKoVoKzrG+QKp56vhNZe4qwkRmMC0zDLvg==} + engines: {node: '>= 20'} + + '@tailwindcss/typography@0.5.19': + resolution: {integrity: sha512-w31dd8HOx3k9vPtcQh5QHP9GwKcgbMp87j58qi6xgiBnFFtKEAgCWnDw4qUT8aHwkCp8bKvb/KGKWWHedP0AAg==} + peerDependencies: + tailwindcss: '>=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1' + + '@tailwindcss/vite@4.2.2': + resolution: {integrity: sha512-mEiF5HO1QqCLXoNEfXVA1Tzo+cYsrqV7w9Juj2wdUFyW07JRenqMG225MvPwr3ZD9N1bFQj46X7r33iHxLUW0w==} + peerDependencies: + vite: ^5.2.0 || ^6 || ^7 || ^8 + + '@tanem/react-nprogress@6.0.3': + resolution: {integrity: sha512-OT3KZiJv/zGI/lEwX3gD9nzNnyUQuSdSL0BK3OJIUC3Prx3JD18u13TZRvzasEXxHPGzm6AtBEr6gYxPeVWjWw==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@tanstack/query-core@5.97.0': + resolution: {integrity: sha512-QdpLP5VzVMgo4VtaPppRA2W04UFjIqX+bxke/ZJhE5cfd5UPkRzqIAJQt9uXkQJjqE8LBOMbKv7f8HCsZltXlg==} + + '@tanstack/react-query@5.97.0': + resolution: {integrity: sha512-y4So4eGcQoK2WVMAcDNZE9ofB/p5v1OlKvtc1F3uqHwrtifobT7q+ZnXk2mRkc8E84HKYSlAE9z6HXl2V0+ySQ==} + peerDependencies: + react: ^18 || ^19 + + '@tybys/wasm-util@0.10.2': + resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==} + + '@types/connect@3.4.38': + resolution: {integrity: sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==} + + '@types/debug@4.1.13': + resolution: {integrity: sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==} + + '@types/estree-jsx@1.0.5': + resolution: {integrity: sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==} + + '@types/estree@1.0.8': + resolution: {integrity: sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==} + + '@types/hast@3.0.4': + resolution: {integrity: sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==} + + '@types/json-schema@7.0.15': + resolution: {integrity: sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==} + + '@types/mdast@4.0.4': + resolution: {integrity: sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==} + + '@types/mdx@2.0.13': + resolution: {integrity: sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==} + + '@types/ms@2.1.0': + resolution: {integrity: sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==} + + '@types/mysql@2.15.27': + resolution: {integrity: sha512-YfWiV16IY0OeBfBCk8+hXKmdTKrKlwKN1MNKAPBu5JYxLwBEZl7QzeEpGnlZb3VMGJrrGmB84gXiH+ofs/TezA==} + + '@types/node@24.12.2': + resolution: {integrity: sha512-A1sre26ke7HDIuY/M23nd9gfB+nrmhtYyMINbjI1zHJxYteKR6qSMX56FsmjMcDb3SMcjJg5BiRRgOCC/yBD0g==} + + '@types/node@25.6.0': + resolution: {integrity: sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==} + + '@types/pg-pool@2.0.7': + resolution: {integrity: sha512-U4CwmGVQcbEuqpyju8/ptOKg6gEC+Tqsvj2xS9o1g71bUh8twxnC6ZL5rZKCsGN0iyH0CwgUyc9VR5owNQF9Ng==} + + '@types/pg@8.15.6': + resolution: {integrity: sha512-NoaMtzhxOrubeL/7UZuNTrejB4MPAJ0RpxZqXQf2qXuVlTPuG6Y8p4u9dKRaue4yjmC7ZhzVO2/Yyyn25znrPQ==} + + '@types/react-dom@19.2.3': + resolution: {integrity: sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==} + peerDependencies: + '@types/react': ^19.2.0 + + '@types/react@19.2.14': + resolution: {integrity: sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==} + + '@types/sax@1.2.7': + resolution: {integrity: sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==} + + '@types/tedious@4.0.14': + resolution: {integrity: sha512-KHPsfX/FoVbUGbyYvk1q9MMQHLPeRZhRJZdO45Q4YjvFkv4hMNghCWTvy7rdKessBsmtz4euWCWAB6/tVpI1Iw==} + + '@types/unist@2.0.11': + resolution: {integrity: sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==} + + '@types/unist@3.0.3': + resolution: {integrity: sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==} + + '@typescript-eslint/eslint-plugin@8.59.2': + resolution: {integrity: sha512-j/bwmkBvHUtPNxzuWe5z6BEk3q54YRyGlBXkSsmfoih7zNrBvl5A9A98anlp/7JbyZcWIJ8KXo/3Tq/DjFLtuQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + '@typescript-eslint/parser': ^8.59.2 + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/parser@8.59.2': + resolution: {integrity: sha512-plR3pp6D+SSUn1HM7xvSkx12/DhoHInI2YF35KAcVFNZvlC0gtrWqx7Qq1oH2Ssgi0vlFRCTbP+DZc7B9+TtsQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/project-service@8.59.2': + resolution: {integrity: sha512-+2hqvEkeyf/0FBor67duF0Ll7Ot8jyKzDQOSrxazF/danillRq2DwR9dLptsXpoZQqxE1UisSmoZewrlPas9Vw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/scope-manager@8.59.2': + resolution: {integrity: sha512-JzfyEpEtOU89CcFSwyNS3mu4MLvLSXqnmX05+aKBDM+TdR5jzcGOEBwxwGNxrEQ7p/z6kK2WyioCGBf2zZBnvg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/tsconfig-utils@8.59.2': + resolution: {integrity: sha512-BKK4alN7oi4C/zv4VqHQ+uRU+lTa6JGIZ7s1juw7b3RHo9OfKB+bKX3u0iVZetdsUCBBkSbdWbarJbmN0fTeSw==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/type-utils@8.59.2': + resolution: {integrity: sha512-nhqaj1nmTdVVl/BP5omXNRGO38jn5iosis2vbdmupF2txCf8ylWT8lx+JlvMYYVqzGVKtjojUFoQ3JRWK+mfzQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/types@8.59.2': + resolution: {integrity: sha512-e82GVOE8Ps3E++Egvb6Y3Dw0S10u8NkQ9KXmtRhCWJJ8kDhOJTvtMAWnFL16kB1583goCWXsr0NieKCZMs2/0Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@typescript-eslint/typescript-estree@8.59.2': + resolution: {integrity: sha512-o0XPGNwcWw+FIwStOWn+BwBuEmL6QXP0rsvAFg7ET1dey1Nr6Wb1ac8p5HEsK0ygO/6mUxlk+YWQD9xcb/nnXg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/utils@8.59.2': + resolution: {integrity: sha512-Juw3EinkXqjaffxz6roowvV7GZT/kET5vSKKZT6upl5TXdWkLkYmNPXwDDL2Vkt2DPn0nODIS4egC/0AGxKo/Q==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + peerDependencies: + eslint: ^8.57.0 || ^9.0.0 || ^10.0.0 + typescript: '>=4.8.4 <6.1.0' + + '@typescript-eslint/visitor-keys@8.59.2': + resolution: {integrity: sha512-NwjLUnGy8/Zfx23fl50tRC8rYaYnM52xNRYFAXvmiil9yh1+K6aRVQMnzW6gQB/1DLgWt977lYQn7C+wtgXZiA==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + '@ungap/structured-clone@1.3.1': + resolution: {integrity: sha512-mUFwbeTqrVgDQxFveS+df2yfap6iuP20NAKAsBt5jDEoOTDew+zwLAOilHCeQJOVSvmgCX4ogqIrA0mnyr08yQ==} + + '@vitejs/plugin-react@6.0.1': + resolution: {integrity: sha512-l9X/E3cDb+xY3SWzlG1MOGt2usfEHGMNIaegaUGFsLkb3RCn/k8/TOXBcab+OndDI4TBtktT8/9BwwW8Vi9KUQ==} + engines: {node: ^20.19.0 || >=22.12.0} + peerDependencies: + '@rolldown/plugin-babel': ^0.1.7 || ^0.2.0 + babel-plugin-react-compiler: ^1.0.0 + vite: ^8.0.0 + peerDependenciesMeta: + '@rolldown/plugin-babel': + optional: true + babel-plugin-react-compiler: + optional: true + + '@whatwg-node/disposablestack@0.0.6': + resolution: {integrity: sha512-LOtTn+JgJvX8WfBVJtF08TGrdjuFzGJc4mkP8EdDI8ADbvO7kiexYep1o8dwnt0okb0jYclCDXF13xU7Ge4zSw==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/events@0.1.2': + resolution: {integrity: sha512-ApcWxkrs1WmEMS2CaLLFUEem/49erT3sxIVjpzU5f6zmVcnijtDSrhoK2zVobOIikZJdH63jdAXOrvjf6eOUNQ==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/fetch@0.10.13': + resolution: {integrity: sha512-b4PhJ+zYj4357zwk4TTuF2nEe0vVtOrwdsrNo5hL+u1ojXNhh1FgJ6pg1jzDlwlT4oBdzfSwaBwMCtFCsIWg8Q==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/node-fetch@0.8.5': + resolution: {integrity: sha512-4xzCl/zphPqlp9tASLVeUhB5+WJHbuWGYpfoC2q1qh5dw0AqZBW7L27V5roxYWijPxj4sspRAAoOH3d2ztaHUQ==} + engines: {node: '>=18.0.0'} + + '@whatwg-node/promise-helpers@1.3.2': + resolution: {integrity: sha512-Nst5JdK47VIl9UcGwtv2Rcgyn5lWtZ0/mhRQ4G8NN2isxpq2TO30iqHzmwoJycjWuyUfg3GFXqP/gFHXeV57IA==} + engines: {node: '>=16.0.0'} + + '@whatwg-node/server@0.10.18': + resolution: {integrity: sha512-kMwLlxUbduttIgaPdSkmEarFpP+mSY8FEm+QWMBRJwxOHWkri+cxd8KZHO9EMrB9vgUuz+5WEaCawaL5wGVoXg==} + engines: {node: '>=18.0.0'} + + '@x0k/json-schema-merge@1.0.2': + resolution: {integrity: sha512-1734qiJHNX3+cJGDMMw2yz7R+7kpbAtl5NdPs1c/0gO5kYT6s4dMbLXiIfpZNsOYhGZI3aH7FWrj4Zxz7epXNg==} + + '@zudoku/httpsnippet@10.0.9': + resolution: {integrity: sha512-qmxuSxmTEEt6bc5j1cChljbztIphkuYMj3bbmT2W54ePGwCk6Iea7vPutHVw7NnCIW6cRI//w+u7xGOfOhwPCw==} + engines: {node: '>=18'} + + '@zudoku/react-helmet-async@2.0.5': + resolution: {integrity: sha512-wDakXPJEiQb4JmacSqPyAiJPdOBFqp6e3VUbm1BtTEkXo6FrC2nN+GmIniQ2OimBEC042rSZ9JdQBX+wou/MKw==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + '@zuplo/mcp@0.0.32': + resolution: {integrity: sha512-rpLTpwL+g1sXtylu2gZwCTXdNIV2L0LPOcCdwogpP5hdyp/JMLSAVtd7H5fs5Wt2cq3jTm6nn7ZZvsBAECdl3A==} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + + acorn-jsx@5.3.2: + resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + acorn@8.16.0: + resolution: {integrity: sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==} + engines: {node: '>=0.4.0'} + hasBin: true + + ajv-draft-04@1.0.0: + resolution: {integrity: sha512-mv00Te6nmYbRp5DCwclxtt7yV/joXJPGS7nM+97GdxvuttCOfgI3K4U25zboyeX0O+myI8ERluxQe5wljMmVIw==} + peerDependencies: + ajv: ^8.5.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv-formats@3.0.1: + resolution: {integrity: sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==} + peerDependencies: + ajv: ^8.0.0 + peerDependenciesMeta: + ajv: + optional: true + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + ansi-styles@4.3.0: + resolution: {integrity: sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==} + engines: {node: '>=8'} + + ansi-styles@6.2.3: + resolution: {integrity: sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==} + engines: {node: '>=12'} + + arg@5.0.2: + resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==} + + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + + argparse@2.0.1: + resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} + + aria-hidden@1.2.6: + resolution: {integrity: sha512-ik3ZgC9dY/lYVVM++OISsaYDeg1tb0VtP5uL3ouh1koGOaUMDPpbFIei4JkFimWUFPn90sbMNMXQAIVOlnYKJA==} + engines: {node: '>=10'} + + astring@1.9.0: + resolution: {integrity: sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==} + hasBin: true + + bail@2.0.2: + resolution: {integrity: sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==} + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + balanced-match@4.0.4: + resolution: {integrity: sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==} + engines: {node: 18 || 20 || >=22} + + base-x@5.0.1: + resolution: {integrity: sha512-M7uio8Zt++eg3jPj+rHMfCC+IuygQHHCOU+IYsVtik6FWjuYpVt/+MRKcgsAMHh8mMFAwnB+Bs+mTrFiXjMzKg==} + + brace-expansion@1.1.14: + resolution: {integrity: sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==} + + brace-expansion@5.0.5: + resolution: {integrity: sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==} + engines: {node: 18 || 20 || >=22} + + bs58@6.0.0: + resolution: {integrity: sha512-PD0wEnEYg6ijszw/u8s+iI3H17cTymlrwkKhDhPZq+Sokl3AU4htyBFTjAeNAlCCmg0f53g6ih3jATyCKftTfw==} + + call-bind-apply-helpers@1.0.2: + resolution: {integrity: sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==} + engines: {node: '>= 0.4'} + + call-bound@1.0.4: + resolution: {integrity: sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==} + engines: {node: '>= 0.4'} + + callsites@3.1.0: + resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==} + engines: {node: '>=6'} + + ccount@2.0.1: + resolution: {integrity: sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==} + + chalk@4.1.2: + resolution: {integrity: sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==} + engines: {node: '>=10'} + + character-entities-html4@2.1.0: + resolution: {integrity: sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==} + + character-entities-legacy@3.0.0: + resolution: {integrity: sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==} + + character-entities@2.0.2: + resolution: {integrity: sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==} + + character-reference-invalid@2.0.1: + resolution: {integrity: sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==} + + cjs-module-lexer@2.2.0: + resolution: {integrity: sha512-4bHTS2YuzUvtoLjdy+98ykbNB5jS0+07EvFNXerqZQJ89F7DI6ET7OQo/HJuW6K0aVsKA9hj9/RVb2kQVOrPDQ==} + + class-variance-authority@0.7.1: + resolution: {integrity: sha512-Ka+9Trutv7G8M6WT6SeiRWz792K5qEqIGEGzXKhAE6xOWAY6pPH8U+9IY3oCMv6kqTmLsv7Xh/2w2RigkePMsg==} + + cliui@9.0.1: + resolution: {integrity: sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==} + engines: {node: '>=20'} + + clsx@2.1.1: + resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==} + engines: {node: '>=6'} + + cmdk@1.1.1: + resolution: {integrity: sha512-Vsv7kFaXm+ptHDMZ7izaRsP70GgrW9NBNGswt9OZaVBLlE0SNpDq8eu/VGXyF9r7M0azK3Wy7OlYXsuyYLFzHg==} + peerDependencies: + react: ^18 || ^19 || ^19.0.0-rc + react-dom: ^18 || ^19 || ^19.0.0-rc + + collapse-white-space@2.1.0: + resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==} + + color-convert@2.0.1: + resolution: {integrity: sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==} + engines: {node: '>=7.0.0'} + + color-name@1.1.4: + resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + + comma-separated-tokens@2.0.3: + resolution: {integrity: sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==} + + concat-map@0.0.1: + resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} + + cookie@1.1.1: + resolution: {integrity: sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==} + engines: {node: '>=18'} + + cross-inspect@1.0.1: + resolution: {integrity: sha512-Pcw1JTvZLSJH83iiGWt6fRcT+BjZlCDRVwYLbUcHzv/CRpB7r0MlSrGbIyQvVSNyGnbt7G4AXuyCiDR3POvZ1A==} + engines: {node: '>=16.0.0'} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + cssesc@3.0.0: + resolution: {integrity: sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==} + engines: {node: '>=4'} + hasBin: true + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + decode-named-character-reference@1.3.0: + resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==} + + deep-is@0.1.4: + resolution: {integrity: sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==} + + delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + + dequal@2.0.3: + resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==} + engines: {node: '>=6'} + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + detect-node-es@1.1.0: + resolution: {integrity: sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==} + + devlop@1.1.0: + resolution: {integrity: sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==} + + dotenv@17.3.1: + resolution: {integrity: sha512-IO8C/dzEb6O3F9/twg6ZLXz164a2fhTnEWb95H23Dm4OuN+92NmEAlTrupP9VW6Jm3sO26tQlqyvyi4CsnY9GA==} + engines: {node: '>=12'} + + dunder-proto@1.0.1: + resolution: {integrity: sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==} + engines: {node: '>= 0.4'} + + embla-carousel-react@8.6.0: + resolution: {integrity: sha512-0/PjqU7geVmo6F734pmPqpyHqiM99olvyecY7zdweCw+6tKEXnrE90pBiBbMMU8s5tICemzpQ3hi5EpxzGW+JA==} + peerDependencies: + react: ^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + + embla-carousel-reactive-utils@8.6.0: + resolution: {integrity: sha512-fMVUDUEx0/uIEDM0Mz3dHznDhfX+znCCDCeIophYb1QGVM7YThSWX+wz11zlYwWFOr74b4QLGg0hrGPJeG2s4A==} + peerDependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: + resolution: {integrity: sha512-SjWyZBHJPbqxHOzckOfo8lHisEaJWmwd23XppYFYVh10bU66/Pn5tkVkbkCMZVdbUE5eTCI2nD8OyIP4Z+uwkA==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + enhanced-resolve@5.21.0: + resolution: {integrity: sha512-otxSQPw4lkOZWkHpB3zaEQs6gWYEsmX4xQF68ElXC/TWvGxGMSGOvoNbaLXm6/cS/fSfHtsEdw90y20PCd+sCA==} + engines: {node: '>=10.13.0'} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + es-define-property@1.0.1: + resolution: {integrity: sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==} + engines: {node: '>= 0.4'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + es-object-atoms@1.1.1: + resolution: {integrity: sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==} + engines: {node: '>= 0.4'} + + esast-util-from-estree@2.0.0: + resolution: {integrity: sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==} + + esast-util-from-js@2.0.1: + resolution: {integrity: sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==} + + esbuild@0.28.0: + resolution: {integrity: sha512-sNR9MHpXSUV/XB4zmsFKN+QgVG82Cc7+/aaxJ8Adi8hyOac+EXptIp45QBPaVyX3N70664wRbTcLTOemCAnyqw==} + engines: {node: '>=18'} + hasBin: true + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + escape-string-regexp@4.0.0: + resolution: {integrity: sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==} + engines: {node: '>=10'} + + escape-string-regexp@5.0.0: + resolution: {integrity: sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==} + engines: {node: '>=12'} + + eslint-scope@8.4.0: + resolution: {integrity: sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@3.4.3: + resolution: {integrity: sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==} + engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + + eslint-visitor-keys@4.2.1: + resolution: {integrity: sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + eslint-visitor-keys@5.0.1: + resolution: {integrity: sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==} + engines: {node: ^20.19.0 || ^22.13.0 || >=24} + + eslint@9.39.4: + resolution: {integrity: sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + hasBin: true + peerDependencies: + jiti: '*' + peerDependenciesMeta: + jiti: + optional: true + + espree@10.4.0: + resolution: {integrity: sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==} + engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0} + + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + + esquery@1.7.0: + resolution: {integrity: sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==} + engines: {node: '>=0.10'} + + esrecurse@4.3.0: + resolution: {integrity: sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==} + engines: {node: '>=4.0'} + + estraverse@5.3.0: + resolution: {integrity: sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==} + engines: {node: '>=4.0'} + + estree-util-attach-comments@3.0.0: + resolution: {integrity: sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==} + + estree-util-build-jsx@3.0.1: + resolution: {integrity: sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==} + + estree-util-is-identifier-name@3.0.0: + resolution: {integrity: sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==} + + estree-util-scope@1.0.0: + resolution: {integrity: sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==} + + estree-util-to-js@2.0.0: + resolution: {integrity: sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==} + + estree-util-value-to-estree@3.5.0: + resolution: {integrity: sha512-aMV56R27Gv3QmfmF1MY12GWkGzzeAezAX+UplqHVASfjc9wNzI/X6hC0S9oxq61WT4aQesLGslWP9tKk6ghRZQ==} + + estree-util-visit@2.0.0: + resolution: {integrity: sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==} + + estree-walker@2.0.2: + resolution: {integrity: sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==} + + estree-walker@3.0.3: + resolution: {integrity: sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==} + + esutils@2.0.3: + resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} + engines: {node: '>=0.10.0'} + + eventsource-parser@3.0.8: + resolution: {integrity: sha512-70QWGkr4snxr0OXLRWsFLeRBIRPuQOvt4s8QYjmUlmlkyTZkRqS7EDVRZtzU3TiyDbXSzaOeF0XUKy8PchzukQ==} + engines: {node: '>=18.0.0'} + + extend-shallow@2.0.1: + resolution: {integrity: sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==} + engines: {node: '>=0.10.0'} + + extend@3.0.2: + resolution: {integrity: sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-equals@6.0.0: + resolution: {integrity: sha512-PFhhIGgdM79r5Uztdj9Zb6Tt1zKafqVfdMGwVca1z5z6fbX7DmsySSuJd8HiP6I1j505DCS83cLxo5rmSNeVEA==} + engines: {node: '>=6.0.0'} + + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + + fast-levenshtein@2.0.6: + resolution: {integrity: sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==} + + fast-printf@1.6.10: + resolution: {integrity: sha512-GwTgG9O4FVIdShhbVF3JxOgSBY2+ePGsu2V/UONgoCPzF9VY6ZdBMKsHKCYQHZwNk3qNouUolRDsgVxcVA5G1w==} + engines: {node: '>=10.0'} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + fault@2.0.1: + resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==} + + fdir@6.5.0: + resolution: {integrity: sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==} + engines: {node: '>=12.0.0'} + peerDependencies: + picomatch: ^3 || ^4 + peerDependenciesMeta: + picomatch: + optional: true + + file-entry-cache@8.0.0: + resolution: {integrity: sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==} + engines: {node: '>=16.0.0'} + + find-up@5.0.0: + resolution: {integrity: sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==} + engines: {node: '>=10'} + + flat-cache@4.0.1: + resolution: {integrity: sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==} + engines: {node: '>=16'} + + flatted@3.4.2: + resolution: {integrity: sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==} + + format@0.2.2: + resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==} + engines: {node: '>=0.4.x'} + + forwarded-parse@2.1.2: + resolution: {integrity: sha512-alTFZZQDKMporBH77856pXgzhEzaUVmLCDk+egLgIgHst3Tpndzz8MnKe+GzRJRfvVdn69HhpW7cmXzvtLvJAw==} + + framer-motion@12.38.0: + resolution: {integrity: sha512-rFYkY/pigbcswl1XQSb7q424kSTQ8q6eAC+YUsSKooHQYuLdzdHjrt6uxUC+PRAO++q5IS7+TamgIw1AphxR+g==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + fsevents@2.3.3: + resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} + engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} + os: [darwin] + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + get-caller-file@2.0.5: + resolution: {integrity: sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==} + engines: {node: 6.* || 8.* || >= 10.*} + + get-east-asian-width@1.5.0: + resolution: {integrity: sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==} + engines: {node: '>=18'} + + get-intrinsic@1.3.0: + resolution: {integrity: sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==} + engines: {node: '>= 0.4'} + + get-nonce@1.0.1: + resolution: {integrity: sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==} + engines: {node: '>=6'} + + get-own-enumerable-property-symbols@3.0.2: + resolution: {integrity: sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==} + + get-proto@1.0.1: + resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} + engines: {node: '>= 0.4'} + + github-slugger@2.0.0: + resolution: {integrity: sha512-IaOQ9puYtjrkq7Y0Ygl9KDZnrf/aiUJYUpVf89y8kyaxbRG7Y1SrX/jaumrv81vc61+kiMempujsM3Yw7w5qcw==} + + glob-parent@6.0.2: + resolution: {integrity: sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==} + engines: {node: '>=10.13.0'} + + glob@13.0.6: + resolution: {integrity: sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==} + engines: {node: 18 || 20 || >=22} + + globals@14.0.0: + resolution: {integrity: sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==} + engines: {node: '>=18'} + + gopd@1.2.0: + resolution: {integrity: sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==} + engines: {node: '>= 0.4'} + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + graphql-type-json@0.3.2: + resolution: {integrity: sha512-J+vjof74oMlCWXSvt0DOf2APEdZOCdubEvGDUAlqH//VBYcOYsGgRW7Xzorr44LvkjiuvecWc8fChxuZZbChtg==} + peerDependencies: + graphql: '>=0.8.0' + + graphql-yoga@5.18.0: + resolution: {integrity: sha512-xFt1DVXS1BZ3AvjnawAGc5OYieSe56WuQuyk3iEpBwJ3QDZJWQGLmU9z/L5NUZ+pUcyprsz/bOwkYIV96fXt/g==} + engines: {node: '>=18.0.0'} + peerDependencies: + graphql: ^15.2.0 || ^16.0.0 + + graphql@16.13.2: + resolution: {integrity: sha512-5bJ+nf/UCpAjHM8i06fl7eLyVC9iuNAjm9qzkiu2ZGhM0VscSvS6WDPfAwkdkBuoXGM9FJSbKl6wylMwP9Ktig==} + engines: {node: ^12.22.0 || ^14.16.0 || ^16.0.0 || >=17.0.0} + + gray-matter@4.0.3: + resolution: {integrity: sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==} + engines: {node: '>=6.0'} + + has-flag@4.0.0: + resolution: {integrity: sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==} + engines: {node: '>=8'} + + has-symbols@1.1.0: + resolution: {integrity: sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==} + engines: {node: '>= 0.4'} + + hasown@2.0.3: + resolution: {integrity: sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg==} + engines: {node: '>= 0.4'} + + hast-util-from-parse5@8.0.3: + resolution: {integrity: sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==} + + hast-util-heading-rank@3.0.0: + resolution: {integrity: sha512-EJKb8oMUXVHcWZTDepnr+WNbfnXKFNf9duMesmr4S8SXTJBJ9M4Yok08pu9vxdJwdlGRhVumk9mEhkEvKGifwA==} + + hast-util-parse-selector@4.0.0: + resolution: {integrity: sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==} + + hast-util-properties-to-mdx-jsx-attributes@1.1.1: + resolution: {integrity: sha512-MMrAoGgvhYULEqMB/r6AlcVz1D3Cyml/9cMB2NIqZsIsEJ+XEXPMqH0gjba8dVs9AnQUYvPReAS+OIYx4ip+Ug==} + + hast-util-raw@9.1.0: + resolution: {integrity: sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==} + + hast-util-to-estree@3.1.3: + resolution: {integrity: sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==} + + hast-util-to-html@9.0.5: + resolution: {integrity: sha512-OguPdidb+fbHQSU4Q4ZiLKnzWo8Wwsf5bZfbvu7//a9oTYoqD/fWpe96NuHkoS9h0ccGOTe0C4NGXdtS0iObOw==} + + hast-util-to-jsx-runtime@2.3.6: + resolution: {integrity: sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==} + + hast-util-to-parse5@8.0.1: + resolution: {integrity: sha512-MlWT6Pjt4CG9lFCjiz4BH7l9wmrMkfkJYCxFwKQic8+RTZgWPuWxwAfjJElsXkex7DJjfSJsQIt931ilUgmwdA==} + + hast-util-to-string@3.0.1: + resolution: {integrity: sha512-XelQVTDWvqcl3axRfI0xSeoVKzyIFPwsAGSLIsKdJKQMXDYJS4WYrBNF/8J7RdhIcFI2BOHgAifggsvsxp/3+A==} + + hast-util-whitespace@3.0.0: + resolution: {integrity: sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==} + + hastscript@9.0.1: + resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + + hoist-non-react-statics@3.3.2: + resolution: {integrity: sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==} + + hono@4.12.14: + resolution: {integrity: sha512-am5zfg3yu6sqn5yjKBNqhnTX7Cv+m00ox+7jbaKkrLMRJ4rAdldd1xPd/JzbBWspqaQv6RSTrgFN95EsfhC+7w==} + engines: {node: '>=16.9.0'} + + html-url-attributes@3.0.1: + resolution: {integrity: sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==} + + html-void-elements@3.0.0: + resolution: {integrity: sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==} + + http-terminator@3.2.0: + resolution: {integrity: sha512-JLjck1EzPaWjsmIf8bziM3p9fgR1Y3JoUKAkyYEbZmFrIvJM6I8vVJfBGWlEtV9IWOvzNnaTtjuwZeBY2kwB4g==} + engines: {node: '>=14'} + + ignore@5.3.2: + resolution: {integrity: sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==} + engines: {node: '>= 4'} + + ignore@7.0.5: + resolution: {integrity: sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==} + engines: {node: '>= 4'} + + import-fresh@3.3.1: + resolution: {integrity: sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==} + engines: {node: '>=6'} + + import-in-the-middle@2.0.6: + resolution: {integrity: sha512-3vZV3jX0XRFW3EJDTwzWoZa+RH1b8eTTx6YOCjglrLyPuepwoBti1k3L2dKwdCUrnVEfc5CuRuGstaC/uQJJaw==} + + import-in-the-middle@3.0.1: + resolution: {integrity: sha512-pYkiyXVL2Mf3pozdlDGV6NAObxQx13Ae8knZk1UJRJ6uRW/ZRmTGHlQYtrsSl7ubuE5F8CD1z+s1n4RHNuTtuA==} + engines: {node: '>=18'} + + imurmurhash@0.1.4: + resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} + engines: {node: '>=0.8.19'} + + inline-style-parser@0.2.7: + resolution: {integrity: sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==} + + invariant@2.2.4: + resolution: {integrity: sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==} + + is-alphabetical@2.0.1: + resolution: {integrity: sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==} + + is-alphanumerical@2.0.1: + resolution: {integrity: sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==} + + is-decimal@2.0.1: + resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==} + + is-extendable@0.1.1: + resolution: {integrity: sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==} + engines: {node: '>=0.10.0'} + + is-extglob@2.1.1: + resolution: {integrity: sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==} + engines: {node: '>=0.10.0'} + + is-glob@4.0.3: + resolution: {integrity: sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==} + engines: {node: '>=0.10.0'} + + is-hexadecimal@2.0.1: + resolution: {integrity: sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==} + + is-obj@1.0.1: + resolution: {integrity: sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==} + engines: {node: '>=0.10.0'} + + is-plain-obj@4.1.0: + resolution: {integrity: sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==} + engines: {node: '>=12'} + + is-regexp@1.0.0: + resolution: {integrity: sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==} + engines: {node: '>=0.10.0'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + javascript-stringify@2.1.0: + resolution: {integrity: sha512-JVAfqNPTvNq3sB/VHQJAFxN/sPgKnsKrCwyRt15zwNCdrMMJDdcEOdubuy+DuJYYdm0ox1J4uzEuYKkN+9yhVg==} + + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + js-yaml@3.14.2: + resolution: {integrity: sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==} + hasBin: true + + js-yaml@4.1.1: + resolution: {integrity: sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==} + hasBin: true + + json-buffer@3.0.1: + resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} + + json-schema-to-typescript-lite@15.0.0: + resolution: {integrity: sha512-5mMORSQm9oTLyjM4mWnyNBi2T042Fhg1/0gCIB6X8U/LVpM2A+Nmj2yEyArqVouDmFThDxpEXcnTgSrjkGJRFA==} + + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + + json-stable-stringify-without-jsonify@1.0.1: + resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} + + jsonpointer@5.0.1: + resolution: {integrity: sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==} + engines: {node: '>=0.10.0'} + + keyv@4.5.4: + resolution: {integrity: sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==} + + kind-of@6.0.3: + resolution: {integrity: sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==} + engines: {node: '>=0.10.0'} + + leven@4.1.0: + resolution: {integrity: sha512-KZ9W9nWDT7rF7Dazg8xyLHGLrmpgq2nVNFUckhqdW3szVP6YhCpp/RAnpmVExA9JvrMynjwSLVrEj3AepHR6ew==} + engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} + + levn@0.4.1: + resolution: {integrity: sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==} + engines: {node: '>= 0.8.0'} + + lightningcss-android-arm64@1.32.0: + resolution: {integrity: sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [android] + + lightningcss-darwin-arm64@1.32.0: + resolution: {integrity: sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [darwin] + + lightningcss-darwin-x64@1.32.0: + resolution: {integrity: sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [darwin] + + lightningcss-freebsd-x64@1.32.0: + resolution: {integrity: sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [freebsd] + + lightningcss-linux-arm-gnueabihf@1.32.0: + resolution: {integrity: sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==} + engines: {node: '>= 12.0.0'} + cpu: [arm] + os: [linux] + + lightningcss-linux-arm64-gnu@1.32.0: + resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [glibc] + + lightningcss-linux-arm64-musl@1.32.0: + resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [linux] + libc: [musl] + + lightningcss-linux-x64-gnu@1.32.0: + resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [glibc] + + lightningcss-linux-x64-musl@1.32.0: + resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [linux] + libc: [musl] + + lightningcss-win32-arm64-msvc@1.32.0: + resolution: {integrity: sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==} + engines: {node: '>= 12.0.0'} + cpu: [arm64] + os: [win32] + + lightningcss-win32-x64-msvc@1.32.0: + resolution: {integrity: sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==} + engines: {node: '>= 12.0.0'} + cpu: [x64] + os: [win32] + + lightningcss@1.32.0: + resolution: {integrity: sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==} + engines: {node: '>= 12.0.0'} + + locate-path@6.0.0: + resolution: {integrity: sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==} + engines: {node: '>=10'} + + lodash.merge@4.6.2: + resolution: {integrity: sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==} + + loglevel@1.9.2: + resolution: {integrity: sha512-HgMmCqIJSAKqo68l0rS2AanEWfkxaZ5wNiEFb5ggm08lDs9Xl2KxBlX3PTcaD2chBM1gXAYf491/M2Rv8Jwayg==} + engines: {node: '>= 0.6.0'} + + longest-streak@3.1.0: + resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + + loose-envify@1.4.0: + resolution: {integrity: sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==} + hasBin: true + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@11.3.6: + resolution: {integrity: sha512-Gf/KoL3C/MlI7Bt0PGI9I+TeTC/I6r/csU58N4BSNc4lppLBeKsOdFYkK+dX0ABDUMJNfCHTyPpzwwO21Awd3A==} + engines: {node: 20 || >=22} + + lucide-react@1.8.0: + resolution: {integrity: sha512-WuvlsjngSk7TnTBJ1hsCy3ql9V9VOdcPkd3PKcSmM34vJD8KG6molxz7m7zbYFgICwsanQWmJ13JlYs4Zp7Arw==} + peerDependencies: + react: ^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + magic-string@0.30.21: + resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + + markdown-extensions@2.0.0: + resolution: {integrity: sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==} + engines: {node: '>=16'} + + markdown-table@3.0.4: + resolution: {integrity: sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==} + + math-intrinsics@1.1.0: + resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} + engines: {node: '>= 0.4'} + + mdast-util-directive@3.1.0: + resolution: {integrity: sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==} + + mdast-util-find-and-replace@3.0.2: + resolution: {integrity: sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==} + + mdast-util-from-markdown@2.0.2: + resolution: {integrity: sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==} + + mdast-util-frontmatter@2.0.1: + resolution: {integrity: sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==} + + mdast-util-gfm-autolink-literal@2.0.1: + resolution: {integrity: sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==} + + mdast-util-gfm-footnote@2.1.0: + resolution: {integrity: sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==} + + mdast-util-gfm-strikethrough@2.0.0: + resolution: {integrity: sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==} + + mdast-util-gfm-table@2.0.0: + resolution: {integrity: sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==} + + mdast-util-gfm-task-list-item@2.0.0: + resolution: {integrity: sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==} + + mdast-util-gfm@3.1.0: + resolution: {integrity: sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==} + + mdast-util-mdx-expression@2.0.1: + resolution: {integrity: sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==} + + mdast-util-mdx-jsx@3.2.0: + resolution: {integrity: sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==} + + mdast-util-mdx@3.0.0: + resolution: {integrity: sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==} + + mdast-util-mdxjs-esm@2.0.1: + resolution: {integrity: sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==} + + mdast-util-phrasing@4.1.0: + resolution: {integrity: sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==} + + mdast-util-to-hast@13.2.1: + resolution: {integrity: sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==} + + mdast-util-to-markdown@2.1.2: + resolution: {integrity: sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==} + + mdast-util-to-string@4.0.0: + resolution: {integrity: sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==} + + micromark-core-commonmark@2.0.3: + resolution: {integrity: sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==} + + micromark-extension-directive@3.0.2: + resolution: {integrity: sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==} + + micromark-extension-frontmatter@2.0.0: + resolution: {integrity: sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==} + + micromark-extension-gfm-autolink-literal@2.1.0: + resolution: {integrity: sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==} + + micromark-extension-gfm-footnote@2.1.0: + resolution: {integrity: sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==} + + micromark-extension-gfm-strikethrough@2.1.0: + resolution: {integrity: sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==} + + micromark-extension-gfm-table@2.1.1: + resolution: {integrity: sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==} + + micromark-extension-gfm-tagfilter@2.0.0: + resolution: {integrity: sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==} + + micromark-extension-gfm-task-list-item@2.1.0: + resolution: {integrity: sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==} + + micromark-extension-gfm@3.0.0: + resolution: {integrity: sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==} + + micromark-extension-mdx-expression@3.0.1: + resolution: {integrity: sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==} + + micromark-extension-mdx-jsx@3.0.2: + resolution: {integrity: sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==} + + micromark-extension-mdx-md@2.0.0: + resolution: {integrity: sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==} + + micromark-extension-mdxjs-esm@3.0.0: + resolution: {integrity: sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==} + + micromark-extension-mdxjs@3.0.0: + resolution: {integrity: sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==} + + micromark-factory-destination@2.0.1: + resolution: {integrity: sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==} + + micromark-factory-label@2.0.1: + resolution: {integrity: sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==} + + micromark-factory-mdx-expression@2.0.3: + resolution: {integrity: sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==} + + micromark-factory-space@1.1.0: + resolution: {integrity: sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==} + + micromark-factory-space@2.0.1: + resolution: {integrity: sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==} + + micromark-factory-title@2.0.1: + resolution: {integrity: sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==} + + micromark-factory-whitespace@2.0.1: + resolution: {integrity: sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==} + + micromark-util-character@1.2.0: + resolution: {integrity: sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==} + + micromark-util-character@2.1.1: + resolution: {integrity: sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==} + + micromark-util-chunked@2.0.1: + resolution: {integrity: sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==} + + micromark-util-classify-character@2.0.1: + resolution: {integrity: sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==} + + micromark-util-combine-extensions@2.0.1: + resolution: {integrity: sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==} + + micromark-util-decode-numeric-character-reference@2.0.2: + resolution: {integrity: sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==} + + micromark-util-decode-string@2.0.1: + resolution: {integrity: sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==} + + micromark-util-encode@2.0.1: + resolution: {integrity: sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==} + + micromark-util-events-to-acorn@2.0.3: + resolution: {integrity: sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==} + + micromark-util-html-tag-name@2.0.1: + resolution: {integrity: sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==} + + micromark-util-normalize-identifier@2.0.1: + resolution: {integrity: sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==} + + micromark-util-resolve-all@2.0.1: + resolution: {integrity: sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==} + + micromark-util-sanitize-uri@2.0.1: + resolution: {integrity: sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==} + + micromark-util-subtokenize@2.1.0: + resolution: {integrity: sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==} + + micromark-util-symbol@1.1.0: + resolution: {integrity: sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==} + + micromark-util-symbol@2.0.1: + resolution: {integrity: sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==} + + micromark-util-types@1.1.0: + resolution: {integrity: sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==} + + micromark-util-types@2.0.2: + resolution: {integrity: sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==} + + micromark@4.0.2: + resolution: {integrity: sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==} + + minimatch@10.2.5: + resolution: {integrity: sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==} + engines: {node: 18 || 20 || >=22} + + minimatch@3.1.5: + resolution: {integrity: sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + module-details-from-path@1.0.4: + resolution: {integrity: sha512-EGWKgxALGMgzvxYF1UyGTy0HXX/2vHLkw6+NvDKW2jypWbHpjQuj4UMcqQWXHERJhVGKikolT06G3bcKe4fi7w==} + + motion-dom@12.38.0: + resolution: {integrity: sha512-pdkHLD8QYRp8VfiNLb8xIBJis1byQ9gPT3Jnh2jqfFtAsWUA3dEepDlsWe/xMpO8McV+VdpKVcp+E+TGJEtOoA==} + + motion-utils@12.36.0: + resolution: {integrity: sha512-eHWisygbiwVvf6PZ1vhaHCLamvkSbPIeAYxWUuL3a2PD/TROgE7FvfHWTIH4vMl798QLfMw15nRqIaRDXTlYRg==} + + motion@12.35.1: + resolution: {integrity: sha512-yEt/49kWC0VU/IEduDfeZw82eDemlPwa1cyo/gcEEUCN4WgpSJpUcxz6BUwakGabvJiTzLQ58J73515I5tfykQ==} + peerDependencies: + '@emotion/is-prop-valid': '*' + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@emotion/is-prop-valid': + optional: true + react: + optional: true + react-dom: + optional: true + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + nanoevents@9.1.0: + resolution: {integrity: sha512-Jd0fILWG44a9luj8v5kED4WI+zfkkgwKyRQKItTtlPfEsh7Lznfi1kr8/iZ+XAIss4Qq5GqRB0qtWbaz9ceO/A==} + engines: {node: ^18.0.0 || >=20.0.0} + + nanoid@3.3.12: + resolution: {integrity: sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==} + engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} + hasBin: true + + natural-compare@1.4.0: + resolution: {integrity: sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==} + + next-themes@0.4.6: + resolution: {integrity: sha512-pZvgD5L0IEvX5/9GWyHMf3m8BKiVQwsCMHfoFosXtXBMnaS0ZnIJ9ST4b4NqLVKDEm8QBxoNNGNaBv2JNF6XNA==} + peerDependencies: + react: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + react-dom: ^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc + + oauth4webapi@3.8.5: + resolution: {integrity: sha512-A8jmyUckVhRJj5lspguklcl90Ydqk61H3dcU0oLhH3Yv13KpAliKTt5hknpGGPZSSfOwGyraNEFmofDYH+1kSg==} + + object-inspect@1.13.4: + resolution: {integrity: sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==} + engines: {node: '>= 0.4'} + + oniguruma-parser@0.12.2: + resolution: {integrity: sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==} + + oniguruma-to-es@4.3.6: + resolution: {integrity: sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==} + + openapi-types@12.1.3: + resolution: {integrity: sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==} + + optionator@0.9.4: + resolution: {integrity: sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==} + engines: {node: '>= 0.8.0'} + + p-finally@1.0.0: + resolution: {integrity: sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==} + engines: {node: '>=4'} + + p-limit@3.1.0: + resolution: {integrity: sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==} + engines: {node: '>=10'} + + p-locate@5.0.0: + resolution: {integrity: sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==} + engines: {node: '>=10'} + + p-timeout@3.2.0: + resolution: {integrity: sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==} + engines: {node: '>=8'} + + p-wait-for@3.2.0: + resolution: {integrity: sha512-wpgERjNkLrBiFmkMEjuZJEWKKDrNfHCKA1OhyN1wg1FrLkULbviEy6py1AyJUgZ72YWFbZ38FIpnqvVqAlDUwA==} + engines: {node: '>=8'} + + pagefind@1.5.2: + resolution: {integrity: sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==} + hasBin: true + + parent-module@1.0.1: + resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} + engines: {node: '>=6'} + + parse-entities@4.0.2: + resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} + + parse-srcset@1.0.2: + resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-exists@4.0.0: + resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} + engines: {node: '>=8'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-scurry@2.0.2: + resolution: {integrity: sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==} + engines: {node: 18 || 20 || >=22} + + pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + + pg-protocol@1.13.0: + resolution: {integrity: sha512-zzdvXfS6v89r6v7OcFCHfHlyG/wvry1ALxZo4LqgUoy7W9xhBDMaqOuMiF3qEV45VqsN6rdlcehHrfDtlCPc8w==} + + pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + + piscina@5.1.4: + resolution: {integrity: sha512-7uU4ZnKeQq22t9AsmHGD2w4OYQGonwFnTypDypaWi7Qr2EvQIFVtG8J5D/3bE7W123Wdc9+v4CZDu5hJXVCtBg==} + engines: {node: '>=20.x'} + + postcss-selector-parser@6.0.10: + resolution: {integrity: sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==} + engines: {node: '>=4'} + + postcss@8.5.14: + resolution: {integrity: sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==} + engines: {node: ^10 || ^12 || >=14} + + postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + + postgres-bytea@1.0.1: + resolution: {integrity: sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ==} + engines: {node: '>=0.10.0'} + + postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + + postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + + posthog-node@5.26.0: + resolution: {integrity: sha512-DK1XF/RiunhvT57cFyPxW9OaliZzl5aREHFwY/AISL3MVOaDUb4wIccMn0G3ws3Ounen8iGH7xvzZQ0x2vEOEQ==} + engines: {node: ^20.20.0 || >=22.22.0} + + prelude-ls@1.2.1: + resolution: {integrity: sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==} + engines: {node: '>= 0.8.0'} + + property-information@7.1.0: + resolution: {integrity: sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==} + + punycode@1.4.1: + resolution: {integrity: sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==} + + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + + qs@6.15.1: + resolution: {integrity: sha512-6YHEFRL9mfgcAvql/XhwTvf5jKcOiiupt2FiJxHkiX1z4j7WL8J/jRHYLluORvc1XxB5rV20KoeK00gVJamspg==} + engines: {node: '>=0.6'} + + react-dom@19.2.5: + resolution: {integrity: sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==} + peerDependencies: + react: ^19.2.5 + + react-error-boundary@6.1.1: + resolution: {integrity: sha512-BrYwPOdXi5mqkk5lw+Uvt0ThHx32rCt3BkukS4X23A2AIWDPSGX6iaWTc0y9TU/mHDA/6qOSGel+B2ERkOvD1w==} + peerDependencies: + react: ^18.0.0 || ^19.0.0 + + react-fast-compare@3.2.2: + resolution: {integrity: sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==} + + react-hook-form@7.71.2: + resolution: {integrity: sha512-1CHvcDYzuRUNOflt4MOq3ZM46AronNJtQ1S7tnX6YN4y72qhgiUItpacZUAQ0TyWYci3yz1X+rXaSxiuEm86PA==} + engines: {node: '>=18.0.0'} + peerDependencies: + react: ^16.8.0 || ^17 || ^18 || ^19 + + react-is@16.13.1: + resolution: {integrity: sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==} + + react-is@19.2.5: + resolution: {integrity: sha512-Dn0t8IQhCmeIT3wu+Apm1/YVsJXsGWi6k4sPdnBIdqMVtHtv0IGi6dcpNpNkNac0zB2uUAqNX3MHzN8c+z2rwQ==} + + react-markdown@10.1.0: + resolution: {integrity: sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==} + peerDependencies: + '@types/react': '>=18' + react: '>=18' + + react-remove-scroll-bar@2.3.8: + resolution: {integrity: sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + peerDependenciesMeta: + '@types/react': + optional: true + + react-remove-scroll@2.7.2: + resolution: {integrity: sha512-Iqb9NjCCTt6Hf+vOdNIZGdTiH1QSqr27H/Ek9sv/a97gfueI/5h1s3yRi1nngzMUaOOToin5dI1dXKdXiF+u0Q==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react-router@7.14.1: + resolution: {integrity: sha512-5BCvFskyAAVumqhEKh/iPhLOIkfxcEUz8WqFIARCkMg8hZZzDYX9CtwxXA0e+qT8zAxmMC0x3Ckb9iMONwc5jg==} + engines: {node: '>=20.0.0'} + peerDependencies: + react: '>=18' + react-dom: '>=18' + peerDependenciesMeta: + react-dom: + optional: true + + react-style-singleton@2.2.3: + resolution: {integrity: sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + react@19.2.5: + resolution: {integrity: sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==} + engines: {node: '>=0.10.0'} + + recma-build-jsx@1.0.0: + resolution: {integrity: sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==} + + recma-jsx@1.0.1: + resolution: {integrity: sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==} + peerDependencies: + acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 + + recma-parse@1.0.0: + resolution: {integrity: sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==} + + recma-stringify@1.0.0: + resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==} + + regex-recursion@6.0.2: + resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==} + + regex-utilities@2.3.0: + resolution: {integrity: sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==} + + regex@6.1.0: + resolution: {integrity: sha512-6VwtthbV4o/7+OaAF9I5L5V3llLEsoPyq9P1JVXkedTP33c7MfCG0/5NOPcSJn0TzXcG9YUrR0gQSWioew3LDg==} + + rehype-mdx-import-media@1.2.0: + resolution: {integrity: sha512-rf+2qnPv3LTqLtCr8GjhHUja2TEbmwWtD1o4jigrmGWbVDggOMxyNeqJhGpC4E3vtH+sY+a+u9WPSEaskEWPFA==} + + rehype-raw@7.0.0: + resolution: {integrity: sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==} + + rehype-recma@1.0.0: + resolution: {integrity: sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==} + + rehype-slug@6.0.0: + resolution: {integrity: sha512-lWyvf/jwu+oS5+hL5eClVd3hNdmwM1kAC0BUvEGD19pajQMIzcNUd/k9GsfQ+FfECvX+JE+e9/btsKH0EjJT6A==} + + remark-comment@1.0.0: + resolution: {integrity: sha512-k8YPo5MGvl8l4gGxOH6Zk4Fa2AhDACN5eqKnKZcHDORZQS15hlnezlBHj2lqyDiqzApNmYOMTibkEJbMSKU25w==} + + remark-directive-rehype@1.0.0: + resolution: {integrity: sha512-10XpgKG/v5pqSpp/lLXEqqJ+EeHY1mhmcgSgno1Zw+PygBvkMguY9X39dj5sMkaMt0GbUmlhWidbBqLzFLZmXg==} + engines: {node: '>=20.0.0'} + + remark-directive@3.0.1: + resolution: {integrity: sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==} + + remark-frontmatter@5.0.0: + resolution: {integrity: sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==} + + remark-gfm@4.0.1: + resolution: {integrity: sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==} + + remark-mdx-frontmatter@5.2.0: + resolution: {integrity: sha512-U/hjUYTkQqNjjMRYyilJgLXSPF65qbLPdoESOkXyrwz2tVyhAnm4GUKhfXqOOS9W34M3545xEMq+aMpHgVjEeQ==} + + remark-mdx@3.1.1: + resolution: {integrity: sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==} + + remark-parse@11.0.0: + resolution: {integrity: sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==} + + remark-rehype@11.1.2: + resolution: {integrity: sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==} + + remark-stringify@11.0.0: + resolution: {integrity: sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + + require-in-the-middle@8.0.1: + resolution: {integrity: sha512-QT7FVMXfWOYFbeRBF6nu+I6tr2Tf3u0q8RIEjNob/heKY/nh7drD/k7eeMFmSQgnTtCzLDcCu/XEnpW2wk4xCQ==} + engines: {node: '>=9.3.0 || >=8.10.0 <9.0.0'} + + reselect@5.1.1: + resolution: {integrity: sha512-K/BG6eIky/SBpzfHZv/dd+9JBFiS4SWV7FIujVyJRux6e45+73RaUHXLmIR1f7WOMaQ0U1km6qwklRQxpJJY0w==} + + resolve-from@4.0.0: + resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} + engines: {node: '>=4'} + + roarr@7.21.4: + resolution: {integrity: sha512-qvfUKCrpPzhWmQ4NxRYnuwhkI5lwmObhBU06BCK/lpj6PID9nL4Hk6XDwek2foKI+TMaV+Yw//XZshGF2Lox/Q==} + engines: {node: '>=18.0'} + + rolldown@1.0.0-rc.16: + resolution: {integrity: sha512-rzi5WqKzEZw3SooTt7cgm4eqIoujPIyGcJNGFL7iPEuajQw7vxMHUkXylu4/vhCkJGXsgRmxqMKXUpT6FEgl0g==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + + rollup@4.60.3: + resolution: {integrity: sha512-pAQK9HalE84QSm4Po3EmWIZPd3FnjkShVkiMlz1iligWYkWQ7wHYd1PF/T7QZ5TVSD6uSTon5gBVMSM4JfBV+A==} + engines: {node: '>=18.0.0', npm: '>=8.0.0'} + hasBin: true + + safe-stable-stringify@2.5.0: + resolution: {integrity: sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA==} + engines: {node: '>=10'} + + sax@1.6.0: + resolution: {integrity: sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==} + engines: {node: '>=11.0.0'} + + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + + section-matter@1.0.0: + resolution: {integrity: sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==} + engines: {node: '>=4'} + + semver-compare@1.0.0: + resolution: {integrity: sha512-YM3/ITh2MJ5MtzaM429anh+x2jiLVjqILF4m4oyQB18W7Ggea7BfqdH/wGMK7dDiMghv/6WG7znWMwUDzJiXow==} + + semver@7.7.4: + resolution: {integrity: sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==} + engines: {node: '>=10'} + hasBin: true + + set-cookie-parser@2.7.2: + resolution: {integrity: sha512-oeM1lpU/UvhTxw+g3cIfxXHyJRc/uidd3yK1P242gzHds0udQBYzs3y8j4gCCW+ZJ7ad0yctld8RYO+bdurlvw==} + + shallowequal@1.1.0: + resolution: {integrity: sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + shiki@4.0.2: + resolution: {integrity: sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==} + engines: {node: '>=20'} + + side-channel-list@1.0.1: + resolution: {integrity: sha512-mjn/0bi/oUURjc5Xl7IaWi/OJJJumuoJFQJfDDyO46+hBWsfaVM65TBHq2eoZBhzl9EchxOijpkbRC8SVBQU0w==} + engines: {node: '>= 0.4'} + + side-channel-map@1.0.1: + resolution: {integrity: sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==} + engines: {node: '>= 0.4'} + + side-channel-weakmap@1.0.2: + resolution: {integrity: sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==} + engines: {node: '>= 0.4'} + + side-channel@1.1.0: + resolution: {integrity: sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==} + engines: {node: '>= 0.4'} + + sitemap@9.0.1: + resolution: {integrity: sha512-S6hzjGJSG3d6if0YoF5kTyeRJvia6FSTBroE5fQ0bu1QNxyJqhhinfUsXi9fH3MgtXODWvwo2BDyQSnhPQ88uQ==} + engines: {node: '>=20.19.5', npm: '>=10.8.2'} + hasBin: true + + source-map-js@1.2.1: + resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} + engines: {node: '>=0.10.0'} + + source-map@0.7.6: + resolution: {integrity: sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==} + engines: {node: '>= 12'} + + space-separated-tokens@2.0.2: + resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==} + + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + stringify-entities@4.0.4: + resolution: {integrity: sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==} + + stringify-object@3.3.0: + resolution: {integrity: sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==} + engines: {node: '>=4'} + + strip-ansi@7.2.0: + resolution: {integrity: sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==} + engines: {node: '>=12'} + + strip-bom-string@1.0.0: + resolution: {integrity: sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==} + engines: {node: '>=0.10.0'} + + strip-json-comments@3.1.1: + resolution: {integrity: sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==} + engines: {node: '>=8'} + + style-to-js@1.1.21: + resolution: {integrity: sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==} + + style-to-object@1.0.14: + resolution: {integrity: sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==} + + supports-color@7.2.0: + resolution: {integrity: sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==} + engines: {node: '>=8'} + + tailwind-merge@3.5.0: + resolution: {integrity: sha512-I8K9wewnVDkL1NTGoqWmVEIlUcB9gFriAEkXkfCjX5ib8ezGxtR3xD7iZIxrfArjEsH7F1CHD4RFUtxefdqV/A==} + + tailwindcss@4.2.1: + resolution: {integrity: sha512-/tBrSQ36vCleJkAOsy9kbNTgaxvGbyOamC30PRePTQe/o1MFwEKHQk4Cn7BNGaPtjp+PuUrByJehM1hgxfq4sw==} + + tailwindcss@4.2.2: + resolution: {integrity: sha512-KWBIxs1Xb6NoLdMVqhbhgwZf2PGBpPEiwOqgI4pFIYbNTfBXiKYyWoTsXgBQ9WFg/OlhnvHaY+AEpW7wSmFo2Q==} + + tapable@2.3.3: + resolution: {integrity: sha512-uxc/zpqFg6x7C8vOE7lh6Lbda8eEL9zmVm/PLeTPBRhh1xCgdWaQ+J1CUieGpIfm2HdtsUpRv+HshiasBMcc6A==} + engines: {node: '>=6'} + + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + + toml@3.0.0: + resolution: {integrity: sha512-y/mWCZinnvxjTKYhJ+pYxwD0mRLVvOtdS2Awbgxln6iEnt4rk0yBxeSBHkGJcPucRiG0e55mwWp+g/05rsrd6w==} + + trim-lines@3.0.1: + resolution: {integrity: sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==} + + trough@2.2.0: + resolution: {integrity: sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==} + + ts-api-utils@2.5.0: + resolution: {integrity: sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==} + engines: {node: '>=18.12'} + peerDependencies: + typescript: '>=4.8.4' + + tslib@2.8.1: + resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + + tw-animate-css@1.4.0: + resolution: {integrity: sha512-7bziOlRqH0hJx80h/3mbicLW7o8qLsH5+RaLR2t+OHM3D0JlWGODQKQ4cxbK7WlvmUxpcj6Kgu6EKqjrGFe3QQ==} + + type-check@0.4.0: + resolution: {integrity: sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==} + engines: {node: '>= 0.8.0'} + + type-fest@2.19.0: + resolution: {integrity: sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==} + engines: {node: '>=12.20'} + + typescript@6.0.3: + resolution: {integrity: sha512-y2TvuxSZPDyQakkFRPZHKFm+KKVqIisdg9/CZwm9ftvKXLP8NRWj38/ODjNbr43SsoXqNuAisEf1GdCxqWcdBw==} + engines: {node: '>=14.17'} + hasBin: true + + undici-types@7.16.0: + resolution: {integrity: sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==} + + undici-types@7.19.2: + resolution: {integrity: sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==} + + unified@11.0.5: + resolution: {integrity: sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==} + + unist-util-is@6.0.1: + resolution: {integrity: sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==} + + unist-util-map@4.0.0: + resolution: {integrity: sha512-HJs1tpkSmRJUzj6fskQrS5oYhBYlmtcvy4SepdDEEsL04FjBrgF0Mgggvxc1/qGBGgW7hRh9+UBK1aqTEnBpIA==} + + unist-util-mdx-define@1.1.2: + resolution: {integrity: sha512-9ncH7i7TN5Xn7/tzX5bE3rXgz1X/u877gYVAUB3mLeTKYJmQHmqKTDBi6BTGXV7AeolBCI9ErcVsOt2qryoD0g==} + + unist-util-position-from-estree@2.0.0: + resolution: {integrity: sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==} + + unist-util-position@5.0.0: + resolution: {integrity: sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==} + + unist-util-stringify-position@4.0.0: + resolution: {integrity: sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==} + + unist-util-visit-parents@6.0.2: + resolution: {integrity: sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==} + + unist-util-visit@5.1.0: + resolution: {integrity: sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==} + + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + + url@0.11.4: + resolution: {integrity: sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==} + engines: {node: '>= 0.4'} + + urlpattern-polyfill@10.1.0: + resolution: {integrity: sha512-IGjKp/o0NL3Bso1PymYURCJxMPNAf/ILOpendP9f5B6e1rTJgdgiOvgfoT8VxCAdY+Wisb9uhGaJJf3yZ2V9nw==} + + use-callback-ref@1.3.3: + resolution: {integrity: sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sidecar@1.1.3: + resolution: {integrity: sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==} + engines: {node: '>=10'} + peerDependencies: + '@types/react': '*' + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + use-sync-external-store@1.6.0: + resolution: {integrity: sha512-Pp6GSwGP/NrPIrxVFAIkOQeyw8lFenOHijQWkUTrDvrF4ALqylP2C/KCkeS9dpUM3KvYRQhna5vt7IL95+ZQ9w==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + + vaul@1.1.2: + resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} + peerDependencies: + react: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc + + vfile-location@5.0.3: + resolution: {integrity: sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==} + + vfile-message@4.0.3: + resolution: {integrity: sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==} + + vfile@6.0.3: + resolution: {integrity: sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==} + + vite@8.0.9: + resolution: {integrity: sha512-t7g7GVRpMXjNpa67HaVWI/8BWtdVIQPCL2WoozXXA7LBGEFK4AkkKkHx2hAQf5x1GZSlcmEDPkVLSGahxnEEZw==} + engines: {node: ^20.19.0 || >=22.12.0} + hasBin: true + peerDependencies: + '@types/node': ^20.19.0 || >=22.12.0 + '@vitejs/devtools': ^0.1.0 + esbuild: ^0.27.0 || ^0.28.0 + jiti: '>=1.21.0' + less: ^4.0.0 + sass: ^1.70.0 + sass-embedded: ^1.70.0 + stylus: '>=0.54.8' + sugarss: ^5.0.0 + terser: ^5.16.0 + tsx: ^4.8.1 + yaml: ^2.4.2 + peerDependenciesMeta: + '@types/node': + optional: true + '@vitejs/devtools': + optional: true + esbuild: + optional: true + jiti: + optional: true + less: + optional: true + sass: + optional: true + sass-embedded: + optional: true + stylus: + optional: true + sugarss: + optional: true + terser: + optional: true + tsx: + optional: true + yaml: + optional: true + + web-namespaces@2.0.1: + resolution: {integrity: sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==} + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + word-wrap@1.2.5: + resolution: {integrity: sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==} + engines: {node: '>=0.10.0'} + + wrap-ansi@9.0.2: + resolution: {integrity: sha512-42AtmgqjV+X1VpdOfyTGOYRi0/zsoLqtXQckTmqTeybT+BDIbM/Guxo7x3pE2vtpr1ok6xRqM9OpBe+Jyoqyww==} + engines: {node: '>=18'} + + xtend@4.0.2: + resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} + engines: {node: '>=0.4'} + + y18n@5.0.8: + resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==} + engines: {node: '>=10'} + + yaml@2.8.3: + resolution: {integrity: sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==} + engines: {node: '>= 14.6'} + hasBin: true + + yargs-parser@22.0.0: + resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yargs@18.0.0: + resolution: {integrity: sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==} + engines: {node: ^20.19.0 || ^22.12.0 || >=23} + + yocto-queue@0.1.0: + resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} + engines: {node: '>=10'} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + + zod@4.3.6: + resolution: {integrity: sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==} + + zudoku@0.77.0: + resolution: {integrity: sha512-3zFmWFCYejA8GND904r902zrU11f4A1xsYnKIXOLXnllHwGZCU8rYjyW5cViUFs6bMEN2aN2kOJ13HKNxwfo8Q==} + hasBin: true + peerDependencies: + '@azure/msal-browser': ^4.13.0 + '@sentry/react': ^10.0.0 + '@supabase/supabase-js': ^2.49.4 + firebase: ^12.6.0 + mermaid: ^11.0.0 + react: '>=19.2.0' + react-dom: '>=19.2.0' + peerDependenciesMeta: + '@azure/msal-browser': + optional: true + '@sentry/react': + optional: true + '@supabase/supabase-js': + optional: true + firebase: + optional: true + mermaid: + optional: true + + zustand@5.0.12: + resolution: {integrity: sha512-i77ae3aZq4dhMlRhJVCYgMLKuSiZAaUPAct2AksxQ+gOtimhGMdXljRT21P5BNpeT4kXlLIckvkPM029OljD7g==} + engines: {node: '>=12.20.0'} + peerDependencies: + '@types/react': '>=18.0.0' + immer: '>=9.0.6' + react: '>=18.0.0' + use-sync-external-store: '>=1.2.0' + peerDependenciesMeta: + '@types/react': + optional: true + immer: + optional: true + react: + optional: true + use-sync-external-store: + optional: true + + zwitch@2.0.4: + resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==} + +snapshots: + + '@apidevtools/json-schema-ref-parser@14.2.1(@types/json-schema@7.0.15)': + dependencies: + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + + '@apidevtools/json-schema-ref-parser@15.3.5(@types/json-schema@7.0.15)': + dependencies: + '@types/json-schema': 7.0.15 + js-yaml: 4.1.1 + + '@babel/runtime@7.29.2': {} + + '@base-ui/react@1.4.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@base-ui/utils': 0.2.8(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@floating-ui/utils': 0.2.11 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + use-sync-external-store: 1.6.0(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + '@base-ui/utils@0.2.8(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + '@floating-ui/utils': 0.2.11 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + reselect: 5.1.1 + use-sync-external-store: 1.6.0(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + '@emnapi/core@1.9.2': + dependencies: + '@emnapi/wasi-threads': 1.2.1 + tslib: 2.8.1 + optional: true + + '@emnapi/runtime@1.9.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@emnapi/wasi-threads@1.2.1': + dependencies: + tslib: 2.8.1 + optional: true + + '@envelop/core@5.5.1': + dependencies: + '@envelop/instrumentation': 1.0.0 + '@envelop/types': 5.2.1 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@envelop/instrumentation@1.0.0': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@envelop/types@5.2.1': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@esbuild/aix-ppc64@0.28.0': + optional: true + + '@esbuild/android-arm64@0.28.0': + optional: true + + '@esbuild/android-arm@0.28.0': + optional: true + + '@esbuild/android-x64@0.28.0': + optional: true + + '@esbuild/darwin-arm64@0.28.0': + optional: true + + '@esbuild/darwin-x64@0.28.0': + optional: true + + '@esbuild/freebsd-arm64@0.28.0': + optional: true + + '@esbuild/freebsd-x64@0.28.0': + optional: true + + '@esbuild/linux-arm64@0.28.0': + optional: true + + '@esbuild/linux-arm@0.28.0': + optional: true + + '@esbuild/linux-ia32@0.28.0': + optional: true + + '@esbuild/linux-loong64@0.28.0': + optional: true + + '@esbuild/linux-mips64el@0.28.0': + optional: true + + '@esbuild/linux-ppc64@0.28.0': + optional: true + + '@esbuild/linux-riscv64@0.28.0': + optional: true + + '@esbuild/linux-s390x@0.28.0': + optional: true + + '@esbuild/linux-x64@0.28.0': + optional: true + + '@esbuild/netbsd-arm64@0.28.0': + optional: true + + '@esbuild/netbsd-x64@0.28.0': + optional: true + + '@esbuild/openbsd-arm64@0.28.0': + optional: true + + '@esbuild/openbsd-x64@0.28.0': + optional: true + + '@esbuild/openharmony-arm64@0.28.0': + optional: true + + '@esbuild/sunos-x64@0.28.0': + optional: true + + '@esbuild/win32-arm64@0.28.0': + optional: true + + '@esbuild/win32-ia32@0.28.0': + optional: true + + '@esbuild/win32-x64@0.28.0': + optional: true + + '@eslint-community/eslint-utils@4.9.1(eslint@9.39.4(jiti@2.7.0))': + dependencies: + eslint: 9.39.4(jiti@2.7.0) + eslint-visitor-keys: 3.4.3 + + '@eslint-community/regexpp@4.12.2': {} + + '@eslint/config-array@0.21.2': + dependencies: + '@eslint/object-schema': 2.1.7 + debug: 4.4.3 + minimatch: 3.1.5 + transitivePeerDependencies: + - supports-color + + '@eslint/config-helpers@0.4.2': + dependencies: + '@eslint/core': 0.17.0 + + '@eslint/core@0.17.0': + dependencies: + '@types/json-schema': 7.0.15 + + '@eslint/eslintrc@3.3.5': + dependencies: + ajv: 6.15.0 + debug: 4.4.3 + espree: 10.4.0 + globals: 14.0.0 + ignore: 5.3.2 + import-fresh: 3.3.1 + js-yaml: 4.1.1 + minimatch: 3.1.5 + strip-json-comments: 3.1.1 + transitivePeerDependencies: + - supports-color + + '@eslint/js@9.39.4': {} + + '@eslint/object-schema@2.1.7': {} + + '@eslint/plugin-kit@0.4.1': + dependencies: + '@eslint/core': 0.17.0 + levn: 0.4.1 + + '@fastify/busboy@3.2.0': {} + + '@fastify/otel@0.18.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.212.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + minimatch: 10.2.5 + transitivePeerDependencies: + - supports-color + + '@floating-ui/core@1.7.5': + dependencies: + '@floating-ui/utils': 0.2.11 + + '@floating-ui/dom@1.7.6': + dependencies: + '@floating-ui/core': 1.7.5 + '@floating-ui/utils': 0.2.11 + + '@floating-ui/react-dom@2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/dom': 1.7.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@floating-ui/utils@0.2.11': {} + + '@graphql-tools/executor@1.5.3(graphql@16.13.2)': + dependencies: + '@graphql-tools/utils': 11.1.0(graphql@16.13.2) + '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2) + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.2 + graphql: 16.13.2 + tslib: 2.8.1 + + '@graphql-tools/merge@9.1.9(graphql@16.13.2)': + dependencies: + '@graphql-tools/utils': 11.1.0(graphql@16.13.2) + graphql: 16.13.2 + tslib: 2.8.1 + + '@graphql-tools/schema@10.0.33(graphql@16.13.2)': + dependencies: + '@graphql-tools/merge': 9.1.9(graphql@16.13.2) + '@graphql-tools/utils': 11.1.0(graphql@16.13.2) + graphql: 16.13.2 + tslib: 2.8.1 + + '@graphql-tools/utils@10.11.0(graphql@16.13.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2) + '@whatwg-node/promise-helpers': 1.3.2 + cross-inspect: 1.0.1 + graphql: 16.13.2 + tslib: 2.8.1 + + '@graphql-tools/utils@11.1.0(graphql@16.13.2)': + dependencies: + '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2) + '@whatwg-node/promise-helpers': 1.3.2 + cross-inspect: 1.0.1 + graphql: 16.13.2 + tslib: 2.8.1 + + '@graphql-typed-document-node/core@3.2.0(graphql@16.13.2)': + dependencies: + graphql: 16.13.2 + + '@graphql-yoga/logger@2.0.1': + dependencies: + tslib: 2.8.1 + + '@graphql-yoga/subscription@5.0.5': + dependencies: + '@graphql-yoga/typed-event-target': 3.0.2 + '@repeaterjs/repeater': 3.0.6 + '@whatwg-node/events': 0.1.2 + tslib: 2.8.1 + + '@graphql-yoga/typed-event-target@3.0.2': + dependencies: + '@repeaterjs/repeater': 3.0.6 + tslib: 2.8.1 + + '@hono/node-server@1.19.13(hono@4.12.14)': + dependencies: + hono: 4.12.14 + + '@humanfs/core@0.19.2': + dependencies: + '@humanfs/types': 0.15.0 + + '@humanfs/node@0.16.8': + dependencies: + '@humanfs/core': 0.19.2 + '@humanfs/types': 0.15.0 + '@humanwhocodes/retry': 0.4.3 + + '@humanfs/types@0.15.0': {} + + '@humanwhocodes/module-importer@1.0.1': {} + + '@humanwhocodes/retry@0.4.3': {} + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/remapping@2.3.5': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@lekoarts/rehype-meta-as-attributes@3.0.3': + dependencies: + unist-util-visit: 5.1.0 + + '@mdx-js/mdx@3.1.1': + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdx': 2.0.13 + acorn: 8.16.0 + collapse-white-space: 2.1.0 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + hast-util-to-jsx-runtime: 2.3.6 + markdown-extensions: 2.0.0 + recma-build-jsx: 1.0.0 + recma-jsx: 1.0.1(acorn@8.16.0) + recma-stringify: 1.0.0 + rehype-recma: 1.0.0 + remark-mdx: 3.1.1 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + source-map: 0.7.6 + unified: 11.0.5 + unist-util-position-from-estree: 2.0.0 + unist-util-stringify-position: 4.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@mdx-js/react@3.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@types/mdx': 2.0.13 + '@types/react': 19.2.14 + react: 19.2.5 + + '@mdx-js/rollup@3.1.1(rollup@4.60.3)': + dependencies: + '@mdx-js/mdx': 3.1.1 + '@rollup/pluginutils': 5.3.0(rollup@4.60.3) + rollup: 4.60.3 + source-map: 0.7.6 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@napi-rs/nice-android-arm-eabi@1.1.1': + optional: true + + '@napi-rs/nice-android-arm64@1.1.1': + optional: true + + '@napi-rs/nice-darwin-arm64@1.1.1': + optional: true + + '@napi-rs/nice-darwin-x64@1.1.1': + optional: true + + '@napi-rs/nice-freebsd-x64@1.1.1': + optional: true + + '@napi-rs/nice-linux-arm-gnueabihf@1.1.1': + optional: true + + '@napi-rs/nice-linux-arm64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-arm64-musl@1.1.1': + optional: true + + '@napi-rs/nice-linux-ppc64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-riscv64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-s390x-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-x64-gnu@1.1.1': + optional: true + + '@napi-rs/nice-linux-x64-musl@1.1.1': + optional: true + + '@napi-rs/nice-openharmony-arm64@1.1.1': + optional: true + + '@napi-rs/nice-win32-arm64-msvc@1.1.1': + optional: true + + '@napi-rs/nice-win32-ia32-msvc@1.1.1': + optional: true + + '@napi-rs/nice-win32-x64-msvc@1.1.1': + optional: true + + '@napi-rs/nice@1.1.1': + optionalDependencies: + '@napi-rs/nice-android-arm-eabi': 1.1.1 + '@napi-rs/nice-android-arm64': 1.1.1 + '@napi-rs/nice-darwin-arm64': 1.1.1 + '@napi-rs/nice-darwin-x64': 1.1.1 + '@napi-rs/nice-freebsd-x64': 1.1.1 + '@napi-rs/nice-linux-arm-gnueabihf': 1.1.1 + '@napi-rs/nice-linux-arm64-gnu': 1.1.1 + '@napi-rs/nice-linux-arm64-musl': 1.1.1 + '@napi-rs/nice-linux-ppc64-gnu': 1.1.1 + '@napi-rs/nice-linux-riscv64-gnu': 1.1.1 + '@napi-rs/nice-linux-s390x-gnu': 1.1.1 + '@napi-rs/nice-linux-x64-gnu': 1.1.1 + '@napi-rs/nice-linux-x64-musl': 1.1.1 + '@napi-rs/nice-openharmony-arm64': 1.1.1 + '@napi-rs/nice-win32-arm64-msvc': 1.1.1 + '@napi-rs/nice-win32-ia32-msvc': 1.1.1 + '@napi-rs/nice-win32-x64-msvc': 1.1.1 + optional: true + + '@napi-rs/wasm-runtime@1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2)': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@tybys/wasm-util': 0.10.2 + optional: true + + '@opentelemetry/api-logs@0.207.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.212.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api-logs@0.214.0': + dependencies: + '@opentelemetry/api': 1.9.1 + + '@opentelemetry/api@1.9.1': {} + + '@opentelemetry/core@2.6.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/instrumentation-amqplib@0.61.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-connect@0.57.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/connect': 3.4.38 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-dataloader@0.31.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-fs@0.33.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-generic-pool@0.57.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-graphql@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-hapi@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-http@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.6.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + forwarded-parse: 2.1.2 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-ioredis@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.38.3 + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-kafkajs@0.23.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-knex@0.58.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-koa@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-lru-memoizer@0.58.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongodb@0.67.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mongoose@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql2@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-mysql@0.60.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/mysql': 2.15.27 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-pg@0.66.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@opentelemetry/sql-common': 0.41.2(@opentelemetry/api@1.9.1) + '@types/pg': 8.15.6 + '@types/pg-pool': 2.0.7 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-redis@0.62.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/redis-common': 0.38.3 + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-tedious@0.33.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@types/tedious': 4.0.14 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation-undici@0.24.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.207.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.207.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.212.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.212.0 + import-in-the-middle: 2.0.6 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/api-logs': 0.214.0 + import-in-the-middle: 3.0.1 + require-in-the-middle: 8.0.1 + transitivePeerDependencies: + - supports-color + + '@opentelemetry/redis-common@0.38.3': {} + + '@opentelemetry/resources@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/resources': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@opentelemetry/semantic-conventions@1.40.0': {} + + '@opentelemetry/sql-common@0.41.2(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + + '@oxc-project/types@0.126.0': {} + + '@pagefind/darwin-arm64@1.5.2': + optional: true + + '@pagefind/darwin-x64@1.5.2': + optional: true + + '@pagefind/freebsd-x64@1.5.2': + optional: true + + '@pagefind/linux-arm64@1.5.2': + optional: true + + '@pagefind/linux-x64@1.5.2': + optional: true + + '@pagefind/windows-arm64@1.5.2': + optional: true + + '@pagefind/windows-x64@1.5.2': + optional: true + + '@posthog/core@1.23.1': + dependencies: + cross-spawn: 7.0.6 + + '@pothos/core@4.12.0(graphql@16.13.2)': + dependencies: + graphql: 16.13.2 + + '@prisma/instrumentation@7.6.0(@opentelemetry/api@1.9.1)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/instrumentation': 0.207.0(@opentelemetry/api@1.9.1) + transitivePeerDependencies: + - supports-color + + '@radix-ui/number@1.1.1': {} + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-accordion@1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-alert-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-arrow@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-aspect-ratio@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-checkbox@1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collapsible@1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-collection@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context@1.1.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-context@1.1.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dialog@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-direction@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-dismissable-layer@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-escape-keydown': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-dropdown-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-focus-guards@1.1.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-focus-scope@1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-hover-card@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-id@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-label@2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-menu@2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-navigation-menu@1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popover@1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-popper@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@floating-ui/react-dom': 2.1.8(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-arrow': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-rect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/rect': 1.1.1 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-portal@1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-presence@1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-primitive@2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-progress@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-context': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-radio-group@1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-roving-focus@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-scroll-area@1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-select@2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-focus-guards': 1.1.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-focus-scope': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + aria-hidden: 1.2.6 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-remove-scroll: 2.7.2(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-separator@1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slider@1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/number': 1.1.1 + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-collection': 1.1.7(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-slot@1.2.4(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-switch@1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-previous': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-size': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tabs@1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle-group@1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-direction': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-roving-focus': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-toggle@1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-tooltip@1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-context': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dismissable-layer': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-popper': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-portal': 1.1.9(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-presence': 1.1.5(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-use-callback-ref@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-escape-keydown@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-callback-ref': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-previous@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-rect@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/rect': 1.1.1 + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-use-size@1.1.1(@types/react@19.2.14)(react@19.2.5)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.14)(react@19.2.5) + react: 19.2.5 + optionalDependencies: + '@types/react': 19.2.14 + + '@radix-ui/react-visually-hidden@1.2.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/react-visually-hidden@1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + + '@radix-ui/rect@1.1.1': {} + + '@repeaterjs/repeater@3.0.6': {} + + '@rolldown/binding-android-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-darwin-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-darwin-x64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-freebsd-x64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm-gnueabihf@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-arm64-musl@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-ppc64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-s390x-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-x64-gnu@1.0.0-rc.16': + optional: true + + '@rolldown/binding-linux-x64-musl@1.0.0-rc.16': + optional: true + + '@rolldown/binding-openharmony-arm64@1.0.0-rc.16': + optional: true + + '@rolldown/binding-wasm32-wasi@1.0.0-rc.16': + dependencies: + '@emnapi/core': 1.9.2 + '@emnapi/runtime': 1.9.2 + '@napi-rs/wasm-runtime': 1.1.4(@emnapi/core@1.9.2)(@emnapi/runtime@1.9.2) + optional: true + + '@rolldown/binding-win32-arm64-msvc@1.0.0-rc.16': + optional: true + + '@rolldown/binding-win32-x64-msvc@1.0.0-rc.16': + optional: true + + '@rolldown/pluginutils@1.0.0-rc.16': {} + + '@rolldown/pluginutils@1.0.0-rc.7': {} + + '@rollup/pluginutils@5.3.0(rollup@4.60.3)': + dependencies: + '@types/estree': 1.0.8 + estree-walker: 2.0.2 + picomatch: 4.0.4 + optionalDependencies: + rollup: 4.60.3 + + '@rollup/rollup-android-arm-eabi@4.60.3': + optional: true + + '@rollup/rollup-android-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-arm64@4.60.3': + optional: true + + '@rollup/rollup-darwin-x64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-arm64@4.60.3': + optional: true + + '@rollup/rollup-freebsd-x64@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-gnueabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm-musleabihf@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-arm64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-loong64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-ppc64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-riscv64-musl@4.60.3': + optional: true + + '@rollup/rollup-linux-s390x-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-linux-x64-musl@4.60.3': + optional: true + + '@rollup/rollup-openbsd-x64@4.60.3': + optional: true + + '@rollup/rollup-openharmony-arm64@4.60.3': + optional: true + + '@rollup/rollup-win32-arm64-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-ia32-msvc@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-gnu@4.60.3': + optional: true + + '@rollup/rollup-win32-x64-msvc@4.60.3': + optional: true + + '@scalar/helpers@0.2.7': {} + + '@scalar/json-magic@0.9.0': + dependencies: + '@scalar/helpers': 0.2.7 + yaml: 2.8.3 + + '@scalar/openapi-parser@0.23.13': + dependencies: + '@scalar/json-magic': 0.9.0 + '@scalar/openapi-types': 0.5.3 + '@scalar/openapi-upgrader': 0.1.7 + ajv: 8.20.0 + ajv-draft-04: 1.0.0(ajv@8.20.0) + ajv-formats: 3.0.1(ajv@8.20.0) + jsonpointer: 5.0.1 + leven: 4.1.0 + yaml: 2.8.3 + + '@scalar/openapi-types@0.5.3': + dependencies: + zod: 4.3.6 + + '@scalar/openapi-upgrader@0.1.7': + dependencies: + '@scalar/openapi-types': 0.5.3 + + '@sentry/core@10.49.0': {} + + '@sentry/node-core@10.49.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': + dependencies: + '@sentry/core': 10.49.0 + '@sentry/opentelemetry': 10.49.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + import-in-the-middle: 3.0.1 + optionalDependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + + '@sentry/node@10.49.0': + dependencies: + '@fastify/otel': 0.18.0(@opentelemetry/api@1.9.1) + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-amqplib': 0.61.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-connect': 0.57.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-dataloader': 0.31.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-fs': 0.33.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-generic-pool': 0.57.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-graphql': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-hapi': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-http': 0.214.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-ioredis': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-kafkajs': 0.23.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-knex': 0.58.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-koa': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-lru-memoizer': 0.58.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongodb': 0.67.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mongoose': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-mysql2': 0.60.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-pg': 0.66.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-redis': 0.62.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-tedious': 0.33.0(@opentelemetry/api@1.9.1) + '@opentelemetry/instrumentation-undici': 0.24.0(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@prisma/instrumentation': 7.6.0(@opentelemetry/api@1.9.1) + '@sentry/core': 10.49.0 + '@sentry/node-core': 10.49.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/instrumentation@0.214.0(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + '@sentry/opentelemetry': 10.49.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0) + import-in-the-middle: 3.0.1 + transitivePeerDependencies: + - '@opentelemetry/exporter-trace-otlp-http' + - supports-color + + '@sentry/opentelemetry@10.49.0(@opentelemetry/api@1.9.1)(@opentelemetry/core@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/sdk-trace-base@2.7.1(@opentelemetry/api@1.9.1))(@opentelemetry/semantic-conventions@1.40.0)': + dependencies: + '@opentelemetry/api': 1.9.1 + '@opentelemetry/core': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/sdk-trace-base': 2.7.1(@opentelemetry/api@1.9.1) + '@opentelemetry/semantic-conventions': 1.40.0 + '@sentry/core': 10.49.0 + + '@shikijs/core@4.0.2': + dependencies: + '@shikijs/primitive': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + hast-util-to-html: 9.0.5 + + '@shikijs/engine-javascript@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + oniguruma-to-es: 4.3.6 + + '@shikijs/engine-oniguruma@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + + '@shikijs/langs@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/primitive@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/rehype@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + '@types/hast': 3.0.4 + hast-util-to-string: 3.0.1 + shiki: 4.0.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + + '@shikijs/themes@4.0.2': + dependencies: + '@shikijs/types': 4.0.2 + + '@shikijs/transformers@4.0.2': + dependencies: + '@shikijs/core': 4.0.2 + '@shikijs/types': 4.0.2 + + '@shikijs/types@4.0.2': + dependencies: + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + '@shikijs/vscode-textmate@10.0.2': {} + + '@tailwindcss/node@4.2.2': + dependencies: + '@jridgewell/remapping': 2.3.5 + enhanced-resolve: 5.21.0 + jiti: 2.7.0 + lightningcss: 1.32.0 + magic-string: 0.30.21 + source-map-js: 1.2.1 + tailwindcss: 4.2.2 + + '@tailwindcss/oxide-android-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-arm64@4.2.2': + optional: true + + '@tailwindcss/oxide-darwin-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-freebsd-x64@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm-gnueabihf@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-arm64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-gnu@4.2.2': + optional: true + + '@tailwindcss/oxide-linux-x64-musl@4.2.2': + optional: true + + '@tailwindcss/oxide-wasm32-wasi@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-arm64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide-win32-x64-msvc@4.2.2': + optional: true + + '@tailwindcss/oxide@4.2.2': + optionalDependencies: + '@tailwindcss/oxide-android-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-arm64': 4.2.2 + '@tailwindcss/oxide-darwin-x64': 4.2.2 + '@tailwindcss/oxide-freebsd-x64': 4.2.2 + '@tailwindcss/oxide-linux-arm-gnueabihf': 4.2.2 + '@tailwindcss/oxide-linux-arm64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-arm64-musl': 4.2.2 + '@tailwindcss/oxide-linux-x64-gnu': 4.2.2 + '@tailwindcss/oxide-linux-x64-musl': 4.2.2 + '@tailwindcss/oxide-wasm32-wasi': 4.2.2 + '@tailwindcss/oxide-win32-arm64-msvc': 4.2.2 + '@tailwindcss/oxide-win32-x64-msvc': 4.2.2 + + '@tailwindcss/typography@0.5.19(tailwindcss@4.2.1)': + dependencies: + postcss-selector-parser: 6.0.10 + tailwindcss: 4.2.1 + + '@tailwindcss/vite@4.2.2(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.8.3))': + dependencies: + '@tailwindcss/node': 4.2.2 + '@tailwindcss/oxide': 4.2.2 + tailwindcss: 4.2.2 + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.8.3) + + '@tanem/react-nprogress@6.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5)': + dependencies: + '@babel/runtime': 7.29.2 + hoist-non-react-statics: 3.3.2 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + '@tanstack/query-core@5.97.0': {} + + '@tanstack/react-query@5.97.0(react@19.2.5)': + dependencies: + '@tanstack/query-core': 5.97.0 + react: 19.2.5 + + '@tybys/wasm-util@0.10.2': + dependencies: + tslib: 2.8.1 + optional: true + + '@types/connect@3.4.38': + dependencies: + '@types/node': 25.6.0 + + '@types/debug@4.1.13': + dependencies: + '@types/ms': 2.1.0 + + '@types/estree-jsx@1.0.5': + dependencies: + '@types/estree': 1.0.8 + + '@types/estree@1.0.8': {} + + '@types/hast@3.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/json-schema@7.0.15': {} + + '@types/mdast@4.0.4': + dependencies: + '@types/unist': 3.0.3 + + '@types/mdx@2.0.13': {} + + '@types/ms@2.1.0': {} + + '@types/mysql@2.15.27': + dependencies: + '@types/node': 25.6.0 + + '@types/node@24.12.2': + dependencies: + undici-types: 7.16.0 + + '@types/node@25.6.0': + dependencies: + undici-types: 7.19.2 + + '@types/pg-pool@2.0.7': + dependencies: + '@types/pg': 8.15.6 + + '@types/pg@8.15.6': + dependencies: + '@types/node': 25.6.0 + pg-protocol: 1.13.0 + pg-types: 2.2.0 + + '@types/react-dom@19.2.3(@types/react@19.2.14)': + dependencies: + '@types/react': 19.2.14 + + '@types/react@19.2.14': + dependencies: + csstype: 3.2.3 + + '@types/sax@1.2.7': + dependencies: + '@types/node': 24.12.2 + + '@types/tedious@4.0.14': + dependencies: + '@types/node': 25.6.0 + + '@types/unist@2.0.11': {} + + '@types/unist@3.0.3': {} + + '@typescript-eslint/eslint-plugin@8.59.2(@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3))(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/regexpp': 4.12.2 + '@typescript-eslint/parser': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/type-utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.2 + eslint: 9.39.4(jiti@2.7.0) + ignore: 7.0.5 + natural-compare: 1.4.0 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/parser@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/project-service@8.59.2(typescript@6.0.3)': + dependencies: + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) + '@typescript-eslint/types': 8.59.2 + debug: 4.4.3 + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/scope-manager@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + + '@typescript-eslint/tsconfig-utils@8.59.2(typescript@6.0.3)': + dependencies: + typescript: 6.0.3 + + '@typescript-eslint/type-utils@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + '@typescript-eslint/utils': 8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3) + debug: 4.4.3 + eslint: 9.39.4(jiti@2.7.0) + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/types@8.59.2': {} + + '@typescript-eslint/typescript-estree@8.59.2(typescript@6.0.3)': + dependencies: + '@typescript-eslint/project-service': 8.59.2(typescript@6.0.3) + '@typescript-eslint/tsconfig-utils': 8.59.2(typescript@6.0.3) + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/visitor-keys': 8.59.2 + debug: 4.4.3 + minimatch: 10.2.5 + semver: 7.7.4 + tinyglobby: 0.2.16 + ts-api-utils: 2.5.0(typescript@6.0.3) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/utils@8.59.2(eslint@9.39.4(jiti@2.7.0))(typescript@6.0.3)': + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@typescript-eslint/scope-manager': 8.59.2 + '@typescript-eslint/types': 8.59.2 + '@typescript-eslint/typescript-estree': 8.59.2(typescript@6.0.3) + eslint: 9.39.4(jiti@2.7.0) + typescript: 6.0.3 + transitivePeerDependencies: + - supports-color + + '@typescript-eslint/visitor-keys@8.59.2': + dependencies: + '@typescript-eslint/types': 8.59.2 + eslint-visitor-keys: 5.0.1 + + '@ungap/structured-clone@1.3.1': {} + + '@vitejs/plugin-react@6.0.1(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.8.3))': + dependencies: + '@rolldown/pluginutils': 1.0.0-rc.7 + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.8.3) + + '@whatwg-node/disposablestack@0.0.6': + dependencies: + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@whatwg-node/events@0.1.2': + dependencies: + tslib: 2.8.1 + + '@whatwg-node/fetch@0.10.13': + dependencies: + '@whatwg-node/node-fetch': 0.8.5 + urlpattern-polyfill: 10.1.0 + + '@whatwg-node/node-fetch@0.8.5': + dependencies: + '@fastify/busboy': 3.2.0 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@whatwg-node/promise-helpers@1.3.2': + dependencies: + tslib: 2.8.1 + + '@whatwg-node/server@0.10.18': + dependencies: + '@envelop/instrumentation': 1.0.0 + '@whatwg-node/disposablestack': 0.0.6 + '@whatwg-node/fetch': 0.10.13 + '@whatwg-node/promise-helpers': 1.3.2 + tslib: 2.8.1 + + '@x0k/json-schema-merge@1.0.2': + dependencies: + '@types/json-schema': 7.0.15 + + '@zudoku/httpsnippet@10.0.9': + dependencies: + qs: 6.15.1 + stringify-object: 3.3.0 + url: 0.11.4 + + '@zudoku/react-helmet-async@2.0.5(react@19.2.5)': + dependencies: + invariant: 2.2.4 + react: 19.2.5 + react-fast-compare: 3.2.2 + shallowequal: 1.1.0 + + '@zuplo/mcp@0.0.32': + dependencies: + eventsource-parser: 3.0.8 + zod: 3.25.76 + + acorn-import-attributes@1.9.5(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn-jsx@5.3.2(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + + acorn@8.16.0: {} + + ajv-draft-04@1.0.0(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv-formats@3.0.1(ajv@8.20.0): + optionalDependencies: + ajv: 8.20.0 + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + + ansi-regex@6.2.2: {} + + ansi-styles@4.3.0: + dependencies: + color-convert: 2.0.1 + + ansi-styles@6.2.3: {} + + arg@5.0.2: {} + + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + + argparse@2.0.1: {} + + aria-hidden@1.2.6: + dependencies: + tslib: 2.8.1 + + astring@1.9.0: {} + + bail@2.0.2: {} + + balanced-match@1.0.2: {} + + balanced-match@4.0.4: {} + + base-x@5.0.1: {} + + brace-expansion@1.1.14: + dependencies: + balanced-match: 1.0.2 + concat-map: 0.0.1 + + brace-expansion@5.0.5: + dependencies: + balanced-match: 4.0.4 + + bs58@6.0.0: + dependencies: + base-x: 5.0.1 + + call-bind-apply-helpers@1.0.2: + dependencies: + es-errors: 1.3.0 + function-bind: 1.1.2 + + call-bound@1.0.4: + dependencies: + call-bind-apply-helpers: 1.0.2 + get-intrinsic: 1.3.0 + + callsites@3.1.0: {} + + ccount@2.0.1: {} + + chalk@4.1.2: + dependencies: + ansi-styles: 4.3.0 + supports-color: 7.2.0 + + character-entities-html4@2.1.0: {} + + character-entities-legacy@3.0.0: {} + + character-entities@2.0.2: {} + + character-reference-invalid@2.0.1: {} + + cjs-module-lexer@2.2.0: {} + + class-variance-authority@0.7.1: + dependencies: + clsx: 2.1.1 + + cliui@9.0.1: + dependencies: + string-width: 7.2.0 + strip-ansi: 7.2.0 + wrap-ansi: 9.0.2 + + clsx@2.1.1: {} + + cmdk@1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-id': 1.1.1(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-primitive': 2.1.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + collapse-white-space@2.1.0: {} + + color-convert@2.0.1: + dependencies: + color-name: 1.1.4 + + color-name@1.1.4: {} + + comma-separated-tokens@2.0.3: {} + + concat-map@0.0.1: {} + + cookie@1.1.1: {} + + cross-inspect@1.0.1: + dependencies: + tslib: 2.8.1 + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + cssesc@3.0.0: {} + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + decode-named-character-reference@1.3.0: + dependencies: + character-entities: 2.0.2 + + deep-is@0.1.4: {} + + delay@5.0.0: {} + + dequal@2.0.3: {} + + detect-libc@2.1.2: {} + + detect-node-es@1.1.0: {} + + devlop@1.1.0: + dependencies: + dequal: 2.0.3 + + dotenv@17.3.1: {} + + dunder-proto@1.0.1: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-errors: 1.3.0 + gopd: 1.2.0 + + embla-carousel-react@8.6.0(react@19.2.5): + dependencies: + embla-carousel: 8.6.0 + embla-carousel-reactive-utils: 8.6.0(embla-carousel@8.6.0) + react: 19.2.5 + + embla-carousel-reactive-utils@8.6.0(embla-carousel@8.6.0): + dependencies: + embla-carousel: 8.6.0 + + embla-carousel@8.6.0: {} + + emoji-regex@10.6.0: {} + + enhanced-resolve@5.21.0: + dependencies: + graceful-fs: 4.2.11 + tapable: 2.3.3 + + entities@6.0.1: {} + + es-define-property@1.0.1: {} + + es-errors@1.3.0: {} + + es-object-atoms@1.1.1: + dependencies: + es-errors: 1.3.0 + + esast-util-from-estree@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + unist-util-position-from-estree: 2.0.0 + + esast-util-from-js@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + acorn: 8.16.0 + esast-util-from-estree: 2.0.0 + vfile-message: 4.0.3 + + esbuild@0.28.0: + optionalDependencies: + '@esbuild/aix-ppc64': 0.28.0 + '@esbuild/android-arm': 0.28.0 + '@esbuild/android-arm64': 0.28.0 + '@esbuild/android-x64': 0.28.0 + '@esbuild/darwin-arm64': 0.28.0 + '@esbuild/darwin-x64': 0.28.0 + '@esbuild/freebsd-arm64': 0.28.0 + '@esbuild/freebsd-x64': 0.28.0 + '@esbuild/linux-arm': 0.28.0 + '@esbuild/linux-arm64': 0.28.0 + '@esbuild/linux-ia32': 0.28.0 + '@esbuild/linux-loong64': 0.28.0 + '@esbuild/linux-mips64el': 0.28.0 + '@esbuild/linux-ppc64': 0.28.0 + '@esbuild/linux-riscv64': 0.28.0 + '@esbuild/linux-s390x': 0.28.0 + '@esbuild/linux-x64': 0.28.0 + '@esbuild/netbsd-arm64': 0.28.0 + '@esbuild/netbsd-x64': 0.28.0 + '@esbuild/openbsd-arm64': 0.28.0 + '@esbuild/openbsd-x64': 0.28.0 + '@esbuild/openharmony-arm64': 0.28.0 + '@esbuild/sunos-x64': 0.28.0 + '@esbuild/win32-arm64': 0.28.0 + '@esbuild/win32-ia32': 0.28.0 + '@esbuild/win32-x64': 0.28.0 + + escalade@3.2.0: {} + + escape-string-regexp@4.0.0: {} + + escape-string-regexp@5.0.0: {} + + eslint-scope@8.4.0: + dependencies: + esrecurse: 4.3.0 + estraverse: 5.3.0 + + eslint-visitor-keys@3.4.3: {} + + eslint-visitor-keys@4.2.1: {} + + eslint-visitor-keys@5.0.1: {} + + eslint@9.39.4(jiti@2.7.0): + dependencies: + '@eslint-community/eslint-utils': 4.9.1(eslint@9.39.4(jiti@2.7.0)) + '@eslint-community/regexpp': 4.12.2 + '@eslint/config-array': 0.21.2 + '@eslint/config-helpers': 0.4.2 + '@eslint/core': 0.17.0 + '@eslint/eslintrc': 3.3.5 + '@eslint/js': 9.39.4 + '@eslint/plugin-kit': 0.4.1 + '@humanfs/node': 0.16.8 + '@humanwhocodes/module-importer': 1.0.1 + '@humanwhocodes/retry': 0.4.3 + '@types/estree': 1.0.8 + ajv: 6.15.0 + chalk: 4.1.2 + cross-spawn: 7.0.6 + debug: 4.4.3 + escape-string-regexp: 4.0.0 + eslint-scope: 8.4.0 + eslint-visitor-keys: 4.2.1 + espree: 10.4.0 + esquery: 1.7.0 + esutils: 2.0.3 + fast-deep-equal: 3.1.3 + file-entry-cache: 8.0.0 + find-up: 5.0.0 + glob-parent: 6.0.2 + ignore: 5.3.2 + imurmurhash: 0.1.4 + is-glob: 4.0.3 + json-stable-stringify-without-jsonify: 1.0.1 + lodash.merge: 4.6.2 + minimatch: 3.1.5 + natural-compare: 1.4.0 + optionator: 0.9.4 + optionalDependencies: + jiti: 2.7.0 + transitivePeerDependencies: + - supports-color + + espree@10.4.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + eslint-visitor-keys: 4.2.1 + + esprima@4.0.1: {} + + esquery@1.7.0: + dependencies: + estraverse: 5.3.0 + + esrecurse@4.3.0: + dependencies: + estraverse: 5.3.0 + + estraverse@5.3.0: {} + + estree-util-attach-comments@3.0.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-build-jsx@3.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + estree-walker: 3.0.3 + + estree-util-is-identifier-name@3.0.0: {} + + estree-util-scope@1.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + + estree-util-to-js@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + astring: 1.9.0 + source-map: 0.7.6 + + estree-util-value-to-estree@3.5.0: + dependencies: + '@types/estree': 1.0.8 + + estree-util-visit@2.0.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/unist': 3.0.3 + + estree-walker@2.0.2: {} + + estree-walker@3.0.3: + dependencies: + '@types/estree': 1.0.8 + + esutils@2.0.3: {} + + eventsource-parser@3.0.8: {} + + extend-shallow@2.0.1: + dependencies: + is-extendable: 0.1.1 + + extend@3.0.2: {} + + fast-deep-equal@3.1.3: {} + + fast-equals@6.0.0: {} + + fast-json-stable-stringify@2.1.0: {} + + fast-levenshtein@2.0.6: {} + + fast-printf@1.6.10: {} + + fast-uri@3.1.2: {} + + fault@2.0.1: + dependencies: + format: 0.2.2 + + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + + file-entry-cache@8.0.0: + dependencies: + flat-cache: 4.0.1 + + find-up@5.0.0: + dependencies: + locate-path: 6.0.0 + path-exists: 4.0.0 + + flat-cache@4.0.1: + dependencies: + flatted: 3.4.2 + keyv: 4.5.4 + + flatted@3.4.2: {} + + format@0.2.2: {} + + forwarded-parse@2.1.2: {} + + framer-motion@12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + motion-dom: 12.38.0 + motion-utils: 12.36.0 + tslib: 2.8.1 + optionalDependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + fsevents@2.3.3: + optional: true + + function-bind@1.1.2: {} + + get-caller-file@2.0.5: {} + + get-east-asian-width@1.5.0: {} + + get-intrinsic@1.3.0: + dependencies: + call-bind-apply-helpers: 1.0.2 + es-define-property: 1.0.1 + es-errors: 1.3.0 + es-object-atoms: 1.1.1 + function-bind: 1.1.2 + get-proto: 1.0.1 + gopd: 1.2.0 + has-symbols: 1.1.0 + hasown: 2.0.3 + math-intrinsics: 1.1.0 + + get-nonce@1.0.1: {} + + get-own-enumerable-property-symbols@3.0.2: {} + + get-proto@1.0.1: + dependencies: + dunder-proto: 1.0.1 + es-object-atoms: 1.1.1 + + github-slugger@2.0.0: {} + + glob-parent@6.0.2: + dependencies: + is-glob: 4.0.3 + + glob@13.0.6: + dependencies: + minimatch: 10.2.5 + minipass: 7.1.3 + path-scurry: 2.0.2 + + globals@14.0.0: {} + + gopd@1.2.0: {} + + graceful-fs@4.2.11: {} + + graphql-type-json@0.3.2(graphql@16.13.2): + dependencies: + graphql: 16.13.2 + + graphql-yoga@5.18.0(graphql@16.13.2): + dependencies: + '@envelop/core': 5.5.1 + '@envelop/instrumentation': 1.0.0 + '@graphql-tools/executor': 1.5.3(graphql@16.13.2) + '@graphql-tools/schema': 10.0.33(graphql@16.13.2) + '@graphql-tools/utils': 10.11.0(graphql@16.13.2) + '@graphql-yoga/logger': 2.0.1 + '@graphql-yoga/subscription': 5.0.5 + '@whatwg-node/fetch': 0.10.13 + '@whatwg-node/promise-helpers': 1.3.2 + '@whatwg-node/server': 0.10.18 + graphql: 16.13.2 + lru-cache: 10.4.3 + tslib: 2.8.1 + + graphql@16.13.2: {} + + gray-matter@4.0.3: + dependencies: + js-yaml: 3.14.2 + kind-of: 6.0.3 + section-matter: 1.0.0 + strip-bom-string: 1.0.0 + + has-flag@4.0.0: {} + + has-symbols@1.1.0: {} + + hasown@2.0.3: + dependencies: + function-bind: 1.1.2 + + hast-util-from-parse5@8.0.3: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + devlop: 1.1.0 + hastscript: 9.0.1 + property-information: 7.1.0 + vfile: 6.0.3 + vfile-location: 5.0.3 + web-namespaces: 2.0.1 + + hast-util-heading-rank@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-parse-selector@4.0.0: + dependencies: + '@types/hast': 3.0.4 + + hast-util-properties-to-mdx-jsx-attributes@1.1.1: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + estree-util-value-to-estree: 3.5.0 + mdast-util-mdx-jsx: 3.2.0 + property-information: 7.1.0 + style-to-js: 1.1.21 + transitivePeerDependencies: + - supports-color + + hast-util-raw@9.1.0: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + '@ungap/structured-clone': 1.3.1 + hast-util-from-parse5: 8.0.3 + hast-util-to-parse5: 8.0.1 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + parse5: 7.3.0 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-estree@3.1.3: + dependencies: + '@types/estree': 1.0.8 + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-attach-comments: 3.0.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + zwitch: 2.0.4 + transitivePeerDependencies: + - supports-color + + hast-util-to-html@9.0.5: + dependencies: + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + comma-separated-tokens: 2.0.3 + hast-util-whitespace: 3.0.0 + html-void-elements: 3.0.0 + mdast-util-to-hast: 13.2.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + stringify-entities: 4.0.4 + zwitch: 2.0.4 + + hast-util-to-jsx-runtime@2.3.6: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/unist': 3.0.3 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + hast-util-whitespace: 3.0.0 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + style-to-js: 1.1.21 + unist-util-position: 5.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + hast-util-to-parse5@8.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + devlop: 1.1.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + web-namespaces: 2.0.1 + zwitch: 2.0.4 + + hast-util-to-string@3.0.1: + dependencies: + '@types/hast': 3.0.4 + + hast-util-whitespace@3.0.0: + dependencies: + '@types/hast': 3.0.4 + + hastscript@9.0.1: + dependencies: + '@types/hast': 3.0.4 + comma-separated-tokens: 2.0.3 + hast-util-parse-selector: 4.0.0 + property-information: 7.1.0 + space-separated-tokens: 2.0.2 + + hoist-non-react-statics@3.3.2: + dependencies: + react-is: 16.13.1 + + hono@4.12.14: {} + + html-url-attributes@3.0.1: {} + + html-void-elements@3.0.0: {} + + http-terminator@3.2.0: + dependencies: + delay: 5.0.0 + p-wait-for: 3.2.0 + roarr: 7.21.4 + type-fest: 2.19.0 + + ignore@5.3.2: {} + + ignore@7.0.5: {} + + import-fresh@3.3.1: + dependencies: + parent-module: 1.0.1 + resolve-from: 4.0.0 + + import-in-the-middle@2.0.6: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + + import-in-the-middle@3.0.1: + dependencies: + acorn: 8.16.0 + acorn-import-attributes: 1.9.5(acorn@8.16.0) + cjs-module-lexer: 2.2.0 + module-details-from-path: 1.0.4 + + imurmurhash@0.1.4: {} + + inline-style-parser@0.2.7: {} + + invariant@2.2.4: + dependencies: + loose-envify: 1.4.0 + + is-alphabetical@2.0.1: {} + + is-alphanumerical@2.0.1: + dependencies: + is-alphabetical: 2.0.1 + is-decimal: 2.0.1 + + is-decimal@2.0.1: {} + + is-extendable@0.1.1: {} + + is-extglob@2.1.1: {} + + is-glob@4.0.3: + dependencies: + is-extglob: 2.1.1 + + is-hexadecimal@2.0.1: {} + + is-obj@1.0.1: {} + + is-plain-obj@4.1.0: {} + + is-regexp@1.0.0: {} + + isexe@2.0.0: {} + + javascript-stringify@2.1.0: {} + + jiti@2.7.0: {} + + js-tokens@4.0.0: {} + + js-yaml@3.14.2: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + + js-yaml@4.1.1: + dependencies: + argparse: 2.0.1 + + json-buffer@3.0.1: {} + + json-schema-to-typescript-lite@15.0.0: + dependencies: + '@apidevtools/json-schema-ref-parser': 14.2.1(@types/json-schema@7.0.15) + '@types/json-schema': 7.0.15 + + json-schema-traverse@0.4.1: {} + + json-schema-traverse@1.0.0: {} + + json-stable-stringify-without-jsonify@1.0.1: {} + + jsonpointer@5.0.1: {} + + keyv@4.5.4: + dependencies: + json-buffer: 3.0.1 + + kind-of@6.0.3: {} + + leven@4.1.0: {} + + levn@0.4.1: + dependencies: + prelude-ls: 1.2.1 + type-check: 0.4.0 + + lightningcss-android-arm64@1.32.0: + optional: true + + lightningcss-darwin-arm64@1.32.0: + optional: true + + lightningcss-darwin-x64@1.32.0: + optional: true + + lightningcss-freebsd-x64@1.32.0: + optional: true + + lightningcss-linux-arm-gnueabihf@1.32.0: + optional: true + + lightningcss-linux-arm64-gnu@1.32.0: + optional: true + + lightningcss-linux-arm64-musl@1.32.0: + optional: true + + lightningcss-linux-x64-gnu@1.32.0: + optional: true + + lightningcss-linux-x64-musl@1.32.0: + optional: true + + lightningcss-win32-arm64-msvc@1.32.0: + optional: true + + lightningcss-win32-x64-msvc@1.32.0: + optional: true + + lightningcss@1.32.0: + dependencies: + detect-libc: 2.1.2 + optionalDependencies: + lightningcss-android-arm64: 1.32.0 + lightningcss-darwin-arm64: 1.32.0 + lightningcss-darwin-x64: 1.32.0 + lightningcss-freebsd-x64: 1.32.0 + lightningcss-linux-arm-gnueabihf: 1.32.0 + lightningcss-linux-arm64-gnu: 1.32.0 + lightningcss-linux-arm64-musl: 1.32.0 + lightningcss-linux-x64-gnu: 1.32.0 + lightningcss-linux-x64-musl: 1.32.0 + lightningcss-win32-arm64-msvc: 1.32.0 + lightningcss-win32-x64-msvc: 1.32.0 + + locate-path@6.0.0: + dependencies: + p-locate: 5.0.0 + + lodash.merge@4.6.2: {} + + loglevel@1.9.2: {} + + longest-streak@3.1.0: {} + + loose-envify@1.4.0: + dependencies: + js-tokens: 4.0.0 + + lru-cache@10.4.3: {} + + lru-cache@11.3.6: {} + + lucide-react@1.8.0(react@19.2.5): + dependencies: + react: 19.2.5 + + magic-string@0.30.21: + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + + markdown-extensions@2.0.0: {} + + markdown-table@3.0.4: {} + + math-intrinsics@1.1.0: {} + + mdast-util-directive@3.1.0: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-visit-parents: 6.0.2 + transitivePeerDependencies: + - supports-color + + mdast-util-find-and-replace@3.0.2: + dependencies: + '@types/mdast': 4.0.4 + escape-string-regexp: 5.0.0 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + mdast-util-from-markdown@2.0.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + mdast-util-to-string: 4.0.0 + micromark: 4.0.2 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-decode-string: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-stringify-position: 4.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-frontmatter@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + escape-string-regexp: 5.0.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-extension-frontmatter: 2.0.0 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-autolink-literal@2.0.1: + dependencies: + '@types/mdast': 4.0.4 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-find-and-replace: 3.0.2 + micromark-util-character: 2.1.1 + + mdast-util-gfm-footnote@2.1.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + micromark-util-normalize-identifier: 2.0.1 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-strikethrough@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-table@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + markdown-table: 3.0.4 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm-task-list-item@2.0.0: + dependencies: + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-gfm@3.1.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-gfm-autolink-literal: 2.0.1 + mdast-util-gfm-footnote: 2.1.0 + mdast-util-gfm-strikethrough: 2.0.0 + mdast-util-gfm-table: 2.0.0 + mdast-util-gfm-task-list-item: 2.0.0 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-expression@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx-jsx@3.2.0: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + ccount: 2.0.1 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + parse-entities: 4.0.2 + stringify-entities: 4.0.4 + unist-util-stringify-position: 4.0.0 + vfile-message: 4.0.3 + transitivePeerDependencies: + - supports-color + + mdast-util-mdx@3.0.0: + dependencies: + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx-expression: 2.0.1 + mdast-util-mdx-jsx: 3.2.0 + mdast-util-mdxjs-esm: 2.0.1 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-mdxjs-esm@2.0.1: + dependencies: + '@types/estree-jsx': 1.0.5 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + devlop: 1.1.0 + mdast-util-from-markdown: 2.0.2 + mdast-util-to-markdown: 2.1.2 + transitivePeerDependencies: + - supports-color + + mdast-util-phrasing@4.1.0: + dependencies: + '@types/mdast': 4.0.4 + unist-util-is: 6.0.1 + + mdast-util-to-hast@13.2.1: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@ungap/structured-clone': 1.3.1 + devlop: 1.1.0 + micromark-util-sanitize-uri: 2.0.1 + trim-lines: 3.0.1 + unist-util-position: 5.0.0 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + + mdast-util-to-markdown@2.1.2: + dependencies: + '@types/mdast': 4.0.4 + '@types/unist': 3.0.3 + longest-streak: 3.1.0 + mdast-util-phrasing: 4.1.0 + mdast-util-to-string: 4.0.0 + micromark-util-classify-character: 2.0.1 + micromark-util-decode-string: 2.0.1 + unist-util-visit: 5.1.0 + zwitch: 2.0.4 + + mdast-util-to-string@4.0.0: + dependencies: + '@types/mdast': 4.0.4 + + micromark-core-commonmark@2.0.3: + dependencies: + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-factory-destination: 2.0.1 + micromark-factory-label: 2.0.1 + micromark-factory-space: 2.0.1 + micromark-factory-title: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-html-tag-name: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-directive@3.0.2: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-factory-whitespace: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + parse-entities: 4.0.2 + + micromark-extension-frontmatter@2.0.0: + dependencies: + fault: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-autolink-literal@2.1.0: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-footnote@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-strikethrough@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-classify-character: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-table@2.1.1: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm-tagfilter@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-gfm-task-list-item@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-gfm@3.0.0: + dependencies: + micromark-extension-gfm-autolink-literal: 2.1.0 + micromark-extension-gfm-footnote: 2.1.0 + micromark-extension-gfm-strikethrough: 2.1.0 + micromark-extension-gfm-table: 2.1.1 + micromark-extension-gfm-tagfilter: 2.0.0 + micromark-extension-gfm-task-list-item: 2.1.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-expression@3.0.1: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-extension-mdx-jsx@3.0.2: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + estree-util-is-identifier-name: 3.0.0 + micromark-factory-mdx-expression: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-extension-mdx-md@2.0.0: + dependencies: + micromark-util-types: 2.0.2 + + micromark-extension-mdxjs-esm@3.0.0: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-extension-mdxjs@3.0.0: + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + micromark-extension-mdx-expression: 3.0.1 + micromark-extension-mdx-jsx: 3.0.2 + micromark-extension-mdx-md: 2.0.0 + micromark-extension-mdxjs-esm: 3.0.0 + micromark-util-combine-extensions: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-destination@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-label@2.0.1: + dependencies: + devlop: 1.1.0 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-mdx-expression@2.0.3: + dependencies: + '@types/estree': 1.0.8 + devlop: 1.1.0 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-events-to-acorn: 2.0.3 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + unist-util-position-from-estree: 2.0.0 + vfile-message: 4.0.3 + + micromark-factory-space@1.1.0: + dependencies: + micromark-util-character: 1.2.0 + micromark-util-types: 1.1.0 + + micromark-factory-space@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-types: 2.0.2 + + micromark-factory-title@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-factory-whitespace@2.0.1: + dependencies: + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-character@1.2.0: + dependencies: + micromark-util-symbol: 1.1.0 + micromark-util-types: 1.1.0 + + micromark-util-character@2.1.1: + dependencies: + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-chunked@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-classify-character@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-combine-extensions@2.0.1: + dependencies: + micromark-util-chunked: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-decode-numeric-character-reference@2.0.2: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-decode-string@2.0.1: + dependencies: + decode-named-character-reference: 1.3.0 + micromark-util-character: 2.1.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-symbol: 2.0.1 + + micromark-util-encode@2.0.1: {} + + micromark-util-events-to-acorn@2.0.3: + dependencies: + '@types/estree': 1.0.8 + '@types/unist': 3.0.3 + devlop: 1.1.0 + estree-util-visit: 2.0.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + vfile-message: 4.0.3 + + micromark-util-html-tag-name@2.0.1: {} + + micromark-util-normalize-identifier@2.0.1: + dependencies: + micromark-util-symbol: 2.0.1 + + micromark-util-resolve-all@2.0.1: + dependencies: + micromark-util-types: 2.0.2 + + micromark-util-sanitize-uri@2.0.1: + dependencies: + micromark-util-character: 2.1.1 + micromark-util-encode: 2.0.1 + micromark-util-symbol: 2.0.1 + + micromark-util-subtokenize@2.1.0: + dependencies: + devlop: 1.1.0 + micromark-util-chunked: 2.0.1 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + + micromark-util-symbol@1.1.0: {} + + micromark-util-symbol@2.0.1: {} + + micromark-util-types@1.1.0: {} + + micromark-util-types@2.0.2: {} + + micromark@4.0.2: + dependencies: + '@types/debug': 4.1.13 + debug: 4.4.3 + decode-named-character-reference: 1.3.0 + devlop: 1.1.0 + micromark-core-commonmark: 2.0.3 + micromark-factory-space: 2.0.1 + micromark-util-character: 2.1.1 + micromark-util-chunked: 2.0.1 + micromark-util-combine-extensions: 2.0.1 + micromark-util-decode-numeric-character-reference: 2.0.2 + micromark-util-encode: 2.0.1 + micromark-util-normalize-identifier: 2.0.1 + micromark-util-resolve-all: 2.0.1 + micromark-util-sanitize-uri: 2.0.1 + micromark-util-subtokenize: 2.1.0 + micromark-util-symbol: 2.0.1 + micromark-util-types: 2.0.2 + transitivePeerDependencies: + - supports-color + + minimatch@10.2.5: + dependencies: + brace-expansion: 5.0.5 + + minimatch@3.1.5: + dependencies: + brace-expansion: 1.1.14 + + minipass@7.1.3: {} + + module-details-from-path@1.0.4: {} + + motion-dom@12.38.0: + dependencies: + motion-utils: 12.36.0 + + motion-utils@12.36.0: {} + + motion@12.35.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + framer-motion: 12.38.0(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + tslib: 2.8.1 + optionalDependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + ms@2.1.3: {} + + nanoevents@9.1.0: {} + + nanoid@3.3.12: {} + + natural-compare@1.4.0: {} + + next-themes@0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + + oauth4webapi@3.8.5: {} + + object-inspect@1.13.4: {} + + oniguruma-parser@0.12.2: {} + + oniguruma-to-es@4.3.6: + dependencies: + oniguruma-parser: 0.12.2 + regex: 6.1.0 + regex-recursion: 6.0.2 + + openapi-types@12.1.3: {} + + optionator@0.9.4: + dependencies: + deep-is: 0.1.4 + fast-levenshtein: 2.0.6 + levn: 0.4.1 + prelude-ls: 1.2.1 + type-check: 0.4.0 + word-wrap: 1.2.5 + + p-finally@1.0.0: {} + + p-limit@3.1.0: + dependencies: + yocto-queue: 0.1.0 + + p-locate@5.0.0: + dependencies: + p-limit: 3.1.0 + + p-timeout@3.2.0: + dependencies: + p-finally: 1.0.0 + + p-wait-for@3.2.0: + dependencies: + p-timeout: 3.2.0 + + pagefind@1.5.2: + optionalDependencies: + '@pagefind/darwin-arm64': 1.5.2 + '@pagefind/darwin-x64': 1.5.2 + '@pagefind/freebsd-x64': 1.5.2 + '@pagefind/linux-arm64': 1.5.2 + '@pagefind/linux-x64': 1.5.2 + '@pagefind/windows-arm64': 1.5.2 + '@pagefind/windows-x64': 1.5.2 + + parent-module@1.0.1: + dependencies: + callsites: 3.1.0 + + parse-entities@4.0.2: + dependencies: + '@types/unist': 2.0.11 + character-entities-legacy: 3.0.0 + character-reference-invalid: 2.0.1 + decode-named-character-reference: 1.3.0 + is-alphanumerical: 2.0.1 + is-decimal: 2.0.1 + is-hexadecimal: 2.0.1 + + parse-srcset@1.0.2: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-exists@4.0.0: {} + + path-key@3.1.1: {} + + path-scurry@2.0.2: + dependencies: + lru-cache: 11.3.6 + minipass: 7.1.3 + + pg-int8@1.0.1: {} + + pg-protocol@1.13.0: {} + + pg-types@2.2.0: + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.1 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + + picocolors@1.1.1: {} + + picomatch@4.0.4: {} + + piscina@5.1.4: + optionalDependencies: + '@napi-rs/nice': 1.1.1 + + postcss-selector-parser@6.0.10: + dependencies: + cssesc: 3.0.0 + util-deprecate: 1.0.2 + + postcss@8.5.14: + dependencies: + nanoid: 3.3.12 + picocolors: 1.1.1 + source-map-js: 1.2.1 + + postgres-array@2.0.0: {} + + postgres-bytea@1.0.1: {} + + postgres-date@1.0.7: {} + + postgres-interval@1.2.0: + dependencies: + xtend: 4.0.2 + + posthog-node@5.26.0: + dependencies: + '@posthog/core': 1.23.1 + + prelude-ls@1.2.1: {} + + property-information@7.1.0: {} + + punycode@1.4.1: {} + + punycode@2.3.1: {} + + qs@6.15.1: + dependencies: + side-channel: 1.1.0 + + react-dom@19.2.5(react@19.2.5): + dependencies: + react: 19.2.5 + scheduler: 0.27.0 + + react-error-boundary@6.1.1(react@19.2.5): + dependencies: + react: 19.2.5 + + react-fast-compare@3.2.2: {} + + react-hook-form@7.71.2(react@19.2.5): + dependencies: + react: 19.2.5 + + react-is@16.13.1: {} + + react-is@19.2.5: {} + + react-markdown@10.1.0(@types/react@19.2.14)(react@19.2.5): + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + '@types/react': 19.2.14 + devlop: 1.1.0 + hast-util-to-jsx-runtime: 2.3.6 + html-url-attributes: 3.0.1 + mdast-util-to-hast: 13.2.1 + react: 19.2.5 + remark-parse: 11.0.0 + remark-rehype: 11.1.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vfile: 6.0.3 + transitivePeerDependencies: + - supports-color + + react-remove-scroll-bar@2.3.8(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react-remove-scroll@2.7.2(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + react-remove-scroll-bar: 2.3.8(@types/react@19.2.14)(react@19.2.5) + react-style-singleton: 2.2.3(@types/react@19.2.14)(react@19.2.5) + tslib: 2.8.1 + use-callback-ref: 1.3.3(@types/react@19.2.14)(react@19.2.5) + use-sidecar: 1.1.3(@types/react@19.2.14)(react@19.2.5) + optionalDependencies: + '@types/react': 19.2.14 + + react-router@7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + cookie: 1.1.1 + react: 19.2.5 + set-cookie-parser: 2.7.2 + optionalDependencies: + react-dom: 19.2.5(react@19.2.5) + + react-style-singleton@2.2.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + get-nonce: 1.0.1 + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + react@19.2.5: {} + + recma-build-jsx@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-build-jsx: 3.0.1 + vfile: 6.0.3 + + recma-jsx@1.0.1(acorn@8.16.0): + dependencies: + acorn: 8.16.0 + acorn-jsx: 5.3.2(acorn@8.16.0) + estree-util-to-js: 2.0.0 + recma-parse: 1.0.0 + recma-stringify: 1.0.0 + unified: 11.0.5 + + recma-parse@1.0.0: + dependencies: + '@types/estree': 1.0.8 + esast-util-from-js: 2.0.1 + unified: 11.0.5 + vfile: 6.0.3 + + recma-stringify@1.0.0: + dependencies: + '@types/estree': 1.0.8 + estree-util-to-js: 2.0.0 + unified: 11.0.5 + vfile: 6.0.3 + + regex-recursion@6.0.2: + dependencies: + regex-utilities: 2.3.0 + + regex-utilities@2.3.0: {} + + regex@6.1.0: + dependencies: + regex-utilities: 2.3.0 + + rehype-mdx-import-media@1.2.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-properties-to-mdx-jsx-attributes: 1.1.1 + parse-srcset: 1.0.2 + unified: 11.0.5 + unist-util-visit: 5.1.0 + transitivePeerDependencies: + - supports-color + + rehype-raw@7.0.0: + dependencies: + '@types/hast': 3.0.4 + hast-util-raw: 9.1.0 + vfile: 6.0.3 + + rehype-recma@1.0.0: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + hast-util-to-estree: 3.1.3 + transitivePeerDependencies: + - supports-color + + rehype-slug@6.0.0: + dependencies: + '@types/hast': 3.0.4 + github-slugger: 2.0.0 + hast-util-heading-rank: 3.0.0 + hast-util-to-string: 3.0.1 + unist-util-visit: 5.1.0 + + remark-comment@1.0.0: + dependencies: + micromark-factory-space: 1.1.0 + micromark-util-character: 1.2.0 + micromark-util-symbol: 1.1.0 + + remark-directive-rehype@1.0.0: + dependencies: + hastscript: 9.0.1 + unist-util-map: 4.0.0 + + remark-directive@3.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-directive: 3.1.0 + micromark-extension-directive: 3.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-frontmatter@5.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-frontmatter: 2.0.1 + micromark-extension-frontmatter: 2.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-gfm@4.0.1: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-gfm: 3.1.0 + micromark-extension-gfm: 3.0.0 + remark-parse: 11.0.0 + remark-stringify: 11.0.0 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-mdx-frontmatter@5.2.0: + dependencies: + '@types/mdast': 4.0.4 + estree-util-value-to-estree: 3.5.0 + toml: 3.0.0 + unified: 11.0.5 + unist-util-mdx-define: 1.1.2 + yaml: 2.8.3 + + remark-mdx@3.1.1: + dependencies: + mdast-util-mdx: 3.0.0 + micromark-extension-mdxjs: 3.0.0 + transitivePeerDependencies: + - supports-color + + remark-parse@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-from-markdown: 2.0.2 + micromark-util-types: 2.0.2 + unified: 11.0.5 + transitivePeerDependencies: + - supports-color + + remark-rehype@11.1.2: + dependencies: + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + mdast-util-to-hast: 13.2.1 + unified: 11.0.5 + vfile: 6.0.3 + + remark-stringify@11.0.0: + dependencies: + '@types/mdast': 4.0.4 + mdast-util-to-markdown: 2.1.2 + unified: 11.0.5 + + require-from-string@2.0.2: {} + + require-in-the-middle@8.0.1: + dependencies: + debug: 4.4.3 + module-details-from-path: 1.0.4 + transitivePeerDependencies: + - supports-color + + reselect@5.1.1: {} + + resolve-from@4.0.0: {} + + roarr@7.21.4: + dependencies: + fast-printf: 1.6.10 + safe-stable-stringify: 2.5.0 + semver-compare: 1.0.0 + + rolldown@1.0.0-rc.16: + dependencies: + '@oxc-project/types': 0.126.0 + '@rolldown/pluginutils': 1.0.0-rc.16 + optionalDependencies: + '@rolldown/binding-android-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-arm64': 1.0.0-rc.16 + '@rolldown/binding-darwin-x64': 1.0.0-rc.16 + '@rolldown/binding-freebsd-x64': 1.0.0-rc.16 + '@rolldown/binding-linux-arm-gnueabihf': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-arm64-musl': 1.0.0-rc.16 + '@rolldown/binding-linux-ppc64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-s390x-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-gnu': 1.0.0-rc.16 + '@rolldown/binding-linux-x64-musl': 1.0.0-rc.16 + '@rolldown/binding-openharmony-arm64': 1.0.0-rc.16 + '@rolldown/binding-wasm32-wasi': 1.0.0-rc.16 + '@rolldown/binding-win32-arm64-msvc': 1.0.0-rc.16 + '@rolldown/binding-win32-x64-msvc': 1.0.0-rc.16 + + rollup@4.60.3: + dependencies: + '@types/estree': 1.0.8 + optionalDependencies: + '@rollup/rollup-android-arm-eabi': 4.60.3 + '@rollup/rollup-android-arm64': 4.60.3 + '@rollup/rollup-darwin-arm64': 4.60.3 + '@rollup/rollup-darwin-x64': 4.60.3 + '@rollup/rollup-freebsd-arm64': 4.60.3 + '@rollup/rollup-freebsd-x64': 4.60.3 + '@rollup/rollup-linux-arm-gnueabihf': 4.60.3 + '@rollup/rollup-linux-arm-musleabihf': 4.60.3 + '@rollup/rollup-linux-arm64-gnu': 4.60.3 + '@rollup/rollup-linux-arm64-musl': 4.60.3 + '@rollup/rollup-linux-loong64-gnu': 4.60.3 + '@rollup/rollup-linux-loong64-musl': 4.60.3 + '@rollup/rollup-linux-ppc64-gnu': 4.60.3 + '@rollup/rollup-linux-ppc64-musl': 4.60.3 + '@rollup/rollup-linux-riscv64-gnu': 4.60.3 + '@rollup/rollup-linux-riscv64-musl': 4.60.3 + '@rollup/rollup-linux-s390x-gnu': 4.60.3 + '@rollup/rollup-linux-x64-gnu': 4.60.3 + '@rollup/rollup-linux-x64-musl': 4.60.3 + '@rollup/rollup-openbsd-x64': 4.60.3 + '@rollup/rollup-openharmony-arm64': 4.60.3 + '@rollup/rollup-win32-arm64-msvc': 4.60.3 + '@rollup/rollup-win32-ia32-msvc': 4.60.3 + '@rollup/rollup-win32-x64-gnu': 4.60.3 + '@rollup/rollup-win32-x64-msvc': 4.60.3 + fsevents: 2.3.3 + + safe-stable-stringify@2.5.0: {} + + sax@1.6.0: {} + + scheduler@0.27.0: {} + + section-matter@1.0.0: + dependencies: + extend-shallow: 2.0.1 + kind-of: 6.0.3 + + semver-compare@1.0.0: {} + + semver@7.7.4: {} + + set-cookie-parser@2.7.2: {} + + shallowequal@1.1.0: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + shiki@4.0.2: + dependencies: + '@shikijs/core': 4.0.2 + '@shikijs/engine-javascript': 4.0.2 + '@shikijs/engine-oniguruma': 4.0.2 + '@shikijs/langs': 4.0.2 + '@shikijs/themes': 4.0.2 + '@shikijs/types': 4.0.2 + '@shikijs/vscode-textmate': 10.0.2 + '@types/hast': 3.0.4 + + side-channel-list@1.0.1: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + + side-channel-map@1.0.1: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + + side-channel-weakmap@1.0.2: + dependencies: + call-bound: 1.0.4 + es-errors: 1.3.0 + get-intrinsic: 1.3.0 + object-inspect: 1.13.4 + side-channel-map: 1.0.1 + + side-channel@1.1.0: + dependencies: + es-errors: 1.3.0 + object-inspect: 1.13.4 + side-channel-list: 1.0.1 + side-channel-map: 1.0.1 + side-channel-weakmap: 1.0.2 + + sitemap@9.0.1: + dependencies: + '@types/node': 24.12.2 + '@types/sax': 1.2.7 + arg: 5.0.2 + sax: 1.6.0 + + source-map-js@1.2.1: {} + + source-map@0.7.6: {} + + space-separated-tokens@2.0.2: {} + + sprintf-js@1.0.3: {} + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.5.0 + strip-ansi: 7.2.0 + + stringify-entities@4.0.4: + dependencies: + character-entities-html4: 2.1.0 + character-entities-legacy: 3.0.0 + + stringify-object@3.3.0: + dependencies: + get-own-enumerable-property-symbols: 3.0.2 + is-obj: 1.0.1 + is-regexp: 1.0.0 + + strip-ansi@7.2.0: + dependencies: + ansi-regex: 6.2.2 + + strip-bom-string@1.0.0: {} + + strip-json-comments@3.1.1: {} + + style-to-js@1.1.21: + dependencies: + style-to-object: 1.0.14 + + style-to-object@1.0.14: + dependencies: + inline-style-parser: 0.2.7 + + supports-color@7.2.0: + dependencies: + has-flag: 4.0.0 + + tailwind-merge@3.5.0: {} + + tailwindcss@4.2.1: {} + + tailwindcss@4.2.2: {} + + tapable@2.3.3: {} + + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + + toml@3.0.0: {} + + trim-lines@3.0.1: {} + + trough@2.2.0: {} + + ts-api-utils@2.5.0(typescript@6.0.3): + dependencies: + typescript: 6.0.3 + + tslib@2.8.1: {} + + tw-animate-css@1.4.0: {} + + type-check@0.4.0: + dependencies: + prelude-ls: 1.2.1 + + type-fest@2.19.0: {} + + typescript@6.0.3: {} + + undici-types@7.16.0: {} + + undici-types@7.19.2: {} + + unified@11.0.5: + dependencies: + '@types/unist': 3.0.3 + bail: 2.0.2 + devlop: 1.1.0 + extend: 3.0.2 + is-plain-obj: 4.1.0 + trough: 2.2.0 + vfile: 6.0.3 + + unist-util-is@6.0.1: + dependencies: + '@types/unist': 3.0.3 + + unist-util-map@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-mdx-define@1.1.2: + dependencies: + '@types/estree': 1.0.8 + '@types/hast': 3.0.4 + '@types/mdast': 4.0.4 + estree-util-is-identifier-name: 3.0.0 + estree-util-scope: 1.0.0 + estree-walker: 3.0.3 + vfile: 6.0.3 + + unist-util-position-from-estree@2.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-position@5.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-stringify-position@4.0.0: + dependencies: + '@types/unist': 3.0.3 + + unist-util-visit-parents@6.0.2: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + + unist-util-visit@5.1.0: + dependencies: + '@types/unist': 3.0.3 + unist-util-is: 6.0.1 + unist-util-visit-parents: 6.0.2 + + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + + url@0.11.4: + dependencies: + punycode: 1.4.1 + qs: 6.15.1 + + urlpattern-polyfill@10.1.0: {} + + use-callback-ref@1.3.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sidecar@1.1.3(@types/react@19.2.14)(react@19.2.5): + dependencies: + detect-node-es: 1.1.0 + react: 19.2.5 + tslib: 2.8.1 + optionalDependencies: + '@types/react': 19.2.14 + + use-sync-external-store@1.6.0(react@19.2.5): + dependencies: + react: 19.2.5 + + util-deprecate@1.0.2: {} + + vaul@1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5): + dependencies: + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + + vfile-location@5.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile: 6.0.3 + + vfile-message@4.0.3: + dependencies: + '@types/unist': 3.0.3 + unist-util-stringify-position: 4.0.0 + + vfile@6.0.3: + dependencies: + '@types/unist': 3.0.3 + vfile-message: 4.0.3 + + vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.8.3): + dependencies: + lightningcss: 1.32.0 + picomatch: 4.0.4 + postcss: 8.5.14 + rolldown: 1.0.0-rc.16 + tinyglobby: 0.2.16 + optionalDependencies: + '@types/node': 25.6.0 + esbuild: 0.28.0 + fsevents: 2.3.3 + jiti: 2.7.0 + yaml: 2.8.3 + + web-namespaces@2.0.1: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + word-wrap@1.2.5: {} + + wrap-ansi@9.0.2: + dependencies: + ansi-styles: 6.2.3 + string-width: 7.2.0 + strip-ansi: 7.2.0 + + xtend@4.0.2: {} + + y18n@5.0.8: {} + + yaml@2.8.3: {} + + yargs-parser@22.0.0: {} + + yargs@18.0.0: + dependencies: + cliui: 9.0.1 + escalade: 3.2.0 + get-caller-file: 2.0.5 + string-width: 7.2.0 + y18n: 5.0.8 + yargs-parser: 22.0.0 + + yocto-queue@0.1.0: {} + + zod@3.25.76: {} + + zod@4.3.6: {} + + zudoku@0.77.0(@types/json-schema@7.0.15)(@types/node@25.6.0)(jiti@2.7.0)(react-dom@19.2.5(react@19.2.5))(react@19.2.5)(rollup@4.60.3)(use-sync-external-store@1.6.0(react@19.2.5)): + dependencies: + '@apidevtools/json-schema-ref-parser': 15.3.5(@types/json-schema@7.0.15) + '@base-ui/react': 1.4.1(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@envelop/core': 5.5.1 + '@graphql-typed-document-node/core': 3.2.0(graphql@16.13.2) + '@hono/node-server': 1.19.13(hono@4.12.14) + '@lekoarts/rehype-meta-as-attributes': 3.0.3 + '@mdx-js/react': 3.1.1(@types/react@19.2.14)(react@19.2.5) + '@mdx-js/rollup': 3.1.1(rollup@4.60.3) + '@pothos/core': 4.12.0(graphql@16.13.2) + '@radix-ui/react-accordion': 1.2.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-alert-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-aspect-ratio': 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-checkbox': 1.3.3(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-collapsible': 1.1.12(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-dropdown-menu': 2.1.16(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-hover-card': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-label': 2.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-navigation-menu': 1.2.14(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-popover': 1.1.15(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-progress': 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-radio-group': 1.3.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-scroll-area': 1.2.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-select': 2.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-separator': 1.1.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slider': 1.3.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-slot': 1.2.4(@types/react@19.2.14)(react@19.2.5) + '@radix-ui/react-switch': 1.2.6(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-tabs': 1.1.13(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-toggle': 1.1.10(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-toggle-group': 1.1.11(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-tooltip': 1.2.8(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@radix-ui/react-visually-hidden': 1.2.4(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@scalar/openapi-parser': 0.23.13 + '@sentry/node': 10.49.0 + '@shikijs/langs': 4.0.2 + '@shikijs/rehype': 4.0.2 + '@shikijs/themes': 4.0.2 + '@shikijs/transformers': 4.0.2 + '@tailwindcss/typography': 0.5.19(tailwindcss@4.2.1) + '@tailwindcss/vite': 4.2.2(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.8.3)) + '@tanem/react-nprogress': 6.0.3(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + '@tanstack/react-query': 5.97.0(react@19.2.5) + '@types/react': 19.2.14 + '@types/react-dom': 19.2.3(@types/react@19.2.14) + '@vitejs/plugin-react': 6.0.1(vite@8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.8.3)) + '@x0k/json-schema-merge': 1.0.2 + '@zudoku/httpsnippet': 10.0.9 + '@zudoku/react-helmet-async': 2.0.5(react@19.2.5) + '@zuplo/mcp': 0.0.32 + bs58: 6.0.0 + class-variance-authority: 0.7.1 + clsx: 2.1.1 + cmdk: 1.1.1(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + dotenv: 17.3.1 + embla-carousel-react: 8.6.0(react@19.2.5) + esbuild: 0.28.0 + estree-util-is-identifier-name: 3.0.0 + estree-util-value-to-estree: 3.5.0 + fast-equals: 6.0.0 + glob: 13.0.6 + glob-parent: 6.0.2 + graphql: 16.13.2 + graphql-type-json: 0.3.2(graphql@16.13.2) + graphql-yoga: 5.18.0(graphql@16.13.2) + gray-matter: 4.0.3 + hast-util-heading-rank: 3.0.0 + hast-util-to-jsx-runtime: 2.3.6 + hast-util-to-string: 3.0.1 + hono: 4.12.14 + http-terminator: 3.2.0 + javascript-stringify: 2.1.0 + json-schema-to-typescript-lite: 15.0.0 + loglevel: 1.9.2 + lucide-react: 1.8.0(react@19.2.5) + mdast-util-from-markdown: 2.0.2 + mdast-util-mdx: 3.0.0 + mdast-util-mdx-jsx: 3.2.0 + micromark-extension-mdxjs: 3.0.0 + motion: 12.35.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + nanoevents: 9.1.0 + next-themes: 0.4.6(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + oauth4webapi: 3.8.5 + openapi-types: 12.1.3 + pagefind: 1.5.2 + picocolors: 1.1.1 + piscina: 5.1.4 + posthog-node: 5.26.0 + react: 19.2.5 + react-dom: 19.2.5(react@19.2.5) + react-error-boundary: 6.1.1(react@19.2.5) + react-hook-form: 7.71.2(react@19.2.5) + react-is: 19.2.5 + react-markdown: 10.1.0(@types/react@19.2.14)(react@19.2.5) + react-router: 7.14.1(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + rehype-mdx-import-media: 1.2.0 + rehype-raw: 7.0.0 + rehype-slug: 6.0.0 + remark-comment: 1.0.0 + remark-directive: 3.0.1 + remark-directive-rehype: 1.0.0 + remark-frontmatter: 5.0.0 + remark-gfm: 4.0.1 + remark-mdx-frontmatter: 5.2.0 + semver: 7.7.4 + shiki: 4.0.2 + sitemap: 9.0.1 + strip-ansi: 7.2.0 + tailwind-merge: 3.5.0 + tailwindcss: 4.2.1 + tw-animate-css: 1.4.0 + unified: 11.0.5 + unist-util-visit: 5.1.0 + vaul: 1.1.2(@types/react-dom@19.2.3(@types/react@19.2.14))(@types/react@19.2.14)(react-dom@19.2.5(react@19.2.5))(react@19.2.5) + vfile: 6.0.3 + vite: 8.0.9(@types/node@25.6.0)(esbuild@0.28.0)(jiti@2.7.0)(yaml@2.8.3) + yaml: 2.8.3 + yargs: 18.0.0 + zod: 4.3.6 + zustand: 5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)) + transitivePeerDependencies: + - '@date-fns/tz' + - '@emotion/is-prop-valid' + - '@opentelemetry/exporter-trace-otlp-http' + - '@rolldown/plugin-babel' + - '@types/json-schema' + - '@types/node' + - '@vitejs/devtools' + - babel-plugin-react-compiler + - date-fns + - immer + - jiti + - less + - rollup + - sass + - sass-embedded + - stylus + - sugarss + - supports-color + - terser + - tsx + - use-sync-external-store + + zustand@5.0.12(@types/react@19.2.14)(react@19.2.5)(use-sync-external-store@1.6.0(react@19.2.5)): + optionalDependencies: + '@types/react': 19.2.14 + react: 19.2.5 + use-sync-external-store: 1.6.0(react@19.2.5) + + zwitch@2.0.4: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml new file mode 100644 index 00000000..49c0ad74 --- /dev/null +++ b/pnpm-workspace.yaml @@ -0,0 +1,2 @@ +allowBuilds: + esbuild: false diff --git a/public/logo-dark.svg b/public/logo-dark.svg new file mode 100644 index 00000000..0609474b --- /dev/null +++ b/public/logo-dark.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/logo-light.svg b/public/logo-light.svg new file mode 100644 index 00000000..81bf2f29 --- /dev/null +++ b/public/logo-light.svg @@ -0,0 +1,29 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/pagefind/pagefind.js b/public/pagefind/pagefind.js new file mode 100644 index 00000000..b37bb638 --- /dev/null +++ b/public/pagefind/pagefind.js @@ -0,0 +1 @@ +throw new Error("NOT_BUILT_YET"); \ No newline at end of file diff --git a/scripts/transform-readme.sh b/scripts/transform-readme.sh deleted file mode 100755 index d8849d96..00000000 --- a/scripts/transform-readme.sh +++ /dev/null @@ -1,25 +0,0 @@ -#!/usr/bin/env bash -# SPDX-License-Identifier: BSD-3-Clause -# Copyright (c) 2025, Unikraft GmbH. -# Licensed under the BSD-3-Clause License (the "License"). -# You may not use this file except in compliance with the License. - -set -euo pipefail - -INPUT_FILE="${1:?Input file required}" -OUTPUT_FILE="${2:?Output file required}" -EXAMPLE_NAME="${3:-}" - -# Extract title from first H1 line, or use example name -TITLE="$(grep -m1 -E '^# ' "$INPUT_FILE" | sed 's/^# //' || echo "$EXAMPLE_NAME")" - -# Convert README.md to .mdx format -# This can be extended to: -# - Add front-matter -# - Convert markdown fenced code blocks if needed -# - Rewrite relative links -# - Add custom components - -# For now, just copy the content -# You can add transformations here as needed -cp "$INPUT_FILE" "$OUTPUT_FILE" diff --git a/scripts/transform_readme.py b/scripts/transform_readme.py new file mode 100755 index 00000000..4a633c1e --- /dev/null +++ b/scripts/transform_readme.py @@ -0,0 +1,508 @@ +#!/usr/bin/env python3 +""" +Convert an examples README.md into an MDX guide with small post-processing. + +Behavior: +- Read INPUT_FILE and determine TITLE from the first H1 (or use example name) +- Insert the Tabs import immediately after the H1 (or create an H1 from TITLE) +- Convert blockquote-style admonitions like: + > **Note:**\n+ > line1\n+ > line2 + into: + :::note\n line1\n line2\n ::: +- Rewrite absolute docs URLs back to site-relative paths +- Group adjacent titled code blocks into wrappers with syncKey +- Append a Source block when EXAMPLE_NAME is provided + +Usage: transform_readme.py INPUT_FILE OUTPUT_FILE [EXAMPLE_NAME] +""" +from __future__ import annotations +import argparse +import os +import re +from pathlib import Path + + +# ANSI color codes +ANSI_RESET = '\x1b[0m' +ANSI_BOLD = '\x1b[1m' +ANSI_DARK_GRAY = '\x1b[90m' +ANSI_GREEN = '\x1b[92m' +ANSI_LIGHT_BLUE = '\x1b[94m' + +STATE_COLORS: dict[str, str] = { + "running": ANSI_GREEN, + "standby": ANSI_LIGHT_BLUE, +} + +# Import statement for Tabs component +TABS_IMPORT = 'import { Tabs, TabsContent, TabsList, TabsTrigger } from "zudoku/ui/Tabs"\n\n' +GENERATED_FILE_COMMENT = "{/* THIS FILE WAS AUTOGENERATED FROM THE PUBLIC EXAMPLE REPOSITORY. DO NOT EDIT THIS FILE DIRECTLY. */}\n\n" + +# Regex patterns +FENCE_PATTERN = re.compile(r"(^```([^\n]*)\n)(.*?)(\n```)$", flags=re.M | re.S) +FRONT_MATTER_PATTERN = re.compile(r"^---\n.*?\n---\n\s*", flags=re.S) +ADMONITION_PATTERN = re.compile(r"^>\s*\*\*([^*]+?):?\*\*:?\s*$") + + +def extract_title(text: str, example_name: str | None) -> str: + m = re.search(r"^# (.+)$", text, flags=re.M) + if m: + return m.group(1).strip() + if example_name: + return example_name + return "" + + +def ensure_front_matter(text: str, title: str) -> str: + """Ensure YAML front-matter with title at the top, replacing existing if present.""" + fm = f"---\ntitle: \"{title}\"\n---\n\n{GENERATED_FILE_COMMENT}" + m = FRONT_MATTER_PATTERN.match(text) + if m: + return fm + text[m.end():] + return fm + text + + +def insert_tabs_import(text: str, title: str) -> str: + """Insert Tabs import after front-matter or H1, avoiding duplicates.""" + # If import already exists, do nothing + if "zudoku/ui/Tabs" in text: + return text + # If there's YAML front-matter at the top, insert after it. + m_fm = FRONT_MATTER_PATTERN.match(text) + if m_fm: + insert_at = m_fm.end() + return text[:insert_at] + TABS_IMPORT + text[insert_at:] + + # Find first H1 and insert after it + m = re.search(r"(^# .+)$", text, flags=re.M) + if m: + insert_at = m.end(1) + return text[:insert_at] + "\n" + TABS_IMPORT + text[insert_at:] + + # No front-matter nor H1: create H1 from title if available + if title: + return f"# {title}\n\n{TABS_IMPORT}\n" + text + + # Fallback: prepend import + return TABS_IMPORT + "\n" + text + + +def convert_admonitions(text: str) -> str: + """Convert blockquote-style admonitions to ::: syntax.""" + lines = text.splitlines() + out = [] + i = 0 + + while i < len(lines): + m = ADMONITION_PATTERN.match(lines[i]) + if m: + label = m.group(1).strip().rstrip(":") + key = label.lower().replace(" ", "-") + i += 1 + block = [] + while i < len(lines) and re.match(r"^> ?", lines[i]): + line = re.sub(r"^> ?", "", lines[i]) + block.append(line) + i += 1 + # Start admonition block + out.append(f":::{key}") + out.extend(block) + out.append(":::") + else: + out.append(lines[i]) + i += 1 + return "\n".join(out) + ("\n" if text.endswith("\n") else "") + + +def rewrite_urls(text: str) -> str: + """Rewrite absolute docs URLs to site-relative paths.""" + # General rule for any other /docs/ link + text = re.sub(r"https://unikraft.com/docs/([^\s\)]+)", r"/\1", text) + return text + + +def remove_cli_section_labels(text: str) -> str: + """Remove bold CLI section labels from examples READMEs.""" + pattern = re.compile( + r"(?m)^\*\*(Using the unikraft CLI \(Recommended\)|Using the legacy kraft CLI)\*\*\s*$\n?" + ) + return pattern.sub("", text) + + +def convert_code_tabs(text: str) -> str: + """Group adjacent code blocks containing title="..." inside a wrapper.""" + block_pattern = re.compile(r"(```([^\n]*?title=\"([^\"]+)\"[^\n]*)\n.*?\n```)", flags=re.S) + + matches = list(block_pattern.finditer(text)) + if not matches: + return text + + groups: list[list] = [] + # Separator that caused the split *before* groups[i]; None for the first group. + group_separators: list[str | None] = [] + current_group = [matches[0]] + current_sep: str | None = None + + for i in range(1, len(matches)): + prev = current_group[-1] + curr = matches[i] + + # Check if only whitespace or the word "or" exists between blocks + between = text[prev.end():curr.start()].strip().lower() + + prev_info = prev.group(2).strip().lower() + curr_info = curr.group(2).strip().lower() + + # "or" between an ansi (output) block and a bash (command) block marks + # the boundary between two CLI alternatives (e.g. listing cmd+output for + # unikraft followed by cmd+output for kraft). Break the group here so + # the two groups can later be interleaved into per-position . + if between == "or" and prev_info.startswith("ansi") and curr_info.startswith("bash"): + groups.append(current_group) + group_separators.append(current_sep) + current_sep = between + current_group = [curr] + elif between in ["", "or"]: + current_group.append(curr) + else: + groups.append(current_group) + group_separators.append(current_sep) + current_sep = between + current_group = [curr] + groups.append(current_group) + group_separators.append(current_sep) + + # Build a list of (start, end, replacement) spans to apply to the text. + # When two consecutive equal-length groups were split by "or" (cmd+output + # pattern), zip them positionally so that: + # [bash:A, ansi:A] "or" [bash:B, ansi:B] + # becomes: + # (bash:A, bash:B) + # (ansi:A, ansi:B) + replacements: list[tuple[int, int, str]] = [] + i = 0 + while i < len(groups): + if ( + i + 1 < len(groups) + and group_separators[i + 1] == "or" + and len(groups[i]) == len(groups[i + 1]) + and len(groups[i]) > 1 + ): + grp_a = groups[i] + grp_b = groups[i + 1] + parts = [] + for j in range(len(grp_a)): + blocks = sorted( + [grp_a[j].group(1), grp_b[j].group(1)], + key=lambda b: (0 if 'title="unikraft"' in b else 1), + ) + parts.append( + '\n\n' + "\n\n".join(blocks) + "\n\n" + ) + replacement = "\n\n".join(parts) + # Replace the entire span covering both groups (including the "or" between them) + start = grp_a[0].start() + end = grp_b[-1].end() + replacements.append((start, end, replacement)) + i += 2 + else: + group = groups[i] + if len(group) > 1: + blocks = sorted( + [m.group(1) for m in group], + key=lambda b: (0 if 'title="unikraft"' in b else 1), + ) + tabs_str = '\n\n' + "\n\n".join(blocks) + "\n\n" + replacements.append((group[0].start(), group[-1].end(), tabs_str)) + i += 1 + + out = text + for start, end, replacement in reversed(replacements): + out = out[:start] + replacement + out[end:] + + return out + + +def color_bracket_and_bullet(line: str) -> str: + """Color the [●] Deployed successfully! line.""" + line = line.replace('[', f'{ANSI_DARK_GRAY}[{ANSI_RESET}') + line = line.replace('●', f'{ANSI_GREEN}●{ANSI_RESET}') + line = line.replace(']', f'{ANSI_DARK_GRAY}]{ANSI_RESET}') + return line + + +def color_box_drawing_line(line: str) -> str: + """Color lines with box-drawing characters and labels.""" + # Match pattern like " ├────── name: value" or " │" (empty line) + colon_match = re.search(r'^(\s*)((?:│|├|└))((?:─*)\s*)([^:]+)(:)(.*)$', line) + + if colon_match: + leading_space = colon_match.group(1) # leading whitespace + box_char = colon_match.group(2) # │, ├, or └ + dashes = colon_match.group(3) # ─── and spaces + label = colon_match.group(4) # the label (e.g., "name") + colon = colon_match.group(5) # the colon + value = colon_match.group(6) # the value after colon + + # Color the box character + box_colored = f'{ANSI_DARK_GRAY}{box_char}{ANSI_RESET}' + + # Color all dashes + dashes_colored = dashes.replace('─', f'{ANSI_DARK_GRAY}─{ANSI_RESET}') + + # Color the label in dark gray (but not the colon) + label_colored = f'{ANSI_DARK_GRAY}{label}{ANSI_RESET}{colon}' + + # Check if this is the state line and color the value + if 'state' in label.lower(): + # Color the state value (first word after colon and spaces) + value_match = re.search(r'^(\s*)(\S+)(.*)$', value) + if value_match: + value_spaces = value_match.group(1) + state_value = value_match.group(2) + rest = value_match.group(3) + color = STATE_COLORS.get(state_value.lower(), ANSI_GREEN) + value = f'{value_spaces}{color}{state_value}{ANSI_RESET}{rest}' + + return leading_space + box_colored + dashes_colored + label_colored + value + else: + # No colon, just color the box-drawing characters + for char in ['│', '├', '└', '─']: + line = line.replace(char, f'{ANSI_DARK_GRAY}{char}{ANSI_RESET}') + return line + + +def color_deployed_block(text: str) -> str: + """Find and colorize deployed successfully blocks with ANSI codes.""" + def repl(m): + fence_start = m.group(1) + lang = (m.group(2) or "").strip() + body = m.group(3) + fence_end = m.group(4) + + # Only operate on blocks that contain the deployed-success marker text. + if 'deployed successfully!' not in body.lower(): + return m.group(0) + + # Check if this block already has proper ANSI coloring applied + if f'{ANSI_DARK_GRAY}│{ANSI_RESET}' in body or f'{ANSI_DARK_GRAY}├{ANSI_RESET}' in body: + return m.group(0) + + # Ensure the fence is marked as 'ansi' for proper rendering, + # preserving other attributes like title="..." + if 'ansi' not in lang.lower(): + rest_attrs = re.sub(r'^\S*', '', lang).strip() + fence_start = (f'```ansi {rest_attrs}\n' if rest_attrs else '```ansi\n') + + # Process line by line to add colors + lines = body.split('\n') + colored_lines = [] + + for line in lines: + # Skip empty lines + if not line.strip(): + colored_lines.append(line) + continue + + # Color the bracket and bullet line: [●] Deployed successfully! + if '●' in line and 'deployed successfully' in line.lower(): + colored_lines.append(color_bracket_and_bullet(line)) + continue + + # Color lines with box-drawing characters (│, ├, └, ─) + if any(char in line for char in ['│', '├', '└', '─']): + colored_lines.append(color_box_drawing_line(line)) + else: + colored_lines.append(line) + + body = '\n'.join(colored_lines) + return fence_start + body + fence_end + + return FENCE_PATTERN.sub(repl, text) + + +def find_state_column_index(header_line: str) -> int | None: + """Find the index of the STATE column in a table header.""" + header_tokens = re.split(r"\s{2,}", header_line.strip()) + for i, token in enumerate(header_tokens): + if token.strip().upper() == "STATE": + return i + return None + + +def bold_table_header(header_line: str) -> str: + """Make each token in the table header bold.""" + pieces = re.split(r'(\s{2,})', header_line) + cols = pieces[0::2] + seps = pieces[1::2] + + for i, col in enumerate(cols): + token = col.strip() + if token and ANSI_BOLD not in token: + cols[i] = col.replace(token, f"{ANSI_BOLD}{token}{ANSI_RESET}", 1) + + result = "" + for i, col in enumerate(cols): + result += col + if i < len(seps): + result += seps[i] + return result + + +def color_state_value_in_row(line: str, state_col: int) -> str: + """Color the state value in a table row.""" + pieces = re.split(r'(\s{2,})', line) + cols = pieces[0::2] + seps = pieces[1::2] + + if len(cols) > state_col: + raw_val = cols[state_col] + val = raw_val.strip() + if val and ANSI_GREEN not in val and ANSI_LIGHT_BLUE not in val: + color = STATE_COLORS.get(val.lower(), ANSI_GREEN) + colored = f"{color}{val}{ANSI_RESET}" + cols[state_col] = raw_val.replace(val, colored, 1) + + # Reconstruct using original separators to preserve spacing + result = "" + for i, col in enumerate(cols): + result += col + if i < len(seps): + result += seps[i] + return result + + +def color_state_in_table_blocks(text: str) -> str: + """Color state values in table-style fenced blocks (STATE column) based on STATE_COLORS.""" + def repl(m): + fence_start = m.group(1) + body = m.group(3) + fence_end = m.group(4) + + lines = body.splitlines() + + # Find header line that contains NAME and STATE (case-insensitive) + header_idx = None + for idx, line in enumerate(lines): + if re.search(r"\bNAME\b", line, flags=re.I) and re.search(r"\bSTATE\b", line, flags=re.I): + header_idx = idx + break + + if header_idx is None: + return m.group(0) + + # Find the STATE column index + state_col = find_state_column_index(lines[header_idx]) + if state_col is None: + return m.group(0) + + # Process header: make each header token bold + new_lines = lines[:header_idx] + new_lines.append(bold_table_header(lines[header_idx]).rstrip('\n')) + + # Process data rows + j = header_idx + 1 + while j < len(lines) and lines[j].strip() != "": + new_lines.append(color_state_value_in_row(lines[j], state_col).rstrip('\n')) + j += 1 + + # Append any remaining lines + new_lines.extend(lines[j:]) + new_body = "\n".join(new_lines) + return fence_start + new_body + fence_end + + return FENCE_PATTERN.sub(repl, text) + + +def color_yaml_deploy_block(text: str) -> str: + """Color state values in YAML-style unikraft deploy output blocks.""" + def repl(m): + body = m.group(3) + + # Only target blocks that look like unikraft YAML deploy output: + # must have 'state:' and 'name:' but NOT the kraft box-drawing style. + if 'deployed successfully!' in body.lower(): + return m.group(0) + if not re.search(r'^state:\s+\S', body, re.M): + return m.group(0) + if not re.search(r'^name:\s+\S', body, re.M): + return m.group(0) + # Skip if already colorized + if ANSI_GREEN in body or ANSI_LIGHT_BLUE in body: + return m.group(0) + + lines = body.split('\n') + colored_lines = [] + for line in lines: + state_m = re.match(r'^(state:)(\s+)(\S+)(.*)$', line, re.I) + if state_m: + key = state_m.group(1) + spaces = state_m.group(2) + val = state_m.group(3) + rest = state_m.group(4) + color = STATE_COLORS.get(val.lower(), ANSI_GREEN) + colored_lines.append(f'{key}{spaces}{color}{val}{ANSI_RESET}{rest}') + else: + colored_lines.append(line) + new_body = '\n'.join(colored_lines) + return m.group(1) + new_body + m.group(4) + + return FENCE_PATTERN.sub(repl, text) + + +def wrap_vale_off(text: str) -> str: + """Wrap the content body in {/* vale off */} / {/* vale on */} comments.""" + # Determine where the preamble (front matter + tabs import) ends + insert_at = 0 + m_fm = FRONT_MATTER_PATTERN.match(text) + if m_fm: + insert_at = m_fm.end() + # Skip past the Tabs import line if present + if text[insert_at:].startswith("import {"): + end_of_import = text.find("\n\n", insert_at) + if end_of_import != -1: + insert_at = end_of_import + 2 + + before = text[:insert_at] + body = text[insert_at:].rstrip("\n") + return before + "{/* vale off */}\n" + body + "\n{/* vale on */}\n" + + +def main(argv: list[str] | None = None) -> int: + """Main entry point for the README to MDX transformation script.""" + p = argparse.ArgumentParser( + description="Convert examples README.md into an MDX guide with ANSI coloring" + ) + p.add_argument("input_file", help="Path to input README.md file") + p.add_argument("output_file", help="Path to output MDX file") + p.add_argument("example_name", nargs="?", default="", help="Optional example name for title") + args = p.parse_args(argv) + + inp = Path(args.input_file) + out = Path(args.output_file) + example_name = args.example_name or None + + txt = inp.read_text(encoding="utf-8") + + # Determine title + title = os.environ.get('TITLE') or extract_title(txt, example_name) + + # Apply transformations in sequence + content = ensure_front_matter(txt, title) + content = re.sub(r"(?m)^# .+\n", "", content, count=1) # Remove first H1 + content = insert_tabs_import(content, title) + content = convert_admonitions(content) + content = rewrite_urls(content) + content = remove_cli_section_labels(content) + content = convert_code_tabs(content) + content = color_deployed_block(content) + content = color_yaml_deploy_block(content) + content = color_state_in_table_blocks(content) + content = wrap_vale_off(content) + + out.write_text(content, encoding="utf-8") + return 0 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/scripts/update_zudoku_guides.py b/scripts/update_zudoku_guides.py new file mode 100755 index 00000000..d4fdad02 --- /dev/null +++ b/scripts/update_zudoku_guides.py @@ -0,0 +1,106 @@ +#!/usr/bin/env python3 +""" +Rewrite the guides list in zudoku.config.tsx based on the MDX files in pages/guides/. + +For each MDX file (except overview.mdx) the title is read from the YAML front-matter. +Entries are sorted alphabetically by title. The overview entry is always first. + +Usage: update_zudoku_guides.py GUIDES_DIR ZUDOKU_CONFIG +""" +from __future__ import annotations + +import re +import sys +from pathlib import Path + + +FRONT_MATTER_TITLE = re.compile(r'^title:\s*["\']?(.+?)["\']?\s*$', re.MULTILINE) + +# Matches the entire items array inside the "Guides" navigation category. +# Captures the indentation of the first item so we can reproduce it. +GUIDES_ITEMS_PATTERN = re.compile( + r'(label:\s*"Guides"[^[]*items:\s*\[)' # up to and including "items: [" + r'(.*?)' # the current list content (group 2) + r'(\s*\])', # closing "]" with optional whitespace + re.DOTALL, +) + + +def extract_title(mdx_path: Path) -> str: + text = mdx_path.read_text(encoding="utf-8") + m = FRONT_MATTER_TITLE.search(text) + if m: + return m.group(1).strip() + # Fall back to the stem if no front-matter title is found + return mdx_path.stem + + +def build_items_block(guides_dir: Path, indent: str) -> str: + entries: list[tuple[str, str]] = [] # (title, slug) + + for mdx in guides_dir.glob("*.mdx"): + if mdx.stem == "overview": + continue + title = extract_title(mdx) + entries.append((title, mdx.stem)) + + entries.sort(key=lambda t: t[0].casefold()) + + lines: list[str] = [] + lines.append(f'{indent}//TODO: Please keep this list sorted by titles, not filenames !!') + lines.append(f'{indent}"/guides/overview", // Guides Overview') + for title, slug in entries: + lines.append(f'{indent}"/guides/{slug}", // {title}') + + return "\n".join(lines) + "\n" + + +def update_config(guides_dir: Path, config_path: Path) -> None: + content = config_path.read_text(encoding="utf-8") + + match = GUIDES_ITEMS_PATTERN.search(content) + if not match: + print("❌ Could not locate the Guides items list in zudoku.config.tsx", file=sys.stderr) + sys.exit(1) + + # Detect indentation from the first non-empty line inside the current block + current_block = match.group(2) + indent_match = re.search(r'\n(\s+)"/', current_block) + indent = indent_match.group(1) if indent_match else " " + + new_block = build_items_block(guides_dir, indent) + + new_content = ( + content[: match.start(2)] + + "\n" + + new_block + + " ]" # closing "]" with fixed indentation, replacing the captured \s*\] + + content[match.end(3):] + ) + + config_path.write_text(new_content, encoding="utf-8") + print(f" ✅ Updated guides list in {config_path.name} " + f"({len(new_block.splitlines()) - 2} guide entries)") + + +def main() -> None: + if len(sys.argv) != 3: + print(f"Usage: {sys.argv[0]} GUIDES_DIR ZUDOKU_CONFIG", file=sys.stderr) + sys.exit(1) + + guides_dir = Path(sys.argv[1]) + config_path = Path(sys.argv[2]) + + if not guides_dir.is_dir(): + print(f"❌ Guides directory not found: {guides_dir}", file=sys.stderr) + sys.exit(1) + + if not config_path.is_file(): + print(f"❌ Config file not found: {config_path}", file=sys.stderr) + sys.exit(1) + + update_config(guides_dir, config_path) + + +if __name__ == "__main__": + main() diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..07158f5c --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,16 @@ +{ + "compilerOptions": { + "target": "ES2022", + "lib": ["ESNext", "DOM", "DOM.Iterable", "WebWorker"], + "module": "ESNext", + "moduleResolution": "Bundler", + "useDefineForClassFields": true, + "skipLibCheck": true, + "skipDefaultLibCheck": true, + "resolveJsonModule": true, + "isolatedModules": true, + "useUnknownInCatchVariables": false, + "jsx": "react-jsx" + }, + "include": ["src", "zudoku.config.tsx"] +} diff --git a/vite.config.ts b/vite.config.ts new file mode 100644 index 00000000..b0235333 --- /dev/null +++ b/vite.config.ts @@ -0,0 +1,6 @@ +/** @type {import('vite').UserConfig} */ +export default { + // This is used at build-time to adjust all the paths. This is necessary as + // the site is built statically and placed inside of a /docs subdirectory. + base: '/docs', +}; diff --git a/zudoku.config.tsx b/zudoku.config.tsx new file mode 100644 index 00000000..465dd2a6 --- /dev/null +++ b/zudoku.config.tsx @@ -0,0 +1,599 @@ +import type { ZudokuConfig } from "zudoku"; + +const config: ZudokuConfig = { + metadata: { + title: "%s | Unikraft Cloud Docs", + favicon: "/favicon.ico", + }, + basePath: "/docs", + site: { + logo: { + src: { light: "/logo-light.svg", dark: "/logo-dark.svg" }, + alt: "Zudoku", + width: "130px", + }, + showPoweredBy: false, + }, + docs: { + files: "/pages/**/*.{md,mdx}", + defaultOptions: { + showLastModified: true, + suggestEdit: { + text: "Edit this page", + url: "https://github.com/unikraft-cloud/docs/edit/prod-stable/{filePath}", + }, + }, + publishMarkdown: true, + llms: { + llmsTxt: true, // Generate llms.txt + llmsTxtFull: true, // Generate llms-full.txt + includeProtected: false, // Exclude protected routes + }, + }, + syntaxHighlighting: { + languages: [ + "c", + "go", + "html", + "javascript", + "json", + "python", + "rust", + "shellscript", + "typescript", + "yaml", + ], + themes: { + light: "github-light", + dark: "github-dark-high-contrast", + }, + }, + navigation: [ + { + type: "category", + label: "Documentation", + icon: "book-open-text", + items: [ + { + type: "category", + label: "Getting Started", + icon: "star", + collapsed: false, + items: [ + "/introduction", + "/faq", + ], + }, + { + type: "category", + label: "Features", + icon: "rocket", + collapsed: false, + items: [ + "/features/scale-to-zero", + "/features/load-balancing", + "/features/snapshots", + "/features/autoscale", + "/features/roms", + "/features/autokill", + "/features/cron-jobs", + "/features/forking", + ], + }, + { + type: "category", + label: "Use Cases", + icon: "lightbulb", + collapsed: false, + items: [ + "/use-cases/sandboxes", + "/use-cases/headless-browsers", + "/use-cases/mcp-servers", + "/use-cases/api-gateways", + "/use-cases/serverless-functions", + "/use-cases/serverless-databases", + "/use-cases/build-test-environments", + "/use-cases/webhooks", + "/use-cases/remote-ides", + "/use-cases/game-servers", + "/use-cases/remote-desktops", + ], + }, + { + type: "category", + label: "Cloud Platform", + icon: "cloud", + collapsed: false, + items: [ + "/platform/metros", + "/platform/instances", + "/platform/services", + "/platform/domains", + "/platform/certificates", + "/platform/volumes", + "/platform/images", + "/platform/quotas", + "/platform/metrics", + "/platform/tagging", + "/platform/delete-locks", + "/platform/troubleshooting", + ], + }, + { + type: "category", + label: "Integrations", + icon: "blocks", + collapsed: false, + items: [ + "/integrations/kubernetes", + "/integrations/sdks/go", + ], + }, + ], + }, + { + type: "category", + label: "Guides", + icon: "graduation-cap", + items: [ + //TODO: Please keep this list sorted by titles, not filenames !! + "/guides/overview", // Guides Overview + "/guides/httpserver-dotnet10.0", // .NET HTTP Server + "/guides/node18-agario", // Agar.io (Node) + "/guides/mcp-server-arxiv", // ArXiv MCP Server + "/guides/httpserver-bun", // Bun HTTP Server + "/guides/httpserver-gcc13.2", // C HTTP Server + "/guides/httpserver-boost1.74-gpp13.2", // C++ Boost HTTP Server + "/guides/httpserver-gpp13.2", // C++ HTTP Server + "/guides/caddy2.7-go1.21", // Caddy + "/guides/debian-ssh", // Debian SSH server + "/guides/httpserver-python3.12-django5.0", // Django HTTP Server + "/guides/dragonflydb", // DragonflyDB + "/guides/duckdb-go1.21", // DuckDB with Go + "/guides/httpserver-elixir1.16", // Elixir HTTP Server + "/guides/httpserver-erlang26.2", // Erlang HTTP Server + "/guides/httpserver-expressjs4.18-node21", // Express HTTP Server + "/guides/httpserver-python3.12-fastapi-0.121.3", // FastAPI HTTP Server + "/guides/httpserver-flask-redis", // Flask + Redis HTTP Server + "/guides/httpserver-python3.12-flask3.0-sqlite", // Flask and SQLite HTTP Server + "/guides/httpserver-python3.12-flask3.0", // Flask HTTP Server + "/guides/github-webhook-node", // GitHub Webhook receiver + "/guides/httpserver-go1.21", // Go HTTP Server + "/guides/grafana", // Grafana + "/guides/haproxy", // HAProxy + "/guides/hugo0.122", // Hugo + "/guides/imaginary", // Imaginary + "/guides/httpserver-java21", // Java HTTP Server + "/guides/httpserver-lua5.1", // Lua HTTP Server + "/guides/mariadb", // MariaDB + "/guides/memcached1.6", // Memcached + "/guides/minio", // Minio + "/guides/mongodb", // MongoDB + "/guides/httpserver-node21-nextjs", // Next.js HTTP Server + "/guides/nginx", // Nginx + "/guides/node24-karaoke", // Node AllKaraoke + "/guides/httpserver-node25", // Node HTTP Server + "/guides/node21-websocket", // Node WebSocket Server + "/guides/novnc-browser", // noVNC + "/guides/opentelemetry-collector", // OpenTelemetry Collector + "/guides/httpserver-perl5.42", // Perl HTTP Server + "/guides/httpserver-php8.2", // PHP HTTP Server + "/guides/node-playwright-chromium", // Playwright (Chromium) with Node.js + "/guides/python-playwright-chromium", // Playwright (Chromium) with Python FastAPI + "/guides/node-playwright-firefox", // Playwright (Firefox) with Node.js + "/guides/node-playwright-webkit", // Playwright (WebKit) with Node.js + "/guides/postgres", // PostgreSQL + "/guides/httpserver-prisma-expressjs4.19-node18", // Prisma HTTP Server + "/guides/httpserver-node-express-puppeteer", // Puppeteer HTTP Server + "/guides/httpserver-python3.12", // Python HTTP Server + "/guides/httpserver-node22-react-router", // React Router HTTP Server + "/guides/redis7.2", // Redis + "/guides/httpserver-ruby3.2", // Ruby HTTP Server + "/guides/ruby3.2-rails", // Ruby on Rails + "/guides/httpserver-rust1.88-actix-web4", // Rust (Actix Web) HTTP Server + "/guides/httpserver-rust-trunkrs-leptos", // Rust (Leptos + Trunk) HTTP Server + "/guides/httpserver-rust1.88-rocket0.5", // Rust (Rocket) HTTP Server + "/guides/httpserver-rust1.75-tokio", // Rust (Tokio) HTTP Server + "/guides/httpserver-rust1.91", // Rust HTTP Server + "/guides/mcp-server-simple", // Simple MCP Server + "/guides/skipper0.18", // Skipper + "/guides/httpserver-node21-solid-start", // SolidJS HTTP Server + "/guides/spin-wagi-http", // Spin + "/guides/httpserver-java17-springboot3.5.x", // Spring Boot HTTP Server + "/guides/httpserver-java17-spring-petclinic", // Spring PetClinic + "/guides/httpserver-c-debug", // SSH and HTTP Server with C and Debugging Tools + "/guides/httpserver-node22-sveltekit", // SvelteKit HTTP Server + "/guides/traefik", // Traefik + "/guides/visual-studio-code-server", // Visual Studio Code Server + "/guides/httpserver-node-vite-vanilla", // Vite (vanilla) + "/guides/httpserver-node-vite-ssr-vanilla", // Vite (vanilla) SSR + "/guides/httpserver-nginx-vite-vanilla", // Vite HTTP Server + "/guides/vsftpd", // vsftpd + "/guides/wazero-import-go", // Wazero + "/guides/node18-wingsio", // Wings.io (Node) + "/guides/wordpress-all-in-one", // Wordpress + ] + }, + { + type: "category", + label: "Tutorials", + icon: "book", + items: [ + "/tutorials/docker-to-ukc", + "/tutorials/environment-variables", + "/tutorials/rootfs-formats", + "/tutorials/rootfs-compression", + "/tutorials/rootfs-volumes-roms", + "/tutorials/scale-to-zero-triggers", + "/tutorials/instance-metrics", + "/tutorials/network-communication" + ] + }, + { + type: "category", + label: "CLI Reference", + icon: "terminal", + items: [ + "/cli/overview", + { + type: "category", + label: "Concepts", + icon: "lightbulb", + collapsed: false, + items: [ + "/cli/registries", + "/cli/filters", + ], + }, + { + type: "category", + label: "unikraft", + icon: "terminal", + collapsed: false, + items: [ + "/cli/unikraft", + "/cli/unikraft/login", + "/cli/unikraft/logout", + "/cli/unikraft/completion", + "/cli/unikraft/run", + "/cli/unikraft/build", + "/cli/unikraft/tui", + "/cli/unikraft/upgrade", + "/cli/unikraft/version", + { + type: "category", + label: "unikraft config", + icon: "settings", + collapsed: false, + items: [ + "/cli/unikraft/config", + "/cli/unikraft/config/get", + ], + }, + { + type: "category", + label: "unikraft profile", + icon: "user-circle", + collapsed: false, + items: [ + "/cli/unikraft/profile", + "/cli/unikraft/profile/get", + "/cli/unikraft/profile/list", + "/cli/unikraft/profile/use", + ], + }, + { + type: "category", + label: "unikraft metros", + icon: "earth", + collapsed: false, + items: [ + "/cli/unikraft/metros", + "/cli/unikraft/metros/get", + "/cli/unikraft/metros/list", + ], + }, + { + type: "category", + label: "unikraft instances", + icon: "rocket", + collapsed: false, + items: [ + "/cli/unikraft/instances", + "/cli/unikraft/instances/create", + "/cli/unikraft/instances/delete", + "/cli/unikraft/instances/edit", + "/cli/unikraft/instances/get", + "/cli/unikraft/instances/list", + "/cli/unikraft/instances/logs", + "/cli/unikraft/instances/restart", + "/cli/unikraft/instances/start", + "/cli/unikraft/instances/stop", + "/cli/unikraft/instances/wait", + ], + }, + { + type: "category", + label: "unikraft volumes", + icon: "cylinder", + collapsed: false, + items: [ + "/cli/unikraft/volumes", + "/cli/unikraft/volumes/clone", + "/cli/unikraft/volumes/create", + "/cli/unikraft/volumes/delete", + "/cli/unikraft/volumes/edit", + "/cli/unikraft/volumes/get", + "/cli/unikraft/volumes/list", + "/cli/unikraft/volumes/wait", + ], + }, + { + type: "category", + label: "unikraft services", + icon: "split", + collapsed: false, + items: [ + "/cli/unikraft/services", + "/cli/unikraft/services/create", + "/cli/unikraft/services/delete", + "/cli/unikraft/services/edit", + "/cli/unikraft/services/get", + "/cli/unikraft/services/list", + "/cli/unikraft/services/wait", + ], + }, + { + type: "category", + label: "unikraft certificates", + icon: "shield-check", + collapsed: false, + items: [ + "/cli/unikraft/certificates", + "/cli/unikraft/certificates/create", + "/cli/unikraft/certificates/delete", + "/cli/unikraft/certificates/get", + "/cli/unikraft/certificates/list", + "/cli/unikraft/certificates/wait", + ], + }, + { + type: "category", + label: "unikraft images", + icon: "package", + collapsed: false, + items: [ + "/cli/unikraft/images", + "/cli/unikraft/images/copy", + "/cli/unikraft/images/get", + "/cli/unikraft/images/list", + ], + }, + ] + }, + { + type: "category", + label: "kraft cloud", + icon: "terminal", + collapsed: false, + items: [ + "/cli/kraft/overview", + "/cli/kraft/deploy", + "/cli/kraft/quota", + "/cli/kraft/tunnel", + { + type: "category", + label: "kraft cloud cert", + icon: "shield-check", + collapsed: false, + items: [ + "/cli/kraft/cert", + "/cli/kraft/cert/create", + "/cli/kraft/cert/get", + "/cli/kraft/cert/list", + "/cli/kraft/cert/remove", + ], + }, + { + type: "category", + label: "kraft cloud compose", + icon: "book-open", + collapsed: false, + items: [ + "/cli/kraft/compose", + "/cli/kraft/compose/build", + "/cli/kraft/compose/create", + "/cli/kraft/compose/down", + "/cli/kraft/compose/log", + "/cli/kraft/compose/ls", + "/cli/kraft/compose/ps", + "/cli/kraft/compose/push", + "/cli/kraft/compose/start", + "/cli/kraft/compose/stop", + "/cli/kraft/compose/up", + ], + }, + { + type: "category", + label: "kraft cloud image", + icon: "package", + collapsed: false, + items: [ + "/cli/kraft/image", + "/cli/kraft/image/list", + "/cli/kraft/image/remove", + ], + }, + { + type: "category", + label: "kraft cloud instance", + icon: "rocket", + collapsed: false, + items: [ + "/cli/kraft/instance", + "/cli/kraft/instance/create", + "/cli/kraft/instance/get", + "/cli/kraft/instance/list", + "/cli/kraft/instance/logs", + "/cli/kraft/instance/remove", + "/cli/kraft/instance/start", + "/cli/kraft/instance/stop", + ], + }, + { + type: "category", + label: "kraft cloud metro", + icon: "earth", + collapsed: false, + items: [ + "/cli/kraft/metro", + "/cli/kraft/metro/list", + ], + }, + { + type: "category", + label: "kraft cloud scale", + icon: "arrow-up-1-0", + collapsed: false, + items: [ + "/cli/kraft/scale", + "/cli/kraft/scale/add", + "/cli/kraft/scale/get", + "/cli/kraft/scale/init", + "/cli/kraft/scale/remove", + "/cli/kraft/scale/reset", + ], + }, + { + type: "category", + label: "kraft cloud service", + icon: "split", + collapsed: false, + items: [ + "/cli/kraft/service", + "/cli/kraft/service/create", + "/cli/kraft/service/get", + "/cli/kraft/service/list", + "/cli/kraft/service/remove", + ], + }, + { + type: "category", + label: "kraft cloud volume", + icon: "cylinder", + collapsed: false, + items: [ + "/cli/kraft/volume", + "/cli/kraft/volume/attach", + "/cli/kraft/volume/create", + "/cli/kraft/volume/detach", + "/cli/kraft/volume/get", + "/cli/kraft/volume/import", + "/cli/kraft/volume/list", + "/cli/kraft/volume/remove", + ], + }, + ] + }, + ], + } as any, + { + type: "category", + label: "Kraftfile", + icon: "file-text", + items: [ + "/kraftfile/v0.7", + { + type: "link", + label: "Kraftfile Reference (v0.6)", + to: "https://unikraft.org/docs/cli/reference/kraftfile/v0.6", + }, + { + type: "link", + label: "Kraftfile Reference (v0.5)", + to: "https://unikraft.org/docs/cli/reference/kraftfile/v0.5", + }, + ], + } as any, + { + type: "link", + label: "Platform API", + icon: "unplug", + to: "/api/platform/v1", + }, + ], + search: { + type: "pagefind", + }, + redirects: [ + { from: "/", to: "/introduction" }, + { from: "/cli", to: "/cli/overview" }, + { from: "/kraftfile", to: "/kraftfile/v0.7" }, + { from: "/guides", to: "/guides/overview" }, + ], + apis: [ + { + type: "file", + input: "./apis/platform.yaml", + path: "/api/platform/v1", + }, + ], + theme: { + fonts: { + sans: { + fontFamily: "Inter, sans-serif", + url: "https://fonts.googleapis.com/css2?family=Inter:wght@400;700&display=swap", + }, + mono: { + fontFamily: "IBM Plex Mono, monospace", + url: "https://fonts.googleapis.com/css2?family=IBM+Plex+Mono:wght@400;700&display=swap", + }, + }, + light: { + background: "#ffffff", // Main background color + foreground: "#020817", // Main text color + card: "#ffffff", // Card background color + cardForeground: "#020817", // Card text color + popover: "#ffffff", // Popover background color + popoverForeground: "#020817", // Popover text color + primary: "#2563eb", // Primary action color + primaryForeground: "#ffffff", // Text color on primary backgrounds + secondary: "#f1f5f9", // Secondary action color + secondaryForeground: "#020817", // Text color on secondary backgrounds + muted: "#f1f5f9", // Muted/subtle background color + mutedForeground: "#64748b", // Text color for muted elements + accent: "#f1f5f9", // Accent color for highlights + accentForeground: "#020817", // Text color on accent backgrounds + destructive: "#ef4444", // Color for destructive actions + destructiveForeground: "#ffffff", // Text color on destructive backgrounds + border: "#cbd5e1", // Border color + input: "#e2e8f0", // Input field border color + ring: "#0284c7", // Focus ring color + radius: "0.4rem", // Border radius value + }, + dark: { + background: "#000000", // Main background color + foreground: "#dbeafe", // Main text color + card: "#000000", // Card background color + cardForeground: "#dbeafe", // Card text color + popover: "hsl(20 14.3% 4.1%)", // Popover background color + popoverForeground: "hsl(60 9.1% 97.8%)", // Popover text color + primary: "#2563eb", // Primary action color + primaryForeground: "#ffffff", // Text color on primary backgrounds + secondary: "var(--color-slate-800)", // Secondary action color + secondaryForeground: "hsl(60 9.1% 97.8%)", // Text color on secondary backgrounds + muted: "var(--color-slate-900)", // Muted/subtle background color + mutedForeground: "var(--color-slate-600)", // Text color for muted elements + accent: "var(--color-slate-800)", // Accent color for highlights + accentForeground: "hsl(60 9.1% 97.8%)", // Text color on accent backgrounds + destructive: "var(--color-rose-800)", // Color for destructive actions + destructiveForeground: "hsl(60 9.1% 97.8%)", // Text color on destructive backgrounds + border: "#1e293b", // Border color + input: "#334155", // Input field border color + ring: "var(--color-amber-500)", // Focus ring color + radius: "0.4rem", // Border radius value + }, + }, +}; + +export default config;