diff --git a/HISTORY/docs-service-plan.md b/HISTORY/docs-service-plan.md new file mode 100644 index 0000000..f9598af --- /dev/null +++ b/HISTORY/docs-service-plan.md @@ -0,0 +1,70 @@ +# План: отдельный сервис документации (Node.js) + +## Целевая архитектура + +``` +S3 (nubes-terraform-registry) — статика документации, только docs/* +Node.js-сервис — раздаёт /docs/* + кеш с TTL +registry (Go) — только Terraform-протокол: + /.well-known/*, /v1/providers/*, /v1/proxy +``` + +## Что выносится из registry + +- Маршрут `/docs/` и функция `docsHandler` (удаляются из `server/main.go` и `server/docs.go`). +- Провайдерская логика (`discoveryHandler`, `router`, `proxyHandler`, `rootHandler`, `healthz/readyz`) — НЕ меняется. + +## Соглашения для сервиса документации (перенос из бывшего server/docs.go) + +Файл `docs.go` не является уникальным активом; он описывает обычный маппинг +URL → S3-ключ. При переносе на Node.js достаточно воспроизвести три правила: + +1. **Формат S3-ключей документации:** + ``` + docs//// + при пустом rest → docs////index.html + ``` + +2. **Fallback-цепочка при неизвестном пути:** + точный путь → `<путь>/index.html` (если путь похож на каталог) → корневой `index.html`. + +3. **Content-Type:** + через `mime.TypeByExtension`; fallback на `text/html; charset=utf-8` для + `index.html`/`.html`, иначе `application/octet-stream`. + +4. **Добавить то, чего в сервисе документации будет больше, чем в docs.go:** + in-memory кеш с TTL (например, 60–300 сек) и заголовок `Cache-Control`. + +## Endpoint Node.js-сервиса + +``` +GET /docs//// +``` + +## Переменные окружения (общие с registry) + +- `S3_ENDPOINT`, `S3_BUCKET`, `S3_ACCESS_KEY`, `S3_SECRET_KEY` + (точки доступа — как в `secrets/env.txt`). + +## DNS / Ingress + +- `/docs/*` → Node.js-сервис. +- `/.well-known/*`, `/v1/providers/*`, `/v1/proxy` → registry. +- Тот же домен `tf-registry.containerk8s.services.ngcloud.ru`, пути разводятся в Ingress. + (При отдельном поддомене потребуется переписывать ссылки в HTML.) + +## Порядок работ + +1. Вынести `/docs/` из registry (этот шаг). +2. Написать Node.js-сервис (маппинг + кеш + Content-Type). +3. Dockerfile + образ в gitea + деплой сервиса. +4. Ingress-правило для `/docs/*`. +5. Переписать генератор документации под новые соглашения (отдельная задача). +6. Smoke-тесты: HTML 200, CSS/JS 200, ссылки валидны. + +## Статус + +- [ ] Вынести документацию из registry +- [ ] Node.js-сервис +- [ ] Деплой + Ingress +- [ ] Переписанный генератор diff --git a/server/docs.go b/server/docs.go deleted file mode 100644 index 3333514..0000000 --- a/server/docs.go +++ /dev/null @@ -1,114 +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////... (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 /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 - } - - ext := filepath.Ext(key) - ctype := mime.TypeByExtension(ext) - if ctype == "" { - 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) -} diff --git a/server/main.go b/server/main.go index 1585f2d..33e838e 100644 --- a/server/main.go +++ b/server/main.go @@ -24,7 +24,7 @@ var ( s3Prefix = os.Getenv("S3_PREFIX") ) -const VERSION = "0.0.5" +const VERSION = "0.0.6" func main() { if hostname == "" { @@ -56,7 +56,6 @@ func main() { http.HandleFunc("/.well-known/terraform.json", discoveryHandler) http.HandleFunc("/v1/providers/", router) http.HandleFunc("/v1/proxy", proxyHandler) - http.HandleFunc("/docs/", docsHandler) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(logoFile)))) http.Handle("/", http.HandlerFunc(rootHandler)) http.HandleFunc("/healthz", healthzHandler)