Author SHA1 Message Date
“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
“Naeel” 73ce2680e9 merge: чистый master → prod-tf-registry 2026-08-09 08:00:36 +04:00
“Naeel” d73bf90f0a v1.1.1 — Nubes design 2026-08-09 07:57:24 +04:00
“Naeel” 94a900d888 v1.1.0 — prod 2026-08-09 07:55:35 +04:00
“Naeel” 90807aae2f prod: креды убраны из Dockerfile → в jsonEnv при создании 2026-08-09 07:46:46 +04:00
“Naeel” 4592a35b3e prod: S3 s3.msk-1.ngcloud.ru, домен tf-registry.services.ngcloud.ru, reader-креды 2026-08-09 07:44:54 +04:00
“Naeel” 3aca89ea60 doc: README.md, CHAT_SUMMARY + REGISTRY_OVERVIEW → HISTORY 2026-08-09 07:22:36 +04:00
“Naeel” f3df103bb3 clean: удалён Flask, k8s, charts, operator, registrykeys, scripts (архив в with-flask-legacy) 2026-08-09 07:21:26 +04:00
“Naeel” 13e57aa096 doc: HOWTO-UPLOAD.md — полная инструкция по заливке провайдера (с учётом диалога) 2026-08-08 23:22:59 +04:00
“Naeel” 25b50ce59b clean: удалены go-registry и kube5s из всего кода (tests, k8s) 2026-08-08 23:17:17 +04:00
“Naeel” 2a9a7311ef doc: ceph-s3-setup.md — subuser reader, конфигурация Ceph 2026-08-08 22:37:45 +04:00
“Naeel” d841ffcbac tests: smoke.sh — 9 тестов для реестра 2026-08-08 22:25:48 +04:00
“Naeel” 01e0245139 doc: HOWTO-PUBLISH.md — инструкция по публикации провайдеров 2026-08-07 08:12:53 +04:00
“Naeel” efa7c5e085 doc: HISTORY/ARCHITECTURE.md — архитектура, S3, GPG, сборка провайдера 2026-08-06 22:04:26 +04:00
“Naeel” c235db00de doc: HISTORY/FULL-2026-08-06.md — всё что делали, обсуждали, план на будущее 2026-08-06 21:49:54 +04:00
Repinoid 24796f4b4f chore: удалить закоммиченный __pycache__ (кэш Python) 2026-08-06 12:11:06 +03:00
76 changed files with 1195 additions and 3270 deletions
-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-конфигах пользователей
+10 -10
View File
@@ -15,16 +15,16 @@ FROM alpine:3.21
RUN apk --no-cache add ca-certificates RUN apk --no-cache add ca-certificates
COPY --from=builder /app/registry /registry COPY --from=builder /app/registry /registry
# ── TEMP: reader creds hardcoded for Ceph test ──────────────────────────── # ── Non-secret config (change here → rebuild → push → redeploy) ─────────
ENV S3_ENDPOINT=ceph.tst.nubes.ru \ ENV S3_ENDPOINT=s3.msk-1.ngcloud.ru \
S3_BUCKET=terraform-registry \ S3_BUCKET=nubes-terraform-registry \
S3_PREFIX=registry.containerk8s.dev.nubes.ru \ S3_PREFIX=tf-registry.containerk8s.services.ngcloud.ru \
REGISTRY_HOSTNAME=registry.containerk8s.dev.nubes.ru \ REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru \
S3_USE_SSL=false \ PORT=5000
PORT=5000 \ # ── Secrets in UI jsonEnv: S3_ACCESS_KEY, S3_SECRET_KEY ───────────────────
S3_ACCESS_KEY=AAYTNYSZIM08DDWV3TK1 \
S3_SECRET_KEY=xQVd9CeGXACPIXTBTH4KV6DIHEAyySOFSTrS17Lc RUN adduser -D -u 1000 registry
# ── TODO: remove hardcoded keys, use jsonEnv for new instance ───────────── 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`
+168
View File
@@ -0,0 +1,168 @@
# tf_registry — Архитектура и рабочий процесс
## Что такое этот реестр
Собственный реестр Terraform-провайдеров по протоколу HashiCorp Provider Registry Protocol.
Позволяет публиковать и распространять кастомные Terraform-провайдеры без HashiCorp Registry.
**Текущий URL:** `https://go-registry.containerk8s.dev.nubes.ru`
---
## Как это работает
### Загрузка провайдера (пользователь)
```hcl
terraform {
required_providers {
nubes = {
source = "go-registry.containerk8s.dev.nubes.ru/nubes-test/nubes"
version = "5.1.16"
}
}
}
```
Terraform сам делает:
1. `GET /.well-known/terraform.json` → узнаёт API-путь
2. `GET /v1/providers/nubes-test/nubes/versions` → список версий и платформ
3. `GET /v1/providers/.../download/linux/amd64` → JSON с URL для скачивания
4. Скачивает zip, проверяет SHA256, проверяет GPG-подпись
5. Всё — провайдер установлен
Никакой аутентификации не нужно.
---
## S3-структура
Все данные хранятся в S3 (`terraform-registry`). Никакой БД нет.
```
{префикс}/{namespace}/{name}/{version}/
├── terraform-provider-{name}_{ver}_{os}_{arch}.zip — бинарник провайдера
├── terraform-provider-{name}_{ver}_SHA256SUMS — контрольные суммы
└── terraform-provider-{name}_{ver}_SHA256SUMS.sig — GPG-подпись сумм
docs/{namespace}/{name}/{version}/
├── index.html — документация провайдера
└── ... — любые статические файлы
```
**Важно:** префикс (первая часть пути в S3) сейчас — `registry.kube5s.ru` (временный хардкод).
Планируется либо сменить на свой домен, либо убрать префикс совсем.
---
## GPG-подпись
### Зачем
Terraform требует, чтобы SHA256SUMS были подписаны GPG. Это гарантирует, что провайдер не подменён.
### Как работает
1. **Приватный ключ** — хранится у того, кто заливает провайдер (`secrets/private_key.asc`)
2. **Публичный ключ** — вшит в код реестра (`gpg_key.go``/v1/providers/.../download` отдаёт его)
3. Terraform при `init` сверяет подпись SHA256SUMS с публичным ключом
### Текущие ключи
| Key ID | Роль |
|---|---|
| `3EC4673EB798238A` | Основной (текущий) |
| `CB3A0DF161ECC416` | Legacy (старый) |
Оба отдаются в `signing_keys.gpg_public_keys` — Terraform пробует каждый.
---
## Как залить новый провайдер
### Скрипт `build-provider.sh`
Находится в `server/build-provider.sh`. Делает:
1. Компилирует Go-провайдер под linux/windows/darwin × amd64
2. Упаковывает в zip
3. Генерирует SHA256SUMS
4. Импортирует приватный GPG-ключ из `secrets/private_key.asc`
5. Подписывает SHA256SUMS
6. Заливает zip + SHA256SUMS + .sig в S3
### Требования
```bash
# Переменные окружения
S3_ENDPOINT=s3.msk-1.ngcloud.ru
S3_ACCESS_KEY=...
S3_SECRET_KEY=...
S3_BUCKET=terraform-registry
# GPG
secrets/private_key.asc # приватный ключ для подписи
# Исходники провайдера
universal_rebuild/ # директория с Go-кодом провайдера
```
### Запуск
```bash
VERSION=5.1.17 ./server/build-provider.sh
```
---
## Документация
Эндпоинт `/docs/{ns}/{name}/{ver}/...` отдаёт статику из S3:
```
docs/{ns}/{name}/{ver}/index.html
```
Работает как статический веб-сервер: HTML, CSS, JS, изображения — всё, что положишь в `docs/` в S3.
Логика поиска (1:1 с Go-кодом):
1. Точный путь (`docs/ns/name/ver/style.css`)
2. Если нет расширения — `{путь}/index.html`
3. Fallback — `docs/ns/name/ver/index.html`
---
## Эндпоинты
| Метод | URL | Ответ |
|---|---|---|
| GET | `/` | HTML с версией |
| 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, shasum, GPG ключи |
| GET | `/v1/proxy?bucket=...&key=...` | Стриминг файла из S3 |
| GET | `/docs/{ns}/{name}/{ver}/...` | Статика из S3 |
| GET | `/healthz`, `/readyz` | `"ok"` 200 |
---
## CI/CD
```bash
# Сборка образа
docker build -t gitea.services.ngcloud.ru/nail/tf_registry:latest .
# Пуш в Gitea Docker Registry
docker push gitea.services.ngcloud.ru/nail/tf_registry:latest
# Редеплой — через UI modify (меняет только CPU/memory/replicas, подхватывает :latest)
```
---
## Платформы
Детектятся по имени zip-файла:
- `_darwin_amd64.zip` → macOS Intel
- `_linux_amd64.zip` → Linux
- `_windows_amd64.zip` → Windows
---
## Текущие проблемы
1. **S3_PREFIX** — хардкод `registry.kube5s.ru` (TODO в коде)
2. **modify** — нельзя менять `jsonEnv` в UI (надо просить devops)
3. **Домен**`go-registry` вместо `registry` (можно пересоздать)
+171
View File
@@ -0,0 +1,171 @@
# tf_registry — ПОЛНАЯ история (2026-08-06)
## Итог: что работает сейчас
| Сервис | Тип | Домен | Статус |
|---|---|---|---|
| `registry` | pythonk8s | `registry.pythonk8s.dev.nubes.ru` | Flask (не обновлён, висит старая версия) |
| `go-registry` | containerk8s | `go-registry.containerk8s.dev.nubes.ru` | Go ✅ (полностью работает) |
### Что проверено на Go:
- `GET /` — HTML с версией v1.0.0 ✅
- `GET /.well-known/terraform.json`
- `GET /v1/providers/nubes-test/nubes/versions` — 17 версий ✅
- `GET /v1/providers/.../download/linux/amd64` — shasum + 2 GPG ключа ✅
- Платформы: darwin, linux, windows ✅
- GPG: `3EC4673EB798238A` (основной) + `CB3A0DF161ECC416` (legacy) ✅
---
## Хронология
### Этап 1: Flask (pythonk8s)
1. Создан `site/app.py` — 7 роутов, структура `site/` (требование Nubes)
2. `requirements.txt`: flask, boto3
3. Первый деплой — ошибка `Init:Error` (gitea приватный)
4. Репо сделан публичным → деплой успешен
5. API не работал: `versions: []`, `shasum: ""`
6. **Причина:** S3-ключи под `registry.kube5s.ru`, а Flask ищет под `registry.pythonk8s.dev.nubes.ru`
7. Добавлен `S3_PREFIX` в код Flask
8. GPG — в исходниках 1 ключ, живой деплой отдаёт 2 → заменён на 2 ключа
9. Добавлен darwin в платформы
### Этап 2: Go-контейнер (containerk8s)
1. Обнаружен тип сервиса `containerk8s` — generic HTTP контейнер
2. Создана ветка `go-container`
3. Правки Go: порт `:5000` (вместо `:8080`), `PORT` из ENV
4. GPG: 2 ключа в `gpg_key.go` (без импорта registrykeys)
5. Платформы: добавлен darwin
6. Dockerfile: multi-stage (golang:1.24-alpine → alpine:3.21)
7. Gitea Docker registry: `docker login gitea.services.ngcloud.ru -u Nail`
8. Образ: `gitea.services.ngcloud.ru/nail/tf_registry:latest`
9. S3_PREFIX: хардкод `registry.kube5s.ru` (с TODO-комментарием)
10. VERSION: `const VERSION = "1.0.0"`, на главной странице
### Ошибки при деплое containerk8s
1. `docker build``repository name must be lowercase``nail/tf_registry`
2. `minio-go/v7` требует Go >= 1.25 → `GOTOOLCHAIN=auto`
3. modify не даёт менять `jsonEnv` → пришлось хардкодить `S3_PREFIX`
---
## Проблема S3_PREFIX
**Суть:** S3-объекты хранятся с префиксом `registry.kube5s.ru/...` (старый Go-реестр).
Новый реестр имеет домен `go-registry.containerk8s.dev.nubes.ru` — не совпадает.
**Текущее решение (временное):** хардкод `s3Prefix = "registry.kube5s.ru"` в `main.go`.
```go
// ── TEMPORARY HARDCODE ──────────────────────────────────────────────────
// TODO: REMOVE after devops adds jsonEnv editing to containerk8s "modify".
// Then read from ENV: s3Prefix = os.Getenv("S3_PREFIX")
// And set in UI jsonEnv: "S3_PREFIX": "registry.kube5s.ru"
// ─────────────────────────────────────────────────────────────────────────
```
**Правильные решения (на будущее):**
Путь 1 — скопировать S3-объекты под новый домен:
```
mc cp s3/.../registry.kube5s.ru/ s3/.../go-registry.containerk8s.dev.nubes.ru/ --recursive
```
Путь 2 — убрать домен из S3-путей вообще:
```
Вместо: {hostname}/{ns}/{name}/{ver}/...
Сделать: {ns}/{name}/{ver}/...
```
После любого из путей — `s3Prefix = hostname`, хардкод не нужен.
---
## Структура репозитория
### Ветка `master`
```
tf_registry/
├── site/ # Flask (НЕ ИСПОЛЬЗУЕТСЯ, мёртвый код)
│ └── app.py
├── requirements.txt # Для Flask (НЕ ИСПОЛЬЗУЕТСЯ)
├── server/ # Go-код (LEGACY, без правок)
├── operator/ # Go operator (LEGACY)
├── k8s/ # Манифесты (LEGACY)
├── charts/ # Helm (LEGACY)
├── registrykeys/ # Старый GPG (LEGACY)
├── secrets/
│ ├── env.txt # S3-креды + jsonEnv (в .gitignore)
│ └── llm.key # Токены (Gitea, LLM)
├── HISTORY/ # Документация
│ ├── README-NEW-CHAT.md
│ ├── plan-sonnet-full.md
│ ├── 2026-08-06-plan-review.md
│ └── 2026-08-06-first-deploy.md
├── Dockerfile # Для Go-контейнера
└── .gitignore # secrets/, __pycache__/
```
### Ветка `go-container` (АКТУАЛЬНАЯ)
```
server/
├── main.go # Порт 5000, S3_PREFIX hardcode, VERSION, darwin
├── gpg_key.go # 2 GPG ключа (primary + legacy)
├── docs.go # Без изменений
└── gpg/ # Без изменений
Dockerfile # golang:1.24-alpine → alpine:3.21
```
---
## CI/CD
### Сборка и пуш образа
```bash
docker build -t gitea.services.ngcloud.ru/nail/tf_registry:latest .
docker push gitea.services.ngcloud.ru/nail/tf_registry:latest
```
### Деплой
- Тип: `containerk8s` (Простой HTTP контейнер)
- `registryPath`: `gitea.services.ngcloud.ru/nail/tf_registry:latest`
- `resourceRealm`: `iot-naeel`
- `jsonEnv`: в UI, креды в `secrets/env.txt`
### Что можно менять в modify
- ТОЛЬКО: CPU, Memory, количество узлов
- jsonEnv и registryPath — НЕЛЬЗЯ (надо просить devops добавить)
---
## Что дальше
1. **Devops** — добавить редактирование `jsonEnv` в modify для containerk8s
2. **S3** — решить что делать с префиксом (копировать или убрать домен)
3. **Убрать хардкод**`s3Prefix` из ENV `S3_PREFIX`
4. **Домен** — пересоздать с именем `registry` (вместо `go-registry`)
5. **Удалить Flask**`site/app.py`, `requirements.txt` из master (мёртвый код)
6. **Удалить старые Go-файлы**`operator/`, `k8s/`, `charts/`, `registrykeys/`
---
## Креды и доступы
| Что | Где |
|---|---|
| S3 креды | `secrets/env.txt`, K8s Secret `s3-credentials` |
| Gitea токен | `secrets/llm.key` (строка `gitea`) |
| Docker login | `gitea.services.ngcloud.ru`, user `Nail`, токен `1999993c...` |
| ВМ | `ssh naeel@5.172.178.213` |
| Кубер | только через ВМ |
| Go-реестр (старый) | `https://registry.kube5s.ru` |
| Flask-реестр | `https://registry.pythonk8s.dev.nubes.ru` |
| Go-реестр (новый) | `https://go-registry.containerk8s.dev.nubes.ru` |
| Gitea репо | `https://gitea.services.ngcloud.ru/Nail/tf_registry.git` |
---
## Правила
- После каждого коммита → сразу push
- Перед деплоем → `docker build && docker push`
- `containerk8s` берёт образ из `registryPath`, НЕ клонирует git
- Gitea Docker registry требует lowercase-имён: `nail/tf_registry`, не `Nail/...`
+97
View File
@@ -0,0 +1,97 @@
# Как залить провайдер в реестр (ПРОД)
## Конфигурация
### Реестр
- URL: `https://tf-registry.containerk8s.services.ngcloud.ru`
- S3: `s3.msk-1.ngcloud.ru` (HTTPS)
- Бакет: `nubes-terraform-registry`
- S3-префикс: `tf-registry.containerk8s.services.ngcloud.ru`
### Креды (аккаунт `1112_terraform`)
| Subuser | Назначение | Права |
|---|---|---|
| `1112_terraform:reader` | Реестр (только читает) | read + list |
| `1112_terraform:super` | Заливка провайдеров | write |
```
Reader: 6DDP5PKQEE37WLSWXP07 / W0vV4YNeHIqMa2sRCXsR61ZuQfAJIZzVj2VgnBrT
Super: L9F0R8CYYKYIUW180203 / yJlgxoKsRBwCtJQh57Z0nwo5EBDYmyBNDDhG6NKw
```
## Структура S3
```
nubes-terraform-registry/
└── tf-registry.containerk8s.services.ngcloud.ru/
└── {namespace}/
└── {provider}/
└── {version}/
├── terraform-provider-{name}_{ver}_linux_amd64.zip
├── terraform-provider-{name}_{ver}_windows_amd64.zip
├── terraform-provider-{name}_{ver}_darwin_amd64.zip
├── terraform-provider-{name}_{ver}_SHA256SUMS
└── terraform-provider-{name}_{ver}_SHA256SUMS.sig
```
## Заливка (build-provider.sh)
### Требования
- `~/tf_registry/` — склонирован этот репозиторий
- `~/tf_provider/` — склонирован репозиторий провайдера
- `~/tf_provider/secrets/private_key.asc` — приватный GPG-ключ для подписи
- `go` установлен
- `mc` (MinIO Client) установлен
### Команды
```bash
# 1. Настроить mc (один раз, креды SUPER)
mc alias set prod-s3 https://s3.msk-1.ngcloud.ru L9F0R8CYYKYIUW180203 yJlgxoKsRBwCtJQh57Z0nwo5EBDYmyBNDDhG6NKw
# 2. Создать бакет (если ещё нет)
mc mb prod-s3/nubes-terraform-registry
# 3. Переменные для сборки
export ROOT_DIR=~/tf_provider
export PROVIDER_DIR=~/tf_provider/provider
export REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru
export NAMESPACE=nubes-test
export PROVIDER_NAME=nubes
export S3_ENDPOINT=https://s3.msk-1.ngcloud.ru
export S3_ACCESS_KEY=L9F0R8CYYKYIUW180203
export S3_SECRET_KEY=yJlgxoKsRBwCtJQh57Z0nwo5EBDYmyBNDDhG6NKw
export S3_BUCKET=nubes-terraform-registry
# 4. Запуск
cd ~/tf_provider
bash ~/tf_registry/server/build-provider.sh 5.1.17
```
## Проверка после заливки
```bash
curl https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-test/nubes/versions
bash ~/tf_registry/tests/smoke.sh https://tf-registry.containerk8s.services.ngcloud.ru
```
## Namespace'ы
| Namespace | Назначение |
|---|---|
| `nubes-test` | Тестовый стенд |
| `nubes-dev` | Dev-стенд |
| `nubes` | Прод |
Terraform-конфиг пользователя:
```hcl
terraform {
required_providers {
nubes = {
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
version = "5.1.17"
}
}
}
```
+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 с утечкой секретов. Остальное — желательно.
-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` |
+86
View File
@@ -0,0 +1,86 @@
# Terraform Provider Registry
Реестр Terraform-провайдеров по протоколу HashiCorp.
Деплой: Docker-контейнер в Nubes Cloud (`containerk8s`).
**URL:** `https://registry.containerk8s.dev.nubes.ru`
## Структура
```
├── server/ # Go-код HTTP-сервера
│ ├── main.go # Роуты, S3, структуры
│ ├── gpg_key.go # GPG-ключи (2 шт.)
│ ├── docs.go # Статика документации из S3
│ ├── build-provider.sh # Сборка и заливка провайдера
│ ├── go.mod / go.sum
│ └── gpg/
├── tests/
│ └── smoke.sh # 9 интеграционных тестов
├── secrets/ # Креды (в .gitignore)
├── HISTORY/ # Документация, планы, ревью
├── Dockerfile # Multi-stage сборка (ветка go-container)
└── .gitignore
```
## Эндпоинты
| Метод | URL | Ответ |
|---|---|---|
| GET | `/` | HTML с версией |
| GET | `/.well-known/terraform.json` | `{"providers.v1":"/v1/providers/"}` |
| GET | `/v1/providers/{ns}/{name}/versions` | JSON: список версий и платформ |
| GET | `/v1/providers/{ns}/{name}/{ver}/download/{os}/{arch}` | JSON: download_url, shasum, GPG |
| GET | `/v1/proxy?key=...` | Стриминг файла из S3 |
| GET | `/docs/{ns}/{name}/{ver}/...` | Статика документации из S3 |
| GET | `/healthz` | `ok` 200 |
| GET | `/readyz` | `ok` 200 (проверяет S3) |
## Сборка и деплой
```bash
# Сборка образа (на ВМ)
ssh naeel@5.172.178.213
cd ~/tf_registry && git checkout go-container
docker build -t gitea.services.ngcloud.ru/nail/tf_registry:latest .
docker push gitea.services.ngcloud.ru/nail/tf_registry:latest
# Редеплой — через UI modify или пересоздать инстанс
```
## Заливка провайдера
Подробно: `HISTORY/HOWTO-UPLOAD.md`
Кратко:
```bash
export REGISTRY_HOSTNAME=registry.containerk8s.dev.nubes.ru
export S3_ENDPOINT=http://ceph.tst.nubes.ru
export S3_ACCESS_KEY=... # super-креды
export S3_SECRET_KEY=...
bash server/build-provider.sh 5.1.17
```
## Тесты
```bash
bash tests/smoke.sh
```
## Конфигурация
| Где | Что |
|---|---|
| `jsonEnv` (UI) | Только `S3_ACCESS_KEY`, `S3_SECRET_KEY` (reader) |
| `Dockerfile ENV` | `S3_ENDPOINT`, `S3_BUCKET`, `S3_PREFIX`, `REGISTRY_HOSTNAME`, `PORT`, `S3_USE_SSL` |
Подробно: `HISTORY/env-config.md`
## Ветки
| Ветка | Назначение |
|---|---|
| `master` | Основная, чистый код |
| `go-container` | Сборка: Go + Dockerfile |
| `with-flask-legacy` | Архив Flask-версии |
| `golang` | Архив исходного Go (до правок) |
-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 или переменные окружения.
-13
View File
@@ -1,13 +0,0 @@
apiVersion: v2
name: terraform-registry
description: Helm chart for Terraform Provider Registry (registry-server + operator)
type: application
version: 0.1.0
appVersion: "1.0.0"
keywords:
- terraform
- registry
- nubes
home: https://terra.k8c.ru
maintainers:
- name: Naeel
-42
View File
@@ -1,42 +0,0 @@
{{/*
Expand the name of the chart.
*/}}
{{- define "terraform-registry.name" -}}
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Create a default fully qualified app name.
*/}}
{{- define "terraform-registry.fullname" -}}
{{- if .Values.fullnameOverride }}
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
{{- else }}
{{- $name := default .Chart.Name .Values.nameOverride }}
{{- printf "%s" $name | trunc 63 | trimSuffix "-" }}
{{- end }}
{{- end }}
{{/*
Create chart label.
*/}}
{{- define "terraform-registry.chart" -}}
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
{{- end }}
{{/*
Common labels.
*/}}
{{- define "terraform-registry.labels" -}}
helm.sh/chart: {{ include "terraform-registry.chart" . }}
{{ include "terraform-registry.selectorLabels" . }}
app.kubernetes.io/managed-by: {{ .Release.Service }}
{{- end }}
{{/*
Selector labels.
*/}}
{{- define "terraform-registry.selectorLabels" -}}
app.kubernetes.io/name: {{ include "terraform-registry.name" . }}
app.kubernetes.io/instance: {{ .Release.Name }}
{{- end }}
@@ -1,61 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: builder-script
namespace: {{ .Values.global.namespace }}
labels:
{{- include "terraform-registry.labels" . | nindent 4 }}
data:
build.sh: |
#!/bin/sh
set -e
echo ">>> Starting Build for $PROVIDER_NAME $VERSION"
# 1. Setup Environment
# Inputs: GIT_REPO, GIT_REF, PROVIDER_NAME, VERSION
WORK_DIR="/workspace"
ARTIFACTS_DIR="/artifacts"
mkdir -p $WORK_DIR $ARTIFACTS_DIR
# 2. Clone Repository
echo ">>> Cloning $GIT_REPO ($GIT_REF)..."
git clone $GIT_REPO $WORK_DIR/src
cd $WORK_DIR/src
git checkout $GIT_REF
# 3. Build for Platforms
echo ">>> Building binaries..."
# Linux amd64
echo "--> linux/amd64"
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o terraform-provider-${PROVIDER_NAME}_v${VERSION}
zip ${ARTIFACTS_DIR}/terraform-provider-${PROVIDER_NAME}_${VERSION}_linux_amd64.zip terraform-provider-${PROVIDER_NAME}_v${VERSION}
rm terraform-provider-${PROVIDER_NAME}_v${VERSION}
# Windows amd64
echo "--> windows/amd64"
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
zip ${ARTIFACTS_DIR}/terraform-provider-${PROVIDER_NAME}_${VERSION}_windows_amd64.zip terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
rm terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
# 4. Checksums
cd $ARTIFACTS_DIR
sha256sum *.zip > terraform-provider-${PROVIDER_NAME}_${VERSION}_SHA256SUMS
# 5. GPG Sign
touch terraform-provider-${PROVIDER_NAME}_${VERSION}_SHA256SUMS.sig
# 6. Upload to S3
if [ -z "$REGISTRY_HOSTNAME" ]; then
REGISTRY_HOSTNAME="{{ .Values.global.registryHostname }}"
fi
TARGET_PATH="terraform-providers/$REGISTRY_HOSTNAME/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}"
echo ">>> Uploading to S3 path: $TARGET_PATH"
mc alias set nubes_s3 https://$S3_ENDPOINT $S3_ACCESS_KEY $S3_SECRET_KEY
mc cp --recursive $ARTIFACTS_DIR/ nubes_s3/$TARGET_PATH/
echo ">>> Build Complete!"
-79
View File
@@ -1,79 +0,0 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: providerreleases.terra.core.nubes.ru
annotations:
app.kubernetes.io/managed-by: Helm
spec:
group: terra.core.nubes.ru
versions:
- name: v1alpha1
served: true
storage: true
subresources:
status: {}
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- providerName
- version
- gitRepo
- gitRef
properties:
providerName:
type: string
description: "Name of the provider (e.g. 'mycloud')"
namespace:
type: string
default: "mycloud"
description: "Terraform registry namespace (e.g. 'nubes')"
version:
type: string
description: "Semantic version without v (e.g. '1.0.1')"
gitRepo:
type: string
description: "Git repository URL to clone"
gitRef:
type: string
description: "Git tag, commit SHA or branch"
platforms:
type: array
items:
type: string
default: ["linux_amd64", "windows_amd64"]
description: "Target platforms for build"
status:
type: object
properties:
phase:
type: string
description: "Current phase: Pending, Building, Publishing, Ready, Failed"
downloadUrl:
type: string
conditions:
type: array
items:
type: object
properties:
type:
type: string
status:
type: string
lastTransitionTime:
type: string
format: date-time
reason:
type: string
message:
type: string
scope: Namespaced
names:
plural: providerreleases
singular: providerrelease
kind: TerraformProviderRelease
shortNames:
- tpr
@@ -1,52 +0,0 @@
{{- if .Values.docs.enabled }}
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry-server-docs
namespace: {{ .Values.global.namespace }}
labels:
app: registry-server-docs
{{- include "terraform-registry.labels" . | nindent 4 }}
spec:
replicas: 1
selector:
matchLabels:
app: registry-server-docs
template:
metadata:
labels:
app: registry-server-docs
spec:
containers:
- name: registry-server
image: "{{ .Values.docs.image.repository }}:{{ .Values.docs.image.tag }}"
imagePullPolicy: {{ .Values.docs.image.pullPolicy }}
env:
- name: REGISTRY_HOSTNAME
value: {{ .Values.global.registryHostname | quote }}
- name: S3_ENDPOINT
value: {{ .Values.s3.endpoint | quote }}
- name: S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.s3.existingSecret }}
key: {{ .Values.s3.accessKeyField }}
- name: S3_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.s3.existingSecret }}
key: {{ .Values.s3.secretKeyField }}
- name: S3_BUCKET
value: {{ .Values.s3.bucket | quote }}
- name: S3_USE_SSL
value: {{ .Values.s3.useSSL | quote }}
ports:
- containerPort: 8080
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
{{- end }}
-37
View File
@@ -1,37 +0,0 @@
{{- if .Values.ingress.enabled }}
# WARNING: Do NOT apply to existing clusters without confirming TLS issuer.
# LetsEncrypt rate limits apply. For testing, use a self-signed issuer.
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: registry-ingress
namespace: {{ .Values.global.namespace }}
annotations:
{{- if .Values.ingress.tls.enabled }}
cert-manager.io/cluster-issuer: {{ .Values.ingress.tls.issuer | quote }}
{{- end }}
{{- range $key, $value := .Values.ingress.annotations }}
{{ $key }}: {{ $value | quote }}
{{- end }}
labels:
{{- include "terraform-registry.labels" . | nindent 4 }}
spec:
ingressClassName: {{ .Values.ingress.className | quote }}
{{- if .Values.ingress.tls.enabled }}
tls:
- hosts:
- {{ .Values.ingress.host | quote }}
secretName: {{ .Values.ingress.tls.secretName }}
{{- end }}
rules:
- host: {{ .Values.ingress.host | quote }}
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: registry-server
port:
number: {{ .Values.registry.service.port }}
{{- end }}
-7
View File
@@ -1,7 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: {{ .Values.global.namespace }}
labels:
name: {{ .Values.global.namespace }}
{{- include "terraform-registry.labels" . | nindent 4 }}
-31
View File
@@ -1,31 +0,0 @@
{{- if .Values.networkPolicy.enabled }}
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: {{ include "terraform-registry.fullname" . }}-netpol
namespace: {{ .Values.global.namespace }}
labels:
{{- include "terraform-registry.labels" . | nindent 4 }}
spec:
podSelector: {}
policyTypes:
- Ingress
- Egress
ingress:
- from:
- namespaceSelector:
matchLabels:
kubernetes.io/metadata.name: ingress-nginx
ports:
- port: {{ .Values.registry.service.targetPort }}
protocol: TCP
egress:
- to: []
ports:
- port: 443 # S3, Vault
protocol: TCP
- port: 53 # DNS
protocol: UDP
- port: 53
protocol: TCP
{{- end }}
-33
View File
@@ -1,33 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: terraform-operator
namespace: {{ .Values.global.namespace }}
labels:
app: terraform-operator
{{- include "terraform-registry.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.operator.replicas }}
selector:
matchLabels:
app: terraform-operator
template:
metadata:
labels:
app: terraform-operator
spec:
serviceAccountName: terraform-operator
containers:
- name: operator
image: "{{ .Values.operator.image.repository }}:{{ .Values.operator.image.tag }}"
imagePullPolicy: IfNotPresent
env:
- name: REGISTRY_HOSTNAME
value: {{ .Values.global.registryHostname | quote }}
resources:
requests:
cpu: {{ .Values.operator.resources.requests.cpu }}
memory: {{ .Values.operator.resources.requests.memory }}
limits:
cpu: {{ .Values.operator.resources.limits.cpu }}
memory: {{ .Values.operator.resources.limits.memory }}
-14
View File
@@ -1,14 +0,0 @@
{{- if .Values.pdb.enabled }}
apiVersion: policy/v1
kind: PodDisruptionBudget
metadata:
name: {{ include "terraform-registry.fullname" . }}-registry
namespace: {{ .Values.global.namespace }}
labels:
{{- include "terraform-registry.labels" . | nindent 4 }}
spec:
minAvailable: {{ .Values.pdb.minAvailable }}
selector:
matchLabels:
app: registry-server
{{- end }}
-41
View File
@@ -1,41 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: terraform-operator
namespace: {{ .Values.global.namespace }}
labels:
{{- include "terraform-registry.labels" . | nindent 4 }}
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: terraform-operator
namespace: {{ .Values.global.namespace }}
labels:
{{- include "terraform-registry.labels" . | nindent 4 }}
rules:
- apiGroups: ["terra.core.nubes.ru"]
resources: ["providerreleases", "providerreleases/status"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: terraform-operator
namespace: {{ .Values.global.namespace }}
labels:
{{- include "terraform-registry.labels" . | nindent 4 }}
subjects:
- kind: ServiceAccount
name: terraform-operator
namespace: {{ .Values.global.namespace }}
roleRef:
kind: Role
name: terraform-operator
apiGroup: rbac.authorization.k8s.io
@@ -1,64 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry-server
namespace: {{ .Values.global.namespace }}
labels:
app: registry-server
{{- include "terraform-registry.labels" . | nindent 4 }}
spec:
replicas: {{ .Values.registry.replicas }}
selector:
matchLabels:
app: registry-server
template:
metadata:
labels:
app: registry-server
spec:
containers:
- name: registry-server
image: "{{ .Values.registry.image.repository }}:{{ .Values.registry.image.tag }}"
imagePullPolicy: {{ .Values.registry.image.pullPolicy }}
ports:
- containerPort: {{ .Values.registry.service.targetPort }}
env:
- name: REGISTRY_HOSTNAME
value: {{ .Values.global.registryHostname | quote }}
- name: S3_ENDPOINT
value: {{ .Values.s3.endpoint | quote }}
- name: S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.s3.existingSecret }}
key: {{ .Values.s3.accessKeyField }}
- name: S3_SECRET_KEY
valueFrom:
secretKeyRef:
name: {{ .Values.s3.existingSecret }}
key: {{ .Values.s3.secretKeyField }}
- name: S3_BUCKET
value: {{ .Values.s3.bucket | quote }}
- name: S3_USE_SSL
value: {{ .Values.s3.useSSL | quote }}
resources:
requests:
cpu: {{ .Values.registry.resources.requests.cpu }}
memory: {{ .Values.registry.resources.requests.memory }}
limits:
cpu: {{ .Values.registry.resources.limits.cpu }}
memory: {{ .Values.registry.resources.limits.memory }}
{{- if .Values.registry.healthcheck.enabled }}
livenessProbe:
httpGet:
path: {{ .Values.registry.healthcheck.path }}
port: {{ .Values.registry.healthcheck.port }}
initialDelaySeconds: 15
periodSeconds: 20
readinessProbe:
httpGet:
path: {{ .Values.registry.healthcheck.path }}
port: {{ .Values.registry.healthcheck.port }}
initialDelaySeconds: 5
periodSeconds: 10
{{- end }}
@@ -1,14 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: registry-server
namespace: {{ .Values.global.namespace }}
labels:
app: registry-server
{{- include "terraform-registry.labels" . | nindent 4 }}
spec:
selector:
app: registry-server
ports:
- port: {{ .Values.registry.service.port }}
targetPort: {{ .Values.registry.service.targetPort }}
-6
View File
@@ -1,6 +0,0 @@
global:
registryHostname: "terra.k8c.ru"
registry:
replicas: 1
pdb:
enabled: false
-14
View File
@@ -1,14 +0,0 @@
global:
registryHostname: "registry.nubes.ru" # Целевой домен
registry:
image:
repository: "pearlharbor.registryk8s.services.ngcloud.ru/terraform/registry-server"
replicas: 2
operator:
image:
repository: "pearlharbor.registryk8s.services.ngcloud.ru/terraform/registry-operator"
pdb:
enabled: true
minAvailable: 1
networkPolicy:
enabled: true
-71
View File
@@ -1,71 +0,0 @@
global:
registryHostname: "terra.k8c.ru" # Переопределяется при миграции
namespace: "terraform-registry"
registry:
image:
repository: "naeel/terraform-registry-server" # → Harbor при миграции
tag: "latest"
pullPolicy: IfNotPresent
replicas: 1 # → 2 в prod
resources:
requests:
cpu: 100m
memory: 128Mi
limits:
cpu: 500m
memory: 256Mi
service:
port: 80
targetPort: 8080
healthcheck:
enabled: true
path: /healthz
port: 8080
operator:
image:
repository: "naeel/terraform-registry-operator"
tag: "latest"
replicas: 1
resources:
requests:
cpu: 50m
memory: 64Mi
limits:
cpu: 500m
memory: 128Mi
docs:
enabled: false
image:
repository: "naeel/terraform-registry-server"
tag: "docs-dev-fixed"
pullPolicy: IfNotPresent
ingress:
enabled: true
className: "nginx"
host: "terra.k8c.ru" # Переопределяется
tls:
enabled: true
issuer: "letsencrypt-prod" # НЕ ТРОГАТЬ LetsEncrypt issuer!
secretName: "registry-tls"
annotations:
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
s3:
endpoint: "s3.msk-1.ngcloud.ru"
bucket: "terraform-registry"
useSSL: true
# credentials через existingSecret
existingSecret: "s3-credentials"
accessKeyField: "access-key"
secretKeyField: "secret-key"
pdb:
enabled: false # → true в prod
minAvailable: 1
networkPolicy:
enabled: false # → true при миграции
-30
View File
@@ -1,30 +0,0 @@
# k8s 🔧
Кратко: Kubernetes‑манифесты для namespace `terra` — развёртывание `registry-server`, `terraform-operator`, Ingress, TLS и связанные ресурсы (S3, CRD, builder ConfigMap).
## Важные файлы
- `00-namespace.yaml` — namespace `terra`
- `00-rbac.yaml` — RBAC для оператора
- `02-crd.yaml` — CRD для операторов (ProviderRelease)
- `03-build-script.yaml` — ConfigMap для сборщика Job'ов
- `04-operator-deployment.yaml` — Deployment для оператора
- `05-registry-server.yaml`, `06-registry-server-docs.yaml` — Deployment, Service и Ingress для `registry-server`
## Как перенести на новый домен (коротко) ✅
1. **Ingress**: в манифесте (обычно `05-*` или `registry-ingress`) заменить `host: terra.k8c.ru` на новый домен и убедиться в корректной аннотации `cert-manager.io/cluster-issuer` (или задать другой `secretName`).
2. **Certificate**: cert-manager автоматически выпустит/обновит TLS; проверьте `kubectl -n terra get certificate registry-tls -o yaml` и дождитесь `status.conditions[*].type == Ready`.
3. **Deployment env**: заменить `REGISTRY_HOSTNAME` в `04-operator-deployment.yaml` и в `registry-server` на новый домен.
4. **DNS**: добавить A/CNAME запись для нового домена, указывающую на IP балансировщика (`kubectl -n terra get ingress registry-ingress -o jsonpath='{.status.loadBalancer.ingress[0].ip}'`).
5. **Secrets & storage**: используйте `s3-credentials` (S3_ENDPOINT/S3_ACCESS_KEY/S3_SECRET_KEY) и убедитесь в доступности S3.
6. **Apply & Verify**:
- `kubectl apply -f manifests/`
- `kubectl -n terra describe ingress registry-ingress`
- `kubectl -n terra get certificate registry-tls`
- `curl -vk https://<domain>/docs/<namespace>/<name>/<version>/`
## Проверки (read-only)
- `kubectl -n terra get ingress,svc,deploy,po,secret`
- `kubectl -n terra get certificate registry-tls -o yaml`
- `kubectl -n terra logs <registry-pod>`
> Примечание: это инструкция для операции вручную/CI; я ничего не меняю в кластере автоматически.
-19
View File
@@ -1,19 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
replicas: 1
selector:
matchLabels:
app: my-app
template:
metadata:
labels:
app: my-app
spec:
containers:
- name: my-container
image: my-image:latest
ports:
- containerPort: 80
-16
View File
@@ -1,16 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
spec:
rules:
- host: example.local
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80
-6
View File
@@ -1,6 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- deployment.yaml
- service.yaml
- ingress.yaml
-11
View File
@@ -1,11 +0,0 @@
apiVersion: v1
kind: Service
metadata:
name: my-service
spec:
selector:
app: my-app
ports:
- protocol: TCP
port: 80
targetPort: 80
@@ -1,6 +0,0 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: selfsigned-dev
spec:
selfSigned: {}
@@ -1,14 +0,0 @@
apiVersion: cert-manager.io/v1
kind: ClusterIssuer
metadata:
name: letsencrypt-staging-dev
spec:
acme:
server: https://acme-staging-v02.api.letsencrypt.org/directory
email: devops@example.com
privateKeySecretRef:
name: letsencrypt-staging-dev
solvers:
- http01:
ingress:
class: nginx
-10
View File
@@ -1,10 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-deployment
spec:
template:
spec:
containers:
- name: my-container
image: nginx:stable
-23
View File
@@ -1,23 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: my-ingress
annotations:
cert-manager.io/cluster-issuer: selfsigned-dev
kubernetes.io/ingress.class: nginx
spec:
tls:
- hosts:
- terr.kube5s.ru
secretName: terr-kube5s-ru-tls
rules:
- host: terr.kube5s.ru
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: my-service
port:
number: 80
-8
View File
@@ -1,8 +0,0 @@
apiVersion: kustomize.config.k8s.io/v1beta1
kind: Kustomization
resources:
- ../../base
namespace: terra-dev
patchesStrategicMerge:
- ingress-patch.yaml
- image-patch.yaml
-42
View File
@@ -1,42 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry-server
namespace: terra
labels:
app: registry-server
spec:
replicas: 1
selector:
matchLabels:
app: registry-server
template:
metadata:
labels:
app: registry-server
spec:
containers:
- name: registry-server
image: naeel/terraform-registry-server:docs-dev
imagePullPolicy: Always
ports:
- containerPort: 8080
env:
- name: REGISTRY_HOSTNAME
value: "terra.k8c.ru"
- name: S3_ENDPOINT
value: "s3.msk-1.ngcloud.ru"
- name: S3_ACCESS_KEY
valueFrom:
secretKeyRef:
name: s3-credentials
key: access-key
- name: S3_SECRET_KEY
valueFrom:
secretKeyRef:
name: s3-credentials
key: secret-key
- name: S3_BUCKET
value: "terraform-registry"
- name: S3_USE_SSL
value: "true"
-26
View File
@@ -1,26 +0,0 @@
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: registry-ingress
namespace: terra
annotations:
cert-manager.io/cluster-issuer: "letsencrypt-prod"
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
nginx.ingress.kubernetes.io/rewrite-target: "/$2"
spec:
ingressClassName: nginx
tls:
- hosts:
- terra.k8c.ru
secretName: registry-tls
rules:
- host: terra.k8c.ru
http:
paths:
- path: "/()(.*)"
pathType: ImplementationSpecific
backend:
service:
name: registry-server
port:
number: 80
-47
View File
@@ -1,47 +0,0 @@
# Exclude files not needed in Docker build context for operator
.git
.gitignore
.dockerignore
# Docs / markdown
*.md
docs/
# Other services / infra
server/
scripts/
charts/
k8s/
# Secrets & keys
secrets/
*.pem
*.key
*.asc
*.gpg
*.pgp
# Artefacts / temp
artifacts/
*.har
HAR/
registry-server-build/
universal_rebuild/
tools/
devops/
# IDE / OS
.idea/
.vscode/
*.swp
*.swo
*~
.DS_Store
# Go test/coverage
*.test
*.out
coverage.out
# Logs
*.log
-23
View File
@@ -1,23 +0,0 @@
IMAGE_REPO ?= naeel
TAG ?= latest
PLATFORM ?= linux/amd64
.PHONY: build-all push-all build-operator push-operator build-registry push-registry tidy
tidy:
go mod tidy
build-all: build-operator build-registry
push-all: push-operator push-registry
build-operator:
docker build --platform $(PLATFORM) -t $(IMAGE_REPO)/terraform-registry-operator:$(TAG) -f build/Dockerfile.operator .
push-operator:
docker push $(IMAGE_REPO)/terraform-registry-operator:$(TAG)
build-registry:
docker build --platform $(PLATFORM) -t $(IMAGE_REPO)/terraform-registry-server:$(TAG) -f build/Dockerfile.registry .
push-registry:
docker push $(IMAGE_REPO)/terraform-registry-server:$(TAG)
-44
View File
@@ -1,44 +0,0 @@
# Terra Operator Manifests
## Minimal Setup for Namespace `terra`
### 1. Structure
- **Namespace**: `terra`
- **Storage**: внешний S3 (Nubes Cloud).
- **CRD**: `TerraformProviderRelease` definitions.
### 2. How to apply
```bash
kubectl apply -f operator/manifests/00-namespace.yaml
kubectl apply -f operator/manifests/02-crd.yaml
kubectl apply -f operator/manifests/04-operator-deployment.yaml
kubectl apply -f operator/manifests/05-registry-server.yaml
```
### 3. Usage
Create a release request:
```yaml
apiVersion: terra.core.nubes.ru/v1alpha1
kind: TerraformProviderRelease
metadata:
name: mycloud-v0-1-0
namespace: terra
spec:
providerName: "mycloud"
version: "0.1.0"
gitRepo: "https://github.com/Start-Ops/terraform-provider-mycloud.git"
gitRef: "v0.1.0"
```
---
## Простыми словами: что это и зачем
Это набор манифестов и небольшой сервис, который автоматизирует сборку Terraform‑провайдеров.
Идея простая: я описываю в CRD, какой провайдер и какую версию собрать, а оператор сам запускает Job,
собирает бинарники и складывает их в хранилище. Дальше registry‑server отдает эти артефакты Terraform‑клиентам.
Зачем это нужно:
- чтобы не собирать провайдеры руками;
- чтобы хранить артефакты в одном месте;
- чтобы Terraform мог скачивать провайдеры по обычному источнику.
-18
View File
@@ -1,18 +0,0 @@
# Build Stage
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY cmd ./cmd
# Build statically
RUN CGO_ENABLED=0 GOOS=linux go build -o /operator cmd/main.go
# Run Stage
FROM alpine:3.19
WORKDIR /app
COPY --from=builder /operator /app/operator
USER 1000:1000
CMD ["/app/operator"]
-20
View File
@@ -1,20 +0,0 @@
# Build Stage
FROM golang:1.24-alpine AS builder
WORKDIR /app
COPY go.mod go.sum ./
COPY registrykeys ./registrykeys
RUN go mod download
COPY cmd/registry ./cmd/registry
RUN CGO_ENABLED=0 GOOS=linux go build -o /registry-server ./cmd/registry
# Run Stage
FROM alpine:3.19
WORKDIR /app
COPY --from=builder /registry-server /app/registry-server
RUN apk add --no-cache ca-certificates
USER 1000:1000
EXPOSE 8080
CMD ["/app/registry-server"]
-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)
}
-8
View File
@@ -1,8 +0,0 @@
package main
import registrykeys "terraform-registry-keys"
var gpgPublicKey = GPGPublicKey{
KeyID: registrykeys.KeyID,
ASCIIArmor: registrykeys.ASCIIArmor,
}
-281
View File
@@ -1,281 +0,0 @@
package main
import (
"bufio"
"context"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"net/url"
"os"
"strings"
s3 "github.com/minio/minio-go/v7"
"github.com/minio/minio-go/v7/pkg/credentials"
)
var (
s3Client *s3.Client
bucketName = "terraform-registry" // Default
hostname = os.Getenv("REGISTRY_HOSTNAME")
)
// 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() {
// 1. Init S3 Connection
if hostname == "" {
hostname = "localhost:8080"
}
endpoint := os.Getenv("S3_ENDPOINT")
accessKeyID := os.Getenv("S3_ACCESS_KEY")
secretAccessKey := os.Getenv("S3_SECRET_KEY")
if os.Getenv("S3_BUCKET") != "" {
bucketName = os.Getenv("S3_BUCKET")
}
var err error
s3Client, err = s3.New(endpoint, &s3.Options{
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
Secure: true, // Force secure for cloud S3
})
if err != nil {
log.Fatalln(err)
}
// 2. HTTP Handlers
http.HandleFunc("/.well-known/terraform.json", discoveryHandler)
http.HandleFunc("/v1/providers/", router)
http.HandleFunc("/v1/proxy", proxyHandler)
http.HandleFunc("/docs/", docsHandler)
http.Handle("/", http.HandlerFunc(rootHandler))
log.Printf("Starting Registry Service on :8080 (Bucket: %s, Endpoint: %s)\n", bucketName, endpoint)
log.Fatal(http.ListenAndServe(":8080", nil))
}
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.Fprint(w, `
<!DOCTYPE html>
<html>
<head><title>Terra Registry</title></head>
<body>
<h1>Terra Registry & Documentation Server</h1>
<p>Status: <span style="color: green">ONLINE</span></p>
<hr>
<p>Powered by Nubes Cloud S3 Storage</p>
</body>
</html>
`)
}
func proxyHandler(w http.ResponseWriter, r *http.Request) {
bucket := r.URL.Query().Get("bucket")
key := r.URL.Query().Get("key")
if bucket == "" || key == "" {
http.Error(w, "Missing bucket or key params", http.StatusBadRequest)
return
}
obj, err := s3Client.GetObject(context.Background(), bucket, key, s3.GetObjectOptions{})
if err != nil {
log.Printf("Error getting object %s/%s: %v", bucket, 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", bucket, 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/", hostname, 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, "_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"})
}
if strings.Contains(fileName, "_darwin_amd64.zip") {
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "amd64"})
}
if strings.Contains(fileName, "_darwin_arm64.zip") {
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "arm64"})
}
}
for _, v := range seenVersions {
versions = append(versions, *v)
}
resp := VersionList{
ID: fmt.Sprintf("%s/%s/%s", hostname, 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", hostname, 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))
gpgKey := gpgPublicKey
resp := DownloadResponse{
Protocols: []string{"5.0"},
OS: osType,
Arch: arch,
Filename: filename,
DownloadURL: downloadLink,
ShasumsURL: shasumsLink,
ShasumsSignatureURL: sigLink,
Shasum: shasumValue,
SigningKeys: SigningKeys{
GPGPublicKeys: []GPGPublicKey{gpgKey},
},
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(resp)
}
-11
View File
@@ -1,11 +0,0 @@
apiVersion: terra.core.nubes.ru/v1alpha1
kind: TerraformProviderRelease
metadata:
name: scaffolding-v0-0-1
namespace: terra
spec:
providerName: "scaffolding"
namespace: "hashicorp"
version: "0.0.1"
gitRepo: "https://github.com/hashicorp/terraform-provider-scaffolding.git"
gitRef: "main"
-37
View File
@@ -1,37 +0,0 @@
module terraform-provider-operator
go 1.24.0
toolchain go1.24.12
require (
github.com/minio/minio-go/v7 v7.0.98
terraform-registry-keys v0.0.0
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/go-ini/ini v1.67.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.2 // indirect
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
github.com/klauspost/crc32 v1.3.0 // indirect
github.com/kr/pretty v0.3.1 // indirect
github.com/minio/crc64nvme v1.1.1 // indirect
github.com/minio/md5-simd v1.1.2 // indirect
github.com/philhofer/fwd v1.2.0 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rogpeppe/go-internal v1.10.0 // indirect
github.com/rs/xid v1.6.0 // indirect
github.com/tinylib/msgp v1.6.1 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/net v0.48.0 // indirect
golang.org/x/sys v0.39.0 // indirect
golang.org/x/text v0.32.0 // indirect
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
replace terraform-registry-keys => ./registrykeys
-58
View File
@@ -1,58 +0,0 @@
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
-6
View File
@@ -1,6 +0,0 @@
apiVersion: v1
kind: Namespace
metadata:
name: terra
labels:
name: terra
-35
View File
@@ -1,35 +0,0 @@
apiVersion: v1
kind: ServiceAccount
metadata:
name: terraform-operator
namespace: terra
---
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: terraform-operator
namespace: terra
rules:
- apiGroups: ["terra.core.nubes.ru"]
resources: ["providerreleases", "providerreleases/status"]
verbs: ["get", "list", "watch", "update", "patch"]
- apiGroups: ["batch"]
resources: ["jobs"]
verbs: ["create", "get", "list", "watch", "delete"]
- apiGroups: [""]
resources: ["pods", "pods/log"]
verbs: ["get", "list", "watch"]
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: terraform-operator
namespace: terra
subjects:
- kind: ServiceAccount
name: terraform-operator
namespace: terra
roleRef:
kind: Role
name: terraform-operator
apiGroup: rbac.authorization.k8s.io
-77
View File
@@ -1,77 +0,0 @@
apiVersion: apiextensions.k8s.io/v1
kind: CustomResourceDefinition
metadata:
name: providerreleases.terra.core.nubes.ru
spec:
group: terra.core.nubes.ru
versions:
- name: v1alpha1
served: true
storage: true
subresources:
status: {}
schema:
openAPIV3Schema:
type: object
properties:
spec:
type: object
required:
- providerName
- version
- gitRepo
- gitRef
properties:
providerName:
type: string
description: "Name of the provider (e.g. 'mycloud')"
namespace:
type: string
default: "mycloud"
description: "Terraform registry namespace (e.g. 'nubes')"
version:
type: string
description: "Semantic version without v (e.g. '1.0.1')"
gitRepo:
type: string
description: "Git repository URL to clone"
gitRef:
type: string
description: "Git tag, commit SHA or branch"
platforms:
type: array
items:
type: string
default: ["linux_amd64", "windows_amd64"]
description: "Target platforms for build"
status:
type: object
properties:
phase:
type: string
description: "Current phase: Pending, Building, Publishing, Ready, Failed"
downloadUrl:
type: string
conditions:
type: array
items:
type: object
properties:
type:
type: string
status:
type: string
lastTransitionTime:
type: string
format: date-time
reason:
type: string
message:
type: string
scope: Namespaced
names:
plural: providerreleases
singular: providerrelease
kind: TerraformProviderRelease
shortNames:
- tpr
-75
View File
@@ -1,75 +0,0 @@
apiVersion: v1
kind: ConfigMap
metadata:
name: builder-script
namespace: terra
data:
build.sh: |
#!/bin/sh
set -e
echo ">>> Starting Build for $PROVIDER_NAME $VERSION"
# 1. Setup Environment
# Inputs: GIT_REPO, GIT_REF, PROVIDER_NAME, VERSION
WORK_DIR="/workspace"
ARTIFACTS_DIR="/artifacts"
mkdir -p $WORK_DIR $ARTIFACTS_DIR
# 2. Clone Repository
echo ">>> Cloning $GIT_REPO ($GIT_REF)..."
git clone $GIT_REPO $WORK_DIR/src
cd $WORK_DIR/src
git checkout $GIT_REF
# 3. Build for Platforms
# Hardcoded platforms for MVP: linux/amd64, windows/amd64
# Naming convention: terraform-provider-NAME_vVERSION_OS_ARCH.zip
echo ">>> Building binaries..."
# Linux amd64
echo "--> linux/amd64"
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o terraform-provider-${PROVIDER_NAME}_v${VERSION}
zip ${ARTIFACTS_DIR}/terraform-provider-${PROVIDER_NAME}_${VERSION}_linux_amd64.zip terraform-provider-${PROVIDER_NAME}_v${VERSION}
rm terraform-provider-${PROVIDER_NAME}_v${VERSION}
# Windows amd64
echo "--> windows/amd64"
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
zip ${ARTIFACTS_DIR}/terraform-provider-${PROVIDER_NAME}_${VERSION}_windows_amd64.zip terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
rm terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
# 4. Checksums
cd $ARTIFACTS_DIR
sha256sum *.zip > terraform-provider-${PROVIDER_NAME}_${VERSION}_SHA256SUMS
# 5. GPG Sign (Mock for MVP - empty sig or skip if client allows, but Terraform strict.
# For now we will generate a dummy signature to satisfy file existence, real signing requires GPG private key in env)
# Ideally: gpg --detach-sign --armor terraform-provider-..._SHA256SUMS
touch terraform-provider-${PROVIDER_NAME}_${VERSION}_SHA256SUMS.sig
# 6. Upload to S3
# Layout: /terraform-providers/hostname/namespace/name/version/
# We will use 'REGISTRY_HOSTNAME' env var if set, else default.
if [ -z "$REGISTRY_HOSTNAME" ]; then
REGISTRY_HOSTNAME="terra.k8c.ru"
fi
TARGET_PATH="terraform-providers/$REGISTRY_HOSTNAME/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}"
echo ">>> Uploading to S3 path: $TARGET_PATH"
# Configure mc using S3 credentials (S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY)
mc alias set nubes_s3 https://$S3_ENDPOINT $S3_ACCESS_KEY $S3_SECRET_KEY
# Upload files
mc cp --recursive $ARTIFACTS_DIR/ nubes_s3/$TARGET_PATH/
# 7. Generate shasums file for download (Terraform download protocol requires a checksums.txt usually or just the sums file)
# We are following the provider registry protocol loosely.
# The most important part for discovery is the 'index.json' or 'versions' endpoint, but we are static hosting.
# We rely on the directory structure.
echo ">>> Build Complete!"
@@ -1,27 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: terraform-operator
namespace: terra
spec:
replicas: 1
selector:
matchLabels:
app: terraform-operator
template:
metadata:
labels:
app: terraform-operator
spec:
serviceAccountName: terraform-operator
containers:
- name: operator
image: naeel/terraform-registry-operator:latest
imagePullPolicy: Always
env:
- name: REGISTRY_HOSTNAME
value: "terra.k8c.ru"
resources:
limits:
memory: "128Mi"
cpu: "500m"
@@ -1,81 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry-server
namespace: terra
labels:
app: registry-server
spec:
replicas: 1
selector:
matchLabels:
app: registry-server
template:
metadata:
labels:
app: registry-server
spec:
containers:
- name: registry-server
image: naeel/terraform-registry-server:gpg-key-20260208
imagePullPolicy: Always
env:
- name: REGISTRY_HOSTNAME
value: "terra.k8c.ru"
- name: S3_ENDPOINT
value: "s3.msk-1.ngcloud.ru"
- name: S3_ACCESS_KEY
valueFrom:
secretKeyRef:
key: access-key
name: s3-credentials
- name: S3_SECRET_KEY
valueFrom:
secretKeyRef:
key: secret-key
name: s3-credentials
- name: S3_BUCKET
value: "terraform-registry"
- name: S3_USE_SSL
value: "true"
ports:
- containerPort: 8080
---
apiVersion: v1
kind: Service
metadata:
name: registry-server
namespace: terra
spec:
selector:
app: registry-server
ports:
- port: 80
targetPort: 8080
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: registry-ingress
namespace: terra
annotations:
nginx.ingress.kubernetes.io/rewrite-target: /
cert-manager.io/cluster-issuer: letsencrypt-prod
spec:
ingressClassName: nginx
tls:
- hosts:
- terra.k8c.ru
secretName: registry-tls
rules:
- host: terra.k8c.ru
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: registry-server
port:
number: 80
@@ -1,42 +0,0 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: registry-server
namespace: terra
labels:
app: registry-server
spec:
replicas: 1
selector:
matchLabels:
app: registry-server
template:
metadata:
labels:
app: registry-server
spec:
containers:
- name: registry-server
image: naeel/terraform-registry-server:docs-dev-fixed
imagePullPolicy: Always
env:
- name: REGISTRY_HOSTNAME
value: "terra.k8c.ru"
- name: S3_ENDPOINT
value: "s3.msk-1.ngcloud.ru"
- name: S3_ACCESS_KEY
valueFrom:
secretKeyRef:
key: access-key
name: s3-credentials
- name: S3_SECRET_KEY
valueFrom:
secretKeyRef:
key: secret-key
name: s3-credentials
- name: S3_BUCKET
value: "terraform-registry"
- name: S3_USE_SSL
value: "true"
ports:
- containerPort: 8080
-5
View File
@@ -1,5 +0,0 @@
module terraform-registry-keys
go 1.24.0
toolchain go1.24.12
-59
View File
@@ -1,59 +0,0 @@
package registrykeys
const KeyID = "3EC4673EB798238A"
const ASCIIArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----
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
6cZ0NQ==
=3Ea2
-----END PGP PUBLIC KEY BLOCK-----
`
-5
View File
@@ -1,5 +0,0 @@
module terraform-registry-keys
go 1.24.0
toolchain go1.24.12
-67
View File
@@ -1,67 +0,0 @@
package registrykeys
const KeyID = "CB3A0DF161ECC416"
const ASCIIArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=q8Tw
-----END PGP PUBLIC KEY BLOCK-----`
-2
View File
@@ -1,2 +0,0 @@
flask
boto3
-19
View File
@@ -1,19 +0,0 @@
#!/usr/bin/env bash
set -euo pipefail
# Build and optionally push the registry-server image with docs handler
# Usage: ./scripts/build-and-push-registry-server.sh <image-repo> <tag>
IMAGE_REPO=${1:-naeel}
TAG=${2:-docs-dev}
cd operator
make build-registry IMAGE_REPO=${IMAGE_REPO} TAG=${TAG}
echo "Built image: ${IMAGE_REPO}/terraform-registry-server:${TAG}"
echo "To push: docker push ${IMAGE_REPO}/terraform-registry-server:${TAG}"
echo "To deploy to cluster (example):"
echo " kubectl apply -f operator/manifests/06-registry-server-docs.yaml"
echo "and edit the file to replace IMAGE_REPO/TAG with your registry and tag before applying."
+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 -253
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.0.5" const VERSION = "0.0.2"
// 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,20 @@ 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.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,199 +88,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>
<head><title>Terra Registry v`+VERSION+`</title></head>
<body>
<h1>Terra Registry & Documentation Server v`+VERSION+`</h1>
<p>Status: <span style="color: green">ONLINE</span></p>
<hr>
<p>Powered by Nubes Cloud S3 Storage</p>
</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"))
}
+55
View File
@@ -0,0 +1,55 @@
package main
import (
"context"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
s3 "github.com/minio/minio-go/v7"
)
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
}
ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second)
defer cancel()
obj, err := s3Client.GetObject(ctx, 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))
written, err := io.Copy(w, obj)
if err != nil {
log.Printf("Error streaming object: %v (sent %d/%d bytes)", err, written, stat.Size)
} else {
log.Printf("Proxy: sent %d bytes for %s", written, key)
}
}
+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>
Binary file not shown.
-411
View File
@@ -1,411 +0,0 @@
VERSION = "1.0.0"
import os
import mimetypes
import urllib.parse
from flask import Flask, Response, jsonify, request, stream_with_context
import boto3
from botocore.config import Config
# ---------------------------------------------------------------------------
# Config (from ENV, matching Go defaults)
# ---------------------------------------------------------------------------
HOSTNAME = os.getenv("REGISTRY_HOSTNAME", "localhost:8080")
S3_PREFIX = os.getenv("S3_PREFIX", "go-registry.containerk8s.dev.nubes.ru")
S3_ENDPOINT = os.getenv("S3_ENDPOINT", "")
S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY", "")
S3_SECRET_KEY = os.getenv("S3_SECRET_KEY", "")
S3_BUCKET = os.getenv("S3_BUCKET", "terraform-registry")
# ---------------------------------------------------------------------------
# GPG public keys (from LIVE Go registry — TWO keys: primary + legacy)
# ---------------------------------------------------------------------------
GPG_PRIMARY_KEY_ID = "3EC4673EB798238A"
GPG_PRIMARY_ASCII_ARMOR = """-----BEGIN PGP PUBLIC KEY BLOCK-----
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
6cZ0NQ==
=3Ea2
-----END PGP PUBLIC KEY BLOCK-----"""
GPG_LEGACY_KEY_ID = "CB3A0DF161ECC416"
GPG_LEGACY_ASCII_ARMOR = """-----BEGIN PGP PUBLIC KEY BLOCK-----
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
=q8Tw
-----END PGP PUBLIC KEY BLOCK-----"""
# ---------------------------------------------------------------------------
# S3 client (MinIO-compatible, Secure=true in Go → use_ssl=True + s3v4)
# ---------------------------------------------------------------------------
s3 = boto3.client(
"s3",
endpoint_url=f"https://{S3_ENDPOINT}",
aws_access_key_id=S3_ACCESS_KEY,
aws_secret_access_key=S3_SECRET_KEY,
config=Config(signature_version="s3v4"),
use_ssl=True,
)
# ---------------------------------------------------------------------------
# Flask app
# ---------------------------------------------------------------------------
app = Flask(__name__)
# ---------------------------------------------------------------------------
# Route: Discovery
# GET /.well-known/terraform.json
# ---------------------------------------------------------------------------
@app.route("/.well-known/terraform.json")
def discovery():
return jsonify({"providers.v1": "/v1/providers/"})
# ---------------------------------------------------------------------------
# Route: Provider router
# GET /v1/providers/{ns}/{name}/versions
# GET /v1/providers/{ns}/{name}/{ver}/download/{os}/{arch}
# ---------------------------------------------------------------------------
@app.route("/v1/providers/<path:provider_path>")
def provider_router(provider_path):
parts = provider_path.split("/")
# /v1/providers/{ns}/{name}/versions
if len(parts) == 3 and parts[2] == "versions":
return _list_versions(parts[0], parts[1])
# /v1/providers/{ns}/{name}/{ver}/download/{os}/{arch}
if len(parts) == 6 and parts[3] == "download":
return _download_version(parts[0], parts[1], parts[2], parts[4], parts[5])
return "Not Found", 404
def _list_versions(namespace, name):
"""List all versions and platforms by scanning S3 objects."""
prefix = f"{S3_PREFIX}/{namespace}/{name}/"
seen = {}
paginator = s3.get_paginator("list_objects_v2")
for page in paginator.paginate(Bucket=S3_BUCKET, Prefix=prefix):
for obj in page.get("Contents", []):
key = obj["Key"]
key_parts = key.split("/")
if len(key_parts) < 5:
continue
ver = key_parts[3]
filename = key_parts[4]
if ver not in seen:
seen[ver] = {
"version": ver,
"protocols": ["5.0"],
"platforms": [],
}
if "_darwin_amd64.zip" in filename:
seen[ver]["platforms"].append({"os": "darwin", "arch": "amd64"})
if "_linux_amd64.zip" in filename:
seen[ver]["platforms"].append({"os": "linux", "arch": "amd64"})
if "_windows_amd64.zip" in filename:
seen[ver]["platforms"].append({"os": "windows", "arch": "amd64"})
versions = list(seen.values())
return jsonify({
"id": f"{namespace}/{name}",
"versions": versions,
"warnings": [],
})
def _download_version(namespace, name, version, os_type, arch):
"""Build download metadata JSON with S3 proxy URLs."""
base = f"{S3_PREFIX}/{namespace}/{name}/{version}"
filename = f"terraform-provider-{name}_{version}_{os_type}_{arch}.zip"
full_key = f"{base}/{filename}"
shasums_key = f"{base}/terraform-provider-{name}_{version}_SHA256SUMS"
sig_key = f"{base}/terraform-provider-{name}_{version}_SHA256SUMS.sig"
# Extract shasum for this file from SHA256SUMS
shasum = ""
try:
resp = s3.get_object(Bucket=S3_BUCKET, Key=shasums_key)
for line in resp["Body"].read().decode("utf-8", errors="replace").splitlines():
if filename in line:
shasum = line.split()[0]
break
except Exception:
pass
base_url = f"https://{HOSTNAME}"
download_url = f"{base_url}/v1/proxy?bucket={S3_BUCKET}&key={urllib.parse.quote(full_key)}"
shasums_url = f"{base_url}/v1/proxy?bucket={S3_BUCKET}&key={urllib.parse.quote(shasums_key)}"
sig_url = f"{base_url}/v1/proxy?bucket={S3_BUCKET}&key={urllib.parse.quote(sig_key)}"
return jsonify({
"protocols": ["5.0"],
"os": os_type,
"arch": arch,
"filename": filename,
"download_url": download_url,
"shasums_url": shasums_url,
"shasums_signature_url": sig_url,
"shasum": shasum,
"signing_keys": {
"gpg_public_keys": [
{
"key_id": GPG_PRIMARY_KEY_ID,
"ascii_armor": GPG_PRIMARY_ASCII_ARMOR,
},
{
"key_id": GPG_LEGACY_KEY_ID,
"ascii_armor": GPG_LEGACY_ASCII_ARMOR,
},
]
},
})
# ---------------------------------------------------------------------------
# Route: Proxy (S3 streaming)
# GET /v1/proxy?bucket=...&key=...
# ---------------------------------------------------------------------------
@app.route("/v1/proxy")
def proxy():
bucket = request.args.get("bucket", "")
key = request.args.get("key", "")
if not bucket or not key:
return "Missing bucket or key params", 400
try:
head = s3.head_object(Bucket=bucket, Key=key)
obj = s3.get_object(Bucket=bucket, Key=key)
return Response(
stream_with_context(obj["Body"].iter_chunks(chunk_size=65536)),
content_type="application/octet-stream",
headers={
"Content-Length": str(head["ContentLength"]),
"Last-Modified": head["LastModified"].strftime("%a, %d %b %Y %H:%M:%S GMT"),
},
)
except Exception:
return "File not found", 404
# ---------------------------------------------------------------------------
# Route: Docs (static from S3)
# GET /docs/{ns}/{name}/{ver}/...
# ---------------------------------------------------------------------------
@app.route("/docs/<path:doc_path>")
def docs(doc_path):
# Parse: docs/{namespace}/{name}/{version}/...rest
parts = doc_path.strip("/").split("/")
if len(parts) < 3:
return "Bad docs path", 400
namespace = parts[0]
name = parts[1]
version = parts[2]
rest = "/".join(parts[3:]) if len(parts) > 3 else ""
# Build candidate S3 keys (exact 1:1 with Go tryCandidateKeys)
candidates = []
if rest == "":
candidates.append(f"docs/{namespace}/{name}/{version}/index.html")
else:
# 1) exact path
candidates.append(f"docs/{namespace}/{name}/{version}/{rest}")
# 2) if looks like directory or no extension → try index.html under it
rest_ext = os.path.splitext(rest)[1] # ".css" or ""
if rest.endswith("/") or rest_ext == "":
candidates.append(
f"docs/{namespace}/{name}/{version}/{rest.rstrip('/')}/index.html"
)
# 3) fallback: root index.html
candidates.append(f"docs/{namespace}/{name}/{version}/index.html")
for s3_key in candidates:
try:
head = s3.head_object(Bucket=S3_BUCKET, Key=s3_key)
obj = s3.get_object(Bucket=S3_BUCKET, Key=s3_key)
# Content-Type (1:1 with Go docsHandler)
ext = os.path.splitext(s3_key)[1] # ".html", ".css", etc.
ctype = mimetypes.types_map.get(ext, "")
if not ctype:
if ext == ".html" or s3_key.endswith("index.html"):
ctype = "text/html; charset=utf-8"
else:
ctype = "application/octet-stream"
return Response(
stream_with_context(obj["Body"].iter_chunks(chunk_size=65536)),
content_type=ctype,
headers={
"Content-Length": str(head["ContentLength"]),
"Last-Modified": head["LastModified"].strftime(
"%a, %d %b %Y %H:%M:%S GMT"
),
},
)
except Exception:
continue
return "Documentation not found", 404
# ---------------------------------------------------------------------------
# Route: Healthz
# GET /healthz
# ---------------------------------------------------------------------------
@app.route("/healthz")
def healthz():
return "ok", 200
# ---------------------------------------------------------------------------
# Route: Readyz
# GET /readyz
# ---------------------------------------------------------------------------
@app.route("/readyz")
def readyz():
return "ok", 200
# ---------------------------------------------------------------------------
# Route: Root (LAST — must not shadow /docs)
# GET /
# ---------------------------------------------------------------------------
@app.route("/")
def index():
return f"""<!DOCTYPE html>
<html>
<head><title>Terra Registry v{VERSION}</title></head>
<body>
<h1>Terra Registry & Documentation Server v{VERSION}</h1>
<p>Status: <span style="color: green">ONLINE</span></p>
<hr>
<p>Powered by Nubes Cloud S3 Storage</p>
</body>
</html>"""
# ---------------------------------------------------------------------------
# Main (mandatory for Nubes managed service)
# ---------------------------------------------------------------------------
if __name__ == "__main__":
app.run(host="0.0.0.0", port=5000)
Executable
+87
View File
@@ -0,0 +1,87 @@
#!/usr/bin/env bash
# Smoke tests for Terraform Registry
# Usage: bash tests/smoke.sh [DOMAIN]
set -euo pipefail
DOMAIN="${1:-https://registry.containerk8s.dev.nubes.ru}"
PASS=0
FAIL=0
check() {
local name="$1"
if eval "$2" &>/dev/null; then
echo "$name"
((PASS++))
else
echo "$name"
((FAIL++))
fi
}
echo "=== Registry Smoke Tests: $DOMAIN ==="
echo
# 1. Root page
check "GET /" \
"curl -s --max-time 5 '$DOMAIN/' | grep -q ONLINE"
# 2. Healthz
check "GET /healthz → ok" \
"test 'ok' = \"\$(curl -s --max-time 5 '$DOMAIN/healthz')\""
# 3. Readyz
check "GET /readyz → ok" \
"test 'ok' = \"\$(curl -s --max-time 5 '$DOMAIN/readyz')\""
# 4. Discovery
check "GET /.well-known/terraform.json" \
"curl -s --max-time 5 '$DOMAIN/.well-known/terraform.json' | grep -q '/v1/providers/'"
# 5. Versions
check "GET /v1/providers/nubes-test/nubes/versions" \
"curl -s --max-time 10 '$DOMAIN/v1/providers/nubes-test/nubes/versions' | \
python3 -c \"
import sys,json
d=json.load(sys.stdin)
vs=[v['version'] for v in d['versions']]
assert d['id']=='nubes-test/nubes'
assert len(vs)>=1
assert vs==sorted(vs), 'not sorted'
for v in d['versions']:
p=[x['os'] for x in v['platforms']]
assert 'linux' in p and 'darwin' in p and 'windows' in p
print('OK')
\""
# 6. Download metadata
check "GET /v1/providers/.../5.1.17/download/linux/amd64" \
"curl -s --max-time 10 '$DOMAIN/v1/providers/nubes-test/nubes/5.1.17/download/linux/amd64' | \
python3 -c \"
import sys,json
d=json.load(sys.stdin)
assert d['os']=='linux' and d['arch']=='amd64'
assert len(d['shasum'])==64
assert 'download_url' in d
keys=d['signing_keys']['gpg_public_keys']
assert len(keys)==2
assert keys[0]['key_id']=='3EC4673EB798238A'
assert keys[1]['key_id']=='CB3A0DF161ECC416'
print('OK')
\""
# 7. Proxy download
check "GET /v1/proxy — download zip" \
"url=\$(curl -s --max-time 5 '$DOMAIN/v1/providers/nubes-test/nubes/5.1.17/download/linux/amd64' | python3 -c \"import sys,json; print(json.load(sys.stdin)['download_url'])\") && \
test '200' = \"\$(curl -s --max-time 15 -o /dev/null -w '%{http_code}' \"\$url\")\""
# 8. 404 for unknown provider
check "GET unknown → 404" \
"test '404' = \"\$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' '$DOMAIN/v1/providers/nosuch/noversion/versions')\""
# 9. Missing key param → 400
check "GET /v1/proxy без key → 400" \
"test '400' = \"\$(curl -s --max-time 5 -o /dev/null -w '%{http_code}' '$DOMAIN/v1/proxy')\""
echo
echo "=== $PASS PASS, $FAIL FAIL ==="
exit $FAIL