Sanitized examples update

This commit is contained in:
“Naeel” 2026-03-11 12:23:53 +04:00
commit 554f4cdace
40 changed files with 1483 additions and 0 deletions

View File

@ -0,0 +1,143 @@
# Bug Report: HTTP route is not removed after Terraform destroy
## Summary
При удалении примера `hello-node` через Terraform команда `terraform destroy` завершается успешно, но публичный HTTP endpoint не удаляется.
Фактическое поведение после `destroy` такое:
1. Сразу после удаления endpoint ещё некоторое время отвечает `HTTP 200` и возвращает корректный ответ функции.
2. Затем backend функции действительно исчезает, но публичный маршрут остаётся опубликованным и начинает отвечать `HTTP 502 function unreachable`.
3. Даже через 120 секунд endpoint не исчезает.
Это выглядит как баг cleanup в platform/backend/provider lifecycle для HTTP trigger/route.
## Affected Example
- Example: `hello-node`
- Terraform files: `hello-node/main.tf`, `hello-node/http.tf`, `hello-node/job.tf`
- Public URL: `https://sless-api.kube5s.ru/fn/default/hello-http`
- Function name: `hello-http`
- Trigger name: `hello-http-trigger`
## Reproduction
Использовался репозиторий examples и скрипт:
- Script: `./run_terraform_examples.sh`
Шаги воспроизведения:
1. Выполнить `terraform init` в `hello-node`
2. Выполнить `terraform apply`
3. Убедиться, что endpoint живой
4. Выполнить `terraform destroy`
5. Проверять публичный URL после destroy
Логика проверки встроена в `run_terraform_examples.sh`:
1. После `apply` endpoint обязан отвечать `200`
2. После `destroy` endpoint должен исчезнуть
3. Скрипт ждёт до 120 секунд и перепроверяет endpoint каждые 5 секунд
## Expected Result
После успешного `terraform destroy`:
1. Публичный URL должен перестать существовать
2. Запрос на URL должен вернуть `404` или другой явный признак отсутствия маршрута
3. Provider не должен возвращать успешный destroy раньше, чем cleanup HTTP route завершён
## Actual Result
После успешного `terraform destroy`:
1. Terraform сообщает `Destroy complete! Resources: 4 destroyed.`
2. Endpoint `https://sless-api.kube5s.ru/fn/default/hello-http` продолжает отвечать `200`
3. Через некоторое время тот же endpoint начинает отвечать `502`
4. Тело ответа на `502`:
```json
{"error":"function unreachable: Post \"http://hello-http.sless-fn-default.svc.cluster.local:8080\": dial tcp 10.106.128.167:8080: connect: operation not permitted"}
```
Это означает:
1. внешний HTTP маршрут всё ещё существует;
2. запрос по нему всё ещё направляется внутрь платформы;
3. backend функции уже удалён или недоступен;
4. cleanup маршрута не завершён.
## Timeline From Real Run
Подтверждённая последовательность из фактического прогона:
1. `terraform destroy` завершился успешно
2. первые проверки после destroy возвращали `HTTP 200`
3. затем проверки начали возвращать `HTTP 502 function unreachable`
4. в течение всех 24 проверок по 5 секунд endpoint не исчез
5. итоговое время ожидания: 120 секунд
Итоговый summary из скрипта:
```text
ERROR SUMMARY
example: hello-node
step: endpoint cleanup after clean destroy
reason: route cleanup bug: public endpoint still exists but backend is already gone (HTTP 502 function unreachable); endpoint was still published after 120s
```
## Why This Is A Real Platform Bug
Это не похоже на проблему тестового скрипта или Terraform CLI по следующим причинам:
1. `terraform destroy` завершается без ошибки
2. state Terraform очищается как ожидалось
3. сначала endpoint отвечает `200`, значит маршрут реально жив после destroy
4. потом endpoint отвечает `502 function unreachable`, значит backend уже исчез, но route ещё остался
5. скрипт ждёт 120 секунд, то есть это не мгновенная eventual consistency на 1-2 секунды
Иными словами: удаление backend и удаление публичного маршрута расходятся по времени, а route cleanup либо не выполняется, либо не дожидается завершения.
## Most Likely Broken Layer
Наиболее вероятные точки проблемы:
1. API/backend destroy trigger возвращает success до фактического удаления HTTP route
2. Controller удаляет function workload, но не удаляет route/ingress/virtualservice/gateway mapping
3. Удаление route запускается асинхронно, но его результат не awaited
4. В системе остаётся запись маршрута на имя функции, хотя service/backend уже удалён
## What To Check In The Development Repo
Нужно проверить destroy flow именно для HTTP trigger:
1. Удаляется ли объект trigger только в metadata/storage или реально удаляется и внешний маршрут
2. Какие Kubernetes/ingress объекты создаются для HTTP trigger и все ли они удаляются
3. Есть ли race condition между удалением function/service и удалением route
4. Не возвращает ли provider success раньше, чем backend подтверждает полное удаление маршрута
5. Есть ли финальный polling/wait на исчезновение route перед возвратом успешного destroy
Если архитектура использует отдельные сущности route/service/function, то destroy должен идти в таком порядке:
1. disable/remove public routing
2. дождаться, что endpoint больше не публикуется снаружи
3. удалить backend/service/workload
4. завершить destroy success
Сейчас по фактическому поведению порядок либо обратный, либо неполный.
## Minimal Acceptance Criteria For Fix
Исправление можно считать рабочим, если после `terraform destroy` для `hello-node` выполняются все условия:
1. URL `https://sless-api.kube5s.ru/fn/default/hello-http` перестаёт отвечать как живой маршрут
2. URL не возвращает `502 function unreachable`
3. URL исчезает в разумное время после destroy
4. `./run_terraform_examples.sh` проходит шаг `endpoint cleanup after clean destroy`
## Current Status
На данный момент массовый прогон examples корректно останавливается на `hello-node`, потому что это первый воспроизводимый failure.
Дальше прогонять остальные примеры без исправления destroy cleanup смысла нет: тест уже доказал platform bug на базовом HTTP сценарии.

160
README.md Normal file
View File

