chore: удаление устаревших examples, новый doc, правки funcs-service

- examples/: удалены старые директории (TNAR, demo-event-log, demo-managed-functions,
  hello-go, hello-node, notes-python, pg-list-python, simple-node, simple-python)
- examples/README.md: обновлён (только POSTGRES остался)
- doc/decisions/build-deploy-pipeline.md: новый документ по пайплайну сборки/деплоя
- services/funcs/main.go: правки из текущей сессии
This commit is contained in:
Naeel
2026-03-21 08:46:30 +03:00
parent 778cbc8b32
commit 7f7aa44e59
79 changed files with 389 additions and 2655 deletions
+359
View File
@@ -0,0 +1,359 @@
# Build & Deploy Pipeline — Terraform Provider + Operator
Дата: 2026-03-20
---
## Контекст
Этот документ описывает **полный цикл** внесения изменений в систему:
от правки Go-кода до работающего `terraform apply` в продакшен-примере.
Охватывает:
- как изменять/добавлять ресурсы в terraform-провайдере
- как собирать и публиковать провайдер
- как собирать Docker-образ оператора
- как деплоить оператор в кластер
- как всё связано
---
## 1. Архитектура компонентов
```
┌──────────────────────────────────────────────────────┐
│ Пользователь пишет Terraform код (functions.tf) │
│ resource "sless_job" {...} / "sless_service" {...} │
└───────────────────┬──────────────────────────────────┘
│ terraform apply
┌──────────────────────────────────────────────────────┐
│ Terraform Provider (terraform-provider-sless) │
│ Go бинарник в /tmp/sless-provider-dev/ │
│ Исходники: sless/terraform/provider/ │
│ Публикуется на: terra.k8c.ru/naeel/sless │
└───────────────────┬──────────────────────────────────┘
│ HTTP REST API
┌──────────────────────────────────────────────────────┐
│ sless-operator (Kubernetes Deployment) │
│ Namespace: sless │
│ Image: pearlharbor.registryk8s.services.ngcloud.ru/ │
│ naeel/sless-operator:v0.1.xx │
│ Исходники: sless/ (корень репо) │
│ Управляет CRD: sless_function, sless_job, │
│ sless_service, sless_trigger │
└──────────────────────────────────────────────────────┘
```
---
## 2. Где что хранится
| Компонент | Путь на удалённой машине | Путь на редактирование (sshfs) |
|-----------|--------------------------|-------------------------------|
| Оператор (Go) | `/home/naeel/terra/sless/` | `/home/naeel/remote_dev/sless/` |
| Terraform provider | `/home/naeel/terra/sless/terraform/provider/` | `/home/naeel/remote_dev/sless/terraform/provider/` |
| Пример POSTGRES | `/home/naeel/terra/sless/examples/POSTGRES/` | `/home/naeel/remote_dev/sless/examples/POSTGRES/` |
| Dev override provider | `/tmp/sless-provider-dev/` | только на удалённой машине |
**Правило:** файлы редактируются через sshfs (`/home/naeel/remote_dev/`),
команды выполняются только на удалённой машине `5.172.178.213` через SSH.
---
## 3. Как изменить ресурс в terraform-провайдере
### 3.1 Структура провайдера
```
terraform/provider/
├── main.go # точка входа, регистрация провайдера
├── go.mod
├── internal/
│ ├── client/
│ │ └── client.go # REST-клиент к sless API (типы запросов/ответов)
│ ├── resources/
│ │ ├── job_resource.go # ресурс sless_job
│ │ ├── service_resource.go # ресурс sless_service
│ │ ├── function_resource.go # ресурс sless_function
│ │ └── trigger_resource.go # ресурс sless_trigger
│ └── provider/
│ └── provider.go # конфигурация провайдера (endpoint, token)
└── hack/
└── build-and-publish.sh # скрипт сборки + публикации в S3
```
### 3.2 Добавить новый атрибут к существующему ресурсу
Пример: добавить `timeout_sec` к `sless_job`.
**Шаг 1 — client.go**: добавить поле в `JobRequest` и `JobResponse`:
```go
type JobRequest struct {
// ... существующие поля
TimeoutSec int32 `json:"timeout_sec,omitempty"`
}
```
**Шаг 2 — job_resource.go**: добавить поле в `JobModel`:
```go
type JobModel struct {
// ... существующие поля
TimeoutSec types.Int64 `tfsdk:"timeout_sec"`
}
```
**Шаг 3 — job_resource.go**: добавить в `Schema()`:
```go
"timeout_sec": schema.Int64Attribute{
Optional: true,
Computed: true,
Default: int64default.StaticInt64(30),
MarkdownDescription: "Таймаут выполнения в секундах.",
},
```
**Шаг 4 — job_resource.go**: использовать в `Create()` при формировании запроса:
```go
req := client.JobRequest{
// ...
TimeoutSec: int32(plan.TimeoutSec.ValueInt64()),
}
```
### 3.3 Создать новый ресурс
1. Создать файл `terraform/provider/internal/resources/myresource_resource.go`
2. Реализовать интерфейс `resource.Resource` (минимум: `Metadata`, `Schema`, `Create`, `Read`, `Update`, `Delete`)
3. Зарегистрировать в `provider/provider.go`:
```go
func (p *SlessProvider) Resources(ctx context.Context) []func() resource.Resource {
return []func() resource.Resource{
resources.NewJobResource,
resources.NewMyResource, // добавить сюда
}
}
```
### 3.4 Что НЕ делать
- Не добавлять поле в `Schema()` без добавления его в `Model` struct — паника при `terraform plan`
- Не забыть `tfsdk:"..."` тег — без него поле невидимо
- При `RequiresReplace`: если ресурс immutable по этому полю, добавить `planmodifier.RequiresReplace()`
---
## 4. Как изменить CRD (API типы) в операторе
### 4.1 Файлы
```
api/v1alpha1/
├── job_types.go # FunctionJobSpec / FunctionJobStatus / FunctionJobPhase
├── function_types.go # FunctionSpec / FunctionStatus
├── service_types.go # ServiceSpec / ServiceStatus
└── zz_generated.deepcopy.go # ГЕНЕРИРУЕТСЯ АВТОМАТИЧЕСКИ — не трогать вручную
```
### 4.2 Добавить поле в Spec
1. Добавить в `job_types.go`:
```go
type FunctionJobSpec struct {
// +kubebuilder:validation:Required
NewField string `json:"newField"`
}
```
2. Регенерировать deepcopy:
```bash
ssh -i /home/naeel/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 \
'cd /home/naeel/terra/sless && bin/controller-gen object paths="./api/..."'
```
3. Регенерировать CRD YAML:
```bash
ssh -i /home/naeel/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 \
'cd /home/naeel/terra/sless && bin/controller-gen crd paths="./api/..." \
output:crd:artifacts:config=config/crd/bases'
```
4. Применить CRD в кластер:
```bash
ssh -i /home/naeel/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 \
'kubectl apply -f /home/naeel/terra/sless/config/crd/bases/'
```
**Важно:** CRD нужно обновлять в кластере **до** деплоя оператора, иначе контроллер
не сможет читать/записывать новые поля из etcd.
---
## 5. Как собрать и задеплоить оператор
### 5.1 Полная последовательность
```bash
# Переменные
REGISTRY="pearlharbor.registryk8s.services.ngcloud.ru"
IMAGE="$REGISTRY/naeel/sless-operator"
VERSION="v0.1.42" # следующий тег
# 1. Логин в registry (один раз, credentials сохраняются)
echo "ieNocheiphaipheep1lie5johl7aqu" | docker login $REGISTRY -u admin --password-stdin
# 2. Сборка образа
docker build -t $IMAGE:$VERSION /home/naeel/terra/sless
# 3. Push
docker push $IMAGE:$VERSION
# 4. Обновить тег в operator.yaml (в файле sless/deployments/k8s/operator.yaml)
# Поле: image: pearlharbor.../naeel/sless-operator:v0.1.41 → v0.1.42
# 5. Apply в кластер
kubectl apply -f /home/naeel/terra/sless/deployments/k8s/operator.yaml
# 6. Дождаться rollout
kubectl rollout status deployment/sless-operator -n sless --timeout=180s
```
### 5.2 Что обязательно в operator.yaml
```yaml
spec:
template:
spec:
imagePullSecrets:
- name: sless-registry-auth # Secret должен существовать в namespace sless
containers:
- name: operator
image: pearlharbor.registryk8s.services.ngcloud.ru/naeel/sless-operator:v0.1.42
imagePullPolicy: Always # Always — иначе k8s возьмёт старый кеш
```
**Секрет `sless-registry-auth`** — содержит docker credentials для pearlharbor.
Если его нет: `kubectl create secret docker-registry sless-registry-auth -n sless ...`
### 5.3 Создать Harbor-проект (если нет)
Harbor требует чтобы проект существовал **до** первого push:
```bash
curl -X POST "https://pearlharbor.registryk8s.services.ngcloud.ru/api/v2.0/projects" \
-u admin:ieNocheiphaipheep1lie5johl7aqu \
-H "Content-Type: application/json" \
-d '{"project_name":"naeel","public":false}'
# Ожидаем: 201 Created
```
---
## 6. Как собрать и опубликовать terraform-провайдер
### 6.1 Быстрая версия (dev override — для локального теста)
Собирает бинарник прямо в `/tmp/sless-provider-dev/` — terraform подберёт его автоматически:
```bash
ssh -i /home/naeel/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 \
'cd /home/naeel/terra/sless/terraform/provider && \
CGO_ENABLED=0 GOOS=linux GOARCH=amd64 \
go build -ldflags "-X main.version=0.1.19" \
-o /tmp/sless-provider-dev/terraform-provider-sless_v0.1.19 . && echo OK'
```
**Важно:** в `/tmp/sless-provider-dev/` должен быть только ОДИН бинарник — удалить старый!
```bash
rm /tmp/sless-provider-dev/terraform-provider-sless_v0.1.18
```
Dev override настроен в `~/.terraformrc`:
```hcl
provider_installation {
dev_overrides {
"terra.k8c.ru/naeel/sless" = "/tmp/sless-provider-dev"
}
direct {}
}
```
### 6.2 Полная публикация в S3-registry (для продакшена)
Скрипт `hack/build-and-publish.sh` собирает бинарники для всех платформ,
вычисляет SHA256, подписывает GPG и загружает в S3:
```bash
ssh -i /home/naeel/.ssh/naeel_vm_id_ed25519 naeel@5.172.178.213 \
'cd /home/naeel/terra/sless/terraform/provider && \
S3CFG=/home/naeel/terra/terraform/secrets/.s3cfg_registry \
GPG_KEY_FILE=/home/naeel/terra/sless/secrets/private_key.asc \
bash hack/build-and-publish.sh 0.1.19'
```
После публикации проверить:
```bash
curl -sk https://terra.k8c.ru/v1/providers/naeel/sless/versions
```
### 6.3 Обновить версию в примерах
В `examples/POSTGRES/main.tf` (и других примерах):
```hcl
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.19" # обновить тут
}
```
Затем `terraform init -upgrade` — скачает новую версию.
---
## 7. Полный цикл: от изменения кода до terraform apply
```
1. Правка кода (локально через sshfs /home/naeel/remote_dev/sless/)
2. git add + git commit + git push
(локально или через SSH)
3. [если изменились api/v1alpha1/*.go]
controller-gen object → zz_generated.deepcopy.go
controller-gen crd → config/crd/bases/*.yaml
kubectl apply -f config/crd/bases/
4. [если изменился operator Go-код]
docker build → docker push → kubectl apply operator.yaml
kubectl rollout status deployment/sless-operator -n sless
5. [если изменился terraform/provider/]
go build → /tmp/sless-provider-dev/terraform-provider-sless_vX.X.X
(удалить старый бинарник из /tmp/sless-provider-dev/)
6. terraform apply в examples/POSTGRES/
cd /home/naeel/terra/sless/examples/POSTGRES && terraform apply -auto-approve
```
---
## 8. Типичные ошибки и решения
| Ошибка | Причина | Решение |
|--------|---------|---------|
| `unknown field FunctionRef` при `docker build` | Остарелая ссылка на удалённое поле CRD | Найти и заменить все вхождения `FunctionRef` в Go-коде |
| `Unsupported argument "timeout_sec"` при `terraform apply` | Провайдер не пересобран / старый бинарник в dev override | Пересобрать и удалить старый файл из `/tmp/sless-provider-dev/` |
| `ImagePullBackOff` при деплое оператора | Нет `imagePullSecrets` или secret не содержит credentials для registry | Добавить `imagePullSecrets: [{name: sless-registry-auth}]` в operator.yaml |
| `project naeel not found` при `docker push` | Проект в Harbor не создан | POST к Harbor API (см. раздел 5.3) |
| `no route to host` при SSH | Машина недоступна напрямую | Подключаться через 5.172.178.213 с SSH-ключом |
| CRD имеет старые поля (`functionRef` вместо `runtime`) | CRD не обновлён в кластере после правки types.go | `kubectl apply -f config/crd/bases/` после регенерации |
---
## 9. Связанные файлы документации
- [doc/run_and_logs.md](../run_and_logs.md) — шаблоны SSH-команд, реквизиты машин
- [doc/decisions/log.md](log.md) — обоснование архитектурных решений
- [doc/infrastructure/overview.md](../infrastructure/overview.md) — инфраструктура кластера
- [doc/progress.md](../progress.md) — трекер задач
+25 -144
View File
@@ -8,187 +8,68 @@
| Ресурс | Назначение |
|---|---|
| `sless_function` | Описывает функцию: язык, точку входа, лимиты, переменные окружения. При создании загружает код и запускает его сборку в образ. Сама по себе недоступна снаружи — нужен триггер или задание. |
| `sless_trigger` | Публикует функцию: тип `http` создаёт публичный URL, тип `cron` — запуск по расписанию. |
| `sless_job` | Запускает функцию однократно и ожидает завершения. Используется для одноразовых операций: инициализация БД, миграции, пакетная обработка. |
| `sless_service` | Long-running HTTP-сервис: всегда активен, отвечает на запросы. Имеет свой URL после деплоя. |
| `sless_job` | Одноразовый запуск функции: собирает образ, выполняет код, завершается. Используется для миграций БД, batch-обработки и т.д. |
Стандартная связка для HTTP API: `sless_function` + `sless_trigger` с `type = "http"` — в результате функция доступна по URL вида `https://sless.kube5s.ru/fn/<namespace>/<имя-функции>`.
Namespace функций вычисляется автоматически из JWT-токена: `sless-{sha256[:8]}`.
---
## Требования
- Terraform >= 1.0
- Terraform >= 1.3
- JWT-токен для аутентификации в sless API
- JWT-токен для Nubes Cloud API (если используются managed-ресурсы: PostgreSQL и т.д.)
- Доступ к `https://sless.kube5s.ru`
## Конфигурация провайдера
Во всех примерах файл `main.tf` содержит блок провайдера. Токен передаётся через переменную, значение которой задаётся в `terraform.tfvars`:
```hcl
provider "sless" {
endpoint = "https://sless.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
token = var.sless_token
}
provider "nubes_cloud" {
base_url = "https://deck-api-test.ngcloud.ru/api/v1"
token = var.nubes_token
}
```
Namespace функций вычисляется автоматически из JWT-токена: `sless-{sha256[:8]}`.
> **Перед запуском любого примера** откройте файл `terraform.tfvars` в директории примера и впишите свой токен Nubes API:
> ```hcl
> token = "ваш токен Nubes API"
> ```
> Токен выдаётся в личном кабинете Nubes. Файл `terraform.tfvars` добавлен в `.gitignore` — он не попадёт в репозиторий.
> Токены задаются в `terraform.tfvars` — этот файл добавлен в `.gitignore`.
---
## Примеры
### `hello-node` — минимальный пример на Node.js
### `POSTGRES` — Serverless-функции с Managed PostgreSQL
Две независимые функции: HTTP-функция, возвращающая приветствие, и одноразовое задание, суммирующее набор чисел. Хорошая отправная точка для знакомства с платформой.
Полный пример: managed PostgreSQL + одноразовый init-job + 3 HTTP-сервиса (чтение/запись данных и информация о PG).
Языки: Python 3.11, Node.js 20.
```bash
cd hello-node
cd POSTGRES
terraform init
terraform apply -auto-approve
# Вызов HTTP-функции с передачей имени:
curl -s -X POST https://sless.kube5s.ru/fn/<namespace>/hello-http \
-H 'Content-Type: application/json' -d '{"name":"World"}'
# Результат задания:
terraform output job_message
terraform apply
```
---
### `hello-go` — минимальный пример на Go 1.23
Аналог `hello-node`, но на Go. Демонстрирует поддержку Go-рантайма: HTTP-функция и одноразовое задание. Код пользователя оформляется как пакет `handler` с функцией `Handle(event)`.
```bash
cd hello-go
terraform init
terraform apply -auto-approve
terraform output job_message
terraform output trigger_url
```
---
### `pg-list-python` — выборка данных из PostgreSQL (Python)
Минимальный пример работы с базой данных: одна HTTP-функция читает список записей из таблицы PostgreSQL и возвращает их в JSON. Таблица с тестовыми данными создаётся автоматически при первом вызове. Нет заданий, нет инициализации — только функция и триггер.
**Переменные:**
| Переменная | Описание | Значение по умолчанию |
|---|---|---|
| `pg_dsn` | Строка подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` |
```bash
cd pg-list-python
terraform init
terraform apply -auto-approve
# URL функции выводится после применения:
terraform output catalog_url
# Запрос к функции:
curl -s $(terraform output -raw catalog_url)
```
---
### `simple-python` — одноразовое задание передаёт данные в HTTP-функцию (Python)
При `apply` выполняется задание, которое фиксирует текущее время. Результат передаётся в HTTP-функцию через переменные окружения и отображается при каждом запросе.
```bash
cd simple-python
terraform init
terraform apply -auto-approve
terraform output job_result
curl -s https://sless.kube5s.ru/fn/<namespace>/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.kube5s.ru/fn/<namespace>/simple-node-time-display
```
---
### `notes-python` — CRUD API на Python с PostgreSQL
Полноценное приложение: инициализация схемы базы данных через задания, CRUD-функция для работы с записями, отдельная функция для получения списка.
**Переменные:**
| Переменная | Описание | Значение по умолчанию |
|---|---|---|
| `pg_dsn` | Строка подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` |
```bash
cd notes-python
terraform init
terraform apply -auto-approve
# Статус инициализации базы данных:
terraform output db_init_table_status
terraform output db_init_index_status
# Создать запись:
curl -s -X POST "$(terraform output -raw notes_url)/add?title=Hello&body=World"
# Получить список записей:
curl -s $(terraform output -raw notes_list_url)
# Обновить запись (id из предыдущего ответа):
curl -s -X POST "$(terraform output -raw notes_url)/update?id=1&title=Updated&body=New+body"
# Удалить запись:
curl -s -X POST "$(terraform output -raw notes_url)/delete?id=1"
```
Подробности: [POSTGRES/README.md](POSTGRES/README.md)
---
## Полезные команды
```bash
# Посмотреть текущее состояние задеплоенных ресурсов:
# Посмотреть состояние задеплоенных ресурсов:
terraform show
# Принудительно пересобрать функцию (например, после изменения кода):
terraform apply -replace=sless_function.<имя> -auto-approve
# Принудительно пересобрать сервис (после изменения кода):
terraform apply -replace=sless_service.<имя>
# Повторно запустить задание: увеличить значение run_id в .tf-файле, затем:
terraform apply -auto-approve
# Повторно запустить job: увеличить run_id в .tf-файле, затем:
terraform apply
# Удалить все ресурсы примера:
terraform destroy -auto-approve
```
## Структура примера
```
<пример>/
├── main.tf — конфигурация провайдера
├── *.tf — ресурсы: функции, триггеры, задания
├── variables.tf — входные переменные
├── terraform.tfvars — значения переменных (не коммитится в git)
└── code/ — исходный код функций
terraform destroy
```
@@ -1,94 +0,0 @@
# 2026-03-18 (обновлено: plain text вывод; фильтрация SLESS_EXCLUDE)
# funcs_list.py — HTTP-функция: список пользовательских функций, человекочитаемый plain text.
# Вызывает внутренний REST API оператора (ClusterIP, без TLS).
# Возвращает str → python runtime отдаёт text/plain напрямую без json.dumps.
#
# Env vars:
# SLESS_API_URL — URL оператора (http://sless-operator.sless.svc.cluster.local:9090)
# SLESS_NAMESPACE — namespace пользователя (sless-{hex16})
# SLESS_TOKEN — JWT токен для /v1/ API
# SLESS_EXTERNAL_URL — публичный базовый URL (https://sless.kube5s.ru)
# SLESS_EXCLUDE — comma-separated имена функций, которые не показывать
import os
import requests
SEP = "" * 52
def _comment(fn, http_trigs, cron_trigs):
phase = fn.get("phase", "?")
runtime = fn.get("runtime", "?")
if http_trigs:
active = "активна" if http_trigs[0].get("active") else "неактивна"
return f"HTTP endpoint ({runtime}) — {phase}, {active}"
elif cron_trigs:
schedule = cron_trigs[0].get("schedule", "?")
active = "активна" if cron_trigs[0].get("active") else "неактивна"
return f"Cron '{schedule}' ({runtime}) — {phase}, {active}"
else:
return f"Job/runner без триггера ({runtime}) — {phase}"
def list_all(event):
api_url = os.environ["SLESS_API_URL"].rstrip("/")
namespace = os.environ["SLESS_NAMESPACE"]
token = os.environ["SLESS_TOKEN"]
ext_url = os.environ.get("SLESS_EXTERNAL_URL", "").rstrip("/")
exclude = {n.strip() for n in os.environ.get("SLESS_EXCLUDE", "").split(",") if n.strip()}
headers = {"Authorization": f"Bearer {token}"}
fns = requests.get(f"{api_url}/v1/namespaces/{namespace}/functions", headers=headers, timeout=10)
trs = requests.get(f"{api_url}/v1/namespaces/{namespace}/triggers", headers=headers, timeout=10)
fns.raise_for_status()
trs.raise_for_status()
trig_idx = {}
for tr in trs.json():
fn_name = tr.get("function") or tr.get("functionRef")
if fn_name:
trig_idx.setdefault(fn_name, []).append(tr)
items = []
for fn in fns.json():
name = fn["name"]
if name in exclude:
continue
http_t = [t for t in trig_idx.get(name, []) if t.get("type") == "http"]
cron_t = [t for t in trig_idx.get(name, []) if t.get("type") == "cron"]
is_active = any(t.get("enabled", True) and t.get("active", False) for t in trig_idx.get(name, []))
items.append((fn, http_t, cron_t, is_active))
# Сортировка: активные вверх, затем по имени
items.sort(key=lambda x: (not x[3], x[0]["name"]))
lines = []
for fn, http_t, cron_t, is_active in items:
name = fn["name"]
lines.append(SEP)
lines.append(f" {_comment(fn, http_t, cron_t)}")
lines.append(f" name: {name}")
lines.append(f" runtime: {fn.get('runtime', '?')}")
lines.append(f" phase: {fn.get('phase', '?')}")
lines.append(f" active: {'да' if is_active else 'нет'}")
if http_t:
url = f"{ext_url}/fn/{namespace}/{name}" if ext_url else http_t[0].get("url", "")
lines.append(f" url: {url}")
if cron_t:
lines.append(f" cron: {cron_t[0].get('schedule', '?')}")
if fn.get("created_at"):
lines.append(f" created: {fn['created_at']}")
if fn.get("last_built_at"):
lines.append(f" built: {fn['last_built_at']}")
if fn.get("message"):
lines.append(f" message: {fn['message']}")
lines.append(SEP)
lines.append(f" namespace: {namespace} | total: {len(items)}")
lines.append(SEP)
# Возвращаем str — python runtime отдаст text/plain напрямую
return "\n".join(lines) + "\n"
@@ -1 +0,0 @@
requests==2.31.0
-8
View File
@@ -1,8 +0,0 @@
{
"name": "pg-info",
"version": "1.0.0",
"description": "sless nodejs20 function: pg version + table info",
"dependencies": {
"pg": "8.11.0"
}
}
-43
View File
@@ -1,43 +0,0 @@
// 2026-03-18
// pg_info.js — NodeJS-функция: проверка работы JS runtime + чтение мета-данных БД.
// Подключается к PostgreSQL через пакет pg, возвращает версию сервера и счётчик строк.
// Демонстрирует: nodejs20 runtime, npm-зависимость (package.json), PG из JS.
//
// ENV (те же что у python-функций):
// PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE
//
// Entrypoint: pg_info.info
'use strict';
const { Client } = require('pg');
exports.info = async (event) => {
const client = new Client({
host: process.env.PGHOST,
port: parseInt(process.env.PGPORT || '5432'),
database: process.env.PGDATABASE,
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
// pg-пакет требует явного ssl-объекта; rejectUnauthorized: false — т.к.
// self-signed cert на nubes managed PG, но канал всё равно шифруется.
ssl: process.env.PGSSLMODE === 'require' ? { rejectUnauthorized: false } : false,
});
await client.connect();
try {
const [versionRes, countRes] = await Promise.all([
client.query('SELECT version() AS v'),
client.query('SELECT COUNT(*) AS cnt FROM terraform_demo_table'),
]);
return {
runtime: 'nodejs20',
node_version: process.version,
pg_version: versionRes.rows[0].v,
table_rows: parseInt(countRes.rows[0].cnt, 10),
};
} finally {
await client.end();
}
};
@@ -1,3 +0,0 @@
# 2026-03-17 00:00
# requirements.txt — зависимости для функции запуска SQL.
psycopg2-binary==2.9.9
@@ -1,39 +0,0 @@
# 2026-03-17 00:00
# sql_runner.py — функция для выполнения SQL-операторов из входного события.
import os
import psycopg2
def run_sql(event):
# Выполняет список SQL-операторов в одной транзакции для атомарной инициализации схемы.
# Параметры подключения передаются раздельно, чтобы избежать ошибок парсинга DSN при спецсимволах.
pg_host = os.environ["PGHOST"]
pg_port = os.environ.get("PGPORT", "5432")
pg_database = os.environ["PGDATABASE"]
pg_user = os.environ["PGUSER"]
pg_password = os.environ["PGPASSWORD"]
pg_sslmode = os.environ.get("PGSSLMODE", "require")
statements = event.get("statements", [])
if not statements:
return {"error": "no statements provided"}
connection = psycopg2.connect(
host=pg_host,
port=pg_port,
dbname=pg_database,
user=pg_user,
password=pg_password,
sslmode=pg_sslmode,
)
try:
cursor = connection.cursor()
for statement in statements:
cursor.execute(statement)
connection.commit()
return {"ok": True, "executed": len(statements)}
except Exception as error:
connection.rollback()
return {"error": str(error)}
finally:
connection.close()
@@ -1 +0,0 @@
psycopg2-binary==2.9.9
-130
View File
@@ -1,130 +0,0 @@
# 2026-03-19
# table_rw.py — чтение и запись строк в terraform_demo_table.
# Два entrypoint в одном файле: list_rows (JSON API) и add_row (HTML-страница + POST-обработчик).
# ENV: PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE
import os
import json
import psycopg2
import psycopg2.extras
def _connect():
return psycopg2.connect(
host=os.environ["PGHOST"],
port=os.environ.get("PGPORT", "5432"),
dbname=os.environ["PGDATABASE"],
user=os.environ["PGUSER"],
password=os.environ["PGPASSWORD"],
sslmode=os.environ.get("PGSSLMODE", "require"),
)
def list_rows(event):
# Возвращает все строки terraform_demo_table, отсортированные по убыванию created_at.
conn = _connect()
try:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute(
"SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC"
)
rows = [dict(r) for r in cur.fetchall()]
return {"rows": rows, "count": len(rows)}
finally:
conn.close()
def _render_page(rows, message=""):
# HTML-страница с формой ввода и таблицей строк.
# message — статус последней операции (успех / ошибка).
rows_html = "".join(
f"<tr><td>{r['id']}</td><td>{r['title']}</td><td>{r['created_at']}</td></tr>"
for r in rows
)
msg_html = f'<p class="msg">{message}</p>' if message else ""
return f"""<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<title>pg-table-writer</title>
<style>
body {{ font-family: sans-serif; max-width: 700px; margin: 40px auto; background: #111; color: #eee; }}
h1 {{ color: #7dd3fc; }}
form {{ display: flex; gap: 8px; margin-bottom: 24px; }}
input[type=text] {{ flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid #444; background: #1e1e1e; color: #eee; font-size: 15px; }}
button {{ padding: 8px 18px; background: #2563eb; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; }}
button:hover {{ background: #1d4ed8; }}
table {{ width: 100%; border-collapse: collapse; }}
th, td {{ padding: 8px 10px; border-bottom: 1px solid #333; text-align: left; }}
th {{ color: #7dd3fc; }}
.msg {{ color: #4ade80; margin-bottom: 12px; }}
</style>
</head>
<body>
<h1>pg-table-writer</h1>
<form method="POST">
<input type="text" name="title" placeholder="Введите строку..." autofocus required>
<button type="submit">Добавить</button>
</form>
{msg_html}
<table>
<thead><tr><th>#</th><th>title</th><th>created_at</th></tr></thead>
<tbody>{rows_html}</tbody>
</table>
</body>
</html>"""
def add_row(event):
# GET → HTML-страница с формой и списком строк.
# POST → вставляет строку из form-поля title или JSON-поля title,
# затем возвращает обновлённую HTML-страницу.
# POST с Content-Type: application/json (curl/API) → возвращает JSON.
method = event.get("_method", "GET")
if method == "GET":
conn = _connect()
try:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute("SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC")
rows = [dict(r) for r in cur.fetchall()]
finally:
conn.close()
return _render_page(rows)
# POST — вставка строки
# Поле title приходит либо из JSON-тела, либо из application/x-www-form-urlencoded.
# Сервер уже распарсил JSON в event; form-данные приходят как event["body"] = "title=...".
title = event.get("title", "").strip()
if not title:
# Попытка распарсить form-encoded body (браузерная форма)
body = event.get("body", "")
if body.startswith("title="):
from urllib.parse import unquote_plus
title = unquote_plus(body[len("title="):].split("&")[0]).strip()
if not title:
return {"ok": False, "error": "title is required"}
conn = _connect()
try:
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
cur.execute(
"INSERT INTO terraform_demo_table (title) VALUES (%s) RETURNING id, title, created_at::text",
(title,),
)
row = dict(cur.fetchone())
conn.commit()
# Если запрос из браузера (form POST) — возвращаем обновлённую страницу.
# Если из curl/API — возвращаем JSON.
accept = event.get("_accept", "")
if "application/json" in accept:
return {"ok": True, "row": row}
# Перечитываем все строки для обновлённой страницы
cur.execute("SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC")
rows = [dict(r) for r in cur.fetchall()]
return _render_page(rows, message=f"Добавлено: «{row['title']}»")
finally:
conn.close()
-129
View File
@@ -1,129 +0,0 @@
# 2026-03-18 (обновлено: фильтрация SLESS_EXCLUDE, читаемый вывод через "#"-ключ)
# funcs_list.py — HTTP-функция: список всех пользовательских функций с их статусами.
# Вызывает внутренний REST API оператора (ClusterIP, без TLS).
# Объединяет данные функций и триггеров в один ответ; скрывает служебные функции.
#
# Env vars:
# SLESS_API_URL — URL оператора (http://sless-operator.sless.svc.cluster.local:9090)
# SLESS_NAMESPACE — namespace пользователя (sless-{hex16})
# SLESS_TOKEN — JWT токен для /v1/ API
# SLESS_EXTERNAL_URL — публичный базовый URL (https://sless.kube5s.ru), для корректных ссылок
# SLESS_EXCLUDE — comma-separated имена функций, которые не надо показывать
# Пример: "funcs,event-writer,event-monitor,event-cleaner"
#
# Формат вывода: JSON-объект, где каждая функция содержит поле "#" — краткий комментарий.
# При pretty-print (python3 -m json.tool) выглядит как читаемый список с аннотациями.
import os
import requests
def _short_comment(fn, http_triggers, cron_triggers):
"""Генерирует однострочный комментарий-описание функции по её метаданным."""
phase = fn.get("phase", "")
runtime = fn.get("runtime", "")
if http_triggers:
active_str = "активна" if http_triggers[0].get("active") else "неактивна"
return f"HTTP endpoint ({runtime}) — {phase}, {active_str}"
elif cron_triggers:
schedule = cron_triggers[0].get("schedule", "?")
active_str = "активна" if cron_triggers[0].get("active") else "неактивна"
return f"Cron '{schedule}' ({runtime}) — {phase}, {active_str}"
else:
return f"Job/runner без триггера ({runtime}) — {phase}"
def list_all(event):
api_url = os.environ["SLESS_API_URL"].rstrip("/")
namespace = os.environ["SLESS_NAMESPACE"]
token = os.environ["SLESS_TOKEN"]
ext_url = os.environ.get("SLESS_EXTERNAL_URL", "").rstrip("/")
# Имена функций, которые не должны присутствовать в выводе.
# Включает саму себя ("funcs") и служебные функции других примеров.
exclude = {
n.strip()
for n in os.environ.get("SLESS_EXCLUDE", "").split(",")
if n.strip()
}
headers = {"Authorization": f"Bearer {token}"}
fns_resp = requests.get(
f"{api_url}/v1/namespaces/{namespace}/functions",
headers=headers,
timeout=10,
)
fns_resp.raise_for_status()
trs_resp = requests.get(
f"{api_url}/v1/namespaces/{namespace}/triggers",
headers=headers,
timeout=10,
)
trs_resp.raise_for_status()
# Индекс триггеров по имени функции
triggers_by_fn = {}
for tr in trs_resp.json():
fn_name = tr.get("function") or tr.get("functionRef")
if fn_name:
triggers_by_fn.setdefault(fn_name, []).append(tr)
result = []
for fn in fns_resp.json():
name = fn["name"]
if name in exclude:
continue
http_triggers = [
t for t in triggers_by_fn.get(name, []) if t.get("type") == "http"
]
cron_triggers = [
t for t in triggers_by_fn.get(name, []) if t.get("type") == "cron"
]
is_active = any(
t.get("enabled", True) and t.get("active", False)
for t in triggers_by_fn.get(name, [])
)
entry = {
# "#" — первый ключ: служит визуальным комментарием при pretty-print
"#": _short_comment(fn, http_triggers, cron_triggers),
"name": name,
"runtime": fn.get("runtime"),
"phase": fn.get("phase"),
"active": is_active,
}
# URL вычисляем из SLESS_EXTERNAL_URL если задан — state может хранить старый домен
if http_triggers:
if ext_url:
entry["url"] = f"{ext_url}/fn/{namespace}/{name}"
else:
entry["url"] = http_triggers[0].get("url", "")
if cron_triggers:
entry["cron"] = cron_triggers[0].get("schedule", "")
if fn.get("message"):
entry["message"] = fn["message"]
# created_at и last_built_at — доступны после обновления оператора до v0.1.32+
if fn.get("created_at"):
entry["created_at"] = fn["created_at"]
if fn.get("last_built_at"):
entry["last_built_at"] = fn["last_built_at"]
result.append(entry)
# Сортировка: активные вверх, затем по имени
result.sort(key=lambda f: (not f["active"], f["name"]))
return {
"namespace": namespace,
"count": len(result),
"functions": result,
}
-73
View File
@@ -1,73 +0,0 @@
# resource "nubes_lucee" "app1" {
# # Lucee-приложение, зависит от Postgres
# resource_name = "lucy_teststand_0"
# # resource_realm = "k8s-3.ext.nubes.ru"
# resource_realm = nubes_postgres.db2.resource_realm
# # resource_realm = "k8s-4-sandbox-nubes-ru"
# domain = "web-test-stand"
# git_path = "https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/testlucee"
# json_env = jsonencode({
# # 🔗 Настройки Data Source 'testds' для Lucee (Application.cfc)
# testds_class = "org.postgresql.Driver" # 📂 Драйвер БД
# testds_bundleName = "org.postgresql.jdbc" # 📦 Имя бандла JDBC
# testds_bundleVersion = "42.6.0" # 🔢 Версия драйвера
# testds_connectionString = "jdbc:postgresql://${nubes_postgres.db2.state_out_flat["internalConnect.master"]}:5432/postgres?sslmode=require" # 🚀 Строка подключения
# testds_username = nubes_postgres_user.db2_user.username # 👤 Логин
# testds_password = jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"] # 🔑 Пароль
# testds_connectionLimit = "5" # 🚦 Лимит соединений
# testds_liveTimeout = "15" # ⏳ Таймаут жизни
# testds_validate = "false" # ✅ Валидация при запросе
# })
# resource_c_p_u = 300
# resource_memory = 512
# resource_instances = 1
# app_version = "5.4"
# depends_on = [nubes_postgres.db2]
# }
# resource "nubes_nodejs" "app3" {
# # NodeJS демо, работающий с тем же Postgres.
# resource_name = "node_01"
# resource_realm = nubes_postgres.db2.resource_realm
# domain = "node07"
# git_path = "https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/testnode.git"
# health_path = "/healthz"
# app_version = "23"
# json_env = jsonencode({
# # Переменные подключения к Postgres.
# PGHOST = nubes_postgres.db2.state_out_flat["internalConnect.master"]
# PGPORT = "5432"
# PGUSER = nubes_postgres_user.db2_user.username
# PGPASSWORD = jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"]
# PGDATABASE = nubes_postgres_database.db2_app.db_name
# PGSSLMODE = "require"
# DATABASE_URL = format(
# "postgresql://%s:%s@%s:5432/%s?sslmode=require",
# nubes_postgres_user.db2_user.username,
# jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"],
# nubes_postgres.db2.state_out_flat["internalConnect.master"],
# nubes_postgres_database.db2_app.db_name
# )
# })
# resource_c_p_u = 300
# resource_memory = 256
# resource_instances = 1
# depends_on = [nubes_postgres.db2]
# }
# output "pg_vault_secrets" {
# value = nubes_postgres.db2.vault_secrets
# sensitive = true
# }
# terraform output -json pg_vault_secrets
-58
View File
@@ -1,58 +0,0 @@
// 2026-03-17 17:05
// main.tf — провайдеры и переменные для Nubes + sless.
terraform {
required_providers {
nubes = {
source = "terra.k8c.ru/nubes/nubes"
version = "5.0.19"
}
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.18"
}
}
}
variable "api_token" {
type = string
sensitive = true
description = "Nubes API token"
}
variable "s3_uid" {
type = string
sensitive = true
description = "Nubes S3 UID"
}
variable "realm" {
type = string
sensitive = true
description = "resource_realm parameter for nubes_postgres resource"
}
// 2026-03-18 — pg_user/pg_password помечены optional (default="") для сверки.
// Реальные credentials берутся из vault_secrets через locals в resources.tf.
variable "pg_user" {
type = string
sensitive = true
default = ""
description = "Только для сверки. Реальный username из nubes_postgres_user.pg_user.username. Должен совпадать с vault."
}
variable "pg_password" {
type = string
sensitive = true
default = ""
description = "Только для сверки. Реальный пароль из vault_secrets. Должен совпадать с tfvars."
}
provider "nubes" {
api_token = var.api_token
api_endpoint = "https://deck-test.ngcloud.ru/api/v1/index.cfm"
}
provider "sless" {
endpoint = "https://sless.kube5s.ru"
token = var.api_token
nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1"
}
-196
View File
@@ -1,196 +0,0 @@
// 2026-03-18 — добавлены locals для извлечения credentials из vault_secrets (без хардкода).
// Для сверки хардкод остаётся в terraform.tfvars на этапе разработки.
// sless_function и sless_job закомментированы — сначала проверяется сетевое соединение.
# Актуальные credentials из vault_secrets (authoritatively) — vault синхронизирован с кластером.
# Структура vault_secrets["users"]: JSON-строка {"username": {"password": "...", "username": "..."}}
locals {
pg_creds_map = jsondecode(nubes_postgres.npg.vault_secrets["users"])
pg_username = nubes_postgres_user.pg_user.username
pg_password = local.pg_creds_map[local.pg_username]["password"]
pg_host = nubes_postgres.npg.state_out_flat["internalConnect.master"]
pg_database = nubes_postgres_database.db.db_name
}
resource "nubes_postgres" "npg" {
resource_name = "testnarod-pg-0"
# s3_uid = "s01325"
s3_uid = var.s3_uid
resource_realm = var.realm
resource_instances = 1
resource_memory = 512
resource_c_p_u = 500
resource_disk = "1"
app_version = "17"
json_parameters = jsonencode({
log_connections = "off"
log_disconnections = "off"
})
enable_pg_pooler_master = false
enable_pg_pooler_slave = false
allow_no_s_s_l = false
auto_scale = false
auto_scale_percentage = 10
auto_scale_tech_window = 0
auto_scale_quota_gb = "1"
need_external_address_master = false
# suspend_on_destroy = false
operation_timeout = "11m"
adopt_existing_on_create = true
}
resource "nubes_postgres_user" "pg_user" {
postgres_id = nubes_postgres.npg.id
username = "u-user0"
role = "ddl_user"
adopt_existing_on_create = true
}
resource "nubes_postgres_database" "db" {
postgres_id = nubes_postgres.npg.id
db_name = "db_terra"
db_owner = nubes_postgres_user.pg_user.username
adopt_existing_on_create = true
# suspend_on_destroy = false
}
# Служебная функция выполняет SQL-операторы из event_json.
# Credentials берутся из locals (vault_secrets) — без хардкода.
# Для сверки хардкод остаётся в terraform.tfvars.
resource "sless_function" "postgres_sql_runner_create_table" {
name = "pg-create-table-runner"
runtime = "python3.11"
entrypoint = "sql_runner.run_sql"
memory_mb = 128
timeout_sec = 30
env_vars = {
PGHOST = local.pg_host
PGPORT = "5432"
PGDATABASE = local.pg_database
PGUSER = local.pg_username
PGPASSWORD = local.pg_password
PGSSLMODE = "require"
# Для сверки (должно совпадать с vault):
# PGUSER = var.pg_user
# PGPASSWORD = var.pg_password
}
source_dir = "${path.module}/code/sql-runner"
}
resource "sless_job" "postgres_table_init_job" {
name = "pg-create-table-job-main-v13"
function = sless_function.postgres_sql_runner_create_table.name
wait_timeout_sec = 180
run_id = 13
event_json = jsonencode({
statements = [
"CREATE TABLE IF NOT EXISTS terraform_demo_table (id serial PRIMARY KEY, title text NOT NULL, created_at timestamp DEFAULT now())"
]
})
depends_on = [nubes_postgres_database.db]
}
# HTTP-функция на NodeJS: возвращает версию PG-сервера и счётчик строк в таблице.
# Единственная функция примера на nodejs20 — проверка что JS runtime работает.
# Доступна по URL: https://sless.kube5s.ru/fn/<namespace>/pg-info
resource "sless_function" "pg_info" {
name = "pg-info"
runtime = "nodejs20"
entrypoint = "pg_info.info"
memory_mb = 128
timeout_sec = 15
env_vars = {
PGHOST = local.pg_host
PGPORT = "5432"
PGDATABASE = local.pg_database
PGUSER = local.pg_username
PGPASSWORD = local.pg_password
PGSSLMODE = "require"
}
source_dir = "${path.module}/code/pg-info"
depends_on = [sless_job.postgres_table_init_job]
}
resource "sless_trigger" "pg_info_http" {
name = "pg-info-http"
type = "http"
function = sless_function.pg_info.name
enabled = true
}
# HTTP-функции чтения и записи строк terraform_demo_table — в одном файле table_rw.py.
# list_rows (GET) — читает все строки; add_row (POST {title}) — вставляет строку.
# Доступны по URL: https://sless.kube5s.ru/fn/<namespace>/pg-table-reader
# https://sless.kube5s.ru/fn/<namespace>/pg-table-writer
resource "sless_function" "postgres_table_reader" {
name = "pg-table-reader"
runtime = "python3.11"
entrypoint = "table_rw.list_rows"
memory_mb = 128
timeout_sec = 30
env_vars = {
PGHOST = local.pg_host
PGPORT = "5432"
PGDATABASE = local.pg_database
PGUSER = local.pg_username
PGPASSWORD = local.pg_password
PGSSLMODE = "require"
}
source_dir = "${path.module}/code/table-rw"
depends_on = [sless_job.postgres_table_init_job]
}
resource "sless_trigger" "postgres_table_reader_http" {
name = "pg-table-reader-http"
type = "http"
function = sless_function.postgres_table_reader.name
enabled = true
}
output "table_reader_url" {
value = sless_trigger.postgres_table_reader_http.url
}
resource "sless_function" "postgres_table_writer" {
name = "pg-table-writer"
runtime = "python3.11"
entrypoint = "table_rw.add_row"
memory_mb = 128
timeout_sec = 30
env_vars = {
PGHOST = local.pg_host
PGPORT = "5432"
PGDATABASE = local.pg_database
PGUSER = local.pg_username
PGPASSWORD = local.pg_password
PGSSLMODE = "require"
}
source_dir = "${path.module}/code/table-rw"
depends_on = [sless_job.postgres_table_init_job]
}
resource "sless_trigger" "postgres_table_writer_http" {
name = "pg-table-writer-http"
type = "http"
function = sless_function.postgres_table_writer.name
enabled = true
}
output "table_writer_url" {
value = sless_trigger.postgres_table_writer_http.url
}
-51
View File
@@ -1,51 +0,0 @@
# 2026-03-18 — debug pod для проверки psql-соединения из namespace функций.
# Запускается разово. Подключается к тому же postgres, что и sless_function.
# kubectl apply -f /tmp/pg-debug-pod.yaml
# kubectl logs -n sless-fn-sless-ffd1f598c169b0ae pg-debug-pod
apiVersion: v1
kind: Pod
metadata:
name: pg-debug-pod
namespace: sless-fn-sless-ffd1f598c169b0ae
labels:
purpose: debug-postgres-connectivity
spec:
restartPolicy: Never
containers:
- name: psql
image: postgres:17-alpine
command:
- sh
- -c
- |
echo "=== Testing TCP connectivity to postgres ==="
nc -zv -w5 $PGHOST 5432 && echo "TCP OK" || echo "TCP FAILED"
echo ""
echo "=== Testing psql connection ==="
PGCONNECT_TIMEOUT=10 psql \
"host=$PGHOST port=$PGPORT dbname=$PGDATABASE user=$PGUSER sslmode=$PGSSLMODE" \
--command="SELECT current_user, current_database(), version();" \
2>&1
echo ""
echo "=== Listing tables ==="
PGCONNECT_TIMEOUT=10 psql \
"host=$PGHOST port=$PGPORT dbname=$PGDATABASE user=$PGUSER sslmode=$PGSSLMODE" \
--command="\dt" \
2>&1
env:
- name: PGHOST
value: "postgresqlk8s-master.36875359-dcea-48c4-a593-b4531f20fe96.svc.cluster.local"
- name: PGPORT
value: "5432"
- name: PGDATABASE
value: "db_terra"
- name: PGUSER
value: "u-user0"
- name: PGPASSWORD
# Актуальный пароль из vault_secrets (совпадает с tfvars.pg_password на 2026-03-18)
value: "M03O6fRsngWcVHB2YGivyLfbfxoii2R21nyh2A2r7WSZS5deLwBgLKkc9Wk24Zyl"
- name: PGSSLMODE
value: "require"
@@ -1,40 +0,0 @@
# 2026-03-17 13:05
# read_pg_user_secret.py — читает пароль пользователя managed PostgreSQL из k8s Secret.
# Используется из Terraform external data source, чтобы apply сам получал актуальный пароль
# даже для уже существующего пользователя, созданного вне текущего state.
import base64
import json
import subprocess
import sys
def main():
# Читаем query от Terraform external provider из stdin.
query = json.load(sys.stdin)
namespace = query["namespace"]
secret_name = query["secret"]
# kubectl уже настроен на удалённой машине; читаем ровно поле data.password.
result = subprocess.run(
[
"kubectl",
"get",
"secret",
"-n",
namespace,
secret_name,
"-o",
"jsonpath={.data.password}",
],
check=True,
capture_output=True,
text=True,
)
password = base64.b64decode(result.stdout.strip()).decode()
json.dump({"password": password}, sys.stdout)
if __name__ == "__main__":
main()
@@ -1,45 +0,0 @@
# Изменено: 2026-03-14
# Функция event-cleaner: удаляет N самых старых строк из таблицы events.
# Вызывается через HTTP POST из Node-RED (который слушает RabbitMQ).
# Env: POSTGRES_DSN — строка подключения к PostgreSQL.
import os
import json
import psycopg2
def handle(request):
"""Удаляет N старейших строк из таблицы events."""
dsn = os.environ["POSTGRES_DSN"]
body = {}
if request.get_data():
try:
body = json.loads(request.get_data())
except Exception:
pass
# Количество строк для удаления — из тела запроса или дефолт 10
delete_n = int(body.get("delete_n", 10))
# Защита от случайного удаления слишком большого количества строк
delete_n = min(delete_n, 100)
conn = psycopg2.connect(dsn)
try:
with conn.cursor() as cur:
cur.execute("""
DELETE FROM events
WHERE id IN (
SELECT id FROM events ORDER BY created_at ASC LIMIT %s
)
""", (delete_n,))
deleted = cur.rowcount
cur.execute("SELECT COUNT(*) FROM events")
remaining = cur.fetchone()[0]
conn.commit()
return json.dumps({
"ok": True,
"deleted": deleted,
"remaining": remaining
}), 200, {"Content-Type": "application/json"}
finally:
conn.close()
@@ -1,47 +0,0 @@
# Изменено: 2026-03-14
# Функция event-monitor: считает строки в events.
# Если больше 50 — публикует сообщение в RabbitMQ queue "cleanup-needed".
# Запускается по cron (каждую минуту).
# Env:
# POSTGRES_DSN — строка подключения к PostgreSQL
# RABBITMQ_URL — amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/
import os
import json
import psycopg2
import pika
THRESHOLD = 50
def handle(request):
"""Мониторит таблицу events. При переполнении шлёт в RabbitMQ."""
dsn = os.environ["POSTGRES_DSN"]
rabbit_url = os.environ["RABBITMQ_URL"]
conn = psycopg2.connect(dsn)
try:
with conn.cursor() as cur:
# Считаем количество событий
cur.execute("SELECT COUNT(*) FROM events")
count = cur.fetchone()[0]
finally:
conn.close()
result = {"count": count, "threshold": THRESHOLD, "action": "none"}
if count > THRESHOLD:
# Публикуем в очередь — event-cleaner получит и удалит старые строки
params = pika.URLParameters(rabbit_url)
connection = pika.BlockingConnection(params)
channel = connection.channel()
channel.queue_declare(queue="cleanup-needed", durable=True)
channel.basic_publish(
exchange="",
routing_key="cleanup-needed",
body=json.dumps({"count": count, "delete_n": 10}),
properties=pika.BasicProperties(delivery_mode=2) # persistent
)
connection.close()
result["action"] = "cleanup_requested"
return json.dumps(result), 200, {"Content-Type": "application/json"}
@@ -1,49 +0,0 @@
# Изменено: 2026-03-14
# Функция event-writer: принимает HTTP POST, пишет одну строку в таблицу events.
# Таблица создаётся автоматически при первом запуске.
# Env: POSTGRES_DSN — строка подключения к PostgreSQL.
import os
import json
import psycopg2
from datetime import datetime, timezone
def handle(request):
"""Записывает одно событие в таблицу events."""
dsn = os.environ["POSTGRES_DSN"]
body = {}
if request.get_data():
try:
body = json.loads(request.get_data())
except Exception:
pass
source = body.get("source", "node-red")
message = body.get("message", "ping")
conn = psycopg2.connect(dsn)
try:
with conn.cursor() as cur:
# Создаём таблицу если нет — безопасно вызывать при каждом запросе
cur.execute("""
CREATE TABLE IF NOT EXISTS events (
id SERIAL PRIMARY KEY,
source VARCHAR(100) NOT NULL DEFAULT 'unknown',
message TEXT NOT NULL DEFAULT '',
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
)
""")
cur.execute(
"INSERT INTO events (source, message) VALUES (%s, %s) RETURNING id, created_at",
(source, message)
)
row = cur.fetchone()
conn.commit()
return json.dumps({
"ok": True,
"id": row[0],
"created_at": row[1].isoformat()
}), 200, {"Content-Type": "application/json"}
finally:
conn.close()
-194
View File
@@ -1,194 +0,0 @@
# Примеры использования sless
## Обзор платформы
**sless** — система управления serverless-функциями на базе Kubernetes. Разработчик загружает код функции, платформа собирает из него Docker-образ, разворачивает его в кластере и предоставляет HTTP-эндпоинт для вызова. Всё описывается декларативно через Terraform.
### Основные ресурсы провайдера
| Ресурс | Назначение |
|---|---|
| `sless_function` | Описывает функцию: язык, точку входа, лимиты, переменные окружения. При создании загружает код и запускает его сборку в образ. Сама по себе недоступна снаружи — нужен триггер или задание. |
| `sless_trigger` | Публикует функцию: тип `http` создаёт публичный URL, тип `cron` — запуск по расписанию. |
| `sless_job` | Запускает функцию однократно и ожидает завершения. Используется для одноразовых операций: инициализация БД, миграции, пакетная обработка. |
Стандартная связка для HTTP API: `sless_function` + `sless_trigger` с `type = "http"` — в результате функция доступна по URL вида `https://sless.kube5s.ru/fn/<namespace>/<имя-функции>`.
---
## Требования
- Terraform >= 1.0
- JWT-токен для аутентификации в sless API
- Доступ к `https://sless.kube5s.ru`
## Конфигурация провайдера
Во всех примерах файл `main.tf` содержит блок провайдера. Токен передаётся через переменную, значение которой задаётся в `terraform.tfvars`:
```hcl
provider "sless" {
endpoint = "https://sless.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
}
```
Namespace функций вычисляется автоматически из JWT-токена: `sless-{sha256[:8]}`.
> **Перед запуском любого примера** откройте файл `terraform.tfvars` в директории примера и впишите свой токен Nubes API:
> ```hcl
> token = "ваш токен Nubes API"
> ```
> Токен выдаётся в личном кабинете Nubes. Файл `terraform.tfvars` добавлен в `.gitignore` — он не попадёт в репозиторий.
---
## Примеры
### `hello-node` — минимальный пример на Node.js
Две независимые функции: HTTP-функция, возвращающая приветствие, и одноразовое задание, суммирующее набор чисел. Хорошая отправная точка для знакомства с платформой.
```bash
cd hello-node
terraform init
terraform apply -auto-approve
# Вызов HTTP-функции с передачей имени:
curl -s -X POST https://sless.kube5s.ru/fn/<namespace>/hello-http \
-H 'Content-Type: application/json' -d '{"name":"World"}'
# Результат задания:
terraform output job_message
```
---
### `hello-go` — минимальный пример на Go 1.23
Аналог `hello-node`, но на Go. Демонстрирует поддержку Go-рантайма: HTTP-функция и одноразовое задание. Код пользователя оформляется как пакет `handler` с функцией `Handle(event)`.
```bash
cd hello-go
terraform init
terraform apply -auto-approve
terraform output job_message
terraform output trigger_url
```
---
### `pg-list-python` — выборка данных из PostgreSQL (Python)
Минимальный пример работы с базой данных: одна HTTP-функция читает список записей из таблицы PostgreSQL и возвращает их в JSON. Таблица с тестовыми данными создаётся автоматически при первом вызове. Нет заданий, нет инициализации — только функция и триггер.
**Переменные:**
| Переменная | Описание | Значение по умолчанию |
|---|---|---|
| `pg_dsn` | Строка подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` |
```bash
cd pg-list-python
terraform init
terraform apply -auto-approve
# URL функции выводится после применения:
terraform output catalog_url
# Запрос к функции:
curl -s $(terraform output -raw catalog_url)
```
---
### `simple-python` — одноразовое задание передаёт данные в HTTP-функцию (Python)
При `apply` выполняется задание, которое фиксирует текущее время. Результат передаётся в HTTP-функцию через переменные окружения и отображается при каждом запросе.
```bash
cd simple-python
terraform init
terraform apply -auto-approve
terraform output job_result
curl -s https://sless.kube5s.ru/fn/<namespace>/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.kube5s.ru/fn/<namespace>/simple-node-time-display
```
---
### `notes-python` — CRUD API на Python с PostgreSQL
Полноценное приложение: инициализация схемы базы данных через задания, CRUD-функция для работы с записями, отдельная функция для получения списка.
**Переменные:**
| Переменная | Описание | Значение по умолчанию |
|---|---|---|
| `pg_dsn` | Строка подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` |
```bash
cd notes-python
terraform init
terraform apply -auto-approve
# Статус инициализации базы данных:
terraform output db_init_table_status
terraform output db_init_index_status
# Создать запись:
curl -s -X POST "$(terraform output -raw notes_url)/add?title=Hello&body=World"
# Получить список записей:
curl -s $(terraform output -raw notes_list_url)
# Обновить запись (id из предыдущего ответа):
curl -s -X POST "$(terraform output -raw notes_url)/update?id=1&title=Updated&body=New+body"
# Удалить запись:
curl -s -X POST "$(terraform output -raw notes_url)/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 — ресурсы: функции, триггеры, задания
├── variables.tf — входные переменные
├── terraform.tfvars — значения переменных (не коммитится в git)
└── code/ — исходный код функций
```
@@ -1,26 +0,0 @@
# Изменено: 2026-03-14
# event-cleaner: HTTP функция для демонстрации контролируемого изменения логики.
# Работает без внешних пакетов.
import json
from datetime import datetime, timezone
def event_cleaner_handle(event_cleaner_event):
"""Принимает delete_n и возвращает подтверждение обработки."""
event_cleaner_payload = event_cleaner_event if isinstance(event_cleaner_event, dict) else {}
event_cleaner_delete_n = int(event_cleaner_payload.get("delete_n", 10))
event_cleaner_delete_n = max(1, min(event_cleaner_delete_n, 100))
event_cleaner_generated_at = datetime.now(timezone.utc).isoformat()
# Здесь intentionally имитируем очистку, чтобы показать реакцию на входные параметры.
return json.dumps(
{
"ok": True,
"accepted_delete_n": event_cleaner_delete_n,
"status": "simulated-cleanup",
"generated_at": event_cleaner_generated_at,
}
), 200, {"Content-Type": "application/json"}
@@ -1,25 +0,0 @@
# Изменено: 2026-03-14
# event-monitor: cron-функция для демонстрации расписания и управления кодом.
# Работает без внешних библиотек и возвращает диагностический JSON.
import json
import os
from datetime import datetime, timezone
EVENT_MONITOR_THRESHOLD = 50
def event_monitor_handle(event_monitor_event):
"""Отдаёт heartbeat для cron-запуска и видимой проверки после apply."""
event_monitor_rabbitmq_url = os.environ.get("RABBITMQ_URL", "not-set")
event_monitor_generated_at = datetime.now(timezone.utc).isoformat()
return json.dumps(
{
"ok": True,
"monitor": "alive",
"threshold": EVENT_MONITOR_THRESHOLD,
"rabbitmq_configured": event_monitor_rabbitmq_url != "not-set",
"generated_at": event_monitor_generated_at,
}
), 200, {"Content-Type": "application/json"}
@@ -1,28 +0,0 @@
# Изменено: 2026-03-14
# event-writer: простая HTTP функция без внешних зависимостей.
# Правка поля response_tag в коде сразу видна в ответе после terraform apply.
import json
import os
from datetime import datetime, timezone
def event_writer_handle(event_writer_event):
"""Возвращает полезный JSON-ответ для визуальной проверки выката кода."""
writer_default_message = os.environ.get("DEFAULT_MESSAGE", "writer-default")
writer_payload = event_writer_event if isinstance(event_writer_event, dict) else {}
writer_message_value = writer_payload.get("message", writer_default_message)
writer_source_name = writer_payload.get("source", "event-writer")
writer_generated_at = datetime.now(timezone.utc).isoformat()
# response_tag удобно менять для демонстрации hot-update кода через terraform apply.
writer_response_tag = "writer-v2"
return {
"ok": True,
"source": writer_source_name,
"message": writer_message_value,
"response_tag": writer_response_tag,
"generated_at": writer_generated_at,
}
@@ -1,77 +0,0 @@
# 2026-03-14
# function.tf — ресурсы managed serverless функций для демонстрации.
# Пользователь правит код в code/* и запускает terraform apply — провайдер сам пересобирает и выкатывает функции.
resource "sless_function" "event_writer" {
name = "event-writer"
runtime = "python3.11"
entrypoint = "event_writer_handler.event_writer_handle"
memory_mb = 128
timeout_sec = 20
source_dir = "${path.module}/code/event-writer"
env_vars = {
POSTGRES_DSN = var.pg_dsn
DEFAULT_MESSAGE = var.writer_message
}
}
resource "sless_trigger" "event_writer_http" {
name = "event-writer-http"
type = "http"
function = sless_function.event_writer.name
enabled = true
}
resource "sless_function" "event_monitor" {
name = "event-monitor"
runtime = "python3.11"
entrypoint = "event_monitor_handler.event_monitor_handle"
memory_mb = 128
timeout_sec = 20
source_dir = "${path.module}/code/event-monitor"
env_vars = {
POSTGRES_DSN = var.pg_dsn
RABBITMQ_URL = var.rabbitmq_url
}
}
resource "sless_trigger" "event_monitor_cron" {
name = "event-monitor-cron"
type = "cron"
function = sless_function.event_monitor.name
enabled = true
schedule = "*/1 * * * *"
}
resource "sless_function" "event_cleaner" {
name = "event-cleaner"
runtime = "python3.11"
entrypoint = "event_cleaner_handler.event_cleaner_handle"
memory_mb = 128
timeout_sec = 20
source_dir = "${path.module}/code/event-cleaner"
env_vars = {
POSTGRES_DSN = var.pg_dsn
}
}
resource "sless_trigger" "event_cleaner_http" {
name = "event-cleaner-http"
type = "http"
function = sless_function.event_cleaner.name
enabled = true
}
output "event_writer_url" {
value = sless_trigger.event_writer_http.url
}
output "event_cleaner_url" {
value = sless_trigger.event_cleaner_http.url
}
-18
View File
@@ -1,18 +0,0 @@
# 2026-03-14
# Terraform demo: managed serverless functions.
# Здесь управляем ТОЛЬКО функциями/триггерами, внешние сервисы считаем уже поднятыми.
terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.18"
}
}
}
provider "sless" {
endpoint = var.sless_endpoint
token = var.token
nubes_endpoint = var.nubes_endpoint
}
@@ -1,11 +0,0 @@
# 2026-03-14
# Скопируй в terraform.tfvars и подставь актуальный токен.
token = "PUT_TOKEN_HERE"
sless_endpoint = "https://sless-api.kube5s.ru"
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
pg_dsn = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable"
rabbitmq_url = "amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/"
writer_message = "writer-default-v1"
@@ -1,38 +0,0 @@
# 2026-03-14
# Переменные для demo-managed-functions.
variable "token" {
description = "JWT токен API"
type = string
sensitive = true
}
variable "sless_endpoint" {
description = "Endpoint sless API"
type = string
default = "https://sless.kube5s.ru"
}
variable "nubes_endpoint" {
description = "Nubes endpoint (нужен провайдеру)"
type = string
default = "https://deck-test.ngcloud.ru/api/v1"
}
variable "pg_dsn" {
description = "PostgreSQL DSN для функций"
type = string
default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable"
}
variable "rabbitmq_url" {
description = "RabbitMQ URL для функций"
type = string
default = "amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/"
}
variable "writer_message" {
description = "Сообщение по умолчанию, которое пишет event-writer"
type = string
default = "writer-default-v1"
}
-29
View File
@@ -1,29 +0,0 @@
// Изменено: 2026-03-11
// greeting.go — пример Go функции: возвращает приветствие.
//
// ТРЕБОВАНИЕ РАНТАЙМА: пакет должен называться handler, точка входа — Handle.
// Вся бизнес-логика — в отдельных функциях с нормальными именами.
package handler
import "fmt"
// buildGreeting — формирует текст приветствия для указанного имени гостя.
func buildGreeting(guestName string) string {
return fmt.Sprintf("Hello, %s! (Go 1.23)", guestName)
}
// Handle — точка входа, вызывается рантаймом на каждый запрос/событие.
// Не переименовывать: это жёсткий контракт рантайма (server.go вызывает handler.Handle).
func Handle(event map[string]interface{}) interface{} {
guestName, ok := event["name"].(string)
if !ok || guestName == "" {
guestName = "world"
}
return map[string]interface{}{
"message": buildGreeting(guestName),
"runtime": "go1.23",
"event": event,
}
}
-23
View File
@@ -1,23 +0,0 @@
# 2026-03-11
# http.tf — HTTP-функция на Go: принимает запрос, возвращает приветствие.
resource "sless_function" "hello_go_http" {
name = "hello-go-http"
runtime = "go1.23"
entrypoint = "handler.Handle"
memory_mb = 128
timeout_sec = 60
source_dir = "${path.module}/code"
}
resource "sless_trigger" "hello_go_http" {
name = "hello-go-http-trigger"
type = "http"
function = sless_function.hello_go_http.name
enabled = true
}
output "trigger_url" {
value = sless_trigger.hello_go_http.url
}
-28
View File
@@ -1,28 +0,0 @@
# 2026-03-11
# job.tf — одноразовый запуск Go функции с event payload.
resource "sless_function" "hello_go_job" {
name = "hello-go-job"
runtime = "go1.23"
entrypoint = "handler.Handle"
memory_mb = 128
timeout_sec = 60
source_dir = "${path.module}/code"
}
resource "sless_job" "hello_go_run" {
name = "hello-go-run"
function = sless_function.hello_go_job.name
event_json = jsonencode({ name = "Go" })
wait_timeout_sec = 300
run_id = 1
}
output "job_phase" {
value = sless_job.hello_go_run.phase
}
output "job_message" {
value = sless_job.hello_go_run.message
}
-17
View File
@@ -1,17 +0,0 @@
# 2026-03-11
# main.tf — провайдеры для hello-go примера.
terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.18"
}
}
}
provider "sless" {
endpoint = "https://sless.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1"
}
@@ -1 +0,0 @@
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
-10
View File
@@ -1,10 +0,0 @@
# 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
}
-8
View File
@@ -1,8 +0,0 @@
// 2026-03-08
// handler-http.js — HTTP-функция: возвращает приветствие.
// Используется с sless_trigger (постоянный эндпоинт).
exports.handle = async (event) => {
const name = event.name || 'World';
return { message: `Hello, ${name}! HTTP !!!` };
};
-11
View File
@@ -1,11 +0,0 @@
// 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
View File
@@ -1,24 +0,0 @@
# 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
View File
@@ -1,34 +0,0 @@
# 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
View File
@@ -1,24 +0,0 @@
# 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.18"
}
}
}
provider "sless" {
endpoint = "https://sless.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1"
}
@@ -1 +0,0 @@
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
@@ -1,2 +0,0 @@
# Временный файл для негативных тестов — не применяется через terraform
# Тесты запускаются вручную с временным переименованием в .tf
-10
View File
@@ -1,10 +0,0 @@
# 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
}
@@ -1,31 +0,0 @@
# 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 +0,0 @@
psycopg2-binary
@@ -1,81 +0,0 @@
# 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 +0,0 @@
psycopg2-binary
@@ -1 +0,0 @@
psycopg2-binary
@@ -1,39 +0,0 @@
# 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
View File
@@ -1,44 +0,0 @@
# 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
View File
@@ -1,27 +0,0 @@
# 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.18"
}
}
}
# sless провайдер подключается к API кластера.
provider "sless" {
endpoint = "https://sless.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1"
}
-22
View File
@@ -1,22 +0,0 @@
# 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
View File
@@ -1,27 +0,0 @@
# 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
View File
@@ -1,42 +0,0 @@
# 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
View File
@@ -1,20 +0,0 @@
# 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 +0,0 @@
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
-23
View File
@@ -1,23 +0,0 @@
# 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
}
-102
View File
@@ -1,102 +0,0 @@
# pg-list-python
Python-функция, которая читает из PostgreSQL и возвращает JSON. Таблица с демо-данными создаётся автоматически при первом вызове — никакой инициализации руками.
## Что тут есть
```
code/
catalog.py — функция list_products(event): SELECT из demo_products
requirements.txt — psycopg2-binary
function.tf — sless_function + sless_trigger + output
main.tf — конфигурация провайдера
variables.tf — token, pg_dsn
terraform.tfvars — значения переменных (не в git)
```
## Запуск
**1. Впишите токен в `terraform.tfvars`:**
```hcl
# terraform.tfvars
token = "ваш токен Nubes API" # ← заменить на реальный токен из личного кабинета Nubes
```
**2. Деплой:**
```bash
terraform init
terraform apply -auto-approve
curl -s $(terraform output -raw catalog_url)
```
Ответ:
```json
{
"products": [
{"id": 1, "name": "Ноутбук", "price": 89999.0},
{"id": 2, "name": "Мышь", "price": 1299.0},
{"id": 3, "name": "Клавиатура", "price": 3499.0},
{"id": 4, "name": "Монитор", "price": 32000.0}
],
"count": 4
}
```
## Переменные
| Переменная | Обязательна | Описание |
|---|---|---|
| `token` | ✅ | JWT-токен облака. Задаётся в `terraform.tfvars` |
| `pg_dsn` | — | DSN подключения к PostgreSQL. Дефолт — внутрикластерный адрес |
`terraform.tfvars` не коммитится в git. Минимальное содержимое:
```hcl
token = "ваш токен Nubes API"
```
## Как работает
```
terraform apply
→ загружает код в S3
→ kaniko собирает Docker-образ (pip install psycopg2-binary)
→ Deployment поднимается в кластере
→ Ingress публикует URL
GET /fn/<namespace>/product-catalog
→ рантайм вызывает list_products(event)
→ psycopg2 подключается к PostgreSQL
→ если таблица demo_products не существует — создаёт и заполняет демо-данными
→ возвращает JSON со списком
```
Таблица `demo_products` создаётся один раз — при первом GET. Повторные вызовы просто читают данные.
## Изменение данных
Функция только читает. Чтобы добавить/изменить записи — подключитесь к PostgreSQL напрямую:
```sql
INSERT INTO demo_products (name, price) VALUES ('Кабель', 499.00);
UPDATE demo_products SET price = 95000 WHERE name = 'Ноутбук';
```
## Пересборка если изменили код
```bash
terraform apply -replace=sless_function.product_catalog -auto-approve
```
## Удаление
```bash
terraform destroy -auto-approve
```
Таблица `demo_products` в PostgreSQL **не удаляется** — только k8s-ресурсы.
-47
View File
@@ -1,47 +0,0 @@
# 2026-03-11
# catalog.py — читает список продуктов из PostgreSQL.
# Таблица demo_products создаётся автоматически при первом вызове.
# Точка входа: list_products(event)
import json
import os
import psycopg2
def list_products(event):
dsn = os.environ["PG_DSN"]
conn = psycopg2.connect(dsn)
try:
with conn.cursor() as cur:
_ensure_table(cur)
conn.commit()
cur.execute("SELECT id, name, price FROM demo_products ORDER BY id")
rows = cur.fetchall()
finally:
conn.close()
products = [{"id": row[0], "name": row[1], "price": float(row[2])} for row in rows]
return {"products": products, "count": len(products)}
def _ensure_table(cur):
# Создаём таблицу и наполняем демо-данными — только один раз
cur.execute("""
CREATE TABLE IF NOT EXISTS demo_products (
id SERIAL PRIMARY KEY,
name TEXT NOT NULL,
price NUMERIC(10,2) NOT NULL
)
""")
cur.execute("SELECT COUNT(*) FROM demo_products")
if cur.fetchone()[0] == 0:
cur.executemany(
"INSERT INTO demo_products (name, price) VALUES (%s, %s)",
[
("Ноутбук", 89999.00),
("Мышь", 1299.00),
("Клавиатура", 3499.00),
("Монитор", 32000.00),
],
)
@@ -1 +0,0 @@
psycopg2-binary
-29
View File
@@ -1,29 +0,0 @@
# 2026-03-11
# function.tf — HTTP-функция: читает из PostgreSQL и возвращает список записей.
# Нет джобов, нет инициализации — только функция + HTTP триггер.
resource "sless_function" "product_catalog" { # объявляем serverless-функцию; "product_catalog" — локальное имя в tf-state
name = "product-catalog" # имя функции в кластере; по нему формируется URL и имя k8s-объекта
runtime = "python3.11" # базовый образ рантайма; определяет как собирается и запускается код
entrypoint = "catalog.list_products" # файл.функция которую вызывает рантайм: catalog.py → def list_products(event)
memory_mb = 128 # лимит памяти пода в мегабайтах
timeout_sec = 10 # максимальное время выполнения одного запроса в секундах
source_dir = "${path.module}/code" # директория с кодом функции; провайдер упакует её в zip и загрузит
# DSN передаётся через env — функция не знает об инфраструктуре
env_vars = {
PG_DSN = var.pg_dsn # строка подключения к PostgreSQL; берётся из переменной (variables.tf)
}
}
resource "sless_trigger" "product_catalog_http" { # триггер публикует функцию наружу; без него функция существует, но недоступна
name = "product-catalog-http" # имя триггера в кластере
type = "http" # тип триггера: "http" создаёт публичный URL; альтернатива — "cron"
function = sless_function.product_catalog.name # ссылка на имя функции выше; terraform гарантирует порядок создания
enabled = true # триггер активен сразу после создания
}
output "catalog_url" {
value = sless_trigger.product_catalog_http.url # URL вида https://sless-api.../fn/<namespace>/<name>; выводится после apply
}
-17
View File
@@ -1,17 +0,0 @@
# 2026-03-11
# main.tf — провайдер для pg-list-python примера.
terraform {
required_providers {
sless = {
source = "terra.k8c.ru/naeel/sless"
version = "~> 0.1.18"
}
}
}
provider "sless" {
endpoint = "https://sless.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1"
}
@@ -1 +0,0 @@
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
-14
View File
@@ -1,14 +0,0 @@
# 2026-03-11
# variables.tf
variable "token" {
description = "JWT токен облака"
type = string
sensitive = true
}
variable "pg_dsn" {
description = "DSN подключения к PostgreSQL"
type = string
default = "postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable"
}
@@ -1,20 +0,0 @@
// Создано: 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 || '/',
};
};
@@ -1,10 +0,0 @@
// Создано: 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
View File
@@ -1,31 +0,0 @@
# Создано: 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.18"
}
}
}
provider "sless" {
endpoint = "https://sless.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1"
}
-14
View File
@@ -1,14 +0,0 @@
# Создано: 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 +0,0 @@
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
-28
View File
@@ -1,28 +0,0 @@
# Создано: 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
View File
@@ -1,27 +0,0 @@
# Создано: 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
View File
@@ -1,10 +0,0 @@
# 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
}
@@ -1,20 +0,0 @@
# Создано: 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", "/"),
}
@@ -1,18 +0,0 @@
# Создано: 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
View File
@@ -1,30 +0,0 @@
# Создано: 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.18"
}
}
}
provider "sless" {
endpoint = "https://sless.kube5s.ru"
token = var.token
nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1"
}
-14
View File
@@ -1,14 +0,0 @@
# Создано: 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 +0,0 @@
token = "замени на Nubes API token" и переименуй файл в terraform.tfvars
-28
View File
@@ -1,28 +0,0 @@
# Создано: 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
View File
@@ -1,27 +0,0 @@
# Создано: 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
View File
@@ -1,10 +0,0 @@
# 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
}