refactor: структура Go — main.go 91стр, handlers/proxy/router_versions/docs/gpg_key
This commit is contained in:
@@ -0,0 +1,6 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed templates/index.html
|
||||||
|
var templateFS embed.FS
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
+2
-394
@@ -1,20 +1,12 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
|
||||||
"context"
|
"context"
|
||||||
"embed"
|
"embed"
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"log"
|
"log"
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -27,62 +19,17 @@ var logoFile embed.FS
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
s3Client *s3.Client
|
s3Client *s3.Client
|
||||||
bucketName = "terraform-registry" // Default
|
bucketName = "terraform-registry"
|
||||||
hostname = os.Getenv("REGISTRY_HOSTNAME")
|
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"
|
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() {
|
func main() {
|
||||||
// 1. Init S3 Connection
|
|
||||||
if hostname == "" {
|
if hostname == "" {
|
||||||
hostname = "localhost:5000"
|
hostname = "localhost:5000"
|
||||||
}
|
}
|
||||||
// S3_PREFIX default comes from Dockerfile ENV; override in jsonEnv if needed.
|
|
||||||
if s3Prefix == "" {
|
if s3Prefix == "" {
|
||||||
s3Prefix = hostname
|
s3Prefix = hostname
|
||||||
}
|
}
|
||||||
@@ -105,9 +52,7 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("S3 init failed: %v", err)
|
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("/.well-known/terraform.json", discoveryHandler)
|
||||||
http.HandleFunc("/v1/providers/", router)
|
http.HandleFunc("/v1/providers/", router)
|
||||||
http.HandleFunc("/v1/proxy", proxyHandler)
|
http.HandleFunc("/v1/proxy", proxyHandler)
|
||||||
@@ -144,340 +89,3 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Println("Server stopped")
|
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, `<!DOCTYPE html>
|
|
||||||
<html lang="ru">
|
|
||||||
<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;
|
|
||||||
}
|
|
||||||
.logo {
|
|
||||||
font-size: 24px; font-weight: 700; color: #001C34;
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
.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>
|
|
||||||
<div class="header">
|
|
||||||
<img src="/static/logo.svg" height="18" alt="nubes">
|
|
||||||
</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>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="footer">Powered by Nubes Cloud · S3 Storage</div>
|
|
||||||
</body>
|
|
||||||
</html>`)
|
|
||||||
}
|
|
||||||
|
|
||||||
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"))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="ru">
|
||||||
|
<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;
|
||||||
|
}
|
||||||
|
.logo {
|
||||||
|
font-size: 24px; font-weight: 700; color: #001C34;
|
||||||
|
letter-spacing: -0.5px;
|
||||||
|
}
|
||||||
|
.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>
|
||||||
|
<div class="header">
|
||||||
|
<img src="/static/logo.svg" height="18" alt="nubes">
|
||||||
|
</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>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="footer">Powered by Nubes Cloud · S3 Storage</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
Reference in New Issue
Block a user