@ -0,0 +1,160 @@
# Примеры sless
## Что такое sless
**sless** — платформа для запуска serverless-функций в Kubernetes-кластере.
Код на Python или Node.js загружается в платформу, которая собирает Docker-образ, деплоит его в кластер и публикует HTTP-эндпоинт. Всё управляется через Terraform.
### Ресурсы
| Ресурс | Что делает |
|---|---|
| `sless_function` | Загружает код и собирает Docker-образ. Сама по себе не принимает запросы — нужен триггер или джоб |
| `sless_trigger` | Публикует функцию — либо как HTTP-эндпоинт, либо по расписанию (cron) |
| `sless_job` | Запускает функцию один раз (например, для инициализации БД) и ждёт результата |
**Типичный сценарий:** `sless_function` с кодом + `sless_trigger` с `type = "http"` → публичный URL вида `https://sless-api.kube5s.ru/fn/default/имя-функции`.
---
Примеры показывают различные сценарии использования serverless функций через Terraform провайдер `terra.k8c.ru/naeel/sless`.
## Требования
- Terraform >= 1.0
- Доступ к `https://sless-api.kube5s.ru`
## Провайдер
Во всех примерах `main.tf` содержит:
```hcl
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = "dev-token-change-me"
}
```
---
## Примеры
### `simple-python` — джоб передаёт результат в HTTP-функцию (Python)
При `apply` запускается джоб, его вывод передаётся в HTTP-функцию через `env_vars`.
```bash
cd simple-python
terraform init
terraform apply -auto-approve
# Что вернул джоб (время на момент деплоя):
terraform output job_result
# Проверить функцию:
curl -s https://sless-api.kube5s.ru/fn/default/simple-py-time-display
```
---
### `simple-node` — то же самое, но на Node.js 20
```bash
cd simple-node
terraform init
terraform apply -auto-approve
terraform output job_result
curl -s https://sless-api.kube5s.ru/fn/default/simple-node-time-display
```
---
### `hello-node` — минимальный пример на Node.js
Две независимые функции: HTTP-функция (возвращает приветствие) и одноразовый джоб (суммирует числа).
```bash
cd hello-node
terraform init
terraform apply -auto-approve
# Проверить HTTP-функцию:
curl -s -X POST https://sless-api.kube5s.ru/fn/default/hello-http \
-H 'Content-Type: application/json' -d '{"name":"World"}'
# Посмотреть результат джоба:
terraform output job_message
```
---
### `notes-python` — CRUD API на Python + PostgreSQL
Полноценное приложение: инициализация схемы БД через джобы, CRUD-функция, read-only функция для списка записей.
**Переменные:**
| Переменная | Описание | Дефолт |
|---|---|---|
| `pg_dsn` | DSN для подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` |
```bash
cd notes-python
terraform init
# Опционально — переопределить DSN:
# export TF_VAR_pg_dsn="postgres://user:pass@host:5432/db?sslmode=disable"
terraform apply -auto-approve
# Проверить инициализацию БД:
terraform output db_init_table_status
terraform output db_init_index_status
# URL функций:
terraform output notes_url # CRUD
terraform output notes_list_url # список всех записей
# Создать запись:
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/add?title=Hello&body=World"
# Список записей:
curl -s https://sless-api.kube5s.ru/fn/default/notes-list
# Обновить (id из предыдущего ответа):
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/update?id=1&title=Updated&body=New+body"
# Удалить:
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/delete?id=1"
```
---
## Общие команды
```bash
# Посмотреть текущее состояние ресурсов:
terraform show
# Пересоздать конкретный ресурс:
terraform apply -replace=sless_function.имя -auto-approve
# Повторно запустить джоб — увеличить run_id в .tf файле, затем:
terraform apply -auto-approve
# Удалить все ресурсы примера:
terraform destroy -auto-approve
```
## Структура каждого примера
```
пример/
├── main.tf — провайдер
├── *.tf — ресурсы (функции, триггеры, джобы)
├── outputs.tf — URLs и статусы после apply
├── variables.tf — входные переменные (если есть)
└── code/ — исходный код функций
```

View File

@ -0,0 +1,8 @@
// 2026-03-08
// handler-http.js — HTTP-функция: возвращает приветствие.
// Используется с sless_trigger (постоянный эндпоинт).
exports.handle = async (event) => {
const name = event.name || 'World';
return { message: `Hello, ${name}! HTTP !!!` };
};

View File

@ -0,0 +1,11 @@
// 2026-03-08
// handler-job.js — batch-функция: суммирует числа и считает среднее.
// Используется с sless_job (одноразовый запуск).
// event.numbers — массив чисел, например [1, 2, 3, 4, 5]
exports.handle = async (event) => {
const numbers = event.numbers || [];
const sum = numbers.reduce((acc, n) => acc + n, 0);
const avg = numbers.length > 0 ? sum / numbers.length : 0;
return { input: numbers, sum, avg, count: numbers.length };
};

24
hello-node/http.tf Normal file
View File

@ -0,0 +1,24 @@
# 2026-03-08 / Изменено: 2026-03-09
# http.tf HTTP-функция: принимает запросы, возвращает приветствие.
# Код: code/handler-http.js
resource "sless_function" "hello_http" {
name = "hello-http"
runtime = "nodejs20"
entrypoint = "handler-http.handle"
memory_mb = 128
timeout_sec = 30
source_dir = "${path.module}/code"
}
resource "sless_trigger" "hello_http" {
name = "hello-http-trigger"
type = "http"
function = sless_function.hello_http.name
enabled = true
}
output "trigger_url" {
value = sless_trigger.hello_http.url
}

34
hello-node/job.tf Normal file
View File

@ -0,0 +1,34 @@
# 2026-03-08 / Изменено: 2026-03-09
# job.tf одноразовая функция: суммирует числа из переданного массива.
# Код: code/handler-job.js
resource "sless_function" "hello_job" {
name = "hello-job"
runtime = "nodejs20"
entrypoint = "handler-job.handle"
memory_mb = 128
timeout_sec = 30
source_dir = "${path.module}/code"
}
# Одноразовый запуск. Для повторного запуска увеличь run_id (123...).
resource "sless_job" "hello_run" {
name = "hello-run"
function = sless_function.hello_job.name
event_json = jsonencode({ numbers = [100, 200, 300] })
wait_timeout_sec = 600
run_id = 9
}
output "job_phase" {
value = sless_job.hello_run.phase
}
output "job_message" {
value = sless_job.hello_run.message
}
output "job_completion_time" {
value = sless_job.hello_run.completion_time
}

24
hello-node/main.tf Normal file
View File

@ -0,0 +1,24 @@
# 2026-03-11
# main.tf провайдеры.
# Функции и их код определены в отдельных файлах:
# http.tf HTTP-триггер (code/handler-http.js)
# job.tf одноразовый запуск (code/handler-job.js)
#
# nubes_endpoint провайдер делает GET запрос для валидации токена.
# Namespace вычисляется автоматически из JWT sub: sless-{sha256[:8]}
terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.13"
}
}
}
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
}

View File

@ -0,0 +1,2 @@
# Временный файл для негативных тестов — не применяется через terraform
# Тесты запускаются вручную с временным переименованием в .tf

10
hello-node/variables.tf Normal file
View File

@ -0,0 +1,10 @@
# 2026-03-11
# variables.tf входные переменные для hello-node примера.
# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored).
# Из токена провайдер вычисляет namespace: sless-{sha256[:8]}
variable "token" {
description = "JWT токен облака для аутентификации в sless API"
type = string
sensitive = true
}

View File

@ -0,0 +1,31 @@
# 2026-03-09
# notes_list.py — чтение всех записей из таблицы notes.
#
# Назначение: отдать полный список заметок одним запросом.
# Принимает GET или POST — тело/query параметры игнорируются.
# Возвращает JSON-массив, сортировка: новые записи первые (ORDER BY created_at DESC).
#
# Пример ответа:
# [
# {"id": 3, "title": "Hello", "body": "World", "created_at": "2026-03-09 ..."},
# {"id": 1, "title": "First", "body": "Note", "created_at": "2026-03-08 ..."}
# ]
import os
import psycopg2
import psycopg2.extras
def list_notes(event):
dsn = os.environ['PG_DSN']
conn = psycopg2.connect(dsn)
try:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute(
"SELECT id, title, body, created_at::text FROM notes ORDER BY created_at DESC"
)
rows = cur.fetchall()
return [dict(r) for r in rows]
except Exception as e:
return {'error': str(e)}
finally:
conn.close()

View File

@ -0,0 +1 @@
psycopg2-binary

View File

@ -0,0 +1,81 @@
# 2026-03-09
# notes_crud.py — CRUD роутер для таблицы notes.
#
# Назначение: единая функция, которая обрабатывает все операции с записями.
# Роутинг осуществляется по sub-path URL (event._path), который runtime
# берёт из входящего HTTP-запроса и добавляет в event автоматически.
#
# Доступные маршруты (все POST):
# /fn/default/notes/add?title=...&body=... → создать запись
# /fn/default/notes/update?id=1&title=...&body=... → обновить запись
# /fn/default/notes/delete?id=1 → удалить запись
#
# Параметры берутся из query string (event._query) или из тела запроса (event).
# event._path и event._query добавляет Python runtime (server.py) автоматически.
import os
import psycopg2
import psycopg2.extras
def crud(event):
dsn = os.environ['PG_DSN']
# sub-path без ведущего слэша: "add", "update", "delete"
action = event.get('_path', '/').strip('/')
q = event.get('_query', {})
conn = psycopg2.connect(dsn)
try:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
if action == 'add':
title = q.get('title') or event.get('title', '')
body = q.get('body') or event.get('body', '')
if not title:
return {'error': 'title is required'}
cur.execute(
"INSERT INTO notes (title, body) VALUES (%s, %s)"
" RETURNING id, title, body, created_at::text",
(title, body)
)
row = cur.fetchone()
conn.commit()
return dict(row)
elif action == 'update':
id_ = q.get('id') or event.get('id')
if not id_:
return {'error': 'id is required'}
title = q.get('title') or event.get('title', '')
body = q.get('body') or event.get('body', '')
cur.execute(
"UPDATE notes SET title=%s, body=%s WHERE id=%s"
" RETURNING id, title, body, created_at::text",
(title, body, int(id_))
)
row = cur.fetchone()
conn.commit()
return dict(row) if row else {'error': 'not found'}
elif action == 'delete':
id_ = q.get('id') or event.get('id')
if not id_:
return {'error': 'id is required'}
cur.execute(
"DELETE FROM notes WHERE id=%s RETURNING id",
(int(id_),)
)
row = cur.fetchone()
conn.commit()
return {'deleted': row['id']} if row else {'error': 'not found'}
else:
return {
'error': f'unknown action: /{action}',
'hint': 'use /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X'
}
except Exception as e:
conn.rollback()
return {'error': str(e)}
finally:
conn.close()

View File

@ -0,0 +1 @@
psycopg2-binary

View File

@ -0,0 +1 @@
psycopg2-binary

View File

@ -0,0 +1,39 @@
# 2026-03-09
# sql_runner.py — универсальный DDL/SQL исполнитель.
#
# Назначение: выполнять произвольные SQL запросы переданные через event.
# Используется ТОЛЬКО через sless_job (init.tf) — HTTP-триггера нет намеренно,
# чтобы никто снаружи не мог выполнить произвольный SQL.
#
# Входящий event:
# {
# "statements": [
# "CREATE TABLE IF NOT EXISTS ...",
# "CREATE INDEX IF NOT EXISTS ..."
# ]
# }
#
# Все statements выполняются последовательно в одной транзакции.
# Если хотя бы один упал — транзакция откатывается целиком.
import os
import psycopg2
def run_sql(event):
dsn = os.environ['PG_DSN']
statements = event.get('statements', [])
if not statements:
return {'error': 'no statements provided'}
conn = psycopg2.connect(dsn)
try:
cur = conn.cursor()
for sql in statements:
cur.execute(sql)
conn.commit()
return {'ok': True, 'executed': len(statements)}
except Exception as e:
conn.rollback()
return {'error': str(e)}
finally:
conn.close()

44
notes-python/init.tf Normal file
View File

@ -0,0 +1,44 @@
# 2026-03-09
# init.tf однократная инициализация схемы БД через sless_job.
#
# Джобы запускаются один раз при terraform apply и ждут завершения.
# Использует функцию sql_runner (без HTTP-триггера) для безопасного DDL.
#
# Порядок выполнения гарантирован через depends_on:
# 1. notes_table_init создаём таблицу
# 2. notes_index_init создаём индекс (требует таблицу)
#
# Для повторного запуска (например, после DROP TABLE) увеличь run_id.
# run_id отслеживается в state: при изменении terraform перезапустит джоб.
# Джоб создания таблицы notes.
# CREATE TABLE IF NOT EXISTS безопасно запускать повторно, таблица не пересоздаётся.
resource "sless_job" "notes_table_init" {
name = "notes-create-table"
function = sless_function.sql_runner.name
wait_timeout_sec = 120
run_id = 1
event_json = jsonencode({
statements = [
"CREATE TABLE IF NOT EXISTS notes (id serial PRIMARY KEY, title text NOT NULL, body text, created_at timestamp DEFAULT now())"
]
})
}
# Джоб создания индекса для сортировки по дате.
# depends_on гарантирует, что таблица уже создана до создания индекса.
resource "sless_job" "notes_index_init" {
depends_on = [sless_job.notes_table_init]
name = "notes-create-index"
function = sless_function.sql_runner.name
wait_timeout_sec = 60
run_id = 1
event_json = jsonencode({
statements = [
"CREATE INDEX IF NOT EXISTS notes_created_idx ON notes(created_at DESC)"
]
})
}

27
notes-python/main.tf Normal file
View File

@ -0,0 +1,27 @@
# 2026-03-09
# main.tf конфигурация terraform и провайдеров.
#
# Все ресурсы вынесены в отдельные .tf файлы по назначению:
# variables.tf входные переменные (pg_dsn)
# sql-runner.tf служебная DDL-функция (без HTTP-триггера)
# init.tf однократная инициализация схемы БД
# notes.tf CRUD функция + HTTP-триггер
# notes-list.tf read-only функция + HTTP-триггер
# outputs.tf URLs развёрнутых эндпоинтов
terraform {
required_providers {
# Провайдер для управления serverless функциями через sless API
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.13"
}
}
}
# sless провайдер подключается к API кластера.
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
}

View File

@ -0,0 +1,22 @@
# 2026-03-09
# notes-list.tf read-only эндпоинт: возвращает все заметки, сортировка новые первые.
resource "sless_function" "notes_list" {
name = "notes-list"
runtime = "python3.11"
entrypoint = "notes_list.list_notes"
memory_mb = 128
timeout_sec = 30
env_vars = {
PG_DSN = var.pg_dsn
}
source_dir = "${path.module}/code/notes-list"
}
resource "sless_trigger" "notes_list_http" {
name = "notes-list-http"
type = "http"
function = sless_function.notes_list.name
}

27
notes-python/notes.tf Normal file
View File

@ -0,0 +1,27 @@
# 2026-03-09
# notes.tf CRUD функция для управления заметками (CREATE / UPDATE / DELETE).
#
# Маршруты (рекомендуется POST):
# /fn/default/notes/add?title=...&body=... INSERT
# /fn/default/notes/update?id=1&title=...&body=... UPDATE
# /fn/default/notes/delete?id=1 DELETE
resource "sless_function" "notes_crud" {
name = "notes"
runtime = "python3.11"
entrypoint = "notes_crud.crud"
memory_mb = 128
timeout_sec = 30
env_vars = {
PG_DSN = var.pg_dsn
}
source_dir = "${path.module}/code/notes"
}
resource "sless_trigger" "notes_crud_http" {
name = "notes-http"
type = "http"
function = sless_function.notes_crud.name
}

42
notes-python/outputs.tf Normal file
View File

@ -0,0 +1,42 @@
# 2026-03-09
# outputs.tf публичные URL развёрнутых функций.
#
# После terraform apply используй эти URLs для тестирования:
# terraform output notes_url базовый URL для CRUD
# terraform output notes_list_url URL для получения всех записей
# URL CRUD-функции (notes_crud).
# Базовый URL к нему добавляй sub-path:
# POST $(terraform output -raw notes_url)/add?title=Hello&body=World
# POST $(terraform output -raw notes_url)/update?id=1&title=Updated
# POST $(terraform output -raw notes_url)/delete?id=1
output "notes_url" {
value = sless_trigger.notes_crud_http.url
description = "CRUD: /add?title=...&body=..., /update?id=X&title=...&body=..., /delete?id=X"
}
# URL read-only функции (notes_list).
# Принимает GET или POST, параметры игнорирует, возвращает все записи.
output "notes_list_url" {
value = sless_trigger.notes_list_http.url
description = "Список всех записей (GET или POST)"
}
# Статус init-джобов показывает результат инициализации БД.
# Если phase="Succeeded" таблица и индекс созданы успешно.
# Если phase="Failed" смотри message, исправь и увеличь run_id в init.tf.
output "db_init_table_status" {
value = {
phase = sless_job.notes_table_init.phase
message = sless_job.notes_table_init.message
}
description = "Статус джоба создания таблицы notes"
}
output "db_init_index_status" {
value = {
phase = sless_job.notes_index_init.phase
message = sless_job.notes_index_init.message
}
description = "Статус джоба создания индекса"
}

View File

@ -0,0 +1,20 @@
# 2026-03-09
# sql-runner.tf служебная DDL-функция для инициализации и миграций БД.
#
# ВАЖНО: эта функция не имеет HTTP-триггера только вызов через sless_job.
# Это сделано намеренно: функция выполняет произвольный SQL, и открывать её
# наружу через HTTP было бы небезопасно.
resource "sless_function" "sql_runner" {
name = "sql-runner"
runtime = "python3.11"
entrypoint = "sql_runner.run_sql"
memory_mb = 128
timeout_sec = 30
env_vars = {
PG_DSN = var.pg_dsn
}
source_dir = "${path.module}/code/sql-runner"
}

23
notes-python/variables.tf Normal file
View File

@ -0,0 +1,23 @@
# 2026-03-09 (обновлён 2026-03-11)
# variables.tf входные переменные для notes-python примера.
#
# PG_DSN передаётся во все функции через env_vars.
# Хранится как sensitive чтобы не светился в terraform output и логах.
# В продакшне не хардкоди DSN здесь, используй TF_VAR_pg_dsn или secrets manager.
# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored).
# Из токена провайдер вычисляет namespace: sless-{sha256[:8]}
variable "token" {
description = "JWT токен облака для аутентификации в sless API"
type = string
sensitive = true
}
# DSN для подключения к PostgreSQL внутри кластера.
# Формат: postgres://user:password@host:port/dbname?sslmode=...
variable "pg_dsn" {
description = "PostgreSQL DSN для подключения к БД внутри кластера"
type = string
default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable"
sensitive = true
}

7
push-sample/Dockerfile Normal file
View File

@ -0,0 +1,7 @@
# 2026-03-11 10:00
# Minimal sample image to push to PearlHarbor registry
# Purpose: небольшой образ для тестирования пуша в реестр
FROM alpine:3.18
CMD ["sh", "-c", "echo Hello from pearlharbor sample image"]

28
push-sample/README.md Normal file
View File

@ -0,0 +1,28 @@
# Пример для пуша в PearlHarbor
Файлы:
- [examples/push-sample/Dockerfile](examples/push-sample/Dockerfile) — минимальный образ
- [examples/push-sample/build_and_push.sh](examples/push-sample/build_and_push.sh) — сборка и опциональный пуш
Как использовать:
1. Сборка локально (в корне репы):
```bash
docker build -t sless-sample:local -f examples/push-sample/Dockerfile examples/push-sample
```
2. Протестировать скрипт (скрипт не будет пушить без переменной DO_PUSH):
```bash
cd examples/push-sample
./build_and_push.sh
```
3. Для реального пуша установите `DO_PUSH=true`. Скрипт прочитает `secrets/pearlharbor_registry.txt`.
```bash
DO_PUSH=true ./build_and_push.sh
```
Примечание: скрипт использует по умолчанию пользователя `admin`. Для другого пользователя задайте `REGISTRY_USER`.

53
push-sample/build_and_push.sh Executable file
View File

@ -0,0 +1,53 @@
#!/usr/bin/env bash
# 2026-03-11 10:02
# Скрипт: собирает минимальный образ и, при разрешении, пушит в реестр PearlHarbor
# Требования: `docker` в PATH. Скрипт НЕ будет пушить без DO_PUSH=true.
set -euo pipefail
# Получаем значения из файла секретов
SECRETS_FILE="secrets/pearlharbor_registry.txt"
if [ ! -f "$SECRETS_FILE" ]; then
echo "Файл с секретами не найден: $SECRETS_FILE"
exit 1
fi
connection_url=$(grep -E '^connection_url=' "$SECRETS_FILE" | cut -d'=' -f2-)
admin_pass=$(grep -E '^admin_pass=' "$SECRETS_FILE" | cut -d'=' -f2-)
if [ -z "$connection_url" ]; then
echo "Не найден connection_url в $SECRETS_FILE"
exit 1
fi
# Убираем протокол и возможный слеш на конце
registry_host=$(echo "$connection_url" | sed -E 's~https?://~~' | sed -E 's~/$~~')
image_name="$registry_host/sless-sample:latest"
echo "Registry host: $registry_host"
echo "Image name: $image_name"
echo "Собираю образ локально..."
docker build -t sless-sample:local -f Dockerfile .. || {
echo "Сборка не удалась"; exit 1
}
echo "Готово. Образ: sless-sample:local"
if [ "${DO_PUSH:-}" != "true" ]; then
echo "DO_PUSH != true — пуш не будет выполнен. Чтобы запушить: DO_PUSH=true ./build_and_push.sh"
exit 0
fi
# Если дошли до сюда — выполняем login/push
registry_user=${REGISTRY_USER:-admin}
echo "Выполняю docker login к $registry_host как '$registry_user'"
echo "$admin_pass" | docker login "$registry_host" -u "$registry_user" --password-stdin
echo "Тегирую и пушу образ: $image_name"
docker tag sless-sample:local "$image_name"
docker push "$image_name"
echo "Пуш завершён. Проверьте реестр для образа: $image_name"

333
run_terraform_examples.sh Executable file
View File

@ -0,0 +1,333 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
LOG_DIR="$ROOT_DIR/.test-logs"
mkdir -p "$LOG_DIR"
EXAMPLES=(
"hello-node"
"simple-node"
"simple-python"
"notes-python"
)
declare -A CHECK_METHOD=(
[hello-node]="POST"
[simple-node]="GET"
[simple-python]="GET"
[notes-python]="GET"
)
declare -A CHECK_URL=(
[hello-node]="https://sless-api.kube5s.ru/fn/default/hello-http"
[simple-node]="https://sless-api.kube5s.ru/fn/default/simple-node-time-display"
[simple-python]="https://sless-api.kube5s.ru/fn/default/simple-py-time-display"
[notes-python]="https://sless-api.kube5s.ru/fn/default/notes-list"
)
declare -A CHECK_DATA=(
[hello-node]='{"name":"Smoke"}'
[simple-node]=''
[simple-python]=''
[notes-python]=''
)
declare -a PREEXISTING_EXAMPLES=()
LAST_LOG_FILE=""
CURRENT_EXAMPLE=""
CURRENT_STEP=""
LAST_ERROR_SUMMARY=""
fail_run() {
local example="$1"
local step="$2"
local details="$3"
echo
echo "ERROR SUMMARY"
echo "example: $example"
echo "step: $step"
echo "reason: $details"
if [ -n "$LAST_LOG_FILE" ] && [ -f "$LAST_LOG_FILE" ]; then
echo "log: $LAST_LOG_FILE"
echo "last log lines:"
tail -n 20 "$LAST_LOG_FILE"
fi
exit 1
}
run_step() {
local example="$1"
local step="$2"
shift 2
CURRENT_EXAMPLE="$example"
CURRENT_STEP="$step"
LAST_ERROR_SUMMARY=""
if ! "$@"; then
local details="$LAST_ERROR_SUMMARY"
if [ -z "$details" ]; then
details="step failed without explicit summary"
fi
fail_run "$example" "$step" "$details"
fi
}
restore_any_backups() {
local backup
while IFS= read -r backup; do
[ -n "$backup" ] || continue
if [ -f "$backup" ]; then
mv "$backup" "${backup%.copilot.bak}"
fi
done < <(find "$ROOT_DIR" -name '*.copilot.bak' | sort)
}
trap restore_any_backups EXIT
clean_local_artifacts() {
local example="$1"
rm -rf \
"$ROOT_DIR/$example/.terraform" \
"$ROOT_DIR/$example/.terraform.lock.hcl" \
"$ROOT_DIR/$example/terraform.tfstate" \
"$ROOT_DIR/$example/terraform.tfstate.backup" \
"$ROOT_DIR/$example"/terraform.tfstate.*.backup \
"$ROOT_DIR/$example/dist"
}
retry_tf() {
local example="$1"
local label="$2"
shift 2
local attempt=1
while [ "$attempt" -le 3 ]; do
LAST_LOG_FILE="$LOG_DIR/${example//\//_}-${label// /_}-${attempt}.log"
echo "==> [$example] $label (attempt $attempt/3)"
(
cd "$ROOT_DIR/$example"
"$@"
) 2>&1 | tee "$LAST_LOG_FILE"
local status=${PIPESTATUS[0]}
if [ "$status" -eq 0 ]; then
return 0
fi
if grep -Eiq 'Unauthorized|401|403' "$LAST_LOG_FILE"; then
LAST_ERROR_SUMMARY="authorization error during $label"
echo "[$example] authorization error during $label"
return 41
fi
if grep -Eiq 'TLS handshake timeout|tls:.*timeout|i/o timeout|Client\.Timeout exceeded while awaiting headers|context deadline exceeded|unexpected EOF' "$LAST_LOG_FILE" && [ "$attempt" -lt 3 ]; then
attempt=$((attempt + 1))
sleep 2
continue
fi
if grep -Eiq 'TLS handshake timeout|tls:.*timeout|i/o timeout|Client\.Timeout exceeded while awaiting headers|context deadline exceeded|unexpected EOF' "$LAST_LOG_FILE"; then
LAST_ERROR_SUMMARY="network/provider download failure during $label after retries"
else
LAST_ERROR_SUMMARY="terraform command failed during $label with exit code $status"
fi
return "$status"
done
LAST_ERROR_SUMMARY="terraform command failed during $label after exhausting retries"
return 1
}
record_preexisting_if_needed() {
local example="$1"
if grep -Fq 'No changes. Your infrastructure matches the configuration.' "$LAST_LOG_FILE"; then
PREEXISTING_EXAMPLES+=("$example")
echo "[$example] detected preexisting remote resources on clean apply"
fi
}
probe_endpoint() {
local example="$1"
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
local status_file="$LOG_DIR/${example//\//_}-endpoint-status.txt"
local method="${CHECK_METHOD[$example]}"
local url="${CHECK_URL[$example]}"
local data="${CHECK_DATA[$example]}"
if [ "$method" = "POST" ]; then
curl -sS -X POST -H 'Content-Type: application/json' -d "$data" -o "$body_file" -w '%{http_code}' "$url" > "$status_file"
else
curl -sS -o "$body_file" -w '%{http_code}' "$url" > "$status_file"
fi
}
assert_live_endpoint() {
local example="$1"
probe_endpoint "$example"
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
local status
status="$(cat "$LOG_DIR/${example//\//_}-endpoint-status.txt")"
if [ "$status" != "200" ]; then
LAST_ERROR_SUMMARY="live endpoint check failed with HTTP $status"
echo "[$example] live endpoint check failed with HTTP $status"
cat "$body_file"
return 1
fi
if grep -Fq 'function unreachable' "$body_file"; then
LAST_ERROR_SUMMARY="live endpoint returned function unreachable"
echo "[$example] live endpoint check returned unreachable function"
cat "$body_file"
return 1
fi
}
assert_destroyed_endpoint() {
local example="$1"
probe_endpoint "$example"
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
local status
status="$(cat "$LOG_DIR/${example//\//_}-endpoint-status.txt")"
if [ "$status" = "404" ] || [ "$status" = "000" ]; then
return 0
fi
if grep -Eiq 'not found|404 page not found' "$body_file"; then
return 0
fi
if [ "$status" = "502" ] && grep -Fq 'function unreachable' "$body_file"; then
LAST_ERROR_SUMMARY="route cleanup bug: public endpoint still exists but backend is already gone (HTTP 502 function unreachable)"
echo "[$example] route still exists after destroy, but backend is already gone (HTTP 502 function unreachable)"
cat "$body_file"
return 1
fi
LAST_ERROR_SUMMARY="endpoint still responds after destroy with HTTP $status"
echo "[$example] endpoint still responds after destroy with HTTP $status"
cat "$body_file"
return 1
}
wait_for_destroyed_endpoint() {
local example="$1"
local attempts=24
local sleep_sec=5
local try=1
while [ "$try" -le "$attempts" ]; do
if assert_destroyed_endpoint "$example"; then
echo "[$example] endpoint disappeared after destroy"
return 0
fi
echo "[$example] endpoint still present after destroy, waiting (${try}/${attempts})"
try=$((try + 1))
sleep "$sleep_sec"
done
echo "[$example] endpoint did not disappear after destroy within $((attempts * sleep_sec))s"
if [ -z "$LAST_ERROR_SUMMARY" ]; then
LAST_ERROR_SUMMARY="endpoint remained reachable for more than $((attempts * sleep_sec))s after destroy"
else
LAST_ERROR_SUMMARY="$LAST_ERROR_SUMMARY; endpoint was still published after $((attempts * sleep_sec))s"
fi
return 1
}
backup_and_modify() {
local example="$1"
case "$example" in
hello-node)
cp "$ROOT_DIR/$example/http.tf" "$ROOT_DIR/$example/http.tf.copilot.bak"
perl -0pi -e 's/enabled\s+=\s+true/enabled = false/' "$ROOT_DIR/$example/http.tf"
;;
simple-node)
cp "$ROOT_DIR/$example/time-display.tf" "$ROOT_DIR/$example/time-display.tf.copilot.bak"
perl -0pi -e 's/memory_mb\s+=\s+64/memory_mb = 96/' "$ROOT_DIR/$example/time-display.tf"
;;
simple-python)
cp "$ROOT_DIR/$example/time-display.tf" "$ROOT_DIR/$example/time-display.tf.copilot.bak"
perl -0pi -e 's/memory_mb\s+=\s+64/memory_mb = 96/' "$ROOT_DIR/$example/time-display.tf"
;;
notes-python)
cp "$ROOT_DIR/$example/notes-list.tf" "$ROOT_DIR/$example/notes-list.tf.copilot.bak"
perl -0pi -e 's/memory_mb\s+=\s+128/memory_mb = 160/' "$ROOT_DIR/$example/notes-list.tf"
;;
esac
}
restore_modified_files() {
local example="$1"
case "$example" in
hello-node)
mv "$ROOT_DIR/$example/http.tf.copilot.bak" "$ROOT_DIR/$example/http.tf"
;;
simple-node)
mv "$ROOT_DIR/$example/time-display.tf.copilot.bak" "$ROOT_DIR/$example/time-display.tf"
;;
simple-python)
mv "$ROOT_DIR/$example/time-display.tf.copilot.bak" "$ROOT_DIR/$example/time-display.tf"
;;
notes-python)
mv "$ROOT_DIR/$example/notes-list.tf.copilot.bak" "$ROOT_DIR/$example/notes-list.tf"
;;
esac
}
run_example() {
local example="$1"
echo
echo "==== $example ===="
clean_local_artifacts "$example"
run_step "$example" "terraform init" retry_tf "$example" "terraform init" terraform init -input=false -no-color
run_step "$example" "terraform apply clean" retry_tf "$example" "terraform apply clean" terraform apply -auto-approve -input=false -no-color
record_preexisting_if_needed "$example"
run_step "$example" "endpoint check after clean apply" assert_live_endpoint "$example"
run_step "$example" "terraform destroy clean" retry_tf "$example" "terraform destroy clean" terraform destroy -auto-approve -input=false -no-color
run_step "$example" "endpoint cleanup after clean destroy" wait_for_destroyed_endpoint "$example"
run_step "$example" "terraform apply second" retry_tf "$example" "terraform apply second" terraform apply -auto-approve -input=false -no-color
run_step "$example" "endpoint check after second apply" assert_live_endpoint "$example"
backup_and_modify "$example"
run_step "$example" "terraform apply modified" retry_tf "$example" "terraform apply modified" terraform apply -auto-approve -input=false -no-color
restore_modified_files "$example"
run_step "$example" "terraform destroy final" retry_tf "$example" "terraform destroy final" terraform destroy -auto-approve -input=false -no-color
run_step "$example" "endpoint cleanup after final destroy" wait_for_destroyed_endpoint "$example"
clean_local_artifacts "$example"
}
main() {
local example
for example in "${EXAMPLES[@]}"; do
run_example "$example"
done
if [ "${#PREEXISTING_EXAMPLES[@]}" -gt 0 ]; then
echo
echo "Preexisting remote resources were detected on first apply for: ${PREEXISTING_EXAMPLES[*]}"
exit 2
fi
echo
echo "All Terraform example lifecycles completed successfully."
}
main "$@"

View File

@ -0,0 +1,20 @@
// Создано: 2026-03-09
// time_display.js — HTTP-функция (постоянный Deployment + Trigger).
// Читает env JOB_TIME, которую terraform передаёт из sless_job.run_getter.message.
// Демонстрирует цепочку: Job вычисляет данные → Function использует их через env.
exports.showTime = function(event) {
// JOB_TIME устанавливается terraform из статуса джоба (JSON строка)
const jobTimeRaw = process.env.JOB_TIME || '{}';
let jobTime;
try {
const parsed = JSON.parse(jobTimeRaw);
jobTime = parsed.time || jobTimeRaw;
} catch (e) {
jobTime = jobTimeRaw;
}
return {
message: `Сервис запустился в: ${jobTime}`,
path: event._path || '/',
};
};

View File

@ -0,0 +1,10 @@
// Создано: 2026-03-09
// time_getter.js — функция запускается как Job (одноразово).
// Возвращает JSON со временем запуска. Оператор v0.1.16 захватывает stdout
// и записывает его в sless_job.run_getter.message — оттуда terraform передаёт
// значение в sless_function.display через env_var JOB_TIME.
exports.getTime = function(event) {
// Возвращаем время в ISO 8601 UTC — без зависимостей, только stdlib
return { time: new Date().toISOString() };
};

31
simple-node/main.tf Normal file
View File

@ -0,0 +1,31 @@
# Создано: 2026-03-09
# main.tf пример: запустить один раз скрипт при деплое и передать его результат в функцию.
# То же самое что simple-python, но на Node.js 20.
#
# Как это работает:
# 1. При «terraform apply» запускается скрипт-джоб (time_getter)
# 2. Скрипт возвращает JSON с текущим временем
# 3. Terraform подхватывает этот JSON и передаёт в переменную окружения HTTP-функции (time_display)
# 4. Функция отдаёт время при каждом запросе
#
# Зачем такое нужно:
# Если данные нужны функции, но считаются один раз при деплое
# напишите логику в джоб, а результат передайте через env_vars.
# Например: получить токен, версию схемы БД, время деплоя и т.д.
#
# namespace захардкодирован внутри провайдера, здесь ничего указывать.
terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.13"
}
}
}
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
}

14
simple-node/outputs.tf Normal file
View File

@ -0,0 +1,14 @@
# Создано: 2026-03-09
# outputs.tf что выводит terraform после apply.
# Адрес вашей функции откройте в браузере или вставьте в curl
output "display_url" {
description = "URL функции time_display"
value = sless_trigger.display_http.url
}
# Что вернул скрипт-джоб именно это передано в функцию как JOB_TIME
output "job_result" {
description = "Результат выполнения скрипта time_getter"
value = sless_job.run_getter.message
}

View File

@ -0,0 +1,28 @@
# Создано: 2026-03-09 / Изменено: 2026-03-09
# time-display.tf HTTP-функция, доступная по URL после apply.
# Получает результат джоба (из time-getter.tf) через переменную окружения JOB_TIME.
# HTTP-функция отвечает на запросы по URL из outputs.tf
resource "sless_function" "time_display" {
name = "simple-node-time-display" # уникальное имя в namespace
runtime = "nodejs20"
entrypoint = "time_display.showTime" # файл.функция в code/time_display/
memory_mb = 64
# Передаём результат джоба в функцию через переменную окружения.
# В коде функции: process.env.JOB_TIME
env_vars = {
JOB_TIME = sless_job.run_getter.message
}
source_dir = "${path.module}/code/time_display"
depends_on = [sless_job.run_getter] # ждём завершения джоба перед деплоем функции
}
# Публикуем функцию по HTTP URL будет в outputs.tf
resource "sless_trigger" "display_http" {
name = "simple-node-display-http"
type = "http"
function = sless_function.time_display.name
}

View File

@ -0,0 +1,27 @@
# Создано: 2026-03-09 / Изменено: 2026-03-09
# time-getter.tf скрипт который запускается ОДИН РАЗ при terraform apply.
# После запуска его результат доступен через: sless_job.run_getter.message
# Смотри time-display.tf там этот результат передаётся в функцию.
# Функция для скрипта без HTTP-триггера, вызывается только через джоб ниже
resource "sless_function" "time_getter" {
name = "simple-node-time-getter" # уникальное имя в namespace
runtime = "nodejs20"
entrypoint = "time_getter.getTime" # файл.функция в code/time_getter/
memory_mb = 96
source_dir = "${path.module}/code/time_getter"
}
# Джоб запускает функцию time_getter один раз прямо при apply.
# run_id = 1 означает «запустить». Если увеличить (2, 3...) запустится снова.
# После завершения: sless_job.run_getter.message = то что вернула функция
resource "sless_job" "run_getter" {
name = "simple-node-getter-run"
function = sless_function.time_getter.name
run_id = 1
wait_timeout_sec = 120 # сколько секунд ждать завершения скрипта
event_json = "{}" # входные данные для скрипта (пусто данные не нужны)
depends_on = [sless_function.time_getter]
}

10
simple-node/variables.tf Normal file
View File

@ -0,0 +1,10 @@
# 2026-03-11
# variables.tf входные переменные для simple-node примера.
# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored).
# Из токена провайдер вычисляет namespace: sless-{sha256[:8]}
variable "token" {
description = "JWT токен облака для аутентификации в sless API"
type = string
sensitive = true
}

View File

@ -0,0 +1,20 @@
# Создано: 2026-03-09
# time_display.py — HTTP-функция (постоянный Deployment + Trigger).
# Читает env JOB_TIME, которую terraform передаёт из sless_job.run_getter.message.
# Демонстрирует цепочку: Job вычисляет данные → Function использует их через env.
import json
import os
def show_time(event):
# JOB_TIME устанавливается terraform из статуса джоба (JSON строка)
job_time_raw = os.environ.get("JOB_TIME", "{}")
try:
job_time = json.loads(job_time_raw).get("time", job_time_raw)
except (json.JSONDecodeError, AttributeError):
job_time = job_time_raw
return {
"message": f"Сервис запустился в: {job_time}",
"path": event.get("_path", "/"),
}

View File

@ -0,0 +1,18 @@
# Создано: 2026-03-09
# time_getter.py — функция запускается как Job (одноразово).
# Возвращает JSON со временем запуска. Оператор v0.1.16 захватывает stdout
# и записывает его в sless_job.run_getter.message — оттуда terraform передаёт
# значение в sless_function.display через env_var JOB_TIME.
import json
from datetime import datetime, timezone
def get_time(event):
# Возвращаем время в ISO 8601 UTC — без зависимостей, только stdlib
return {"time": datetime.now(timezone.utc).isoformat()}
if __name__ == "__main__":
# Для локального тестирования без оператора
print(json.dumps(get_time({})))

30
simple-python/main.tf Normal file
View File

@ -0,0 +1,30 @@
# Создано: 2026-03-09
# main.tf пример: запустить один раз скрипт при деплое и передать его результат в функцию.
#
# Как это работает:
# 1. При «terraform apply» запускается скрипт-джоб (time_getter)
# 2. Скрипт возвращает JSON с текущим временем
# 3. Terraform подхватывает этот JSON и передаёт в переменную окружения HTTP-функции (time_display)
# 4. Функция отдаёт время при каждом запросе
#
# Зачем такое нужно:
# Если данные нужны функции, но считаются один раз при деплое
# напишите логику в джоб, а результат передайте через env_vars.
# Например: получить токен, версию схемы БД, время деплоя и т.д.
#
# namespace захардкодирован внутри провайдера, здесь ничего указывать.
terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.13"
}
}
}
provider "sless" {
endpoint = "https://sless-api.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
}

14
simple-python/outputs.tf Normal file
View File

@ -0,0 +1,14 @@
# Создано: 2026-03-09
# outputs.tf что выводит terraform после apply.
# Адрес вашей функции откройте в браузере или вставьте в curl
output "display_url" {
description = "URL функции time_display"
value = sless_trigger.display_http.url
}
# Что вернул скрипт-джоб именно это передано в функцию как JOB_TIME
output "job_result" {
description = "Результат выполнения скрипта time_getter"
value = sless_job.run_getter.message
}

View File

@ -0,0 +1,28 @@
# Создано: 2026-03-09 / Изменено: 2026-03-09
# time-display.tf HTTP-функция, доступная по URL после apply.
# Получает результат джоба (из time-getter.tf) через переменную окружения JOB_TIME.
# HTTP-функция отвечает на запросы по URL из outputs.tf
resource "sless_function" "time_display" {
name = "simple-py-time-display" # уникальное имя в namespace
runtime = "python3.11"
entrypoint = "time_display.show_time" # файл.функция в code/time_display/
memory_mb = 64
# Передаём результат джоба в функцию через переменную окружения.
# В коде функции: os.environ.get("JOB_TIME")
env_vars = {
JOB_TIME = sless_job.run_getter.message
}
source_dir = "${path.module}/code/time_display"
depends_on = [sless_job.run_getter] # ждём завершения джоба перед деплоем функции
}
# Публикуем функцию по HTTP URL будет в outputs.tf
resource "sless_trigger" "display_http" {
name = "simple-py-display-http"
type = "http"
function = sless_function.time_display.name
}

View File

@ -0,0 +1,27 @@
# Создано: 2026-03-09 / Изменено: 2026-03-09
# time-getter.tf скрипт который запускается ОДИН РАЗ при terraform apply.
# После запуска его результат доступен через: sless_job.run_getter.message
# Смотри time-display.tf там этот результат передаётся в функцию.
# Функция для скрипта без HTTP-триггера, вызывается только через джоб ниже
resource "sless_function" "time_getter" {
name = "simple-py-time-getter" # уникальное имя в namespace
runtime = "python3.11"
entrypoint = "time_getter.get_time" # файл.функция в code/time_getter/
memory_mb = 96
source_dir = "${path.module}/code/time_getter"
}
# Джоб запускает функцию time_getter один раз прямо при apply.
# run_id = 1 означает «запустить». Если увеличить (2, 3...) запустится снова.
# После завершения: sless_job.run_getter.message = то что вернула функция
resource "sless_job" "run_getter" {
name = "simple-py-getter-run"
function = sless_function.time_getter.name
run_id = 1
wait_timeout_sec = 120 # сколько секунд ждать завершения скрипта
event_json = "{}" # входные данные для скрипта (пусто данные не нужны)
depends_on = [sless_function.time_getter]
}

View File

@ -0,0 +1,10 @@
# 2026-03-11
# variables.tf входные переменные для simple-python примера.
# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored).
# Из токена провайдер вычисляет namespace: sless-{sha256[:8]}
variable "token" {
description = "JWT токен облака для аутентификации в sless API"
type = string
sensitive = true
}