diff --git a/examples/POSTGRES/code/calc-node/handler.js b/examples/POSTGRES/code/calc-node/handler.js new file mode 100644 index 0000000..7ee3462 --- /dev/null +++ b/examples/POSTGRES/code/calc-node/handler.js @@ -0,0 +1,116 @@ +// Создано: 2026-04-10 +// Простой калькулятор — возвращает HTML страницу с кнопками. +// GET → HTML; POST с {a, op, b} → вычисление с результатом на странице. +// Рантайм nodejs20 v0.1.3+ поддерживает HTML-ответ (строка начинающаяся с '<'). + +'use strict'; + +const HTML = ` + + + + +Калькулятор (Node.js) + + + +
+

Калькулятор

+
Node.js 20
+
0
+
RESULT_LINE
+
+ + + + + + + + + + + + + + + + + +
+
+ + +`; + +// Форматирует число — убирает лишний .0 для целых +function fmt(n) { return n === Math.trunc(n) ? String(Math.trunc(n)) : String(n); } + +function buildPage(resultLine) { + return HTML.replace('RESULT_LINE', resultLine || ''); +} + +function compute(event) { + // POST: тело приходит как JSON; поля a, op, b — строки из form + const a = parseFloat(event.a); + const b = parseFloat(event.b); + const op = event.op; + if (isNaN(a) || isNaN(b)) return buildPage('Некорректное число'); + let res; + switch (op) { + case '+': res = a + b; break; + case '-': res = a - b; break; + case '*': res = a * b; break; + case '/': + if (b === 0) return buildPage('Деление на ноль'); + res = a / b; break; + default: return buildPage('Неизвестный оператор'); + } + return buildPage(`${fmt(a)} ${op} ${fmt(b)} = ${fmt(res)}`); +} + +module.exports.handler = async function handler(event) { + if (event._method === 'POST') return compute(event); + return buildPage(''); +}; diff --git a/examples/POSTGRES/code/calc-node/package.json b/examples/POSTGRES/code/calc-node/package.json new file mode 100644 index 0000000..18a1e41 --- /dev/null +++ b/examples/POSTGRES/code/calc-node/package.json @@ -0,0 +1,3 @@ +{ + "dependencies": {} +} diff --git a/examples/POSTGRES/code/calc-python/handler.py b/examples/POSTGRES/code/calc-python/handler.py new file mode 100644 index 0000000..57179b3 --- /dev/null +++ b/examples/POSTGRES/code/calc-python/handler.py @@ -0,0 +1,120 @@ +# Создано: 2026-04-10 +# Простой калькулятор — возвращает HTML страницу с кнопками. +# Роутинг: GET → HTML страница; POST с {a, op, b} → вычисление и редирект с результатом. +# _method приходит от python3.11 рантайма в event. + +_HTML = """ + + + + +Калькулятор (Python) + + + +
+

Калькулятор

+
Python 3.11
+
0
+
{result_line}
+
+ + + + + + + + + + + + + + + + + +
+
+ + +""" + +def handler(event): + # POST: тело приходит как JSON от рантайма + if event.get('_method') == 'POST': + return _compute(event) + # GET: показываем чистую страницу + return _html() + + +def _html(result_line='', fresh_js='true'): + return _HTML.replace('{result_line}', result_line).replace('{fresh_js}', fresh_js) + + +def _compute(event): + try: + a = float(event.get('a', 0)) + b = float(event.get('b', 0)) + op = event.get('op', '') + if op == '+': + res = a + b + elif op == '-': + res = a - b + elif op == '*': + res = a * b + elif op == '/': + if b == 0: + return _html('Деление на ноль', 'false') + res = a / b + else: + return _html('Неизвестный оператор', 'false') + # Убираем лишний .0 для целых результатов + res_str = str(int(res)) if res == int(res) else str(res) + return _html(f'{int(a) if a == int(a) else a} {op} {int(b) if b == int(b) else b} = {res_str}', 'false') + except (ValueError, TypeError) as exc: + return _html(f'Ошибка: {exc}', 'false') diff --git a/examples/POSTGRES/code/calc-python/requirements.txt b/examples/POSTGRES/code/calc-python/requirements.txt new file mode 100644 index 0000000..d45663c --- /dev/null +++ b/examples/POSTGRES/code/calc-python/requirements.txt @@ -0,0 +1 @@ +# нет внешних зависимостей diff --git a/examples/POSTGRES/functions.tf b/examples/POSTGRES/functions.tf new file mode 100644 index 0000000..b61f5b3 --- /dev/null +++ b/examples/POSTGRES/functions.tf @@ -0,0 +1,32 @@ +# Создано: 2026-04-10 +# functions.tf — sless_service ресурсы для примера POSTGRES. +# Здесь: два калькуляторa — Python и Node.js. +# sless_service = long-running Deployment + постоянный URL (в отличие от sless_function). + +# ─── Python-калькулятор ────────────────────────────────────────────────────── + +resource "sless_service" "calc_python" { + name = "calc-python" + runtime = "python3.11" + entrypoint = "handler.handler" + source_dir = "${path.module}/code/calc-python" +} + +output "calc_python_url" { + description = "URL Python-калькулятора" + value = sless_service.calc_python.url +} + +# ─── Node.js-калькулятор ───────────────────────────────────────────────────── + +resource "sless_service" "calc_node" { + name = "calc-node" + runtime = "nodejs20" + entrypoint = "handler.handler" + source_dir = "${path.module}/code/calc-node" +} + +output "calc_node_url" { + description = "URL Node.js-калькулятора" + value = sless_service.calc_node.url +} diff --git a/examples/VM/README.md b/examples/VM/README.md new file mode 100644 index 0000000..d5fe144 --- /dev/null +++ b/examples/VM/README.md @@ -0,0 +1,93 @@ +# Пример: Виртуальная машина (vApp + VM) в Nubes vDC + +Создаёт: +- **vApp** — виртуальный каталог (контейнер для ВМ в VMware vDC) +- **ВМ** — Ubuntu 22.04, 2 CPU / 2 GB RAM / 20 GB disk + +--- + +## Что нужно сделать перед запуском + +### 1. Сгенерировать SSH-ключ + +Публичный ключ прописывается в ВМ при создании — это единственный способ зайти по SSH. +Приватный ключ нужен хранить у себя. + +```bash +ssh-keygen -t ed25519 -f ~/.ssh/sless-demo-vm -N "" -C "sless-demo-vm" +``` + +Публичный ключ (`~/.ssh/sless-demo-vm.pub`) — строка вида: +``` +ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA... sless-demo-vm +``` + +### 2. Заполнить terraform.tfvars + +Открыть файл `terraform.tfvars` и заменить значения: + +```hcl +# Ваш API-токен из панели Nubes +api_token = "ВСТАВИТЬ_ТОКЕН" + +# Публичный ключ из шага 1 +vm_public_key = "ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAA..." +``` + +> **Токен** — берётся в панели Nubes: профиль → API-токены. +> **Ключ** — содержимое файла `~/.ssh/sless-demo-vm.pub` (публичный, не приватный!). + +--- + +## Запуск + +```bash +cd examples/VM + +terraform init +terraform apply +``` + +После `apply` в выводе будет: + +``` +Outputs: + +vm_id = "..." +vm_state = { + "externalIp" = "1.2.3.4" + ... +} +vapp_id = "..." +``` + +--- + +## Подключение по SSH + +```bash +ssh -i ~/.ssh/sless-demo-vm ubuntu@ +``` + +Логин — `ubuntu` (задан в `vm.tf`). + +--- + +## Удаление + +```bash +terraform destroy +``` + +Порядок автоматический: сначала suspend → потом delete. Без suspend удаление упадёт с ошибкой — это поведение Nubes, параметр `suspend_on_destroy = true` в ресурсах решает это. + +--- + +## Что можно менять + +| Параметр | Файл | Примечание | +|----------|------|-----------| +| `vm_cpu`, `vm_ram`, `vm_disk` | `vm.tf` | Можно менять и переприменять | +| `resource_name`, `vapp_name` | `vapp.tf` | **Не изменяется после создания** | +| `image_vm`, `user_login`, `user_public_key` | `vm.tf` | **Не изменяется после создания** | +| `vdc_uid`, `nsxt_uid` | `vapp.tf` | **Не изменяется после создания** | diff --git a/examples/VM/main.tf b/examples/VM/main.tf new file mode 100644 index 0000000..2fcef81 --- /dev/null +++ b/examples/VM/main.tf @@ -0,0 +1,38 @@ +// 2026-03-25 — main.tf для примера с vApp + ВМ (Виртуальный датацентр Nubes). +// Провайдер nubes. Sless-провайдер не нужен — пример чисто инфраструктурный. + +terraform { + required_providers { + nubes = { + source = "terra.k8c.ru/nubes/nubes" + version = "5.0.31" + } + } +} + +# ------------------------------------------------------------------ +# Переменные +# ------------------------------------------------------------------ + +variable "vm_public_key" { + type = string + sensitive = true + description = "Публичный SSH-ключ для ВМ. Приватный ключ: ~/terra/sless/examples/VM/vm_key" +} + +variable "api_token" { + type = string + sensitive = true + description = "Nubes API token" +} + +# ------------------------------------------------------------------ +# Провайдер +# ------------------------------------------------------------------ + +# API Dashboard (для Terraform-провайдеров): https://deck-api-test.ngcloud.ru/api/v1/index.cfm +# UI облака (только браузер): https://deck-test.ngcloud.ru/ +provider "nubes" { + api_token = var.api_token + api_endpoint = "https://deck-api-test.ngcloud.ru/api/v1/index.cfm" +} diff --git a/examples/VM/vapp.tf b/examples/VM/vapp.tf new file mode 100644 index 0000000..59b6c30 --- /dev/null +++ b/examples/VM/vapp.tf @@ -0,0 +1,28 @@ +// 2026-03-25 — vapp.tf: виртуальный каталог ВМ (vApp) в Nubes vDC. +// nubes_vapp — контейнер для ВМ внутри Виртуального датацентра. +// Обязательные поля: vdc_uid, nsxt_uid, vapp_name, resource_name. + +resource "nubes_vapp" "vapp" { + resource_name = "vm-sless-demo-vapp" + vapp_name = "vapp-sless-demo" # Уникальное в рамках организации. Не изменяется после создания. + vdc_uid = "e3c9e4f1-24da-4992-a003-f8a2a803a5f0" # UUID Услуги «Виртуальный датацентр (vDC)». Не изменяется после создания. + nsxt_uid = "0fe88e2a-31b6-4385-ad52-e27c6c0d38a6" # UUID Услуги «Сетевой шлюз периметра (Edge)». Не изменяется после создания. + + adopt_existing_on_create = true + operation_timeout = "15m" + + # ВАЖНО: delete без предварительного suspend завершается ошибкой + # "Невозможно выполнить операцию удаления услуги. Услуга не остановлена" + # suspend_on_destroy гарантирует правильный порядок: suspend → delete. + suspend_on_destroy = true +} + +output "vapp_id" { + value = nubes_vapp.vapp.id + description = "ID созданного vApp (используется как vapp_uid при создании ВМ)" +} + +output "vapp_state" { + value = nubes_vapp.vapp.state_out_flat + description = "Плоский state vApp — адреса, статусы сети и т.д." +} diff --git a/examples/VM/vm.tf b/examples/VM/vm.tf new file mode 100644 index 0000000..0746817 --- /dev/null +++ b/examples/VM/vm.tf @@ -0,0 +1,34 @@ +// 2026-03-25 — vm.tf: виртуальная машина (nubes_vc_vm_v3) внутри vApp. +// Зависит от nubes_vapp.vapp — создаётся после vApp. +// image_vm, vapp_uid, user_public_key не изменяются после создания. + +resource "nubes_vc_vm_v3" "vm" { + resource_name = "vm-sless-demo" + + vapp_uid = nubes_vapp.vapp.id # ссылка на vApp. Не изменяется после создания. + image_vm = "Ubuntu 22.04 LTS" # Не изменяется после создания. + ip_space_name = "internet-ipv4-v1" + + user_login = "ubuntu" + user_public_key = var.vm_public_key # задаётся в terraform.tfvars + + vm_cpu = 2 + vm_ram = 2 # GB + vm_disk = 20 # GB + + adopt_existing_on_create = true + operation_timeout = "15m" + + # delete без предварительного suspend завершается ошибкой (аналогично vApp). + suspend_on_destroy = true +} + +output "vm_id" { + value = nubes_vc_vm_v3.vm.id + description = "ID созданной ВМ" +} + +output "vm_state" { + value = nubes_vc_vm_v3.vm.state_out_flat + description = "Плоский state ВМ — IP-адреса, статус и т.д." +} diff --git a/runtimes/nodejs20/server.js b/runtimes/nodejs20/server.js index 34f2431..db23b88 100644 --- a/runtimes/nodejs20/server.js +++ b/runtimes/nodejs20/server.js @@ -1,4 +1,5 @@ // Изменено: 2026-03-09 +// Изменено: 2026-04-10 (v0.1.3) — sendResult: если handler вернул строку начинающуюся с '<' → text/html. // HTTP-обёртка для serverless функций на Node.js 20. // Загружает модуль из SLESS_ENTRYPOINT или handler.js по умолчанию. // Формат SLESS_ENTRYPOINT: "module-name.functionName" (например: handler-http.handle) @@ -65,7 +66,7 @@ const server = http.createServer(async (req, res) => { try { const result = await userHandle(event); - sendJSON(res, 200, result); + sendResult(res, 200, result); } catch (err) { console.error('Handler error:', err); sendJSON(res, 500, { error: err.message }); @@ -73,6 +74,21 @@ const server = http.createServer(async (req, res) => { }); }); +function sendResult(res, status, data) { + // Если handler вернул строку начинающуюся с '<' — HTML страница. + // Если строка, но не HTML — text/plain. Иначе — JSON. + if (typeof data === 'string') { + const ctype = data.trimStart().startsWith('<') + ? 'text/html; charset=utf-8' + : 'text/plain; charset=utf-8'; + const buf = Buffer.from(data, 'utf-8'); + res.writeHead(status, { 'Content-Type': ctype, 'Content-Length': buf.length }); + res.end(buf); + return; + } + sendJSON(res, status, data); +} + function sendJSON(res, status, data) { const body = JSON.stringify(data); res.writeHead(status, {