1903
This commit is contained in:
parent
a768454c08
commit
4fa1e71cf2
94
POSTGRES/code/funcs-list/funcs_list.py
Normal file
94
POSTGRES/code/funcs-list/funcs_list.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# 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
POSTGRES/code/funcs-list/requirements.txt
Normal file
1
POSTGRES/code/funcs-list/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
requests==2.31.0
|
||||||
8
POSTGRES/code/pg-info/package.json
Normal file
8
POSTGRES/code/pg-info/package.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "pg-info",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "sless nodejs20 function: pg version + table info",
|
||||||
|
"dependencies": {
|
||||||
|
"pg": "8.11.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
43
POSTGRES/code/pg-info/pg_info.js
Normal file
43
POSTGRES/code/pg-info/pg_info.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// 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
POSTGRES/code/table-rw/requirements.txt
Normal file
1
POSTGRES/code/table-rw/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
psycopg2-binary==2.9.9
|
||||||
130
POSTGRES/code/table-rw/table_rw.py
Normal file
130
POSTGRES/code/table-rw/table_rw.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# 2026-03-19
|
||||||
|
# table_rw.py — чтение и запись строк в terraform_demo_table.
|
||||||
|
# Два entrypoint в одном файле: list_rows (JSON API) и add_row (HTML-страница + POST-обработчик).
|
||||||
|
# ENV: PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
|
||||||
|
|
||||||
|
def _connect():
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=os.environ["PGHOST"],
|
||||||
|
port=os.environ.get("PGPORT", "5432"),
|
||||||
|
dbname=os.environ["PGDATABASE"],
|
||||||
|
user=os.environ["PGUSER"],
|
||||||
|
password=os.environ["PGPASSWORD"],
|
||||||
|
sslmode=os.environ.get("PGSSLMODE", "require"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_rows(event):
|
||||||
|
# Возвращает все строки terraform_demo_table, отсортированные по убыванию created_at.
|
||||||
|
conn = _connect()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC"
|
||||||
|
)
|
||||||
|
rows = [dict(r) for r in cur.fetchall()]
|
||||||
|
return {"rows": rows, "count": len(rows)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _render_page(rows, message=""):
|
||||||
|
# HTML-страница с формой ввода и таблицей строк.
|
||||||
|
# message — статус последней операции (успех / ошибка).
|
||||||
|
rows_html = "".join(
|
||||||
|
f"<tr><td>{r['id']}</td><td>{r['title']}</td><td>{r['created_at']}</td></tr>"
|
||||||
|
for r in rows
|
||||||
|
)
|
||||||
|
msg_html = f'<p class="msg">{message}</p>' if message else ""
|
||||||
|
return f"""<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>pg-table-writer</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: sans-serif; max-width: 700px; margin: 40px auto; background: #111; color: #eee; }}
|
||||||
|
h1 {{ color: #7dd3fc; }}
|
||||||
|
form {{ display: flex; gap: 8px; margin-bottom: 24px; }}
|
||||||
|
input[type=text] {{ flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid #444; background: #1e1e1e; color: #eee; font-size: 15px; }}
|
||||||
|
button {{ padding: 8px 18px; background: #2563eb; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; }}
|
||||||
|
button:hover {{ background: #1d4ed8; }}
|
||||||
|
table {{ width: 100%; border-collapse: collapse; }}
|
||||||
|
th, td {{ padding: 8px 10px; border-bottom: 1px solid #333; text-align: left; }}
|
||||||
|
th {{ color: #7dd3fc; }}
|
||||||
|
.msg {{ color: #4ade80; margin-bottom: 12px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>pg-table-writer</h1>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="text" name="title" placeholder="Введите строку..." autofocus required>
|
||||||
|
<button type="submit">Добавить</button>
|
||||||
|
</form>
|
||||||
|
{msg_html}
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>#</th><th>title</th><th>created_at</th></tr></thead>
|
||||||
|
<tbody>{rows_html}</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
def add_row(event):
|
||||||
|
# GET → HTML-страница с формой и списком строк.
|
||||||
|
# POST → вставляет строку из form-поля title или JSON-поля title,
|
||||||
|
# затем возвращает обновлённую HTML-страницу.
|
||||||
|
# POST с Content-Type: application/json (curl/API) → возвращает JSON.
|
||||||
|
method = event.get("_method", "GET")
|
||||||
|
|
||||||
|
if method == "GET":
|
||||||
|
conn = _connect()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||||
|
cur.execute("SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC")
|
||||||
|
rows = [dict(r) for r in cur.fetchall()]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return _render_page(rows)
|
||||||
|
|
||||||
|
# POST — вставка строки
|
||||||
|
# Поле title приходит либо из JSON-тела, либо из application/x-www-form-urlencoded.
|
||||||
|
# Сервер уже распарсил JSON в event; form-данные приходят как event["body"] = "title=...".
|
||||||
|
title = event.get("title", "").strip()
|
||||||
|
if not title:
|
||||||
|
# Попытка распарсить form-encoded body (браузерная форма)
|
||||||
|
body = event.get("body", "")
|
||||||
|
if body.startswith("title="):
|
||||||
|
from urllib.parse import unquote_plus
|
||||||
|
title = unquote_plus(body[len("title="):].split("&")[0]).strip()
|
||||||
|
|
||||||
|
if not title:
|
||||||
|
return {"ok": False, "error": "title is required"}
|
||||||
|
|
||||||
|
conn = _connect()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO terraform_demo_table (title) VALUES (%s) RETURNING id, title, created_at::text",
|
||||||
|
(title,),
|
||||||
|
)
|
||||||
|
row = dict(cur.fetchone())
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Если запрос из браузера (form POST) — возвращаем обновлённую страницу.
|
||||||
|
# Если из curl/API — возвращаем JSON.
|
||||||
|
accept = event.get("_accept", "")
|
||||||
|
if "application/json" in accept:
|
||||||
|
return {"ok": True, "row": row}
|
||||||
|
|
||||||
|
# Перечитываем все строки для обновлённой страницы
|
||||||
|
cur.execute("SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC")
|
||||||
|
rows = [dict(r) for r in cur.fetchall()]
|
||||||
|
return _render_page(rows, message=f"Добавлено: «{row['title']}»")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
129
POSTGRES/funcs_list.py
Normal file
129
POSTGRES/funcs_list.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# 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,
|
||||||
|
}
|
||||||
|
|
||||||
@ -29,16 +29,20 @@ variable "realm" {
|
|||||||
description = "resource_realm parameter for nubes_postgres resource"
|
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" {
|
variable "pg_user" {
|
||||||
type = string
|
type = string
|
||||||
sensitive = true
|
sensitive = true
|
||||||
description = "PostgreSQL username used by sless SQL runner"
|
default = ""
|
||||||
|
description = "Только для сверки. Реальный username из nubes_postgres_user.pg_user.username. Должен совпадать с vault."
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "pg_password" {
|
variable "pg_password" {
|
||||||
type = string
|
type = string
|
||||||
sensitive = true
|
sensitive = true
|
||||||
description = "PostgreSQL password used by sless SQL runner"
|
default = ""
|
||||||
|
description = "Только для сверки. Реальный пароль из vault_secrets. Должен совпадать с tfvars."
|
||||||
}
|
}
|
||||||
|
|
||||||
provider "nubes" {
|
provider "nubes" {
|
||||||
@ -47,7 +51,7 @@ provider "nubes" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
provider "sless" {
|
provider "sless" {
|
||||||
endpoint = "https://sless-api.kube5s.ru"
|
endpoint = "https://sless.kube5s.ru"
|
||||||
token = var.api_token
|
token = var.api_token
|
||||||
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,5 +1,17 @@
|
|||||||
// 2026-03-17 17:30
|
// 2026-03-18 — добавлены locals для извлечения credentials из vault_secrets (без хардкода).
|
||||||
// resources.tf — ресурсы Postgres и однократный запуск SQL-инициализации через sless_job.
|
// Для сверки хардкод остаётся в 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 "nubes_postgres" "npg" {
|
||||||
resource_name = "teststand-pg-2"
|
resource_name = "teststand-pg-2"
|
||||||
# s3_uid = "s01325"
|
# s3_uid = "s01325"
|
||||||
@ -44,6 +56,8 @@ resource "nubes_postgres_database" "db" {
|
|||||||
}
|
}
|
||||||
|
|
||||||
# Служебная функция выполняет SQL-операторы из event_json.
|
# Служебная функция выполняет SQL-операторы из event_json.
|
||||||
|
# Credentials берутся из locals (vault_secrets) — без хардкода.
|
||||||
|
# Для сверки хардкод остаётся в terraform.tfvars.
|
||||||
resource "sless_function" "postgres_sql_runner_create_table" {
|
resource "sless_function" "postgres_sql_runner_create_table" {
|
||||||
name = "pg-create-table-runner"
|
name = "pg-create-table-runner"
|
||||||
runtime = "python3.11"
|
runtime = "python3.11"
|
||||||
@ -52,21 +66,25 @@ resource "sless_function" "postgres_sql_runner_create_table" {
|
|||||||
timeout_sec = 30
|
timeout_sec = 30
|
||||||
|
|
||||||
env_vars = {
|
env_vars = {
|
||||||
PGHOST = nubes_postgres.npg.state_out_flat["internalConnect.master"]
|
PGHOST = local.pg_host
|
||||||
PGPORT = "5432"
|
PGPORT = "5432"
|
||||||
PGDATABASE = nubes_postgres_database.db.db_name
|
PGDATABASE = local.pg_database
|
||||||
PGUSER = var.pg_user
|
PGUSER = local.pg_username
|
||||||
PGPASSWORD = var.pg_password
|
PGPASSWORD = local.pg_password
|
||||||
PGSSLMODE = "require"
|
PGSSLMODE = "require"
|
||||||
|
# Для сверки (должно совпадать с vault):
|
||||||
|
# PGUSER = var.pg_user
|
||||||
|
# PGPASSWORD = var.pg_password
|
||||||
}
|
}
|
||||||
|
|
||||||
source_dir = "${path.module}/code/sql-runner"
|
source_dir = "${path.module}/code/sql-runner"
|
||||||
}
|
}
|
||||||
|
|
||||||
resource "sless_job" "postgres_table_init_job" {
|
resource "sless_job" "postgres_table_init_job" {
|
||||||
name = "pg-create-table-job-main-v12"
|
name = "pg-create-table-job-main-v13"
|
||||||
function = sless_function.postgres_sql_runner_create_table.name
|
function = sless_function.postgres_sql_runner_create_table.name
|
||||||
wait_timeout_sec = 180
|
wait_timeout_sec = 180
|
||||||
run_id = 12
|
run_id = 13
|
||||||
|
|
||||||
event_json = jsonencode({
|
event_json = jsonencode({
|
||||||
statements = [
|
statements = [
|
||||||
@ -74,8 +92,105 @@ resource "sless_job" "postgres_table_init_job" {
|
|||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
|
||||||
depends_on = [
|
depends_on = [nubes_postgres_database.db]
|
||||||
nubes_postgres_database.db
|
}
|
||||||
]
|
|
||||||
|
# HTTP-функция на NodeJS: возвращает версию PG-сервера и счётчик строк в таблице.
|
||||||
|
# Единственная функция примера на nodejs20 — проверка что JS runtime работает.
|
||||||
|
# Доступна по URL: https://sless.kube5s.ru/fn/<namespace>/pg-info
|
||||||
|
resource "sless_function" "pg_info" {
|
||||||
|
name = "pg-info"
|
||||||
|
runtime = "nodejs20"
|
||||||
|
entrypoint = "pg_info.info"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 15
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
PGHOST = local.pg_host
|
||||||
|
PGPORT = "5432"
|
||||||
|
PGDATABASE = local.pg_database
|
||||||
|
PGUSER = local.pg_username
|
||||||
|
PGPASSWORD = local.pg_password
|
||||||
|
PGSSLMODE = "require"
|
||||||
|
}
|
||||||
|
|
||||||
|
source_dir = "${path.module}/code/pg-info"
|
||||||
|
|
||||||
|
depends_on = [sless_job.postgres_table_init_job]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "pg_info_http" {
|
||||||
|
name = "pg-info-http"
|
||||||
|
type = "http"
|
||||||
|
function = sless_function.pg_info.name
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP-функции чтения и записи строк terraform_demo_table — в одном файле table_rw.py.
|
||||||
|
# list_rows (GET) — читает все строки; add_row (POST {title}) — вставляет строку.
|
||||||
|
# Доступны по URL: https://sless.kube5s.ru/fn/<namespace>/pg-table-reader
|
||||||
|
# https://sless.kube5s.ru/fn/<namespace>/pg-table-writer
|
||||||
|
resource "sless_function" "postgres_table_reader" {
|
||||||
|
name = "pg-table-reader"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "table_rw.list_rows"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 30
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
PGHOST = local.pg_host
|
||||||
|
PGPORT = "5432"
|
||||||
|
PGDATABASE = local.pg_database
|
||||||
|
PGUSER = local.pg_username
|
||||||
|
PGPASSWORD = local.pg_password
|
||||||
|
PGSSLMODE = "require"
|
||||||
|
}
|
||||||
|
|
||||||
|
source_dir = "${path.module}/code/table-rw"
|
||||||
|
|
||||||
|
depends_on = [sless_job.postgres_table_init_job]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "postgres_table_reader_http" {
|
||||||
|
name = "pg-table-reader-http"
|
||||||
|
type = "http"
|
||||||
|
function = sless_function.postgres_table_reader.name
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "table_reader_url" {
|
||||||
|
value = sless_trigger.postgres_table_reader_http.url
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_function" "postgres_table_writer" {
|
||||||
|
name = "pg-table-writer"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "table_rw.add_row"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 30
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
PGHOST = local.pg_host
|
||||||
|
PGPORT = "5432"
|
||||||
|
PGDATABASE = local.pg_database
|
||||||
|
PGUSER = local.pg_username
|
||||||
|
PGPASSWORD = local.pg_password
|
||||||
|
PGSSLMODE = "require"
|
||||||
|
}
|
||||||
|
|
||||||
|
source_dir = "${path.module}/code/table-rw"
|
||||||
|
|
||||||
|
depends_on = [sless_job.postgres_table_init_job]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "postgres_table_writer_http" {
|
||||||
|
name = "pg-table-writer-http"
|
||||||
|
type = "http"
|
||||||
|
function = sless_function.postgres_table_writer.name
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "table_writer_url" {
|
||||||
|
value = sless_trigger.postgres_table_writer_http.url
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
51
POSTGRES/scripts/pg-debug-pod.yaml
Normal file
51
POSTGRES/scripts/pg-debug-pod.yaml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# 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"
|
||||||
14
README.md
14
README.md
@ -12,7 +12,7 @@
|
|||||||
| `sless_trigger` | Публикует функцию: тип `http` создаёт публичный URL, тип `cron` — запуск по расписанию. |
|
| `sless_trigger` | Публикует функцию: тип `http` создаёт публичный URL, тип `cron` — запуск по расписанию. |
|
||||||
| `sless_job` | Запускает функцию однократно и ожидает завершения. Используется для одноразовых операций: инициализация БД, миграции, пакетная обработка. |
|
| `sless_job` | Запускает функцию однократно и ожидает завершения. Используется для одноразовых операций: инициализация БД, миграции, пакетная обработка. |
|
||||||
|
|
||||||
Стандартная связка для HTTP API: `sless_function` + `sless_trigger` с `type = "http"` — в результате функция доступна по URL вида `https://sless-api.kube5s.ru/fn/<namespace>/<имя-функции>`.
|
Стандартная связка для HTTP API: `sless_function` + `sless_trigger` с `type = "http"` — в результате функция доступна по URL вида `https://sless.kube5s.ru/fn/<namespace>/<имя-функции>`.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -20,7 +20,7 @@
|
|||||||
|
|
||||||
- Terraform >= 1.0
|
- Terraform >= 1.0
|
||||||
- JWT-токен для аутентификации в sless API
|
- JWT-токен для аутентификации в sless API
|
||||||
- Доступ к `https://sless-api.kube5s.ru`
|
- Доступ к `https://sless.kube5s.ru`
|
||||||
|
|
||||||
## Конфигурация провайдера
|
## Конфигурация провайдера
|
||||||
|
|
||||||
@ -28,9 +28,9 @@
|
|||||||
|
|
||||||
```hcl
|
```hcl
|
||||||
provider "sless" {
|
provider "sless" {
|
||||||
endpoint = "https://sless-api.kube5s.ru"
|
endpoint = "https://sless.kube5s.ru"
|
||||||
token = var.token
|
token = var.token
|
||||||
nubes_endpoint = "https://deck-api.ngcloud.ru/api/v1"
|
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -56,7 +56,7 @@ terraform init
|
|||||||
terraform apply -auto-approve
|
terraform apply -auto-approve
|
||||||
|
|
||||||
# Вызов HTTP-функции с передачей имени:
|
# Вызов HTTP-функции с передачей имени:
|
||||||
curl -s -X POST https://sless-api.kube5s.ru/fn/<namespace>/hello-http \
|
curl -s -X POST https://sless.kube5s.ru/fn/<namespace>/hello-http \
|
||||||
-H 'Content-Type: application/json' -d '{"name":"World"}'
|
-H 'Content-Type: application/json' -d '{"name":"World"}'
|
||||||
|
|
||||||
# Результат задания:
|
# Результат задания:
|
||||||
@ -114,7 +114,7 @@ terraform init
|
|||||||
terraform apply -auto-approve
|
terraform apply -auto-approve
|
||||||
|
|
||||||
terraform output job_result
|
terraform output job_result
|
||||||
curl -s https://sless-api.kube5s.ru/fn/<namespace>/simple-py-time-display
|
curl -s https://sless.kube5s.ru/fn/<namespace>/simple-py-time-display
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -127,7 +127,7 @@ terraform init
|
|||||||
terraform apply -auto-approve
|
terraform apply -auto-approve
|
||||||
|
|
||||||
terraform output job_result
|
terraform output job_result
|
||||||
curl -s https://sless-api.kube5s.ru/fn/<namespace>/simple-node-time-display
|
curl -s https://sless.kube5s.ru/fn/<namespace>/simple-node-time-display
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
94
TNAR/code/funcs-list/funcs_list.py
Normal file
94
TNAR/code/funcs-list/funcs_list.py
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
# 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
TNAR/code/funcs-list/requirements.txt
Normal file
1
TNAR/code/funcs-list/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
requests==2.31.0
|
||||||
8
TNAR/code/pg-info/package.json
Normal file
8
TNAR/code/pg-info/package.json
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
{
|
||||||
|
"name": "pg-info",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "sless nodejs20 function: pg version + table info",
|
||||||
|
"dependencies": {
|
||||||
|
"pg": "8.11.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
43
TNAR/code/pg-info/pg_info.js
Normal file
43
TNAR/code/pg-info/pg_info.js
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
|
};
|
||||||
3
TNAR/code/sql-runner/requirements.txt
Normal file
3
TNAR/code/sql-runner/requirements.txt
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
# 2026-03-17 00:00
|
||||||
|
# requirements.txt — зависимости для функции запуска SQL.
|
||||||
|
psycopg2-binary==2.9.9
|
||||||
39
TNAR/code/sql-runner/sql_runner.py
Normal file
39
TNAR/code/sql-runner/sql_runner.py
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
# 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
TNAR/code/table-rw/requirements.txt
Normal file
1
TNAR/code/table-rw/requirements.txt
Normal file
@ -0,0 +1 @@
|
|||||||
|
psycopg2-binary==2.9.9
|
||||||
130
TNAR/code/table-rw/table_rw.py
Normal file
130
TNAR/code/table-rw/table_rw.py
Normal file
@ -0,0 +1,130 @@
|
|||||||
|
# 2026-03-19
|
||||||
|
# table_rw.py — чтение и запись строк в terraform_demo_table.
|
||||||
|
# Два entrypoint в одном файле: list_rows (JSON API) и add_row (HTML-страница + POST-обработчик).
|
||||||
|
# ENV: PGHOST, PGPORT, PGDATABASE, PGUSER, PGPASSWORD, PGSSLMODE
|
||||||
|
|
||||||
|
import os
|
||||||
|
import json
|
||||||
|
import psycopg2
|
||||||
|
import psycopg2.extras
|
||||||
|
|
||||||
|
|
||||||
|
def _connect():
|
||||||
|
return psycopg2.connect(
|
||||||
|
host=os.environ["PGHOST"],
|
||||||
|
port=os.environ.get("PGPORT", "5432"),
|
||||||
|
dbname=os.environ["PGDATABASE"],
|
||||||
|
user=os.environ["PGUSER"],
|
||||||
|
password=os.environ["PGPASSWORD"],
|
||||||
|
sslmode=os.environ.get("PGSSLMODE", "require"),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def list_rows(event):
|
||||||
|
# Возвращает все строки terraform_demo_table, отсортированные по убыванию created_at.
|
||||||
|
conn = _connect()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||||
|
cur.execute(
|
||||||
|
"SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC"
|
||||||
|
)
|
||||||
|
rows = [dict(r) for r in cur.fetchall()]
|
||||||
|
return {"rows": rows, "count": len(rows)}
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def _render_page(rows, message=""):
|
||||||
|
# HTML-страница с формой ввода и таблицей строк.
|
||||||
|
# message — статус последней операции (успех / ошибка).
|
||||||
|
rows_html = "".join(
|
||||||
|
f"<tr><td>{r['id']}</td><td>{r['title']}</td><td>{r['created_at']}</td></tr>"
|
||||||
|
for r in rows
|
||||||
|
)
|
||||||
|
msg_html = f'<p class="msg">{message}</p>' if message else ""
|
||||||
|
return f"""<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>pg-table-writer</title>
|
||||||
|
<style>
|
||||||
|
body {{ font-family: sans-serif; max-width: 700px; margin: 40px auto; background: #111; color: #eee; }}
|
||||||
|
h1 {{ color: #7dd3fc; }}
|
||||||
|
form {{ display: flex; gap: 8px; margin-bottom: 24px; }}
|
||||||
|
input[type=text] {{ flex: 1; padding: 8px 12px; border-radius: 6px; border: 1px solid #444; background: #1e1e1e; color: #eee; font-size: 15px; }}
|
||||||
|
button {{ padding: 8px 18px; background: #2563eb; color: #fff; border: none; border-radius: 6px; cursor: pointer; font-size: 15px; }}
|
||||||
|
button:hover {{ background: #1d4ed8; }}
|
||||||
|
table {{ width: 100%; border-collapse: collapse; }}
|
||||||
|
th, td {{ padding: 8px 10px; border-bottom: 1px solid #333; text-align: left; }}
|
||||||
|
th {{ color: #7dd3fc; }}
|
||||||
|
.msg {{ color: #4ade80; margin-bottom: 12px; }}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<h1>pg-table-writer</h1>
|
||||||
|
<form method="POST">
|
||||||
|
<input type="text" name="title" placeholder="Введите строку..." autofocus required>
|
||||||
|
<button type="submit">Добавить</button>
|
||||||
|
</form>
|
||||||
|
{msg_html}
|
||||||
|
<table>
|
||||||
|
<thead><tr><th>#</th><th>title</th><th>created_at</th></tr></thead>
|
||||||
|
<tbody>{rows_html}</tbody>
|
||||||
|
</table>
|
||||||
|
</body>
|
||||||
|
</html>"""
|
||||||
|
|
||||||
|
|
||||||
|
def add_row(event):
|
||||||
|
# GET → HTML-страница с формой и списком строк.
|
||||||
|
# POST → вставляет строку из form-поля title или JSON-поля title,
|
||||||
|
# затем возвращает обновлённую HTML-страницу.
|
||||||
|
# POST с Content-Type: application/json (curl/API) → возвращает JSON.
|
||||||
|
method = event.get("_method", "GET")
|
||||||
|
|
||||||
|
if method == "GET":
|
||||||
|
conn = _connect()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||||
|
cur.execute("SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC")
|
||||||
|
rows = [dict(r) for r in cur.fetchall()]
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return _render_page(rows)
|
||||||
|
|
||||||
|
# POST — вставка строки
|
||||||
|
# Поле title приходит либо из JSON-тела, либо из application/x-www-form-urlencoded.
|
||||||
|
# Сервер уже распарсил JSON в event; form-данные приходят как event["body"] = "title=...".
|
||||||
|
title = event.get("title", "").strip()
|
||||||
|
if not title:
|
||||||
|
# Попытка распарсить form-encoded body (браузерная форма)
|
||||||
|
body = event.get("body", "")
|
||||||
|
if body.startswith("title="):
|
||||||
|
from urllib.parse import unquote_plus
|
||||||
|
title = unquote_plus(body[len("title="):].split("&")[0]).strip()
|
||||||
|
|
||||||
|
if not title:
|
||||||
|
return {"ok": False, "error": "title is required"}
|
||||||
|
|
||||||
|
conn = _connect()
|
||||||
|
try:
|
||||||
|
cur = conn.cursor(cursor_factory=psycopg2.extras.RealDictCursor)
|
||||||
|
cur.execute(
|
||||||
|
"INSERT INTO terraform_demo_table (title) VALUES (%s) RETURNING id, title, created_at::text",
|
||||||
|
(title,),
|
||||||
|
)
|
||||||
|
row = dict(cur.fetchone())
|
||||||
|
conn.commit()
|
||||||
|
|
||||||
|
# Если запрос из браузера (form POST) — возвращаем обновлённую страницу.
|
||||||
|
# Если из curl/API — возвращаем JSON.
|
||||||
|
accept = event.get("_accept", "")
|
||||||
|
if "application/json" in accept:
|
||||||
|
return {"ok": True, "row": row}
|
||||||
|
|
||||||
|
# Перечитываем все строки для обновлённой страницы
|
||||||
|
cur.execute("SELECT id, title, created_at::text FROM terraform_demo_table ORDER BY created_at DESC")
|
||||||
|
rows = [dict(r) for r in cur.fetchall()]
|
||||||
|
return _render_page(rows, message=f"Добавлено: «{row['title']}»")
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
129
TNAR/funcs_list.py
Normal file
129
TNAR/funcs_list.py
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
# 2026-03-18 (обновлено: фильтрация SLESS_EXCLUDE, читаемый вывод через "#"-ключ)
|
||||||
|
# funcs_list.py — HTTP-функция: список всех пользовательских функций с их статусами.
|
||||||
|
# Вызывает внутренний REST API оператора (ClusterIP, без TLS).
|
||||||
|
# Объединяет данные функций и триггеров в один ответ; скрывает служебные функции.
|
||||||
|
#
|
||||||
|
# Env vars:
|
||||||
|
# SLESS_API_URL — URL оператора (http://sless-operator.sless.svc.cluster.local:9090)
|
||||||
|
# SLESS_NAMESPACE — namespace пользователя (sless-{hex16})
|
||||||
|
# SLESS_TOKEN — JWT токен для /v1/ API
|
||||||
|
# SLESS_EXTERNAL_URL — публичный базовый URL (https://sless.kube5s.ru), для корректных ссылок
|
||||||
|
# SLESS_EXCLUDE — comma-separated имена функций, которые не надо показывать
|
||||||
|
# Пример: "funcs,event-writer,event-monitor,event-cleaner"
|
||||||
|
#
|
||||||
|
# Формат вывода: JSON-объект, где каждая функция содержит поле "#" — краткий комментарий.
|
||||||
|
# При pretty-print (python3 -m json.tool) выглядит как читаемый список с аннотациями.
|
||||||
|
|
||||||
|
import os
|
||||||
|
import requests
|
||||||
|
|
||||||
|
|
||||||
|
def _short_comment(fn, http_triggers, cron_triggers):
|
||||||
|
"""Генерирует однострочный комментарий-описание функции по её метаданным."""
|
||||||
|
phase = fn.get("phase", "")
|
||||||
|
runtime = fn.get("runtime", "")
|
||||||
|
|
||||||
|
if http_triggers:
|
||||||
|
active_str = "активна" if http_triggers[0].get("active") else "неактивна"
|
||||||
|
return f"HTTP endpoint ({runtime}) — {phase}, {active_str}"
|
||||||
|
elif cron_triggers:
|
||||||
|
schedule = cron_triggers[0].get("schedule", "?")
|
||||||
|
active_str = "активна" if cron_triggers[0].get("active") else "неактивна"
|
||||||
|
return f"Cron '{schedule}' ({runtime}) — {phase}, {active_str}"
|
||||||
|
else:
|
||||||
|
return f"Job/runner без триггера ({runtime}) — {phase}"
|
||||||
|
|
||||||
|
|
||||||
|
def list_all(event):
|
||||||
|
api_url = os.environ["SLESS_API_URL"].rstrip("/")
|
||||||
|
namespace = os.environ["SLESS_NAMESPACE"]
|
||||||
|
token = os.environ["SLESS_TOKEN"]
|
||||||
|
ext_url = os.environ.get("SLESS_EXTERNAL_URL", "").rstrip("/")
|
||||||
|
|
||||||
|
# Имена функций, которые не должны присутствовать в выводе.
|
||||||
|
# Включает саму себя ("funcs") и служебные функции других примеров.
|
||||||
|
exclude = {
|
||||||
|
n.strip()
|
||||||
|
for n in os.environ.get("SLESS_EXCLUDE", "").split(",")
|
||||||
|
if n.strip()
|
||||||
|
}
|
||||||
|
|
||||||
|
headers = {"Authorization": f"Bearer {token}"}
|
||||||
|
|
||||||
|
fns_resp = requests.get(
|
||||||
|
f"{api_url}/v1/namespaces/{namespace}/functions",
|
||||||
|
headers=headers,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
fns_resp.raise_for_status()
|
||||||
|
|
||||||
|
trs_resp = requests.get(
|
||||||
|
f"{api_url}/v1/namespaces/{namespace}/triggers",
|
||||||
|
headers=headers,
|
||||||
|
timeout=10,
|
||||||
|
)
|
||||||
|
trs_resp.raise_for_status()
|
||||||
|
|
||||||
|
# Индекс триггеров по имени функции
|
||||||
|
triggers_by_fn = {}
|
||||||
|
for tr in trs_resp.json():
|
||||||
|
fn_name = tr.get("function") or tr.get("functionRef")
|
||||||
|
if fn_name:
|
||||||
|
triggers_by_fn.setdefault(fn_name, []).append(tr)
|
||||||
|
|
||||||
|
result = []
|
||||||
|
for fn in fns_resp.json():
|
||||||
|
name = fn["name"]
|
||||||
|
if name in exclude:
|
||||||
|
continue
|
||||||
|
|
||||||
|
http_triggers = [
|
||||||
|
t for t in triggers_by_fn.get(name, []) if t.get("type") == "http"
|
||||||
|
]
|
||||||
|
cron_triggers = [
|
||||||
|
t for t in triggers_by_fn.get(name, []) if t.get("type") == "cron"
|
||||||
|
]
|
||||||
|
is_active = any(
|
||||||
|
t.get("enabled", True) and t.get("active", False)
|
||||||
|
for t in triggers_by_fn.get(name, [])
|
||||||
|
)
|
||||||
|
|
||||||
|
entry = {
|
||||||
|
# "#" — первый ключ: служит визуальным комментарием при pretty-print
|
||||||
|
"#": _short_comment(fn, http_triggers, cron_triggers),
|
||||||
|
"name": name,
|
||||||
|
"runtime": fn.get("runtime"),
|
||||||
|
"phase": fn.get("phase"),
|
||||||
|
"active": is_active,
|
||||||
|
}
|
||||||
|
|
||||||
|
# URL вычисляем из SLESS_EXTERNAL_URL если задан — state может хранить старый домен
|
||||||
|
if http_triggers:
|
||||||
|
if ext_url:
|
||||||
|
entry["url"] = f"{ext_url}/fn/{namespace}/{name}"
|
||||||
|
else:
|
||||||
|
entry["url"] = http_triggers[0].get("url", "")
|
||||||
|
|
||||||
|
if cron_triggers:
|
||||||
|
entry["cron"] = cron_triggers[0].get("schedule", "")
|
||||||
|
|
||||||
|
if fn.get("message"):
|
||||||
|
entry["message"] = fn["message"]
|
||||||
|
|
||||||
|
# created_at и last_built_at — доступны после обновления оператора до v0.1.32+
|
||||||
|
if fn.get("created_at"):
|
||||||
|
entry["created_at"] = fn["created_at"]
|
||||||
|
if fn.get("last_built_at"):
|
||||||
|
entry["last_built_at"] = fn["last_built_at"]
|
||||||
|
|
||||||
|
result.append(entry)
|
||||||
|
|
||||||
|
# Сортировка: активные вверх, затем по имени
|
||||||
|
result.sort(key=lambda f: (not f["active"], f["name"]))
|
||||||
|
|
||||||
|
return {
|
||||||
|
"namespace": namespace,
|
||||||
|
"count": len(result),
|
||||||
|
"functions": result,
|
||||||
|
}
|
||||||
|
|
||||||
73
TNAR/luceUNDnode.tf
Normal file
73
TNAR/luceUNDnode.tf
Normal file
@ -0,0 +1,73 @@
|
|||||||
|
|
||||||
|
# resource "nubes_lucee" "app1" {
|
||||||
|
# # Lucee-приложение, зависит от Postgres
|
||||||
|
# resource_name = "lucy_teststand_0"
|
||||||
|
# # resource_realm = "k8s-3.ext.nubes.ru"
|
||||||
|
# resource_realm = nubes_postgres.db2.resource_realm
|
||||||
|
# # resource_realm = "k8s-4-sandbox-nubes-ru"
|
||||||
|
# domain = "web-test-stand"
|
||||||
|
|
||||||
|
# git_path = "https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/testlucee"
|
||||||
|
|
||||||
|
# json_env = jsonencode({
|
||||||
|
# # 🔗 Настройки Data Source 'testds' для Lucee (Application.cfc)
|
||||||
|
# testds_class = "org.postgresql.Driver" # 📂 Драйвер БД
|
||||||
|
# testds_bundleName = "org.postgresql.jdbc" # 📦 Имя бандла JDBC
|
||||||
|
# testds_bundleVersion = "42.6.0" # 🔢 Версия драйвера
|
||||||
|
# testds_connectionString = "jdbc:postgresql://${nubes_postgres.db2.state_out_flat["internalConnect.master"]}:5432/postgres?sslmode=require" # 🚀 Строка подключения
|
||||||
|
# testds_username = nubes_postgres_user.db2_user.username # 👤 Логин
|
||||||
|
# testds_password = jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"] # 🔑 Пароль
|
||||||
|
# testds_connectionLimit = "5" # 🚦 Лимит соединений
|
||||||
|
# testds_liveTimeout = "15" # ⏳ Таймаут жизни
|
||||||
|
# testds_validate = "false" # ✅ Валидация при запросе
|
||||||
|
# })
|
||||||
|
|
||||||
|
# resource_c_p_u = 300
|
||||||
|
# resource_memory = 512
|
||||||
|
# resource_instances = 1
|
||||||
|
# app_version = "5.4"
|
||||||
|
|
||||||
|
# depends_on = [nubes_postgres.db2]
|
||||||
|
# }
|
||||||
|
|
||||||
|
# resource "nubes_nodejs" "app3" {
|
||||||
|
# # NodeJS демо, работающий с тем же Postgres.
|
||||||
|
# resource_name = "node_01"
|
||||||
|
# resource_realm = nubes_postgres.db2.resource_realm
|
||||||
|
# domain = "node07"
|
||||||
|
# git_path = "https://gitea-naeel.giteak8s.services.ngcloud.ru/naeel/testnode.git"
|
||||||
|
# health_path = "/healthz"
|
||||||
|
# app_version = "23"
|
||||||
|
|
||||||
|
# json_env = jsonencode({
|
||||||
|
# # Переменные подключения к Postgres.
|
||||||
|
# PGHOST = nubes_postgres.db2.state_out_flat["internalConnect.master"]
|
||||||
|
# PGPORT = "5432"
|
||||||
|
# PGUSER = nubes_postgres_user.db2_user.username
|
||||||
|
# PGPASSWORD = jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"]
|
||||||
|
# PGDATABASE = nubes_postgres_database.db2_app.db_name
|
||||||
|
# PGSSLMODE = "require"
|
||||||
|
# DATABASE_URL = format(
|
||||||
|
# "postgresql://%s:%s@%s:5432/%s?sslmode=require",
|
||||||
|
# nubes_postgres_user.db2_user.username,
|
||||||
|
# jsondecode(nubes_postgres.db2.vault_secrets["users"])[nubes_postgres_user.db2_user.username]["password"],
|
||||||
|
# nubes_postgres.db2.state_out_flat["internalConnect.master"],
|
||||||
|
# nubes_postgres_database.db2_app.db_name
|
||||||
|
# )
|
||||||
|
# })
|
||||||
|
|
||||||
|
# resource_c_p_u = 300
|
||||||
|
# resource_memory = 256
|
||||||
|
# resource_instances = 1
|
||||||
|
|
||||||
|
# depends_on = [nubes_postgres.db2]
|
||||||
|
# }
|
||||||
|
|
||||||
|
# output "pg_vault_secrets" {
|
||||||
|
# value = nubes_postgres.db2.vault_secrets
|
||||||
|
# sensitive = true
|
||||||
|
# }
|
||||||
|
|
||||||
|
# terraform output -json pg_vault_secrets
|
||||||
|
|
||||||
|
|
||||||
58
TNAR/main.tf
Normal file
58
TNAR/main.tf
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
// 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-api-test.ngcloud.ru/api/v1/index.cfm"
|
||||||
|
}
|
||||||
|
|
||||||
|
provider "sless" {
|
||||||
|
endpoint = "https://sless.kube5s.ru"
|
||||||
|
token = var.api_token
|
||||||
|
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||||
|
}
|
||||||
|
|
||||||
196
TNAR/resources.tf
Normal file
196
TNAR/resources.tf
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
// 2026-03-18 — добавлены locals для извлечения credentials из vault_secrets (без хардкода).
|
||||||
|
// Для сверки хардкод остаётся в terraform.tfvars на этапе разработки.
|
||||||
|
// sless_function и sless_job закомментированы — сначала проверяется сетевое соединение.
|
||||||
|
|
||||||
|
# Актуальные credentials из vault_secrets (authoritatively) — vault синхронизирован с кластером.
|
||||||
|
# Структура vault_secrets["users"]: JSON-строка {"username": {"password": "...", "username": "..."}}
|
||||||
|
locals {
|
||||||
|
pg_creds_map = jsondecode(nubes_postgres.npg.vault_secrets["users"])
|
||||||
|
pg_username = nubes_postgres_user.pg_user.username
|
||||||
|
pg_password = local.pg_creds_map[local.pg_username]["password"]
|
||||||
|
pg_host = nubes_postgres.npg.state_out_flat["internalConnect.master"]
|
||||||
|
pg_database = nubes_postgres_database.db.db_name
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "nubes_postgres" "npg" {
|
||||||
|
resource_name = "testnarod-pg-0"
|
||||||
|
# s3_uid = "s01325"
|
||||||
|
s3_uid = var.s3_uid
|
||||||
|
resource_realm = var.realm
|
||||||
|
resource_instances = 1
|
||||||
|
resource_memory = 512
|
||||||
|
resource_c_p_u = 500
|
||||||
|
resource_disk = "1"
|
||||||
|
app_version = "17"
|
||||||
|
json_parameters = jsonencode({
|
||||||
|
log_connections = "off"
|
||||||
|
log_disconnections = "off"
|
||||||
|
})
|
||||||
|
enable_pg_pooler_master = false
|
||||||
|
enable_pg_pooler_slave = false
|
||||||
|
allow_no_s_s_l = false
|
||||||
|
auto_scale = false
|
||||||
|
auto_scale_percentage = 10
|
||||||
|
auto_scale_tech_window = 0
|
||||||
|
auto_scale_quota_gb = "1"
|
||||||
|
need_external_address_master = false
|
||||||
|
|
||||||
|
# suspend_on_destroy = false
|
||||||
|
operation_timeout = "11m"
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "nubes_postgres_user" "pg_user" {
|
||||||
|
postgres_id = nubes_postgres.npg.id
|
||||||
|
username = "u-user0"
|
||||||
|
role = "ddl_user"
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "nubes_postgres_database" "db" {
|
||||||
|
postgres_id = nubes_postgres.npg.id
|
||||||
|
db_name = "db_terra"
|
||||||
|
db_owner = nubes_postgres_user.pg_user.username
|
||||||
|
adopt_existing_on_create = true
|
||||||
|
# suspend_on_destroy = false
|
||||||
|
}
|
||||||
|
|
||||||
|
# Служебная функция выполняет SQL-операторы из event_json.
|
||||||
|
# Credentials берутся из locals (vault_secrets) — без хардкода.
|
||||||
|
# Для сверки хардкод остаётся в terraform.tfvars.
|
||||||
|
resource "sless_function" "postgres_sql_runner_create_table" {
|
||||||
|
name = "pg-create-table-runner"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "sql_runner.run_sql"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 30
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
PGHOST = local.pg_host
|
||||||
|
PGPORT = "5432"
|
||||||
|
PGDATABASE = local.pg_database
|
||||||
|
PGUSER = local.pg_username
|
||||||
|
PGPASSWORD = local.pg_password
|
||||||
|
PGSSLMODE = "require"
|
||||||
|
# Для сверки (должно совпадать с vault):
|
||||||
|
# PGUSER = var.pg_user
|
||||||
|
# PGPASSWORD = var.pg_password
|
||||||
|
}
|
||||||
|
|
||||||
|
source_dir = "${path.module}/code/sql-runner"
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_job" "postgres_table_init_job" {
|
||||||
|
name = "pg-create-table-job-main-v13"
|
||||||
|
function = sless_function.postgres_sql_runner_create_table.name
|
||||||
|
wait_timeout_sec = 180
|
||||||
|
run_id = 13
|
||||||
|
|
||||||
|
event_json = jsonencode({
|
||||||
|
statements = [
|
||||||
|
"CREATE TABLE IF NOT EXISTS terraform_demo_table (id serial PRIMARY KEY, title text NOT NULL, created_at timestamp DEFAULT now())"
|
||||||
|
]
|
||||||
|
})
|
||||||
|
|
||||||
|
depends_on = [nubes_postgres_database.db]
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP-функция на NodeJS: возвращает версию PG-сервера и счётчик строк в таблице.
|
||||||
|
# Единственная функция примера на nodejs20 — проверка что JS runtime работает.
|
||||||
|
# Доступна по URL: https://sless.kube5s.ru/fn/<namespace>/pg-info
|
||||||
|
resource "sless_function" "pg_info" {
|
||||||
|
name = "pg-info"
|
||||||
|
runtime = "nodejs20"
|
||||||
|
entrypoint = "pg_info.info"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 15
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
PGHOST = local.pg_host
|
||||||
|
PGPORT = "5432"
|
||||||
|
PGDATABASE = local.pg_database
|
||||||
|
PGUSER = local.pg_username
|
||||||
|
PGPASSWORD = local.pg_password
|
||||||
|
PGSSLMODE = "require"
|
||||||
|
}
|
||||||
|
|
||||||
|
source_dir = "${path.module}/code/pg-info"
|
||||||
|
|
||||||
|
depends_on = [sless_job.postgres_table_init_job]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "pg_info_http" {
|
||||||
|
name = "pg-info-http"
|
||||||
|
type = "http"
|
||||||
|
function = sless_function.pg_info.name
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
# HTTP-функции чтения и записи строк terraform_demo_table — в одном файле table_rw.py.
|
||||||
|
# list_rows (GET) — читает все строки; add_row (POST {title}) — вставляет строку.
|
||||||
|
# Доступны по URL: https://sless.kube5s.ru/fn/<namespace>/pg-table-reader
|
||||||
|
# https://sless.kube5s.ru/fn/<namespace>/pg-table-writer
|
||||||
|
resource "sless_function" "postgres_table_reader" {
|
||||||
|
name = "pg-table-reader"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "table_rw.list_rows"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 30
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
PGHOST = local.pg_host
|
||||||
|
PGPORT = "5432"
|
||||||
|
PGDATABASE = local.pg_database
|
||||||
|
PGUSER = local.pg_username
|
||||||
|
PGPASSWORD = local.pg_password
|
||||||
|
PGSSLMODE = "require"
|
||||||
|
}
|
||||||
|
|
||||||
|
source_dir = "${path.module}/code/table-rw"
|
||||||
|
|
||||||
|
depends_on = [sless_job.postgres_table_init_job]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "postgres_table_reader_http" {
|
||||||
|
name = "pg-table-reader-http"
|
||||||
|
type = "http"
|
||||||
|
function = sless_function.postgres_table_reader.name
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "table_reader_url" {
|
||||||
|
value = sless_trigger.postgres_table_reader_http.url
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_function" "postgres_table_writer" {
|
||||||
|
name = "pg-table-writer"
|
||||||
|
runtime = "python3.11"
|
||||||
|
entrypoint = "table_rw.add_row"
|
||||||
|
memory_mb = 128
|
||||||
|
timeout_sec = 30
|
||||||
|
|
||||||
|
env_vars = {
|
||||||
|
PGHOST = local.pg_host
|
||||||
|
PGPORT = "5432"
|
||||||
|
PGDATABASE = local.pg_database
|
||||||
|
PGUSER = local.pg_username
|
||||||
|
PGPASSWORD = local.pg_password
|
||||||
|
PGSSLMODE = "require"
|
||||||
|
}
|
||||||
|
|
||||||
|
source_dir = "${path.module}/code/table-rw"
|
||||||
|
|
||||||
|
depends_on = [sless_job.postgres_table_init_job]
|
||||||
|
}
|
||||||
|
|
||||||
|
resource "sless_trigger" "postgres_table_writer_http" {
|
||||||
|
name = "pg-table-writer-http"
|
||||||
|
type = "http"
|
||||||
|
function = sless_function.postgres_table_writer.name
|
||||||
|
enabled = true
|
||||||
|
}
|
||||||
|
|
||||||
|
output "table_writer_url" {
|
||||||
|
value = sless_trigger.postgres_table_writer_http.url
|
||||||
|
}
|
||||||
|
|
||||||
51
TNAR/scripts/pg-debug-pod.yaml
Normal file
51
TNAR/scripts/pg-debug-pod.yaml
Normal file
@ -0,0 +1,51 @@
|
|||||||
|
# 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"
|
||||||
40
TNAR/scripts/read_pg_user_secret.py
Normal file
40
TNAR/scripts/read_pg_user_secret.py
Normal file
@ -0,0 +1,40 @@
|
|||||||
|
# 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()
|
||||||
194
demo-managed-functions/README.md
Normal file
194
demo-managed-functions/README.md
Normal file
@ -0,0 +1,194 @@
|
|||||||
|
# Примеры использования 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/ — исходный код функций
|
||||||
|
```
|
||||||
@ -2,8 +2,8 @@
|
|||||||
# Скопируй в terraform.tfvars и подставь актуальный токен.
|
# Скопируй в terraform.tfvars и подставь актуальный токен.
|
||||||
|
|
||||||
token = "PUT_TOKEN_HERE"
|
token = "PUT_TOKEN_HERE"
|
||||||
sless_endpoint = "http://sless-api.185.247.187.147.nip.io"
|
sless_endpoint = "https://sless-api.kube5s.ru"
|
||||||
nubes_endpoint = "https://deck-test.ngcloud.ru/api/v1"
|
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"
|
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/"
|
rabbitmq_url = "amqp://sless:sless123@rabbitmq.sless.svc.cluster.local:5672/"
|
||||||
|
|||||||
@ -10,7 +10,7 @@ variable "token" {
|
|||||||
variable "sless_endpoint" {
|
variable "sless_endpoint" {
|
||||||
description = "Endpoint sless API"
|
description = "Endpoint sless API"
|
||||||
type = string
|
type = string
|
||||||
default = "https://sless-api.kube5s.ru"
|
default = "https://sless.kube5s.ru"
|
||||||
}
|
}
|
||||||
|
|
||||||
variable "nubes_endpoint" {
|
variable "nubes_endpoint" {
|
||||||
|
|||||||
@ -11,7 +11,7 @@ terraform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
provider "sless" {
|
provider "sless" {
|
||||||
endpoint = "https://sless-api.kube5s.ru"
|
endpoint = "https://sless.kube5s.ru"
|
||||||
token = var.token
|
token = var.token
|
||||||
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -17,7 +17,7 @@ terraform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
provider "sless" {
|
provider "sless" {
|
||||||
endpoint = "https://sless-api.kube5s.ru"
|
endpoint = "https://sless.kube5s.ru"
|
||||||
token = var.token
|
token = var.token
|
||||||
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ terraform {
|
|||||||
|
|
||||||
# sless провайдер подключается к API кластера.
|
# sless провайдер подключается к API кластера.
|
||||||
provider "sless" {
|
provider "sless" {
|
||||||
endpoint = "https://sless-api.kube5s.ru"
|
endpoint = "https://sless.kube5s.ru"
|
||||||
token = var.token
|
token = var.token
|
||||||
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -11,7 +11,7 @@ terraform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
provider "sless" {
|
provider "sless" {
|
||||||
endpoint = "https://sless-api.kube5s.ru"
|
endpoint = "https://sless.kube5s.ru"
|
||||||
token = var.token
|
token = var.token
|
||||||
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -25,7 +25,7 @@ terraform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
provider "sless" {
|
provider "sless" {
|
||||||
endpoint = "https://sless-api.kube5s.ru"
|
endpoint = "https://sless.kube5s.ru"
|
||||||
token = var.token
|
token = var.token
|
||||||
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||||
}
|
}
|
||||||
|
|||||||
@ -24,7 +24,7 @@ terraform {
|
|||||||
}
|
}
|
||||||
|
|
||||||
provider "sless" {
|
provider "sless" {
|
||||||
endpoint = "https://sless-api.kube5s.ru"
|
endpoint = "https://sless.kube5s.ru"
|
||||||
token = var.token
|
token = var.token
|
||||||
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
nubes_endpoint = "https://deck-api-test.ngcloud.ru/api/v1"
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user