package main import ( "bufio" "context" "embed" "encoding/json" "fmt" "io" "log" "net" "net/http" "net/url" "os" "os/signal" "sort" "strings" "syscall" "time" s3 "github.com/minio/minio-go/v7" "github.com/minio/minio-go/v7/pkg/credentials" ) //go:embed logo.svg var logoFile embed.FS var ( s3Client *s3.Client bucketName = "terraform-registry" // Default hostname = os.Getenv("REGISTRY_HOSTNAME") s3Prefix = os.Getenv("S3_PREFIX") // S3 key prefix (may differ from hostname) ) const VERSION = "0.0.2" // Terraform Registry Protocol Structs type Discovery struct { ProvidersV1 string `json:"providers.v1"` } type VersionList struct { ID string `json:"id"` Versions []Version `json:"versions"` Warnings []string `json:"warnings"` } type Version struct { Version string `json:"version"` Protocols []string `json:"protocols"` Platforms []Platform `json:"platforms"` } type Platform struct { OS string `json:"os"` Arch string `json:"arch"` } type DownloadResponse struct { Protocols []string `json:"protocols"` OS string `json:"os"` Arch string `json:"arch"` Filename string `json:"filename"` DownloadURL string `json:"download_url"` ShasumsURL string `json:"shasums_url"` ShasumsSignatureURL string `json:"shasums_signature_url"` Shasum string `json:"shasum"` SigningKeys SigningKeys `json:"signing_keys"` } type SigningKeys struct { GPGPublicKeys []GPGPublicKey `json:"gpg_public_keys"` } type GPGPublicKey struct { KeyID string `json:"key_id"` ASCIIArmor string `json:"ascii_armor"` } func main() { // 1. Init S3 Connection if hostname == "" { hostname = "localhost:5000" } // S3_PREFIX default comes from Dockerfile ENV; override in jsonEnv if needed. if s3Prefix == "" { s3Prefix = hostname } 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 useSSL := os.Getenv("S3_USE_SSL") != "false" log.Printf("S3 connecting: endpoint=%s bucket=%s useSSL=%v", endpoint, bucketName, useSSL) s3Client, err = s3.New(endpoint, &s3.Options{ Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), Secure: useSSL, }) if err != nil { log.Fatalf("S3 init failed: %v", err) } log.Printf("S3 client created, testing BucketExists...") // 2. HTTP Handlers http.HandleFunc("/.well-known/terraform.json", discoveryHandler) http.HandleFunc("/v1/providers/", router) http.HandleFunc("/v1/proxy", proxyHandler) http.HandleFunc("/docs/", docsHandler) http.Handle("/static/", http.StripPrefix("/static/", http.FileServer(http.FS(logoFile)))) http.Handle("/", http.HandlerFunc(rootHandler)) http.HandleFunc("/healthz", healthzHandler) http.HandleFunc("/readyz", readyzHandler) http.HandleFunc("/debug", debugHandler) port := os.Getenv("PORT") 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 debugHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "text/plain; charset=utf-8") fmt.Fprintf(w, "=== ENV ===\n") for _, e := range os.Environ() { if strings.Contains(e, "S3_") || strings.Contains(e, "REGISTRY") || strings.Contains(e, "PORT") { fmt.Fprintln(w, e) } } s3ep := os.Getenv("S3_ENDPOINT") fmt.Fprintf(w, "\n=== DNS: %s ===\n", s3ep) addrs, err := net.LookupHost(s3ep) if err != nil { fmt.Fprintf(w, "DNS ERROR: %v\n", err) } else { for _, a := range addrs { fmt.Fprintf(w, " %s\n", a) } } fmt.Fprintf(w, "\n=== TCP dial: %s:443 ===\n", s3ep) conn, err := net.DialTimeout("tcp", s3ep+":443", 5*time.Second) if err != nil { fmt.Fprintf(w, "TCP ERROR: %v\n", err) } else { fmt.Fprintf(w, "TCP OK: %s -> %s\n", conn.LocalAddr(), conn.RemoteAddr()) conn.Close() } fmt.Fprintf(w, "\n=== S3 BucketExists ===\n") ctx, cancel := context.WithTimeout(r.Context(), 10*time.Second) defer cancel() _, err = s3Client.BucketExists(ctx, bucketName) if err != nil { fmt.Fprintf(w, "S3 ERROR: %v\n", err) } else { fmt.Fprintf(w, "S3 OK: bucket %s accessible\n", bucketName) } } func rootHandler(w http.ResponseWriter, r *http.Request) { if r.URL.Path != "/" { http.NotFound(w, r) return } w.Header().Set("Content-Type", "text/html; charset=utf-8") fmt.Fprintf(w, ` Terraform Registry · Nubes
nubes
Terraform Provider Registry
Статус ONLINE
Версия v`+VERSION+`
`) } func proxyHandler(w http.ResponseWriter, r *http.Request) { key := r.URL.Query().Get("key") if key == "" { http.Error(w, "Missing key param", http.StatusBadRequest) return } if !strings.HasPrefix(key, s3Prefix+"/") && !strings.HasPrefix(key, "docs/") { http.Error(w, "Forbidden", http.StatusForbidden) return } ctx, cancel := context.WithTimeout(r.Context(), 60*time.Second) defer cancel() obj, err := s3Client.GetObject(ctx, bucketName, key, s3.GetObjectOptions{}) if err != nil { log.Printf("Error getting object %s/%s: %v", bucketName, key, err) http.Error(w, "File not found", http.StatusNotFound) return } defer obj.Close() stat, err := obj.Stat() if err != nil { log.Printf("Error stating object %s/%s: %v", bucketName, key, err) http.Error(w, "File not found or not accessible", http.StatusNotFound) return } w.Header().Set("Content-Length", fmt.Sprintf("%d", stat.Size)) w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Last-Modified", stat.LastModified.Format(http.TimeFormat)) buf := make([]byte, 64*1024) // 64KB buffer flusher, canFlush := w.(http.Flusher) written := int64(0) for { n, readErr := obj.Read(buf) if n > 0 { nw, writeErr := w.Write(buf[:n]) if writeErr != nil { log.Printf("Error writing response: %v", writeErr) return } written += int64(nw) if canFlush { flusher.Flush() } } if readErr != nil { if readErr != io.EOF { log.Printf("Error reading object: %v", readErr) } break } } log.Printf("Proxy: sent %d/%d bytes for %s", written, stat.Size, key) } func discoveryHandler(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(Discovery{ProvidersV1: "/v1/providers/"}) } func router(w http.ResponseWriter, r *http.Request) { path := strings.TrimPrefix(r.URL.Path, "/v1/providers/") parts := strings.Split(path, "/") if len(parts) == 3 && parts[2] == "versions" { listVersions(w, r, parts[0], parts[1]) return } if len(parts) == 6 && parts[3] == "download" { downloadVersion(w, r, parts[0], parts[1], parts[2], parts[4], parts[5]) return } http.Error(w, "Not Found", http.StatusNotFound) } func listVersions(w http.ResponseWriter, r *http.Request, namespace, pType string) { prefix := fmt.Sprintf("%s/%s/%s/", s3Prefix, namespace, pType) ctx := context.Background() versions := []Version{} seenVersions := map[string]*Version{} objectCh := s3Client.ListObjects(ctx, bucketName, s3.ListObjectsOptions{ Prefix: prefix, Recursive: true, }) for object := range objectCh { if object.Err != nil { continue } parts := strings.Split(object.Key, "/") if len(parts) < 5 { continue } verStr := parts[3] fileName := parts[4] if _, ok := seenVersions[verStr]; !ok { seenVersions[verStr] = &Version{ Version: verStr, Protocols: []string{"5.0"}, Platforms: []Platform{}, } } if strings.Contains(fileName, "_darwin_amd64.zip") { seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "amd64"}) } if strings.Contains(fileName, "_linux_amd64.zip") { seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "linux", Arch: "amd64"}) } if strings.Contains(fileName, "_windows_amd64.zip") { seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "windows", Arch: "amd64"}) } } for _, v := range seenVersions { versions = append(versions, *v) } sort.Slice(versions, func(i, j int) bool { return versions[i].Version < versions[j].Version }) resp := VersionList{ ID: fmt.Sprintf("%s/%s", namespace, pType), Versions: versions, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } func downloadVersion(w http.ResponseWriter, r *http.Request, namespace, pType, version, osType, arch string) { basePath := fmt.Sprintf("%s/%s/%s/%s", s3Prefix, namespace, pType, version) filename := fmt.Sprintf("terraform-provider-%s_%s_%s_%s.zip", pType, version, osType, arch) fullKey := fmt.Sprintf("%s/%s", basePath, filename) shasumsKey := fmt.Sprintf("%s/terraform-provider-%s_%s_SHA256SUMS", basePath, pType, version) sigKey := fmt.Sprintf("%s/terraform-provider-%s_%s_SHA256SUMS.sig", basePath, pType, version) var shasumValue string shasumsObj, err := s3Client.GetObject(context.Background(), bucketName, shasumsKey, s3.GetObjectOptions{}) if err == nil { defer shasumsObj.Close() scanner := bufio.NewScanner(shasumsObj) for scanner.Scan() { line := scanner.Text() if strings.Contains(line, filename) { fields := strings.Fields(line) if len(fields) >= 1 { shasumValue = fields[0] } break } } } baseURL := "https://" + hostname downloadLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(fullKey)) shasumsLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(shasumsKey)) sigLink := fmt.Sprintf("%s/v1/proxy?bucket=%s&key=%s", baseURL, bucketName, url.QueryEscape(sigKey)) resp := DownloadResponse{ Protocols: []string{"5.0"}, OS: osType, Arch: arch, Filename: filename, DownloadURL: downloadLink, ShasumsURL: shasumsLink, ShasumsSignatureURL: sigLink, Shasum: shasumValue, SigningKeys: SigningKeys{ GPGPublicKeys: []GPGPublicKey{gpgPrimaryKey, gpgLegacyKey}, }, } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(resp) } func healthzHandler(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) } func readyzHandler(w http.ResponseWriter, r *http.Request) { ctx, cancel := context.WithTimeout(r.Context(), 5*time.Second) defer cancel() _, err := s3Client.BucketExists(ctx, bucketName) if err != nil { msg := fmt.Sprintf("s3 unreachable: %v", err) log.Printf("readyz: %s", msg) w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte(msg)) return } w.WriteHeader(http.StatusOK) w.Write([]byte("ok")) }