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:
@@ -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
@@ -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
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"name": "pg-info",
|
||||
"version": "1.0.0",
|
||||
"description": "sless nodejs20 function: pg version + table info",
|
||||
"dependencies": {
|
||||
"pg": "8.11.0"
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
@@ -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
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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,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 !!!` };
|
||||
};
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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()
|
||||
@@ -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)"
|
||||
]
|
||||
})
|
||||
}
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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 = "Статус джоба создания индекса"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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-ресурсы.
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
@@ -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() };
|
||||
};
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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({})))
|
||||
@@ -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"
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user