w golang
This commit is contained in:
parent
a2094c82c5
commit
b4a72850e9
39
.gitignore
vendored
Normal file
39
.gitignore
vendored
Normal file
@ -0,0 +1,39 @@
|
||||
# Created: 2026-03-11
|
||||
# Purpose: ignore generated artifacts for the `examples` repository
|
||||
|
||||
# Terraform
|
||||
.terraform/
|
||||
*.tfstate
|
||||
*.tfstate.*
|
||||
.terraform.lock.hcl
|
||||
crash.log
|
||||
|
||||
# Terraform plans / backups
|
||||
*.tfplan
|
||||
*.backup
|
||||
*.bak
|
||||
|
||||
# Provider plugins / caches
|
||||
.terraform.d/
|
||||
|
||||
# Archives and build artifacts
|
||||
*.zip
|
||||
dist/
|
||||
build/
|
||||
|
||||
# Node / Python
|
||||
node_modules/
|
||||
__pycache__/
|
||||
*.pyc
|
||||
venv/
|
||||
.venv/
|
||||
|
||||
# Editor / OS files
|
||||
.DS_Store
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# Environment files
|
||||
.env
|
||||
*.local
|
||||
*.log
|
||||
@ -1,143 +0,0 @@
|
||||
# Bug Report: HTTP route is not removed after Terraform destroy
|
||||
|
||||
## Summary
|
||||
|
||||
При удалении примера `hello-node` через Terraform команда `terraform destroy` завершается успешно, но публичный HTTP endpoint не удаляется.
|
||||
|
||||
Фактическое поведение после `destroy` такое:
|
||||
|
||||
1. Сразу после удаления endpoint ещё некоторое время отвечает `HTTP 200` и возвращает корректный ответ функции.
|
||||
2. Затем backend функции действительно исчезает, но публичный маршрут остаётся опубликованным и начинает отвечать `HTTP 502 function unreachable`.
|
||||
3. Даже через 120 секунд endpoint не исчезает.
|
||||
|
||||
Это выглядит как баг cleanup в platform/backend/provider lifecycle для HTTP trigger/route.
|
||||
|
||||
## Affected Example
|
||||
|
||||
- Example: `hello-node`
|
||||
- Terraform files: `hello-node/main.tf`, `hello-node/http.tf`, `hello-node/job.tf`
|
||||
- Public URL: `https://sless-api.kube5s.ru/fn/default/hello-http`
|
||||
- Function name: `hello-http`
|
||||
- Trigger name: `hello-http-trigger`
|
||||
|
||||
## Reproduction
|
||||
|
||||
Использовался репозиторий examples и скрипт:
|
||||
|
||||
- Script: `./run_terraform_examples.sh`
|
||||
|
||||
Шаги воспроизведения:
|
||||
|
||||
1. Выполнить `terraform init` в `hello-node`
|
||||
2. Выполнить `terraform apply`
|
||||
3. Убедиться, что endpoint живой
|
||||
4. Выполнить `terraform destroy`
|
||||
5. Проверять публичный URL после destroy
|
||||
|
||||
Логика проверки встроена в `run_terraform_examples.sh`:
|
||||
|
||||
1. После `apply` endpoint обязан отвечать `200`
|
||||
2. После `destroy` endpoint должен исчезнуть
|
||||
3. Скрипт ждёт до 120 секунд и перепроверяет endpoint каждые 5 секунд
|
||||
|
||||
## Expected Result
|
||||
|
||||
После успешного `terraform destroy`:
|
||||
|
||||
1. Публичный URL должен перестать существовать
|
||||
2. Запрос на URL должен вернуть `404` или другой явный признак отсутствия маршрута
|
||||
3. Provider не должен возвращать успешный destroy раньше, чем cleanup HTTP route завершён
|
||||
|
||||
## Actual Result
|
||||
|
||||
После успешного `terraform destroy`:
|
||||
|
||||
1. Terraform сообщает `Destroy complete! Resources: 4 destroyed.`
|
||||
2. Endpoint `https://sless-api.kube5s.ru/fn/default/hello-http` продолжает отвечать `200`
|
||||
3. Через некоторое время тот же endpoint начинает отвечать `502`
|
||||
4. Тело ответа на `502`:
|
||||
|
||||
```json
|
||||
{"error":"function unreachable: Post \"http://hello-http.sless-fn-default.svc.cluster.local:8080\": dial tcp 10.106.128.167:8080: connect: operation not permitted"}
|
||||
```
|
||||
|
||||
Это означает:
|
||||
|
||||
1. внешний HTTP маршрут всё ещё существует;
|
||||
2. запрос по нему всё ещё направляется внутрь платформы;
|
||||
3. backend функции уже удалён или недоступен;
|
||||
4. cleanup маршрута не завершён.
|
||||
|
||||
## Timeline From Real Run
|
||||
|
||||
Подтверждённая последовательность из фактического прогона:
|
||||
|
||||
1. `terraform destroy` завершился успешно
|
||||
2. первые проверки после destroy возвращали `HTTP 200`
|
||||
3. затем проверки начали возвращать `HTTP 502 function unreachable`
|
||||
4. в течение всех 24 проверок по 5 секунд endpoint не исчез
|
||||
5. итоговое время ожидания: 120 секунд
|
||||
|
||||
Итоговый summary из скрипта:
|
||||
|
||||
```text
|
||||
ERROR SUMMARY
|
||||
example: hello-node
|
||||
step: endpoint cleanup after clean destroy
|
||||
reason: route cleanup bug: public endpoint still exists but backend is already gone (HTTP 502 function unreachable); endpoint was still published after 120s
|
||||
```
|
||||
|
||||
## Why This Is A Real Platform Bug
|
||||
|
||||
Это не похоже на проблему тестового скрипта или Terraform CLI по следующим причинам:
|
||||
|
||||
1. `terraform destroy` завершается без ошибки
|
||||
2. state Terraform очищается как ожидалось
|
||||
3. сначала endpoint отвечает `200`, значит маршрут реально жив после destroy
|
||||
4. потом endpoint отвечает `502 function unreachable`, значит backend уже исчез, но route ещё остался
|
||||
5. скрипт ждёт 120 секунд, то есть это не мгновенная eventual consistency на 1-2 секунды
|
||||
|
||||
Иными словами: удаление backend и удаление публичного маршрута расходятся по времени, а route cleanup либо не выполняется, либо не дожидается завершения.
|
||||
|
||||
## Most Likely Broken Layer
|
||||
|
||||
Наиболее вероятные точки проблемы:
|
||||
|
||||
1. API/backend destroy trigger возвращает success до фактического удаления HTTP route
|
||||
2. Controller удаляет function workload, но не удаляет route/ingress/virtualservice/gateway mapping
|
||||
3. Удаление route запускается асинхронно, но его результат не awaited
|
||||
4. В системе остаётся запись маршрута на имя функции, хотя service/backend уже удалён
|
||||
|
||||
## What To Check In The Development Repo
|
||||
|
||||
Нужно проверить destroy flow именно для HTTP trigger:
|
||||
|
||||
1. Удаляется ли объект trigger только в metadata/storage или реально удаляется и внешний маршрут
|
||||
2. Какие Kubernetes/ingress объекты создаются для HTTP trigger и все ли они удаляются
|
||||
3. Есть ли race condition между удалением function/service и удалением route
|
||||
4. Не возвращает ли provider success раньше, чем backend подтверждает полное удаление маршрута
|
||||
5. Есть ли финальный polling/wait на исчезновение route перед возвратом успешного destroy
|
||||
|
||||
Если архитектура использует отдельные сущности route/service/function, то destroy должен идти в таком порядке:
|
||||
|
||||
1. disable/remove public routing
|
||||
2. дождаться, что endpoint больше не публикуется снаружи
|
||||
3. удалить backend/service/workload
|
||||
4. завершить destroy success
|
||||
|
||||
Сейчас по фактическому поведению порядок либо обратный, либо неполный.
|
||||
|
||||
## Minimal Acceptance Criteria For Fix
|
||||
|
||||
Исправление можно считать рабочим, если после `terraform destroy` для `hello-node` выполняются все условия:
|
||||
|
||||
1. URL `https://sless-api.kube5s.ru/fn/default/hello-http` перестаёт отвечать как живой маршрут
|
||||
2. URL не возвращает `502 function unreachable`
|
||||
3. URL исчезает в разумное время после destroy
|
||||
4. `./run_terraform_examples.sh` проходит шаг `endpoint cleanup after clean destroy`
|
||||
|
||||
## Current Status
|
||||
|
||||
На данный момент массовый прогон examples корректно останавливается на `hello-node`, потому что это первый воспроизводимый failure.
|
||||
|
||||
Дальше прогонять остальные примеры без исправления destroy cleanup смысла нет: тест уже доказал platform bug на базовом HTTP сценарии.
|
||||
180
README.md
180
README.md
@ -1,64 +1,119 @@
|
||||
# Примеры sless
|
||||
# Примеры использования sless
|
||||
|
||||
## Что такое sless
|
||||
## Обзор платформы
|
||||
|
||||
**sless** — платформа для запуска serverless-функций в Kubernetes-кластере.
|
||||
**sless** — система управления serverless-функциями на базе Kubernetes. Разработчик загружает код функции, платформа собирает из него Docker-образ, разворачивает его в кластере и предоставляет HTTP-эндпоинт для вызова. Всё описывается декларативно через Terraform.
|
||||
|
||||
Код на Python или Node.js загружается в платформу, которая собирает Docker-образ, деплоит его в кластер и публикует HTTP-эндпоинт. Всё управляется через Terraform.
|
||||
### Основные ресурсы провайдера
|
||||
|
||||
### Ресурсы
|
||||
|
||||
| Ресурс | Что делает |
|
||||
| Ресурс | Назначение |
|
||||
|---|---|
|
||||
| `sless_function` | Загружает код и собирает Docker-образ. Сама по себе не принимает запросы — нужен триггер или джоб |
|
||||
| `sless_trigger` | Публикует функцию — либо как HTTP-эндпоинт, либо по расписанию (cron) |
|
||||
| `sless_job` | Запускает функцию один раз (например, для инициализации БД) и ждёт результата |
|
||||
| `sless_function` | Описывает функцию: язык, точку входа, лимиты, переменные окружения. При создании загружает код и запускает его сборку в образ. Сама по себе недоступна снаружи — нужен триггер или задание. |
|
||||
| `sless_trigger` | Публикует функцию: тип `http` создаёт публичный URL, тип `cron` — запуск по расписанию. |
|
||||
| `sless_job` | Запускает функцию однократно и ожидает завершения. Используется для одноразовых операций: инициализация БД, миграции, пакетная обработка. |
|
||||
|
||||
**Типичный сценарий:** `sless_function` с кодом + `sless_trigger` с `type = "http"` → публичный URL вида `https://sless-api.kube5s.ru/fn/default/имя-функции`.
|
||||
Стандартная связка для HTTP API: `sless_function` + `sless_trigger` с `type = "http"` — в результате функция доступна по URL вида `https://sless-api.kube5s.ru/fn/<namespace>/<имя-функции>`.
|
||||
|
||||
---
|
||||
|
||||
Примеры показывают различные сценарии использования serverless функций через Terraform провайдер `terra.k8c.ru/naeel/sless`.
|
||||
|
||||
## Требования
|
||||
|
||||
- Terraform >= 1.0
|
||||
- JWT-токен для аутентификации в sless API
|
||||
- Доступ к `https://sless-api.kube5s.ru`
|
||||
|
||||
## Провайдер
|
||||
## Конфигурация провайдера
|
||||
|
||||
Во всех примерах `main.tf` содержит:
|
||||
Во всех примерах файл `main.tf` содержит блок провайдера. Токен передаётся через переменную, значение которой задаётся в `terraform.tfvars`:
|
||||
|
||||
```hcl
|
||||
provider "sless" {
|
||||
endpoint = "https://sless-api.kube5s.ru"
|
||||
token = "dev-token-change-me"
|
||||
endpoint = "https://sless-api.kube5s.ru"
|
||||
token = var.token
|
||||
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
||||
}
|
||||
```
|
||||
|
||||
Namespace функций вычисляется автоматически из JWT-токена: `sless-{sha256[:8]}`.
|
||||
|
||||
---
|
||||
|
||||
## Примеры
|
||||
|
||||
### `simple-python` — джоб передаёт результат в HTTP-функцию (Python)
|
||||
### `hello-node` — минимальный пример на Node.js
|
||||
|
||||
При `apply` запускается джоб, его вывод передаётся в HTTP-функцию через `env_vars`.
|
||||
Две независимые функции: HTTP-функция, возвращающая приветствие, и одноразовое задание, суммирующее набор чисел. Хорошая отправная точка для знакомства с платформой.
|
||||
|
||||
```bash
|
||||
cd hello-node
|
||||
terraform init
|
||||
terraform apply -auto-approve
|
||||
|
||||
# Вызов HTTP-функции с передачей имени:
|
||||
curl -s -X POST https://sless-api.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-api.kube5s.ru/fn/default/simple-py-time-display
|
||||
curl -s https://sless-api.kube5s.ru/fn/<namespace>/simple-py-time-display
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `simple-node` — то же самое, но на Node.js 20
|
||||
### `simple-node` — то же самое на Node.js 20
|
||||
|
||||
```bash
|
||||
cd simple-node
|
||||
@ -66,95 +121,68 @@ terraform init
|
||||
terraform apply -auto-approve
|
||||
|
||||
terraform output job_result
|
||||
curl -s https://sless-api.kube5s.ru/fn/default/simple-node-time-display
|
||||
curl -s https://sless-api.kube5s.ru/fn/<namespace>/simple-node-time-display
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `hello-node` — минимальный пример на Node.js
|
||||
### `notes-python` — CRUD API на Python с PostgreSQL
|
||||
|
||||
Две независимые функции: HTTP-функция (возвращает приветствие) и одноразовый джоб (суммирует числа).
|
||||
|
||||
```bash
|
||||
cd hello-node
|
||||
terraform init
|
||||
terraform apply -auto-approve
|
||||
|
||||
# Проверить HTTP-функцию:
|
||||
curl -s -X POST https://sless-api.kube5s.ru/fn/default/hello-http \
|
||||
-H 'Content-Type: application/json' -d '{"name":"World"}'
|
||||
|
||||
# Посмотреть результат джоба:
|
||||
terraform output job_message
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### `notes-python` — CRUD API на Python + PostgreSQL
|
||||
|
||||
Полноценное приложение: инициализация схемы БД через джобы, CRUD-функция, read-only функция для списка записей.
|
||||
Полноценное приложение: инициализация схемы базы данных через задания, CRUD-функция для работы с записями, отдельная функция для получения списка.
|
||||
|
||||
**Переменные:**
|
||||
|
||||
| Переменная | Описание | Дефолт |
|
||||
| Переменная | Описание | Значение по умолчанию |
|
||||
|---|---|---|
|
||||
| `pg_dsn` | DSN для подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` |
|
||||
| `pg_dsn` | Строка подключения к PostgreSQL | `postgres://sless:sless-pg-password@postgres.sless.svc.cluster.local:5432/sless?sslmode=disable` |
|
||||
|
||||
```bash
|
||||
cd notes-python
|
||||
terraform init
|
||||
|
||||
# Опционально — переопределить DSN:
|
||||
# export TF_VAR_pg_dsn="postgres://user:pass@host:5432/db?sslmode=disable"
|
||||
|
||||
terraform apply -auto-approve
|
||||
|
||||
# Проверить инициализацию БД:
|
||||
# Статус инициализации базы данных:
|
||||
terraform output db_init_table_status
|
||||
terraform output db_init_index_status
|
||||
|
||||
# URL функций:
|
||||
terraform output notes_url # CRUD
|
||||
terraform output notes_list_url # список всех записей
|
||||
|
||||
# Создать запись:
|
||||
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/add?title=Hello&body=World"
|
||||
curl -s -X POST "$(terraform output -raw notes_url)/add?title=Hello&body=World"
|
||||
|
||||
# Список записей:
|
||||
curl -s https://sless-api.kube5s.ru/fn/default/notes-list
|
||||
# Получить список записей:
|
||||
curl -s $(terraform output -raw notes_list_url)
|
||||
|
||||
# Обновить (id из предыдущего ответа):
|
||||
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/update?id=1&title=Updated&body=New+body"
|
||||
# Обновить запись (id из предыдущего ответа):
|
||||
curl -s -X POST "$(terraform output -raw notes_url)/update?id=1&title=Updated&body=New+body"
|
||||
|
||||
# Удалить:
|
||||
curl -s -X POST "https://sless-api.kube5s.ru/fn/default/notes/delete?id=1"
|
||||
# Удалить запись:
|
||||
curl -s -X POST "$(terraform output -raw notes_url)/delete?id=1"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Общие команды
|
||||
## Полезные команды
|
||||
|
||||
```bash
|
||||
# Посмотреть текущее состояние ресурсов:
|
||||
# Посмотреть текущее состояние задеплоенных ресурсов:
|
||||
terraform show
|
||||
|
||||
# Пересоздать конкретный ресурс:
|
||||
terraform apply -replace=sless_function.имя -auto-approve
|
||||
# Принудительно пересобрать функцию (например, после изменения кода):
|
||||
terraform apply -replace=sless_function.<имя> -auto-approve
|
||||
|
||||
# Повторно запустить джоб — увеличить run_id в .tf файле, затем:
|
||||
# Повторно запустить задание: увеличить значение run_id в .tf-файле, затем:
|
||||
terraform apply -auto-approve
|
||||
|
||||
# Удалить все ресурсы примера:
|
||||
terraform destroy -auto-approve
|
||||
```
|
||||
|
||||
## Структура каждого примера
|
||||
## Структура примера
|
||||
|
||||
```
|
||||
пример/
|
||||
├── main.tf — провайдер
|
||||
├── *.tf — ресурсы (функции, триггеры, джобы)
|
||||
├── outputs.tf — URLs и статусы после apply
|
||||
├── variables.tf — входные переменные (если есть)
|
||||
└── code/ — исходный код функций
|
||||
<пример>/
|
||||
├── main.tf — конфигурация провайдера
|
||||
├── *.tf — ресурсы: функции, триггеры, задания
|
||||
├── variables.tf — входные переменные
|
||||
├── terraform.tfvars — значения переменных (не коммитится в git)
|
||||
└── code/ — исходный код функций
|
||||
```
|
||||
|
||||
29
hello-go/code/greeting.go
Normal file
29
hello-go/code/greeting.go
Normal file
@ -0,0 +1,29 @@
|
||||
// Изменено: 2026-03-11
|
||||
// greeting.go — пример Go функции: возвращает приветствие.
|
||||
//
|
||||
// ТРЕБОВАНИЕ РАНТАЙМА: пакет должен называться handler, точка входа — Handle.
|
||||
// Вся бизнес-логика — в отдельных функциях с нормальными именами.
|
||||
|
||||
package handler
|
||||
|
||||
import "fmt"
|
||||
|
||||
// buildGreeting — формирует текст приветствия для указанного имени гостя.
|
||||
func buildGreeting(guestName string) string {
|
||||
return fmt.Sprintf("Hello, %s! (Go 1.23)", guestName)
|
||||
}
|
||||
|
||||
// Handle — точка входа, вызывается рантаймом на каждый запрос/событие.
|
||||
// Не переименовывать: это жёсткий контракт рантайма (server.go вызывает handler.Handle).
|
||||
func Handle(event map[string]interface{}) interface{} {
|
||||
guestName, ok := event["name"].(string)
|
||||
if !ok || guestName == "" {
|
||||
guestName = "world"
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"message": buildGreeting(guestName),
|
||||
"runtime": "go1.23",
|
||||
"event": event,
|
||||
}
|
||||
}
|
||||
23
hello-go/http.tf
Normal file
23
hello-go/http.tf
Normal file
@ -0,0 +1,23 @@
|
||||
# 2026-03-11
|
||||
# http.tf — HTTP-функция на Go: принимает запрос, возвращает приветствие.
|
||||
|
||||
resource "sless_function" "hello_go_http" {
|
||||
name = "hello-go-http"
|
||||
runtime = "go1.23"
|
||||
entrypoint = "handler.Handle"
|
||||
memory_mb = 128
|
||||
timeout_sec = 60
|
||||
|
||||
source_dir = "${path.module}/code"
|
||||
}
|
||||
|
||||
resource "sless_trigger" "hello_go_http" {
|
||||
name = "hello-go-http-trigger"
|
||||
type = "http"
|
||||
function = sless_function.hello_go_http.name
|
||||
enabled = true
|
||||
}
|
||||
|
||||
output "trigger_url" {
|
||||
value = sless_trigger.hello_go_http.url
|
||||
}
|
||||
28
hello-go/job.tf
Normal file
28
hello-go/job.tf
Normal file
@ -0,0 +1,28 @@
|
||||
# 2026-03-11
|
||||
# job.tf — одноразовый запуск Go функции с event payload.
|
||||
|
||||
resource "sless_function" "hello_go_job" {
|
||||
name = "hello-go-job"
|
||||
runtime = "go1.23"
|
||||
entrypoint = "handler.Handle"
|
||||
memory_mb = 128
|
||||
timeout_sec = 60
|
||||
|
||||
source_dir = "${path.module}/code"
|
||||
}
|
||||
|
||||
resource "sless_job" "hello_go_run" {
|
||||
name = "hello-go-run"
|
||||
function = sless_function.hello_go_job.name
|
||||
event_json = jsonencode({ name = "Go" })
|
||||
wait_timeout_sec = 300
|
||||
run_id = 1
|
||||
}
|
||||
|
||||
output "job_phase" {
|
||||
value = sless_job.hello_go_run.phase
|
||||
}
|
||||
|
||||
output "job_message" {
|
||||
value = sless_job.hello_go_run.message
|
||||
}
|
||||
17
hello-go/main.tf
Normal file
17
hello-go/main.tf
Normal file
@ -0,0 +1,17 @@
|
||||
# 2026-03-11
|
||||
# main.tf — провайдеры для hello-go примера.
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
sless = {
|
||||
source = "terra.k8c.ru/naeel/sless"
|
||||
version = "~> 0.1.14"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "sless" {
|
||||
endpoint = "https://sless-api.kube5s.ru"
|
||||
token = var.token
|
||||
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
||||
}
|
||||
1
hello-go/terraform.tfvars
Normal file
1
hello-go/terraform.tfvars
Normal file
@ -0,0 +1 @@
|
||||
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOTllMzI1LTFjZGYtN2NkYS05MzE5LWU1MzAyYTg1ZTI5MSIsImV4cCI6MTc4NjkzMjI2MCwiaWF0IjoxNzcxMzgwMjYwLCJqdGkiOiIzOTQ3ZTgyMy0yNjljLTQ0MTAtYmU0My1iNGVkNTc1Njg0ZTQiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJncm91cHMiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6IiIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUifQ.hzpIIqNWkKIoYUXDaLY7DLyGKH70rz0ZTqanv19qxF10i3N1t1g_KknA4Qsw1MduTyLzIz7y5SRSr4PSQ1gzR0vB_C0GudSFUhyBNNKkS4ClhRDWW9eN_IIEljbiJMLQi2L07XJ7Y5DQ0sIHRAPLkreCDFMKQ0yTCrKoScCJIDuUqzaTcOaX-hfjaxW8iV0SZMDxl0C5O3tke0btxkaLBaAcWH0V-1yu2r2m29fyU33FqikF0xAcDXiuZphfsrShKQYArZjKAphYCP_Vpmr-1sdjinkn8sPSk1qZny0rka8G6WVZUGaZSOnW8SYNLVUwdqtuQmK-Y18o7U0Suzrsjg"
|
||||
10
hello-go/variables.tf
Normal file
10
hello-go/variables.tf
Normal file
@ -0,0 +1,10 @@
|
||||
# 2026-03-11
|
||||
# variables.tf — входные переменные для hello-node примера.
|
||||
|
||||
# JWT токен облака (nubes). Передаётся через terraform.tfvars (gitignored).
|
||||
# Из токена провайдер вычисляет namespace: sless-{sha256[:8]}
|
||||
variable "token" {
|
||||
description = "JWT токен облака для аутентификации в sless API"
|
||||
type = string
|
||||
sensitive = true
|
||||
}
|
||||
47
pg-list-python/code/catalog.py
Normal file
47
pg-list-python/code/catalog.py
Normal file
@ -0,0 +1,47 @@
|
||||
# 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
pg-list-python/code/requirements.txt
Normal file
1
pg-list-python/code/requirements.txt
Normal file
@ -0,0 +1 @@
|
||||
psycopg2-binary
|
||||
29
pg-list-python/function.tf
Normal file
29
pg-list-python/function.tf
Normal file
@ -0,0 +1,29 @@
|
||||
# 2026-03-11
|
||||
# function.tf — HTTP-функция: читает из PostgreSQL и возвращает список записей.
|
||||
# Нет джобов, нет инициализации — только функция + HTTP триггер.
|
||||
|
||||
resource "sless_function" "product_catalog" { # объявляем serverless-функцию; "product_catalog" — локальное имя в tf-state
|
||||
name = "product-catalog" # имя функции в кластере; по нему формируется URL и имя k8s-объекта
|
||||
runtime = "python3.11" # базовый образ рантайма; определяет как собирается и запускается код
|
||||
entrypoint = "catalog.list_products" # файл.функция которую вызывает рантайм: catalog.py → def list_products(event)
|
||||
memory_mb = 128 # лимит памяти пода в мегабайтах
|
||||
timeout_sec = 10 # максимальное время выполнения одного запроса в секундах
|
||||
|
||||
source_dir = "${path.module}/code" # директория с кодом функции; провайдер упакует её в zip и загрузит
|
||||
|
||||
# DSN передаётся через env — функция не знает об инфраструктуре
|
||||
env_vars = {
|
||||
PG_DSN = var.pg_dsn # строка подключения к PostgreSQL; берётся из переменной (variables.tf)
|
||||
}
|
||||
}
|
||||
|
||||
resource "sless_trigger" "product_catalog_http" { # триггер публикует функцию наружу; без него функция существует, но недоступна
|
||||
name = "product-catalog-http" # имя триггера в кластере
|
||||
type = "http" # тип триггера: "http" создаёт публичный URL; альтернатива — "cron"
|
||||
function = sless_function.product_catalog.name # ссылка на имя функции выше; terraform гарантирует порядок создания
|
||||
enabled = true # триггер активен сразу после создания
|
||||
}
|
||||
|
||||
output "catalog_url" {
|
||||
value = sless_trigger.product_catalog_http.url # URL вида https://sless-api.../fn/<namespace>/<name>; выводится после apply
|
||||
}
|
||||
17
pg-list-python/main.tf
Normal file
17
pg-list-python/main.tf
Normal file
@ -0,0 +1,17 @@
|
||||
# 2026-03-11
|
||||
# main.tf — провайдер для pg-list-python примера.
|
||||
|
||||
terraform {
|
||||
required_providers {
|
||||
sless = {
|
||||
source = "terra.k8c.ru/naeel/sless"
|
||||
version = "~> 0.1.14"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
provider "sless" {
|
||||
endpoint = "https://sless-api.kube5s.ru"
|
||||
token = var.token
|
||||
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
||||
}
|
||||
1
pg-list-python/terraform.tfvars
Normal file
1
pg-list-python/terraform.tfvars
Normal file
@ -0,0 +1 @@
|
||||
token = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJhdXRoLWFwaSIsInN1YiI6IjAxOTllMzI1LTFjZGYtN2NkYS05MzE5LWU1MzAyYTg1ZTI5MSIsImV4cCI6MTc4NjkzMjI2MCwiaWF0IjoxNzcxMzgwMjYwLCJqdGkiOiIzOTQ3ZTgyMy0yNjljLTQ0MTAtYmU0My1iNGVkNTc1Njg0ZTQiLCJhdXRoX3RpbWUiOjAsInR5cCI6IiIsImF6cCI6IiIsInNlc3Npb25fc3RhdGUiOiIiLCJhY3IiOiIiLCJhbGxvd2VkLW9yaWdpbnMiOm51bGwsInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6bnVsbH0sInJlc291cmNlX2FjY2VzcyI6eyJhY2NvdW50Ijp7InJvbGVzIjpudWxsfX0sInNjb3BlIjoiIiwic2lkIjoiIiwiZW1haWxfdmVyaWZpZWQiOmZhbHNlLCJuYW1lIjoiIiwiQ2xpZW50SUQiOiIiLCJncm91cHMiOm51bGwsInByZWZlcnJlZF91c2VybmFtZSI6IiIsImdpdmVuX25hbWUiOiIiLCJmYW1pbHlfbmFtZSI6IiIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUifQ.hzpIIqNWkKIoYUXDaLY7DLyGKH70rz0ZTqanv19qxF10i3N1t1g_KknA4Qsw1MduTyLzIz7y5SRSr4PSQ1gzR0vB_C0GudSFUhyBNNKkS4ClhRDWW9eN_IIEljbiJMLQi2L07XJ7Y5DQ0sIHRAPLkreCDFMKQ0yTCrKoScCJIDuUqzaTcOaX-hfjaxW8iV0SZMDxl0C5O3tke0btxkaLBaAcWH0V-1yu2r2m29fyU33FqikF0xAcDXiuZphfsrShKQYArZjKAphYCP_Vpmr-1sdjinkn8sPSk1qZny0rka8G6WVZUGaZSOnW8SYNLVUwdqtuQmK-Y18o7U0Suzrsjg"
|
||||
14
pg-list-python/variables.tf
Normal file
14
pg-list-python/variables.tf
Normal file
@ -0,0 +1,14 @@
|
||||
# 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,7 +0,0 @@
|
||||
# 2026-03-11 10:00
|
||||
# Minimal sample image to push to PearlHarbor registry
|
||||
# Purpose: небольшой образ для тестирования пуша в реестр
|
||||
|
||||
FROM alpine:3.18
|
||||
|
||||
CMD ["sh", "-c", "echo Hello from pearlharbor sample image"]
|
||||
@ -1,28 +0,0 @@
|
||||
# Пример для пуша в PearlHarbor
|
||||
|
||||
Файлы:
|
||||
- [examples/push-sample/Dockerfile](examples/push-sample/Dockerfile) — минимальный образ
|
||||
- [examples/push-sample/build_and_push.sh](examples/push-sample/build_and_push.sh) — сборка и опциональный пуш
|
||||
|
||||
Как использовать:
|
||||
|
||||
1. Сборка локально (в корне репы):
|
||||
|
||||
```bash
|
||||
docker build -t sless-sample:local -f examples/push-sample/Dockerfile examples/push-sample
|
||||
```
|
||||
|
||||
2. Протестировать скрипт (скрипт не будет пушить без переменной DO_PUSH):
|
||||
|
||||
```bash
|
||||
cd examples/push-sample
|
||||
./build_and_push.sh
|
||||
```
|
||||
|
||||
3. Для реального пуша установите `DO_PUSH=true`. Скрипт прочитает `secrets/pearlharbor_registry.txt`.
|
||||
|
||||
```bash
|
||||
DO_PUSH=true ./build_and_push.sh
|
||||
```
|
||||
|
||||
Примечание: скрипт использует по умолчанию пользователя `admin`. Для другого пользователя задайте `REGISTRY_USER`.
|
||||
@ -1,53 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
# 2026-03-11 10:02
|
||||
# Скрипт: собирает минимальный образ и, при разрешении, пушит в реестр PearlHarbor
|
||||
# Требования: `docker` в PATH. Скрипт НЕ будет пушить без DO_PUSH=true.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# Получаем значения из файла секретов
|
||||
SECRETS_FILE="secrets/pearlharbor_registry.txt"
|
||||
if [ ! -f "$SECRETS_FILE" ]; then
|
||||
echo "Файл с секретами не найден: $SECRETS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
connection_url=$(grep -E '^connection_url=' "$SECRETS_FILE" | cut -d'=' -f2-)
|
||||
admin_pass=$(grep -E '^admin_pass=' "$SECRETS_FILE" | cut -d'=' -f2-)
|
||||
|
||||
if [ -z "$connection_url" ]; then
|
||||
echo "Не найден connection_url в $SECRETS_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Убираем протокол и возможный слеш на конце
|
||||
registry_host=$(echo "$connection_url" | sed -E 's~https?://~~' | sed -E 's~/$~~')
|
||||
|
||||
image_name="$registry_host/sless-sample:latest"
|
||||
|
||||
echo "Registry host: $registry_host"
|
||||
echo "Image name: $image_name"
|
||||
|
||||
echo "Собираю образ локально..."
|
||||
docker build -t sless-sample:local -f Dockerfile .. || {
|
||||
echo "Сборка не удалась"; exit 1
|
||||
}
|
||||
|
||||
echo "Готово. Образ: sless-sample:local"
|
||||
|
||||
if [ "${DO_PUSH:-}" != "true" ]; then
|
||||
echo "DO_PUSH != true — пуш не будет выполнен. Чтобы запушить: DO_PUSH=true ./build_and_push.sh"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Если дошли до сюда — выполняем login/push
|
||||
registry_user=${REGISTRY_USER:-admin}
|
||||
|
||||
echo "Выполняю docker login к $registry_host как '$registry_user'"
|
||||
echo "$admin_pass" | docker login "$registry_host" -u "$registry_user" --password-stdin
|
||||
|
||||
echo "Тегирую и пушу образ: $image_name"
|
||||
docker tag sless-sample:local "$image_name"
|
||||
docker push "$image_name"
|
||||
|
||||
echo "Пуш завершён. Проверьте реестр для образа: $image_name"
|
||||
@ -1,333 +0,0 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
LOG_DIR="$ROOT_DIR/.test-logs"
|
||||
mkdir -p "$LOG_DIR"
|
||||
|
||||
EXAMPLES=(
|
||||
"hello-node"
|
||||
"simple-node"
|
||||
"simple-python"
|
||||
"notes-python"
|
||||
)
|
||||
|
||||
declare -A CHECK_METHOD=(
|
||||
[hello-node]="POST"
|
||||
[simple-node]="GET"
|
||||
[simple-python]="GET"
|
||||
[notes-python]="GET"
|
||||
)
|
||||
|
||||
declare -A CHECK_URL=(
|
||||
[hello-node]="https://sless-api.kube5s.ru/fn/default/hello-http"
|
||||
[simple-node]="https://sless-api.kube5s.ru/fn/default/simple-node-time-display"
|
||||
[simple-python]="https://sless-api.kube5s.ru/fn/default/simple-py-time-display"
|
||||
[notes-python]="https://sless-api.kube5s.ru/fn/default/notes-list"
|
||||
)
|
||||
|
||||
declare -A CHECK_DATA=(
|
||||
[hello-node]='{"name":"Smoke"}'
|
||||
[simple-node]=''
|
||||
[simple-python]=''
|
||||
[notes-python]=''
|
||||
)
|
||||
|
||||
declare -a PREEXISTING_EXAMPLES=()
|
||||
LAST_LOG_FILE=""
|
||||
CURRENT_EXAMPLE=""
|
||||
CURRENT_STEP=""
|
||||
LAST_ERROR_SUMMARY=""
|
||||
|
||||
fail_run() {
|
||||
local example="$1"
|
||||
local step="$2"
|
||||
local details="$3"
|
||||
|
||||
echo
|
||||
echo "ERROR SUMMARY"
|
||||
echo "example: $example"
|
||||
echo "step: $step"
|
||||
echo "reason: $details"
|
||||
|
||||
if [ -n "$LAST_LOG_FILE" ] && [ -f "$LAST_LOG_FILE" ]; then
|
||||
echo "log: $LAST_LOG_FILE"
|
||||
echo "last log lines:"
|
||||
tail -n 20 "$LAST_LOG_FILE"
|
||||
fi
|
||||
|
||||
exit 1
|
||||
}
|
||||
|
||||
run_step() {
|
||||
local example="$1"
|
||||
local step="$2"
|
||||
shift 2
|
||||
|
||||
CURRENT_EXAMPLE="$example"
|
||||
CURRENT_STEP="$step"
|
||||
LAST_ERROR_SUMMARY=""
|
||||
|
||||
if ! "$@"; then
|
||||
local details="$LAST_ERROR_SUMMARY"
|
||||
if [ -z "$details" ]; then
|
||||
details="step failed without explicit summary"
|
||||
fi
|
||||
fail_run "$example" "$step" "$details"
|
||||
fi
|
||||
}
|
||||
|
||||
restore_any_backups() {
|
||||
local backup
|
||||
while IFS= read -r backup; do
|
||||
[ -n "$backup" ] || continue
|
||||
if [ -f "$backup" ]; then
|
||||
mv "$backup" "${backup%.copilot.bak}"
|
||||
fi
|
||||
done < <(find "$ROOT_DIR" -name '*.copilot.bak' | sort)
|
||||
}
|
||||
|
||||
trap restore_any_backups EXIT
|
||||
|
||||
clean_local_artifacts() {
|
||||
local example="$1"
|
||||
rm -rf \
|
||||
"$ROOT_DIR/$example/.terraform" \
|
||||
"$ROOT_DIR/$example/.terraform.lock.hcl" \
|
||||
"$ROOT_DIR/$example/terraform.tfstate" \
|
||||
"$ROOT_DIR/$example/terraform.tfstate.backup" \
|
||||
"$ROOT_DIR/$example"/terraform.tfstate.*.backup \
|
||||
"$ROOT_DIR/$example/dist"
|
||||
}
|
||||
|
||||
retry_tf() {
|
||||
local example="$1"
|
||||
local label="$2"
|
||||
shift 2
|
||||
|
||||
local attempt=1
|
||||
while [ "$attempt" -le 3 ]; do
|
||||
LAST_LOG_FILE="$LOG_DIR/${example//\//_}-${label// /_}-${attempt}.log"
|
||||
echo "==> [$example] $label (attempt $attempt/3)"
|
||||
|
||||
(
|
||||
cd "$ROOT_DIR/$example"
|
||||
"$@"
|
||||
) 2>&1 | tee "$LAST_LOG_FILE"
|
||||
|
||||
local status=${PIPESTATUS[0]}
|
||||
if [ "$status" -eq 0 ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if grep -Eiq 'Unauthorized|401|403' "$LAST_LOG_FILE"; then
|
||||
LAST_ERROR_SUMMARY="authorization error during $label"
|
||||
echo "[$example] authorization error during $label"
|
||||
return 41
|
||||
fi
|
||||
|
||||
if grep -Eiq 'TLS handshake timeout|tls:.*timeout|i/o timeout|Client\.Timeout exceeded while awaiting headers|context deadline exceeded|unexpected EOF' "$LAST_LOG_FILE" && [ "$attempt" -lt 3 ]; then
|
||||
attempt=$((attempt + 1))
|
||||
sleep 2
|
||||
continue
|
||||
fi
|
||||
|
||||
if grep -Eiq 'TLS handshake timeout|tls:.*timeout|i/o timeout|Client\.Timeout exceeded while awaiting headers|context deadline exceeded|unexpected EOF' "$LAST_LOG_FILE"; then
|
||||
LAST_ERROR_SUMMARY="network/provider download failure during $label after retries"
|
||||
else
|
||||
LAST_ERROR_SUMMARY="terraform command failed during $label with exit code $status"
|
||||
fi
|
||||
|
||||
return "$status"
|
||||
done
|
||||
|
||||
LAST_ERROR_SUMMARY="terraform command failed during $label after exhausting retries"
|
||||
return 1
|
||||
}
|
||||
|
||||
record_preexisting_if_needed() {
|
||||
local example="$1"
|
||||
if grep -Fq 'No changes. Your infrastructure matches the configuration.' "$LAST_LOG_FILE"; then
|
||||
PREEXISTING_EXAMPLES+=("$example")
|
||||
echo "[$example] detected preexisting remote resources on clean apply"
|
||||
fi
|
||||
}
|
||||
|
||||
probe_endpoint() {
|
||||
local example="$1"
|
||||
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
|
||||
local status_file="$LOG_DIR/${example//\//_}-endpoint-status.txt"
|
||||
local method="${CHECK_METHOD[$example]}"
|
||||
local url="${CHECK_URL[$example]}"
|
||||
local data="${CHECK_DATA[$example]}"
|
||||
|
||||
if [ "$method" = "POST" ]; then
|
||||
curl -sS -X POST -H 'Content-Type: application/json' -d "$data" -o "$body_file" -w '%{http_code}' "$url" > "$status_file"
|
||||
else
|
||||
curl -sS -o "$body_file" -w '%{http_code}' "$url" > "$status_file"
|
||||
fi
|
||||
}
|
||||
|
||||
assert_live_endpoint() {
|
||||
local example="$1"
|
||||
probe_endpoint "$example"
|
||||
|
||||
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
|
||||
local status
|
||||
status="$(cat "$LOG_DIR/${example//\//_}-endpoint-status.txt")"
|
||||
|
||||
if [ "$status" != "200" ]; then
|
||||
LAST_ERROR_SUMMARY="live endpoint check failed with HTTP $status"
|
||||
echo "[$example] live endpoint check failed with HTTP $status"
|
||||
cat "$body_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if grep -Fq 'function unreachable' "$body_file"; then
|
||||
LAST_ERROR_SUMMARY="live endpoint returned function unreachable"
|
||||
echo "[$example] live endpoint check returned unreachable function"
|
||||
cat "$body_file"
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
assert_destroyed_endpoint() {
|
||||
local example="$1"
|
||||
probe_endpoint "$example"
|
||||
|
||||
local body_file="$LOG_DIR/${example//\//_}-endpoint-body.txt"
|
||||
local status
|
||||
status="$(cat "$LOG_DIR/${example//\//_}-endpoint-status.txt")"
|
||||
|
||||
if [ "$status" = "404" ] || [ "$status" = "000" ]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if grep -Eiq 'not found|404 page not found' "$body_file"; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
if [ "$status" = "502" ] && grep -Fq 'function unreachable' "$body_file"; then
|
||||
LAST_ERROR_SUMMARY="route cleanup bug: public endpoint still exists but backend is already gone (HTTP 502 function unreachable)"
|
||||
echo "[$example] route still exists after destroy, but backend is already gone (HTTP 502 function unreachable)"
|
||||
cat "$body_file"
|
||||
return 1
|
||||
fi
|
||||
|
||||
LAST_ERROR_SUMMARY="endpoint still responds after destroy with HTTP $status"
|
||||
echo "[$example] endpoint still responds after destroy with HTTP $status"
|
||||
cat "$body_file"
|
||||
return 1
|
||||
}
|
||||
|
||||
wait_for_destroyed_endpoint() {
|
||||
local example="$1"
|
||||
local attempts=24
|
||||
local sleep_sec=5
|
||||
local try=1
|
||||
|
||||
while [ "$try" -le "$attempts" ]; do
|
||||
if assert_destroyed_endpoint "$example"; then
|
||||
echo "[$example] endpoint disappeared after destroy"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "[$example] endpoint still present after destroy, waiting (${try}/${attempts})"
|
||||
try=$((try + 1))
|
||||
sleep "$sleep_sec"
|
||||
done
|
||||
|
||||
echo "[$example] endpoint did not disappear after destroy within $((attempts * sleep_sec))s"
|
||||
if [ -z "$LAST_ERROR_SUMMARY" ]; then
|
||||
LAST_ERROR_SUMMARY="endpoint remained reachable for more than $((attempts * sleep_sec))s after destroy"
|
||||
else
|
||||
LAST_ERROR_SUMMARY="$LAST_ERROR_SUMMARY; endpoint was still published after $((attempts * sleep_sec))s"
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
backup_and_modify() {
|
||||
local example="$1"
|
||||
case "$example" in
|
||||
hello-node)
|
||||
cp "$ROOT_DIR/$example/http.tf" "$ROOT_DIR/$example/http.tf.copilot.bak"
|
||||
perl -0pi -e 's/enabled\s+=\s+true/enabled = false/' "$ROOT_DIR/$example/http.tf"
|
||||
;;
|
||||
simple-node)
|
||||
cp "$ROOT_DIR/$example/time-display.tf" "$ROOT_DIR/$example/time-display.tf.copilot.bak"
|
||||
perl -0pi -e 's/memory_mb\s+=\s+64/memory_mb = 96/' "$ROOT_DIR/$example/time-display.tf"
|
||||
;;
|
||||
simple-python)
|
||||
cp "$ROOT_DIR/$example/time-display.tf" "$ROOT_DIR/$example/time-display.tf.copilot.bak"
|
||||
perl -0pi -e 's/memory_mb\s+=\s+64/memory_mb = 96/' "$ROOT_DIR/$example/time-display.tf"
|
||||
;;
|
||||
notes-python)
|
||||
cp "$ROOT_DIR/$example/notes-list.tf" "$ROOT_DIR/$example/notes-list.tf.copilot.bak"
|
||||
perl -0pi -e 's/memory_mb\s+=\s+128/memory_mb = 160/' "$ROOT_DIR/$example/notes-list.tf"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
restore_modified_files() {
|
||||
local example="$1"
|
||||
case "$example" in
|
||||
hello-node)
|
||||
mv "$ROOT_DIR/$example/http.tf.copilot.bak" "$ROOT_DIR/$example/http.tf"
|
||||
;;
|
||||
simple-node)
|
||||
mv "$ROOT_DIR/$example/time-display.tf.copilot.bak" "$ROOT_DIR/$example/time-display.tf"
|
||||
;;
|
||||
simple-python)
|
||||
mv "$ROOT_DIR/$example/time-display.tf.copilot.bak" "$ROOT_DIR/$example/time-display.tf"
|
||||
;;
|
||||
notes-python)
|
||||
mv "$ROOT_DIR/$example/notes-list.tf.copilot.bak" "$ROOT_DIR/$example/notes-list.tf"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
run_example() {
|
||||
local example="$1"
|
||||
|
||||
echo
|
||||
echo "==== $example ===="
|
||||
|
||||
clean_local_artifacts "$example"
|
||||
run_step "$example" "terraform init" retry_tf "$example" "terraform init" terraform init -input=false -no-color
|
||||
run_step "$example" "terraform apply clean" retry_tf "$example" "terraform apply clean" terraform apply -auto-approve -input=false -no-color
|
||||
record_preexisting_if_needed "$example"
|
||||
run_step "$example" "endpoint check after clean apply" assert_live_endpoint "$example"
|
||||
|
||||
run_step "$example" "terraform destroy clean" retry_tf "$example" "terraform destroy clean" terraform destroy -auto-approve -input=false -no-color
|
||||
run_step "$example" "endpoint cleanup after clean destroy" wait_for_destroyed_endpoint "$example"
|
||||
|
||||
run_step "$example" "terraform apply second" retry_tf "$example" "terraform apply second" terraform apply -auto-approve -input=false -no-color
|
||||
run_step "$example" "endpoint check after second apply" assert_live_endpoint "$example"
|
||||
|
||||
backup_and_modify "$example"
|
||||
run_step "$example" "terraform apply modified" retry_tf "$example" "terraform apply modified" terraform apply -auto-approve -input=false -no-color
|
||||
restore_modified_files "$example"
|
||||
|
||||
run_step "$example" "terraform destroy final" retry_tf "$example" "terraform destroy final" terraform destroy -auto-approve -input=false -no-color
|
||||
run_step "$example" "endpoint cleanup after final destroy" wait_for_destroyed_endpoint "$example"
|
||||
clean_local_artifacts "$example"
|
||||
}
|
||||
|
||||
main() {
|
||||
local example
|
||||
for example in "${EXAMPLES[@]}"; do
|
||||
run_example "$example"
|
||||
done
|
||||
|
||||
if [ "${#PREEXISTING_EXAMPLES[@]}" -gt 0 ]; then
|
||||
echo
|
||||
echo "Preexisting remote resources were detected on first apply for: ${PREEXISTING_EXAMPLES[*]}"
|
||||
exit 2
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "All Terraform example lifecycles completed successfully."
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Loading…
Reference in New Issue
Block a user