init: registry server + operator from VM
This commit is contained in:
@@ -0,0 +1,10 @@
|
|||||||
|
# Правила
|
||||||
|
|
||||||
|
> Подробные правила: [`.github/pravila.md`](pravila.md)
|
||||||
|
|
||||||
|
1. Не трогать рабочий код без явного указания.
|
||||||
|
2. Файлы редактировать локально — `~/remote_dev/` = `~/terra/` на ВМ (sshfs), SCP не нужен.
|
||||||
|
3. Все команды — **только через SSH**, никогда локально:
|
||||||
|
```bash
|
||||||
|
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА'
|
||||||
|
```
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Правила работы агента
|
||||||
|
|
||||||
|
## Файловая система
|
||||||
|
|
||||||
|
`~/remote_dev/` (локально) примонтирован через sshfs к `~/terra/` на ВМ — **одна ФС**.
|
||||||
|
Файлы, сохранённые локально, мгновенно видны на ВМ. SCP не нужен.
|
||||||
|
|
||||||
|
Монтирование может слетать. Признак: файлы рассинхронизированы.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Размонтировать
|
||||||
|
fusermount -u ~/remote_dev
|
||||||
|
# Если завис: sudo umount -l /home/naeel/remote_dev
|
||||||
|
|
||||||
|
# Примонтировать
|
||||||
|
sshfs naeel@5.172.178.213:/home/naeel/terra ~/remote_dev \
|
||||||
|
-o cache=no -o no_readahead -o reconnect \
|
||||||
|
-o ServerAliveInterval=15 -o ServerAliveCountMax=3 \
|
||||||
|
-o IdentityFile=~/.ssh/naeel_vm_id_ed25519
|
||||||
|
```
|
||||||
|
|
||||||
|
## SSH
|
||||||
|
|
||||||
|
Все команды — только через SSH на ВМ. Локально — только читать и редактировать файлы.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
ssh -i ~/.ssh/naeel_vm_id_ed25519 -o StrictHostKeyChecking=no -o ConnectTimeout=10 naeel@5.172.178.213 'КОМАНДА'
|
||||||
|
```
|
||||||
|
|
||||||
|
Запрещено локально: `go`, `docker`, `kubectl`, `helm`, `terraform`, `curl/wget`, `git push/pull`, любые скрипты проекта.
|
||||||
|
|
||||||
|
## Документация
|
||||||
|
|
||||||
|
- `doc/thinking/` — лог рассуждений агента (обязательно)
|
||||||
|
- `doc/progress.md` — трекер задач
|
||||||
|
- Старые файлы `doc/` не перезаписывать — новое в новых файлах с датой
|
||||||
|
|
||||||
|
## Git
|
||||||
|
|
||||||
|
Коммитить и пушить через SSH после каждого завершённого этапа.
|
||||||
|
|
||||||
|
Версионирование тегами: `vMAJOR.MINOR.PATCH`
|
||||||
|
- Patch — любое изменение кода
|
||||||
|
- Minor — новая фича / компонент
|
||||||
|
- Major — breaking change
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git tag vX.Y.Z && git push origin vX.Y.Z
|
||||||
|
```
|
||||||
|
|
||||||
|
## Поведение агента
|
||||||
|
|
||||||
|
- Не трогать рабочий код без явного указания
|
||||||
|
- Не делать ничего сверх того, о чём явно попросили
|
||||||
|
- Деструктивные операции (`kubectl delete`, `rm -rf`, `terraform destroy` и др.) — только после явного подтверждения с указанием конкретных объектов
|
||||||
|
- Отвечать кратко, без вступлений, извинений, благодарностей и прочей воды
|
||||||
+76
@@ -0,0 +1,76 @@
|
|||||||
|
# ── Go ─────────────────────────────────────────────────────────────────────────
|
||||||
|
bin/
|
||||||
|
dist/
|
||||||
|
*.exe
|
||||||
|
*.exe~
|
||||||
|
*.dll
|
||||||
|
*.so
|
||||||
|
*.dylib
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
vendor/
|
||||||
|
go.sum.bak
|
||||||
|
|
||||||
|
# Coverage / profiling
|
||||||
|
coverage.out
|
||||||
|
coverage.html
|
||||||
|
*.prof
|
||||||
|
*.pprof
|
||||||
|
|
||||||
|
# ── Helm ───────────────────────────────────────────────────────────────────────
|
||||||
|
charts/*.tgz
|
||||||
|
charts/Chart.lock
|
||||||
|
charts/charts/
|
||||||
|
|
||||||
|
# ── Kubernetes / secrets ───────────────────────────────────────────────────────
|
||||||
|
*secret*.yaml
|
||||||
|
*secret*.yml
|
||||||
|
*secrets*.yaml
|
||||||
|
*secrets*.yml
|
||||||
|
kubeconfig
|
||||||
|
kubeconfig.*
|
||||||
|
*.kubeconfig
|
||||||
|
.kube/
|
||||||
|
|
||||||
|
# ── GPG / crypto keys ──────────────────────────────────────────────────────────
|
||||||
|
*.asc
|
||||||
|
*.gpg
|
||||||
|
*.pgp
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.p12
|
||||||
|
*.pfx
|
||||||
|
server/gpg/
|
||||||
|
registrykeys/*.key
|
||||||
|
private_key*
|
||||||
|
|
||||||
|
# ── Build artefacts ────────────────────────────────────────────────────────────
|
||||||
|
artifacts/
|
||||||
|
build/output/
|
||||||
|
*.har
|
||||||
|
HAR/
|
||||||
|
|
||||||
|
# ── IDE ────────────────────────────────────────────────────────────────────────
|
||||||
|
.idea/
|
||||||
|
*.iml
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
|
||||||
|
# ── OS ─────────────────────────────────────────────────────────────────────────
|
||||||
|
.DS_Store
|
||||||
|
.DS_Store?
|
||||||
|
._*
|
||||||
|
.Spotlight-V100
|
||||||
|
.Trashes
|
||||||
|
Thumbs.db
|
||||||
|
ehthumbs.db
|
||||||
|
Desktop.ini
|
||||||
|
|
||||||
|
# ── Misc ───────────────────────────────────────────────────────────────────────
|
||||||
|
*.log
|
||||||
|
*.tmp
|
||||||
|
.env
|
||||||
|
.env.*
|
||||||
|
!.env.example
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM2akNDQWRLZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJMk1EUXdOekE0TURRME0xb1hEVE0yTURRd05EQTRNRGswTTFvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBTUZECm5ZQjRQaHBxQWZ2cU9uTlMzYXp2b1hzc29uRTlYTHQ0Z2xXM216RHpEWHU4NVZlRGp2a2phUHRnTFcvaXhnN2wKKzN3VDZ0YXpHUkxSNDNrcE00ZHA2czNXYkh5blVuZHRMVHhNaVBrbXZNMk0rVVdUaHpCTGc1VHRHUXhKaTRvVwpnZTNMTy9lS1RYNjhYTTl0Y3lsc0JoM2ZLTFVaL3c3OHNaelM0QVB4WWh4aVV4My9YcnM0YVNTdXk4aS9OWGVpCnIySlBWMnI5VzM4OEhPQzRnM0xWM2RLeEdKdW14dVdXcC9wRTUvWmo2Rk9XRExGTFBnV1Z2T3pTRFkvN2RtSHIKQjZ5REFzd1VpM01ob091ckVvSlNZYTNib3FJL0JGZklFSW9sNFVETlVqYVMvTnMrUks5MWpyU1dsOHVEMmhWSgp5WXFPc2dnZklKTi9xTWNsS0k4Q0F3RUFBYU5GTUVNd0RnWURWUjBQQVFIL0JBUURBZ0trTUJJR0ExVWRFd0VCCi93UUlNQVlCQWY4Q0FRQXdIUVlEVlIwT0JCWUVGSTZHUnBYTVkyMjJ6d1ltT1lNWXN5TmhLVm5ITUEwR0NTcUcKU0liM0RRRUJDd1VBQTRJQkFRQ1ZiK2kwa1hrRjBwSlBiREJCdXJHc0licFFyMFBuL2EvUWtKRVlmbGVqS0c2WAozL2d0d3dOL0lxY1NMRzMyU2JBbHZ6c2xrbmY1c1pBQmhibWJRWHIvQ2JybjRiZWVxTExyYUxQS3JuUVd2ZVZHCkN1a0ViV2dKU3EyS1BIaStKS0JPZ0RPT1FGa1ZZZlR3ek5JYjI5eTRqU1o1R3dRL0xiZU1NaEQ0ZUV6c1lYS0QKWWF6S0JmaGZsZWVKT0VDZWlWTXMrUDRPMC81TEV1ZVNWVkZwYndIeCtRSncyVDlZUGJ1dEkvMGoxQjNKT3lxbQptRG9JN1RIM3NWdVorNTRHT3kwRXlhQUdiQmVETmduQlRxM0djdUJBbjJSNDErMzJzdHk5V0lIb1RvNnhZZ0lsCjBUaVI5N3lDZ25rejZlNFE3UTFCT29Ib0JFNmVFNk1YSkJNUFJvTHEKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
|
||||||
|
server: https://185.247.187.149:6443
|
||||||
|
name: iot-naeel
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: iot-naeel
|
||||||
|
user: tazetdinovn@gmail.com
|
||||||
|
name: tazetdinovn@gmail.com@iot-naeel
|
||||||
|
current-context: tazetdinovn@gmail.com@iot-naeel
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
||||||
|
users:
|
||||||
|
- name: tazetdinovn@gmail.com
|
||||||
|
user:
|
||||||
|
token: eyJhbGciOiJSUzI1NiIsImtpZCI6InNoIiwidHlwIjoiSldUIn0.eyJhdWQiOiJiYWNrZW5kIiwiZXhwIjoxNzc1OTAwNzI4LCJqdGkiOiIwOWMwZjJmOC02MDcyLTRkMTctOWEzNC1lZTQ4MzMzMjE3ODUiLCJpYXQiOjE3NzU4MTQzMjgsImlzcyI6Imh0dHBzOi8vYXV0aC5rOHMubmdjbG91ZC5ydSIsInN1YiI6InRhemV0ZGlub3ZuQGdtYWlsLmNvbSIsImNsYWltcyI6WyIiLCIiLCJ3ejAzNzA5LXNodHVydmFsLWFkbWluLWlvdC1uYWVlbCIsInd6MDM3MDktc2h0dXJ2YWwtYWRtaW4tbmFlZWwtdGVzdC0zIl0sIm5hbWUiOiLQndCw0LjQu9GMINCi0LDQt9C10YLQtNC40L3QvtCyIiwiZW1haWwiOiJ0YXpldGRpbm92bkBnbWFpbC5jb20iLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0YXpldGRpbm92bkBnbWFpbC5jb20ifQ.v14knWeokzb7-MegHj2XY8X20Z9eEcEhQA2MBBZNTX_eHtctC3lOAk7RU8VgBkyr_yX-kpaw9gW1k0GPmDoOZg-sH8brmjfHTIahEUN0ui3cUOq-KN7yTAof3ZfZWVRaVvScDD_cDI90NFEeNhO5uPZs7zvVSpKflKQQJlZNbH69gjdRvScpDp0m0BTuhRxMoYyVhTxP2RfJdYcT6179xyfbevUQkC8bkX1fOXrnD7mkLXpXG0v0ClYvFaq8VNY9_MejhEoA4P01lEMbl8NkDw04JhhSjHJ2ZXViwswYsit5T8JYezhFrKQ1HeDnkRkrh5fwsoii_9Lm4PHUGPDYEw
|
||||||
+18
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
insecure-skip-tls-verify: true
|
||||||
|
server: https://5.172.178.181:6443
|
||||||
|
name: wheel
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: wheel
|
||||||
|
user: tazet@narod.ru
|
||||||
|
name: tazet@narod.ru@wheel
|
||||||
|
current-context: tazet@narod.ru@wheel
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
||||||
|
users:
|
||||||
|
- name: tazet@narod.ru
|
||||||
|
user:
|
||||||
|
token: eyJhbGciOiJSUzI1NiIsImtpZCI6InNoIiwidHlwIjoiSldUIn0.eyJhdWQiOiJiYWNrZW5kIiwiZXhwIjoxNzc1ODk5ODc2LCJqdGkiOiI5NjE4Yzg4NC05NjQ5LTQ1NjQtYjNjMi03Njc0Zjg5ZDAzMDciLCJpYXQiOjE3NzU4MTM0NzYsImlzcyI6Imh0dHBzOi8vYXV0aC5rOHMubmdjbG91ZC5ydSIsInN1YiI6InRhemV0QG5hcm9kLnJ1IiwiY2xhaW1zIjpbIiIsIiIsIiIsIjU1YzQ5MTRlLTdiYzUtNGJjYy1iYzcxLTI0NDQwMjViYzZmY19BRE1JTiIsIldaMDEzMjUtc2h0dXJ2YWwtYWRtaW4tdGVzdGVyIiwiV1owMTMyNS1zaHR1cnZhbC1hZG1pbi13aGVlbCJdLCJuYW1lIjoi0KLQsNC30LXRgtC00LjQvdC-0LIg0J3QsNC40LvRjCIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0YXpldEBuYXJvZC5ydSJ9.GiPjJonDXxxu4U6QE4LVPc7rMV8I_0Ubr8dugNo8g4HeCwh5Y0MtWYsPhQ4GOB2A2Fy_43ItVIlMypmG9ZwVzhaRxfmNIAuNlSV-N49x_C9zGqIOagL2Us6UVr30XCYdfgcSiN2fQGx5F-LYxCMOyePA_vxyAl8dYFKT2lZmH4poOxJjgkwclZ9tej7NKcfFy31PmJu26li6LE_8TzaNN130woO0Rat3bvHfHRLk_eKj-0iAlXD5a_rt3QEli2wyhxi7DZo5ohaJ0PjhNIrdqkghBLM0KjUERRHXVyhSBUEWjsz8OAXwItmaMb6iOH_Wv50vPPLmZ_KVm8FdkQD1Qg
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY operator/go.mod operator/go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY operator/cmd/registry ./cmd/registry
|
||||||
|
COPY registrykeys ./registrykeys
|
||||||
|
RUN CGO_ENABLED=0 GOOS=linux go build -o /registry-server ./cmd/registry
|
||||||
|
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"]
|
||||||
@@ -0,0 +1,198 @@
|
|||||||
|
# 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 или переменные окружения.
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
{{/*
|
||||||
|
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 }}
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
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!"
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,52 @@
|
|||||||
|
{{- 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 }}
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
{{- 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 }}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: {{ .Values.global.namespace }}
|
||||||
|
labels:
|
||||||
|
name: {{ .Values.global.namespace }}
|
||||||
|
{{- include "terraform-registry.labels" . | nindent 4 }}
|
||||||
@@ -0,0 +1,31 @@
|
|||||||
|
{{- 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 }}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
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 }}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
{{- 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 }}
|
||||||
@@ -0,0 +1,41 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
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 }}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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 }}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
global:
|
||||||
|
registryHostname: "terra.k8c.ru"
|
||||||
|
registry:
|
||||||
|
replicas: 1
|
||||||
|
pdb:
|
||||||
|
enabled: false
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,71 @@
|
|||||||
|
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 при миграции
|
||||||
@@ -0,0 +1,30 @@
|
|||||||
|
# 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; я ничего не меняю в кластере автоматически.
|
||||||
@@ -0,0 +1,19 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- deployment.yaml
|
||||||
|
- service.yaml
|
||||||
|
- ingress.yaml
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Service
|
||||||
|
metadata:
|
||||||
|
name: my-service
|
||||||
|
spec:
|
||||||
|
selector:
|
||||||
|
app: my-app
|
||||||
|
ports:
|
||||||
|
- protocol: TCP
|
||||||
|
port: 80
|
||||||
|
targetPort: 80
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: cert-manager.io/v1
|
||||||
|
kind: ClusterIssuer
|
||||||
|
metadata:
|
||||||
|
name: selfsigned-dev
|
||||||
|
spec:
|
||||||
|
selfSigned: {}
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,10 @@
|
|||||||
|
apiVersion: apps/v1
|
||||||
|
kind: Deployment
|
||||||
|
metadata:
|
||||||
|
name: my-deployment
|
||||||
|
spec:
|
||||||
|
template:
|
||||||
|
spec:
|
||||||
|
containers:
|
||||||
|
- name: my-container
|
||||||
|
image: nginx:stable
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
apiVersion: networking.k8s.io/v1
|
||||||
|
kind: Ingress
|
||||||
|
metadata:
|
||||||
|
name: my-ingress
|
||||||
|
annotations:
|
||||||
|
cert-manager.io/cluster-issuer: selfsigned-dev
|
||||||
|
kubernetes.io/ingress.class: nginx
|
||||||
|
spec:
|
||||||
|
tls:
|
||||||
|
- hosts:
|
||||||
|
- terr.kube5s.ru
|
||||||
|
secretName: terr-kube5s-ru-tls
|
||||||
|
rules:
|
||||||
|
- host: terr.kube5s.ru
|
||||||
|
http:
|
||||||
|
paths:
|
||||||
|
- path: /
|
||||||
|
pathType: Prefix
|
||||||
|
backend:
|
||||||
|
service:
|
||||||
|
name: my-service
|
||||||
|
port:
|
||||||
|
number: 80
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
apiVersion: kustomize.config.k8s.io/v1beta1
|
||||||
|
kind: Kustomization
|
||||||
|
resources:
|
||||||
|
- ../../base
|
||||||
|
namespace: terra-dev
|
||||||
|
patchesStrategicMerge:
|
||||||
|
- ingress-patch.yaml
|
||||||
|
- image-patch.yaml
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# 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
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
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)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
# 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 мог скачивать провайдеры по обычному источнику.
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
# 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"]
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# 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"]
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
# Build Stage
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
# Only copy what's needed for the registry server
|
||||||
|
COPY go.mod go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
|
||||||
|
COPY cmd/registry ./cmd/registry
|
||||||
|
# Build statically
|
||||||
|
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
|
||||||
|
# Registry server uses HTTPS for S3, so CA certs are good to have
|
||||||
|
RUN apk add --no-cache ca-certificates
|
||||||
|
|
||||||
|
USER 1000:1000
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
CMD ["/app/registry-server"]
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
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,8 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import registrykeys "terraform-registry-keys"
|
||||||
|
|
||||||
|
var gpgPublicKey = GPGPublicKey{
|
||||||
|
KeyID: registrykeys.KeyID,
|
||||||
|
ASCIIArmor: registrykeys.ASCIIArmor,
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
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", 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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,11 @@
|
|||||||
|
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"
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
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=
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
kind: Namespace
|
||||||
|
metadata:
|
||||||
|
name: terra
|
||||||
|
labels:
|
||||||
|
name: terra
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
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!"
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
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"
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,42 @@
|
|||||||
|
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
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module terraform-registry-keys
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
toolchain go1.24.12
|
||||||
@@ -0,0 +1,59 @@
|
|||||||
|
package registrykeys
|
||||||
|
|
||||||
|
const KeyID = "3EC4673EB798238A"
|
||||||
|
|
||||||
|
const ASCIIArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
6cZ0NQ==
|
||||||
|
=3Ea2
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----
|
||||||
|
`
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
module terraform-registry-keys
|
||||||
|
|
||||||
|
go 1.24.0
|
||||||
|
|
||||||
|
toolchain go1.24.12
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
package registrykeys
|
||||||
|
|
||||||
|
const KeyID = "CB3A0DF161ECC416"
|
||||||
|
|
||||||
|
const ASCIIArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
=q8Tw
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----`
|
||||||
Executable
+19
@@ -0,0 +1,19 @@
|
|||||||
|
#!/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."
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# Exclude files not needed in Docker build context for registry-server
|
||||||
|
.git
|
||||||
|
.gitignore
|
||||||
|
.dockerignore
|
||||||
|
|
||||||
|
# Docs / markdown
|
||||||
|
*.md
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# Other services / infra
|
||||||
|
operator/
|
||||||
|
registrykeys/
|
||||||
|
scripts/
|
||||||
|
charts/
|
||||||
|
k8s/
|
||||||
|
|
||||||
|
# Secrets & keys
|
||||||
|
secrets/
|
||||||
|
*.pem
|
||||||
|
*.key
|
||||||
|
*.asc
|
||||||
|
*.gpg
|
||||||
|
*.pgp
|
||||||
|
|
||||||
|
# Artefacts / temp
|
||||||
|
artifacts/
|
||||||
|
*.har
|
||||||
|
HAR/
|
||||||
|
universal_rebuild/
|
||||||
|
tools/
|
||||||
|
devops/
|
||||||
|
|
||||||
|
# IDE / OS
|
||||||
|
.idea/
|
||||||
|
.vscode/
|
||||||
|
*.swp
|
||||||
|
*.swo
|
||||||
|
*~
|
||||||
|
.DS_Store
|
||||||
|
|
||||||
|
# Go test/coverage
|
||||||
|
*.test
|
||||||
|
*.out
|
||||||
|
coverage.out
|
||||||
|
|
||||||
|
# Logs
|
||||||
|
*.log
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
# Registry Server Build
|
||||||
|
|
||||||
|
This folder holds the standalone registry server build and helper scripts.
|
||||||
|
|
||||||
|
## Provider build and upload
|
||||||
|
|
||||||
|
Script: `build-provider.sh`
|
||||||
|
|
||||||
|
What it does:
|
||||||
|
- Builds the provider from `universal_rebuild` for 3 platforms: linux_amd64, windows_amd64, darwin_amd64
|
||||||
|
- Creates ZIPs and SHA256SUMS
|
||||||
|
- Signs SHA256SUMS with a local GPG key file
|
||||||
|
- Uploads artifacts to S3 for the registry server to serve
|
||||||
|
|
||||||
|
### Requirements
|
||||||
|
- Go 1.22+
|
||||||
|
- `python3` (for zipfile)
|
||||||
|
- `gpg`
|
||||||
|
- `mc` (MinIO/S3 client)
|
||||||
|
|
||||||
|
### GPG key file
|
||||||
|
Store the private key at:
|
||||||
|
- `secrets/private_key.asc` (ignored by git)
|
||||||
|
|
||||||
|
The public key is at:
|
||||||
|
- `secrets/public_key.asc`
|
||||||
|
|
||||||
|
The registry server embeds the public key it returns to Terraform. After regenerating keys:
|
||||||
|
- Update the embedded ASCII Armor in `registry-server-build/main.go` and `operator/cmd/registry/main.go`.
|
||||||
|
- Rebuild and redeploy the registry server.
|
||||||
|
- Re-upload provider artifacts signed with the new private key.
|
||||||
|
|
||||||
|
### Environment
|
||||||
|
Set these variables before running:
|
||||||
|
- `S3_ENDPOINT` (example: `s3.msk-1.ngcloud.ru`)
|
||||||
|
- `S3_ACCESS_KEY`
|
||||||
|
- `S3_SECRET_KEY`
|
||||||
|
|
||||||
|
Optional overrides:
|
||||||
|
- `REGISTRY_HOSTNAME` (default: `terra.k8c.ru`)
|
||||||
|
- `NAMESPACE` (default: `nubes`)
|
||||||
|
- `NAME` (default: `nubes`)
|
||||||
|
- `S3_BUCKET` (default: `terraform-registry`)
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export S3_ENDPOINT="s3.msk-1.ngcloud.ru"
|
||||||
|
export S3_ACCESS_KEY="..."
|
||||||
|
export S3_SECRET_KEY="..."
|
||||||
|
|
||||||
|
./registry-server-build/build-provider.sh 2.0.2
|
||||||
|
```
|
||||||
|
|
||||||
|
Artifacts are uploaded to:
|
||||||
|
`registry/<bucket>/<hostname>/<namespace>/<name>/<version>/`
|
||||||
Executable
+115
@@ -0,0 +1,115 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Build and upload the Terraform provider to the registry S3 bucket.
|
||||||
|
#
|
||||||
|
# GPG signing key requirements:
|
||||||
|
# - A private key is required for signing SHA256SUMS.
|
||||||
|
# - The private key must be available at:
|
||||||
|
# ${ROOT_DIR}/secrets/private_key.asc
|
||||||
|
# - The private key is intentionally ignored by git.
|
||||||
|
#
|
||||||
|
# If the key is missing, generate it with a no-passphrase batch run, then
|
||||||
|
# export it to the expected location. Example (email only, no passphrase):
|
||||||
|
# GPG_DIR=${ROOT_DIR}/secrets
|
||||||
|
# GNUPGHOME=$(mktemp -d)
|
||||||
|
# cat > /tmp/gpg_batch <<'EOF'
|
||||||
|
# %no-protection
|
||||||
|
# Key-Type: RSA
|
||||||
|
# Key-Length: 3072
|
||||||
|
# Subkey-Type: RSA
|
||||||
|
# Subkey-Length: 3072
|
||||||
|
# Name-Real: tazet@narod.ru
|
||||||
|
# Name-Email: tazet@narod.ru
|
||||||
|
# Expire-Date: 0
|
||||||
|
# EOF
|
||||||
|
# gpg --batch --homedir "$GNUPGHOME" --gen-key /tmp/gpg_batch
|
||||||
|
# gpg --batch --homedir "$GNUPGHOME" --armor --export-secret-keys > "$GPG_DIR/private_key.asc"
|
||||||
|
# gpg --batch --homedir "$GNUPGHOME" --armor --export > "$GPG_DIR/public_key.asc"
|
||||||
|
# rm -rf "$GNUPGHOME" /tmp/gpg_batch
|
||||||
|
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}"
|
||||||
|
BUILD_DIR="${BUILD_DIR:-${ROOT_DIR}/artifacts/provider_build}"
|
||||||
|
GPG_KEY_FILE="${GPG_KEY_FILE:-${ROOT_DIR}/secrets/private_key.asc}"
|
||||||
|
|
||||||
|
VERSION="${1:-2.0.2}"
|
||||||
|
REGISTRY_HOSTNAME="${REGISTRY_HOSTNAME:-terra.k8c.ru}"
|
||||||
|
NAMESPACE="${NAMESPACE:-nubes}"
|
||||||
|
PROVIDER_NAME="${PROVIDER_NAME:-nubes}"
|
||||||
|
S3_BUCKET="${S3_BUCKET:-terraform-registry}"
|
||||||
|
|
||||||
|
S3_ENDPOINT="${S3_ENDPOINT:-}"
|
||||||
|
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
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -f "$GPG_KEY_FILE" ]]; then
|
||||||
|
echo "Error: GPG key file not found at $GPG_KEY_FILE" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ ! -d "$PROVIDER_DIR" ]]; then
|
||||||
|
echo "Error: provider source dir not found: $PROVIDER_DIR" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
s3_endpoint_url="$S3_ENDPOINT"
|
||||||
|
if [[ "$s3_endpoint_url" != http* ]]; then
|
||||||
|
s3_endpoint_url="https://${s3_endpoint_url}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
rm -rf "$BUILD_DIR"
|
||||||
|
mkdir -p "$BUILD_DIR"
|
||||||
|
|
||||||
|
build_and_zip() {
|
||||||
|
local os="$1"
|
||||||
|
local arch="$2"
|
||||||
|
local binary="terraform-provider-nubes_v${VERSION}"
|
||||||
|
|
||||||
|
if [[ "$os" == "windows" ]]; then
|
||||||
|
binary+=".exe"
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Building for ${os}/${arch}..."
|
||||||
|
(cd "$PROVIDER_DIR" && CGO_ENABLED=0 GOOS="$os" GOARCH="$arch" go build -o "${BUILD_DIR}/${binary}" .)
|
||||||
|
|
||||||
|
(cd "$BUILD_DIR" && python3 -m zipfile -c "terraform-provider-nubes_${VERSION}_${os}_${arch}.zip" "$binary")
|
||||||
|
rm -f "${BUILD_DIR:?}/${binary}"
|
||||||
|
}
|
||||||
|
|
||||||
|
build_and_zip linux amd64
|
||||||
|
build_and_zip windows amd64
|
||||||
|
build_and_zip darwin amd64
|
||||||
|
|
||||||
|
( cd "$BUILD_DIR" && sha256sum *.zip > "terraform-provider-nubes_${VERSION}_SHA256SUMS" )
|
||||||
|
|
||||||
|
GNUPGHOME="$(mktemp -d)"
|
||||||
|
export GNUPGHOME
|
||||||
|
trap 'rm -rf "$GNUPGHOME"' EXIT
|
||||||
|
|
||||||
|
gpg --batch --import "$GPG_KEY_FILE" >/dev/null
|
||||||
|
key_id=$(gpg --list-keys --with-colons | awk -F: '/^pub/{print $5; exit}')
|
||||||
|
if [[ -z "$key_id" ]]; then
|
||||||
|
echo "Error: failed to read key id from $GPG_KEY_FILE" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
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}/"
|
||||||
|
|
||||||
|
echo "Uploading to: ${S3_PATH}"
|
||||||
|
mc cp "${BUILD_DIR}"/*.zip "$S3_PATH"
|
||||||
|
mc cp "${BUILD_DIR}/terraform-provider-nubes_${VERSION}_SHA256SUMS" "$S3_PATH"
|
||||||
|
mc cp "${BUILD_DIR}/terraform-provider-nubes_${VERSION}_SHA256SUMS.sig" "$S3_PATH"
|
||||||
|
|
||||||
|
echo "Done. Version ${VERSION} uploaded."
|
||||||
+116
@@ -0,0 +1,116 @@
|
|||||||
|
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,8 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import registrykeys "terraform-registry-keys"
|
||||||
|
|
||||||
|
var gpgPublicKey = GPGPublicKey{
|
||||||
|
KeyID: registrykeys.KeyID,
|
||||||
|
ASCIIArmor: registrykeys.ASCIIArmor,
|
||||||
|
}
|
||||||
+288
@@ -0,0 +1,288 @@
|
|||||||
|
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))
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
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"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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"))
|
||||||
|
}
|
||||||
@@ -0,0 +1,18 @@
|
|||||||
|
apiVersion: v1
|
||||||
|
clusters:
|
||||||
|
- cluster:
|
||||||
|
certificate-authority-data: LS0tLS1CRUdJTiBDRVJUSUZJQ0FURS0tLS0tCk1JSUM2akNDQWRLZ0F3SUJBZ0lCQURBTkJna3Foa2lHOXcwQkFRc0ZBREFWTVJNd0VRWURWUVFERXdwcmRXSmwKY201bGRHVnpNQjRYRFRJMU1UQXlNREV5TVRVMU5Wb1hEVE0xTVRBeE9ERXlNakExTlZvd0ZURVRNQkVHQTFVRQpBeE1LYTNWaVpYSnVaWFJsY3pDQ0FTSXdEUVlKS29aSWh2Y05BUUVCQlFBRGdnRVBBRENDQVFvQ2dnRUJBUGsvClp2SXFlTWY5cEM0V0YvZW4yVVpNVWV6bHdWYTB5Wm9XMlFWWXdQM1dubHZDNDB4Vld2aXlNSEFtWmxKZzdkdzgKK1ZUbEZSRVA5YVRZS3czSk10QnZlOVM1RXU1MnZ3ZW1FSGxCZDFOWFlVNHB3U08xNDhieCtzWnQrNWlkNk9BbAp6ck9UaUhKQXJzcTFvc0sxTEFmTXh6K0lvR015SG5WUjNlaXI5SnQxSWdPYTNWemJvN01qWGFhOHg0bG1MRlllCnpiN0R6dHBqdTdyYzlKZVdMRVdkbkNWL2NjVzVmTzBXU3RKZzlTVUVDZHc3MUs4VVBYQ2FxUmRKMTB4SDJoVGwKjalVFVDlJYjk0emgvL0dkMUp3RFlsNFJkWUZKR2FSYW5mQlFVRzhBck1iUmVDWmdrYTJIY0ZtUDJtdnBidGYxUQo3ZEtQdElsb3ZqSVRWblBGNURzQ0F3RUFBYU5GTUVNd0RnWURWUjBQQVFIL0JBUURBZ0trTUJJR0ExVWRFd0VCCi93UUlNQVlCQWY4Q0FRQXdIUVlEVlIwT0JCWUVGTWdTamRrd3lMOFhNVkdvbU1FV1ZOT2hEYzBCTUEwR0NTcUcKU0liM0RRRUJDd1VBQTRJQkFRQnkvTDZPTTFZK0JUWUJGRVY3TDRLeHNWaHFzZVk3UGZidnhiQWhObVFleEJlSQpYMmRxVDVyQlFaa1NXS3E3cEk3d1Q0Ny81QkN0UUtZQnpNSzRzR2NKQXVza2NHM09UejFRSzIxdEczMXp4N2lMCngrT0dDYzV0ZTR4Vydila3VCczdnN0RkNmlLRFFrbjZia1FTKzhvcFJ6SC9FU0pHUm5adXFlYTBGRW8wc1BuaW0KSGZlV3JTRm5OUk1YQ1J6dkRFY2V5cThjZ21WVEhHRGhOd2tJSTRrQ0F3ZWRnVi9vYlhjSEluZVl6SGhZVVpmbgpkbWhVSjNNVDNHZVErYkhsZFZoNjQyTXB6R2VQRTdPVzNvdjN5aFV4WUpaZ3BWZUU4a2VLTGc3YTcvd2FmeG5jCngxRWY2ekRNdlgrV2gwUjIyM3hPTldWUElPYUNyWUxoM0lHYkxCMTUKLS0tLS1FTkQgQ0VSVElGSUNBVEUtLS0tLQo=
|
||||||
|
server: https://5.172.178.181:6443
|
||||||
|
name: wheel
|
||||||
|
contexts:
|
||||||
|
- context:
|
||||||
|
cluster: wheel
|
||||||
|
user: tazet@narod.ru
|
||||||
|
name: tazet@narod.ru@wheel
|
||||||
|
current-context: tazet@narod.ru@wheel
|
||||||
|
kind: Config
|
||||||
|
preferences: {}
|
||||||
|
users:
|
||||||
|
- name: tazet@narod.ru
|
||||||
|
user:
|
||||||
|
token: eyJhbGciOiJSUzI1NiIsImtpZCI6InNoIiwidHlwIjoiSldUIn0.eyJhdWQiOiJiYWNrZW5kIiwiZXhwIjoxNzc1ODk5ODc2LCJqdGkiOiI5NjE4Yzg4NC05NjQ5LTQ1NjQtYjNjMi03Njc0Zjg5ZDAzMDciLCJpYXQiOjE3NzU4MTM0NzYsImlzcyI6Imh0dHBzOi8vYXV0aC5rOHMubmdjbG91ZC5ydSIsInN1YiI6InRhemV0QG5hcm9kLnJ1IiwiY2xhaW1zIjpbIiIsIiIsIiIsIjU1YzQ5MTRlLTdiYzUtNGJjYy1iYzcxLTI0NDQwMjViYzZmY19BRE1JTiIsIldaMDEzMjUtc2h0dXJ2YWwtYWRtaW4tdGVzdGVyIiwiV1owMTMyNS1zaHR1cnZhbC1hZG1pbi13aGVlbCJdLCJuYW1lIjoi0KLQsNC30LXRgtC00LjQvdC-0LIg0J3QsNC40LvRjCIsImVtYWlsIjoidGF6ZXRAbmFyb2QucnUiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJ0YXpldEBuYXJvZC5ydSJ9.GiPjJonDXxxu4U6QE4LVPc7rMV8I_0Ubr8dugNo8g4HeCwh5Y0MtWYsPhQ4GOB2A2Fy_43ItVIlMypmG9ZwVzhaRxfmNIAuNlSV-N49x_C9zGqIOagL2Us6UVr30XCYdfgcSiN2fQGx5F-LYxCMOyePA_vxyAl8dYFKT2lZmH4poOxJjgkwclZ9tej7NKcfFy31PmJu26li6LE_8TzaNN130woO0Rat3bvHfHRLk_eKj-0iAlXD5a_rt3QEli2wyhxi7DZo5ohaJ0PjhNIrdqkghBLM0KjUERRHXVyhSBUEWjsz8OAXwItmaMb6iOH_Wv50vPPLmZ_KVm8FdkQD1Qg
|
||||||
Reference in New Issue
Block a user