package main import ( "bufio" "context" "encoding/json" "fmt" "io" "log" "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" ) 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.1" // 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" s3Client, err = s3.New(endpoint, &s3.Options{ Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""), Secure: useSSL, }) if err != nil { log.Fatalln(err) } // 2. HTTP Handlers http.HandleFunc("/.well-known/terraform.json", discoveryHandler) http.HandleFunc("/v1/providers/", router) http.HandleFunc("/v1/proxy", proxyHandler) http.HandleFunc("/docs/", docsHandler) http.Handle("/", http.HandlerFunc(rootHandler)) http.HandleFunc("/healthz", healthzHandler) http.HandleFunc("/readyz", readyzHandler) 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 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
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 } obj, err := s3Client.GetObject(context.Background(), 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)) 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/", 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) { _, 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.Write([]byte("ok")) }