Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
73ce2680e9 | ||
|
|
d73bf90f0a | ||
|
|
94a900d888 | ||
|
|
90807aae2f | ||
|
|
4592a35b3e | ||
|
|
3aca89ea60 | ||
|
|
f3df103bb3 | ||
|
|
6ec580c616 | ||
|
|
86c4a1f25c | ||
|
|
13f8ce8be6 | ||
|
|
e87eab4df9 | ||
|
|
da03bd023b | ||
|
|
4c10a3b836 | ||
|
|
f10f2d3998 | ||
|
|
b9c47765ac | ||
|
|
a9cfea16e9 | ||
|
|
91472e1259 | ||
|
|
35a07ca374 | ||
|
|
8110aad493 | ||
|
|
e19bb6f290 | ||
|
|
a0e72b1c7f |
+27
@@ -0,0 +1,27 @@
|
|||||||
|
# Multi-stage build for Terraform Registry
|
||||||
|
#
|
||||||
|
# Stage 1: Build Go binary
|
||||||
|
# Stage 2: Minimal Alpine runtime
|
||||||
|
|
||||||
|
FROM golang:1.24-alpine AS builder
|
||||||
|
ENV GOTOOLCHAIN=auto
|
||||||
|
WORKDIR /app
|
||||||
|
COPY server/go.mod server/go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY server/ .
|
||||||
|
RUN CGO_ENABLED=0 go build -o registry .
|
||||||
|
|
||||||
|
FROM alpine:3.21
|
||||||
|
RUN apk --no-cache add ca-certificates
|
||||||
|
COPY --from=builder /app/registry /registry
|
||||||
|
|
||||||
|
# ── Non-secret config (change here → rebuild → push → redeploy) ─────────
|
||||||
|
ENV S3_ENDPOINT=s3.msk-1.ngcloud.ru \
|
||||||
|
S3_BUCKET=terraform-registry \
|
||||||
|
S3_PREFIX=tf-registry.services.ngcloud.ru \
|
||||||
|
REGISTRY_HOSTNAME=tf-registry.services.ngcloud.ru \
|
||||||
|
PORT=5000
|
||||||
|
# ── Secrets in UI jsonEnv: S3_ACCESS_KEY, S3_SECRET_KEY ───────────────────
|
||||||
|
|
||||||
|
EXPOSE 5000
|
||||||
|
CMD ["/registry"]
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
# Code Review Request — Go Terraform Registry
|
||||||
|
|
||||||
|
## Контекст
|
||||||
|
|
||||||
|
Terraform Provider Registry по протоколу HashiCorp. Деплоится как Docker-контейнер (containerk8s managed service в Nubes Cloud). Работает на проде: `https://go-registry.containerk8s.dev.nubes.ru`.
|
||||||
|
|
||||||
|
Хранилище — S3 (MinIO). Провайдеры, SHA256SUMS, GPG-подписи, документация — всё в S3.
|
||||||
|
|
||||||
|
## Файлы для ревью
|
||||||
|
|
||||||
|
- `server/main.go` — HTTP-сервер, S3, все роуты
|
||||||
|
- `server/gpg_key.go` — GPG-ключи (2 штуки, вшиты в код)
|
||||||
|
- `server/docs.go` — статика документации из S3
|
||||||
|
- `Dockerfile` — multi-stage сборка
|
||||||
|
|
||||||
|
## Вопросы
|
||||||
|
|
||||||
|
1. **`downloadVersion`: `defer` внутри `if err == nil`** — defer в Go выполняется при выходе из функции, а не из блока. Правильно ли это здесь? Не приведёт ли к накоплению defer'ов или утечке?
|
||||||
|
|
||||||
|
2. **`readyzHandler`**: комментарий «Проверить доступность S3», но возвращает просто "ok". Стоит ли добавить реальную проверку (head bucket)?
|
||||||
|
|
||||||
|
3. **`listVersions`**: версии не сортируются, порядок зависит от S3 ListObjects. Стоит ли сортировать?
|
||||||
|
|
||||||
|
4. **`proxHandler`**: никаких ограничений на размер файла, таймаутов, rate-limiting. Нормально ли для внутреннего сервиса?
|
||||||
|
|
||||||
|
5. **`rootHandler`**: HTML захардкожен. Вынести в template или embed?
|
||||||
|
|
||||||
|
6. **Graceful shutdown**: нет обработки SIGTERM. Для managed-сервиса это критично?
|
||||||
|
|
||||||
|
7. **`tyCandidateKeys` в docs.go**: индентация сломана (табы/пробелы вперемешку). Это баг форматирования или Go это нормально компилирует?
|
||||||
|
|
||||||
|
8. **GPG-ключи в коде**: стоит ли вынести в файлы/ENV или оставить как есть?
|
||||||
|
|
||||||
|
9. **Dockerfile**: `go mod init` + `go mod tidy` при каждой сборке. Стоит ли добавить `go.sum` в репо для воспроизводимости?
|
||||||
|
|
||||||
|
10. **Общая архитектура**: что можно улучшить? Есть ли проблемы с безопасностью?
|
||||||
|
|
||||||
|
## Как отвечать
|
||||||
|
|
||||||
|
Если что-то неясно — спрашивай. Контекст: сервис внутренний, не публичный, трафик низкий.
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
# Code Review — Соннет (2026-08-08)
|
||||||
|
|
||||||
|
## Файлы ревью
|
||||||
|
- `server/main.go` — HTTP, S3, роуты
|
||||||
|
- `server/gpg_key.go` — GPG-ключи
|
||||||
|
- `server/docs.go` — статика из S3
|
||||||
|
- `Dockerfile` — сборка
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. `defer` внутри `if err == nil` — ✅ OK
|
||||||
|
|
||||||
|
```go
|
||||||
|
if err == nil {
|
||||||
|
defer shasumsObj.Close() // выполнится при выходе из ФУНКЦИИ, не из if
|
||||||
|
}
|
||||||
|
```
|
||||||
|
Не утечка. `defer` всегда выполняется при выходе из функции. `shasumsObj` открывается один раз на вызов и закрывается при возврате — правильный паттерн.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. `readyzHandler` без проверки S3 — ⚠️ надо чинить
|
||||||
|
|
||||||
|
```go
|
||||||
|
// Проверить доступность S3 ← комментарий, не реализовано
|
||||||
|
w.Write([]byte("ok"))
|
||||||
|
```
|
||||||
|
K8s readiness probe всегда 200 — даже если S3 недоступен. Pod остаётся в rotation с неработающим S3.
|
||||||
|
|
||||||
|
**Фикс:** `s3Client.BucketExists(ctx, bucketName)`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Версии не сортируются — ⚠️ надо чинить
|
||||||
|
|
||||||
|
`seenVersions` — map, итерация недетерминирована. Terraform может некорректно выбрать последнюю версию.
|
||||||
|
|
||||||
|
**Фикс:** `sort.Slice` по семверу.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. `proxyHandler` без лимитов — 🔴 КРИТИЧНО
|
||||||
|
|
||||||
|
### 4a. Размер/таймаут — некритично для внутреннего сервиса.
|
||||||
|
|
||||||
|
### 4b. SSRF / Authorization bypass — КРИТИЧНО
|
||||||
|
|
||||||
|
```go
|
||||||
|
bucket := r.URL.Query().Get("bucket") // ← принимает ЛЮБОЙ bucket!
|
||||||
|
key := r.URL.Query().Get("key")
|
||||||
|
```
|
||||||
|
Кто знает URL сервера — может читать объекты из любого бакета в том же S3.
|
||||||
|
|
||||||
|
**Фикс:** захардкодить `bucket = bucketName`, игнорировать параметр `bucket` из запроса. `key` валидировать что начинается с разрешённого префикса.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. HTML хардкод — ✅ OK
|
||||||
|
|
||||||
|
```go
|
||||||
|
fmt.Fprintf(w, `...` + VERSION + `...`)
|
||||||
|
```
|
||||||
|
Для крошечной страницы приемлемо. `VERSION` сейчас `"1.0.2"` — безопасно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Graceful Shutdown — ⚠️ желательно
|
||||||
|
|
||||||
|
```go
|
||||||
|
log.Fatal(http.ListenAndServe(":"+port, nil))
|
||||||
|
```
|
||||||
|
SIGTERM убивает процесс мгновенно. In-flight download обрывается → Terraform-клиент теряет кэш.
|
||||||
|
|
||||||
|
**Фикс:**
|
||||||
|
```go
|
||||||
|
srv := &http.Server{Addr: ":" + port}
|
||||||
|
go srv.ListenAndServe()
|
||||||
|
// signal.NotifyContext + srv.Shutdown(ctx)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Индентация в docs.go — ✅ косметика
|
||||||
|
|
||||||
|
Смешаны табы и пробелы. Go компилирует нормально, но `gofmt -w docs.go` переформатирует полностью. Если CI проверяет `gofmt -l` — упадёт.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. GPG-ключи в коде — ✅ OK
|
||||||
|
|
||||||
|
Это **публичные** ключи — их задача быть известными. Смена ключа = rebuild + redeploy — для редкого события приемлемо.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. `go mod init` + `go mod tidy` в Dockerfile — 🔴 КРИТИЧНО
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
RUN go mod init tf-registry && go mod tidy && ...
|
||||||
|
```
|
||||||
|
Каждая сборка качает зависимости из интернета. Если proxy недоступен или версия изменилась — сборка сломается.
|
||||||
|
|
||||||
|
**Фикс:** добавить `go.mod` + `go.sum` в репо (`server/go.mod`, `server/go.sum`).
|
||||||
|
```dockerfile
|
||||||
|
COPY server/go.mod server/go.sum ./
|
||||||
|
RUN go mod download
|
||||||
|
COPY server/ .
|
||||||
|
RUN CGO_ENABLED=0 go build -o registry .
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Общая архитектура и безопасность
|
||||||
|
|
||||||
|
| Проблема | Серьёзность | Статус |
|
||||||
|
|---|---|---|
|
||||||
|
| `proxyHandler` принимает любой `bucket` | 🔴 Высокая | SSRF — фикс п.4b |
|
||||||
|
| Нет аутентификации | 🟡 Средняя | Внутренний сервис |
|
||||||
|
| `key` не валидируется | 🟡 Средняя | `../` MinIO отклонит, но лучше проверять |
|
||||||
|
| JSON-ошибки не логируются | 🟢 Низкая | `json.NewEncoder` без проверки ошибок |
|
||||||
|
| Нет `go.sum` | 🟡 Средняя | Нерепродуцируемые сборки |
|
||||||
|
| `readyz` всегда 200 | 🟡 Средняя | см. п.2 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Приоритеты к исправлению
|
||||||
|
|
||||||
|
| # | Что | Серьёзность |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `proxyHandler` — убрать параметр `bucket` из запроса | 🔴 SSRF |
|
||||||
|
| 2 | `go.sum` в репо | 🔴 Воспроизводимость |
|
||||||
|
| 3 | `readyz` — реальная проверка S3 | 🟡 |
|
||||||
|
| 4 | Сортировка версий | 🟡 |
|
||||||
|
| 5 | Graceful shutdown | 🟡 |
|
||||||
|
| 6 | `gofmt` docs.go | 🟢 |
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
# Переменные инстанса и конфигурация
|
||||||
|
|
||||||
|
## Принцип
|
||||||
|
|
||||||
|
- **jsonEnv** — только секреты. Задаются при создании инстанса, НЕ меняются (modify не поддерживает).
|
||||||
|
- **Dockerfile ENV** — всё несекретное. Меняются через правку Dockerfile → rebuild → push → redeploy.
|
||||||
|
- **Go-код** — `os.Getenv()` с fallback'ами, без хардкода.
|
||||||
|
|
||||||
|
## jsonEnv (создание инстанса)
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"S3_ACCESS_KEY": "...",
|
||||||
|
"S3_SECRET_KEY": "..."
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
| Переменная | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `S3_ACCESS_KEY` | Доступ к S3 |
|
||||||
|
| `S3_SECRET_KEY` | Доступ к S3 |
|
||||||
|
|
||||||
|
## Dockerfile ENV (rebuild)
|
||||||
|
|
||||||
|
```dockerfile
|
||||||
|
ENV S3_ENDPOINT=s3.msk-1.ngcloud.ru \
|
||||||
|
S3_BUCKET=terraform-registry \
|
||||||
|
S3_PREFIX=go-registry.containerk8s.dev.nubes.ru \
|
||||||
|
REGISTRY_HOSTNAME=go-registry.containerk8s.dev.nubes.ru \
|
||||||
|
PORT=5000
|
||||||
|
```
|
||||||
|
|
||||||
|
| Переменная | Назначение | Пример |
|
||||||
|
|---|---|---|
|
||||||
|
| `S3_ENDPOINT` | Адрес S3 | `s3.msk-1.ngcloud.ru` |
|
||||||
|
| `S3_BUCKET` | Имя бакета | `terraform-registry` |
|
||||||
|
| `S3_PREFIX` | Префикс S3-ключей | `go-registry.containerk8s.dev.nubes.ru` |
|
||||||
|
| `REGISTRY_HOSTNAME` | Домен реестра (для download_url) | `go-registry.containerk8s.dev.nubes.ru` |
|
||||||
|
| `PORT` | Порт HTTP | `5000` |
|
||||||
|
|
||||||
|
## При создании нового инстанса (другой домен/стенд)
|
||||||
|
|
||||||
|
1. В Dockerfile поменять `S3_PREFIX`, `REGISTRY_HOSTNAME` под новый домен
|
||||||
|
2. Поменять `S3_ENDPOINT`, `S3_BUCKET` если S3 другой
|
||||||
|
3. `docker build && docker push`
|
||||||
|
4. Создать инстанс: `registryPath` → образ, `jsonEnv` → ключи S3
|
||||||
|
|
||||||
|
## Смена окружения
|
||||||
|
|
||||||
|
Не менять инстанс — создать новый с другими параметрами. Старый удалить.
|
||||||
|
|
||||||
|
| Окружение | Домен | S3_PREFIX |
|
||||||
|
|---|---|---|
|
||||||
|
| Тест | `go-registry.containerk8s.dev.nubes.ru` | `go-registry.containerk8s.dev.nubes.ru` |
|
||||||
|
| Прод | (будет отдельно) | (будет отдельно) |
|
||||||
@@ -0,0 +1,86 @@
|
|||||||
|
# Terraform Provider Registry
|
||||||
|
|
||||||
|
Реестр Terraform-провайдеров по протоколу HashiCorp.
|
||||||
|
Деплой: Docker-контейнер в Nubes Cloud (`containerk8s`).
|
||||||
|
|
||||||
|
**URL:** `https://registry.containerk8s.dev.nubes.ru`
|
||||||
|
|
||||||
|
## Структура
|
||||||
|
|
||||||
|
```
|
||||||
|
├── server/ # Go-код HTTP-сервера
|
||||||
|
│ ├── main.go # Роуты, S3, структуры
|
||||||
|
│ ├── gpg_key.go # GPG-ключи (2 шт.)
|
||||||
|
│ ├── docs.go # Статика документации из S3
|
||||||
|
│ ├── build-provider.sh # Сборка и заливка провайдера
|
||||||
|
│ ├── go.mod / go.sum
|
||||||
|
│ └── gpg/
|
||||||
|
├── tests/
|
||||||
|
│ └── smoke.sh # 9 интеграционных тестов
|
||||||
|
├── secrets/ # Креды (в .gitignore)
|
||||||
|
├── HISTORY/ # Документация, планы, ревью
|
||||||
|
├── Dockerfile # Multi-stage сборка (ветка go-container)
|
||||||
|
└── .gitignore
|
||||||
|
```
|
||||||
|
|
||||||
|
## Эндпоинты
|
||||||
|
|
||||||
|
| Метод | URL | Ответ |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/` | HTML с версией |
|
||||||
|
| GET | `/.well-known/terraform.json` | `{"providers.v1":"/v1/providers/"}` |
|
||||||
|
| GET | `/v1/providers/{ns}/{name}/versions` | JSON: список версий и платформ |
|
||||||
|
| GET | `/v1/providers/{ns}/{name}/{ver}/download/{os}/{arch}` | JSON: download_url, shasum, GPG |
|
||||||
|
| GET | `/v1/proxy?key=...` | Стриминг файла из S3 |
|
||||||
|
| GET | `/docs/{ns}/{name}/{ver}/...` | Статика документации из S3 |
|
||||||
|
| GET | `/healthz` | `ok` 200 |
|
||||||
|
| GET | `/readyz` | `ok` 200 (проверяет S3) |
|
||||||
|
|
||||||
|
## Сборка и деплой
|
||||||
|
|
||||||
|
```bash
|
||||||
|
# Сборка образа (на ВМ)
|
||||||
|
ssh naeel@5.172.178.213
|
||||||
|
cd ~/tf_registry && git checkout go-container
|
||||||
|
docker build -t gitea.services.ngcloud.ru/nail/tf_registry:latest .
|
||||||
|
docker push gitea.services.ngcloud.ru/nail/tf_registry:latest
|
||||||
|
|
||||||
|
# Редеплой — через UI modify или пересоздать инстанс
|
||||||
|
```
|
||||||
|
|
||||||
|
## Заливка провайдера
|
||||||
|
|
||||||
|
Подробно: `HISTORY/HOWTO-UPLOAD.md`
|
||||||
|
|
||||||
|
Кратко:
|
||||||
|
```bash
|
||||||
|
export REGISTRY_HOSTNAME=registry.containerk8s.dev.nubes.ru
|
||||||
|
export S3_ENDPOINT=http://ceph.tst.nubes.ru
|
||||||
|
export S3_ACCESS_KEY=... # super-креды
|
||||||
|
export S3_SECRET_KEY=...
|
||||||
|
bash server/build-provider.sh 5.1.17
|
||||||
|
```
|
||||||
|
|
||||||
|
## Тесты
|
||||||
|
|
||||||
|
```bash
|
||||||
|
bash tests/smoke.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
## Конфигурация
|
||||||
|
|
||||||
|
| Где | Что |
|
||||||
|
|---|---|
|
||||||
|
| `jsonEnv` (UI) | Только `S3_ACCESS_KEY`, `S3_SECRET_KEY` (reader) |
|
||||||
|
| `Dockerfile ENV` | `S3_ENDPOINT`, `S3_BUCKET`, `S3_PREFIX`, `REGISTRY_HOSTNAME`, `PORT`, `S3_USE_SSL` |
|
||||||
|
|
||||||
|
Подробно: `HISTORY/env-config.md`
|
||||||
|
|
||||||
|
## Ветки
|
||||||
|
|
||||||
|
| Ветка | Назначение |
|
||||||
|
|---|---|
|
||||||
|
| `master` | Основная, чистый код |
|
||||||
|
| `go-container` | Сборка: Go + Dockerfile |
|
||||||
|
| `with-flask-legacy` | Архив Flask-версии |
|
||||||
|
| `golang` | Архив исходного Go (до правок) |
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
apiVersion: v2
|
|
||||||
name: terraform-registry
|
|
||||||
description: Helm chart for Terraform Provider Registry (registry-server + operator)
|
|
||||||
type: application
|
|
||||||
version: 0.1.0
|
|
||||||
appVersion: "1.0.0"
|
|
||||||
keywords:
|
|
||||||
- terraform
|
|
||||||
- registry
|
|
||||||
- nubes
|
|
||||||
home: https://terra.k8c.ru
|
|
||||||
maintainers:
|
|
||||||
- name: Naeel
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
{{/*
|
|
||||||
Expand the name of the chart.
|
|
||||||
*/}}
|
|
||||||
{{- define "terraform-registry.name" -}}
|
|
||||||
{{- default .Chart.Name .Values.nameOverride | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create a default fully qualified app name.
|
|
||||||
*/}}
|
|
||||||
{{- define "terraform-registry.fullname" -}}
|
|
||||||
{{- if .Values.fullnameOverride }}
|
|
||||||
{{- .Values.fullnameOverride | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- else }}
|
|
||||||
{{- $name := default .Chart.Name .Values.nameOverride }}
|
|
||||||
{{- printf "%s" $name | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Create chart label.
|
|
||||||
*/}}
|
|
||||||
{{- define "terraform-registry.chart" -}}
|
|
||||||
{{- printf "%s-%s" .Chart.Name .Chart.Version | replace "+" "_" | trunc 63 | trimSuffix "-" }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Common labels.
|
|
||||||
*/}}
|
|
||||||
{{- define "terraform-registry.labels" -}}
|
|
||||||
helm.sh/chart: {{ include "terraform-registry.chart" . }}
|
|
||||||
{{ include "terraform-registry.selectorLabels" . }}
|
|
||||||
app.kubernetes.io/managed-by: {{ .Release.Service }}
|
|
||||||
{{- end }}
|
|
||||||
|
|
||||||
{{/*
|
|
||||||
Selector labels.
|
|
||||||
*/}}
|
|
||||||
{{- define "terraform-registry.selectorLabels" -}}
|
|
||||||
app.kubernetes.io/name: {{ include "terraform-registry.name" . }}
|
|
||||||
app.kubernetes.io/instance: {{ .Release.Name }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: builder-script
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
data:
|
|
||||||
build.sh: |
|
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo ">>> Starting Build for $PROVIDER_NAME $VERSION"
|
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
# Inputs: GIT_REPO, GIT_REF, PROVIDER_NAME, VERSION
|
|
||||||
WORK_DIR="/workspace"
|
|
||||||
ARTIFACTS_DIR="/artifacts"
|
|
||||||
mkdir -p $WORK_DIR $ARTIFACTS_DIR
|
|
||||||
|
|
||||||
# 2. Clone Repository
|
|
||||||
echo ">>> Cloning $GIT_REPO ($GIT_REF)..."
|
|
||||||
git clone $GIT_REPO $WORK_DIR/src
|
|
||||||
cd $WORK_DIR/src
|
|
||||||
git checkout $GIT_REF
|
|
||||||
|
|
||||||
# 3. Build for Platforms
|
|
||||||
echo ">>> Building binaries..."
|
|
||||||
|
|
||||||
# Linux amd64
|
|
||||||
echo "--> linux/amd64"
|
|
||||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o terraform-provider-${PROVIDER_NAME}_v${VERSION}
|
|
||||||
zip ${ARTIFACTS_DIR}/terraform-provider-${PROVIDER_NAME}_${VERSION}_linux_amd64.zip terraform-provider-${PROVIDER_NAME}_v${VERSION}
|
|
||||||
rm terraform-provider-${PROVIDER_NAME}_v${VERSION}
|
|
||||||
|
|
||||||
# Windows amd64
|
|
||||||
echo "--> windows/amd64"
|
|
||||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
|
|
||||||
zip ${ARTIFACTS_DIR}/terraform-provider-${PROVIDER_NAME}_${VERSION}_windows_amd64.zip terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
|
|
||||||
rm terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
|
|
||||||
|
|
||||||
# 4. Checksums
|
|
||||||
cd $ARTIFACTS_DIR
|
|
||||||
sha256sum *.zip > terraform-provider-${PROVIDER_NAME}_${VERSION}_SHA256SUMS
|
|
||||||
|
|
||||||
# 5. GPG Sign
|
|
||||||
touch terraform-provider-${PROVIDER_NAME}_${VERSION}_SHA256SUMS.sig
|
|
||||||
|
|
||||||
# 6. Upload to S3
|
|
||||||
if [ -z "$REGISTRY_HOSTNAME" ]; then
|
|
||||||
REGISTRY_HOSTNAME="{{ .Values.global.registryHostname }}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
TARGET_PATH="terraform-providers/$REGISTRY_HOSTNAME/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}"
|
|
||||||
|
|
||||||
echo ">>> Uploading to S3 path: $TARGET_PATH"
|
|
||||||
|
|
||||||
mc alias set nubes_s3 https://$S3_ENDPOINT $S3_ACCESS_KEY $S3_SECRET_KEY
|
|
||||||
mc cp --recursive $ARTIFACTS_DIR/ nubes_s3/$TARGET_PATH/
|
|
||||||
|
|
||||||
echo ">>> Build Complete!"
|
|
||||||
@@ -1,79 +0,0 @@
|
|||||||
apiVersion: apiextensions.k8s.io/v1
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
metadata:
|
|
||||||
name: providerreleases.terra.core.nubes.ru
|
|
||||||
annotations:
|
|
||||||
app.kubernetes.io/managed-by: Helm
|
|
||||||
spec:
|
|
||||||
group: terra.core.nubes.ru
|
|
||||||
versions:
|
|
||||||
- name: v1alpha1
|
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
subresources:
|
|
||||||
status: {}
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
spec:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- providerName
|
|
||||||
- version
|
|
||||||
- gitRepo
|
|
||||||
- gitRef
|
|
||||||
properties:
|
|
||||||
providerName:
|
|
||||||
type: string
|
|
||||||
description: "Name of the provider (e.g. 'mycloud')"
|
|
||||||
namespace:
|
|
||||||
type: string
|
|
||||||
default: "mycloud"
|
|
||||||
description: "Terraform registry namespace (e.g. 'nubes')"
|
|
||||||
version:
|
|
||||||
type: string
|
|
||||||
description: "Semantic version without v (e.g. '1.0.1')"
|
|
||||||
gitRepo:
|
|
||||||
type: string
|
|
||||||
description: "Git repository URL to clone"
|
|
||||||
gitRef:
|
|
||||||
type: string
|
|
||||||
description: "Git tag, commit SHA or branch"
|
|
||||||
platforms:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
default: ["linux_amd64", "windows_amd64"]
|
|
||||||
description: "Target platforms for build"
|
|
||||||
status:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
phase:
|
|
||||||
type: string
|
|
||||||
description: "Current phase: Pending, Building, Publishing, Ready, Failed"
|
|
||||||
downloadUrl:
|
|
||||||
type: string
|
|
||||||
conditions:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
type:
|
|
||||||
type: string
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
lastTransitionTime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
reason:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
scope: Namespaced
|
|
||||||
names:
|
|
||||||
plural: providerreleases
|
|
||||||
singular: providerrelease
|
|
||||||
kind: TerraformProviderRelease
|
|
||||||
shortNames:
|
|
||||||
- tpr
|
|
||||||
@@ -1,52 +0,0 @@
|
|||||||
{{- if .Values.docs.enabled }}
|
|
||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: registry-server-docs
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
app: registry-server-docs
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: registry-server-docs
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: registry-server-docs
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: registry-server
|
|
||||||
image: "{{ .Values.docs.image.repository }}:{{ .Values.docs.image.tag }}"
|
|
||||||
imagePullPolicy: {{ .Values.docs.image.pullPolicy }}
|
|
||||||
env:
|
|
||||||
- name: REGISTRY_HOSTNAME
|
|
||||||
value: {{ .Values.global.registryHostname | quote }}
|
|
||||||
- name: S3_ENDPOINT
|
|
||||||
value: {{ .Values.s3.endpoint | quote }}
|
|
||||||
- name: S3_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{ .Values.s3.existingSecret }}
|
|
||||||
key: {{ .Values.s3.accessKeyField }}
|
|
||||||
- name: S3_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{ .Values.s3.existingSecret }}
|
|
||||||
key: {{ .Values.s3.secretKeyField }}
|
|
||||||
- name: S3_BUCKET
|
|
||||||
value: {{ .Values.s3.bucket | quote }}
|
|
||||||
- name: S3_USE_SSL
|
|
||||||
value: {{ .Values.s3.useSSL | quote }}
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 50m
|
|
||||||
memory: 64Mi
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 128Mi
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
{{- if .Values.ingress.enabled }}
|
|
||||||
# WARNING: Do NOT apply to existing clusters without confirming TLS issuer.
|
|
||||||
# LetsEncrypt rate limits apply. For testing, use a self-signed issuer.
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: registry-ingress
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
annotations:
|
|
||||||
{{- if .Values.ingress.tls.enabled }}
|
|
||||||
cert-manager.io/cluster-issuer: {{ .Values.ingress.tls.issuer | quote }}
|
|
||||||
{{- end }}
|
|
||||||
{{- range $key, $value := .Values.ingress.annotations }}
|
|
||||||
{{ $key }}: {{ $value | quote }}
|
|
||||||
{{- end }}
|
|
||||||
labels:
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
ingressClassName: {{ .Values.ingress.className | quote }}
|
|
||||||
{{- if .Values.ingress.tls.enabled }}
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- {{ .Values.ingress.host | quote }}
|
|
||||||
secretName: {{ .Values.ingress.tls.secretName }}
|
|
||||||
{{- end }}
|
|
||||||
rules:
|
|
||||||
- host: {{ .Values.ingress.host | quote }}
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: registry-server
|
|
||||||
port:
|
|
||||||
number: {{ .Values.registry.service.port }}
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
name: {{ .Values.global.namespace }}
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
{{- if .Values.networkPolicy.enabled }}
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: NetworkPolicy
|
|
||||||
metadata:
|
|
||||||
name: {{ include "terraform-registry.fullname" . }}-netpol
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
podSelector: {}
|
|
||||||
policyTypes:
|
|
||||||
- Ingress
|
|
||||||
- Egress
|
|
||||||
ingress:
|
|
||||||
- from:
|
|
||||||
- namespaceSelector:
|
|
||||||
matchLabels:
|
|
||||||
kubernetes.io/metadata.name: ingress-nginx
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.registry.service.targetPort }}
|
|
||||||
protocol: TCP
|
|
||||||
egress:
|
|
||||||
- to: []
|
|
||||||
ports:
|
|
||||||
- port: 443 # S3, Vault
|
|
||||||
protocol: TCP
|
|
||||||
- port: 53 # DNS
|
|
||||||
protocol: UDP
|
|
||||||
- port: 53
|
|
||||||
protocol: TCP
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: terraform-operator
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
app: terraform-operator
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
replicas: {{ .Values.operator.replicas }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: terraform-operator
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: terraform-operator
|
|
||||||
spec:
|
|
||||||
serviceAccountName: terraform-operator
|
|
||||||
containers:
|
|
||||||
- name: operator
|
|
||||||
image: "{{ .Values.operator.image.repository }}:{{ .Values.operator.image.tag }}"
|
|
||||||
imagePullPolicy: IfNotPresent
|
|
||||||
env:
|
|
||||||
- name: REGISTRY_HOSTNAME
|
|
||||||
value: {{ .Values.global.registryHostname | quote }}
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: {{ .Values.operator.resources.requests.cpu }}
|
|
||||||
memory: {{ .Values.operator.resources.requests.memory }}
|
|
||||||
limits:
|
|
||||||
cpu: {{ .Values.operator.resources.limits.cpu }}
|
|
||||||
memory: {{ .Values.operator.resources.limits.memory }}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
{{- if .Values.pdb.enabled }}
|
|
||||||
apiVersion: policy/v1
|
|
||||||
kind: PodDisruptionBudget
|
|
||||||
metadata:
|
|
||||||
name: {{ include "terraform-registry.fullname" . }}-registry
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
minAvailable: {{ .Values.pdb.minAvailable }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: registry-server
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: terraform-operator
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: Role
|
|
||||||
metadata:
|
|
||||||
name: terraform-operator
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
rules:
|
|
||||||
- apiGroups: ["terra.core.nubes.ru"]
|
|
||||||
resources: ["providerreleases", "providerreleases/status"]
|
|
||||||
verbs: ["get", "list", "watch", "update", "patch"]
|
|
||||||
- apiGroups: ["batch"]
|
|
||||||
resources: ["jobs"]
|
|
||||||
verbs: ["create", "get", "list", "watch", "delete"]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["pods", "pods/log"]
|
|
||||||
verbs: ["get", "list", "watch"]
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: RoleBinding
|
|
||||||
metadata:
|
|
||||||
name: terraform-operator
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: terraform-operator
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
roleRef:
|
|
||||||
kind: Role
|
|
||||||
name: terraform-operator
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
@@ -1,64 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: registry-server
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
app: registry-server
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
replicas: {{ .Values.registry.replicas }}
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: registry-server
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: registry-server
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: registry-server
|
|
||||||
image: "{{ .Values.registry.image.repository }}:{{ .Values.registry.image.tag }}"
|
|
||||||
imagePullPolicy: {{ .Values.registry.image.pullPolicy }}
|
|
||||||
ports:
|
|
||||||
- containerPort: {{ .Values.registry.service.targetPort }}
|
|
||||||
env:
|
|
||||||
- name: REGISTRY_HOSTNAME
|
|
||||||
value: {{ .Values.global.registryHostname | quote }}
|
|
||||||
- name: S3_ENDPOINT
|
|
||||||
value: {{ .Values.s3.endpoint | quote }}
|
|
||||||
- name: S3_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{ .Values.s3.existingSecret }}
|
|
||||||
key: {{ .Values.s3.accessKeyField }}
|
|
||||||
- name: S3_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: {{ .Values.s3.existingSecret }}
|
|
||||||
key: {{ .Values.s3.secretKeyField }}
|
|
||||||
- name: S3_BUCKET
|
|
||||||
value: {{ .Values.s3.bucket | quote }}
|
|
||||||
- name: S3_USE_SSL
|
|
||||||
value: {{ .Values.s3.useSSL | quote }}
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: {{ .Values.registry.resources.requests.cpu }}
|
|
||||||
memory: {{ .Values.registry.resources.requests.memory }}
|
|
||||||
limits:
|
|
||||||
cpu: {{ .Values.registry.resources.limits.cpu }}
|
|
||||||
memory: {{ .Values.registry.resources.limits.memory }}
|
|
||||||
{{- if .Values.registry.healthcheck.enabled }}
|
|
||||||
livenessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: {{ .Values.registry.healthcheck.path }}
|
|
||||||
port: {{ .Values.registry.healthcheck.port }}
|
|
||||||
initialDelaySeconds: 15
|
|
||||||
periodSeconds: 20
|
|
||||||
readinessProbe:
|
|
||||||
httpGet:
|
|
||||||
path: {{ .Values.registry.healthcheck.path }}
|
|
||||||
port: {{ .Values.registry.healthcheck.port }}
|
|
||||||
initialDelaySeconds: 5
|
|
||||||
periodSeconds: 10
|
|
||||||
{{- end }}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: registry-server
|
|
||||||
namespace: {{ .Values.global.namespace }}
|
|
||||||
labels:
|
|
||||||
app: registry-server
|
|
||||||
{{- include "terraform-registry.labels" . | nindent 4 }}
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: registry-server
|
|
||||||
ports:
|
|
||||||
- port: {{ .Values.registry.service.port }}
|
|
||||||
targetPort: {{ .Values.registry.service.targetPort }}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
global:
|
|
||||||
registryHostname: "terra.k8c.ru"
|
|
||||||
registry:
|
|
||||||
replicas: 1
|
|
||||||
pdb:
|
|
||||||
enabled: false
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
global:
|
|
||||||
registryHostname: "registry.nubes.ru" # Целевой домен
|
|
||||||
registry:
|
|
||||||
image:
|
|
||||||
repository: "pearlharbor.registryk8s.services.ngcloud.ru/terraform/registry-server"
|
|
||||||
replicas: 2
|
|
||||||
operator:
|
|
||||||
image:
|
|
||||||
repository: "pearlharbor.registryk8s.services.ngcloud.ru/terraform/registry-operator"
|
|
||||||
pdb:
|
|
||||||
enabled: true
|
|
||||||
minAvailable: 1
|
|
||||||
networkPolicy:
|
|
||||||
enabled: true
|
|
||||||
@@ -1,71 +0,0 @@
|
|||||||
global:
|
|
||||||
registryHostname: "terra.k8c.ru" # Переопределяется при миграции
|
|
||||||
namespace: "terraform-registry"
|
|
||||||
|
|
||||||
registry:
|
|
||||||
image:
|
|
||||||
repository: "naeel/terraform-registry-server" # → Harbor при миграции
|
|
||||||
tag: "latest"
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
replicas: 1 # → 2 в prod
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 100m
|
|
||||||
memory: 128Mi
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 256Mi
|
|
||||||
service:
|
|
||||||
port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
healthcheck:
|
|
||||||
enabled: true
|
|
||||||
path: /healthz
|
|
||||||
port: 8080
|
|
||||||
|
|
||||||
operator:
|
|
||||||
image:
|
|
||||||
repository: "naeel/terraform-registry-operator"
|
|
||||||
tag: "latest"
|
|
||||||
replicas: 1
|
|
||||||
resources:
|
|
||||||
requests:
|
|
||||||
cpu: 50m
|
|
||||||
memory: 64Mi
|
|
||||||
limits:
|
|
||||||
cpu: 500m
|
|
||||||
memory: 128Mi
|
|
||||||
|
|
||||||
docs:
|
|
||||||
enabled: false
|
|
||||||
image:
|
|
||||||
repository: "naeel/terraform-registry-server"
|
|
||||||
tag: "docs-dev-fixed"
|
|
||||||
pullPolicy: IfNotPresent
|
|
||||||
|
|
||||||
ingress:
|
|
||||||
enabled: true
|
|
||||||
className: "nginx"
|
|
||||||
host: "terra.k8c.ru" # Переопределяется
|
|
||||||
tls:
|
|
||||||
enabled: true
|
|
||||||
issuer: "letsencrypt-prod" # НЕ ТРОГАТЬ LetsEncrypt issuer!
|
|
||||||
secretName: "registry-tls"
|
|
||||||
annotations:
|
|
||||||
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
|
|
||||||
|
|
||||||
s3:
|
|
||||||
endpoint: "s3.msk-1.ngcloud.ru"
|
|
||||||
bucket: "terraform-registry"
|
|
||||||
useSSL: true
|
|
||||||
# credentials через existingSecret
|
|
||||||
existingSecret: "s3-credentials"
|
|
||||||
accessKeyField: "access-key"
|
|
||||||
secretKeyField: "secret-key"
|
|
||||||
|
|
||||||
pdb:
|
|
||||||
enabled: false # → true в prod
|
|
||||||
minAvailable: 1
|
|
||||||
|
|
||||||
networkPolicy:
|
|
||||||
enabled: false # → true при миграции
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# k8s 🔧
|
|
||||||
|
|
||||||
Кратко: Kubernetes‑манифесты для namespace `terra` — развёртывание `registry-server`, `terraform-operator`, Ingress, TLS и связанные ресурсы (S3, CRD, builder ConfigMap).
|
|
||||||
|
|
||||||
## Важные файлы
|
|
||||||
- `00-namespace.yaml` — namespace `terra`
|
|
||||||
- `00-rbac.yaml` — RBAC для оператора
|
|
||||||
- `02-crd.yaml` — CRD для операторов (ProviderRelease)
|
|
||||||
- `03-build-script.yaml` — ConfigMap для сборщика Job'ов
|
|
||||||
- `04-operator-deployment.yaml` — Deployment для оператора
|
|
||||||
- `05-registry-server.yaml`, `06-registry-server-docs.yaml` — Deployment, Service и Ingress для `registry-server`
|
|
||||||
|
|
||||||
## Как перенести на новый домен (коротко) ✅
|
|
||||||
1. **Ingress**: в манифесте (обычно `05-*` или `registry-ingress`) заменить `host: terra.k8c.ru` на новый домен и убедиться в корректной аннотации `cert-manager.io/cluster-issuer` (или задать другой `secretName`).
|
|
||||||
2. **Certificate**: cert-manager автоматически выпустит/обновит TLS; проверьте `kubectl -n terra get certificate registry-tls -o yaml` и дождитесь `status.conditions[*].type == Ready`.
|
|
||||||
3. **Deployment env**: заменить `REGISTRY_HOSTNAME` в `04-operator-deployment.yaml` и в `registry-server` на новый домен.
|
|
||||||
4. **DNS**: добавить A/CNAME запись для нового домена, указывающую на IP балансировщика (`kubectl -n terra get ingress registry-ingress -o jsonpath='{.status.loadBalancer.ingress[0].ip}'`).
|
|
||||||
5. **Secrets & storage**: используйте `s3-credentials` (S3_ENDPOINT/S3_ACCESS_KEY/S3_SECRET_KEY) и убедитесь в доступности S3.
|
|
||||||
6. **Apply & Verify**:
|
|
||||||
- `kubectl apply -f manifests/`
|
|
||||||
- `kubectl -n terra describe ingress registry-ingress`
|
|
||||||
- `kubectl -n terra get certificate registry-tls`
|
|
||||||
- `curl -vk https://<domain>/docs/<namespace>/<name>/<version>/`
|
|
||||||
|
|
||||||
## Проверки (read-only)
|
|
||||||
- `kubectl -n terra get ingress,svc,deploy,po,secret`
|
|
||||||
- `kubectl -n terra get certificate registry-tls -o yaml`
|
|
||||||
- `kubectl -n terra logs <registry-pod>`
|
|
||||||
|
|
||||||
> Примечание: это инструкция для операции вручную/CI; я ничего не меняю в кластере автоматически.
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: my-deployment
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: my-app
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: my-app
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: my-container
|
|
||||||
image: my-image:latest
|
|
||||||
ports:
|
|
||||||
- containerPort: 80
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: my-ingress
|
|
||||||
spec:
|
|
||||||
rules:
|
|
||||||
- host: example.local
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: my-service
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- deployment.yaml
|
|
||||||
- service.yaml
|
|
||||||
- ingress.yaml
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: my-service
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: my-app
|
|
||||||
ports:
|
|
||||||
- protocol: TCP
|
|
||||||
port: 80
|
|
||||||
targetPort: 80
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: ClusterIssuer
|
|
||||||
metadata:
|
|
||||||
name: selfsigned-dev
|
|
||||||
spec:
|
|
||||||
selfSigned: {}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
apiVersion: cert-manager.io/v1
|
|
||||||
kind: ClusterIssuer
|
|
||||||
metadata:
|
|
||||||
name: letsencrypt-staging-dev
|
|
||||||
spec:
|
|
||||||
acme:
|
|
||||||
server: https://acme-staging-v02.api.letsencrypt.org/directory
|
|
||||||
email: devops@example.com
|
|
||||||
privateKeySecretRef:
|
|
||||||
name: letsencrypt-staging-dev
|
|
||||||
solvers:
|
|
||||||
- http01:
|
|
||||||
ingress:
|
|
||||||
class: nginx
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: my-deployment
|
|
||||||
spec:
|
|
||||||
template:
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: my-container
|
|
||||||
image: nginx:stable
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: my-ingress
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: selfsigned-dev
|
|
||||||
kubernetes.io/ingress.class: nginx
|
|
||||||
spec:
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- registry.containerk8s.dev.nubes.ru
|
|
||||||
secretName: registry-containerk8s-tls
|
|
||||||
rules:
|
|
||||||
- host: registry.containerk8s.dev.nubes.ru
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: my-service
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
apiVersion: kustomize.config.k8s.io/v1beta1
|
|
||||||
kind: Kustomization
|
|
||||||
resources:
|
|
||||||
- ../../base
|
|
||||||
namespace: terra-dev
|
|
||||||
patchesStrategicMerge:
|
|
||||||
- ingress-patch.yaml
|
|
||||||
- image-patch.yaml
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: registry-server
|
|
||||||
namespace: terra
|
|
||||||
labels:
|
|
||||||
app: registry-server
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: registry-server
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: registry-server
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: registry-server
|
|
||||||
image: naeel/terraform-registry-server:docs-dev
|
|
||||||
imagePullPolicy: Always
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
env:
|
|
||||||
- name: REGISTRY_HOSTNAME
|
|
||||||
value: "terra.k8c.ru"
|
|
||||||
- name: S3_ENDPOINT
|
|
||||||
value: "s3.msk-1.ngcloud.ru"
|
|
||||||
- name: S3_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: s3-credentials
|
|
||||||
key: access-key
|
|
||||||
- name: S3_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
name: s3-credentials
|
|
||||||
key: secret-key
|
|
||||||
- name: S3_BUCKET
|
|
||||||
value: "terraform-registry"
|
|
||||||
- name: S3_USE_SSL
|
|
||||||
value: "true"
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: registry-ingress
|
|
||||||
namespace: terra
|
|
||||||
annotations:
|
|
||||||
cert-manager.io/cluster-issuer: "letsencrypt-prod"
|
|
||||||
nginx.ingress.kubernetes.io/proxy-body-size: "100m"
|
|
||||||
nginx.ingress.kubernetes.io/rewrite-target: "/$2"
|
|
||||||
spec:
|
|
||||||
ingressClassName: nginx
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- terra.k8c.ru
|
|
||||||
secretName: registry-tls
|
|
||||||
rules:
|
|
||||||
- host: terra.k8c.ru
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: "/()(.*)"
|
|
||||||
pathType: ImplementationSpecific
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: registry-server
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# Exclude files not needed in Docker build context for operator
|
|
||||||
.git
|
|
||||||
.gitignore
|
|
||||||
.dockerignore
|
|
||||||
|
|
||||||
# Docs / markdown
|
|
||||||
*.md
|
|
||||||
docs/
|
|
||||||
|
|
||||||
# Other services / infra
|
|
||||||
server/
|
|
||||||
scripts/
|
|
||||||
charts/
|
|
||||||
k8s/
|
|
||||||
|
|
||||||
# Secrets & keys
|
|
||||||
secrets/
|
|
||||||
*.pem
|
|
||||||
*.key
|
|
||||||
*.asc
|
|
||||||
*.gpg
|
|
||||||
*.pgp
|
|
||||||
|
|
||||||
# Artefacts / temp
|
|
||||||
artifacts/
|
|
||||||
*.har
|
|
||||||
HAR/
|
|
||||||
registry-server-build/
|
|
||||||
universal_rebuild/
|
|
||||||
tools/
|
|
||||||
devops/
|
|
||||||
|
|
||||||
# IDE / OS
|
|
||||||
.idea/
|
|
||||||
.vscode/
|
|
||||||
*.swp
|
|
||||||
*.swo
|
|
||||||
*~
|
|
||||||
.DS_Store
|
|
||||||
|
|
||||||
# Go test/coverage
|
|
||||||
*.test
|
|
||||||
*.out
|
|
||||||
coverage.out
|
|
||||||
|
|
||||||
# Logs
|
|
||||||
*.log
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
IMAGE_REPO ?= naeel
|
|
||||||
TAG ?= latest
|
|
||||||
PLATFORM ?= linux/amd64
|
|
||||||
|
|
||||||
.PHONY: build-all push-all build-operator push-operator build-registry push-registry tidy
|
|
||||||
|
|
||||||
tidy:
|
|
||||||
go mod tidy
|
|
||||||
|
|
||||||
build-all: build-operator build-registry
|
|
||||||
push-all: push-operator push-registry
|
|
||||||
|
|
||||||
build-operator:
|
|
||||||
docker build --platform $(PLATFORM) -t $(IMAGE_REPO)/terraform-registry-operator:$(TAG) -f build/Dockerfile.operator .
|
|
||||||
|
|
||||||
push-operator:
|
|
||||||
docker push $(IMAGE_REPO)/terraform-registry-operator:$(TAG)
|
|
||||||
|
|
||||||
build-registry:
|
|
||||||
docker build --platform $(PLATFORM) -t $(IMAGE_REPO)/terraform-registry-server:$(TAG) -f build/Dockerfile.registry .
|
|
||||||
|
|
||||||
push-registry:
|
|
||||||
docker push $(IMAGE_REPO)/terraform-registry-server:$(TAG)
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
# Terra Operator Manifests
|
|
||||||
|
|
||||||
## Minimal Setup for Namespace `terra`
|
|
||||||
|
|
||||||
### 1. Structure
|
|
||||||
- **Namespace**: `terra`
|
|
||||||
- **Storage**: внешний S3 (Nubes Cloud).
|
|
||||||
- **CRD**: `TerraformProviderRelease` definitions.
|
|
||||||
|
|
||||||
### 2. How to apply
|
|
||||||
```bash
|
|
||||||
kubectl apply -f operator/manifests/00-namespace.yaml
|
|
||||||
kubectl apply -f operator/manifests/02-crd.yaml
|
|
||||||
kubectl apply -f operator/manifests/04-operator-deployment.yaml
|
|
||||||
kubectl apply -f operator/manifests/05-registry-server.yaml
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. Usage
|
|
||||||
Create a release request:
|
|
||||||
```yaml
|
|
||||||
apiVersion: terra.core.nubes.ru/v1alpha1
|
|
||||||
kind: TerraformProviderRelease
|
|
||||||
metadata:
|
|
||||||
name: mycloud-v0-1-0
|
|
||||||
namespace: terra
|
|
||||||
spec:
|
|
||||||
providerName: "mycloud"
|
|
||||||
version: "0.1.0"
|
|
||||||
gitRepo: "https://github.com/Start-Ops/terraform-provider-mycloud.git"
|
|
||||||
gitRef: "v0.1.0"
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Простыми словами: что это и зачем
|
|
||||||
|
|
||||||
Это набор манифестов и небольшой сервис, который автоматизирует сборку Terraform‑провайдеров.
|
|
||||||
Идея простая: я описываю в CRD, какой провайдер и какую версию собрать, а оператор сам запускает Job,
|
|
||||||
собирает бинарники и складывает их в хранилище. Дальше registry‑server отдает эти артефакты Terraform‑клиентам.
|
|
||||||
|
|
||||||
Зачем это нужно:
|
|
||||||
- чтобы не собирать провайдеры руками;
|
|
||||||
- чтобы хранить артефакты в одном месте;
|
|
||||||
- чтобы Terraform мог скачивать провайдеры по обычному источнику.
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
# Build Stage
|
|
||||||
FROM golang:1.24-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
RUN go mod download
|
|
||||||
|
|
||||||
COPY cmd ./cmd
|
|
||||||
# Build statically
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /operator cmd/main.go
|
|
||||||
|
|
||||||
# Run Stage
|
|
||||||
FROM alpine:3.19
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=builder /operator /app/operator
|
|
||||||
USER 1000:1000
|
|
||||||
|
|
||||||
CMD ["/app/operator"]
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# Build Stage
|
|
||||||
FROM golang:1.24-alpine AS builder
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
COPY go.mod go.sum ./
|
|
||||||
COPY registrykeys ./registrykeys
|
|
||||||
RUN go mod download
|
|
||||||
COPY cmd/registry ./cmd/registry
|
|
||||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /registry-server ./cmd/registry
|
|
||||||
|
|
||||||
# Run Stage
|
|
||||||
FROM alpine:3.19
|
|
||||||
WORKDIR /app
|
|
||||||
COPY --from=builder /registry-server /app/registry-server
|
|
||||||
RUN apk add --no-cache ca-certificates
|
|
||||||
|
|
||||||
USER 1000:1000
|
|
||||||
EXPOSE 8080
|
|
||||||
|
|
||||||
CMD ["/app/registry-server"]
|
|
||||||
@@ -1,116 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"mime"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
s3 "github.com/minio/minio-go/v7"
|
|
||||||
)
|
|
||||||
|
|
||||||
// parseDocsRequestPath parses paths like:
|
|
||||||
// /docs/<namespace>/<name>/<version>/... (rest may be empty)
|
|
||||||
func parseDocsRequestPath(p string) (namespace, name, version, rest string, err error) {
|
|
||||||
p = strings.TrimPrefix(p, "/")
|
|
||||||
p = strings.TrimPrefix(p, "docs/")
|
|
||||||
parts := strings.SplitN(p, "/", 4)
|
|
||||||
if len(parts) < 3 {
|
|
||||||
err = fmt.Errorf("invalid docs path: %s", p)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
namespace = parts[0]
|
|
||||||
name = parts[1]
|
|
||||||
version = parts[2]
|
|
||||||
if len(parts) == 3 {
|
|
||||||
rest = ""
|
|
||||||
} else {
|
|
||||||
rest = parts[3]
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// docsObjectKey builds the S3 key for a docs object given parsed parts.
|
|
||||||
func docsObjectKey(namespace, name, version, pathPart string) string {
|
|
||||||
clean := strings.TrimPrefix(pathPart, "/")
|
|
||||||
if clean == "" {
|
|
||||||
return fmt.Sprintf("docs/%s/%s/%s/index.html", namespace, name, version)
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("docs/%s/%s/%s/%s", namespace, name, version, clean)
|
|
||||||
}
|
|
||||||
|
|
||||||
// tryCandidateKeys returns a list of keys to attempt for a given request path.
|
|
||||||
// Order matters: exact path first, then <path>/index.html, then top-level index.
|
|
||||||
func tryCandidateKeys(namespace, name, version, rest string) []string {
|
|
||||||
keys := []string{}
|
|
||||||
if rest == "" {
|
|
||||||
keys = append(keys, docsObjectKey(namespace, name, version, "index.html"))
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
// exact
|
|
||||||
keys = append(keys, docsObjectKey(namespace, name, version, rest))
|
|
||||||
// if it looks like a directory or has no extension, try index under it
|
|
||||||
if strings.HasSuffix(rest, "/") || filepath.Ext(rest) == "" {
|
|
||||||
keys = append(keys, docsObjectKey(namespace, name, version, strings.TrimSuffix(rest, "/")+"/index.html"))
|
|
||||||
}
|
|
||||||
// finally, try root index
|
|
||||||
keys = append(keys, docsObjectKey(namespace, name, version, "index.html"))
|
|
||||||
return keys
|
|
||||||
}
|
|
||||||
|
|
||||||
// docsHandler serves static documentation files from S3 (public-facing via Ingress).
|
|
||||||
func docsHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ns, name, ver, rest, err := parseDocsRequestPath(r.URL.Path)
|
|
||||||
if err != nil {
|
|
||||||
http.Error(w, "Bad docs path", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx := context.Background()
|
|
||||||
candidates := tryCandidateKeys(ns, name, ver, rest)
|
|
||||||
log.Printf("Docs candidates (host=%s): %v", hostname, candidates)
|
|
||||||
|
|
||||||
var lastErr error
|
|
||||||
for _, key := range candidates {
|
|
||||||
obj, err := s3Client.GetObject(ctx, bucketName, key, s3.GetObjectOptions{})
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
stat, err := obj.Stat()
|
|
||||||
if err != nil {
|
|
||||||
lastErr = err
|
|
||||||
_ = obj.Close()
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// Determine content-type
|
|
||||||
ext := filepath.Ext(key)
|
|
||||||
ctype := mime.TypeByExtension(ext)
|
|
||||||
if ctype == "" {
|
|
||||||
// fallback for HTML
|
|
||||||
if ext == ".html" || strings.HasSuffix(key, "index.html") {
|
|
||||||
ctype = "text/html; charset=utf-8"
|
|
||||||
} else {
|
|
||||||
ctype = "application/octet-stream"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", ctype)
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
|
|
||||||
w.Header().Set("Last-Modified", stat.LastModified.Format(http.TimeFormat))
|
|
||||||
|
|
||||||
if _, err := io.Copy(w, obj); err != nil {
|
|
||||||
log.Printf("Error streaming object %s: %v", key, err)
|
|
||||||
}
|
|
||||||
_ = obj.Close()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Printf("Docs not found in candidates: %v, lastErr: %v", candidates, lastErr)
|
|
||||||
http.Error(w, "Documentation not found", http.StatusNotFound)
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import registrykeys "terraform-registry-keys"
|
|
||||||
|
|
||||||
var gpgPublicKey = GPGPublicKey{
|
|
||||||
KeyID: registrykeys.KeyID,
|
|
||||||
ASCIIArmor: registrykeys.ASCIIArmor,
|
|
||||||
}
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bufio"
|
|
||||||
"context"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
s3 "github.com/minio/minio-go/v7"
|
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
s3Client *s3.Client
|
|
||||||
bucketName = "terraform-registry" // Default
|
|
||||||
hostname = os.Getenv("REGISTRY_HOSTNAME")
|
|
||||||
)
|
|
||||||
|
|
||||||
// Terraform Registry Protocol Structs
|
|
||||||
type Discovery struct {
|
|
||||||
ProvidersV1 string `json:"providers.v1"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type VersionList struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
Versions []Version `json:"versions"`
|
|
||||||
Warnings []string `json:"warnings"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Version struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
Protocols []string `json:"protocols"`
|
|
||||||
Platforms []Platform `json:"platforms"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type Platform struct {
|
|
||||||
OS string `json:"os"`
|
|
||||||
Arch string `json:"arch"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type DownloadResponse struct {
|
|
||||||
Protocols []string `json:"protocols"`
|
|
||||||
OS string `json:"os"`
|
|
||||||
Arch string `json:"arch"`
|
|
||||||
Filename string `json:"filename"`
|
|
||||||
DownloadURL string `json:"download_url"`
|
|
||||||
ShasumsURL string `json:"shasums_url"`
|
|
||||||
ShasumsSignatureURL string `json:"shasums_signature_url"`
|
|
||||||
Shasum string `json:"shasum"`
|
|
||||||
SigningKeys SigningKeys `json:"signing_keys"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type SigningKeys struct {
|
|
||||||
GPGPublicKeys []GPGPublicKey `json:"gpg_public_keys"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GPGPublicKey struct {
|
|
||||||
KeyID string `json:"key_id"`
|
|
||||||
ASCIIArmor string `json:"ascii_armor"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
// 1. Init S3 Connection
|
|
||||||
if hostname == "" {
|
|
||||||
hostname = "localhost:8080"
|
|
||||||
}
|
|
||||||
|
|
||||||
endpoint := os.Getenv("S3_ENDPOINT")
|
|
||||||
accessKeyID := os.Getenv("S3_ACCESS_KEY")
|
|
||||||
secretAccessKey := os.Getenv("S3_SECRET_KEY")
|
|
||||||
|
|
||||||
if os.Getenv("S3_BUCKET") != "" {
|
|
||||||
bucketName = os.Getenv("S3_BUCKET")
|
|
||||||
}
|
|
||||||
|
|
||||||
var err error
|
|
||||||
s3Client, err = s3.New(endpoint, &s3.Options{
|
|
||||||
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
|
|
||||||
Secure: true, // Force secure for cloud S3
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
log.Fatalln(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. HTTP Handlers
|
|
||||||
http.HandleFunc("/.well-known/terraform.json", discoveryHandler)
|
|
||||||
http.HandleFunc("/v1/providers/", router)
|
|
||||||
http.HandleFunc("/v1/proxy", proxyHandler)
|
|
||||||
http.HandleFunc("/docs/", docsHandler)
|
|
||||||
http.Handle("/", http.HandlerFunc(rootHandler))
|
|
||||||
|
|
||||||
log.Printf("Starting Registry Service on :8080 (Bucket: %s, Endpoint: %s)\n", bucketName, endpoint)
|
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
func rootHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
if r.URL.Path != "/" {
|
|
||||||
http.NotFound(w, r)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
|
||||||
fmt.Fprint(w, `
|
|
||||||
<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head><title>Terra Registry</title></head>
|
|
||||||
<body>
|
|
||||||
<h1>Terra Registry & Documentation Server</h1>
|
|
||||||
<p>Status: <span style="color: green">ONLINE</span></p>
|
|
||||||
<hr>
|
|
||||||
<p>Powered by Nubes Cloud S3 Storage</p>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
`)
|
|
||||||
}
|
|
||||||
|
|
||||||
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
bucket := r.URL.Query().Get("bucket")
|
|
||||||
key := r.URL.Query().Get("key")
|
|
||||||
|
|
||||||
if bucket == "" || key == "" {
|
|
||||||
http.Error(w, "Missing bucket or key params", http.StatusBadRequest)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
obj, err := s3Client.GetObject(context.Background(), bucket, key, s3.GetObjectOptions{})
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error getting object %s/%s: %v", bucket, key, err)
|
|
||||||
http.Error(w, "File not found", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
defer obj.Close()
|
|
||||||
|
|
||||||
stat, err := obj.Stat()
|
|
||||||
if err != nil {
|
|
||||||
log.Printf("Error stating object %s/%s: %v", bucket, key, err)
|
|
||||||
http.Error(w, "File not found or not accessible", http.StatusNotFound)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
|
|
||||||
w.Header().Set("Content-Type", "application/octet-stream")
|
|
||||||
w.Header().Set("Last-Modified", stat.LastModified.Format(http.TimeFormat))
|
|
||||||
|
|
||||||
if _, err := io.Copy(w, obj); err != nil {
|
|
||||||
log.Printf("Error streaming object: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func discoveryHandler(w http.ResponseWriter, r *http.Request) {
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(Discovery{ProvidersV1: "/v1/providers/"})
|
|
||||||
}
|
|
||||||
|
|
||||||
func router(w http.ResponseWriter, r *http.Request) {
|
|
||||||
path := strings.TrimPrefix(r.URL.Path, "/v1/providers/")
|
|
||||||
parts := strings.Split(path, "/")
|
|
||||||
|
|
||||||
if len(parts) == 3 && parts[2] == "versions" {
|
|
||||||
listVersions(w, r, parts[0], parts[1])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(parts) == 6 && parts[3] == "download" {
|
|
||||||
downloadVersion(w, r, parts[0], parts[1], parts[2], parts[4], parts[5])
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
http.Error(w, "Not Found", http.StatusNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func listVersions(w http.ResponseWriter, r *http.Request, namespace, pType string) {
|
|
||||||
prefix := fmt.Sprintf("%s/%s/%s/", hostname, namespace, pType)
|
|
||||||
ctx := context.Background()
|
|
||||||
versions := []Version{}
|
|
||||||
seenVersions := map[string]*Version{}
|
|
||||||
|
|
||||||
objectCh := s3Client.ListObjects(ctx, bucketName, s3.ListObjectsOptions{
|
|
||||||
Prefix: prefix,
|
|
||||||
Recursive: true,
|
|
||||||
})
|
|
||||||
|
|
||||||
for object := range objectCh {
|
|
||||||
if object.Err != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
parts := strings.Split(object.Key, "/")
|
|
||||||
if len(parts) < 5 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
verStr := parts[3]
|
|
||||||
fileName := parts[4]
|
|
||||||
|
|
||||||
if _, ok := seenVersions[verStr]; !ok {
|
|
||||||
seenVersions[verStr] = &Version{
|
|
||||||
Version: verStr,
|
|
||||||
Protocols: []string{"5.0"},
|
|
||||||
Platforms: []Platform{},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if strings.Contains(fileName, "_linux_amd64.zip") {
|
|
||||||
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "linux", Arch: "amd64"})
|
|
||||||
}
|
|
||||||
if strings.Contains(fileName, "_windows_amd64.zip") {
|
|
||||||
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "windows", Arch: "amd64"})
|
|
||||||
}
|
|
||||||
if strings.Contains(fileName, "_darwin_amd64.zip") {
|
|
||||||
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "amd64"})
|
|
||||||
}
|
|
||||||
if strings.Contains(fileName, "_darwin_arm64.zip") {
|
|
||||||
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "arm64"})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, v := range seenVersions {
|
|
||||||
versions = append(versions, *v)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := VersionList{
|
|
||||||
ID: fmt.Sprintf("%s/%s/%s", hostname, namespace, pType),
|
|
||||||
Versions: versions,
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(resp)
|
|
||||||
}
|
|
||||||
|
|
||||||
func downloadVersion(w http.ResponseWriter, r *http.Request, namespace, pType, version, osType, arch string) {
|
|
||||||
basePath := fmt.Sprintf("%s/%s/%s/%s", hostname, namespace, pType, version)
|
|
||||||
filename := fmt.Sprintf("terraform-provider-%s_%s_%s_%s.zip", pType, version, osType, arch)
|
|
||||||
fullKey := fmt.Sprintf("%s/%s", basePath, filename)
|
|
||||||
shasumsKey := fmt.Sprintf("%s/terraform-provider-%s_%s_SHA256SUMS", basePath, pType, version)
|
|
||||||
sigKey := fmt.Sprintf("%s/terraform-provider-%s_%s_SHA256SUMS.sig", basePath, pType, version)
|
|
||||||
|
|
||||||
var shasumValue string
|
|
||||||
shasumsObj, err := s3Client.GetObject(context.Background(), bucketName, shasumsKey, s3.GetObjectOptions{})
|
|
||||||
if err == nil {
|
|
||||||
defer shasumsObj.Close()
|
|
||||||
scanner := bufio.NewScanner(shasumsObj)
|
|
||||||
for scanner.Scan() {
|
|
||||||
line := scanner.Text()
|
|
||||||
if strings.Contains(line, filename) {
|
|
||||||
fields := strings.Fields(line)
|
|
||||||
if len(fields) >= 1 {
|
|
||||||
shasumValue = fields[0]
|
|
||||||
}
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
baseURL := "https://" + hostname
|
|
||||||
downloadLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(fullKey))
|
|
||||||
shasumsLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(shasumsKey))
|
|
||||||
sigLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(sigKey))
|
|
||||||
|
|
||||||
gpgKey := gpgPublicKey
|
|
||||||
|
|
||||||
resp := DownloadResponse{
|
|
||||||
Protocols: []string{"5.0"},
|
|
||||||
OS: osType,
|
|
||||||
Arch: arch,
|
|
||||||
Filename: filename,
|
|
||||||
DownloadURL: downloadLink,
|
|
||||||
ShasumsURL: shasumsLink,
|
|
||||||
ShasumsSignatureURL: sigLink,
|
|
||||||
Shasum: shasumValue,
|
|
||||||
SigningKeys: SigningKeys{
|
|
||||||
GPGPublicKeys: []GPGPublicKey{gpgKey},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
json.NewEncoder(w).Encode(resp)
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
apiVersion: terra.core.nubes.ru/v1alpha1
|
|
||||||
kind: TerraformProviderRelease
|
|
||||||
metadata:
|
|
||||||
name: scaffolding-v0-0-1
|
|
||||||
namespace: terra
|
|
||||||
spec:
|
|
||||||
providerName: "scaffolding"
|
|
||||||
namespace: "hashicorp"
|
|
||||||
version: "0.0.1"
|
|
||||||
gitRepo: "https://github.com/hashicorp/terraform-provider-scaffolding.git"
|
|
||||||
gitRef: "main"
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
module terraform-provider-operator
|
|
||||||
|
|
||||||
go 1.24.0
|
|
||||||
|
|
||||||
toolchain go1.24.12
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/minio/minio-go/v7 v7.0.98
|
|
||||||
terraform-registry-keys v0.0.0
|
|
||||||
)
|
|
||||||
|
|
||||||
require (
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
|
||||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
|
||||||
github.com/go-ini/ini v1.67.0 // indirect
|
|
||||||
github.com/google/uuid v1.6.0 // indirect
|
|
||||||
github.com/klauspost/compress v1.18.2 // indirect
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
|
||||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
|
||||||
github.com/kr/pretty v0.3.1 // indirect
|
|
||||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
|
||||||
github.com/minio/md5-simd v1.1.2 // indirect
|
|
||||||
github.com/philhofer/fwd v1.2.0 // indirect
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
|
||||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
|
||||||
github.com/rs/xid v1.6.0 // indirect
|
|
||||||
github.com/tinylib/msgp v1.6.1 // indirect
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
|
||||||
golang.org/x/crypto v0.46.0 // indirect
|
|
||||||
golang.org/x/net v0.48.0 // indirect
|
|
||||||
golang.org/x/sys v0.39.0 // indirect
|
|
||||||
golang.org/x/text v0.32.0 // indirect
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
|
||||||
)
|
|
||||||
|
|
||||||
replace terraform-registry-keys => ./registrykeys
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
|
||||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
|
||||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
|
||||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
|
||||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
|
||||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
|
||||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
|
||||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
|
||||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
|
||||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
|
||||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
|
||||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
|
||||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
|
||||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
|
||||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|
||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
|
||||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
|
||||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
|
||||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
|
||||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
|
||||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
|
||||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
|
||||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
|
||||||
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
|
|
||||||
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
|
|
||||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
|
||||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
|
||||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
|
||||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
|
||||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
|
||||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
|
||||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
|
||||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
|
||||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
|
||||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
|
||||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
|
||||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
|
||||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
|
||||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
|
||||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
|
||||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
|
||||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
|
||||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
|
||||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
|
||||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
|
||||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
|
||||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
|
||||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
|
||||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: Namespace
|
|
||||||
metadata:
|
|
||||||
name: terra
|
|
||||||
labels:
|
|
||||||
name: terra
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ServiceAccount
|
|
||||||
metadata:
|
|
||||||
name: terraform-operator
|
|
||||||
namespace: terra
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: Role
|
|
||||||
metadata:
|
|
||||||
name: terraform-operator
|
|
||||||
namespace: terra
|
|
||||||
rules:
|
|
||||||
- apiGroups: ["terra.core.nubes.ru"]
|
|
||||||
resources: ["providerreleases", "providerreleases/status"]
|
|
||||||
verbs: ["get", "list", "watch", "update", "patch"]
|
|
||||||
- apiGroups: ["batch"]
|
|
||||||
resources: ["jobs"]
|
|
||||||
verbs: ["create", "get", "list", "watch", "delete"]
|
|
||||||
- apiGroups: [""]
|
|
||||||
resources: ["pods", "pods/log"]
|
|
||||||
verbs: ["get", "list", "watch"]
|
|
||||||
---
|
|
||||||
apiVersion: rbac.authorization.k8s.io/v1
|
|
||||||
kind: RoleBinding
|
|
||||||
metadata:
|
|
||||||
name: terraform-operator
|
|
||||||
namespace: terra
|
|
||||||
subjects:
|
|
||||||
- kind: ServiceAccount
|
|
||||||
name: terraform-operator
|
|
||||||
namespace: terra
|
|
||||||
roleRef:
|
|
||||||
kind: Role
|
|
||||||
name: terraform-operator
|
|
||||||
apiGroup: rbac.authorization.k8s.io
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
apiVersion: apiextensions.k8s.io/v1
|
|
||||||
kind: CustomResourceDefinition
|
|
||||||
metadata:
|
|
||||||
name: providerreleases.terra.core.nubes.ru
|
|
||||||
spec:
|
|
||||||
group: terra.core.nubes.ru
|
|
||||||
versions:
|
|
||||||
- name: v1alpha1
|
|
||||||
served: true
|
|
||||||
storage: true
|
|
||||||
subresources:
|
|
||||||
status: {}
|
|
||||||
schema:
|
|
||||||
openAPIV3Schema:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
spec:
|
|
||||||
type: object
|
|
||||||
required:
|
|
||||||
- providerName
|
|
||||||
- version
|
|
||||||
- gitRepo
|
|
||||||
- gitRef
|
|
||||||
properties:
|
|
||||||
providerName:
|
|
||||||
type: string
|
|
||||||
description: "Name of the provider (e.g. 'mycloud')"
|
|
||||||
namespace:
|
|
||||||
type: string
|
|
||||||
default: "mycloud"
|
|
||||||
description: "Terraform registry namespace (e.g. 'nubes')"
|
|
||||||
version:
|
|
||||||
type: string
|
|
||||||
description: "Semantic version without v (e.g. '1.0.1')"
|
|
||||||
gitRepo:
|
|
||||||
type: string
|
|
||||||
description: "Git repository URL to clone"
|
|
||||||
gitRef:
|
|
||||||
type: string
|
|
||||||
description: "Git tag, commit SHA or branch"
|
|
||||||
platforms:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: string
|
|
||||||
default: ["linux_amd64", "windows_amd64"]
|
|
||||||
description: "Target platforms for build"
|
|
||||||
status:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
phase:
|
|
||||||
type: string
|
|
||||||
description: "Current phase: Pending, Building, Publishing, Ready, Failed"
|
|
||||||
downloadUrl:
|
|
||||||
type: string
|
|
||||||
conditions:
|
|
||||||
type: array
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
type:
|
|
||||||
type: string
|
|
||||||
status:
|
|
||||||
type: string
|
|
||||||
lastTransitionTime:
|
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
reason:
|
|
||||||
type: string
|
|
||||||
message:
|
|
||||||
type: string
|
|
||||||
scope: Namespaced
|
|
||||||
names:
|
|
||||||
plural: providerreleases
|
|
||||||
singular: providerrelease
|
|
||||||
kind: TerraformProviderRelease
|
|
||||||
shortNames:
|
|
||||||
- tpr
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
apiVersion: v1
|
|
||||||
kind: ConfigMap
|
|
||||||
metadata:
|
|
||||||
name: builder-script
|
|
||||||
namespace: terra
|
|
||||||
data:
|
|
||||||
build.sh: |
|
|
||||||
#!/bin/sh
|
|
||||||
set -e
|
|
||||||
|
|
||||||
echo ">>> Starting Build for $PROVIDER_NAME $VERSION"
|
|
||||||
|
|
||||||
# 1. Setup Environment
|
|
||||||
# Inputs: GIT_REPO, GIT_REF, PROVIDER_NAME, VERSION
|
|
||||||
WORK_DIR="/workspace"
|
|
||||||
ARTIFACTS_DIR="/artifacts"
|
|
||||||
mkdir -p $WORK_DIR $ARTIFACTS_DIR
|
|
||||||
|
|
||||||
# 2. Clone Repository
|
|
||||||
echo ">>> Cloning $GIT_REPO ($GIT_REF)..."
|
|
||||||
git clone $GIT_REPO $WORK_DIR/src
|
|
||||||
cd $WORK_DIR/src
|
|
||||||
git checkout $GIT_REF
|
|
||||||
|
|
||||||
# 3. Build for Platforms
|
|
||||||
# Hardcoded platforms for MVP: linux/amd64, windows/amd64
|
|
||||||
# Naming convention: terraform-provider-NAME_vVERSION_OS_ARCH.zip
|
|
||||||
|
|
||||||
echo ">>> Building binaries..."
|
|
||||||
|
|
||||||
# Linux amd64
|
|
||||||
echo "--> linux/amd64"
|
|
||||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o terraform-provider-${PROVIDER_NAME}_v${VERSION}
|
|
||||||
zip ${ARTIFACTS_DIR}/terraform-provider-${PROVIDER_NAME}_${VERSION}_linux_amd64.zip terraform-provider-${PROVIDER_NAME}_v${VERSION}
|
|
||||||
rm terraform-provider-${PROVIDER_NAME}_v${VERSION}
|
|
||||||
|
|
||||||
# Windows amd64
|
|
||||||
echo "--> windows/amd64"
|
|
||||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
|
|
||||||
zip ${ARTIFACTS_DIR}/terraform-provider-${PROVIDER_NAME}_${VERSION}_windows_amd64.zip terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
|
|
||||||
rm terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
|
|
||||||
|
|
||||||
# 4. Checksums
|
|
||||||
cd $ARTIFACTS_DIR
|
|
||||||
sha256sum *.zip > terraform-provider-${PROVIDER_NAME}_${VERSION}_SHA256SUMS
|
|
||||||
|
|
||||||
# 5. GPG Sign (Mock for MVP - empty sig or skip if client allows, but Terraform strict.
|
|
||||||
# For now we will generate a dummy signature to satisfy file existence, real signing requires GPG private key in env)
|
|
||||||
# Ideally: gpg --detach-sign --armor terraform-provider-..._SHA256SUMS
|
|
||||||
touch terraform-provider-${PROVIDER_NAME}_${VERSION}_SHA256SUMS.sig
|
|
||||||
|
|
||||||
# 6. Upload to S3
|
|
||||||
# Layout: /terraform-providers/hostname/namespace/name/version/
|
|
||||||
# We will use 'REGISTRY_HOSTNAME' env var if set, else default.
|
|
||||||
|
|
||||||
if [ -z "$REGISTRY_HOSTNAME" ]; then
|
|
||||||
REGISTRY_HOSTNAME="terra.k8c.ru"
|
|
||||||
fi
|
|
||||||
|
|
||||||
TARGET_PATH="terraform-providers/$REGISTRY_HOSTNAME/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}"
|
|
||||||
|
|
||||||
echo ">>> Uploading to S3 path: $TARGET_PATH"
|
|
||||||
|
|
||||||
# Configure mc using S3 credentials (S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY)
|
|
||||||
mc alias set nubes_s3 https://$S3_ENDPOINT $S3_ACCESS_KEY $S3_SECRET_KEY
|
|
||||||
|
|
||||||
# Upload files
|
|
||||||
mc cp --recursive $ARTIFACTS_DIR/ nubes_s3/$TARGET_PATH/
|
|
||||||
|
|
||||||
# 7. Generate shasums file for download (Terraform download protocol requires a checksums.txt usually or just the sums file)
|
|
||||||
# We are following the provider registry protocol loosely.
|
|
||||||
# The most important part for discovery is the 'index.json' or 'versions' endpoint, but we are static hosting.
|
|
||||||
# We rely on the directory structure.
|
|
||||||
|
|
||||||
echo ">>> Build Complete!"
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: terraform-operator
|
|
||||||
namespace: terra
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: terraform-operator
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: terraform-operator
|
|
||||||
spec:
|
|
||||||
serviceAccountName: terraform-operator
|
|
||||||
containers:
|
|
||||||
- name: operator
|
|
||||||
image: naeel/terraform-registry-operator:latest
|
|
||||||
imagePullPolicy: Always
|
|
||||||
env:
|
|
||||||
- name: REGISTRY_HOSTNAME
|
|
||||||
value: "terra.k8c.ru"
|
|
||||||
resources:
|
|
||||||
limits:
|
|
||||||
memory: "128Mi"
|
|
||||||
cpu: "500m"
|
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: registry-server
|
|
||||||
namespace: terra
|
|
||||||
labels:
|
|
||||||
app: registry-server
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: registry-server
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: registry-server
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: registry-server
|
|
||||||
image: naeel/terraform-registry-server:gpg-key-20260208
|
|
||||||
imagePullPolicy: Always
|
|
||||||
env:
|
|
||||||
- name: REGISTRY_HOSTNAME
|
|
||||||
value: "terra.k8c.ru"
|
|
||||||
- name: S3_ENDPOINT
|
|
||||||
value: "s3.msk-1.ngcloud.ru"
|
|
||||||
- name: S3_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
key: access-key
|
|
||||||
name: s3-credentials
|
|
||||||
- name: S3_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
key: secret-key
|
|
||||||
name: s3-credentials
|
|
||||||
- name: S3_BUCKET
|
|
||||||
value: "terraform-registry"
|
|
||||||
- name: S3_USE_SSL
|
|
||||||
value: "true"
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
|
|
||||||
---
|
|
||||||
apiVersion: v1
|
|
||||||
kind: Service
|
|
||||||
metadata:
|
|
||||||
name: registry-server
|
|
||||||
namespace: terra
|
|
||||||
spec:
|
|
||||||
selector:
|
|
||||||
app: registry-server
|
|
||||||
ports:
|
|
||||||
- port: 80
|
|
||||||
targetPort: 8080
|
|
||||||
---
|
|
||||||
apiVersion: networking.k8s.io/v1
|
|
||||||
kind: Ingress
|
|
||||||
metadata:
|
|
||||||
name: registry-ingress
|
|
||||||
namespace: terra
|
|
||||||
annotations:
|
|
||||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
|
||||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
|
||||||
spec:
|
|
||||||
ingressClassName: nginx
|
|
||||||
tls:
|
|
||||||
- hosts:
|
|
||||||
- terra.k8c.ru
|
|
||||||
secretName: registry-tls
|
|
||||||
rules:
|
|
||||||
- host: terra.k8c.ru
|
|
||||||
http:
|
|
||||||
paths:
|
|
||||||
- path: /
|
|
||||||
pathType: Prefix
|
|
||||||
backend:
|
|
||||||
service:
|
|
||||||
name: registry-server
|
|
||||||
port:
|
|
||||||
number: 80
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
apiVersion: apps/v1
|
|
||||||
kind: Deployment
|
|
||||||
metadata:
|
|
||||||
name: registry-server
|
|
||||||
namespace: terra
|
|
||||||
labels:
|
|
||||||
app: registry-server
|
|
||||||
spec:
|
|
||||||
replicas: 1
|
|
||||||
selector:
|
|
||||||
matchLabels:
|
|
||||||
app: registry-server
|
|
||||||
template:
|
|
||||||
metadata:
|
|
||||||
labels:
|
|
||||||
app: registry-server
|
|
||||||
spec:
|
|
||||||
containers:
|
|
||||||
- name: registry-server
|
|
||||||
image: naeel/terraform-registry-server:docs-dev-fixed
|
|
||||||
imagePullPolicy: Always
|
|
||||||
env:
|
|
||||||
- name: REGISTRY_HOSTNAME
|
|
||||||
value: "terra.k8c.ru"
|
|
||||||
- name: S3_ENDPOINT
|
|
||||||
value: "s3.msk-1.ngcloud.ru"
|
|
||||||
- name: S3_ACCESS_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
key: access-key
|
|
||||||
name: s3-credentials
|
|
||||||
- name: S3_SECRET_KEY
|
|
||||||
valueFrom:
|
|
||||||
secretKeyRef:
|
|
||||||
key: secret-key
|
|
||||||
name: s3-credentials
|
|
||||||
- name: S3_BUCKET
|
|
||||||
value: "terraform-registry"
|
|
||||||
- name: S3_USE_SSL
|
|
||||||
value: "true"
|
|
||||||
ports:
|
|
||||||
- containerPort: 8080
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module terraform-registry-keys
|
|
||||||
|
|
||||||
go 1.24.0
|
|
||||||
|
|
||||||
toolchain go1.24.12
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package registrykeys
|
|
||||||
|
|
||||||
const KeyID = "3EC4673EB798238A"
|
|
||||||
|
|
||||||
const ASCIIArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
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
|
|
||||||
6cZ0NQ==
|
|
||||||
=3Ea2
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----
|
|
||||||
`
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module terraform-registry-keys
|
|
||||||
|
|
||||||
go 1.24.0
|
|
||||||
|
|
||||||
toolchain go1.24.12
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
package registrykeys
|
|
||||||
|
|
||||||
const KeyID = "CB3A0DF161ECC416"
|
|
||||||
|
|
||||||
const ASCIIArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
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
|
|
||||||
=q8Tw
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----`
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
flask
|
|
||||||
boto3
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
# Build and optionally push the registry-server image with docs handler
|
|
||||||
# Usage: ./scripts/build-and-push-registry-server.sh <image-repo> <tag>
|
|
||||||
IMAGE_REPO=${1:-naeel}
|
|
||||||
TAG=${2:-docs-dev}
|
|
||||||
|
|
||||||
cd operator
|
|
||||||
make build-registry IMAGE_REPO=${IMAGE_REPO} TAG=${TAG}
|
|
||||||
|
|
||||||
echo "Built image: ${IMAGE_REPO}/terraform-registry-server:${TAG}"
|
|
||||||
|
|
||||||
echo "To push: docker push ${IMAGE_REPO}/terraform-registry-server:${TAG}"
|
|
||||||
|
|
||||||
echo "To deploy to cluster (example):"
|
|
||||||
echo " kubectl apply -f operator/manifests/06-registry-server-docs.yaml"
|
|
||||||
|
|
||||||
echo "and edit the file to replace IMAGE_REPO/TAG with your registry and tag before applying."
|
|
||||||
+77
-77
@@ -16,101 +16,101 @@ import (
|
|||||||
// parseDocsRequestPath parses paths like:
|
// parseDocsRequestPath parses paths like:
|
||||||
// /docs/<namespace>/<name>/<version>/... (rest may be empty)
|
// /docs/<namespace>/<name>/<version>/... (rest may be empty)
|
||||||
func parseDocsRequestPath(p string) (namespace, name, version, rest string, err error) {
|
func parseDocsRequestPath(p string) (namespace, name, version, rest string, err error) {
|
||||||
p = strings.TrimPrefix(p, "/")
|
p = strings.TrimPrefix(p, "/")
|
||||||
p = strings.TrimPrefix(p, "docs/")
|
p = strings.TrimPrefix(p, "docs/")
|
||||||
parts := strings.SplitN(p, "/", 4)
|
parts := strings.SplitN(p, "/", 4)
|
||||||
if len(parts) < 3 {
|
if len(parts) < 3 {
|
||||||
err = fmt.Errorf("invalid docs path: %s", p)
|
err = fmt.Errorf("invalid docs path: %s", p)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
namespace = parts[0]
|
namespace = parts[0]
|
||||||
name = parts[1]
|
name = parts[1]
|
||||||
version = parts[2]
|
version = parts[2]
|
||||||
if len(parts) == 3 {
|
if len(parts) == 3 {
|
||||||
rest = ""
|
rest = ""
|
||||||
} else {
|
} else {
|
||||||
rest = parts[3]
|
rest = parts[3]
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// docsObjectKey builds the S3 key for a docs object given parsed parts.
|
// docsObjectKey builds the S3 key for a docs object given parsed parts.
|
||||||
func docsObjectKey(namespace, name, version, pathPart string) string {
|
func docsObjectKey(namespace, name, version, pathPart string) string {
|
||||||
clean := strings.TrimPrefix(pathPart, "/")
|
clean := strings.TrimPrefix(pathPart, "/")
|
||||||
if clean == "" {
|
if clean == "" {
|
||||||
return fmt.Sprintf("docs/%s/%s/%s/index.html", namespace, name, version)
|
return fmt.Sprintf("docs/%s/%s/%s/index.html", namespace, name, version)
|
||||||
}
|
}
|
||||||
return fmt.Sprintf("docs/%s/%s/%s/%s", namespace, name, version, clean)
|
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.
|
// 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.
|
// Order matters: exact path first, then <path>/index.html, then top-level index.
|
||||||
func tryCandidateKeys(namespace, name, version, rest string) []string {
|
func tryCandidateKeys(namespace, name, version, rest string) []string {
|
||||||
keys := []string{}
|
keys := []string{}
|
||||||
if rest == "" {
|
if rest == "" {
|
||||||
keys = append(keys, docsObjectKey(namespace, name, version, "index.html"))
|
keys = append(keys, docsObjectKey(namespace, name, version, "index.html"))
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
// exact
|
// exact
|
||||||
keys = append(keys, docsObjectKey(namespace, name, version, rest))
|
keys = append(keys, docsObjectKey(namespace, name, version, rest))
|
||||||
// if it looks like a directory or has no extension, try index under it
|
// if it looks like a directory or has no extension, try index under it
|
||||||
if strings.HasSuffix(rest, "/") || filepath.Ext(rest) == "" {
|
if strings.HasSuffix(rest, "/") || filepath.Ext(rest) == "" {
|
||||||
keys = append(keys, docsObjectKey(namespace, name, version, strings.TrimSuffix(rest, "/")+"/index.html"))
|
keys = append(keys, docsObjectKey(namespace, name, version, strings.TrimSuffix(rest, "/")+"/index.html"))
|
||||||
}
|
}
|
||||||
// finally, try root index
|
// finally, try root index
|
||||||
keys = append(keys, docsObjectKey(namespace, name, version, "index.html"))
|
keys = append(keys, docsObjectKey(namespace, name, version, "index.html"))
|
||||||
return keys
|
return keys
|
||||||
}
|
}
|
||||||
|
|
||||||
// docsHandler serves static documentation files from S3 (public-facing via Ingress).
|
// docsHandler serves static documentation files from S3 (public-facing via Ingress).
|
||||||
func docsHandler(w http.ResponseWriter, r *http.Request) {
|
func docsHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
ns, name, ver, rest, err := parseDocsRequestPath(r.URL.Path)
|
ns, name, ver, rest, err := parseDocsRequestPath(r.URL.Path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
http.Error(w, "Bad docs path", http.StatusBadRequest)
|
http.Error(w, "Bad docs path", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
candidates := tryCandidateKeys(ns, name, ver, rest)
|
candidates := tryCandidateKeys(ns, name, ver, rest)
|
||||||
log.Printf("Docs candidates (host=%s): %v", hostname, candidates)
|
log.Printf("Docs candidates (host=%s): %v", hostname, candidates)
|
||||||
|
|
||||||
var lastErr error
|
var lastErr error
|
||||||
for _, key := range candidates {
|
for _, key := range candidates {
|
||||||
obj, err := s3Client.GetObject(ctx, bucketName, key, s3.GetObjectOptions{})
|
obj, err := s3Client.GetObject(ctx, bucketName, key, s3.GetObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lastErr = err
|
lastErr = err
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
stat, err := obj.Stat()
|
stat, err := obj.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lastErr = err
|
lastErr = err
|
||||||
_ = obj.Close()
|
_ = obj.Close()
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determine content-type
|
// Determine content-type
|
||||||
ext := filepath.Ext(key)
|
ext := filepath.Ext(key)
|
||||||
ctype := mime.TypeByExtension(ext)
|
ctype := mime.TypeByExtension(ext)
|
||||||
if ctype == "" {
|
if ctype == "" {
|
||||||
// fallback for HTML
|
// fallback for HTML
|
||||||
if ext == ".html" || strings.HasSuffix(key, "index.html") {
|
if ext == ".html" || strings.HasSuffix(key, "index.html") {
|
||||||
ctype = "text/html; charset=utf-8"
|
ctype = "text/html; charset=utf-8"
|
||||||
} else {
|
} else {
|
||||||
ctype = "application/octet-stream"
|
ctype = "application/octet-stream"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
w.Header().Set("Content-Type", ctype)
|
w.Header().Set("Content-Type", ctype)
|
||||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
|
w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size))
|
||||||
w.Header().Set("Last-Modified", stat.LastModified.Format(http.TimeFormat))
|
w.Header().Set("Last-Modified", stat.LastModified.Format(http.TimeFormat))
|
||||||
|
|
||||||
if _, err := io.Copy(w, obj); err != nil {
|
if _, err := io.Copy(w, obj); err != nil {
|
||||||
log.Printf("Error streaming object %s: %v", key, err)
|
log.Printf("Error streaming object %s: %v", key, err)
|
||||||
}
|
}
|
||||||
_ = obj.Close()
|
_ = obj.Close()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Docs not found in candidates: %v, lastErr: %v", candidates, lastErr)
|
log.Printf("Docs not found in candidates: %v, lastErr: %v", candidates, lastErr)
|
||||||
http.Error(w, "Documentation not found", http.StatusNotFound)
|
http.Error(w, "Documentation not found", http.StatusNotFound)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
module tf-registry
|
||||||
|
|
||||||
|
go 1.25.0
|
||||||
|
|
||||||
|
require github.com/minio/minio-go/v7 v7.2.1
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.6 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||||
|
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||||
|
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||||
|
github.com/minio/md5-simd v1.1.2 // indirect
|
||||||
|
github.com/philhofer/fwd v1.2.0 // indirect
|
||||||
|
github.com/rs/xid v1.6.0 // indirect
|
||||||
|
github.com/tinylib/msgp v1.6.1 // indirect
|
||||||
|
github.com/zeebo/xxh3 v1.1.0 // indirect
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
|
golang.org/x/crypto v0.51.0 // indirect
|
||||||
|
golang.org/x/net v0.53.0 // indirect
|
||||||
|
golang.org/x/sys v0.44.0 // indirect
|
||||||
|
golang.org/x/text v0.37.0 // indirect
|
||||||
|
gopkg.in/ini.v1 v1.67.2 // indirect
|
||||||
|
)
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||||
|
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||||
|
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||||
|
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||||
|
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||||
|
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
|
||||||
|
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||||
|
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||||
|
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||||
|
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||||
|
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||||
|
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||||
|
github.com/minio/minio-go/v7 v7.2.1 h1:PfBfwvKB/MmqyN8Vb1G9voWisaM9OrLv+WwOvMwS9Dw=
|
||||||
|
github.com/minio/minio-go/v7 v7.2.1/go.mod h1:EU9hENAStx/xXduNdrGO5e4X5vk19NtgB+RIPjZO8o0=
|
||||||
|
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||||
|
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||||
|
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||||
|
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
|
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||||
|
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||||
|
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||||
|
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||||
|
github.com/zeebo/assert v1.3.0 h1:g7C04CbJuIDKNPFHmsk4hwZDO5O+kntRxzaUoNXj+IQ=
|
||||||
|
github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
|
github.com/zeebo/xxh3 v1.1.0 h1:s7DLGDK45Dyfg7++yxI0khrfwq9661w9EN78eP/UZVs=
|
||||||
|
github.com/zeebo/xxh3 v1.1.0/go.mod h1:IisAie1LELR4xhVinxWS5+zf1lA4p0MW4T+w+W07F5s=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||||
|
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||||
|
golang.org/x/crypto v0.51.0 h1:IBPXwPfKxY7cWQZ38ZCIRPI50YLeevDLlLnyC5wRGTI=
|
||||||
|
golang.org/x/crypto v0.51.0/go.mod h1:8AdwkbraGNABw2kOX6YFPs3WM22XqI4EXEd8g+x7Oc8=
|
||||||
|
golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA=
|
||||||
|
golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs=
|
||||||
|
golang.org/x/sys v0.44.0 h1:ildZl3J4uzeKP07r2F++Op7E9B29JRUy+a27EibtBTQ=
|
||||||
|
golang.org/x/sys v0.44.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||||
|
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||||
|
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/ini.v1 v1.67.2 h1:JtOSMb9OuaCZKr7h5D/h6iii14sK0hLbplTc6frx4Ss=
|
||||||
|
gopkg.in/ini.v1 v1.67.2/go.mod h1:x/cyOwCgZqOkJoDIJ3c1KNHMo10+nLGAhh+kn3Zizss=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
+125
-4
@@ -1,8 +1,129 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import registrykeys "terraform-registry-keys"
|
// GPG public keys from live deployment (two keys: primary + legacy).
|
||||||
|
// Primary key is used by the current operator-built binary.
|
||||||
|
|
||||||
var gpgPublicKey = GPGPublicKey{
|
var gpgPrimaryKey = GPGPublicKey{
|
||||||
KeyID: registrykeys.KeyID,
|
KeyID: "3EC4673EB798238A",
|
||||||
ASCIIArmor: registrykeys.ASCIIArmor,
|
ASCIIArmor: `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
6cZ0NQ==
|
||||||
|
=3Ea2
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----`,
|
||||||
|
}
|
||||||
|
|
||||||
|
var gpgLegacyKey = GPGPublicKey{
|
||||||
|
KeyID: "CB3A0DF161ECC416",
|
||||||
|
ASCIIArmor: `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||||
|
|
||||||
|
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
|
||||||
|
=q8Tw
|
||||||
|
-----END PGP PUBLIC KEY BLOCK-----`,
|
||||||
}
|
}
|
||||||
|
|||||||
+166
-26
@@ -10,7 +10,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
s3 "github.com/minio/minio-go/v7"
|
s3 "github.com/minio/minio-go/v7"
|
||||||
"github.com/minio/minio-go/v7/pkg/credentials"
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
||||||
@@ -20,8 +24,11 @@ var (
|
|||||||
s3Client *s3.Client
|
s3Client *s3.Client
|
||||||
bucketName = "terraform-registry" // Default
|
bucketName = "terraform-registry" // Default
|
||||||
hostname = os.Getenv("REGISTRY_HOSTNAME")
|
hostname = os.Getenv("REGISTRY_HOSTNAME")
|
||||||
|
s3Prefix = os.Getenv("S3_PREFIX") // S3 key prefix (may differ from hostname)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
const VERSION = "1.1.1"
|
||||||
|
|
||||||
// Terraform Registry Protocol Structs
|
// Terraform Registry Protocol Structs
|
||||||
type Discovery struct {
|
type Discovery struct {
|
||||||
ProvidersV1 string `json:"providers.v1"`
|
ProvidersV1 string `json:"providers.v1"`
|
||||||
@@ -68,7 +75,11 @@ type GPGPublicKey struct {
|
|||||||
func main() {
|
func main() {
|
||||||
// 1. Init S3 Connection
|
// 1. Init S3 Connection
|
||||||
if hostname == "" {
|
if hostname == "" {
|
||||||
hostname = "localhost:8080"
|
hostname = "localhost:5000"
|
||||||
|
}
|
||||||
|
// S3_PREFIX default comes from Dockerfile ENV; override in jsonEnv if needed.
|
||||||
|
if s3Prefix == "" {
|
||||||
|
s3Prefix = hostname
|
||||||
}
|
}
|
||||||
|
|
||||||
endpoint := os.Getenv("S3_ENDPOINT")
|
endpoint := os.Getenv("S3_ENDPOINT")
|
||||||
@@ -80,9 +91,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
|
useSSL := os.Getenv("S3_USE_SSL") != "false"
|
||||||
s3Client, err = s3.New(endpoint, &s3.Options{
|
s3Client, err = s3.New(endpoint, &s3.Options{
|
||||||
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
|
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
|
||||||
Secure: true, // Force secure for cloud S3
|
Secure: useSSL,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
log.Fatalln(err)
|
||||||
@@ -97,8 +109,31 @@ func main() {
|
|||||||
http.HandleFunc("/healthz", healthzHandler)
|
http.HandleFunc("/healthz", healthzHandler)
|
||||||
http.HandleFunc("/readyz", readyzHandler)
|
http.HandleFunc("/readyz", readyzHandler)
|
||||||
|
|
||||||
log.Printf("Starting Registry Service on :8080 (Bucket: %s, Endpoint: %s)\n", bucketName, endpoint)
|
port := os.Getenv("PORT")
|
||||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
if port == "" {
|
||||||
|
port = "5000"
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := &http.Server{Addr: ":" + port}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
log.Printf("Starting Registry Service on :%s (Bucket: %s, Endpoint: %s)\n", port, bucketName, endpoint)
|
||||||
|
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
|
||||||
|
log.Fatalf("HTTP server error: %v", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
quit := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
<-quit
|
||||||
|
log.Println("Shutting down server...")
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
if err := srv.Shutdown(ctx); err != nil {
|
||||||
|
log.Fatalf("Server forced to shutdown: %v", err)
|
||||||
|
}
|
||||||
|
log.Println("Server stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
func rootHandler(w http.ResponseWriter, r *http.Request) {
|
func rootHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -107,32 +142,126 @@ func rootHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
w.Header().Set("Content-Type", "text/html; charset=utf-8")
|
||||||
fmt.Fprint(w, `
|
fmt.Fprintf(w, `<!DOCTYPE html>
|
||||||
<!DOCTYPE html>
|
<html lang="ru">
|
||||||
<html>
|
<head>
|
||||||
<head><title>Terra Registry</title></head>
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||||
|
<title>Terraform Registry · Nubes</title>
|
||||||
|
<style>
|
||||||
|
:root {
|
||||||
|
--primary: #2563eb;
|
||||||
|
--primary-dark: #1d4ed8;
|
||||||
|
--bg: #f8fafc;
|
||||||
|
--card-bg: #fff;
|
||||||
|
--border: #e2e8f0;
|
||||||
|
--text: #1a1a1a;
|
||||||
|
--muted: #6b7280;
|
||||||
|
--green: #22c55e;
|
||||||
|
--radius: 12px;
|
||||||
|
}
|
||||||
|
* { margin: 0; padding: 0; box-sizing: border-box; }
|
||||||
|
body {
|
||||||
|
font-family: system-ui, -apple-system, sans-serif;
|
||||||
|
background: var(--bg);
|
||||||
|
color: var(--text);
|
||||||
|
min-height: 100vh;
|
||||||
|
display: flex; flex-direction: column;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border-bottom: 1px solid var(--border);
|
||||||
|
padding: 16px 24px;
|
||||||
|
display: flex; align-items: center; gap: 12px;
|
||||||
|
}
|
||||||
|
.header svg { width: 120px; height: auto; }
|
||||||
|
.main {
|
||||||
|
flex: 1;
|
||||||
|
display: flex; align-items: center; justify-content: center;
|
||||||
|
padding: 40px 24px;
|
||||||
|
}
|
||||||
|
.card {
|
||||||
|
background: var(--card-bg);
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: var(--radius);
|
||||||
|
box-shadow: 0 1px 3px rgba(0,0,0,.04);
|
||||||
|
width: 100%%; max-width: 480px;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
background: #f3f4f6;
|
||||||
|
padding: 14px 20px;
|
||||||
|
font-weight: 600; font-size: 16px;
|
||||||
|
display: flex; align-items: center; gap: 8px;
|
||||||
|
}
|
||||||
|
.card-body { padding: 20px; }
|
||||||
|
.row { display: flex; justify-content: space-between; align-items: center; padding: 8px 0; }
|
||||||
|
.row + .row { border-top: 1px solid var(--border); }
|
||||||
|
.label { color: var(--muted); font-size: 14px; }
|
||||||
|
.value { font-size: 14px; font-weight: 500; }
|
||||||
|
.badge {
|
||||||
|
display: inline-flex; align-items: center; gap: 6px;
|
||||||
|
background: #f0fdf4; color: #166534;
|
||||||
|
padding: 4px 10px; border-radius: 999px;
|
||||||
|
font-size: 13px; font-weight: 500;
|
||||||
|
}
|
||||||
|
.badge::before {
|
||||||
|
content: ''; width: 8px; height: 8px;
|
||||||
|
background: var(--green); border-radius: 50%%;
|
||||||
|
}
|
||||||
|
.footer {
|
||||||
|
text-align: center; padding: 16px 24px;
|
||||||
|
color: var(--muted); font-size: 12px;
|
||||||
|
}
|
||||||
|
a { color: var(--primary); text-decoration: none; }
|
||||||
|
a:hover { color: var(--primary-dark); }
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<h1>Terra Registry & Documentation Server</h1>
|
<div class="header">
|
||||||
<p>Status: <span style="color: green">ONLINE</span></p>
|
<svg viewBox="0 0 8222 1821" xmlns="http://www.w3.org/2000/svg">
|
||||||
<hr>
|
<path d="M477 480H64L0 756h178l2 1029v27h276V941c0-101 82-184 184-184h589c101 0 184 83 184 184v872h276V941c0-254-206-460-460-460H640L477 480zM6665 1813h1181c212 0 376-175 376-387 0-212-173-385-385-385h-654c-60 0-109-49-109-109v-66c0-60 49-109 109-109h696l64-275h-760c-212 0-384 172-384 384v66c0 212 172 385 384 385h654c60 0 109 49 109 109s-49 116-109 116H6719l-54 271zM6201 484h-723c-216 0-392 175-392 391v486c0 248 202 450 451 450h989l62-269h-1052c-96 0-175-85-175-181v-40h924l308-2v-238-206c0-216-175-392-391-392zm116 560H5362V875c0-64 52-116 116-116h723c64 0 116 52 116 116v169zM4683 1194v169c0 101-81 178-182 178h-590c-93 0-170-70-182-160V923c11-90 89-160 182-160h589c101 0 183 82 183 183v31 213zm-1230-345-1-795 277-54v526c56-24 117-38 181-38h590c253 0 459 206 459 459v30 213 169c0 252-206 458-459 458h-589c-253 0-459-206-459-459V975l1-98zM3041 1151v204c0 101-82 183-183 183h-582c-101 0-183-82-183-183v-67l1 1V426l-277 54v874c0 253 206 458 458 458h583c253 0 458-205 458-458v-71l1-863-277 54v675z" fill="#001C34"/>
|
||||||
<p>Powered by Nubes Cloud S3 Storage</p>
|
</svg>
|
||||||
|
</div>
|
||||||
|
<div class="main">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">Terraform Provider Registry</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<span class="label">Статус</span>
|
||||||
|
<span class="badge">ONLINE</span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="label">Версия</span>
|
||||||
|
<span class="value">v`+VERSION+`</span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="label">Discovery</span>
|
||||||
|
<span class="value"><a href="/.well-known/terraform.json">/.well-known/terraform.json</a></span>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<span class="label">Провайдеры</span>
|
||||||
|
<span class="value"><a href="/v1/providers/">/v1/providers/</a></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">Powered by Nubes Cloud · S3 Storage</div>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>`)
|
||||||
`)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
bucket := r.URL.Query().Get("bucket")
|
|
||||||
key := r.URL.Query().Get("key")
|
key := r.URL.Query().Get("key")
|
||||||
|
|
||||||
if bucket == "" || key == "" {
|
if key == "" {
|
||||||
http.Error(w, "Missing bucket or key params", http.StatusBadRequest)
|
http.Error(w, "Missing key param", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
obj, err := s3Client.GetObject(context.Background(), bucket, key, s3.GetObjectOptions{})
|
obj, err := s3Client.GetObject(context.Background(), bucketName, key, s3.GetObjectOptions{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error getting object %s/%s: %v", bucket, key, err)
|
log.Printf("Error getting object %s/%s: %v", bucketName, key, err)
|
||||||
http.Error(w, "File not found", http.StatusNotFound)
|
http.Error(w, "File not found", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -140,7 +269,7 @@ func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
stat, err := obj.Stat()
|
stat, err := obj.Stat()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Error stating object %s/%s: %v", bucket, key, err)
|
log.Printf("Error stating object %s/%s: %v", bucketName, key, err)
|
||||||
http.Error(w, "File not found or not accessible", http.StatusNotFound)
|
http.Error(w, "File not found or not accessible", http.StatusNotFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -177,7 +306,7 @@ func router(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func listVersions(w http.ResponseWriter, r *http.Request, namespace, pType string) {
|
func listVersions(w http.ResponseWriter, r *http.Request, namespace, pType string) {
|
||||||
prefix := fmt.Sprintf("%s/%s/%s/", hostname, namespace, pType)
|
prefix := fmt.Sprintf("%s/%s/%s/", s3Prefix, namespace, pType)
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
versions := []Version{}
|
versions := []Version{}
|
||||||
seenVersions := map[string]*Version{}
|
seenVersions := map[string]*Version{}
|
||||||
@@ -206,6 +335,9 @@ func listVersions(w http.ResponseWriter, r *http.Request, namespace, pType strin
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if strings.Contains(fileName, "_darwin_amd64.zip") {
|
||||||
|
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "amd64"})
|
||||||
|
}
|
||||||
if strings.Contains(fileName, "_linux_amd64.zip") {
|
if strings.Contains(fileName, "_linux_amd64.zip") {
|
||||||
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "linux", Arch: "amd64"})
|
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "linux", Arch: "amd64"})
|
||||||
}
|
}
|
||||||
@@ -218,6 +350,10 @@ func listVersions(w http.ResponseWriter, r *http.Request, namespace, pType strin
|
|||||||
versions = append(versions, *v)
|
versions = append(versions, *v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sort.Slice(versions, func(i, j int) bool {
|
||||||
|
return versions[i].Version < versions[j].Version
|
||||||
|
})
|
||||||
|
|
||||||
resp := VersionList{
|
resp := VersionList{
|
||||||
ID: fmt.Sprintf("%s/%s", namespace, pType),
|
ID: fmt.Sprintf("%s/%s", namespace, pType),
|
||||||
Versions: versions,
|
Versions: versions,
|
||||||
@@ -228,7 +364,7 @@ func listVersions(w http.ResponseWriter, r *http.Request, namespace, pType strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
func downloadVersion(w http.ResponseWriter, r *http.Request, namespace, pType, version, osType, arch string) {
|
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)
|
basePath := fmt.Sprintf("%s/%s/%s/%s", s3Prefix, namespace, pType, version)
|
||||||
filename := fmt.Sprintf("terraform-provider-%s_%s_%s_%s.zip", pType, version, osType, arch)
|
filename := fmt.Sprintf("terraform-provider-%s_%s_%s_%s.zip", pType, version, osType, arch)
|
||||||
fullKey := fmt.Sprintf("%s/%s", basePath, filename)
|
fullKey := fmt.Sprintf("%s/%s", basePath, filename)
|
||||||
shasumsKey := fmt.Sprintf("%s/terraform-provider-%s_%s_SHA256SUMS", basePath, pType, version)
|
shasumsKey := fmt.Sprintf("%s/terraform-provider-%s_%s_SHA256SUMS", basePath, pType, version)
|
||||||
@@ -256,8 +392,6 @@ func downloadVersion(w http.ResponseWriter, r *http.Request, namespace, pType, v
|
|||||||
shasumsLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(shasumsKey))
|
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))
|
sigLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(sigKey))
|
||||||
|
|
||||||
gpgKey := gpgPublicKey
|
|
||||||
|
|
||||||
resp := DownloadResponse{
|
resp := DownloadResponse{
|
||||||
Protocols: []string{"5.0"},
|
Protocols: []string{"5.0"},
|
||||||
OS: osType,
|
OS: osType,
|
||||||
@@ -268,7 +402,7 @@ func downloadVersion(w http.ResponseWriter, r *http.Request, namespace, pType, v
|
|||||||
ShasumsSignatureURL: sigLink,
|
ShasumsSignatureURL: sigLink,
|
||||||
Shasum: shasumValue,
|
Shasum: shasumValue,
|
||||||
SigningKeys: SigningKeys{
|
SigningKeys: SigningKeys{
|
||||||
GPGPublicKeys: []GPGPublicKey{gpgKey},
|
GPGPublicKeys: []GPGPublicKey{gpgPrimaryKey, gpgLegacyKey},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -282,7 +416,13 @@ func healthzHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func readyzHandler(w http.ResponseWriter, r *http.Request) {
|
func readyzHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
// Проверить доступность S3
|
_, err := s3Client.BucketExists(context.Background(), bucketName)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("readyz: S3 bucket check failed: %v", err)
|
||||||
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
|
w.Write([]byte("s3 unreachable"))
|
||||||
|
return
|
||||||
|
}
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
w.Write([]byte("ok"))
|
w.Write([]byte("ok"))
|
||||||
}
|
}
|
||||||
|
|||||||
-411
@@ -1,411 +0,0 @@
|
|||||||
VERSION = "1.0.0"
|
|
||||||
|
|
||||||
import os
|
|
||||||
import mimetypes
|
|
||||||
import urllib.parse
|
|
||||||
|
|
||||||
from flask import Flask, Response, jsonify, request, stream_with_context
|
|
||||||
|
|
||||||
import boto3
|
|
||||||
from botocore.config import Config
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Config (from ENV, matching Go defaults)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
HOSTNAME = os.getenv("REGISTRY_HOSTNAME", "localhost:8080")
|
|
||||||
S3_PREFIX = os.getenv("S3_PREFIX", "registry.containerk8s.dev.nubes.ru")
|
|
||||||
S3_ENDPOINT = os.getenv("S3_ENDPOINT", "")
|
|
||||||
S3_ACCESS_KEY = os.getenv("S3_ACCESS_KEY", "")
|
|
||||||
S3_SECRET_KEY = os.getenv("S3_SECRET_KEY", "")
|
|
||||||
S3_BUCKET = os.getenv("S3_BUCKET", "terraform-registry")
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# GPG public keys (from LIVE Go registry — TWO keys: primary + legacy)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
GPG_PRIMARY_KEY_ID = "3EC4673EB798238A"
|
|
||||||
GPG_PRIMARY_ASCII_ARMOR = """-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
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
|
|
||||||
6cZ0NQ==
|
|
||||||
=3Ea2
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----"""
|
|
||||||
|
|
||||||
GPG_LEGACY_KEY_ID = "CB3A0DF161ECC416"
|
|
||||||
GPG_LEGACY_ASCII_ARMOR = """-----BEGIN PGP PUBLIC KEY BLOCK-----
|
|
||||||
|
|
||||||
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
|
|
||||||
=q8Tw
|
|
||||||
-----END PGP PUBLIC KEY BLOCK-----"""
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# S3 client (MinIO-compatible, Secure=true in Go → use_ssl=True + s3v4)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
s3 = boto3.client(
|
|
||||||
"s3",
|
|
||||||
endpoint_url=f"https://{S3_ENDPOINT}",
|
|
||||||
aws_access_key_id=S3_ACCESS_KEY,
|
|
||||||
aws_secret_access_key=S3_SECRET_KEY,
|
|
||||||
config=Config(signature_version="s3v4"),
|
|
||||||
use_ssl=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Flask app
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
app = Flask(__name__)
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Route: Discovery
|
|
||||||
# GET /.well-known/terraform.json
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
@app.route("/.well-known/terraform.json")
|
|
||||||
def discovery():
|
|
||||||
return jsonify({"providers.v1": "/v1/providers/"})
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Route: Provider router
|
|
||||||
# GET /v1/providers/{ns}/{name}/versions
|
|
||||||
# GET /v1/providers/{ns}/{name}/{ver}/download/{os}/{arch}
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
@app.route("/v1/providers/<path:provider_path>")
|
|
||||||
def provider_router(provider_path):
|
|
||||||
parts = provider_path.split("/")
|
|
||||||
|
|
||||||
# /v1/providers/{ns}/{name}/versions
|
|
||||||
if len(parts) == 3 and parts[2] == "versions":
|
|
||||||
return _list_versions(parts[0], parts[1])
|
|
||||||
|
|
||||||
# /v1/providers/{ns}/{name}/{ver}/download/{os}/{arch}
|
|
||||||
if len(parts) == 6 and parts[3] == "download":
|
|
||||||
return _download_version(parts[0], parts[1], parts[2], parts[4], parts[5])
|
|
||||||
|
|
||||||
return "Not Found", 404
|
|
||||||
|
|
||||||
|
|
||||||
def _list_versions(namespace, name):
|
|
||||||
"""List all versions and platforms by scanning S3 objects."""
|
|
||||||
prefix = f"{S3_PREFIX}/{namespace}/{name}/"
|
|
||||||
seen = {}
|
|
||||||
|
|
||||||
paginator = s3.get_paginator("list_objects_v2")
|
|
||||||
for page in paginator.paginate(Bucket=S3_BUCKET, Prefix=prefix):
|
|
||||||
for obj in page.get("Contents", []):
|
|
||||||
key = obj["Key"]
|
|
||||||
key_parts = key.split("/")
|
|
||||||
if len(key_parts) < 5:
|
|
||||||
continue
|
|
||||||
ver = key_parts[3]
|
|
||||||
filename = key_parts[4]
|
|
||||||
|
|
||||||
if ver not in seen:
|
|
||||||
seen[ver] = {
|
|
||||||
"version": ver,
|
|
||||||
"protocols": ["5.0"],
|
|
||||||
"platforms": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
if "_darwin_amd64.zip" in filename:
|
|
||||||
seen[ver]["platforms"].append({"os": "darwin", "arch": "amd64"})
|
|
||||||
if "_linux_amd64.zip" in filename:
|
|
||||||
seen[ver]["platforms"].append({"os": "linux", "arch": "amd64"})
|
|
||||||
if "_windows_amd64.zip" in filename:
|
|
||||||
seen[ver]["platforms"].append({"os": "windows", "arch": "amd64"})
|
|
||||||
|
|
||||||
versions = list(seen.values())
|
|
||||||
return jsonify({
|
|
||||||
"id": f"{namespace}/{name}",
|
|
||||||
"versions": versions,
|
|
||||||
"warnings": [],
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
def _download_version(namespace, name, version, os_type, arch):
|
|
||||||
"""Build download metadata JSON with S3 proxy URLs."""
|
|
||||||
base = f"{S3_PREFIX}/{namespace}/{name}/{version}"
|
|
||||||
filename = f"terraform-provider-{name}_{version}_{os_type}_{arch}.zip"
|
|
||||||
full_key = f"{base}/{filename}"
|
|
||||||
shasums_key = f"{base}/terraform-provider-{name}_{version}_SHA256SUMS"
|
|
||||||
sig_key = f"{base}/terraform-provider-{name}_{version}_SHA256SUMS.sig"
|
|
||||||
|
|
||||||
# Extract shasum for this file from SHA256SUMS
|
|
||||||
shasum = ""
|
|
||||||
try:
|
|
||||||
resp = s3.get_object(Bucket=S3_BUCKET, Key=shasums_key)
|
|
||||||
for line in resp["Body"].read().decode("utf-8", errors="replace").splitlines():
|
|
||||||
if filename in line:
|
|
||||||
shasum = line.split()[0]
|
|
||||||
break
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
|
|
||||||
base_url = f"https://{HOSTNAME}"
|
|
||||||
download_url = f"{base_url}/v1/proxy?bucket={S3_BUCKET}&key={urllib.parse.quote(full_key)}"
|
|
||||||
shasums_url = f"{base_url}/v1/proxy?bucket={S3_BUCKET}&key={urllib.parse.quote(shasums_key)}"
|
|
||||||
sig_url = f"{base_url}/v1/proxy?bucket={S3_BUCKET}&key={urllib.parse.quote(sig_key)}"
|
|
||||||
|
|
||||||
return jsonify({
|
|
||||||
"protocols": ["5.0"],
|
|
||||||
"os": os_type,
|
|
||||||
"arch": arch,
|
|
||||||
"filename": filename,
|
|
||||||
"download_url": download_url,
|
|
||||||
"shasums_url": shasums_url,
|
|
||||||
"shasums_signature_url": sig_url,
|
|
||||||
"shasum": shasum,
|
|
||||||
"signing_keys": {
|
|
||||||
"gpg_public_keys": [
|
|
||||||
{
|
|
||||||
"key_id": GPG_PRIMARY_KEY_ID,
|
|
||||||
"ascii_armor": GPG_PRIMARY_ASCII_ARMOR,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"key_id": GPG_LEGACY_KEY_ID,
|
|
||||||
"ascii_armor": GPG_LEGACY_ASCII_ARMOR,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Route: Proxy (S3 streaming)
|
|
||||||
# GET /v1/proxy?bucket=...&key=...
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
@app.route("/v1/proxy")
|
|
||||||
def proxy():
|
|
||||||
bucket = request.args.get("bucket", "")
|
|
||||||
key = request.args.get("key", "")
|
|
||||||
|
|
||||||
if not bucket or not key:
|
|
||||||
return "Missing bucket or key params", 400
|
|
||||||
|
|
||||||
try:
|
|
||||||
head = s3.head_object(Bucket=bucket, Key=key)
|
|
||||||
obj = s3.get_object(Bucket=bucket, Key=key)
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
stream_with_context(obj["Body"].iter_chunks(chunk_size=65536)),
|
|
||||||
content_type="application/octet-stream",
|
|
||||||
headers={
|
|
||||||
"Content-Length": str(head["ContentLength"]),
|
|
||||||
"Last-Modified": head["LastModified"].strftime("%a, %d %b %Y %H:%M:%S GMT"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
return "File not found", 404
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Route: Docs (static from S3)
|
|
||||||
# GET /docs/{ns}/{name}/{ver}/...
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
@app.route("/docs/<path:doc_path>")
|
|
||||||
def docs(doc_path):
|
|
||||||
# Parse: docs/{namespace}/{name}/{version}/...rest
|
|
||||||
parts = doc_path.strip("/").split("/")
|
|
||||||
if len(parts) < 3:
|
|
||||||
return "Bad docs path", 400
|
|
||||||
|
|
||||||
namespace = parts[0]
|
|
||||||
name = parts[1]
|
|
||||||
version = parts[2]
|
|
||||||
rest = "/".join(parts[3:]) if len(parts) > 3 else ""
|
|
||||||
|
|
||||||
# Build candidate S3 keys (exact 1:1 with Go tryCandidateKeys)
|
|
||||||
candidates = []
|
|
||||||
|
|
||||||
if rest == "":
|
|
||||||
candidates.append(f"docs/{namespace}/{name}/{version}/index.html")
|
|
||||||
else:
|
|
||||||
# 1) exact path
|
|
||||||
candidates.append(f"docs/{namespace}/{name}/{version}/{rest}")
|
|
||||||
|
|
||||||
# 2) if looks like directory or no extension → try index.html under it
|
|
||||||
rest_ext = os.path.splitext(rest)[1] # ".css" or ""
|
|
||||||
if rest.endswith("/") or rest_ext == "":
|
|
||||||
candidates.append(
|
|
||||||
f"docs/{namespace}/{name}/{version}/{rest.rstrip('/')}/index.html"
|
|
||||||
)
|
|
||||||
|
|
||||||
# 3) fallback: root index.html
|
|
||||||
candidates.append(f"docs/{namespace}/{name}/{version}/index.html")
|
|
||||||
|
|
||||||
for s3_key in candidates:
|
|
||||||
try:
|
|
||||||
head = s3.head_object(Bucket=S3_BUCKET, Key=s3_key)
|
|
||||||
obj = s3.get_object(Bucket=S3_BUCKET, Key=s3_key)
|
|
||||||
|
|
||||||
# Content-Type (1:1 with Go docsHandler)
|
|
||||||
ext = os.path.splitext(s3_key)[1] # ".html", ".css", etc.
|
|
||||||
ctype = mimetypes.types_map.get(ext, "")
|
|
||||||
if not ctype:
|
|
||||||
if ext == ".html" or s3_key.endswith("index.html"):
|
|
||||||
ctype = "text/html; charset=utf-8"
|
|
||||||
else:
|
|
||||||
ctype = "application/octet-stream"
|
|
||||||
|
|
||||||
return Response(
|
|
||||||
stream_with_context(obj["Body"].iter_chunks(chunk_size=65536)),
|
|
||||||
content_type=ctype,
|
|
||||||
headers={
|
|
||||||
"Content-Length": str(head["ContentLength"]),
|
|
||||||
"Last-Modified": head["LastModified"].strftime(
|
|
||||||
"%a, %d %b %Y %H:%M:%S GMT"
|
|
||||||
),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
except Exception:
|
|
||||||
continue
|
|
||||||
|
|
||||||
return "Documentation not found", 404
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Route: Healthz
|
|
||||||
# GET /healthz
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
@app.route("/healthz")
|
|
||||||
def healthz():
|
|
||||||
return "ok", 200
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Route: Readyz
|
|
||||||
# GET /readyz
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
@app.route("/readyz")
|
|
||||||
def readyz():
|
|
||||||
return "ok", 200
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Route: Root (LAST — must not shadow /docs)
|
|
||||||
# GET /
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
@app.route("/")
|
|
||||||
def index():
|
|
||||||
return f"""<!DOCTYPE html>
|
|
||||||
<html>
|
|
||||||
<head><title>Terra Registry v{VERSION}</title></head>
|
|
||||||
<body>
|
|
||||||
<h1>Terra Registry & Documentation Server v{VERSION}</h1>
|
|
||||||
<p>Status: <span style="color: green">ONLINE</span></p>
|
|
||||||
<hr>
|
|
||||||
<p>Powered by Nubes Cloud S3 Storage</p>
|
|
||||||
</body>
|
|
||||||
</html>"""
|
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
# Main (mandatory for Nubes managed service)
|
|
||||||
# ---------------------------------------------------------------------------
|
|
||||||
if __name__ == "__main__":
|
|
||||||
app.run(host="0.0.0.0", port=5000)
|
|
||||||
Reference in New Issue
Block a user