Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ee0065630 | ||
|
|
391223e61e | ||
|
|
c3e75f11d6 | ||
|
|
5e81c06124 | ||
|
|
8bbb5adf21 | ||
|
|
35dd57c13b | ||
|
|
33b694f5f5 | ||
|
|
422736f5fa | ||
|
|
cf2789be76 | ||
|
|
3a47b8546f | ||
|
|
c286d190ba | ||
|
|
76f70c91de | ||
|
|
e8891c2643 | ||
|
|
78be235ec5 | ||
|
|
37d69ead42 | ||
|
|
575ba6dcb3 | ||
|
|
9b5a2a6fb0 | ||
|
|
c587e78a18 | ||
|
|
19bd727bac | ||
|
|
fe3913e51d | ||
|
|
4f5a1bfdc9 | ||
|
|
fe4b2f502a | ||
|
|
b1263a2e30 | ||
|
|
503df159b6 | ||
|
|
1e50cc1a83 | ||
|
|
6e1ce97f14 | ||
|
|
3d290ba317 | ||
|
|
be5fe36d63 | ||
|
|
cd81d2fe07 | ||
|
|
2ac2ca83a6 | ||
|
|
45876fe87b | ||
|
|
22036b253a | ||
|
|
ee53e0b11a | ||
|
|
c19ce9c4bf | ||
|
|
a369ff7c59 | ||
|
|
b5ff148ebc | ||
|
|
a9fa5d3835 | ||
|
|
73ce2680e9 | ||
|
|
d73bf90f0a | ||
|
|
94a900d888 | ||
|
|
90807aae2f | ||
|
|
4592a35b3e | ||
|
|
3aca89ea60 | ||
|
|
f3df103bb3 | ||
|
|
6ec580c616 | ||
|
|
86c4a1f25c | ||
|
|
13f8ce8be6 | ||
|
|
e87eab4df9 | ||
|
|
da03bd023b | ||
|
|
4c10a3b836 | ||
|
|
f10f2d3998 | ||
|
|
b9c47765ac | ||
|
|
a9cfea16e9 | ||
|
|
91472e1259 | ||
|
|
35a07ca374 | ||
|
|
8110aad493 | ||
|
|
e19bb6f290 | ||
|
|
a0e72b1c7f |
@@ -10,6 +10,7 @@ dist/
|
||||
*.out
|
||||
vendor/
|
||||
go.sum.bak
|
||||
server/tf-registry
|
||||
|
||||
# ── Python ─────────────────────────────────────────────────────────────────────
|
||||
__pycache__/
|
||||
|
||||
@@ -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-конфигах пользователей
|
||||
+30
@@ -0,0 +1,30 @@
|
||||
# Multi-stage build for Terraform Registry
|
||||
#
|
||||
# Stage 1: Build Go binary
|
||||
# Stage 2: Minimal Alpine runtime
|
||||
|
||||
FROM golang:1.24-alpine AS builder
|
||||
ENV GOTOOLCHAIN=auto
|
||||
WORKDIR /app
|
||||
COPY server/go.mod server/go.sum ./
|
||||
RUN go mod download
|
||||
COPY server/ .
|
||||
RUN CGO_ENABLED=0 go build -o registry .
|
||||
|
||||
FROM alpine:3.21
|
||||
RUN apk --no-cache add ca-certificates
|
||||
COPY --from=builder /app/registry /registry
|
||||
|
||||
# ── Non-secret config (change here → rebuild → push → redeploy) ─────────
|
||||
ENV S3_ENDPOINT=s3.msk-1.ngcloud.ru \
|
||||
S3_BUCKET=nubes-terraform-registry \
|
||||
S3_PREFIX=tf-registry.containerk8s.services.ngcloud.ru \
|
||||
REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru \
|
||||
PORT=5000
|
||||
# ── Secrets in UI jsonEnv: S3_ACCESS_KEY, S3_SECRET_KEY ───────────────────
|
||||
|
||||
RUN adduser -D -u 1000 registry
|
||||
USER registry
|
||||
|
||||
EXPOSE 5000
|
||||
CMD ["/registry"]
|
||||
@@ -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.
|
||||
@@ -1,91 +0,0 @@
|
||||
# 2026-08-06 — Ревизия плана Flask-реестра
|
||||
|
||||
## Контекст
|
||||
|
||||
Переписываем Go-реестр (`server/main.go`) на Python/Flask для managed-сервиса в облаке.
|
||||
Логика 1:1 — те же эндпоинты, та же S3-структура, тот же GPG-ключ.
|
||||
|
||||
## Файлы прочитаны
|
||||
|
||||
| Файл | Строки | Что |
|
||||
|---|---|---|
|
||||
| `server/main.go` | 1-289 | Весь HTTP-сервер: 7 роутов, S3, GPG |
|
||||
| `server/docs.go` | 1-200 | Docs-сервер с кандидатами ключей |
|
||||
| `server/gpg_key.go` | 1-50 | Прокси к `registrykeys` пакету |
|
||||
| `registrykeys/public_key.go` | 1-50 | Константы KeyID + ASCIIArmor |
|
||||
| `CHAT_SUMMARY_2026-08-06.md` | 1-100 | Контекст: архитектура, S3-структура, ENV |
|
||||
|
||||
## План (из plan.md) — TL;DR
|
||||
|
||||
- **Фаза 1**: git checkout -b golang → удалить Go-код из master
|
||||
- **Фаза 2**: `app.py` (~250-300 строк Flask) + `requirements.txt` (flask, boto3)
|
||||
- **Фаза 3**: py_compile → деплой → curl-проверки → terraform init
|
||||
|
||||
### Эндпоинты (из Go-кода):
|
||||
|
||||
| URL | Ответ |
|
||||
|---|---|
|
||||
| `GET /.well-known/terraform.json` | `{"providers.v1": "/v1/providers/"}` |
|
||||
| `GET /v1/providers/{ns}/{name}/versions` | JSON: id, versions[], warnings[] |
|
||||
| `GET /v1/providers/{ns}/{name}/{ver}/download/{os}/{arch}` | JSON: download_url, shasums_url, sig_url, shasum, signing_keys |
|
||||
| `GET /v1/proxy?bucket=...&key=...` | Стриминг файла из S3 |
|
||||
| `GET /docs/{ns}/{name}/{ver}/...` | Статика из S3 (кандидаты ключей) |
|
||||
| `GET /healthz` | `"ok"` 200 |
|
||||
| `GET /readyz` | `"ok"` 200 |
|
||||
| `GET /` | HTML-страница "ONLINE" |
|
||||
|
||||
### S3-ключи:
|
||||
|
||||
```
|
||||
{hostname}/{namespace}/{name}/{version}/terraform-provider-{name}_{ver}_{os}_{arch}.zip
|
||||
{hostname}/{namespace}/{name}/{version}/terraform-provider-{name}_{ver}_SHA256SUMS
|
||||
{hostname}/{namespace}/{name}/{version}/terraform-provider-{name}_{ver}_SHA256SUMS.sig
|
||||
docs/{namespace}/{name}/{version}/{path}
|
||||
```
|
||||
|
||||
### ENV-переменные:
|
||||
- `REGISTRY_HOSTNAME` (default: `localhost:8080`)
|
||||
- `S3_ENDPOINT`
|
||||
- `S3_ACCESS_KEY`
|
||||
- `S3_SECRET_KEY`
|
||||
- `S3_BUCKET` (default: `terraform-registry`)
|
||||
|
||||
## 6 замечаний к плану
|
||||
|
||||
### 1. ⚠️ S3-пагинация (ВАЖНО)
|
||||
Go: `ListObjects` → канал с авто-пагинацией (все объекты).
|
||||
boto3: `list_objects_v2` → макс 1000 за раз.
|
||||
**Надо:** `Paginator` или цикл с `ContinuationToken`.
|
||||
|
||||
### 2. Docs-кандидаты (точность)
|
||||
Go-логика чуть точнее, чем описано в плане:
|
||||
- `rest` пустой → только `index.html`
|
||||
- `rest` не пустой: exact → (если `/` на конце ИЛИ нет расширения → `{rest}/index.html`) → `index.html`
|
||||
- Для `.css`/`.js` НЕ пробуется `{rest}/index.html`
|
||||
|
||||
### 3. Content-Length при стриминге
|
||||
Go ставит через `obj.Stat()`. Flask `stream_with_context` — нет.
|
||||
**Надо:** `head_object` → взять `ContentLength` → выставить вручную.
|
||||
|
||||
### 4. boto3 + MinIO
|
||||
```python
|
||||
boto3.client('s3',
|
||||
endpoint_url=f'https://{s3_endpoint}',
|
||||
use_ssl=True,
|
||||
config=Config(signature_version='s3v4')
|
||||
)
|
||||
```
|
||||
Без `s3v4` могут быть ошибки подписи на MinIO.
|
||||
|
||||
### 5. /readyz
|
||||
Go: комментарий «Проверить S3», но реально просто `"ok"`.
|
||||
Делать 1:1 → просто `"ok"`.
|
||||
|
||||
### 6. Порядок роутов во Flask
|
||||
Flask матчит сверху вниз. `/` должен быть последним, чтобы не перехватить `/docs/`.
|
||||
|
||||
## Решения (подтверждены)
|
||||
- GPG: только константа, без python-gnupg ✅
|
||||
- Прокси: `?bucket=&key=` (НЕ `?url=`) ✅
|
||||
- Платформы: только `linux_amd64` + `windows_amd64` ✅
|
||||
- Домен: `registry.pythonk8s.dev.nubes.ru` ✅
|
||||
@@ -0,0 +1,34 @@
|
||||
# 2026-09-02: Перенос git-репозиториев и Docker-образов из `Nail` в организацию `terraform`
|
||||
|
||||
## Факты
|
||||
|
||||
- Git-репозитории `tf_provider`, `tf_registry`, `tf_docs` перенесены вручную из аккаунта `Nail` в организацию `terraform` на Gitea.
|
||||
- Старые адреса `gitea.services.ngcloud.ru/Nail/...` отдают redirect на `terraform/...`.
|
||||
- Docker-образы реестра **не переносились автоматически** — оставались в `gitea.services.ngcloud.ru/nail/tf_registry`.
|
||||
|
||||
## Выполнено (перенос образов)
|
||||
|
||||
Локально (Krupski) перетегированы и запушены в `gitea.services.ngcloud.ru/terraform/tf_registry` все актуальные теги:
|
||||
|
||||
- `0.0.1`, `0.0.3`, `0.0.4`, `0.0.5`, `0.0.6`, `latest`
|
||||
- Проверено: `docker manifest inspect gitea.services.ngcloud.ru/terraform/tf_registry:0.0.6` → OK.
|
||||
- `0.0.6` digest: `sha256:0888e5bc16b8359fecc829b67f801245e7df0c355a7bc0086ab37bd21db897f2`
|
||||
- `latest` и `0.0.3` указывают на один digest: `sha256:e3f8510d521025f4980c65226507f31aa9f4defe68e3bc508625919927792bda5`
|
||||
- Тег `0.0.2` отсутствовал и локально, и в registry (не публиковался).
|
||||
|
||||
## Обновления remote в локальных копиях
|
||||
|
||||
- `tf_provider` origin → `https://gitea.services.ngcloud.ru/terraform/tf_provider.git`
|
||||
- `tf_registry` origin → `https://gitea.services.ngcloud.ru/terraform/tf_registry.git`
|
||||
- `tf_docs` origin → `https://gitea.services.ngcloud.ru/terraform/tf_docs.git`
|
||||
|
||||
## Осталось (ручной шаг в Nubes Cloud UI)
|
||||
|
||||
- Обновить `registryPath` инстанса реестра (`tf-registry.containerk8s.services.ngcloud.ru`) с `gitea.services.ngcloud.ru/nail/tf_registry:<тег>` на `gitea.services.ngcloud.ru/terraform/tf_registry:<тег>`.
|
||||
- Выполнить redeploy через UI `modify`.
|
||||
- Старые образы в `nail/tf_registry` можно удалить после подтверждения, что новый путь работает.
|
||||
|
||||
## Напоминание
|
||||
|
||||
- Docker-имя registry требует lowercase: `terraform/tf_registry`, не `Terraform/...`.
|
||||
- Команды сборки/пуша в README теперь указывают на `gitea.services.ngcloud.ru/terraform/tf_registry`.
|
||||
@@ -1,104 +0,0 @@
|
||||
# Инструкция: как опубликовать провайдер в реестре
|
||||
|
||||
Реестр: `https://go-registry.containerk8s.dev.nubes.ru`
|
||||
|
||||
## 1. Структура S3
|
||||
|
||||
```
|
||||
{prefix}/{namespace}/{name}/{version}/
|
||||
├── terraform-provider-{name}_{version}_{os}_{arch}.zip ← бинарник
|
||||
├── terraform-provider-{name}_{version}_SHA256SUMS ← контрольные суммы
|
||||
└── terraform-provider-{name}_{version}_SHA256SUMS.sig ← GPG-подпись
|
||||
```
|
||||
|
||||
Где:
|
||||
- `{prefix}` — сейчас `registry.kube5s.ru` (будет сменён)
|
||||
- `{namespace}` — `nubes-test`, `nubes-dev`, `nubes` (PROD)
|
||||
- `{name}` — `nubes`
|
||||
- `{os}_{arch}` — `linux_amd64`, `windows_amd64`, `darwin_amd64`
|
||||
|
||||
Пример:
|
||||
```
|
||||
registry.kube5s.ru/nubes-test/nubes/5.1.17/terraform-provider-nubes_5.1.17_linux_amd64.zip
|
||||
```
|
||||
|
||||
## 2. Как загрузить в S3
|
||||
|
||||
### Нужны креды
|
||||
```
|
||||
S3_ENDPOINT=s3.msk-1.ngcloud.ru
|
||||
S3_ACCESS_KEY=...
|
||||
S3_SECRET_KEY=...
|
||||
S3_BUCKET=terraform-registry
|
||||
```
|
||||
|
||||
### Утилита `mc` (MinIO Client)
|
||||
```bash
|
||||
mc alias set nubes-s3 https://s3.msk-1.ngcloud.ru $S3_ACCESS_KEY $S3_SECRET_KEY
|
||||
|
||||
# Загрузка
|
||||
mc cp ./terraform-provider-nubes_5.1.17_linux_amd64.zip \
|
||||
nubes-s3/terraform-registry/registry.kube5s.ru/nubes-test/nubes/5.1.17/
|
||||
mc cp ./SHA256SUMS nubes-s3/terraform-registry/registry.kube5s.ru/nubes-test/nubes/5.1.17/
|
||||
mc cp ./SHA256SUMS.sig nubes-s3/terraform-registry/registry.kube5s.ru/nubes-test/nubes/5.1.17/
|
||||
```
|
||||
|
||||
### Или `awscli`
|
||||
```bash
|
||||
aws s3 cp ... s3://terraform-registry/registry.kube5s.ru/nubes-test/nubes/5.1.17/ \
|
||||
--endpoint-url https://s3.msk-1.ngcloud.ru
|
||||
```
|
||||
|
||||
## 3. GPG-подпись (обязательно)
|
||||
|
||||
Terraform требует подпись SHA256SUMS.
|
||||
|
||||
### Приватный ключ
|
||||
Хранится в `secrets/private_key.asc`. Публичный ключ (KEY ID: `3EC4673EB798238A`) вшит в реестр.
|
||||
|
||||
### Подписать
|
||||
```bash
|
||||
gpg --import secrets/private_key.asc
|
||||
gpg --detach-sign --armor SHA256SUMS
|
||||
# → SHA256SUMS.asc (переименовать в SHA256SUMS.sig)
|
||||
```
|
||||
|
||||
## 4. Документация
|
||||
|
||||
```
|
||||
docs/{namespace}/{name}/{version}/index.html
|
||||
docs/{namespace}/{name}/{version}/...любые файлы...
|
||||
```
|
||||
|
||||
Заливаешь в S3 — автоматом доступна по:
|
||||
```
|
||||
https://go-registry.containerk8s.dev.nubes.ru/docs/{ns}/{name}/{ver}/
|
||||
```
|
||||
|
||||
## 5. Использование пользователем
|
||||
|
||||
```hcl
|
||||
terraform {
|
||||
required_providers {
|
||||
nubes = {
|
||||
source = "go-registry.containerk8s.dev.nubes.ru/nubes-test/nubes"
|
||||
version = "5.1.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Terraform сам найдёт реестр, скачает провайдер, проверит SHA256 и GPG-подпись.
|
||||
|
||||
## 6. Скрипт автоматизации
|
||||
|
||||
В репозитории `tf_registry` есть скрипт `server/build-provider.sh` — он делает всё сам:
|
||||
- Компилирует провайдер под linux/windows/darwin × amd64
|
||||
- Упаковывает в zip
|
||||
- Генерирует SHA256SUMS
|
||||
- Подписывает GPG
|
||||
- Заливает в S3
|
||||
|
||||
```bash
|
||||
VERSION=5.1.17 NAMESPACE=nubes-test ./server/build-provider.sh
|
||||
```
|
||||
+29
-51
@@ -1,30 +1,30 @@
|
||||
# Как залить провайдер в реестр
|
||||
# Как залить провайдер в реестр (ПРОД)
|
||||
|
||||
## Конфигурация
|
||||
|
||||
### Реестр
|
||||
- URL: `https://registry.containerk8s.dev.nubes.ru`
|
||||
- S3: Ceph `http://ceph.tst.nubes.ru`
|
||||
- Бакет: `terraform-registry`
|
||||
- S3-префикс: `registry.containerk8s.dev.nubes.ru`
|
||||
- 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 | Назначение | Права |
|
||||
|---|---|---|
|
||||
| `3709:reader` | Реестр (только читает) | read + list |
|
||||
| `3709:super` | Заливка провайдеров | write |
|
||||
| `1112_terraform:reader` | Реестр (только читает) | read + list |
|
||||
| `1112_terraform:super` | Заливка провайдеров | write |
|
||||
|
||||
```
|
||||
Reader: AAYTNYSZIM08DDWV3TK1 / xQVd9CeGXACPIXTBTH4KV6DIHEAyySOFSTrS17Lc
|
||||
Super: 63A90SRCBP51XQ5TX8OB / euEJDZkueh0JHGP1B5jlAnAKriB8kYBR0t466GQ9
|
||||
Reader: 6DDP5PKQEE37WLSWXP07 / W0vV4YNeHIqMa2sRCXsR61ZuQfAJIZzVj2VgnBrT
|
||||
Super: L9F0R8CYYKYIUW180203 / yJlgxoKsRBwCtJQh57Z0nwo5EBDYmyBNDDhG6NKw
|
||||
```
|
||||
|
||||
## Структура S3
|
||||
|
||||
```
|
||||
terraform-registry/
|
||||
└── registry.containerk8s.dev.nubes.ru/
|
||||
nubes-terraform-registry/
|
||||
└── tf-registry.containerk8s.services.ngcloud.ru/
|
||||
└── {namespace}/
|
||||
└── {provider}/
|
||||
└── {version}/
|
||||
@@ -37,58 +37,43 @@ terraform-registry/
|
||||
|
||||
## Заливка (build-provider.sh)
|
||||
|
||||
### ⚠️ Важно: S3_ENDPOINT с http://
|
||||
Ceph тестовый — HTTP, не HTTPS. Если задать `S3_ENDPOINT=ceph.tst.nubes.ru`, скрипт добавит `https://` → упадёт.
|
||||
**Всегда:** `S3_ENDPOINT=http://ceph.tst.nubes.ru` (с префиксом `http://`).
|
||||
### Требования
|
||||
- `~/tf_registry/` — склонирован этот репозиторий
|
||||
- `~/tf_provider/` — склонирован репозиторий провайдера
|
||||
- `~/tf_provider/secrets/private_key.asc` — приватный GPG-ключ для подписи
|
||||
- `go` установлен
|
||||
- `mc` (MinIO Client) установлен
|
||||
|
||||
### Команды
|
||||
|
||||
```bash
|
||||
# 1. Настроить mc (один раз, креды SUPER)
|
||||
mc alias set ceph http://ceph.tst.nubes.ru \
|
||||
63A90SRCBP51XQ5TX8OB \
|
||||
euEJDZkueh0JHGP1B5jlAnAKriB8kYBR0t466GQ9
|
||||
mc alias set prod-s3 https://s3.msk-1.ngcloud.ru L9F0R8CYYKYIUW180203 yJlgxoKsRBwCtJQh57Z0nwo5EBDYmyBNDDhG6NKw
|
||||
|
||||
# 2. Создать бакет (если ещё нет)
|
||||
mc mb ceph/terraform-registry
|
||||
mc mb prod-s3/nubes-terraform-registry
|
||||
|
||||
# 3. Переменные для сборки
|
||||
export ROOT_DIR=~/tf_provider
|
||||
export PROVIDER_DIR=~/tf_provider/provider
|
||||
export REGISTRY_HOSTNAME=registry.containerk8s.dev.nubes.ru
|
||||
export NAMESPACE=nubes-test # nubes-test | nubes-dev | nubes
|
||||
export REGISTRY_HOSTNAME=tf-registry.containerk8s.services.ngcloud.ru
|
||||
export NAMESPACE=nubes-test
|
||||
export PROVIDER_NAME=nubes
|
||||
export S3_ENDPOINT=http://ceph.tst.nubes.ru # ← http:// обязательно!
|
||||
export S3_ACCESS_KEY=63A90SRCBP51XQ5TX8OB # super
|
||||
export S3_SECRET_KEY=euEJDZkueh0JHGP1B5jlAnAKriB8kYBR0t466GQ9 # super
|
||||
export S3_BUCKET=terraform-registry
|
||||
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
|
||||
```
|
||||
|
||||
### Что делает скрипт
|
||||
1. Компилирует Go → linux/windows/darwin × amd64
|
||||
2. Упаковывает в zip
|
||||
3. Генерирует SHA256SUMS
|
||||
4. Подписывает GPG (ключ `secrets/private_key.asc`)
|
||||
5. Заливает в S3
|
||||
|
||||
### Требования
|
||||
- `~/tf_provider/secrets/private_key.asc` — приватный GPG-ключ
|
||||
- `~/tf_provider/provider/` — исходники Go-провайдера
|
||||
- `mc` (MinIO Client) установлен
|
||||
- `go` установлен
|
||||
|
||||
## Проверка после заливки
|
||||
|
||||
```bash
|
||||
# API
|
||||
curl https://registry.containerk8s.dev.nubes.ru/v1/providers/nubes-test/nubes/versions
|
||||
|
||||
# Полный тест
|
||||
bash ~/tf_registry/tests/smoke.sh
|
||||
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'ы
|
||||
@@ -104,16 +89,9 @@ Terraform-конфиг пользователя:
|
||||
terraform {
|
||||
required_providers {
|
||||
nubes = {
|
||||
source = "registry.containerk8s.dev.nubes.ru/nubes-test/nubes"
|
||||
source = "tf-registry.containerk8s.services.ngcloud.ru/nubes-test/nubes"
|
||||
version = "5.1.17"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Уроки из диалога с чатом сборки
|
||||
|
||||
1. **S3_ENDPOINT** — всегда с протоколом (`http://` или `https://`). Без протокола скрипт форсит `https://`.
|
||||
2. **Бакет** — может не существовать на новом Ceph. Создать через `mc mb`.
|
||||
3. **Префикс** — должен совпадать с `REGISTRY_HOSTNAME`. Если домен поменялся — обновить и в скрипте, и в Dockerfile реестра.
|
||||
4. **Креды** — для заливки нужны super (write), для реестра — reader (read+list).
|
||||
|
||||
@@ -1,59 +0,0 @@
|
||||
# Ceph S3 — конфигурация для реестра
|
||||
|
||||
## Инстанс S3
|
||||
|
||||
| Поле | Значение |
|
||||
|---|---|
|
||||
| userName | `naeels3` |
|
||||
| instanceUid | `332cdb0d-34bf-43bf-864d-4adcc3b556fb` |
|
||||
| urlConnect | `http://ceph.tst.nubes.ru/` |
|
||||
| resourceRealm | `ceph.tst.nubes.ru` |
|
||||
|
||||
## Пользователи (users)
|
||||
|
||||
| user_id | Назначение |
|
||||
|---|---|
|
||||
| `3709` | Основной владелец |
|
||||
| `3709:super` | Subuser с повышенными правами |
|
||||
| `3709:technicalS3Backup` | Subuser для бэкапов |
|
||||
| `3709:reader` | **(планируется)** Subuser для реестра — только read+list |
|
||||
|
||||
## Subuser `3709:reader` для реестра
|
||||
|
||||
### Права
|
||||
- `read` — чтение объектов
|
||||
- `list` — список объектов (нужно для `listVersions`)
|
||||
- **Без** `write` и `delete` — реестр не может менять данные
|
||||
|
||||
### Креды (после создания)
|
||||
```json
|
||||
{
|
||||
"S3_ACCESS_KEY": "...",
|
||||
"S3_SECRET_KEY": "..."
|
||||
}
|
||||
```
|
||||
Вставить в `jsonEnv` инстанса реестра.
|
||||
|
||||
### Конфигурация реестра под Ceph
|
||||
|
||||
Dockerfile ENV:
|
||||
```dockerfile
|
||||
ENV S3_ENDPOINT=ceph.tst.nubes.ru \
|
||||
S3_BUCKET=terraform-registry \
|
||||
S3_PREFIX=go-registry.containerk8s.dev.nubes.ru \
|
||||
REGISTRY_HOSTNAME=go-registry.containerk8s.dev.nubes.ru \
|
||||
PORT=5000
|
||||
```
|
||||
|
||||
### Различия с текущим S3 (MinIO)
|
||||
|
||||
| | Старый (MinIO) | Новый (Ceph) |
|
||||
|---|---|---|
|
||||
| ENDPOINT | `s3.msk-1.ngcloud.ru` | `ceph.tst.nubes.ru` |
|
||||
| Креды | Статические | Subuser `3709:reader` |
|
||||
| Протокол | HTTPS | HTTP (судя по `urlConnect`) |
|
||||
| Безопасность | Приватный MinIO | Ceph с разделением прав |
|
||||
|
||||
### ⚠️ Если HTTP
|
||||
В Go-коде сейчас `Secure: true` — для HTTP надо поменять на `Secure: false`.
|
||||
Лучше через ENV: `S3_USE_SSL=true/false` (сейчас захардкожено).
|
||||
@@ -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. **Общее** — перед деплоем в прод, что ещё проверить?
|
||||
@@ -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. **Общее** — перед тем как объявить готовым к проду, что ещё?
|
||||
@@ -0,0 +1,40 @@
|
||||
# Code Review Request — Go Terraform Registry
|
||||
|
||||
## Контекст
|
||||
|
||||
Terraform Provider Registry по протоколу HashiCorp. Деплоится как Docker-контейнер (containerk8s managed service в Nubes Cloud). Работает на проде: `https://go-registry.containerk8s.dev.nubes.ru`.
|
||||
|
||||
Хранилище — S3 (MinIO). Провайдеры, SHA256SUMS, GPG-подписи, документация — всё в S3.
|
||||
|
||||
## Файлы для ревью
|
||||
|
||||
- `server/main.go` — HTTP-сервер, S3, все роуты
|
||||
- `server/gpg_key.go` — GPG-ключи (2 штуки, вшиты в код)
|
||||
- `server/docs.go` — статика документации из S3
|
||||
- `Dockerfile` — multi-stage сборка
|
||||
|
||||
## Вопросы
|
||||
|
||||
1. **`downloadVersion`: `defer` внутри `if err == nil`** — defer в Go выполняется при выходе из функции, а не из блока. Правильно ли это здесь? Не приведёт ли к накоплению defer'ов или утечке?
|
||||
|
||||
2. **`readyzHandler`**: комментарий «Проверить доступность S3», но возвращает просто "ok". Стоит ли добавить реальную проверку (head bucket)?
|
||||
|
||||
3. **`listVersions`**: версии не сортируются, порядок зависит от S3 ListObjects. Стоит ли сортировать?
|
||||
|
||||
4. **`proxHandler`**: никаких ограничений на размер файла, таймаутов, rate-limiting. Нормально ли для внутреннего сервиса?
|
||||
|
||||
5. **`rootHandler`**: HTML захардкожен. Вынести в template или embed?
|
||||
|
||||
6. **Graceful shutdown**: нет обработки SIGTERM. Для managed-сервиса это критично?
|
||||
|
||||
7. **`tyCandidateKeys` в docs.go**: индентация сломана (табы/пробелы вперемешку). Это баг форматирования или Go это нормально компилирует?
|
||||
|
||||
8. **GPG-ключи в коде**: стоит ли вынести в файлы/ENV или оставить как есть?
|
||||
|
||||
9. **Dockerfile**: `go mod init` + `go mod tidy` при каждой сборке. Стоит ли добавить `go.sum` в репо для воспроизводимости?
|
||||
|
||||
10. **Общая архитектура**: что можно улучшить? Есть ли проблемы с безопасностью?
|
||||
|
||||
## Как отвечать
|
||||
|
||||
Если что-то неясно — спрашивай. Контекст: сервис внутренний, не публичный, трафик низкий.
|
||||
@@ -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 без таймаута | 🟡 |
|
||||
@@ -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 с утечкой секретов. Остальное — желательно.
|
||||
@@ -0,0 +1,134 @@
|
||||
# Code Review — Соннет (2026-08-08)
|
||||
|
||||
## Файлы ревью
|
||||
- `server/main.go` — HTTP, S3, роуты
|
||||
- `server/gpg_key.go` — GPG-ключи
|
||||
- `server/docs.go` — статика из S3
|
||||
- `Dockerfile` — сборка
|
||||
|
||||
---
|
||||
|
||||
## 1. `defer` внутри `if err == nil` — ✅ OK
|
||||
|
||||
```go
|
||||
if err == nil {
|
||||
defer shasumsObj.Close() // выполнится при выходе из ФУНКЦИИ, не из if
|
||||
}
|
||||
```
|
||||
Не утечка. `defer` всегда выполняется при выходе из функции. `shasumsObj` открывается один раз на вызов и закрывается при возврате — правильный паттерн.
|
||||
|
||||
---
|
||||
|
||||
## 2. `readyzHandler` без проверки S3 — ⚠️ надо чинить
|
||||
|
||||
```go
|
||||
// Проверить доступность S3 ← комментарий, не реализовано
|
||||
w.Write([]byte("ok"))
|
||||
```
|
||||
K8s readiness probe всегда 200 — даже если S3 недоступен. Pod остаётся в rotation с неработающим S3.
|
||||
|
||||
**Фикс:** `s3Client.BucketExists(ctx, bucketName)`
|
||||
|
||||
---
|
||||
|
||||
## 3. Версии не сортируются — ⚠️ надо чинить
|
||||
|
||||
`seenVersions` — map, итерация недетерминирована. Terraform может некорректно выбрать последнюю версию.
|
||||
|
||||
**Фикс:** `sort.Slice` по семверу.
|
||||
|
||||
---
|
||||
|
||||
## 4. `proxyHandler` без лимитов — 🔴 КРИТИЧНО
|
||||
|
||||
### 4a. Размер/таймаут — некритично для внутреннего сервиса.
|
||||
|
||||
### 4b. SSRF / Authorization bypass — КРИТИЧНО
|
||||
|
||||
```go
|
||||
bucket := r.URL.Query().Get("bucket") // ← принимает ЛЮБОЙ bucket!
|
||||
key := r.URL.Query().Get("key")
|
||||
```
|
||||
Кто знает URL сервера — может читать объекты из любого бакета в том же S3.
|
||||
|
||||
**Фикс:** захардкодить `bucket = bucketName`, игнорировать параметр `bucket` из запроса. `key` валидировать что начинается с разрешённого префикса.
|
||||
|
||||
---
|
||||
|
||||
## 5. HTML хардкод — ✅ OK
|
||||
|
||||
```go
|
||||
fmt.Fprintf(w, `...` + VERSION + `...`)
|
||||
```
|
||||
Для крошечной страницы приемлемо. `VERSION` сейчас `"1.0.2"` — безопасно.
|
||||
|
||||
---
|
||||
|
||||
## 6. Graceful Shutdown — ⚠️ желательно
|
||||
|
||||
```go
|
||||
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||
```
|
||||
SIGTERM убивает процесс мгновенно. In-flight download обрывается → Terraform-клиент теряет кэш.
|
||||
|
||||
**Фикс:**
|
||||
```go
|
||||
srv := &http.Server{Addr: ":" + port}
|
||||
go srv.ListenAndServe()
|
||||
// signal.NotifyContext + srv.Shutdown(ctx)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Индентация в docs.go — ✅ косметика
|
||||
|
||||
Смешаны табы и пробелы. Go компилирует нормально, но `gofmt -w docs.go` переформатирует полностью. Если CI проверяет `gofmt -l` — упадёт.
|
||||
|
||||
---
|
||||
|
||||
## 8. GPG-ключи в коде — ✅ OK
|
||||
|
||||
Это **публичные** ключи — их задача быть известными. Смена ключа = rebuild + redeploy — для редкого события приемлемо.
|
||||
|
||||
---
|
||||
|
||||
## 9. `go mod init` + `go mod tidy` в Dockerfile — 🔴 КРИТИЧНО
|
||||
|
||||
```dockerfile
|
||||
RUN go mod init tf-registry && go mod tidy && ...
|
||||
```
|
||||
Каждая сборка качает зависимости из интернета. Если proxy недоступен или версия изменилась — сборка сломается.
|
||||
|
||||
**Фикс:** добавить `go.mod` + `go.sum` в репо (`server/go.mod`, `server/go.sum`).
|
||||
```dockerfile
|
||||
COPY server/go.mod server/go.sum ./
|
||||
RUN go mod download
|
||||
COPY server/ .
|
||||
RUN CGO_ENABLED=0 go build -o registry .
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 10. Общая архитектура и безопасность
|
||||
|
||||
| Проблема | Серьёзность | Статус |
|
||||
|---|---|---|
|
||||
| `proxyHandler` принимает любой `bucket` | 🔴 Высокая | SSRF — фикс п.4b |
|
||||
| Нет аутентификации | 🟡 Средняя | Внутренний сервис |
|
||||
| `key` не валидируется | 🟡 Средняя | `../` MinIO отклонит, но лучше проверять |
|
||||
| JSON-ошибки не логируются | 🟢 Низкая | `json.NewEncoder` без проверки ошибок |
|
||||
| Нет `go.sum` | 🟡 Средняя | Нерепродуцируемые сборки |
|
||||
| `readyz` всегда 200 | 🟡 Средняя | см. п.2 |
|
||||
|
||||
---
|
||||
|
||||
## Приоритеты к исправлению
|
||||
|
||||
| # | Что | Серьёзность |
|
||||
|---|---|---|
|
||||
| 1 | `proxyHandler` — убрать параметр `bucket` из запроса | 🔴 SSRF |
|
||||
| 2 | `go.sum` в репо | 🔴 Воспроизводимость |
|
||||
| 3 | `readyz` — реальная проверка S3 | 🟡 |
|
||||
| 4 | Сортировка версий | 🟡 |
|
||||
| 5 | Graceful shutdown | 🟡 |
|
||||
| 6 | `gofmt` docs.go | 🟢 |
|
||||
@@ -0,0 +1,290 @@
|
||||
Список задач обновлен
|
||||
|
||||
Проверялись следующие endpoint’ы registry:
|
||||
|
||||
```text
|
||||
https://tf-registry.containerk8s.services.ngcloud.ru/.well-known/terraform.json
|
||||
```
|
||||
|
||||
Результат: `HTTP 200`
|
||||
|
||||
```json
|
||||
{"providers.v1":"/v1/providers/"}
|
||||
```
|
||||
|
||||
Список версий:
|
||||
|
||||
```text
|
||||
https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-test/nubes/versions
|
||||
```
|
||||
|
||||
Результат: `HTTP 200`
|
||||
|
||||
Опубликованы:
|
||||
|
||||
```text
|
||||
5.0.1
|
||||
5.0.2
|
||||
5.0.3
|
||||
5.0.4
|
||||
5.0.5
|
||||
5.1.17
|
||||
```
|
||||
|
||||
Download metadata для `5.0.5`:
|
||||
|
||||
```text
|
||||
https://tf-registry.containerk8s.services.ngcloud.ru/v1/providers/nubes-test/nubes/5.0.5/download/linux/amd64
|
||||
```
|
||||
|
||||
Результат: `HTTP 200`, ответ около `8 KB`, содержит:
|
||||
|
||||
```text
|
||||
filename:
|
||||
terraform-provider-nubes_5.0.5_linux_amd64.zip
|
||||
|
||||
download_url:
|
||||
https://tf-registry.containerk8s.services.ngcloud.ru/v1/proxy?key=...
|
||||
```
|
||||
|
||||
Фактический proxy URL:
|
||||
|
||||
```text
|
||||
https://tf-registry.containerk8s.services.ngcloud.ru/v1/proxy?key=tf-registry.containerk8s.services.ngcloud.ru%2Fnubes-test%2Fnubes%2F5.0.5%2Fterraform-provider-nubes_5.0.5_linux_amd64.zip
|
||||
```
|
||||
|
||||
Проверка proxy:
|
||||
|
||||
```text
|
||||
HTTP: 200
|
||||
Content-Type: application/octet-stream
|
||||
Content-Length: 13083236
|
||||
```
|
||||
|
||||
Но фактически за 40 секунд скачалось только:
|
||||
|
||||
```text
|
||||
20983 байт
|
||||
```
|
||||
|
||||
и затем:
|
||||
|
||||
```text
|
||||
curl: (28) Operation timed out
|
||||
```
|
||||
|
||||
Дополнительно проверялись:
|
||||
|
||||
```text
|
||||
nubes-test/nubes:5.1.17
|
||||
nubes/nubes:2.0.6
|
||||
```
|
||||
|
||||
На них результат такой же: metadata отдается быстро, а ZIP через `/v1/proxy` обрывается примерно на `18–23 KB`.
|
||||
|
||||
Проверялся также заголовок:
|
||||
|
||||
```text
|
||||
Range: bytes=0-1023
|
||||
```
|
||||
|
||||
и диапазон из середины файла:
|
||||
|
||||
```text
|
||||
Range: bytes=1048576-1049599
|
||||
```
|
||||
|
||||
Proxy игнорирует `Range`, отвечает `HTTP 200` и снова начинает отдавать весь файл, после чего передача зависает.
|
||||
|
||||
Проверка DNS:
|
||||
|
||||
```text
|
||||
getent hosts tf-registry.containerk8s.services.ngcloud.ru
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
5.172.178.231
|
||||
```
|
||||
|
||||
Также проверялся Kubernetes API:
|
||||
|
||||
```text
|
||||
kubectl get ingress -A
|
||||
kubectl get pods -A
|
||||
kubectl get svc -A
|
||||
kubectl get deploy -A
|
||||
kubectl get configmap -A
|
||||
```
|
||||
|
||||
Но текущий kubeconfig не имеет credentials:
|
||||
|
||||
```text
|
||||
the server has asked for the client to provide credentials
|
||||
Unauthorized
|
||||
```
|
||||
|
||||
Итог: API registry и metadata работают. Проблемный участок:
|
||||
|
||||
```text
|
||||
/v1/proxy -> S3 object -> HTTP streaming через Kubernetes
|
||||
```
|
||||
|
||||
Именно там большой ZIP возвращается с `HTTP 200`, но поток фактически обрывается на десятках килобайт.
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
Read
|
||||
|
||||
Read memory
|
||||
|
||||
Read , lines 1 to 80
|
||||
|
||||
Read , lines 1 to 80
|
||||
|
||||
Read , lines 1 to 80
|
||||
|
||||
Теперь у меня есть контекст по drhider. Давай сравню симптомы:
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
## Анализ: сравнение проблем drhider и registry
|
||||
|
||||
### Проблема в drhider (решена)
|
||||
|
||||
| Аспект | Детали |
|
||||
|--------|--------|
|
||||
| Симптом | POST из браузера → тело запроса обрывалось на ~64КБ (HTTP 000, ~10с) |
|
||||
| Направление | **Входящий** трафик: браузер → кластер |
|
||||
| Корень | Managed-кластер, шлюз ingress обрывает тела входящих запросов >64КБ |
|
||||
| Решение | Браузер PUT файл на ВМ-буфер (обходит шлюз) → Flask тянет с ВМ (egress, без лимита) |
|
||||
|
||||
### Проблема в registry (сейчас)
|
||||
|
||||
| Аспект | Детали |
|
||||
|--------|--------|
|
||||
| Симптом | `GET /v1/proxy?key=...` → HTTP 200 мгновенно, но данные обрываются на 18–23 КБ |
|
||||
| Направление | **Исходящий** трафик: кластер → Terraform клиент |
|
||||
| Корень | Тот же кластерный шлюз, но теперь режет **streaming-ответ** (буферизация / flush-лимит) |
|
||||
|
||||
### Это та же проблема?
|
||||
|
||||
**Да, вероятно одна и та же инфраструктура** (`services.ngcloud.ru` — managed Nubes, тот же managed-кластер что и `pythonk8s.dev.nubes.ru`). Только направление противоположное:
|
||||
- drhider: шлюз режет входящее тело
|
||||
- registry: шлюз режет исходящий stream большого файла
|
||||
|
||||
Дополнительный признак: у вас есть ещё одна задокументированная проблема — HTTP/2 резал git push к gitea (тот же шлюз, тот же паттерн). Range-запросы игнорируются — тоже поведение промежуточного буферизующего прокси.
|
||||
|
||||
---
|
||||
|
||||
### Применим ли обход через ВМ?
|
||||
|
||||
**Да, но схема зеркальная.** В drhider проблема была на входе — данные шли в кластер. Здесь проблема на выходе — данные идут из кластера к Terraform.
|
||||
|
||||
**Варианты обхода:**
|
||||
|
||||
**Вариант A — Redirect на S3 pre-signed URL (чище всего)**
|
||||
|
||||
`/v1/proxy` вместо стриминга генерирует временную pre-signed S3 ссылку и возвращает `302 Location: <s3-presigned-url>`. Terraform следует редиректу и скачивает ZIP напрямую с S3, минуя кластерный шлюз полностью.
|
||||
|
||||
```
|
||||
Terraform → /v1/proxy?key=... → 302 Location: https://s3.../object?X-Amz-Expires=...
|
||||
Terraform → S3 напрямую → 200 + 13MB ✅
|
||||
```
|
||||
|
||||
Terraform `go-getter` (используется в `terraform init`) редиректы поддерживает.
|
||||
|
||||
**Вариант B — Redirect на ВМ-прокси**
|
||||
|
||||
Если S3 не публичный / нет pre-signed URL — `/v1/proxy` отдаёт `302` на эндпоинт ВМ (`contracts.kube5s.ru/tf-proxy?key=...`). ВМ скачивает файл с S3 и стримит Terraform клиенту. ВМ находится вне шлюза managed-кластера.
|
||||
|
||||
**Вариант C — Через download_url в metadata (самое прозрачное)**
|
||||
|
||||
Вместо того чтобы `download_url` указывал на `/v1/proxy` внутри кластера — указывать сразу на S3 pre-signed URL или ВМ-прокси. Не нужно менять `/v1/proxy` вообще.
|
||||
|
||||
---
|
||||
|
||||
### Рекомендация
|
||||
|
||||
Вариант A или C — наиболее чистые. Смотреть, есть ли доступ к S3 с кредами для pre-signed URL из кода registry. Если есть — минимальное изменение: в `/v1/proxy` заменить стриминг на `redirect(presigned_url, 302)`.
|
||||
|
||||
Нужно смотреть код registry (`/v1/proxy` handler) чтобы подтвердить технически — он в этом же репозитории или в отдельном?
|
||||
|
||||
Создано задач: 3
|
||||
|
||||
Список задач обновлен
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Read
|
||||
|
||||
Картина полная. Вот анализ:
|
||||
|
||||
---
|
||||
|
||||
## Диагностика: та же проблема, другая сторона
|
||||
|
||||
**Код проблемы — `proxy.go`, `proxyHandler`:**
|
||||
|
||||
```go
|
||||
obj, _ := s3Client.GetObject(ctx, bucketName, key, ...)
|
||||
io.Copy(w, obj) // ← Kubernetes шлюз режет этот stream
|
||||
```
|
||||
|
||||
Тот же managed-кластер (`containerk8s.services.ngcloud.ru` — Nubes), тот же шлюз. В drhider он резал входящие тела >64KB. Здесь режет исходящий stream с S3 (~18–23KB — это первый буфер ответа, который шлюз успел пропустить до блокировки).
|
||||
|
||||
`Content-Length: 13083236` выставляется, но данных доходит ~20KB — шлюз буферизует и не флашит остаток.
|
||||
|
||||
---
|
||||
|
||||
## Решение: presigned URL redirect
|
||||
|
||||
`minio-go` уже импортирован, он умеет генерировать pre-signed URL:
|
||||
|
||||
```go
|
||||
// вместо GetObject + io.Copy:
|
||||
presigned, err := s3Client.PresignedGetObject(ctx, bucketName, key, 15*time.Minute, url.Values{})
|
||||
http.Redirect(w, r, presigned.String(), http.StatusFound)
|
||||
```
|
||||
|
||||
Terraform следует 302. Terraform client скачивает ZIP **напрямую с S3**, кластерный шлюз больше не участвует.
|
||||
|
||||
Это затрагивает только `proxy.go` — 1 файл, примерно 10 строк замены.
|
||||
|
||||
`ShasumsURL` и `ShasumsSignatureURL` тоже идут через `/v1/proxy` — они маленькие (<10KB), работают и сейчас, но с redirect тоже будут работать корректно.
|
||||
|
||||
---
|
||||
|
||||
## План
|
||||
|
||||
**Файл:** server/proxy.go
|
||||
|
||||
**Изменение:** в `proxyHandler` — убрать `GetObject` / `Stat` / `io.Copy` / установку заголовков, заменить на `PresignedGetObject` + `http.Redirect 302`. Добавить `"net/url"` в imports.
|
||||
|
||||
**Верификация:**
|
||||
1. `go build `nubes`.` — компиляция
|
||||
2. `curl -v 'https://...registry.../v1/proxy?key=...' -L` — должен получить 302, затем скачать 13MB полностью
|
||||
3. `terraform init` в тестовом проекте с этим провайдером
|
||||
|
||||
Делать?
|
||||
|
||||
Создано задач: 4
|
||||
|
||||
НИЧЕГО НЕ ДЕЛАЙ. КРАТКО ОТВЕТ ДАВАЙ - это надо менять код регистри ? и всё ?
|
||||
|
||||
10:03
|
||||
Да. Только server/proxy.go — заменить стриминг на redirect к pre-signed S3 URL. Больше ничего.
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
# План: отдельный сервис документации (Node.js)
|
||||
|
||||
## Целевая архитектура
|
||||
|
||||
```
|
||||
S3 (nubes-terraform-registry) — статика документации, только docs/*
|
||||
Node.js-сервис — раздаёт /docs/* + кеш с TTL
|
||||
registry (Go) — только Terraform-протокол:
|
||||
/.well-known/*, /v1/providers/*, /v1/proxy
|
||||
```
|
||||
|
||||
## Что выносится из registry
|
||||
|
||||
- Маршрут `/docs/` и функция `docsHandler` (удаляются из `server/main.go` и `server/docs.go`).
|
||||
- Провайдерская логика (`discoveryHandler`, `router`, `proxyHandler`, `rootHandler`, `healthz/readyz`) — НЕ меняется.
|
||||
|
||||
## Соглашения для сервиса документации (перенос из бывшего server/docs.go)
|
||||
|
||||
Файл `docs.go` не является уникальным активом; он описывает обычный маппинг
|
||||
URL → S3-ключ. При переносе на Node.js достаточно воспроизвести три правила:
|
||||
|
||||
1. **Формат S3-ключей документации:**
|
||||
```
|
||||
docs/<namespace>/<name>/<version>/<rest>
|
||||
при пустом rest → docs/<namespace>/<name>/<version>/index.html
|
||||
```
|
||||
|
||||
2. **Fallback-цепочка при неизвестном пути:**
|
||||
точный путь → `<путь>/index.html` (если путь похож на каталог) → корневой `index.html`.
|
||||
|
||||
3. **Content-Type:**
|
||||
через `mime.TypeByExtension`; fallback на `text/html; charset=utf-8` для
|
||||
`index.html`/`.html`, иначе `application/octet-stream`.
|
||||
|
||||
4. **Добавить то, чего в сервисе документации будет больше, чем в docs.go:**
|
||||
in-memory кеш с TTL (например, 60–300 сек) и заголовок `Cache-Control`.
|
||||
|
||||
## Endpoint Node.js-сервиса
|
||||
|
||||
```
|
||||
GET /docs/<namespace>/<name>/<version>/<rest>
|
||||
```
|
||||
|
||||
## Переменные окружения (общие с registry)
|
||||
|
||||
- `S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY`
|
||||
(точки доступа — как в `secrets/env.txt`).
|
||||
|
||||
## DNS / Ingress
|
||||
|
||||
- `/docs/*` → Node.js-сервис.
|
||||
- `/.well-known/*`, `/v1/providers/*`, `/v1/proxy` → registry.
|
||||
- Тот же домен `tf-registry.containerk8s.services.ngcloud.ru`, пути разводятся в Ingress.
|
||||
(При отдельном поддомене потребуется переписывать ссылки в HTML.)
|
||||
|
||||
## Порядок работ
|
||||
|
||||
1. Вынести `/docs/` из registry (этот шаг).
|
||||
2. Написать Node.js-сервис (маппинг + кеш + Content-Type).
|
||||
3. Dockerfile + образ в gitea + деплой сервиса.
|
||||
4. Ingress-правило для `/docs/*`.
|
||||
5. Переписать генератор документации под новые соглашения (отдельная задача).
|
||||
6. Smoke-тесты: HTML 200, CSS/JS 200, ссылки валидны.
|
||||
|
||||
## Статус
|
||||
|
||||
- [x] Вынести документацию из registry (v0.0.6)
|
||||
- [ ] Node.js-сервис (отдельная репа)
|
||||
- [ ] Деплой + Ingress
|
||||
- [ ] Переписанный генератор
|
||||
|
||||
---
|
||||
|
||||
# План (принципиальный) — отдельная репа
|
||||
|
||||
Код — в отдельной репе, сюда только итоговый результат.
|
||||
|
||||
## Принципы
|
||||
|
||||
1. **Разделение по ответственности.**
|
||||
- registry — только Terraform-протокол (уже сделано, v0.0.6).
|
||||
- docs-сервис — только раздача статики `/docs/*`.
|
||||
- S3 — только хранилище статики, prefix `docs/`.
|
||||
|
||||
2. **Сервис документации = статический сервер, не приложение.**
|
||||
Никакого рендеринга и шаблонов. Одна функция: URL → S3-ключ → файл.
|
||||
|
||||
3. **Язык — Node.js.**
|
||||
Лёгкий, быстрый, встроенный кеш, тот же S3 SDK. (nginx — запасной вариант, если не нужен код.)
|
||||
|
||||
4. **Кеш обязателен.**
|
||||
in-memory кеш с TTL (60–300 сек) + `Cache-Control`. Это решает исходную проблему «тормозит и кеширует».
|
||||
|
||||
5. **Один домен, пути разводятся в Ingress.**
|
||||
- `/docs/*` → docs-сервис
|
||||
- `/.well-known/*`, `/v1/providers/*`, `/v1/proxy` → registry
|
||||
Отдельный поддомен не нужен — тогда не придётся переписывать ссылки в HTML.
|
||||
|
||||
6. **Доступ к S3 — те же credentials, но только к `docs/*`.**
|
||||
Ключ доступа с правом чтения `docs/`, без доступа к провайдерским артефактам.
|
||||
|
||||
## Соглашения из бывшего docs.go (перенести на Node)
|
||||
|
||||
- S3-ключ: `docs/<ns>/<name>/<version>/<rest>`; пустой `rest` → `index.html`.
|
||||
- Fallback: точный путь → `<путь>/index.html` → корневой `index.html`.
|
||||
- Content-Type по расширению; `index.html` → `text/html; charset=utf-8`.
|
||||
|
||||
## Минимальный объём сервиса
|
||||
|
||||
- `GET /docs/:ns/:name/:version/*` → S3-ключ → отдать файл + кеш.
|
||||
- Dockerfile → образ в gitea → деплой.
|
||||
- Ingress-правило для `/docs/*`.
|
||||
|
||||
## Что отдаётся обратно в эту репу
|
||||
|
||||
- Только ссылка на репу + итоговый Dockerfile/конфиг, если договоримся держать деплой-артефакты здесь.
|
||||
- Сам код docs-сервиса — в отдельной репе.
|
||||
|
||||
## Генератор — отдельная задача (НЕ входит в этот план)
|
||||
|
||||
- Переписывается отдельно, чтобы ссылки на ассеты были корректны.
|
||||
- К docs-сервису относится только как «потребитель» результата.
|
||||
@@ -0,0 +1,55 @@
|
||||
# Переменные инстанса и конфигурация
|
||||
|
||||
## Принцип
|
||||
|
||||
- **jsonEnv** — только секреты. Задаются при создании инстанса, НЕ меняются (modify не поддерживает).
|
||||
- **Dockerfile ENV** — всё несекретное. Меняются через правку Dockerfile → rebuild → push → redeploy.
|
||||
- **Go-код** — `os.Getenv()` с fallback'ами, без хардкода.
|
||||
|
||||
## jsonEnv (создание инстанса)
|
||||
|
||||
```json
|
||||
{
|
||||
"S3_ACCESS_KEY": "...",
|
||||
"S3_SECRET_KEY": "..."
|
||||
}
|
||||
```
|
||||
|
||||
| Переменная | Назначение |
|
||||
|---|---|
|
||||
| `S3_ACCESS_KEY` | Доступ к S3 |
|
||||
| `S3_SECRET_KEY` | Доступ к S3 |
|
||||
|
||||
## Dockerfile ENV (rebuild)
|
||||
|
||||
```dockerfile
|
||||
ENV S3_ENDPOINT=s3.msk-1.ngcloud.ru \
|
||||
S3_BUCKET=terraform-registry \
|
||||
S3_PREFIX=go-registry.containerk8s.dev.nubes.ru \
|
||||
REGISTRY_HOSTNAME=go-registry.containerk8s.dev.nubes.ru \
|
||||
PORT=5000
|
||||
```
|
||||
|
||||
| Переменная | Назначение | Пример |
|
||||
|---|---|---|
|
||||
| `S3_ENDPOINT` | Адрес S3 | `s3.msk-1.ngcloud.ru` |
|
||||
| `S3_BUCKET` | Имя бакета | `terraform-registry` |
|
||||
| `S3_PREFIX` | Префикс S3-ключей | `go-registry.containerk8s.dev.nubes.ru` |
|
||||
| `REGISTRY_HOSTNAME` | Домен реестра (для download_url) | `go-registry.containerk8s.dev.nubes.ru` |
|
||||
| `PORT` | Порт HTTP | `5000` |
|
||||
|
||||
## При создании нового инстанса (другой домен/стенд)
|
||||
|
||||
1. В Dockerfile поменять `S3_PREFIX`, `REGISTRY_HOSTNAME` под новый домен
|
||||
2. Поменять `S3_ENDPOINT`, `S3_BUCKET` если S3 другой
|
||||
3. `docker build && docker push`
|
||||
4. Создать инстанс: `registryPath` → образ, `jsonEnv` → ключи S3
|
||||
|
||||
## Смена окружения
|
||||
|
||||
Не менять инстанс — создать новый с другими параметрами. Старый удалить.
|
||||
|
||||
| Окружение | Домен | S3_PREFIX |
|
||||
|---|---|---|
|
||||
| Тест | `go-registry.containerk8s.dev.nubes.ru` | `go-registry.containerk8s.dev.nubes.ru` |
|
||||
| Прод | (будет отдельно) | (будет отдельно) |
|
||||
@@ -0,0 +1,32 @@
|
||||
# Инцидент: `tf-registry.containerk8s.services.ngcloud.ru` отдаёт 503
|
||||
|
||||
## Зафиксированные факты
|
||||
|
||||
- `tf-registry.containerk8s.services.ngcloud.ru` резолвится в `5.172.178.231`.
|
||||
- Запросы `GET /healthz` и `GET /readyz` возвращают `HTTP/2 503` с телом nginx `503 Service Temporarily Unavailable`.
|
||||
- Прямое подключение к `5.172.178.231:5000` не устанавливается (порт backend наружу не публикуется).
|
||||
- VM `5.172.178.213` доступна, но это ВМ для сборки — managed-контейнер к ней отношения не имеет.
|
||||
- Следовательно, запрос не доходит до Go-сервера: это недоступный или не запущенный upstream, а не ошибка обработчиков `/healthz`, `/readyz` или S3 readiness.
|
||||
|
||||
## Диагностика образа (подтверждённые факты)
|
||||
|
||||
- Docker Registry Gitea (`gitea.services.ngcloud.ru/nail/tf_registry`) был **пуст**: `tags=` (тегов нет).
|
||||
- `docker manifest inspect ...:latest` → `manifest unknown`; `:0.0.2` → `manifest unknown`.
|
||||
- С ВМ `5.172.178.213` доступа до Gitea **нет**: TCP `194.31.9.41:443` — `SYN-SENT`, DNS `lookup gitea.services.ngcloud.ru` — `i/o timeout`, traceroute обрывается на `100.64.24.24`. Общий интернет с ВМ работает (docker hub доступен).
|
||||
- Локальная машина (Krupski) имеет доступ до Gitea напрямую; `docker login gitea.services.ngcloud.ru --username Naeel` → `Login Succeeded`.
|
||||
- Образ собран на ВМ из текущего `master` (rsync), затем перенесён локально через `docker save`/`scp`/`load`.
|
||||
|
||||
## Push образа 0.0.1 (выполнено)
|
||||
|
||||
- `docker push gitea.services.ngcloud.ru/nail/tf_registry:0.0.1` (с локальной машины) — успешно.
|
||||
- Digest: `sha256:9a00836c916e2cae597e31dff2184a08550bd1be262bdba980d17de2943aa4a7`
|
||||
- Проверка: `docker pull gitea.services.ngcloud.ru/nail/tf_registry:0.0.1` → `PULL_OK`.
|
||||
- ID образа: `sha256:3c76e53a7213d0404932aac6b8ad0dc0c254b35f14d6d3d420a18ba4ec07b0d3`
|
||||
|
||||
## Требуемое восстановление
|
||||
|
||||
1. В Nubes Cloud открыть инстанс с доменом `tf-registry.containerk8s.services.ngcloud.ru` и проверить состояние deployment/реплик и событий запуска.
|
||||
2. Обновить `registryPath` на `gitea.services.ngcloud.ru/nail/tf_registry:0.0.1` (образ с этим тегом уже опубликован).
|
||||
3. Проверить, что контейнер слушает `PORT=5000`, а healthcheck использует доступный путь.
|
||||
4. Выполнить redeploy через UI `modify`.
|
||||
5. После запуска проверить `https://tf-registry.containerk8s.services.ngcloud.ru/healthz` и `/.well-known/terraform.json`.
|
||||
@@ -0,0 +1,64 @@
|
||||
# Инцидент 2026-09-01: обрыв скачивания провайдера + медленный DNS на ВМ
|
||||
|
||||
## Симптом (исходный)
|
||||
|
||||
- `GET /v1/proxy?key=...` отдавал `HTTP 200` + `Content-Length: 13083236`, но поток обрывался на ~18–23 KB (curl `(28) Operation timed out`).
|
||||
- `Range`-запросы игнорировались — сервер заново отдавал весь файл и зависал.
|
||||
- Гипотеза: кластерный ingress-шлюз режет исходящий streaming (по аналогии с drhider, где резались входящие тела >64 KB).
|
||||
|
||||
## Диагностика
|
||||
|
||||
- `s3.msk-1.ngcloud.ru` публично доступен: DNS `185.228.248.165/166/167`, `HTTP 200` (Ceph Object Gateway).
|
||||
- `gitea.services.ngcloud.ru` НЕ доступен с ВМ сборки `5.172.178.213`:
|
||||
- публичный IP `194.31.9.41:443` — TCP таймаут;
|
||||
- внутренний ClusterIP `10.96.52.11:443` (`gitea.mgmt.nubes.ru`) — TCP таймаут (подсеть `10.96.0.0/16` с ВМ не маршрутизируется);
|
||||
- с локальной машины gitea доступен (`200` за 0.36s). → «в одном облаке» ≠ открыто: сети изолированы security-group/маршрутизацией.
|
||||
|
||||
## Причина обрыва — НЕ кластер, а локальный прокси
|
||||
|
||||
- На локальной машине задан `HTTP_PROXY/HTTPS_PROXY=http://172.17.192.1:10808` (v2ray/clash-тип).
|
||||
- Через прокси режется любой большой исходящий поток (~16–23 KB) — и старый стриминг через кластер, и прямое скачивание с S3.
|
||||
- Без прокси (`curl --noproxy '*'`) файл качается полностью: 13 083 236 байт за 6.3s, `unzip -t` — OK.
|
||||
- С ВМ (без прокси) тот же файл: 13 083 236 байт за 11.3s, `unzip -t` — OK.
|
||||
- **Вывод:** исходная гипотеза «кластерный шлюз режет» не подтвердилась.
|
||||
|
||||
## Фикс registry: redirect на pre-signed S3 URL (выполнено)
|
||||
|
||||
- `server/proxy.go`, `proxyHandler`: убраны `GetObject`/`Stat`/`io.Copy`; вместо этого
|
||||
`PresignedGetObject(ctx, bucketName, key, 15*time.Minute, nil)` + `http.Redirect(w, r, ..., http.StatusFound)`.
|
||||
- Удалён неиспользуемый импорт `s3`.
|
||||
- `server/main.go`: `VERSION 0.0.2 → 0.0.3`.
|
||||
- Проверка: `go build ./...` — OK.
|
||||
- Коммит `422736f`, запушен в `master`.
|
||||
- Образ: `docker build` (локально, т.к. gitea недоступен с ВМ) → `gitea.services.ngcloud.ru/nail/tf_registry:latest` и `:0.0.3`.
|
||||
- Digest: `sha256:e3f8510d521025f4980c65226507f31aa9f4defe68e3bc50862591992792bda5`.
|
||||
- Инстанс пересоздан в UI Nubes с образом `:0.0.3`.
|
||||
|
||||
### Проверка фикса
|
||||
|
||||
- `GET /v1/proxy?key=...` → `HTTP 302` + `Location: https://s3.msk-1.ngcloud.ru/...?X-Amz-...` (presigned, креды reader `6DDP5...`).
|
||||
- `curl -L` — 13 083 236 байт за 6.3s (без прокси), архив валиден.
|
||||
|
||||
## Проблема terraform init на ВМ: медленный DNS
|
||||
|
||||
- `terraform init` на ВМ падал: `failed to retrieve authentication checksums ... context deadline exceeded` (2 попытки).
|
||||
- Причина: DNS на ВМ. `resolv.conf` = `8.8.8.8` + `1.1.1.1`, оба **недоступны** с ВМ
|
||||
(`nslookup` → `communications error: timed out`).
|
||||
- Каждый резолв доменов Nubes висел ~10 сек: `dns=10.6s` (registry), `dns=10.0s` (s3).
|
||||
- Terraform делает много резолвов → суммарный таймаут. Сама передача файлов работала нормально.
|
||||
|
||||
### Решение: статические записи в /etc/hosts на ВМ
|
||||
|
||||
- Добавлено (бэкап: `/etc/hosts.bak`):
|
||||
- `5.172.178.231 tf-registry.containerk8s.services.ngcloud.ru`
|
||||
- `185.228.248.165 s3.msk-1.ngcloud.ru`
|
||||
- После правки резолв мгновенный: `dns=0.0003s`, registry `total=0.022s`.
|
||||
- `terraform init` в `~/tf_provider/TEST_STAND/PGwNewRegistry` — **успех**:
|
||||
`Installed ... nubes v5.0.5 (self-signed, key ID CB3A0DF161ECC416)`, `Terraform has been successfully initialized!`.
|
||||
|
||||
## Итоговые выводы
|
||||
|
||||
1. Обрыв больших файлов на ~16–23 KB — локальный прокси на машине тестирования, НЕ кластер.
|
||||
2. Фикс registry (302 → presigned S3) корректен и полезен: registry больше не участвует в передаче файла.
|
||||
3. `terraform init` с ВМ ломался из-за недоступного внешнего DNS; лечится `/etc/hosts` (или рабочим внутренним DNS).
|
||||
4. Gitea с ВМ сборки недоступен по сети — для сборки образа использовать машину с доступом (локальную).
|
||||
@@ -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` |
|
||||
@@ -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/terraform/tf_registry:latest .
|
||||
docker push gitea.services.ngcloud.ru/terraform/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 (до правок) |
|
||||
@@ -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 или переменные окружения.
|
||||
@@ -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
|
||||
@@ -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!"
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -1,7 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: {{ .Values.global.namespace }}
|
||||
labels:
|
||||
name: {{ .Values.global.namespace }}
|
||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -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 }}
|
||||
@@ -1,6 +0,0 @@
|
||||
global:
|
||||
registryHostname: "terra.k8c.ru"
|
||||
registry:
|
||||
replicas: 1
|
||||
pdb:
|
||||
enabled: false
|
||||
@@ -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
|
||||
@@ -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 при миграции
|
||||
@@ -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; я ничего не меняю в кластере автоматически.
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- deployment.yaml
|
||||
- service.yaml
|
||||
- ingress.yaml
|
||||
@@ -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
|
||||
@@ -1,10 +0,0 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: my-deployment
|
||||
spec:
|
||||
template:
|
||||
spec:
|
||||
containers:
|
||||
- name: my-container
|
||||
image: nginx:stable
|
||||
@@ -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:
|
||||
- registry.containerk8s.dev.nubes.ru
|
||||
secretName: registry-containerk8s-tls
|
||||
rules:
|
||||
- host: registry.containerk8s.dev.nubes.ru
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: my-service
|
||||
port:
|
||||
number: 80
|
||||
@@ -1,8 +0,0 @@
|
||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||
kind: Kustomization
|
||||
resources:
|
||||
- ../../base
|
||||
namespace: terra-dev
|
||||
patchesStrategicMerge:
|
||||
- ingress-patch.yaml
|
||||
- image-patch.yaml
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -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 мог скачивать провайдеры по обычному источнику.
|
||||
@@ -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"]
|
||||
@@ -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"]
|
||||
@@ -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)
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
package main
|
||||
|
||||
import registrykeys "terraform-registry-keys"
|
||||
|
||||
var gpgPublicKey = GPGPublicKey{
|
||||
KeyID: registrykeys.KeyID,
|
||||
ASCIIArmor: registrykeys.ASCIIArmor,
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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=
|
||||
@@ -1,6 +0,0 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: terra
|
||||
labels:
|
||||
name: terra
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,5 +0,0 @@
|
||||
module terraform-registry-keys
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.12
|
||||
@@ -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-----
|
||||
`
|
||||
@@ -1,5 +0,0 @@
|
||||
module terraform-registry-keys
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.12
|
||||
@@ -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-----`
|
||||
@@ -1,2 +0,0 @@
|
||||
flask
|
||||
boto3
|
||||
@@ -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."
|
||||
+11
-12
@@ -29,23 +29,22 @@ set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="${ROOT_DIR:-$(cd "${SCRIPT_DIR}/.." && pwd)}"
|
||||
PROVIDER_DIR="${PROVIDER_DIR:-${ROOT_DIR}/universal_rebuild}"
|
||||
PROVIDER_DIR="${PROVIDER_DIR:-${HOME}/tf_provider/provider}"
|
||||
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/artifacts/provider_build}"
|
||||
GPG_KEY_FILE="${GPG_KEY_FILE:-${ROOT_DIR}/secrets/private_key.asc}"
|
||||
GPG_KEY_FILE="${GPG_KEY_FILE:-${HOME}/tf_provider/secrets/private_key.asc}"
|
||||
|
||||
VERSION="${1:-2.0.2}"
|
||||
REGISTRY_HOSTNAME="${REGISTRY_HOSTNAME:-terra.k8c.ru}"
|
||||
NAMESPACE="${NAMESPACE:-nubes}"
|
||||
REGISTRY_HOSTNAME="${REGISTRY_HOSTNAME:-tf-registry.containerk8s.services.ngcloud.ru}"
|
||||
NAMESPACE="${NAMESPACE:-nubes-test}"
|
||||
PROVIDER_NAME="${PROVIDER_NAME:-nubes}"
|
||||
S3_BUCKET="${S3_BUCKET:-terraform-registry}"
|
||||
S3_BUCKET="${S3_BUCKET:-nubes-terraform-registry}"
|
||||
|
||||
S3_ENDPOINT="${S3_ENDPOINT:-}"
|
||||
S3_ENDPOINT="${S3_ENDPOINT:-https://s3.msk-1.ngcloud.ru}"
|
||||
S3_ACCESS_KEY="${S3_ACCESS_KEY:-}"
|
||||
S3_SECRET_KEY="${S3_SECRET_KEY:-}"
|
||||
|
||||
if [[ -z "$S3_ENDPOINT" || -z "$S3_ACCESS_KEY" || -z "$S3_SECRET_KEY" ]]; then
|
||||
echo "Error: S3_ENDPOINT/S3_ACCESS_KEY/S3_SECRET_KEY must be set" >&2
|
||||
echo "Hint: see ${ROOT_DIR}/secrets/.s3cfg_registry" >&2
|
||||
if [[ -z "$S3_ACCESS_KEY" || -z "$S3_SECRET_KEY" ]]; then
|
||||
echo "Error: S3_ACCESS_KEY/S3_SECRET_KEY must be set" >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
@@ -77,7 +76,7 @@ build_and_zip() {
|
||||
fi
|
||||
|
||||
echo "Building for ${os}/${arch}..."
|
||||
(cd "$PROVIDER_DIR" && CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -o "${BUILD_DIR}/${binary}" .)
|
||||
(cd "$PROVIDER_DIR" && CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -ldflags "-X main.address=${REGISTRY_HOSTNAME}/${NAMESPACE}/${PROVIDER_NAME} -X main.version=${VERSION}" -o "${BUILD_DIR}/${binary}" .)
|
||||
|
||||
(cd "$BUILD_DIR" && python3 -m zipfile -c "terraform-provider-nubes_${VERSION}_${os}_${arch}.zip" "$binary")
|
||||
rm -f "${BUILD_DIR:?}/${binary}"
|
||||
@@ -104,8 +103,8 @@ gpg --batch --yes --detach-sign --default-key "$key_id" \
|
||||
--output "${BUILD_DIR}/terraform-provider-nubes_${VERSION}_SHA256SUMS.sig" \
|
||||
"${BUILD_DIR}/terraform-provider-nubes_${VERSION}_SHA256SUMS"
|
||||
|
||||
mc alias set registry "$s3_endpoint_url" "$S3_ACCESS_KEY" "$S3_SECRET_KEY" --api S3v4
|
||||
S3_PATH="registry/${S3_BUCKET}/${REGISTRY_HOSTNAME}/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}/"
|
||||
mc alias set prod-s3 "$s3_endpoint_url" "$S3_ACCESS_KEY" "$S3_SECRET_KEY" --api S3v4
|
||||
S3_PATH="prod-s3/${S3_BUCKET}/${REGISTRY_HOSTNAME}/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}/"
|
||||
|
||||
echo "Uploading to: ${S3_PATH}"
|
||||
mc cp "${BUILD_DIR}"/*.zip "$S3_PATH"
|
||||
|
||||
-116
@@ -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)
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
package main
|
||||
|
||||
import "embed"
|
||||
|
||||
//go:embed templates/index.html
|
||||
var templateFS embed.FS
|
||||
@@ -0,0 +1,26 @@
|
||||
module tf-registry
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require github.com/minio/minio-go/v7 v7.2.1
|
||||
|
||||
require (
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.6 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // 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/rs/xid v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.51.0 // indirect
|
||||
golang.org/x/net v0.53.0 // indirect
|
||||
golang.org/x/sys v0.44.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
gopkg.in/ini.v1 v1.67.2 // indirect
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
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/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.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||
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/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.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
|
||||
github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
|
||||
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/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
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/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||
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.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss=
|
||||
gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
+125
-4
@@ -1,8 +1,129 @@
|
||||
package main
|
||||
|
||||
import registrykeys "terraform-registry-keys"
|
||||
// GPG public keys from live deployment (two keys: primary + legacy).
|
||||
// Primary key is used by the current operator-built binary.
|
||||
|
||||
var gpgPublicKey = GPGPublicKey{
|
||||
KeyID: registrykeys.KeyID,
|
||||
ASCIIArmor: registrykeys.ASCIIArmor,
|
||||
var gpgPrimaryKey = GPGPublicKey{
|
||||
KeyID: "3EC4673EB798238A",
|
||||
ASCIIArmor: `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
6cZ0NQ==
|
||||
=3Ea2
|
||||
-----END PGP PUBLIC KEY BLOCK-----`,
|
||||
}
|
||||
|
||||
var gpgLegacyKey = GPGPublicKey{
|
||||
KeyID: "CB3A0DF161ECC416",
|
||||
ASCIIArmor: `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
=q8Tw
|
||||
-----END PGP PUBLIC KEY BLOCK-----`,
|
||||
}
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
Executable
+25
@@ -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 |
+37
-236
@@ -1,74 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"embed"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
s3 "github.com/minio/minio-go/v7"
|
||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||
)
|
||||
|
||||
//go:embed logo.svg
|
||||
var logoFile embed.FS
|
||||
|
||||
var (
|
||||
s3Client *s3.Client
|
||||
bucketName = "terraform-registry" // Default
|
||||
bucketName = "terraform-registry"
|
||||
hostname = os.Getenv("REGISTRY_HOSTNAME")
|
||||
s3Prefix = os.Getenv("S3_PREFIX")
|
||||
)
|
||||
|
||||
// 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"`
|
||||
}
|
||||
const VERSION = "0.0.6"
|
||||
|
||||
func main() {
|
||||
// 1. Init S3 Connection
|
||||
if hostname == "" {
|
||||
hostname = "localhost:8080"
|
||||
hostname = "localhost:5000"
|
||||
}
|
||||
if s3Prefix == "" {
|
||||
s3Prefix = hostname
|
||||
}
|
||||
|
||||
endpoint := os.Getenv("S3_ENDPOINT")
|
||||
@@ -80,209 +43,47 @@ func main() {
|
||||
}
|
||||
|
||||
var err error
|
||||
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{
|
||||
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
|
||||
Secure: true, // Force secure for cloud S3
|
||||
Secure: useSSL,
|
||||
})
|
||||
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("/v1/providers/", router)
|
||||
http.HandleFunc("/v1/proxy", proxyHandler)
|
||||
http.HandleFunc("/docs/", docsHandler)
|
||||
http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(logoFile))))
|
||||
http.Handle("/", http.HandlerFunc(rootHandler))
|
||||
http.HandleFunc("/healthz", healthzHandler)
|
||||
http.HandleFunc("/readyz", readyzHandler)
|
||||
|
||||
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
|
||||
port := os.Getenv("PORT")
|
||||
if port == "" {
|
||||
port = "5000"
|
||||
}
|
||||
|
||||
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()
|
||||
srv := &http.Server{Addr: ":" + port}
|
||||
|
||||
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
|
||||
go func() {
|
||||
log.Printf("Starting Registry Service on :%s (Bucket: %s, Endpoint: %s)\n", port, bucketName, endpoint)
|
||||
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||
log.Fatalf("HTTP server error: %v", err)
|
||||
}
|
||||
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{},
|
||||
}
|
||||
}
|
||||
quit := make(chan os.Signal, 1)
|
||||
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||
<-quit
|
||||
log.Println("Shutting down server...")
|
||||
|
||||
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"})
|
||||
}
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("Server forced to shutdown: %v", err)
|
||||
}
|
||||
|
||||
for _, v := range seenVersions {
|
||||
versions = append(versions, *v)
|
||||
}
|
||||
|
||||
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", 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)
|
||||
}
|
||||
|
||||
func healthzHandler(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func readyzHandler(w http.ResponseWriter, r *http.Request) {
|
||||
// Проверить доступность S3
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte("ok"))
|
||||
log.Println("Server stopped")
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
key := r.URL.Query().Get("key")
|
||||
|
||||
if key == "" {
|
||||
http.Error(w, "Missing key param", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
if !strings.HasPrefix(key, s3Prefix+"/") && !strings.HasPrefix(key, "docs/") {
|
||||
http.Error(w, "Forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
|
||||
presigned, err := s3Client.PresignedGetObject(r.Context(), bucketName, key, 15*time.Minute, nil)
|
||||
if err != nil {
|
||||
http.Error(w, "Failed to sign object", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
http.Redirect(w, r, presigned.String(), http.StatusFound)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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>
|
||||
-411
@@ -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", "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)
|
||||
Reference in New Issue
Block a user