Initial examples import
This commit is contained in:
commit
a2094c82c5
143
DESTROY_ROUTE_CLEANUP_BUG.md
Normal file
143
DESTROY_ROUTE_CLEANUP_BUG.md
Normal 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
160
README.md
Normal 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/ — исходный код функций
|
||||||
|
```
|
||||||
8
hello-node/code/handler-http.js
Normal file
8
hello-node/code/handler-http.js
Normal 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 !!!` };
|
||||||
|
};
|
||||||
|
|
||||||
11
hello-node/code/handler-job.js
Normal file
11
hello-node/code/handler-job.js
Normal 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
24
hello-node/http.tf
Normal 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
34
hello-node/job.tf
Normal 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 (1→2→3...).
|
||||||
|
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
24
hello-node/main.tf
Normal 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"
|
||||||
|
}
|
||||||
|
|
||||||
1
hello-node/terraform.tfvars
Normal file
1
hello-node/terraform.tfvars
Normal file
@ -0,0 +1 @@
|
|||||||
|
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOTllMzI1LTFjZGYtN2NkYS05MzE5LWU1MzAyYTg1ZTI5MSIsImV4cCI6MTc4NjkzMjI2MCwiaWF0IjoxNzcxMzgwMjYwLCJqdGkiOiIzOTQ3ZTgyMy0yNjljLTQ0MTAtYmU0My1iNGVkNTc1Njg0ZTQiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJncm91cHMiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6IiIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUifQ.hzpIIqNWkKIoYUXDaLY7DLyGKH70rz0ZTqanv19qxF10i3N1t1g_KknA4Qsw1MduTyLzIz7y5SRSr4PSQ1gzR0vB_C0GudSFUhyBNNKkS4ClhRDWW9eN_IIEljbiJMLQi2L07XJ7Y5DQ0sIHRAPLkreCDFMKQ0yTCrKoScCJIDuUqzaTcOaX-hfjaxW8iV0SZMDxl0C5O3tke0btxkaLBaAcWH0V-1yu2r2m29fyU33FqikF0xAcDXiuZphfsrShKQYArZjKAphYCP_Vpmr-1sdjinkn8sPSk1qZny0rka8G6WVZUGaZSOnW8SYNLVUwdqtuQmK-Y18o7U0Suzrsjg"
|
||||||
2
hello-node/test_invalid.tf.disabled
Normal file
2
hello-node/test_invalid.tf.disabled
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
# Временный файл для негативных тестов — не применяется через terraform
|
||||||
|
# Тесты запускаются вручную с временным переименованием в .tf
|
||||||
10
hello-node/variables.tf
Normal file
10
hello-node/variables.tf
Normal 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
|
||||||
|
}
|
||||||
31
notes-python/code/notes-list/notes_list.py
Normal file
31
notes-python/code/notes-list/notes_list.py
Normal 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()
|
||||||
1
notes-python/code/notes-list/requirements.txt
Normal file
1
notes-python/code/notes-list/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
psycopg2-binary
|
||||||
81
notes-python/code/notes/notes_crud.py
Normal file
81
notes-python/code/notes/notes_crud.py
Normal 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()
|
||||||
1
notes-python/code/notes/requirements.txt
Normal file
1
notes-python/code/notes/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
psycopg2-binary
|
||||||
1
notes-python/code/sql-runner/requirements.txt
Normal file
1
notes-python/code/sql-runner/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
psycopg2-binary
|
||||||
39
notes-python/code/sql-runner/sql_runner.py
Normal file
39
notes-python/code/sql-runner/sql_runner.py
Normal 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
44
notes-python/init.tf
Normal 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
27
notes-python/main.tf
Normal 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"
|
||||||
|
}
|
||||||
22
notes-python/notes-list.tf
Normal file
22
notes-python/notes-list.tf
Normal 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
27
notes-python/notes.tf
Normal 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
42
notes-python/outputs.tf
Normal 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 = "Статус джоба создания индекса"
|
||||||
|
}
|
||||||
20
notes-python/sql-runner.tf
Normal file
20
notes-python/sql-runner.tf
Normal 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"
|
||||||
|
}
|
||||||
1
notes-python/terraform.tfvars
Normal file
1
notes-python/terraform.tfvars
Normal file
@ -0,0 +1 @@
|
|||||||
|
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOTllMzI1LTFjZGYtN2NkYS05MzE5LWU1MzAyYTg1ZTI5MSIsImV4cCI6MTc4NjkzMjI2MCwiaWF0IjoxNzcxMzgwMjYwLCJqdGkiOiIzOTQ3ZTgyMy0yNjljLTQ0MTAtYmU0My1iNGVkNTc1Njg0ZTQiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJncm91cHMiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6IiIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUifQ.hzpIIqNWkKIoYUXDaLY7DLyGKH70rz0ZTqanv19qxF10i3N1t1g_KknA4Qsw1MduTyLzIz7y5SRSr4PSQ1gzR0vB_C0GudSFUhyBNNKkS4ClhRDWW9eN_IIEljbiJMLQi2L07XJ7Y5DQ0sIHRAPLkreCDFMKQ0yTCrKoScCJIDuUqzaTcOaX-hfjaxW8iV0SZMDxl0C5O3tke0btxkaLBaAcWH0V-1yu2r2m29fyU33FqikF0xAcDXiuZphfsrShKQYArZjKAphYCP_Vpmr-1sdjinkn8sPSk1qZny0rka8G6WVZUGaZSOnW8SYNLVUwdqtuQmK-Y18o7U0Suzrsjg"
|
||||||
23
notes-python/variables.tf
Normal file
23
notes-python/variables.tf
Normal 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
7
push-sample/Dockerfile
Normal 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
28
push-sample/README.md
Normal 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
53
push-sample/build_and_push.sh
Executable 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
333
run_terraform_examples.sh
Executable 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 "$@"
|
||||||
20
simple-node/code/time_display/time_display.js
Normal file
20
simple-node/code/time_display/time_display.js
Normal 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 || '/',
|
||||||
|
};
|
||||||
|
};
|
||||||
10
simple-node/code/time_getter/time_getter.js
Normal file
10
simple-node/code/time_getter/time_getter.js
Normal 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
31
simple-node/main.tf
Normal 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
14
simple-node/outputs.tf
Normal 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
|
||||||
|
}
|
||||||
1
simple-node/terraform.tfvars
Normal file
1
simple-node/terraform.tfvars
Normal file
@ -0,0 +1 @@
|
|||||||
|
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOTllMzI1LTFjZGYtN2NkYS05MzE5LWU1MzAyYTg1ZTI5MSIsImV4cCI6MTc4NjkzMjI2MCwiaWF0IjoxNzcxMzgwMjYwLCJqdGkiOiIzOTQ3ZTgyMy0yNjljLTQ0MTAtYmU0My1iNGVkNTc1Njg0ZTQiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJncm91cHMiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6IiIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUifQ.hzpIIqNWkKIoYUXDaLY7DLyGKH70rz0ZTqanv19qxF10i3N1t1g_KknA4Qsw1MduTyLzIz7y5SRSr4PSQ1gzR0vB_C0GudSFUhyBNNKkS4ClhRDWW9eN_IIEljbiJMLQi2L07XJ7Y5DQ0sIHRAPLkreCDFMKQ0yTCrKoScCJIDuUqzaTcOaX-hfjaxW8iV0SZMDxl0C5O3tke0btxkaLBaAcWH0V-1yu2r2m29fyU33FqikF0xAcDXiuZphfsrShKQYArZjKAphYCP_Vpmr-1sdjinkn8sPSk1qZny0rka8G6WVZUGaZSOnW8SYNLVUwdqtuQmK-Y18o7U0Suzrsjg"
|
||||||
28
simple-node/time-display.tf
Normal file
28
simple-node/time-display.tf
Normal 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
|
||||||
|
}
|
||||||
27
simple-node/time-getter.tf
Normal file
27
simple-node/time-getter.tf
Normal 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 = 128
|
||||||
|
|
||||||
|
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
10
simple-node/variables.tf
Normal 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
|
||||||
|
}
|
||||||
20
simple-python/code/time_display/time_display.py
Normal file
20
simple-python/code/time_display/time_display.py
Normal 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", "/"),
|
||||||
|
}
|
||||||
18
simple-python/code/time_getter/time_getter.py
Normal file
18
simple-python/code/time_getter/time_getter.py
Normal 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
30
simple-python/main.tf
Normal 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
14
simple-python/outputs.tf
Normal 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
|
||||||
|
}
|
||||||
1
simple-python/terraform.tfvars
Normal file
1
simple-python/terraform.tfvars
Normal file
@ -0,0 +1 @@
|
|||||||
|
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOTllMzI1LTFjZGYtN2NkYS05MzE5LWU1MzAyYTg1ZTI5MSIsImV4cCI6MTc4NjkzMjI2MCwiaWF0IjoxNzcxMzgwMjYwLCJqdGkiOiIzOTQ3ZTgyMy0yNjljLTQ0MTAtYmU0My1iNGVkNTc1Njg0ZTQiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJncm91cHMiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6IiIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUifQ.hzpIIqNWkKIoYUXDaLY7DLyGKH70rz0ZTqanv19qxF10i3N1t1g_KknA4Qsw1MduTyLzIz7y5SRSr4PSQ1gzR0vB_C0GudSFUhyBNNKkS4ClhRDWW9eN_IIEljbiJMLQi2L07XJ7Y5DQ0sIHRAPLkreCDFMKQ0yTCrKoScCJIDuUqzaTcOaX-hfjaxW8iV0SZMDxl0C5O3tke0btxkaLBaAcWH0V-1yu2r2m29fyU33FqikF0xAcDXiuZphfsrShKQYArZjKAphYCP_Vpmr-1sdjinkn8sPSk1qZny0rka8G6WVZUGaZSOnW8SYNLVUwdqtuQmK-Y18o7U0Suzrsjg"
|
||||||
28
simple-python/time-display.tf
Normal file
28
simple-python/time-display.tf
Normal 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
|
||||||
|
}
|
||||||
27
simple-python/time-getter.tf
Normal file
27
simple-python/time-getter.tf
Normal 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 = 128
|
||||||
|
|
||||||
|
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]
|
||||||
|
}
|
||||||
10
simple-python/variables.tf
Normal file
10
simple-python/variables.tf
Normal 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
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user