Author SHA1 Message Date
“Naeel” 8ee0065630 docs: migrate docker image path nail->terraform in README; add HISTORY note 2026-09-02 08:15:29 +03:00
“Naeel” 391223e61e docs: план отдельного сервиса документации (принципы, отдельная репа) 2026-09-02 07:45:37 +03:00
“Naeel” c3e75f11d6 docs: убрать документацию из registry (оставить только провайдеры), v0.0.6 2026-09-02 07:34:18 +03:00
“Naeel” 5e81c06124 docs: вернуть стриминг документации из S3 через registry (v0.0.5) 2026-09-02 07:19:20 +03:00
“Naeel” 8bbb5adf21 docs: redirect documentation files to S3 2026-09-01 21:16:31 +03:00
“Naeel” 35dd57c13b chore: .gitignore для бинарника сборки server/tf-registry 2026-09-01 17:40:54 +03:00
“Naeel” 33b694f5f5 doc: инцидент 2026-09-01 — фикс proxy (302 presigned S3), DNS на ВМ 2026-09-01 17:32:07 +03:00
“Naeel” 422736f5fa proxy: redirect 302 на pre-signed S3 URL вместо стриминга (v0.0.3) 2026-09-01 16:42:46 +03:00
“Naeel” cf2789be76 1 2026-09-01 12:24:23 +03:00
“Naeel” 3a47b8546f docs: зафиксирован инцидент 503 tf-registry — пустой Gitea registry, push образа 0.0.1 2026-08-31 12:14:29 +03:00
“Naeel” c286d190ba fix: PROVIDER_DIR и GPG_KEY_FILE указывают на ~/tf_provider вместо tf_registry 2026-08-09 15:14:59 +04:00
“Naeel” 76f70c91de fix: build-provider.sh — обновлены все параметры под текущий прод (ngcloud, новый домен, бакет, ldflags, mc alias) 2026-08-09 15:03:25 +04:00
“Naeel” e8891c2643 clean: удалена устаревшая документация (kube5s, тестовый Ceph, старые планы) 2026-08-09 14:37:35 +04:00
“Naeel” 78be235ec5 clean: удалён устаревший HOWTO-PUBLISH.md (заменён на HOWTO-UPLOAD.md) 2026-08-09 14:36:39 +04:00
“Naeel” 37d69ead42 fix: таймауты, ReadFile error, ?bucket= убран, USER registry 2026-08-09 14:28:56 +04:00
“Naeel” 575ba6dcb3 fix: /debug убран (утечка секретов) 2026-08-09 11:00:24 +04:00
“Naeel” 9b5a2a6fb0 doc: запрос на третье код-ревью Соннета 2026-08-09 10:53:54 +04:00
“Naeel” c587e78a18 doc: HOWTO-UPLOAD — добавлены неявные требования 2026-08-09 10:53:00 +04:00
“Naeel” 19bd727bac fix: дерево в HOWTO-UPLOAD 2026-08-09 10:51:48 +04:00
“Naeel” fe3913e51d doc: HOWTO-UPLOAD.md обновлён под прод 2026-08-09 10:51:24 +04:00
“Naeel” 4f5a1bfdc9 fix: путь к шаблону templates/index.html 2026-08-09 10:33:13 +04:00
“Naeel” fe4b2f502a fix: scanner.Err() + log import 2026-08-09 10:25:12 +04:00
“Naeel” b1263a2e30 refactor: структура Go — main.go 91стр, handlers/proxy/router_versions/docs/gpg_key 2026-08-09 10:23:58 +04:00
“Naeel” 503df159b6 fix: proxyHandler — io.Copy вместо ручного цикла 2026-08-09 10:11:15 +04:00
“Naeel” 1e50cc1a83 fix: proxyHandler — timeout 60s, валидация key по s3Prefix; readyz — timeout 5s 2026-08-09 10:05:03 +04:00
“Naeel” 6e1ce97f14 doc: результаты код-ревью v2 2026-08-09 10:03:53 +04:00
“Naeel” 3d290ba317 doc: запрос на второе код-ревью Соннета 2026-08-09 10:01:24 +04:00
“Naeel” be5fe36d63 fix: proxyHandler — streaming с Flush, 64KB буфер 2026-08-09 10:00:16 +04:00
“Naeel” cd81d2fe07 fix: S3_BUCKET=nubes-terraform-registry 2026-08-09 09:46:19 +04:00
“Naeel” 2ac2ca83a6 debug: /debug endpoint — DNS, TCP, S3 диагностика 2026-08-09 09:27:24 +04:00
“Naeel” 45876fe87b v0.0.2 2026-08-09 09:24:27 +04:00
“Naeel” 22036b253a fix: домены containerk8s.services.ngcloud.ru 2026-08-09 09:18:39 +04:00
“Naeel” ee53e0b11a fix: /static/logo.svg как в drhider (embed) 2026-08-09 09:06:11 +04:00
“Naeel” c19ce9c4bf fix: лого через <img> как в drhider 2026-08-09 08:36:53 +04:00
“Naeel” a369ff7c59 fix: лого текстом вместо SVG-кляксы 2026-08-09 08:34:51 +04:00
“Naeel” b5ff148ebc fix: лого с width/height 2026-08-09 08:27:30 +04:00
“Naeel” a9fa5d3835 v0.0.1 — чистая страница, без ссылок 2026-08-09 08:24:43 +04:00
29 changed files with 1140 additions and 1339 deletions
+1
View File
@@ -10,6 +10,7 @@ dist/
*.out *.out
vendor/ vendor/
go.sum.bak go.sum.bak
server/tf-registry
# ── Python ───────────────────────────────────────────────────────────────────── # ── Python ─────────────────────────────────────────────────────────────────────
__pycache__/ __pycache__/
+6 -3
View File
@@ -17,11 +17,14 @@ COPY --from=builder /app/registry /registry
# ── Non-secret config (change here → rebuild → push → redeploy) ───────── # ── Non-secret config (change here → rebuild → push → redeploy) ─────────
ENV S3_ENDPOINT=s3.msk-1.ngcloud.ru \ ENV S3_ENDPOINT=s3.msk-1.ngcloud.ru \
S3_BUCKET=terraform-registry \ S3_BUCKET=nubes-terraform-registry \
S3_PREFIX=tf-registry.services.ngcloud.ru \ S3_PREFIX=tf-registry.containerk8s.services.ngcloud.ru \
REGISTRY_HOSTNAME=tf-registry.services.ngcloud.ru \ REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru \
PORT=5000 PORT=5000
# ── Secrets in UI jsonEnv: S3_ACCESS_KEY, S3_SECRET_KEY ─────────────────── # ── Secrets in UI jsonEnv: S3_ACCESS_KEY, S3_SECRET_KEY ───────────────────
RUN adduser -D -u 1000 registry
USER registry
EXPOSE 5000 EXPOSE 5000
CMD ["/registry"] CMD ["/registry"]
-32
View File
@@ -1,32 +0,0 @@
# 2026-08-06 — Первый деплой Flask (v1.0.0)
## Хронология
### 09:41 — Деплой v1.0.0
- `gitPath`: `https://gitea.services.ngcloud.ru/Nail/tf_registry.git`
- `app.py`: минимальный Flask, `GET /``<h1>tf_registry v1.0.0</h1>`
### Ошибка: Init:Error
```
Cloning into '/tmp/app'...
fatal: could not read Username for 'https://gitea.services.ngcloud.ru'
```
Причина: gitea — приватный репо, managed-сервис не имеет кредов для git clone.
Предыдущий деплой работал на `github.com/Foxyhhd/Baldurs-Gate-test.git` (публичный).
### Решение
Репозиторий сделан публичным → передеплой → успешно.
### Результат
`https://registry.pythonk8s.dev.nubes.ru/``tf_registry v1.0.0`
## Далее
По плану (`HISTORY/2026-08-06-plan-review.md`, `plan.md`):
1. **Git-реорганизация**`git checkout -b golang` (архив Go), удалить Go-код из master
2. **Полный `app.py`** — все 7 роутов (discovery, versions, download, proxy, docs, healthz, readyz)
3. **Проверка** — curl + terraform init
## Урок
- gitea.services.ngcloud.ru — приватный. Либо сделать репо публичным, либо вшить `user:token@` в gitPath.
-91
View File
@@ -1,91 +0,0 @@
# 2026-08-06 — Ревизия плана Flask-реестра
## Контекст
Переписываем Go-реестр (`server/main.go`) на Python/Flask для managed-сервиса в облаке.
Логика 1:1 — те же эндпоинты, та же S3-структура, тот же GPG-ключ.
## Файлы прочитаны
| Файл | Строки | Что |
|---|---|---|
| `server/main.go` | 1-289 | Весь HTTP-сервер: 7 роутов, S3, GPG |
| `server/docs.go` | 1-200 | Docs-сервер с кандидатами ключей |
| `server/gpg_key.go` | 1-50 | Прокси к `registrykeys` пакету |
| `registrykeys/public_key.go` | 1-50 | Константы KeyID + ASCIIArmor |
| `CHAT_SUMMARY_2026-08-06.md` | 1-100 | Контекст: архитектура, S3-структура, ENV |
## План (из plan.md) — TL;DR
- **Фаза 1**: git checkout -b golang → удалить Go-код из master
- **Фаза 2**: `app.py` (~250-300 строк Flask) + `requirements.txt` (flask, boto3)
- **Фаза 3**: py_compile → деплой → curl-проверки → terraform init
### Эндпоинты (из Go-кода):
| URL | Ответ |
|---|---|
| `GET /.well-known/terraform.json` | `{"providers.v1": "/v1/providers/"}` |
| `GET /v1/providers/{ns}/{name}/versions` | JSON: id, versions[], warnings[] |
| `GET /v1/providers/{ns}/{name}/{ver}/download/{os}/{arch}` | JSON: download_url, shasums_url, sig_url, shasum, signing_keys |
| `GET /v1/proxy?bucket=...&key=...` | Стриминг файла из S3 |
| `GET /docs/{ns}/{name}/{ver}/...` | Статика из S3 (кандидаты ключей) |
| `GET /healthz` | `"ok"` 200 |
| `GET /readyz` | `"ok"` 200 |
| `GET /` | HTML-страница "ONLINE" |
### S3-ключи:
```
{hostname}/{namespace}/{name}/{version}/terraform-provider-{name}_{ver}_{os}_{arch}.zip
{hostname}/{namespace}/{name}/{version}/terraform-provider-{name}_{ver}_SHA256SUMS
{hostname}/{namespace}/{name}/{version}/terraform-provider-{name}_{ver}_SHA256SUMS.sig
docs/{namespace}/{name}/{version}/{path}
```
### ENV-переменные:
- `REGISTRY_HOSTNAME` (default: `localhost:8080`)
- `S3_ENDPOINT`
- `S3_ACCESS_KEY`
- `S3_SECRET_KEY`
- `S3_BUCKET` (default: `terraform-registry`)
## 6 замечаний к плану
### 1. ⚠️ S3-пагинация (ВАЖНО)
Go: `ListObjects` → канал с авто-пагинацией (все объекты).
boto3: `list_objects_v2` → макс 1000 за раз.
**Надо:** `Paginator` или цикл с `ContinuationToken`.
### 2. Docs-кандидаты (точность)
Go-логика чуть точнее, чем описано в плане:
- `rest` пустой → только `index.html`
- `rest` не пустой: exact → (если `/` на конце ИЛИ нет расширения → `{rest}/index.html`) → `index.html`
- Для `.css`/`.js` НЕ пробуется `{rest}/index.html`
### 3. Content-Length при стриминге
Go ставит через `obj.Stat()`. Flask `stream_with_context` — нет.
**Надо:** `head_object` → взять `ContentLength` → выставить вручную.
### 4. boto3 + MinIO
```python
boto3.client('s3',
endpoint_url=f'https://{s3_endpoint}',
use_ssl=True,
config=Config(signature_version='s3v4')
)
```
Без `s3v4` могут быть ошибки подписи на MinIO.
### 5. /readyz
Go: комментарий «Проверить S3», но реально просто `"ok"`.
Делать 1:1 → просто `"ok"`.
### 6. Порядок роутов во Flask
Flask матчит сверху вниз. `/` должен быть последним, чтобы не перехватить `/docs/`.
## Решения (подтверждены)
- GPG: только константа, без python-gnupg ✅
- Прокси: `?bucket=&key=` (НЕ `?url=`) ✅
- Платформы: только `linux_amd64` + `windows_amd64`
- Домен: `registry.pythonk8s.dev.nubes.ru`
@@ -0,0 +1,34 @@
# 2026-09-02: Перенос git-репозиториев и Docker-образов из `Nail` в организацию `terraform`
## Факты
- Git-репозитории `tf_provider`, `tf_registry`, `tf_docs` перенесены вручную из аккаунта `Nail` в организацию `terraform` на Gitea.
- Старые адреса `gitea.services.ngcloud.ru/Nail/...` отдают redirect на `terraform/...`.
- Docker-образы реестра **не переносились автоматически** — оставались в `gitea.services.ngcloud.ru/nail/tf_registry`.
## Выполнено (перенос образов)
Локально (Krupski) перетегированы и запушены в `gitea.services.ngcloud.ru/terraform/tf_registry` все актуальные теги:
- `0.0.1`, `0.0.3`, `0.0.4`, `0.0.5`, `0.0.6`, `latest`
- Проверено: `docker manifest inspect gitea.services.ngcloud.ru/terraform/tf_registry:0.0.6` → OK.
- `0.0.6` digest: `sha256:0888e5bc16b8359fecc829b67f801245e7df0c355a7bc0086ab37bd21db897f2`
- `latest` и `0.0.3` указывают на один digest: `sha256:e3f8510d521025f4980c65226507f31aa9f4defe68e3bc508625919927792bda5`
- Тег `0.0.2` отсутствовал и локально, и в registry (не публиковался).
## Обновления remote в локальных копиях
- `tf_provider` origin → `https://gitea.services.ngcloud.ru/terraform/tf_provider.git`
- `tf_registry` origin → `https://gitea.services.ngcloud.ru/terraform/tf_registry.git`
- `tf_docs` origin → `https://gitea.services.ngcloud.ru/terraform/tf_docs.git`
## Осталось (ручной шаг в Nubes Cloud UI)
- Обновить `registryPath` инстанса реестра (`tf-registry.containerk8s.services.ngcloud.ru`) с `gitea.services.ngcloud.ru/nail/tf_registry:<тег>` на `gitea.services.ngcloud.ru/terraform/tf_registry:<тег>`.
- Выполнить redeploy через UI `modify`.
- Старые образы в `nail/tf_registry` можно удалить после подтверждения, что новый путь работает.
## Напоминание
- Docker-имя registry требует lowercase: `terraform/tf_registry`, не `Terraform/...`.
- Команды сборки/пуша в README теперь указывают на `gitea.services.ngcloud.ru/terraform/tf_registry`.
-199
View File
@@ -1,199 +0,0 @@
# Chat Summary — Реестр Terraform-провайдера Nubes
> **Дата**: 2026-08-06
> **Тема**: Разбор архитектуры registry-сервера, план переноса с Go/K8s на Flask managed-сервис в облаке
> **Для нового чата**: прочитай этот файл — и ты в контексте
---
## 1. Что такое провайдер `nubes`
Кастомный Terraform-провайдер для облака NGCloud. Написан на Go (`~/tf_provider/provider/main.go`). Публикуется в собственном реестре `registry.kube5s.ru`.
**Terraform-конфиг (как у пользователя)**:
```hcl
terraform {
required_providers {
nubes = {
source = "registry.kube5s.ru/nubes-test/nubes"
version = "5.1.16"
}
}
}
provider "nubes" {
api_token = var.api_token
api_endpoint = "https://lk-api-gateway-test.ngcloud.ru/api/v1/svc"
# log_level = "debug" # none | info | debug
}
```
**Разбор адреса** `registry.kube5s.ru/nubes-test/nubes`:
- `registry.kube5s.ru` — домен реестра (DNS пользователя → IP балансировщика K8s)
- `nubes-test`**namespace** внутри реестра (для тестовых версий)
- `nubes` — имя провайдера
**Namespace'ы реестра:**
| Namespace | Где используется |
|---|---|
| `nubes-test` | Тестовый стенд |
| `nubes-dev` | Dev-стенд |
| `nubes` | Прод (PROD) |
---
## 2. Где и как запущен реестр (текущее состояние)
### Подтверждено через SSH → ВМ → kubectl:
- **ВМ**: `naeel@5.172.178.213` (ключ `~/remote_dev/terraform/secrets/id_ed25519.txt`, или по умолчанию из `~/.ssh`)
- **Кластер**: `iot-naeel` (self-managed K8s, **личный кластер пользователя**)
- **Namespace**: `terra`
### Деплойменты:
| Компонент | Образ | Статус |
|---|---|---|
| `registry-server` | `naeel/terraform-registry-server:gpg-fix-2` | 1/1 Running |
| `terraform-operator` | `naeel/terraform-registry-operator:latest` | 1/1 Running |
| `cloud-dashboard` | `naeel/cloud-dashboard:v9.52` | 1/1 Running |
### Ingress `registry-ingress`:
- Хосты: `terra.k8c.ru`, **`registry.kube5s.ru`**
- TLS: `registry-kube5s-tls` (Ready)
- Балансировщик: `185.247.187.151`
### ENV `registry-server`:
```
REGISTRY_HOSTNAME = registry.kube5s.ru
S3_ENDPOINT = s3.msk-1.ngcloud.ru
S3_BUCKET = terraform-registry
S3_USE_SSL = true
```
### Проверка живого реестра (HTTP 200):
```
GET /.well-known/terraform.json → {"providers.v1":"/v1/providers/"}
GET /v1/providers/nubes-test/nubes/versions → версии 5.1.0 ... 5.1.16
```
---
## 3. Код реестра — `~/tf_registry`
- **Репозиторий**: `https://gitea.services.ngcloud.ru/Nail/tf_registry.git`
- **Ветка**: `master`
- **Язык**: Go
- **Структура**:
- `server/main.go`**актуальный код** HTTP-сервера реестра
- `operator/` — Kubernetes Operator
- `k8s/`, `charts/` — манифесты деплоя
- `registrykeys/` — GPG-ключ
### Архитектура текущего реестра:
```
registry.kube5s.ru (DNS)
→ K8s Ingress (личный кластер iot-naeel)
→ Deployment registry-server (Go, Docker)
→ S3 bucket terraform-registry/
└── {REGISTRY_HOSTNAME}/nubes-test/nubes/{version}/
├── terraform-provider-nubes_{version}_{os}_{arch}.zip
├── ..._SHA256SUMS
└── ..._SHA256SUMS.sig
```
### Протокол (Terraform Registry Protocol v1):
| Эндпоинт | Назначение |
|---|---|
| `GET /.well-known/terraform.json` | Discovery |
| `GET /v1/providers/{ns}/{name}/versions` | Список версий и платформ |
| `GET /v1/providers/{ns}/{name}/{ver}/download/{os}/{arch}` | URL для скачивания |
| `GET /v1/proxy?url=...` | Прокси из S3 |
| `GET /docs/...` | Документация |
---
## 4. План: перенос реестра на Flask (managed-сервис в облаке)
### Проблема:
Кубер — **личный** кластер пользователя. Реестр должен быть **чисто облачным**, как managed-сервис (Flask или Node.js), который создаётся через UI облака.
### Выбор языка: **Flask (Python)**
- `boto3` — лучший S3-клиент
- Уже есть работающий Flask managed-сервис (`tfflaskcrud/`)
- Проще и короче, чем Node.js для этой задачи
### Новая архитектура:
```
registry.pythonk8s.dev.nubes.ru (managed-сервис, URL даёт облако)
→ Flask app (app.py)
→ S3 bucket terraform-registry/ (тот же)
```
### Что больше НЕ нужно:
- ❌ K8s-деплой `registry-server`
- ❌ K8s Ingress `registry-ingress`
- ❌ Сертификат `registry-kube5s-tls` + cert-manager
- ❌ Домен `registry.kube5s.ru` (заменяется на managed URL)
- ❌ Docker-образы для реестра
- ❌ Go-код для реестра
### План реорганизации Git-репозитория:
```
git checkout -b golang # сохранить Go-код в ветку golang
git checkout master # вернуться в master
# удалить всё лишнее
# оставить только app.py + requirements.txt + README.md
```
- **`master`** → Flask-реестр (основная ветка)
- **`golang`** → старый Go-код (архив, можно поправить и задеплоить при необходимости)
### Имя managed-сервиса: **`registry`**
```
https://registry.pythonk8s.dev.nubes.ru/
```
### Terraform-конфиг пользователя после миграции:
```hcl
source = "registry.pythonk8s.dev.nubes.ru/nubes-test/nubes"
```
### Файлы Flask-реестра:
```
tf_registry/
├── app.py # весь код (~300 строк)
├── requirements.txt # Flask, boto3, python-gnupg
└── README.md
```
---
## 5. Managed-сервисы в облаке
Пользовательские managed-сервисы (Flask, Node.js, Lucee) создаются через **UI облака** (не через Terraform). Terraform-провайдер `nubes` тоже умеет их создавать (`nubes_flask`, `nubes_nodejs`, `nubes_lucee`), но это вторичный способ.
---
## 6. Ключевые файлы и репозитории
| Что | Путь |
|---|---|
| Код registry-сервера (Go) | `~/tf_registry/server/main.go` |
| Код провайдера (Go) | `~/tf_provider/provider/main.go` |
| Git реестра | `https://gitea.services.ngcloud.ru/Nail/tf_registry.git` |
| Пример Flask managed-сервиса | `~/tf_provider/tfflaskcrud/site/app.py` |
| Пример Node.js managed-сервиса | `~/tf_provider/tfnodejscrud/server.js` |
| SSH-доступ к ВМ | `naeel@5.172.178.213` |
| Кластер | `iot-naeel`, namespace `terra` |
---
## 7. Следующие шаги (для нового чата)
1. Прочитать `~/tf_registry/server/main.go` (весь) — понять точную логику
2. Написать `app.py` — Flask-реестр с теми же эндпоинтами
3. Проверить синтаксис Python
4. Создать ветку `golang` из `master` и сохранить Go-код
5. Очистить `master`, оставить только Flask
6. Залить `app.py` в managed-сервис `registry` через UI облака
7. Проверить: `curl https://registry.pythonk8s.dev.nubes.ru/.well-known/terraform.json`
8. Обновить `source` в Terraform-конфигах пользователей
-104
View File
@@ -1,104 +0,0 @@
# Инструкция: как опубликовать провайдер в реестре
Реестр: `https://go-registry.containerk8s.dev.nubes.ru`
## 1. Структура S3
```
{prefix}/{namespace}/{name}/{version}/
├── terraform-provider-{name}_{version}_{os}_{arch}.zip ← бинарник
├── terraform-provider-{name}_{version}_SHA256SUMS ← контрольные суммы
└── terraform-provider-{name}_{version}_SHA256SUMS.sig ← GPG-подпись
```
Где:
- `{prefix}` — сейчас `registry.kube5s.ru` (будет сменён)
- `{namespace}``nubes-test`, `nubes-dev`, `nubes` (PROD)
- `{name}``nubes`
- `{os}_{arch}``linux_amd64`, `windows_amd64`, `darwin_amd64`
Пример:
```
registry.kube5s.ru/nubes-test/nubes/5.1.17/terraform-provider-nubes_5.1.17_linux_amd64.zip
```
## 2. Как загрузить в S3
### Нужны креды
```
S3_ENDPOINT=s3.msk-1.ngcloud.ru
S3_ACCESS_KEY=...
S3_SECRET_KEY=...
S3_BUCKET=terraform-registry
```
### Утилита `mc` (MinIO Client)
```bash
mc alias set nubes-s3 https://s3.msk-1.ngcloud.ru $S3_ACCESS_KEY $S3_SECRET_KEY
# Загрузка
mc cp ./terraform-provider-nubes_5.1.17_linux_amd64.zip \
nubes-s3/terraform-registry/registry.kube5s.ru/nubes-test/nubes/5.1.17/
mc cp ./SHA256SUMS nubes-s3/terraform-registry/registry.kube5s.ru/nubes-test/nubes/5.1.17/
mc cp ./SHA256SUMS.sig nubes-s3/terraform-registry/registry.kube5s.ru/nubes-test/nubes/5.1.17/
```
### Или `awscli`
```bash
aws s3 cp ... s3://terraform-registry/registry.kube5s.ru/nubes-test/nubes/5.1.17/ \
--endpoint-url https://s3.msk-1.ngcloud.ru
```
## 3. GPG-подпись (обязательно)
Terraform требует подпись SHA256SUMS.
### Приватный ключ
Хранится в `secrets/private_key.asc`. Публичный ключ (KEY ID: `3EC4673EB798238A`) вшит в реестр.
### Подписать
```bash
gpg --import secrets/private_key.asc
gpg --detach-sign --armor SHA256SUMS
# → SHA256SUMS.asc (переименовать в SHA256SUMS.sig)
```
## 4. Документация
```
docs/{namespace}/{name}/{version}/index.html
docs/{namespace}/{name}/{version}/...любые файлы...
```
Заливаешь в S3 — автоматом доступна по:
```
https://go-registry.containerk8s.dev.nubes.ru/docs/{ns}/{name}/{ver}/
```
## 5. Использование пользователем
```hcl
terraform {
required_providers {
nubes = {
source = "go-registry.containerk8s.dev.nubes.ru/nubes-test/nubes"
version = "5.1.17"
}
}
}
```
Terraform сам найдёт реестр, скачает провайдер, проверит SHA256 и GPG-подпись.
## 6. Скрипт автоматизации
В репозитории `tf_registry` есть скрипт `server/build-provider.sh` — он делает всё сам:
- Компилирует провайдер под linux/windows/darwin × amd64
- Упаковывает в zip
- Генерирует SHA256SUMS
- Подписывает GPG
- Заливает в S3
```bash
VERSION=5.1.17 NAMESPACE=nubes-test ./server/build-provider.sh
```
+29 -51
View File
@@ -1,30 +1,30 @@
# Как залить провайдер в реестр # Как залить провайдер в реестр (ПРОД)
## Конфигурация ## Конфигурация
### Реестр ### Реестр
- URL: `https://registry.containerk8s.dev.nubes.ru` - URL: `https://tf-registry.containerk8s.services.ngcloud.ru`
- S3: Ceph `http://ceph.tst.nubes.ru` - S3: `s3.msk-1.ngcloud.ru` (HTTPS)
- Бакет: `terraform-registry` - Бакет: `nubes-terraform-registry`
- S3-префикс: `registry.containerk8s.dev.nubes.ru` - S3-префикс: `tf-registry.containerk8s.services.ngcloud.ru`
### Креды ### Креды (аккаунт `1112_terraform`)
| Subuser | Назначение | Права | | Subuser | Назначение | Права |
|---|---|---| |---|---|---|
| `3709:reader` | Реестр (только читает) | read + list | | `1112_terraform:reader` | Реестр (только читает) | read + list |
| `3709:super` | Заливка провайдеров | write | | `1112_terraform:super` | Заливка провайдеров | write |
``` ```
Reader: AAYTNYSZIM08DDWV3TK1 / xQVd9CeGXACPIXTBTH4KV6DIHEAyySOFSTrS17Lc Reader: 6DDP5PKQEE37WLSWXP07 / W0vV4YNeHIqMa2sRCXsR61ZuQfAJIZzVj2VgnBrT
Super: 63A90SRCBP51XQ5TX8OB / euEJDZkueh0JHGP1B5jlAnAKriB8kYBR0t466GQ9 Super: L9F0R8CYYKYIUW180203 / yJlgxoKsRBwCtJQh57Z0nwo5EBDYmyBNDDhG6NKw
``` ```
## Структура S3 ## Структура S3
``` ```
terraform-registry/ nubes-terraform-registry/
└── registry.containerk8s.dev.nubes.ru/ └── tf-registry.containerk8s.services.ngcloud.ru/
└── {namespace}/ └── {namespace}/
└── {provider}/ └── {provider}/
└── {version}/ └── {version}/
@@ -37,58 +37,43 @@ terraform-registry/
## Заливка (build-provider.sh) ## Заливка (build-provider.sh)
### ⚠️ Важно: S3_ENDPOINT с http:// ### Требования
Ceph тестовый — HTTP, не HTTPS. Если задать `S3_ENDPOINT=ceph.tst.nubes.ru`, скрипт добавит `https://` → упадёт. - `~/tf_registry/` — склонирован этот репозиторий
**Всегда:** `S3_ENDPOINT=http://ceph.tst.nubes.ru` (с префиксом `http://`). - `~/tf_provider/` — склонирован репозиторий провайдера
- `~/tf_provider/secrets/private_key.asc` — приватный GPG-ключ для подписи
- `go` установлен
- `mc` (MinIO Client) установлен
### Команды ### Команды
```bash ```bash
# 1. Настроить mc (один раз, креды SUPER) # 1. Настроить mc (один раз, креды SUPER)
mc alias set ceph http://ceph.tst.nubes.ru \ mc alias set prod-s3 https://s3.msk-1.ngcloud.ru L9F0R8CYYKYIUW180203 yJlgxoKsRBwCtJQh57Z0nwo5EBDYmyBNDDhG6NKw
63A90SRCBP51XQ5TX8OB \
euEJDZkueh0JHGP1B5jlAnAKriB8kYBR0t466GQ9
# 2. Создать бакет (если ещё нет) # 2. Создать бакет (если ещё нет)
mc mb ceph/terraform-registry mc mb prod-s3/nubes-terraform-registry
# 3. Переменные для сборки # 3. Переменные для сборки
export ROOT_DIR=~/tf_provider export ROOT_DIR=~/tf_provider
export PROVIDER_DIR=~/tf_provider/provider export PROVIDER_DIR=~/tf_provider/provider
export REGISTRY_HOSTNAME=registry.containerk8s.dev.nubes.ru export REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru
export NAMESPACE=nubes-test # nubes-test | nubes-dev | nubes export NAMESPACE=nubes-test
export PROVIDER_NAME=nubes export PROVIDER_NAME=nubes
export S3_ENDPOINT=http://ceph.tst.nubes.ru # ← http:// обязательно! export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
export S3_ACCESS_KEY=63A90SRCBP51XQ5TX8OB # super export S3_ACCESS_KEY=L9F0R8CYYKYIUW180203
export S3_SECRET_KEY=euEJDZkueh0JHGP1B5jlAnAKriB8kYBR0t466GQ9 # super export S3_SECRET_KEY=yJlgxoKsRBwCtJQh57Z0nwo5EBDYmyBNDDhG6NKw
export S3_BUCKET=terraform-registry export S3_BUCKET=nubes-terraform-registry
# 4. Запуск # 4. Запуск
cd ~/tf_provider cd ~/tf_provider
bash ~/tf_registry/server/build-provider.sh 5.1.17 bash ~/tf_registry/server/build-provider.sh 5.1.17
``` ```
### Что делает скрипт
1. Компилирует Go → linux/windows/darwin × amd64
2. Упаковывает в zip
3. Генерирует SHA256SUMS
4. Подписывает GPG (ключ `secrets/private_key.asc`)
5. Заливает в S3
### Требования
- `~/tf_provider/secrets/private_key.asc` — приватный GPG-ключ
- `~/tf_provider/provider/` — исходники Go-провайдера
- `mc` (MinIO Client) установлен
- `go` установлен
## Проверка после заливки ## Проверка после заливки
```bash ```bash
# API curl https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-test/nubes/versions
curl https://registry.containerk8s.dev.nubes.ru/v1/providers/nubes-test/nubes/versions bash ~/tf_registry/tests/smoke.sh https://tf-registry.containerk8s.services.ngcloud.ru
# Полный тест
bash ~/tf_registry/tests/smoke.sh
``` ```
## Namespace'ы ## Namespace'ы
@@ -104,16 +89,9 @@ Terraform-конфиг пользователя:
terraform { terraform {
required_providers { required_providers {
nubes = { nubes = {
source = "registry.containerk8s.dev.nubes.ru/nubes-test/nubes" source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.1.17" version = "5.1.17"
} }
} }
} }
``` ```
## Уроки из диалога с чатом сборки
1. **S3_ENDPOINT** — всегда с протоколом (`http://` или `https://`). Без протокола скрипт форсит `https://`.
2. **Бакет** — может не существовать на новом Ceph. Создать через `mc mb`.
3. **Префикс** — должен совпадать с `REGISTRY_HOSTNAME`. Если домен поменялся — обновить и в скрипте, и в Dockerfile реестра.
4. **Креды** — для заливки нужны super (write), для реестра — reader (read+list).
-198
View File
@@ -1,198 +0,0 @@
# Terraform Registry — Полное описание
> Этот документ — точка входа для нового чата/агента. Содержит всё необходимое для понимания, сопровождения и доработки реестра провайдера.
---
## Что это такое
Кастомный Terraform Registry Server для провайдера `nubes` (облако Nubes / ngcloud.ru).
Когда разработчик пишет в своём `.tf`:
```hcl
terraform {
required_providers {
nubes = {
source = "terra.k8c.ru/nubes/nubes"
version = "5.0.18"
}
}
}
```
...и выполняет `terraform init`, Terraform идёт к **этому** серверу за бинарниками провайдера.
---
## Структура папки
```
REGISTRY/
├── server/ — Go-код HTTP-сервера реестра (АКТУАЛЬНЫЙ)
├── operator/ — Kubernetes Operator (CRD TerraformProviderRelease) + старая копия server-кода в cmd/registry/
├── k8s/ — Kubernetes манифесты деплоя (namespace, ingress, deployment, service, TLS)
├── registrykeys/ — Go-модуль с публичным GPG-ключом (для верификации провайдера)
├── charts/ — Helm chart для деплоя registry-server
└── scripts/
└── build-and-push-registry-server.sh — сборка Docker-образа и push в registry
```
**Важно:** `server/` и `operator/cmd/registry/` — дублирующийся код. Актуальный и полный — только `server/main.go`. В `operator/cmd/registry/main.go` нет health-эндпоинтов и не удалены darwin-платформы. При правках менять только `server/`.
---
## Как работает registry-server
### Реализованный протокол
[Terraform Registry Protocol v1](https://developer.hashicorp.com/terraform/internals/provider-registry-protocol).
| Эндпоинт | Назначение |
|---|---|
| `GET /.well-known/terraform.json` | Discovery — говорит Terraform, где API реестра |
| `GET /v1/providers/{ns}/{name}/versions` | Список доступных версий и платформ |
| `GET /v1/providers/{ns}/{name}/{version}/download/{os}/{arch}` | URL для скачивания бинарника |
| `GET /v1/proxy?url=...` | Проксирование файлов из S3 (бинарники, SHA, GPG-сигнатура) |
| `GET /docs/{ns}/{name}/{version}/...` | Документация провайдера (статический HTML из S3) |
| `GET /healthz` | Health probe для Kubernetes |
| `GET /readyz` | Readiness probe для Kubernetes |
### Как определяются версии
Сервер листит S3 bucket в поиске файлов вида:
```
<hostname>/<namespace>/<name>/<version>/terraform-provider-<name>_<version>_linux_amd64.zip
```
Из имён файлов парсит версию и платформы. Поддерживаемые платформы: `linux_amd64`, `linux_arm64`, `windows_amd64`.
### Переменные окружения
| Переменная | Default | Назначение |
|---|---|---|
| `REGISTRY_HOSTNAME` | `localhost:8080` | Домен реестра (используется в URL ответов) |
| `S3_ENDPOINT` | — | Хост S3 без схемы, например `s3.msk-1.ngcloud.ru` |
| `S3_ACCESS_KEY` | — | S3 access key |
| `S3_SECRET_KEY` | — | S3 secret key |
| `S3_BUCKET` | `terraform-registry` | Имя бакета |
| `S3_USE_SSL` | — | Если `true` — HTTPS (в коде Secure: true захардкожено) |
### S3 структура бакета
```
terraform-registry/ ← бакет
└── terra.k8c.ru/
└── nubes/
└── nubes/
└── 5.0.18/
├── terraform-provider-nubes_5.0.18_linux_amd64.zip
├── terraform-provider-nubes_5.0.18_windows_amd64.zip
├── terraform-provider-nubes_5.0.18_SHA256SUMS
└── terraform-provider-nubes_5.0.18_SHA256SUMS.sig
docs/
└── nubes/
└── nubes/
└── 5.0.18/
└── index.html (и другие файлы документации)
```
---
## GPG-ключ
Terraform верифицирует подпись провайдера через GPG.
- Публичный ключ **встроен** в `server/main.go` (ASCII Armor, константа в коде).
- Также хранится в `registrykeys/public_key.go` (отдельный go-модуль).
- Приватный ключ — `secrets/private_key.asc` (НЕ в репо, gitignore).
**При ротации ключей:**
1. Сгенерировать новую пару `gpg --gen-key`
2. Обновить ASCII Armor в `server/main.go`
3. Пересобрать и передеплоить Docker-образ
4. Перезалить все артефакты провайдера, подписанные новым ключом
---
## Деплой в Kubernetes
### Кластер и namespace
- Namespace: `terra`
- Домен: `terra.k8c.ru` (планируемая миграция → `registry.nubes.ru`, см. `../docs/ops/DOMAIN_MIGRATION.md`)
- TLS: cert-manager с `letsencrypt-prod` issuer — **НЕ ТРОГАТЬ** сертификат руками!
### Что задеплоено
| Ресурс | Kind | Образ |
|---|---|---|
| `registry-server` | Deployment | `naeel/terraform-registry-server:gpg-key-20260208` |
| `registry-server` | Service | ClusterIP :8080 |
| `registry-ingress` | Ingress | host: `terra.k8c.ru` |
Credentials S3 — из секрета `s3-credentials` (ключи `access-key`, `secret-key`).
### Применение манифестов (только при смене конфигурации кластера)
```bash
kubectl apply -f k8s/operator/manifests/00-namespace.yaml
kubectl apply -f k8s/operator/manifests/00-rbac.yaml
kubectl apply -f k8s/operator/manifests/02-crd.yaml
kubectl apply -f k8s/operator/manifests/05-registry-server.yaml
```
### Диагностика (read-only)
```bash
kubectl -n terra get pods,svc,ingress,deploy
kubectl -n terra logs -l app=registry-server --tail=50
kubectl -n terra get certificate registry-tls -o yaml
```
---
## Сборка и публикация нового образа
Скрипт: `scripts/build-and-push-registry-server.sh`
```bash
cd REGISTRY
./scripts/build-and-push-registry-server.sh
```
Собирает Docker-образ из `server/` и пушит в Docker Hub (`naeel/terraform-registry-server:<tag>`).
После push нужно обновить тег образа в `operator/manifests/05-registry-server.yaml` и применить `kubectl apply`.
---
## Когда registry НЕ нужно трогать
Registry — stateless сервис. При выпуске новой версии провайдера (`nubes`) registry не меняется:
```
Новая версия провайдера:
1. go build → бинарники
2. devops/03_build_and_upload_provider.sh → бинарники в S3
3. devops/04_build_and_publish_docs.sh → документация в S3
✅ Registry автоматически начинает отдавать новую версию
❌ kubectl не нужен
❌ код registry не меняется
❌ Docker-образ не пересобирается
```
Registry пересобирается только если меняется **сам сервер**: новые эндпоинты, логика, ротация GPG-ключа.
---
## Связь с основным репо провайдера
Этот код был перемещён из основного репо `terraform/` в папку `REGISTRY/` для последующего выноса в отдельный репозиторий. Код провайдера и код реестра полностью независимы — нет общих go-модулей, нет импортов друг друга.
---
## Ограничения и правила работы
- **Ручные правки `operator/cmd/registry/`** — не делать, это устаревшая копия. Актуальный код — только `server/`.
- **Let's Encrypt cert-manager** (`letsencrypt-prod`) — не применять `kubectl apply` на Ingress-ресурсы без именённой необходимости (риск бана от CA).
- **docker build** — только с явного разрешения разработчика.
- **kubectl** — только read-only (`get`, `describe`, `logs`) без разрешения.
- **S3 credentials** — никогда не коммитить в репо. Только через секреты k8s или переменные окружения.
-59
View File
@@ -1,59 +0,0 @@
# Ceph S3 — конфигурация для реестра
## Инстанс S3
| Поле | Значение |
|---|---|
| userName | `naeels3` |
| instanceUid | `332cdb0d-34bf-43bf-864d-4adcc3b556fb` |
| urlConnect | `http://ceph.tst.nubes.ru/` |
| resourceRealm | `ceph.tst.nubes.ru` |
## Пользователи (users)
| user_id | Назначение |
|---|---|
| `3709` | Основной владелец |
| `3709:super` | Subuser с повышенными правами |
| `3709:technicalS3Backup` | Subuser для бэкапов |
| `3709:reader` | **(планируется)** Subuser для реестра — только read+list |
## Subuser `3709:reader` для реестра
### Права
- `read` — чтение объектов
- `list` — список объектов (нужно для `listVersions`)
- **Без** `write` и `delete` — реестр не может менять данные
### Креды (после создания)
```json
{
"S3_ACCESS_KEY": "...",
"S3_SECRET_KEY": "..."
}
```
Вставить в `jsonEnv` инстанса реестра.
### Конфигурация реестра под Ceph
Dockerfile ENV:
```dockerfile
ENV S3_ENDPOINT=ceph.tst.nubes.ru \
S3_BUCKET=terraform-registry \
S3_PREFIX=go-registry.containerk8s.dev.nubes.ru \
REGISTRY_HOSTNAME=go-registry.containerk8s.dev.nubes.ru \
PORT=5000
```
### Различия с текущим S3 (MinIO)
| | Старый (MinIO) | Новый (Ceph) |
|---|---|---|
| ENDPOINT | `s3.msk-1.ngcloud.ru` | `ceph.tst.nubes.ru` |
| Креды | Статические | Subuser `3709:reader` |
| Протокол | HTTPS | HTTP (судя по `urlConnect`) |
| Безопасность | Приватный MinIO | Ceph с разделением прав |
### ⚠️ Если HTTP
В Go-коде сейчас `Secure: true` — для HTTP надо поменять на `Secure: false`.
Лучше через ENV: `S3_USE_SSL=true/false` (сейчас захардкожено).
+33
View File
@@ -0,0 +1,33 @@
# Code Review Request v2 — Go Terraform Registry
## Контекст
После первого ревью исправлено: SSRF в proxyHandler, go.sum, readyz, сортировка версий, graceful shutdown, gofmt. Добавлен /debug и дизайн Nubes.
Продакшен: `https://tf-registry.containerk8s.services.ngcloud.ru`
S3: Ceph `s3.msk-1.ngcloud.ru`, бакет `nubes-terraform-registry`
## Файлы для ревью
- `server/main.go` — всё в одном файле (~430 строк)
- `server/gpg_key.go` — GPG-ключи
- `server/docs.go` — статика
- `Dockerfile` — сборка
## Вопросы
1. **proxyHandler** — новый стриминг с ручным `Read/Write/Flush` циклом и 64KB буфером. Нормально ли это? Не упущены ли edge-cases (обрыв соединения, таймаут)?
2. **/debug endpoint** — показывает ENV, DNS, TCP, S3 статус. Безопасно ли это на проде? DNS-резолв и TCP-dial из пода могут быть использованы для SSRF?
3. **Один файл** — main.go разросся до ~430 строк (роуты + S3 + debug + HTML-шаблон). Стоит ли дробить или пока ок?
4. **embed для logo.svg** — нормальный подход? Не раздует ли бинарник?
5. **S3_PREFIX hardcode в Dockerfile**`tf-registry.containerk8s.services.ngcloud.ru` зашит в Dockerfile ENV. Это конфиг или хардкод?
6. **downloadVersion**`defer` внутри `if err == nil` (с прошлого ревью сказали что ок). Перепроверь — defer внутри if-блока точно не проблема?
7. **Dockerfile** — есть `go.mod`/`go.sum`, билд воспроизводимый. Всё ок?
8. **Общее** — перед деплоем в прод, что ещё проверить?
+40
View File
@@ -0,0 +1,40 @@
# Code Review v3 — Go Terraform Registry (финальный)
## Контекст
Реестр на проде: `https://tf-registry.containerk8s.services.ngcloud.ru`
После ревью v2: структура переписана, HTML в embed, proxyHandler фикс, таймауты, валидация key.
## Файлы
```
server/
├── main.go # vars, main()
├── handlers.go # типы, root, discovery, healthz, readyz, debug
├── proxy.go # proxyHandler
├── router_versions.go # router, listVersions, downloadVersion
├── gpg_key.go # GPG-ключи
├── docs.go # docs-хендлер
├── embed.go # embed templates/index.html
├── logo.svg
└── templates/index.html
Dockerfile
```
## Вопросы
1. **Структура пакета** — всё в `package main`, разнесено по файлам. Ок для команды или стоит выносить в `internal/`?
2. **embed подход**`embed.go` с `//go:embed templates/index.html`, `rootHandler` читает через `templateFS.ReadFile`, заменяет `{{.Version}}`. Ок или лучше `html/template`?
3. **proxyHandler**`io.Copy(w, obj)` с MinIO Object, таймаут 60s, валидация key по s3Prefix. Всё ок?
4. **listVersions**`context.Background()` без таймаута для `ListObjects`. Стоит добавить?
5. **downloadVersion**`defer` внутри `if err == nil` (defer shasumsObj.Close). Перепроверь — это ок?
6. **/debug** — показывает ENV (включая секреты S3_ACCESS_KEY/S3_SECRET_KEY). Оставить или убрать?
7. **Dockerfile** — ENV с дефолтами, секреты в jsonEnv. Нормальный паттерн?
8. **Общее** — перед тем как объявить готовым к проду, что ещё?
+43
View File
@@ -0,0 +1,43 @@
# Code Review v2 — Результаты (Соннет, 2026-08-09)
## 1. proxyHandler стриминг — ✅ OK
Цикл Read/Write/Flush корректен. Обрыв соединения обработан.
⚠️ Нет таймаута — `context.Background()` без дедлайна. При зависании S3 горутина повиснет навсегда.
📎 `?bucket=` в URL игнорируется (безопасно), можно убрать из download_url.
## 2. /debug — 🔴 КРИТИЧНО
**Утечка секретов.** Фильтр `strings.Contains(e, "S3_")` захватывает `S3_ACCESS_KEY` и `S3_SECRET_KEY` — выводятся в открытом виде.
**Фикс:** исключить ключи из вывода.
## 3. main.go ~430 строк — ✅ OK
Дробить рано. Порог ~700-1000 строк. gpg_key.go и docs.go уже выделены правильно.
## 4. embed logo.svg — ✅ OK
Нормально. SVG < 5KB на бинарник не влияет.
## 5. S3_PREFIX в Dockerfile — ✅ Конфиг
ENV в Dockerfile = default, переопределяется через jsonEnv. Не хардкод.
## 6. defer внутри if — ✅ OK
defer привязан к функции, не к блоку. Корректно.
## 7. Dockerfile go.mod/go.sum — ✅ OK
Канонический паттерн. Билд воспроизводимый.
## 8. Перед продом
| # | Проблема | Риск |
|---|----------|------|
| 1 | `/debug` — утечка S3 ключей | 🔴 Критично |
| 2 | `proxyHandler` — нет таймаута на S3 | 🟡 |
| 3 | `proxyHandler` — key не валидируется по s3Prefix | 🟡 |
| 4 | Платформы hardcoded | 🟢 |
| 5 | `readyz` BucketExists без таймаута | 🟡 |
+25
View File
@@ -0,0 +1,25 @@
# Code Review v3 — Результаты (Соннет, 2026-08-09)
## 1. Структура — ✅ ОК
package main по файлам, ~400 строк — дробить рано.
## 2. embed — ✅ ОК
Но `tmpl, _ := ReadFile` — ошибка игнорируется.
## 3. proxyHandler — 🟡 мёртвый параметр
`?bucket=` в URL не читается proxyHandler — можно убрать из downloadLink.
## 4. listVersions — 🟡 нет таймаута
`context.Background()` без дедлайна.
## 5. defer в if — ✅ ОК
Корректно, defer привязан к функции.
## 6. /debug — 🔴 КРИТИЧНО
Секреты S3 отдаются публично. Убрать.
## 7. Dockerfile — ✅ ОК
Паттерн нормальный. Рекомендация: добавить USER.
## 8. Итог
Единственный блокер: /debug с утечкой секретов. Остальное — желательно.
+290
View File
@@ -0,0 +1,290 @@
Список задач обновлен
Проверялись следующие endpoint’ы registry:
```text
https://tf-registry.containerk8s.services.ngcloud.ru/.well-known/terraform.json
```
Результат: `HTTP 200`
```json
{"providers.v1":"/v1/providers/"}
```
Список версий:
```text
https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-test/nubes/versions
```
Результат: `HTTP 200`
Опубликованы:
```text
5.0.1
5.0.2
5.0.3
5.0.4
5.0.5
5.1.17
```
Download metadata для `5.0.5`:
```text
https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-test/nubes/5.0.5/download/linux/amd64
```
Результат: `HTTP 200`, ответ около `8 KB`, содержит:
```text
filename:
terraform-provider-nubes_5.0.5_linux_amd64.zip
download_url:
https://tf-registry.containerk8s.services.ngcloud.ru/v1/proxy?key=...
```
Фактический proxy URL:
```text
https://tf-registry.containerk8s.services.ngcloud.ru/v1/proxy?key=tf-registry.containerk8s.services.ngcloud.ru%2Fnubes-test%2Fnubes%2F5.0.5%2Fterraform-provider-nubes_5.0.5_linux_amd64.zip
```
Проверка proxy:
```text
HTTP: 200
Content-Type: application/octet-stream
Content-Length: 13083236
```
Но фактически за 40 секунд скачалось только:
```text
20983 байт
```
и затем:
```text
curl: (28) Operation timed out
```
Дополнительно проверялись:
```text
nubes-test/nubes:5.1.17
nubes/nubes:2.0.6
```
На них результат такой же: metadata отдается быстро, а ZIP через `/v1/proxy` обрывается примерно на `1823 KB`.
Проверялся также заголовок:
```text
Range: bytes=0-1023
```
и диапазон из середины файла:
```text
Range: bytes=1048576-1049599
```
Proxy игнорирует `Range`, отвечает `HTTP 200` и снова начинает отдавать весь файл, после чего передача зависает.
Проверка DNS:
```text
getent hosts tf-registry.containerk8s.services.ngcloud.ru
```
Результат:
```text
5.172.178.231
```
Также проверялся Kubernetes API:
```text
kubectl get ingress -A
kubectl get pods -A
kubectl get svc -A
kubectl get deploy -A
kubectl get configmap -A
```
Но текущий kubeconfig не имеет credentials:
```text
the server has asked for the client to provide credentials
Unauthorized
```
Итог: API registry и metadata работают. Проблемный участок:
```text
/v1/proxy -> S3 object -> HTTP streaming через Kubernetes
```
Именно там большой ZIP возвращается с `HTTP 200`, но поток фактически обрывается на десятках килобайт.
Список задач обновлен
Read
Read memory
Read , lines 1 to 80
Read , lines 1 to 80
Read , lines 1 to 80
Теперь у меня есть контекст по drhider. Давай сравню симптомы:
Read
Read
## Анализ: сравнение проблем drhider и registry
### Проблема в drhider (решена)
| Аспект | Детали |
|--------|--------|
| Симптом | POST из браузера → тело запроса обрывалось на ~64КБ (HTTP 000, ~10с) |
| Направление | **Входящий** трафик: браузер → кластер |
| Корень | Managed-кластер, шлюз ingress обрывает тела входящих запросов >64КБ |
| Решение | Браузер PUT файл на ВМ-буфер (обходит шлюз) → Flask тянет с ВМ (egress, без лимита) |
### Проблема в registry (сейчас)
| Аспект | Детали |
|--------|--------|
| Симптом | `GET /v1/proxy?key=...` → HTTP 200 мгновенно, но данные обрываются на 18–23 КБ |
| Направление | **Исходящий** трафик: кластер → Terraform клиент |
| Корень | Тот же кластерный шлюз, но теперь режет **streaming-ответ** (буферизация / flush-лимит) |
### Это та же проблема?
**Да, вероятно одна и та же инфраструктура** (`services.ngcloud.ru` — managed Nubes, тот же managed-кластер что и `pythonk8s.dev.nubes.ru`). Только направление противоположное:
- drhider: шлюз режет входящее тело
- registry: шлюз режет исходящий stream большого файла
Дополнительный признак: у вас есть ещё одна задокументированная проблема — HTTP/2 резал git push к gitea (тот же шлюз, тот же паттерн). Range-запросы игнорируются — тоже поведение промежуточного буферизующего прокси.
---
### Применим ли обход через ВМ?
**Да, но схема зеркальная.** В drhider проблема была на входе — данные шли в кластер. Здесь проблема на выходе — данные идут из кластера к Terraform.
**Варианты обхода:**
**Вариант A — Redirect на S3 pre-signed URL (чище всего)**
`/v1/proxy` вместо стриминга генерирует временную pre-signed S3 ссылку и возвращает `302 Location: <s3-presigned-url>`. Terraform следует редиректу и скачивает ZIP напрямую с S3, минуя кластерный шлюз полностью.
```
Terraform → /v1/proxy?key=... → 302 Location: https://s3.../object?X-Amz-Expires=...
Terraform → S3 напрямую → 200 + 13MB ✅
```
Terraform `go-getter` (используется в `terraform init`) редиректы поддерживает.
**Вариант B — Redirect на ВМ-прокси**
Если S3 не публичный / нет pre-signed URL — `/v1/proxy` отдаёт `302` на эндпоинт ВМ (`contracts.kube5s.ru/tf-proxy?key=...`). ВМ скачивает файл с S3 и стримит Terraform клиенту. ВМ находится вне шлюза managed-кластера.
**Вариант C — Через download_url в metadata (самое прозрачное)**
Вместо того чтобы `download_url` указывал на `/v1/proxy` внутри кластера — указывать сразу на S3 pre-signed URL или ВМ-прокси. Не нужно менять `/v1/proxy` вообще.
---
### Рекомендация
Вариант A или C — наиболее чистые. Смотреть, есть ли доступ к S3 с кредами для pre-signed URL из кода registry. Если есть — минимальное изменение: в `/v1/proxy` заменить стриминг на `redirect(presigned_url, 302)`.
Нужно смотреть код registry (`/v1/proxy` handler) чтобы подтвердить технически — он в этом же репозитории или в отдельном?
Создано задач: 3
Список задач обновлен
Read
Read
Read
Read
Read
Read
Read
Картина полная. Вот анализ:
---
## Диагностика: та же проблема, другая сторона
**Код проблемы — `proxy.go`, `proxyHandler`:**
```go
obj, _ := s3Client.GetObject(ctx, bucketName, key, ...)
io.Copy(w, obj) // ← Kubernetes шлюз режет этот stream
```
Тот же managed-кластер (`containerk8s.services.ngcloud.ru` — Nubes), тот же шлюз. В drhider он резал входящие тела >64KB. Здесь режет исходящий stream с S3 (~18–23KB — это первый буфер ответа, который шлюз успел пропустить до блокировки).
`Content-Length: 13083236` выставляется, но данных доходит ~20KB — шлюз буферизует и не флашит остаток.
---
## Решение: presigned URL redirect
`minio-go` уже импортирован, он умеет генерировать pre-signed URL:
```go
// вместо GetObject + io.Copy:
presigned, err := s3Client.PresignedGetObject(ctx, bucketName, key, 15*time.Minute, url.Values{})
http.Redirect(w, r, presigned.String(), http.StatusFound)
```
Terraform следует 302. Terraform client скачивает ZIP **напрямую с S3**, кластерный шлюз больше не участвует.
Это затрагивает только `proxy.go` — 1 файл, примерно 10 строк замены.
`ShasumsURL` и `ShasumsSignatureURL` тоже идут через `/v1/proxy` — они маленькие (<10KB), работают и сейчас, но с redirect тоже будут работать корректно.
---
## План
**Файл:** server/proxy.go
**Изменение:** в `proxyHandler` — убрать `GetObject` / `Stat` / `io.Copy` / установку заголовков, заменить на `PresignedGetObject` + `http.Redirect 302`. Добавить `"net/url"` в imports.
**Верификация:**
1. `go build `nubes`.` — компиляция
2. `curl -v 'https://...registry.../v1/proxy?key=...' -L` — должен получить 302, затем скачать 13MB полностью
3. `terraform init` в тестовом проекте с этим провайдером
Делать?
Создано задач: 4
НИЧЕГО НЕ ДЕЛАЙ. КРАТКО ОТВЕТ ДАВАЙ - это надо менять код регистри ? и всё ?
10:03
Да. Только server/proxy.go — заменить стриминг на redirect к pre-signed S3 URL. Больше ничего.
+122
View File
@@ -0,0 +1,122 @@
# План: отдельный сервис документации (Node.js)
## Целевая архитектура
```
S3 (nubes-terraform-registry) — статика документации, только docs/*
Node.js-сервис — раздаёт /docs/* + кеш с TTL
registry (Go) — только Terraform-протокол:
/.well-known/*, /v1/providers/*, /v1/proxy
```
## Что выносится из registry
- Маршрут `/docs/` и функция `docsHandler` (удаляются из `server/main.go` и `server/docs.go`).
- Провайдерская логика (`discoveryHandler`, `router`, `proxyHandler`, `rootHandler`, `healthz/readyz`) — НЕ меняется.
## Соглашения для сервиса документации (перенос из бывшего server/docs.go)
Файл `docs.go` не является уникальным активом; он описывает обычный маппинг
URL → S3-ключ. При переносе на Node.js достаточно воспроизвести три правила:
1. **Формат S3-ключей документации:**
```
docs/<namespace>/<name>/<version>/<rest>
при пустом rest → docs/<namespace>/<name>/<version>/index.html
```
2. **Fallback-цепочка при неизвестном пути:**
точный путь → `<путь>/index.html` (если путь похож на каталог) → корневой `index.html`.
3. **Content-Type:**
через `mime.TypeByExtension`; fallback на `text/html; charset=utf-8` для
`index.html`/`.html`, иначе `application/octet-stream`.
4. **Добавить то, чего в сервисе документации будет больше, чем в docs.go:**
in-memory кеш с TTL (например, 60–300 сек) и заголовок `Cache-Control`.
## Endpoint Node.js-сервиса
```
GET /docs/<namespace>/<name>/<version>/<rest>
```
## Переменные окружения (общие с registry)
- `S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`
(точки доступа — как в `secrets/env.txt`).
## DNS / Ingress
- `/docs/*` → Node.js-сервис.
- `/.well-known/*`, `/v1/providers/*`, `/v1/proxy` → registry.
- Тот же домен `tf-registry.containerk8s.services.ngcloud.ru`, пути разводятся в Ingress.
(При отдельном поддомене потребуется переписывать ссылки в HTML.)
## Порядок работ
1. Вынести `/docs/` из registry (этот шаг).
2. Написать Node.js-сервис (маппинг + кеш + Content-Type).
3. Dockerfile + образ в gitea + деплой сервиса.
4. Ingress-правило для `/docs/*`.
5. Переписать генератор документации под новые соглашения (отдельная задача).
6. Smoke-тесты: HTML 200, CSS/JS 200, ссылки валидны.
## Статус
- [x] Вынести документацию из registry (v0.0.6)
- [ ] Node.js-сервис (отдельная репа)
- [ ] Деплой + Ingress
- [ ] Переписанный генератор
---
# План (принципиальный) — отдельная репа
Код — в отдельной репе, сюда только итоговый результат.
## Принципы
1. **Разделение по ответственности.**
- registry — только Terraform-протокол (уже сделано, v0.0.6).
- docs-сервис — только раздача статики `/docs/*`.
- S3 — только хранилище статики, prefix `docs/`.
2. **Сервис документации = статический сервер, не приложение.**
Никакого рендеринга и шаблонов. Одна функция: URL → S3-ключ → файл.
3. **Язык — Node.js.**
Лёгкий, быстрый, встроенный кеш, тот же S3 SDK. (nginx — запасной вариант, если не нужен код.)
4. **Кеш обязателен.**
in-memory кеш с TTL (60300 сек) + `Cache-Control`. Это решает исходную проблему «тормозит и кеширует».
5. **Один домен, пути разводятся в Ingress.**
- `/docs/*` → docs-сервис
- `/.well-known/*`, `/v1/providers/*`, `/v1/proxy` → registry
Отдельный поддомен не нужен — тогда не придётся переписывать ссылки в HTML.
6. **Доступ к S3 — те же credentials, но только к `docs/*`.**
Ключ доступа с правом чтения `docs/`, без доступа к провайдерским артефактам.
## Соглашения из бывшего docs.go (перенести на Node)
- S3-ключ: `docs/<ns>/<name>/<version>/<rest>`; пустой `rest` → `index.html`.
- Fallback: точный путь → `<путь>/index.html` → корневой `index.html`.
- Content-Type по расширению; `index.html` → `text/html; charset=utf-8`.
## Минимальный объём сервиса
- `GET /docs/:ns/:name/:version/*` → S3-ключ → отдать файл + кеш.
- Dockerfile → образ в gitea → деплой.
- Ingress-правило для `/docs/*`.
## Что отдаётся обратно в эту репу
- Только ссылка на репу + итоговый Dockerfile/конфиг, если договоримся держать деплой-артефакты здесь.
- Сам код docs-сервиса — в отдельной репе.
## Генератор — отдельная задача (НЕ входит в этот план)
- Переписывается отдельно, чтобы ссылки на ассеты были корректны.
- К docs-сервису относится только как «потребитель» результата.
+32
View File
@@ -0,0 +1,32 @@
# Инцидент: `tf-registry.containerk8s.services.ngcloud.ru` отдаёт 503
## Зафиксированные факты
- `tf-registry.containerk8s.services.ngcloud.ru` резолвится в `5.172.178.231`.
- Запросы `GET /healthz` и `GET /readyz` возвращают `HTTP/2 503` с телом nginx `503 Service Temporarily Unavailable`.
- Прямое подключение к `5.172.178.231:5000` не устанавливается (порт backend наружу не публикуется).
- VM `5.172.178.213` доступна, но это ВМ для сборки — managed-контейнер к ней отношения не имеет.
- Следовательно, запрос не доходит до Go-сервера: это недоступный или не запущенный upstream, а не ошибка обработчиков `/healthz`, `/readyz` или S3 readiness.
## Диагностика образа (подтверждённые факты)
- Docker Registry Gitea (`gitea.services.ngcloud.ru/nail/tf_registry`) был **пуст**: `tags=` (тегов нет).
- `docker manifest inspect ...:latest``manifest unknown`; `:0.0.2``manifest unknown`.
- С ВМ `5.172.178.213` доступа до Gitea **нет**: TCP `194.31.9.41:443``SYN-SENT`, DNS `lookup gitea.services.ngcloud.ru``i/o timeout`, traceroute обрывается на `100.64.24.24`. Общий интернет с ВМ работает (docker hub доступен).
- Локальная машина (Krupski) имеет доступ до Gitea напрямую; `docker login gitea.services.ngcloud.ru --username Naeel``Login Succeeded`.
- Образ собран на ВМ из текущего `master` (rsync), затем перенесён локально через `docker save`/`scp`/`load`.
## Push образа 0.0.1 (выполнено)
- `docker push gitea.services.ngcloud.ru/nail/tf_registry:0.0.1` (с локальной машины) — успешно.
- Digest: `sha256:9a00836c916e2cae597e31dff2184a08550bd1be262bdba980d17de2943aa4a7`
- Проверка: `docker pull gitea.services.ngcloud.ru/nail/tf_registry:0.0.1``PULL_OK`.
- ID образа: `sha256:3c76e53a7213d0404932aac6b8ad0dc0c254b35f14d6d3d420a18ba4ec07b0d3`
## Требуемое восстановление
1. В Nubes Cloud открыть инстанс с доменом `tf-registry.containerk8s.services.ngcloud.ru` и проверить состояние deployment/реплик и событий запуска.
2. Обновить `registryPath` на `gitea.services.ngcloud.ru/nail/tf_registry:0.0.1` (образ с этим тегом уже опубликован).
3. Проверить, что контейнер слушает `PORT=5000`, а healthcheck использует доступный путь.
4. Выполнить redeploy через UI `modify`.
5. После запуска проверить `https://tf-registry.containerk8s.services.ngcloud.ru/healthz` и `/.well-known/terraform.json`.
@@ -0,0 +1,64 @@
# Инцидент 2026-09-01: обрыв скачивания провайдера + медленный DNS на ВМ
## Симптом (исходный)
- `GET /v1/proxy?key=...` отдавал `HTTP 200` + `Content-Length: 13083236`, но поток обрывался на ~18–23 KB (curl `(28) Operation timed out`).
- `Range`-запросы игнорировались — сервер заново отдавал весь файл и зависал.
- Гипотеза: кластерный ingress-шлюз режет исходящий streaming (по аналогии с drhider, где резались входящие тела >64 KB).
## Диагностика
- `s3.msk-1.ngcloud.ru` публично доступен: DNS `185.228.248.165/166/167`, `HTTP 200` (Ceph Object Gateway).
- `gitea.services.ngcloud.ru` НЕ доступен с ВМ сборки `5.172.178.213`:
- публичный IP `194.31.9.41:443` — TCP таймаут;
- внутренний ClusterIP `10.96.52.11:443` (`gitea.mgmt.nubes.ru`) — TCP таймаут (подсеть `10.96.0.0/16` с ВМ не маршрутизируется);
- с локальной машины gitea доступен (`200` за 0.36s). → «в одном облаке» ≠ открыто: сети изолированы security-group/маршрутизацией.
## Причина обрыва — НЕ кластер, а локальный прокси
- На локальной машине задан `HTTP_PROXY/HTTPS_PROXY=http://172.17.192.1:10808` (v2ray/clash-тип).
- Через прокси режется любой большой исходящий поток (~16–23 KB) — и старый стриминг через кластер, и прямое скачивание с S3.
- Без прокси (`curl --noproxy '*'`) файл качается полностью: 13 083 236 байт за 6.3s, `unzip -t` — OK.
- С ВМ (без прокси) тот же файл: 13 083 236 байт за 11.3s, `unzip -t` — OK.
- **Вывод:** исходная гипотеза «кластерный шлюз режет» не подтвердилась.
## Фикс registry: redirect на pre-signed S3 URL (выполнено)
- `server/proxy.go`, `proxyHandler`: убраны `GetObject`/`Stat`/`io.Copy`; вместо этого
`PresignedGetObject(ctx, bucketName, key, 15*time.Minute, nil)` + `http.Redirect(w, r, ..., http.StatusFound)`.
- Удалён неиспользуемый импорт `s3`.
- `server/main.go`: `VERSION 0.0.2 → 0.0.3`.
- Проверка: `go build ./...` — OK.
- Коммит `422736f`, запушен в `master`.
- Образ: `docker build` (локально, т.к. gitea недоступен с ВМ) → `gitea.services.ngcloud.ru/nail/tf_registry:latest` и `:0.0.3`.
- Digest: `sha256:e3f8510d521025f4980c65226507f31aa9f4defe68e3bc50862591992792bda5`.
- Инстанс пересоздан в UI Nubes с образом `:0.0.3`.
### Проверка фикса
- `GET /v1/proxy?key=...``HTTP 302` + `Location: https://s3.msk-1.ngcloud.ru/...?X-Amz-...` (presigned, креды reader `6DDP5...`).
- `curl -L` — 13 083 236 байт за 6.3s (без прокси), архив валиден.
## Проблема terraform init на ВМ: медленный DNS
- `terraform init` на ВМ падал: `failed to retrieve authentication checksums ... context deadline exceeded` (2 попытки).
- Причина: DNS на ВМ. `resolv.conf` = `8.8.8.8` + `1.1.1.1`, оба **недоступны** с ВМ
(`nslookup``communications error: timed out`).
- Каждый резолв доменов Nubes висел ~10 сек: `dns=10.6s` (registry), `dns=10.0s` (s3).
- Terraform делает много резолвов → суммарный таймаут. Сама передача файлов работала нормально.
### Решение: статические записи в /etc/hosts на ВМ
- Добавлено (бэкап: `/etc/hosts.bak`):
- `5.172.178.231 tf-registry.containerk8s.services.ngcloud.ru`
- `185.228.248.165 s3.msk-1.ngcloud.ru`
- После правки резолв мгновенный: `dns=0.0003s`, registry `total=0.022s`.
- `terraform init` в `~/tf_provider/TEST_STAND/PGwNewRegistry`**успех**:
`Installed ... nubes v5.0.5 (self-signed, key ID CB3A0DF161ECC416)`, `Terraform has been successfully initialized!`.
## Итоговые выводы
1. Обрыв больших файлов на ~16–23 KB — локальный прокси на машине тестирования, НЕ кластер.
2. Фикс registry (302 → presigned S3) корректен и полезен: registry больше не участвует в передаче файла.
3. `terraform init` с ВМ ломался из-за недоступного внешнего DNS; лечится `/etc/hosts` (или рабочим внутренним DNS).
4. Gitea с ВМ сборки недоступен по сети — для сборки образа использовать машину с доступом (локальную).
-123
View File
@@ -1,123 +0,0 @@
# Plan: Flask Registry — tf_registry (ПОЛНЫЙ ОРИГИНАЛ)
> Создан на основе CHAT_SUMMARY_2026-08-06.md + анализа Go-кода.
> Сохранён из session-памяти в постоянный HISTORY.
## TL;DR
Переписать Go-реестр (`server/main.go`) на Python/Flask для деплоя как managed-сервис в облаке.
Логика 1:1 — те же эндпоинты, та же S3-структура, тот же GPG-ключ.
---
## Анализ Go-кода
### ENV-переменные (те же в Flask):
| Переменная | По умолчанию |
|---|---|
| `REGISTRY_HOSTNAME` | `localhost:8080` |
| `S3_ENDPOINT` | — |
| `S3_ACCESS_KEY` | — |
| `S3_SECRET_KEY` | — |
| `S3_BUCKET` | `terraform-registry` |
### S3-структура путей (ключи):
```
Провайдер: {hostname}/{namespace}/{name}/{version}/terraform-provider-{name}_{version}_{os}_{arch}.zip
SHA256: {hostname}/{namespace}/{name}/{version}/terraform-provider-{name}_{version}_SHA256SUMS
Подпись: {hostname}/{namespace}/{name}/{version}/terraform-provider-{name}_{version}_SHA256SUMS.sig
Docs: docs/{namespace}/{name}/{version}/{path}
```
### Эндпоинты:
| Метод | URL | Что делает |
|---|---|---|
| GET | `/.well-known/terraform.json` | Discovery: `{"providers.v1": "/v1/providers/"}` |
| GET | `/v1/providers/{ns}/{name}/versions` | Список версий + платформ из S3 |
| GET | `/v1/providers/{ns}/{name}/{ver}/download/{os}/{arch}` | JSON с URL для скачивания |
| GET | `/v1/proxy?bucket=...&key=...` | Стриминг файла из S3 |
| GET | `/docs/{ns}/{name}/{ver}/...` | Статика документации из S3 |
| GET | `/healthz`, `/readyz` | Отвечают "ok" 200 |
| GET | `/` | HTML-страница "ONLINE" |
### Платформы (listVersions):
Только два паттерна из имён файлов (в исходниках):
- `_linux_amd64.zip``{os: linux, arch: amd64}`
- `_windows_amd64.zip``{os: windows, arch: amd64}`
> ⚠️ Живой Go-деплой также отдаёт `darwin_amd64` — operator собрал более новую версию.
### GPG-ключ:
Жёстко зашит в `registrykeys/public_key.go`:
- `KeyID = "CB3A0DF161ECC416"` (legacy)
- `ASCIIArmor = "-----BEGIN PGP PUBLIC KEY BLOCK-----\n..."`
Flask просто возвращает его как строку в JSON — **никакого python-gnupg не нужно**.
> ⚠️ Живой Go-деплой отдаёт ДВА ключа: основной `3EC4673EB798238A` + legacy `CB3A0DF161ECC416`.
### Прокси:
Параметры: `?bucket=...&key=...` (НЕ `?url=...` — в chat summary ошибка).
Стримит файл через `Response(stream_with_context(...))`.
### Docs:
Кандидаты S3-ключей (в порядке попытки):
1. `docs/{ns}/{name}/{ver}/{rest}` (точный путь)
2. `docs/{ns}/{name}/{ver}/{rest}/index.html` (если нет расширения)
3. `docs/{ns}/{name}/{ver}/index.html` (fallback)
MIME по расширению файла.
---
## Файлы для создания:
```
tf_registry/
├── site/
│ └── app.py # ~300 строк, весь Flask
└── requirements.txt # flask, boto3
```
## Шаги:
### Фаза 1 — Git-реорганизация (опционально)
1. `git checkout -b golang` — сохранить Go-код
2. `git checkout master`
3. Удалить: `server/`, `operator/`, `k8s/`, `charts/`, `scripts/`, `registrykeys/`, `REGISTRY_OVERVIEW.md`
4. Оставить: `site/app.py`, `requirements.txt`, `README.md`
### Фаза 2 — Flask app.py
**Структура app.py:**
1. Импорты: `flask`, `boto3`, `os`, `mimetypes`, `io`
2. Конфиг из ENV (5 переменных выше)
3. S3-клиент: `boto3.client('s3', endpoint_url=..., aws_access_key_id=..., aws_secret_access_key=...)`
4. GPG_KEY_ID и GPG_ASCII_ARMOR — константы
5. Роуты в точном соответствии с таблицей выше
### Фаза 3 — Проверка
1. `python -m py_compile site/app.py`
2. Деплой в managed-сервис `registry` через UI
3. `curl /.well-known/terraform.json`
4. `curl /v1/providers/nubes-test/nubes/versions`
5. `terraform init` с новым `source`
---
## Решения:
- GPG: только публичный ключ в JSON, никакой криптографии в Flask
- Прокси: `?bucket=&key=` (не `?url=`)
- Платформы: linux_amd64 + windows_amd64 (как в исходниках Go) → + darwin (как в живом деплое)
- Домен: `registry.pythonk8s.dev.nubes.ru`
- S3_PREFIX: `registry.kube5s.ru` (S3-ключи под старым хостнеймом)
## Исключено из scope:
- Operator (остаётся в ветке golang)
- K8s-манифесты
- Docker-образы
- Docs-сервер как отдельный деплоймент
## Расхождения исходники vs живой деплой (обнаружены при проверке):
| Что | Исходники | Живой Go |
|-----|-----------|----------|
| Платформы | linux, windows | linux, windows, **darwin** |
| GPG-ключи | 1 ключ (CB3A0D...) | **2 ключа** (3EC467... + CB3A0D...) |
| `id` в versions | `nubes-test/nubes` | `registry.kube5s.ru/nubes-test/nubes` |
+2 -2
View File
@@ -42,8 +42,8 @@
# Сборка образа (на ВМ) # Сборка образа (на ВМ)
ssh naeel@5.172.178.213 ssh naeel@5.172.178.213
cd ~/tf_registry && git checkout go-container cd ~/tf_registry && git checkout go-container
docker build -t gitea.services.ngcloud.ru/nail/tf_registry:latest . docker build -t gitea.services.ngcloud.ru/terraform/tf_registry:latest .
docker push gitea.services.ngcloud.ru/nail/tf_registry:latest docker push gitea.services.ngcloud.ru/terraform/tf_registry:latest
# Редеплой — через UI modify или пересоздать инстанс # Редеплой — через UI modify или пересоздать инстанс
``` ```
+11 -12
View File
@@ -29,23 +29,22 @@ set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/.." && pwd)}" ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/.." && pwd)}"
PROVIDER_DIR="${PROVIDER_DIR:-${ROOT_DIR}/universal_rebuild}" PROVIDER_DIR="${PROVIDER_DIR:-${HOME}/tf_provider/provider}"
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/artifacts/provider_build}" BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/artifacts/provider_build}"
GPG_KEY_FILE="${GPG_KEY_FILE:-${ROOT_DIR}/secrets/private_key.asc}" GPG_KEY_FILE="${GPG_KEY_FILE:-${HOME}/tf_provider/secrets/private_key.asc}"
VERSION="${1:-2.0.2}" VERSION="${1:-2.0.2}"
REGISTRY_HOSTNAME="${REGISTRY_HOSTNAME:-terra.k8c.ru}" REGISTRY_HOSTNAME="${REGISTRY_HOSTNAME:-tf-registry.containerk8s.services.ngcloud.ru}"
NAMESPACE="${NAMESPACE:-nubes}" NAMESPACE="${NAMESPACE:-nubes-test}"
PROVIDER_NAME="${PROVIDER_NAME:-nubes}" PROVIDER_NAME="${PROVIDER_NAME:-nubes}"
S3_BUCKET="${S3_BUCKET:-terraform-registry}" S3_BUCKET="${S3_BUCKET:-nubes-terraform-registry}"
S3_ENDPOINT="${S3_ENDPOINT:-}" S3_ENDPOINT="${S3_ENDPOINT:-https://s3.msk-1.ngcloud.ru}"
S3_ACCESS_KEY="${S3_ACCESS_KEY:-}" S3_ACCESS_KEY="${S3_ACCESS_KEY:-}"
S3_SECRET_KEY="${S3_SECRET_KEY:-}" S3_SECRET_KEY="${S3_SECRET_KEY:-}"
if [[ -z "$S3_ENDPOINT" || -z "$S3_ACCESS_KEY" || -z "$S3_SECRET_KEY" ]]; then if [[ -z "$S3_ACCESS_KEY" || -z "$S3_SECRET_KEY" ]]; then
echo "Error: S3_ENDPOINT/S3_ACCESS_KEY/S3_SECRET_KEY must be set" >&2 echo "Error: S3_ACCESS_KEY/S3_SECRET_KEY must be set" >&2
echo "Hint: see ${ROOT_DIR}/secrets/.s3cfg_registry" >&2
exit 2 exit 2
fi fi
@@ -77,7 +76,7 @@ build_and_zip() {
fi fi
echo "Building for ${os}/${arch}..." echo "Building for ${os}/${arch}..."
(cd "$PROVIDER_DIR" && CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -o "${BUILD_DIR}/${binary}" .) (cd "$PROVIDER_DIR" && CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -ldflags "-X main.address=${REGISTRY_HOSTNAME}/${NAMESPACE}/${PROVIDER_NAME} -X main.version=${VERSION}" -o "${BUILD_DIR}/${binary}" .)
(cd "$BUILD_DIR" && python3 -m zipfile -c "terraform-provider-nubes_${VERSION}_${os}_${arch}.zip" "$binary") (cd "$BUILD_DIR" && python3 -m zipfile -c "terraform-provider-nubes_${VERSION}_${os}_${arch}.zip" "$binary")
rm -f "${BUILD_DIR:?}/${binary}" rm -f "${BUILD_DIR:?}/${binary}"
@@ -104,8 +103,8 @@ gpg --batch --yes --detach-sign --default-key "$key_id" \
--output "${BUILD_DIR}/terraform-provider-nubes_${VERSION}_SHA256SUMS.sig" \ --output "${BUILD_DIR}/terraform-provider-nubes_${VERSION}_SHA256SUMS.sig" \
"${BUILD_DIR}/terraform-provider-nubes_${VERSION}_SHA256SUMS" "${BUILD_DIR}/terraform-provider-nubes_${VERSION}_SHA256SUMS"
mc alias set registry "$s3_endpoint_url" "$S3_ACCESS_KEY" "$S3_SECRET_KEY" --api S3v4 mc alias set prod-s3 "$s3_endpoint_url" "$S3_ACCESS_KEY" "$S3_SECRET_KEY" --api S3v4
S3_PATH="registry/${S3_BUCKET}/${REGISTRY_HOSTNAME}/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}/" S3_PATH="prod-s3/${S3_BUCKET}/${REGISTRY_HOSTNAME}/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}/"
echo "Uploading to: ${S3_PATH}" echo "Uploading to: ${S3_PATH}"
mc cp "${BUILD_DIR}"/*.zip "$S3_PATH" mc cp "${BUILD_DIR}"/*.zip "$S3_PATH"
-116
View File
@@ -1,116 +0,0 @@
package main
import (
"context"
"fmt"
"io"
"log"
"mime"
"net/http"
"path/filepath"
"strings"
s3 "github.com/minio/minio-go/v7"
)
// parseDocsRequestPath parses paths like:
// /docs/<namespace>/<name>/<version>/... (rest may be empty)
func parseDocsRequestPath(p string) (namespace, name, version, rest string, err error) {
p = strings.TrimPrefix(p, "/")
p = strings.TrimPrefix(p, "docs/")
parts := strings.SplitN(p, "/", 4)
if len(parts) < 3 {
err = fmt.Errorf("invalid docs path: %s", p)
return
}
namespace = parts[0]
name = parts[1]
version = parts[2]
if len(parts) == 3 {
rest = ""
} else {
rest = parts[3]
}
return
}
// docsObjectKey builds the S3 key for a docs object given parsed parts.
func docsObjectKey(namespace, name, version, pathPart string) string {
clean := strings.TrimPrefix(pathPart, "/")
if clean == "" {
return fmt.Sprintf("docs/%s/%s/%s/index.html", namespace, name, version)
}
return fmt.Sprintf("docs/%s/%s/%s/%s", namespace, name, version, clean)
}
// tryCandidateKeys returns a list of keys to attempt for a given request path.
// Order matters: exact path first, then <path>/index.html, then top-level index.
func tryCandidateKeys(namespace, name, version, rest string) []string {
keys := []string{}
if rest == "" {
keys = append(keys, docsObjectKey(namespace, name, version, "index.html"))
return keys
}
// exact
keys = append(keys, docsObjectKey(namespace, name, version, rest))
// if it looks like a directory or has no extension, try index under it
if strings.HasSuffix(rest, "/") || filepath.Ext(rest) == "" {
keys = append(keys, docsObjectKey(namespace, name, version, strings.TrimSuffix(rest, "/")+"/index.html"))
}
// finally, try root index
keys = append(keys, docsObjectKey(namespace, name, version, "index.html"))
return keys
}
// docsHandler serves static documentation files from S3 (public-facing via Ingress).
func docsHandler(w http.ResponseWriter, r *http.Request) {
ns, name, ver, rest, err := parseDocsRequestPath(r.URL.Path)
if err != nil {
http.Error(w, "Bad docs path", http.StatusBadRequest)
return
}
ctx := context.Background()
candidates := tryCandidateKeys(ns, name, ver, rest)
log.Printf("Docs candidates (host=%s): %v", hostname, candidates)
var lastErr error
for _, key := range candidates {
obj, err := s3Client.GetObject(ctx, bucketName, key, s3.GetObjectOptions{})
if err != nil {
lastErr = err
continue
}
stat, err := obj.Stat()
if err != nil {
lastErr = err
_ = obj.Close()
continue
}
// Determine content-type
ext := filepath.Ext(key)
ctype := mime.TypeByExtension(ext)
if ctype == "" {
// fallback for HTML
if ext == ".html" || strings.HasSuffix(key, "index.html") {
ctype = "text/html; charset=utf-8"
} else {
ctype = "application/octet-stream"
}
}
w.Header().Set("Content-Type", ctype)
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
w.Header().Set("Last-Modified", stat.LastModified.Format(http.TimeFormat))
if _, err := io.Copy(w, obj); err != nil {
log.Printf("Error streaming object %s: %v", key, err)
}
_ = obj.Close()
return
}
log.Printf("Docs not found in candidates: %v, lastErr: %v", candidates, lastErr)
http.Error(w, "Documentation not found", http.StatusNotFound)
}
+6
View File
@@ -0,0 +1,6 @@
package main
import "embed"
//go:embed templates/index.html
var templateFS embed.FS
+95
View File
@@ -0,0 +1,95 @@
package main
import (
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"strings"
"time"
)
// Terraform Registry Protocol Structs
type Discovery struct {
ProvidersV1 string `json:"providers.v1"`
}
type VersionList struct {
ID string `json:"id"`
Versions []Version `json:"versions"`
Warnings []string `json:"warnings"`
}
type Version struct {
Version string `json:"version"`
Protocols []string `json:"protocols"`
Platforms []Platform `json:"platforms"`
}
type Platform struct {
OS string `json:"os"`
Arch string `json:"arch"`
}
type DownloadResponse struct {
Protocols []string `json:"protocols"`
OS string `json:"os"`
Arch string `json:"arch"`
Filename string `json:"filename"`
DownloadURL string `json:"download_url"`
ShasumsURL string `json:"shasums_url"`
ShasumsSignatureURL string `json:"shasums_signature_url"`
Shasum string `json:"shasum"`
SigningKeys SigningKeys `json:"signing_keys"`
}
type SigningKeys struct {
GPGPublicKeys []GPGPublicKey `json:"gpg_public_keys"`
}
type GPGPublicKey struct {
KeyID string `json:"key_id"`
ASCIIArmor string `json:"ascii_armor"`
}
func discoveryHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Discovery{ProvidersV1: "/v1/providers/"})
}
func rootHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
tmpl, err := templateFS.ReadFile("templates/index.html")
if err != nil {
log.Printf("root: template read error: %v", err)
http.Error(w, "Internal Server Error", http.StatusInternalServerError)
return
}
html := strings.Replace(string(tmpl), "{{.Version}}", VERSION, 1)
fmt.Fprint(w, html)
}
func healthzHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
func readyzHandler(w http.ResponseWriter, r *http.Request) {
ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second)
defer cancel()
_, err := s3Client.BucketExists(ctx, bucketName)
if err != nil {
msg := fmt.Sprintf("s3 unreachable: %v", err)
log.Printf("readyz: %s", msg)
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte(msg))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
+25
View File
@@ -0,0 +1,25 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">
<!-- Creator: CorelDRAW -->
<svg xmlns="http://www.w3.org/2000/svg" xml:space="preserve" width="82.3711mm" height="18.2443mm" version="1.1" style="shape-rendering:geometricPrecision; text-rendering:geometricPrecision; image-rendering:optimizeQuality; fill-rule:evenodd; clip-rule:evenodd"
viewBox="0 0 8221.93 1821.07"
xmlns:xlink="http://www.w3.org/1999/xlink"
xmlns:xodm="http://www.corel.com/coreldraw/odm/2003">
<defs>
<style type="text/css">
<![CDATA[
.fil0 {fill:#001C34}
]]>
</style>
</defs>
<g id="Слой_x0020_1">
<metadata id="CorelCorpID_0Corel-Layer"/>
<g id="_2283355517888">
<path class="fil0" d="M477.49 479.69l-413.51 0 -63.98 276.48 178.44 0 1.17 1028.71 0 26.75 0.03 0 276.39 0 0 -871.72c0,-101.28 82.43,-183.69 183.75,-183.69l589.25 3.44c101.34,0 183.76,82.46 183.76,183.74l0 871.72 276.44 0 0 -871.72c0,-253.77 -206.46,-460.15 -460.05,-460.15l-589.52 -3.53 -162.17 0.45 0 -0.48z"/>
<path class="fil0" d="M6664.65 1813.37l1181.04 0c212.14,0 376.24,-175.01 376.24,-387.08 0,-212.13 -172.55,-384.68 -384.69,-384.68l-653.66 0c-60.22,0 -109.16,-48.94 -109.16,-109.18l0 -66.18c0,-60.19 48.94,-109.14 109.16,-109.14l695.76 0 63.74 -275.49 -759.5 0c-212.13,0 -384.66,172.53 -384.66,384.63l0 66.18c0,212.13 172.53,384.67 384.66,384.67l653.66 0c60.2,0 109.2,49 109.2,109.19 0,60.13 -49,116.1 -109.2,116.1l-1118.9 0 -53.68 270.98z"/>
<path class="fil0" d="M6200.79 483.5l-723.21 0c-215.88,0 -391.47,175.6 -391.47,391.51l0 485.5c0,248.38 202.05,450.4 450.4,450.4l989.38 0 62.13 -268.51 -1051.51 0c-96.41,0 -174.95,-85.37 -174.95,-181.88l-1.5 -39.46 923.62 0 308.51 -1.84 0 -237.95 0 -206.26c0,-215.91 -175.59,-391.51 -391.41,-391.51zm115.96 560.28l-955.19 0 0 -168.77c0,-63.96 52.07,-116.05 116.02,-116.05l723.21 0c63.91,0 115.96,52.08 115.96,116.05l0 168.77z"/>
<path class="fil0" d="M4683.49 1194.24l0 168.28c0,100.92 -81.7,178.37 -182.67,178.37l-589.87 1.23c-93.18,0 -170.28,-69.96 -181.63,-160.09l0 -458.13c11.36,-90.1 88.46,-160.07 181.63,-160.07l589.44 3.5c100.97,0 183.1,82.18 183.1,183.1l0 30.42 0 213.4zm-1230.2 -345.53l-0.89 -795.03 276.91 -53.68 0 526.07c55.74,-24.16 117.03,-37.74 181.5,-37.74l589.71 3.5c252.69,0 458.42,205.72 458.42,458.59l0 30.42 0 213.4 0 168.28c0,252.86 -205.73,458.54 -458.42,458.54l-589.71 -3.5c-252.7,0 -458.41,-205.67 -458.41,-458.56l0 -171.61 0 -240.48 0.89 -98.2z"/>
<path class="fil0" d="M3041.25 1150.84l0 204.28c0,100.93 -82.15,183.05 -183.11,183.05l-582.11 -3.46c-100.96,0 -183.11,-82.16 -183.11,-183.08l0 -67.42 1.48 0.24 0 -862.65 -276.91 53.68 0.12 103.41 -0.12 0.04 0 772.69c0,252.87 205.72,458.54 458.39,458.54l582.4 3.5c252.67,0 458.4,-205.68 458.4,-458.55l0 -70.79 0.71 0.12 0 -862.65 -276.91 53.68 0.76 675.35z"/>
</g>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

+10 -349
View File
@@ -1,18 +1,12 @@
package main package main
import ( import (
"bufio"
"context" "context"
"encoding/json" "embed"
"fmt"
"io"
"log" "log"
"net/http" "net/http"
"net/url"
"os" "os"
"os/signal" "os/signal"
"sort"
"strings"
"syscall" "syscall"
"time" "time"
@@ -20,64 +14,22 @@ import (
"github.com/minio/minio-go/v7/pkg/credentials" "github.com/minio/minio-go/v7/pkg/credentials"
) )
//go:embed logo.svg
var logoFile embed.FS
var ( var (
s3Client *s3.Client s3Client *s3.Client
bucketName = "terraform-registry" // Default bucketName = "terraform-registry"
hostname = os.Getenv("REGISTRY_HOSTNAME") hostname = os.Getenv("REGISTRY_HOSTNAME")
s3Prefix = os.Getenv("S3_PREFIX") // S3 key prefix (may differ from hostname) s3Prefix = os.Getenv("S3_PREFIX")
) )
const VERSION = "1.1.1" const VERSION = "0.0.6"
// Terraform Registry Protocol Structs
type Discovery struct {
ProvidersV1 string `json:"providers.v1"`
}
type VersionList struct {
ID string `json:"id"`
Versions []Version `json:"versions"`
Warnings []string `json:"warnings"`
}
type Version struct {
Version string `json:"version"`
Protocols []string `json:"protocols"`
Platforms []Platform `json:"platforms"`
}
type Platform struct {
OS string `json:"os"`
Arch string `json:"arch"`
}
type DownloadResponse struct {
Protocols []string `json:"protocols"`
OS string `json:"os"`
Arch string `json:"arch"`
Filename string `json:"filename"`
DownloadURL string `json:"download_url"`
ShasumsURL string `json:"shasums_url"`
ShasumsSignatureURL string `json:"shasums_signature_url"`
Shasum string `json:"shasum"`
SigningKeys SigningKeys `json:"signing_keys"`
}
type SigningKeys struct {
GPGPublicKeys []GPGPublicKey `json:"gpg_public_keys"`
}
type GPGPublicKey struct {
KeyID string `json:"key_id"`
ASCIIArmor string `json:"ascii_armor"`
}
func main() { func main() {
// 1. Init S3 Connection
if hostname == "" { if hostname == "" {
hostname = "localhost:5000" hostname = "localhost:5000"
} }
// S3_PREFIX default comes from Dockerfile ENV; override in jsonEnv if needed.
if s3Prefix == "" { if s3Prefix == "" {
s3Prefix = hostname s3Prefix = hostname
} }
@@ -92,19 +44,19 @@ func main() {
var err error var err error
useSSL := os.Getenv("S3_USE_SSL") != "false" useSSL := os.Getenv("S3_USE_SSL") != "false"
log.Printf("S3 connecting: endpoint=%s bucket=%s useSSL=%v", endpoint, bucketName, useSSL)
s3Client, err = s3.New(endpoint, &s3.Options{ s3Client, err = s3.New(endpoint, &s3.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: useSSL, Secure: useSSL,
}) })
if err != nil { if err != nil {
log.Fatalln(err) log.Fatalf("S3 init failed: %v", err)
} }
// 2. HTTP Handlers
http.HandleFunc("/.well-known/terraform.json", discoveryHandler) http.HandleFunc("/.well-known/terraform.json", discoveryHandler)
http.HandleFunc("/v1/providers/", router) http.HandleFunc("/v1/providers/", router)
http.HandleFunc("/v1/proxy", proxyHandler) http.HandleFunc("/v1/proxy", proxyHandler)
http.HandleFunc("/docs/", docsHandler) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(logoFile))))
http.Handle("/", http.HandlerFunc(rootHandler)) http.Handle("/", http.HandlerFunc(rootHandler))
http.HandleFunc("/healthz", healthzHandler) http.HandleFunc("/healthz", healthzHandler)
http.HandleFunc("/readyz", readyzHandler) http.HandleFunc("/readyz", readyzHandler)
@@ -135,294 +87,3 @@ func main() {
} }
log.Println("Server stopped") log.Println("Server stopped")
} }
func rootHandler(w http.ResponseWriter, r *http.Request) {
if r.URL.Path != "/" {
http.NotFound(w, r)
return
}
w.Header().Set("Content-Type", "text/html; charset=utf-8")
fmt.Fprintf(w, `<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Terraform Registry · Nubes</title>
<style>
:root {
--primary: #2563eb;
--primary-dark: #1d4ed8;
--bg: #f8fafc;
--card-bg: #fff;
--border: #e2e8f0;
--text: #1a1a1a;
--muted: #6b7280;
--green: #22c55e;
--radius: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex; flex-direction: column;
}
.header {
background: var(--card-bg);
border-bottom: 1px solid var(--border);
padding: 16px 24px;
display: flex; align-items: center; gap: 12px;
}
.header svg { width: 120px; height: auto; }
.main {
flex: 1;
display: flex; align-items: center; justify-content: center;
padding: 40px 24px;
}
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: 0 1px 3px rgba(0,0,0,.04);
width: 100%%; max-width: 480px;
overflow: hidden;
}
.card-header {
background: #f3f4f6;
padding: 14px 20px;
font-weight: 600; font-size: 16px;
display: flex; align-items: center; gap: 8px;
}
.card-body { padding: 20px; }
.row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; }
.row + .row { border-top: 1px solid var(--border); }
.label { color: var(--muted); font-size: 14px; }
.value { font-size: 14px; font-weight: 500; }
.badge {
display: inline-flex; align-items: center; gap: 6px;
background: #f0fdf4; color: #166534;
padding: 4px 10px; border-radius: 999px;
font-size: 13px; font-weight: 500;
}
.badge::before {
content: ''; width: 8px; height: 8px;
background: var(--green); border-radius: 50%%;
}
.footer {
text-align: center; padding: 16px 24px;
color: var(--muted); font-size: 12px;
}
a { color: var(--primary); text-decoration: none; }
a:hover { color: var(--primary-dark); }
</style>
</head>
<body>
<div class="header">
<svg viewBox="0 0 8222 1821" xmlns="http://www.w3.org/2000/svg">
<path d="M477 480H64L0 756h178l2 1029v27h276V941c0-101 82-184 184-184h589c101 0 184 83 184 184v872h276V941c0-254-206-460-460-460H640L477 480zM6665 1813h1181c212 0 376-175 376-387 0-212-173-385-385-385h-654c-60 0-109-49-109-109v-66c0-60 49-109 109-109h696l64-275h-760c-212 0-384 172-384 384v66c0 212 172 385 384 385h654c60 0 109 49 109 109s-49 116-109 116H6719l-54 271zM6201 484h-723c-216 0-392 175-392 391v486c0 248 202 450 451 450h989l62-269h-1052c-96 0-175-85-175-181v-40h924l308-2v-238-206c0-216-175-392-391-392zm116 560H5362V875c0-64 52-116 116-116h723c64 0 116 52 116 116v169zM4683 1194v169c0 101-81 178-182 178h-590c-93 0-170-70-182-160V923c11-90 89-160 182-160h589c101 0 183 82 183 183v31 213zm-1230-345-1-795 277-54v526c56-24 117-38 181-38h590c253 0 459 206 459 459v30 213 169c0 252-206 458-459 458h-589c-253 0-459-206-459-459V975l1-98zM3041 1151v204c0 101-82 183-183 183h-582c-101 0-183-82-183-183v-67l1 1V426l-277 54v874c0 253 206 458 458 458h583c253 0 458-205 458-458v-71l1-863-277 54v675z" fill="#001C34"/>
</svg>
</div>
<div class="main">
<div class="card">
<div class="card-header">Terraform Provider Registry</div>
<div class="card-body">
<div class="row">
<span class="label">Статус</span>
<span class="badge">ONLINE</span>
</div>
<div class="row">
<span class="label">Версия</span>
<span class="value">v`+VERSION+`</span>
</div>
<div class="row">
<span class="label">Discovery</span>
<span class="value"><a href="/.well-known/terraform.json">/.well-known/terraform.json</a></span>
</div>
<div class="row">
<span class="label">Провайдеры</span>
<span class="value"><a href="/v1/providers/">/v1/providers/</a></span>
</div>
</div>
</div>
</div>
<div class="footer">Powered by Nubes Cloud · S3 Storage</div>
</body>
</html>`)
}
func proxyHandler(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
if key == "" {
http.Error(w, "Missing key param", http.StatusBadRequest)
return
}
obj, err := s3Client.GetObject(context.Background(), bucketName, key, s3.GetObjectOptions{})
if err != nil {
log.Printf("Error getting object %s/%s: %v", bucketName, key, err)
http.Error(w, "File not found", http.StatusNotFound)
return
}
defer obj.Close()
stat, err := obj.Stat()
if err != nil {
log.Printf("Error stating object %s/%s: %v", bucketName, key, err)
http.Error(w, "File not found or not accessible", http.StatusNotFound)
return
}
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
w.Header().Set("Content-Type", "application/octet-stream")
w.Header().Set("Last-Modified", stat.LastModified.Format(http.TimeFormat))
if _, err := io.Copy(w, obj); err != nil {
log.Printf("Error streaming object: %v", err)
}
}
func discoveryHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(Discovery{ProvidersV1: "/v1/providers/"})
}
func router(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/v1/providers/")
parts := strings.Split(path, "/")
if len(parts) == 3 && parts[2] == "versions" {
listVersions(w, r, parts[0], parts[1])
return
}
if len(parts) == 6 && parts[3] == "download" {
downloadVersion(w, r, parts[0], parts[1], parts[2], parts[4], parts[5])
return
}
http.Error(w, "Not Found", http.StatusNotFound)
}
func listVersions(w http.ResponseWriter, r *http.Request, namespace, pType string) {
prefix := fmt.Sprintf("%s/%s/%s/", s3Prefix, namespace, pType)
ctx := context.Background()
versions := []Version{}
seenVersions := map[string]*Version{}
objectCh := s3Client.ListObjects(ctx, bucketName, s3.ListObjectsOptions{
Prefix: prefix,
Recursive: true,
})
for object := range objectCh {
if object.Err != nil {
continue
}
parts := strings.Split(object.Key, "/")
if len(parts) < 5 {
continue
}
verStr := parts[3]
fileName := parts[4]
if _, ok := seenVersions[verStr]; !ok {
seenVersions[verStr] = &Version{
Version: verStr,
Protocols: []string{"5.0"},
Platforms: []Platform{},
}
}
if strings.Contains(fileName, "_darwin_amd64.zip") {
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "amd64"})
}
if strings.Contains(fileName, "_linux_amd64.zip") {
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "linux", Arch: "amd64"})
}
if strings.Contains(fileName, "_windows_amd64.zip") {
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "windows", Arch: "amd64"})
}
}
for _, v := range seenVersions {
versions = append(versions, *v)
}
sort.Slice(versions, func(i, j int) bool {
return versions[i].Version < versions[j].Version
})
resp := VersionList{
ID: fmt.Sprintf("%s/%s", namespace, pType),
Versions: versions,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func downloadVersion(w http.ResponseWriter, r *http.Request, namespace, pType, version, osType, arch string) {
basePath := fmt.Sprintf("%s/%s/%s/%s", s3Prefix, namespace, pType, version)
filename := fmt.Sprintf("terraform-provider-%s_%s_%s_%s.zip", pType, version, osType, arch)
fullKey := fmt.Sprintf("%s/%s", basePath, filename)
shasumsKey := fmt.Sprintf("%s/terraform-provider-%s_%s_SHA256SUMS", basePath, pType, version)
sigKey := fmt.Sprintf("%s/terraform-provider-%s_%s_SHA256SUMS.sig", basePath, pType, version)
var shasumValue string
shasumsObj, err := s3Client.GetObject(context.Background(), bucketName, shasumsKey, s3.GetObjectOptions{})
if err == nil {
defer shasumsObj.Close()
scanner := bufio.NewScanner(shasumsObj)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, filename) {
fields := strings.Fields(line)
if len(fields) >= 1 {
shasumValue = fields[0]
}
break
}
}
}
baseURL := "https://" + hostname
downloadLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(fullKey))
shasumsLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(shasumsKey))
sigLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(sigKey))
resp := DownloadResponse{
Protocols: []string{"5.0"},
OS: osType,
Arch: arch,
Filename: filename,
DownloadURL: downloadLink,
ShasumsURL: shasumsLink,
ShasumsSignatureURL: sigLink,
Shasum: shasumValue,
SigningKeys: SigningKeys{
GPGPublicKeys: []GPGPublicKey{gpgPrimaryKey, gpgLegacyKey},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func healthzHandler(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
func readyzHandler(w http.ResponseWriter, r *http.Request) {
_, err := s3Client.BucketExists(context.Background(), bucketName)
if err != nil {
log.Printf("readyz: S3 bucket check failed: %v", err)
w.WriteHeader(http.StatusServiceUnavailable)
w.Write([]byte("s3 unreachable"))
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte("ok"))
}
+28
View File
@@ -0,0 +1,28 @@
package main
import (
"net/http"
"strings"
"time"
)
func proxyHandler(w http.ResponseWriter, r *http.Request) {
key := r.URL.Query().Get("key")
if key == "" {
http.Error(w, "Missing key param", http.StatusBadRequest)
return
}
if !strings.HasPrefix(key, s3Prefix+"/") && !strings.HasPrefix(key, "docs/") {
http.Error(w, "Forbidden", http.StatusForbidden)
return
}
presigned, err := s3Client.PresignedGetObject(r.Context(), bucketName, key, 15*time.Minute, nil)
if err != nil {
http.Error(w, "Failed to sign object", http.StatusInternalServerError)
return
}
http.Redirect(w, r, presigned.String(), http.StatusFound)
}
+145
View File
@@ -0,0 +1,145 @@
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"log"
"net/http"
"net/url"
"sort"
"strings"
"time"
s3 "github.com/minio/minio-go/v7"
)
func router(w http.ResponseWriter, r *http.Request) {
path := strings.TrimPrefix(r.URL.Path, "/v1/providers/")
parts := strings.Split(path, "/")
if len(parts) == 3 && parts[2] == "versions" {
listVersions(w, parts[0], parts[1])
return
}
if len(parts) == 6 && parts[3] == "download" {
downloadVersion(w, parts[0], parts[1], parts[2], parts[4], parts[5])
return
}
http.Error(w, "Not Found", http.StatusNotFound)
}
func listVersions(w http.ResponseWriter, namespace, pType string) {
prefix := fmt.Sprintf("%s/%s/%s/", s3Prefix, namespace, pType)
versions := []Version{}
seenVersions := map[string]*Version{}
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
objectCh := s3Client.ListObjects(ctx, bucketName, s3.ListObjectsOptions{
Prefix: prefix,
Recursive: true,
})
for object := range objectCh {
if object.Err != nil {
continue
}
parts := strings.Split(object.Key, "/")
if len(parts) < 5 {
continue
}
verStr := parts[3]
fileName := parts[4]
if _, ok := seenVersions[verStr]; !ok {
seenVersions[verStr] = &Version{
Version: verStr,
Protocols: []string{"5.0"},
Platforms: []Platform{},
}
}
if strings.Contains(fileName, "_darwin_amd64.zip") {
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "amd64"})
}
if strings.Contains(fileName, "_linux_amd64.zip") {
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "linux", Arch: "amd64"})
}
if strings.Contains(fileName, "_windows_amd64.zip") {
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "windows", Arch: "amd64"})
}
}
for _, v := range seenVersions {
versions = append(versions, *v)
}
sort.Slice(versions, func(i, j int) bool {
return versions[i].Version < versions[j].Version
})
resp := VersionList{
ID: fmt.Sprintf("%s/%s", namespace, pType),
Versions: versions,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
func downloadVersion(w http.ResponseWriter, namespace, pType, version, osType, arch string) {
basePath := fmt.Sprintf("%s/%s/%s/%s", s3Prefix, namespace, pType, version)
filename := fmt.Sprintf("terraform-provider-%s_%s_%s_%s.zip", pType, version, osType, arch)
fullKey := fmt.Sprintf("%s/%s", basePath, filename)
shasumsKey := fmt.Sprintf("%s/terraform-provider-%s_%s_SHA256SUMS", basePath, pType, version)
sigKey := fmt.Sprintf("%s/terraform-provider-%s_%s_SHA256SUMS.sig", basePath, pType, version)
var shasumValue string
s3ctx, s3cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer s3cancel()
shasumsObj, err := s3Client.GetObject(s3ctx, bucketName, shasumsKey, s3.GetObjectOptions{})
if err == nil {
defer shasumsObj.Close()
scanner := bufio.NewScanner(shasumsObj)
for scanner.Scan() {
line := scanner.Text()
if strings.Contains(line, filename) {
fields := strings.Fields(line)
if len(fields) >= 1 {
shasumValue = fields[0]
}
break
}
}
if err := scanner.Err(); err != nil {
log.Printf("Error scanning SHA256SUMS: %v", err)
}
}
baseURL := "https://" + hostname
downloadLink := fmt.Sprintf("%s/v1/proxy?key=%s", baseURL, url.QueryEscape(fullKey))
shasumsLink := fmt.Sprintf("%s/v1/proxy?key=%s", baseURL, url.QueryEscape(shasumsKey))
sigLink := fmt.Sprintf("%s/v1/proxy?key=%s", baseURL, url.QueryEscape(sigKey))
resp := DownloadResponse{
Protocols: []string{"5.0"},
OS: osType,
Arch: arch,
Filename: filename,
DownloadURL: downloadLink,
ShasumsURL: shasumsLink,
ShasumsSignatureURL: sigLink,
Shasum: shasumValue,
SigningKeys: SigningKeys{
GPGPublicKeys: []GPGPublicKey{gpgPrimaryKey, gpgLegacyKey},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
+99
View File
@@ -0,0 +1,99 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<title>Terraform Registry · Nubes</title>
<style>
:root {
--primary: #2563eb;
--primary-dark: #1d4ed8;
--bg: #f8fafc;
--card-bg: #fff;
--border: #e2e8f0;
--text: #1a1a1a;
--muted: #6b7280;
--green: #22c55e;
--radius: 12px;
}
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
font-family: system-ui, -apple-system, sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
display: flex; flex-direction: column;
}
.header {
background: var(--card-bg);
border-bottom: 1px solid var(--border);
padding: 16px 24px;
}
.logo {
font-size: 24px; font-weight: 700; color: #001C34;
letter-spacing: -0.5px;
}
.main {
flex: 1;
display: flex; align-items: center; justify-content: center;
padding: 40px 24px;
}
.card {
background: var(--card-bg);
border: 1px solid var(--border);
border-radius: var(--radius);
box-shadow: 0 1px 3px rgba(0,0,0,.04);
width: 100%; max-width: 480px;
overflow: hidden;
}
.card-header {
background: #f3f4f6;
padding: 14px 20px;
font-weight: 600; font-size: 16px;
display: flex; align-items: center; gap: 8px;
}
.card-body { padding: 20px; }
.row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; }
.row + .row { border-top: 1px solid var(--border); }
.label { color: var(--muted); font-size: 14px; }
.value { font-size: 14px; font-weight: 500; }
.badge {
display: inline-flex; align-items: center; gap: 6px;
background: #f0fdf4; color: #166534;
padding: 4px 10px; border-radius: 999px;
font-size: 13px; font-weight: 500;
}
.badge::before {
content: ''; width: 8px; height: 8px;
background: var(--green); border-radius: 50%;
}
.footer {
text-align: center; padding: 16px 24px;
color: var(--muted); font-size: 12px;
}
a { color: var(--primary); text-decoration: none; }
a:hover { color: var(--primary-dark); }
</style>
</head>
<body>
<div class="header">
<img src="/static/logo.svg" height="18" alt="nubes">
</div>
<div class="main">
<div class="card">
<div class="card-header">Terraform Provider Registry</div>
<div class="card-body">
<div class="row">
<span class="label">Статус</span>
<span class="badge">ONLINE</span>
</div>
<div class="row">
<span class="label">Версия</span>
<span class="value">v{{.Version}}</span>
</div>
</div>
</div>
</div>
<div class="footer">Powered by Nubes Cloud · S3 Storage</div>
</body>
</html>