diff --git a/server/embed.go b/server/embed.go
new file mode 100644
index 0000000..09a0875
--- /dev/null
+++ b/server/embed.go
@@ -0,0 +1,6 @@
+package main
+
+import "embed"
+
+//go:embed templates/index.html
+var templateFS embed.FS
diff --git a/server/handlers.go b/server/handlers.go
new file mode 100644
index 0000000..39c7c26
--- /dev/null
+++ b/server/handlers.go
@@ -0,0 +1,133 @@
+package main
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "log"
+ "net"
+ "net/http"
+ "os"
+ "strings"
+ "time"
+)
+
+// 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 discoveryHandler(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(Discovery{ProvidersV1: "/v1/providers/"})
+}
+
+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")
+ tmpl, _ := templateFS.ReadFile("index.html")
+ html := strings.Replace(string(tmpl), "{{.Version}}", VERSION, 1)
+ fmt.Fprint(w, html)
+}
+
+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"))
+}
+
+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)
+ }
+}
diff --git a/server/main.go b/server/main.go
index 265fe3e..8becc3b 100644
--- a/server/main.go
+++ b/server/main.go
@@ -1,20 +1,12 @@
package main
import (
- "bufio"
"context"
"embed"
- "encoding/json"
- "fmt"
- "io"
"log"
- "net"
"net/http"
- "net/url"
"os"
"os/signal"
- "sort"
- "strings"
"syscall"
"time"
@@ -27,62 +19,17 @@ var logoFile embed.FS
var (
s3Client *s3.Client
- bucketName = "terraform-registry" // Default
+ bucketName = "terraform-registry"
hostname = os.Getenv("REGISTRY_HOSTNAME")
- s3Prefix = os.Getenv("S3_PREFIX") // S3 key prefix (may differ from hostname)
+ s3Prefix = os.Getenv("S3_PREFIX")
)
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
}
@@ -105,9 +52,7 @@ func main() {
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)
@@ -144,340 +89,3 @@ func main() {
}
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
-
-
-
-
-
-
-
-
-
- Статус
- 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))
-
- written, err := io.Copy(w, obj)
- if err != nil {
- log.Printf("Error streaming object: %v (sent %d/%d bytes)", err, written, stat.Size)
- } else {
- log.Printf("Proxy: sent %d bytes for %s", written, 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"))
-}
diff --git a/server/proxy.go b/server/proxy.go
new file mode 100644
index 0000000..82bf0b6
--- /dev/null
+++ b/server/proxy.go
@@ -0,0 +1,55 @@
+package main
+
+import (
+ "context"
+ "fmt"
+ "io"
+ "log"
+ "net/http"
+ "strings"
+ "time"
+
+ s3 "github.com/minio/minio-go/v7"
+)
+
+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))
+
+ written, err := io.Copy(w, obj)
+ if err != nil {
+ log.Printf("Error streaming object: %v (sent %d/%d bytes)", err, written, stat.Size)
+ } else {
+ log.Printf("Proxy: sent %d bytes for %s", written, key)
+ }
+}
diff --git a/server/router_versions.go b/server/router_versions.go
new file mode 100644
index 0000000..29b6a33
--- /dev/null
+++ b/server/router_versions.go
@@ -0,0 +1,135 @@
+package main
+
+import (
+ "bufio"
+ "context"
+ "encoding/json"
+ "fmt"
+ "net/http"
+ "net/url"
+ "sort"
+ "strings"
+
+ s3 "github.com/minio/minio-go/v7"
+)
+
+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, parts[0], parts[1])
+ return
+ }
+
+ if len(parts) == 6 && parts[3] == "download" {
+ downloadVersion(w, parts[0], parts[1], parts[2], parts[4], parts[5])
+ return
+ }
+
+ http.Error(w, "Not Found", http.StatusNotFound)
+}
+
+func listVersions(w http.ResponseWriter, namespace, pType string) {
+ prefix := fmt.Sprintf("%s/%s/%s/", s3Prefix, namespace, pType)
+ versions := []Version{}
+ seenVersions := map[string]*Version{}
+
+ objectCh := s3Client.ListObjects(context.Background(), 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, 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)
+}
diff --git a/server/templates/index.html b/server/templates/index.html
new file mode 100644
index 0000000..e159592
--- /dev/null
+++ b/server/templates/index.html
@@ -0,0 +1,99 @@
+
+
+
+
+
+Terraform Registry · Nubes
+
+
+
+
+
+
+
+
+
+ Статус
+ ONLINE
+
+
+ Версия
+ v{{.Version}}
+
+
+
+
+
+
+