init: registry server + operator from VM
This commit is contained in:
@@ -0,0 +1,47 @@
|
||||
# Exclude files not needed in Docker build context for operator
|
||||
.git
|
||||
.gitignore
|
||||
.dockerignore
|
||||
|
||||
# Docs / markdown
|
||||
*.md
|
||||
docs/
|
||||
|
||||
# Other services / infra
|
||||
server/
|
||||
scripts/
|
||||
charts/
|
||||
k8s/
|
||||
|
||||
# Secrets & keys
|
||||
secrets/
|
||||
*.pem
|
||||
*.key
|
||||
*.asc
|
||||
*.gpg
|
||||
*.pgp
|
||||
|
||||
# Artefacts / temp
|
||||
artifacts/
|
||||
*.har
|
||||
HAR/
|
||||
registry-server-build/
|
||||
universal_rebuild/
|
||||
tools/
|
||||
devops/
|
||||
|
||||
# IDE / OS
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
.DS_Store
|
||||
|
||||
# Go test/coverage
|
||||
*.test
|
||||
*.out
|
||||
coverage.out
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
@@ -0,0 +1,23 @@
|
||||
IMAGE_REPO ?= naeel
|
||||
TAG ?= latest
|
||||
PLATFORM ?= linux/amd64
|
||||
|
||||
.PHONY: build-all push-all build-operator push-operator build-registry push-registry tidy
|
||||
|
||||
tidy:
|
||||
go mod tidy
|
||||
|
||||
build-all: build-operator build-registry
|
||||
push-all: push-operator push-registry
|
||||
|
||||
build-operator:
|
||||
docker build --platform $(PLATFORM) -t $(IMAGE_REPO)/terraform-registry-operator:$(TAG) -f build/Dockerfile.operator .
|
||||
|
||||
push-operator:
|
||||
docker push $(IMAGE_REPO)/terraform-registry-operator:$(TAG)
|
||||
|
||||
build-registry:
|
||||
docker build --platform $(PLATFORM) -t $(IMAGE_REPO)/terraform-registry-server:$(TAG) -f build/Dockerfile.registry .
|
||||
|
||||
push-registry:
|
||||
docker push $(IMAGE_REPO)/terraform-registry-server:$(TAG)
|
||||
@@ -0,0 +1,44 @@
|
||||
# Terra Operator Manifests
|
||||
|
||||
## Minimal Setup for Namespace `terra`
|
||||
|
||||
### 1. Structure
|
||||
- **Namespace**: `terra`
|
||||
- **Storage**: внешний S3 (Nubes Cloud).
|
||||
- **CRD**: `TerraformProviderRelease` definitions.
|
||||
|
||||
### 2. How to apply
|
||||
```bash
|
||||
kubectl apply -f operator/manifests/00-namespace.yaml
|
||||
kubectl apply -f operator/manifests/02-crd.yaml
|
||||
kubectl apply -f operator/manifests/04-operator-deployment.yaml
|
||||
kubectl apply -f operator/manifests/05-registry-server.yaml
|
||||
```
|
||||
|
||||
### 3. Usage
|
||||
Create a release request:
|
||||
```yaml
|
||||
apiVersion: terra.core.nubes.ru/v1alpha1
|
||||
kind: TerraformProviderRelease
|
||||
metadata:
|
||||
name: mycloud-v0-1-0
|
||||
namespace: terra
|
||||
spec:
|
||||
providerName: "mycloud"
|
||||
version: "0.1.0"
|
||||
gitRepo: "https://github.com/Start-Ops/terraform-provider-mycloud.git"
|
||||
gitRef: "v0.1.0"
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Простыми словами: что это и зачем
|
||||
|
||||
Это набор манифестов и небольшой сервис, который автоматизирует сборку Terraform‑провайдеров.
|
||||
Идея простая: я описываю в CRD, какой провайдер и какую версию собрать, а оператор сам запускает Job,
|
||||
собирает бинарники и складывает их в хранилище. Дальше registry‑server отдает эти артефакты Terraform‑клиентам.
|
||||
|
||||
Зачем это нужно:
|
||||
- чтобы не собирать провайдеры руками;
|
||||
- чтобы хранить артефакты в одном месте;
|
||||
- чтобы Terraform мог скачивать провайдеры по обычному источнику.
|
||||
@@ -0,0 +1,18 @@
|
||||
# Build Stage
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY cmd ./cmd
|
||||
# Build statically
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /operator cmd/main.go
|
||||
|
||||
# Run Stage
|
||||
FROM alpine:3.19
|
||||
WORKDIR /app
|
||||
COPY --from=builder /operator /app/operator
|
||||
USER 1000:1000
|
||||
|
||||
CMD ["/app/operator"]
|
||||
@@ -0,0 +1,20 @@
|
||||
# Build Stage
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
COPY go.mod go.sum ./
|
||||
COPY registrykeys ./registrykeys
|
||||
RUN go mod download
|
||||
COPY cmd/registry ./cmd/registry
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /registry-server ./cmd/registry
|
||||
|
||||
# Run Stage
|
||||
FROM alpine:3.19
|
||||
WORKDIR /app
|
||||
COPY --from=builder /registry-server /app/registry-server
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
USER 1000:1000
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["/app/registry-server"]
|
||||
@@ -0,0 +1,23 @@
|
||||
# Build Stage
|
||||
FROM golang:1.24-alpine AS builder
|
||||
|
||||
WORKDIR /app
|
||||
# Only copy what's needed for the registry server
|
||||
COPY go.mod go.sum ./
|
||||
RUN go mod download
|
||||
|
||||
COPY cmd/registry ./cmd/registry
|
||||
# Build statically
|
||||
RUN CGO_ENABLED=0 GOOS=linux go build -o /registry-server ./cmd/registry
|
||||
|
||||
# Run Stage
|
||||
FROM alpine:3.19
|
||||
WORKDIR /app
|
||||
COPY --from=builder /registry-server /app/registry-server
|
||||
# Registry server uses HTTPS for S3, so CA certs are good to have
|
||||
RUN apk add --no-cache ca-certificates
|
||||
|
||||
USER 1000:1000
|
||||
EXPOSE 8080
|
||||
|
||||
CMD ["/app/registry-server"]
|
||||
@@ -0,0 +1,116 @@
|
||||
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/<namespace>/<name>/<version>/... (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 <path>/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
|
||||
}
|
||||
|
||||
// Determine content-type
|
||||
ext := filepath.Ext(key)
|
||||
ctype := mime.TypeByExtension(ext)
|
||||
if ctype == "" {
|
||||
// fallback for HTML
|
||||
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)
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
package main
|
||||
|
||||
import registrykeys "terraform-registry-keys"
|
||||
|
||||
var gpgPublicKey = GPGPublicKey{
|
||||
KeyID: registrykeys.KeyID,
|
||||
ASCIIArmor: registrykeys.ASCIIArmor,
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
// 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:8080"
|
||||
}
|
||||
|
||||
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
|
||||
s3Client, err = s3.New(endpoint, &s3.Options{
|
||||
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
|
||||
Secure: true, // Force secure for cloud S3
|
||||
})
|
||||
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))
|
||||
|
||||
log.Printf("Starting Registry Service on :8080 (Bucket: %s, Endpoint: %s)\n", bucketName, endpoint)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
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.Fprint(w, `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Terra Registry</title></head>
|
||||
<body>
|
||||
<h1>Terra Registry & Documentation Server</h1>
|
||||
<p>Status: <span style="color: green">ONLINE</span></p>
|
||||
<hr>
|
||||
<p>Powered by Nubes Cloud S3 Storage</p>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
|
||||
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
bucket := r.URL.Query().Get("bucket")
|
||||
key := r.URL.Query().Get("key")
|
||||
|
||||
if bucket == "" || key == "" {
|
||||
http.Error(w, "Missing bucket or key params", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
obj, err := s3Client.GetObject(context.Background(), bucket, key, s3.GetObjectOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Error getting object %s/%s: %v", bucket, 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", bucket, 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/", hostname, 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, "_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"})
|
||||
}
|
||||
if strings.Contains(fileName, "_darwin_amd64.zip") {
|
||||
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "amd64"})
|
||||
}
|
||||
if strings.Contains(fileName, "_darwin_arm64.zip") {
|
||||
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "arm64"})
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range seenVersions {
|
||||
versions = append(versions, *v)
|
||||
}
|
||||
|
||||
resp := VersionList{
|
||||
ID: fmt.Sprintf("%s/%s/%s", hostname, 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", hostname, 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))
|
||||
|
||||
gpgKey := gpgPublicKey
|
||||
|
||||
resp := DownloadResponse{
|
||||
Protocols: []string{"5.0"},
|
||||
OS: osType,
|
||||
Arch: arch,
|
||||
Filename: filename,
|
||||
DownloadURL: downloadLink,
|
||||
ShasumsURL: shasumsLink,
|
||||
ShasumsSignatureURL: sigLink,
|
||||
Shasum: shasumValue,
|
||||
SigningKeys: SigningKeys{
|
||||
GPGPublicKeys: []GPGPublicKey{gpgKey},
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
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")
|
||||
)
|
||||
|
||||
// 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:8080"
|
||||
}
|
||||
|
||||
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
|
||||
s3Client, err = s3.New(endpoint, &s3.Options{
|
||||
Creds: credentials.NewStaticV4(accessKeyID, secretAccessKey, ""),
|
||||
Secure: true, // Force secure for cloud S3
|
||||
})
|
||||
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))
|
||||
|
||||
log.Printf("Starting Registry Service on :8080 (Bucket: %s, Endpoint: %s)\n", bucketName, endpoint)
|
||||
log.Fatal(http.ListenAndServe(":8080", nil))
|
||||
}
|
||||
|
||||
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.Fprint(w, `
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head><title>Terra Registry</title></head>
|
||||
<body>
|
||||
<h1>Terra Registry & Documentation Server</h1>
|
||||
<p>Status: <span style="color: green">ONLINE</span></p>
|
||||
<hr>
|
||||
<p>Powered by Nubes Cloud S3 Storage</p>
|
||||
</body>
|
||||
</html>
|
||||
`)
|
||||
}
|
||||
|
||||
func proxyHandler(w http.ResponseWriter, r *http.Request) {
|
||||
bucket := r.URL.Query().Get("bucket")
|
||||
key := r.URL.Query().Get("key")
|
||||
|
||||
if bucket == "" || key == "" {
|
||||
http.Error(w, "Missing bucket or key params", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
obj, err := s3Client.GetObject(context.Background(), bucket, key, s3.GetObjectOptions{})
|
||||
if err != nil {
|
||||
log.Printf("Error getting object %s/%s: %v", bucket, 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", bucket, 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/", hostname, 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, "_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"})
|
||||
}
|
||||
if strings.Contains(fileName, "_darwin_amd64.zip") {
|
||||
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "amd64"})
|
||||
}
|
||||
if strings.Contains(fileName, "_darwin_arm64.zip") {
|
||||
seenVersions[verStr].Platforms = append(seenVersions[verStr].Platforms, Platform{OS: "darwin", Arch: "arm64"})
|
||||
}
|
||||
}
|
||||
|
||||
for _, v := range seenVersions {
|
||||
versions = append(versions, *v)
|
||||
}
|
||||
|
||||
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", hostname, 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))
|
||||
|
||||
gpgKey := gpgPublicKey
|
||||
|
||||
resp := DownloadResponse{
|
||||
Protocols: []string{"5.0"},
|
||||
OS: osType,
|
||||
Arch: arch,
|
||||
Filename: filename,
|
||||
DownloadURL: downloadLink,
|
||||
ShasumsURL: shasumsLink,
|
||||
ShasumsSignatureURL: sigLink,
|
||||
Shasum: shasumValue,
|
||||
SigningKeys: SigningKeys{
|
||||
GPGPublicKeys: []GPGPublicKey{gpgKey},
|
||||
},
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
apiVersion: terra.core.nubes.ru/v1alpha1
|
||||
kind: TerraformProviderRelease
|
||||
metadata:
|
||||
name: scaffolding-v0-0-1
|
||||
namespace: terra
|
||||
spec:
|
||||
providerName: "scaffolding"
|
||||
namespace: "hashicorp"
|
||||
version: "0.0.1"
|
||||
gitRepo: "https://github.com/hashicorp/terraform-provider-scaffolding.git"
|
||||
gitRef: "main"
|
||||
@@ -0,0 +1,37 @@
|
||||
module terraform-provider-operator
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.12
|
||||
|
||||
require (
|
||||
github.com/minio/minio-go/v7 v7.0.98
|
||||
terraform-registry-keys v0.0.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/go-ini/ini v1.67.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 // indirect
|
||||
github.com/klauspost/crc32 v1.3.0 // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/minio/crc64nvme v1.1.1 // indirect
|
||||
github.com/minio/md5-simd v1.1.2 // indirect
|
||||
github.com/philhofer/fwd v1.2.0 // indirect
|
||||
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||
github.com/rogpeppe/go-internal v1.10.0 // indirect
|
||||
github.com/rs/xid v1.6.0 // indirect
|
||||
github.com/tinylib/msgp v1.6.1 // indirect
|
||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||
golang.org/x/crypto v0.46.0 // indirect
|
||||
golang.org/x/net v0.48.0 // indirect
|
||||
golang.org/x/sys v0.39.0 // indirect
|
||||
golang.org/x/text v0.32.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace terraform-registry-keys => ./registrykeys
|
||||
@@ -0,0 +1,58 @@
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/go-ini/ini v1.67.0 h1:z6ZrTEZqSWOTyH2FlglNbNgARyHG8oLW9gMELqKr06A=
|
||||
github.com/go-ini/ini v1.67.0/go.mod h1:ByCAeIL28uOIIG0E3PJtZPDL8WnHpFKFOtgjp+3Ies8=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.0.1/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11 h1:0OwqZRYI2rFrjS4kvkDnqJkKHdHaRnCm68/DY4OxRzU=
|
||||
github.com/klauspost/cpuid/v2 v2.2.11/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/klauspost/crc32 v1.3.0 h1:sSmTt3gUt81RP655XGZPElI0PelVTZ6YwCRnPSupoFM=
|
||||
github.com/klauspost/crc32 v1.3.0/go.mod h1:D7kQaZhnkX/Y0tstFGf8VUzv2UofNGqCjnC3zdHB0Hw=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/minio/crc64nvme v1.1.1 h1:8dwx/Pz49suywbO+auHCBpCtlW1OfpcLN7wYgVR6wAI=
|
||||
github.com/minio/crc64nvme v1.1.1/go.mod h1:eVfm2fAzLlxMdUGc0EEBGSMmPwmXD5XiNRpnu9J3bvg=
|
||||
github.com/minio/md5-simd v1.1.2 h1:Gdi1DZK69+ZVMoNHRXJyNcxrMA4dSxoYHZSQbirFg34=
|
||||
github.com/minio/md5-simd v1.1.2/go.mod h1:MzdKDxYpY2BT9XQFocsiZf/NKVtR7nkE4RoEpN+20RM=
|
||||
github.com/minio/minio-go/v7 v7.0.98 h1:MeAVKjLVz+XJ28zFcuYyImNSAh8Mq725uNW4beRisi0=
|
||||
github.com/minio/minio-go/v7 v7.0.98/go.mod h1:cY0Y+W7yozf0mdIclrttzo1Iiu7mEf9y7nk2uXqMOvM=
|
||||
github.com/philhofer/fwd v1.2.0 h1:e6DnBTl7vGY+Gz322/ASL4Gyp1FspeMvx1RNDoToZuM=
|
||||
github.com/philhofer/fwd v1.2.0/go.mod h1:RqIHx9QI14HlwKwm98g9Re5prTQ6LdeRQn+gXJFxsJM=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.6.0 h1:fV591PaemRlL6JfRxGDEPl69wICngIQ3shQtzfy2gxU=
|
||||
github.com/rs/xid v1.6.0/go.mod h1:7XoLgs4eV+QndskICGsho+ADou8ySMSjJKDIan90Nz0=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/tinylib/msgp v1.6.1 h1:ESRv8eL3u+DNHUoSAAQRE50Hm162zqAnBoGv9PzScPY=
|
||||
github.com/tinylib/msgp v1.6.1/go.mod h1:RSp0LW9oSxFut3KzESt5Voq4GVWyS+PSulT77roAqEA=
|
||||
go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc=
|
||||
go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg=
|
||||
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
|
||||
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
|
||||
golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU=
|
||||
golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY=
|
||||
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
|
||||
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
|
||||
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,6 @@
|
||||
apiVersion: v1
|
||||
kind: Namespace
|
||||
metadata:
|
||||
name: terra
|
||||
labels:
|
||||
name: terra
|
||||
@@ -0,0 +1,35 @@
|
||||
apiVersion: v1
|
||||
kind: ServiceAccount
|
||||
metadata:
|
||||
name: terraform-operator
|
||||
namespace: terra
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: Role
|
||||
metadata:
|
||||
name: terraform-operator
|
||||
namespace: terra
|
||||
rules:
|
||||
- apiGroups: ["terra.core.nubes.ru"]
|
||||
resources: ["providerreleases", "providerreleases/status"]
|
||||
verbs: ["get", "list", "watch", "update", "patch"]
|
||||
- apiGroups: ["batch"]
|
||||
resources: ["jobs"]
|
||||
verbs: ["create", "get", "list", "watch", "delete"]
|
||||
- apiGroups: [""]
|
||||
resources: ["pods", "pods/log"]
|
||||
verbs: ["get", "list", "watch"]
|
||||
---
|
||||
apiVersion: rbac.authorization.k8s.io/v1
|
||||
kind: RoleBinding
|
||||
metadata:
|
||||
name: terraform-operator
|
||||
namespace: terra
|
||||
subjects:
|
||||
- kind: ServiceAccount
|
||||
name: terraform-operator
|
||||
namespace: terra
|
||||
roleRef:
|
||||
kind: Role
|
||||
name: terraform-operator
|
||||
apiGroup: rbac.authorization.k8s.io
|
||||
@@ -0,0 +1,77 @@
|
||||
apiVersion: apiextensions.k8s.io/v1
|
||||
kind: CustomResourceDefinition
|
||||
metadata:
|
||||
name: providerreleases.terra.core.nubes.ru
|
||||
spec:
|
||||
group: terra.core.nubes.ru
|
||||
versions:
|
||||
- name: v1alpha1
|
||||
served: true
|
||||
storage: true
|
||||
subresources:
|
||||
status: {}
|
||||
schema:
|
||||
openAPIV3Schema:
|
||||
type: object
|
||||
properties:
|
||||
spec:
|
||||
type: object
|
||||
required:
|
||||
- providerName
|
||||
- version
|
||||
- gitRepo
|
||||
- gitRef
|
||||
properties:
|
||||
providerName:
|
||||
type: string
|
||||
description: "Name of the provider (e.g. 'mycloud')"
|
||||
namespace:
|
||||
type: string
|
||||
default: "mycloud"
|
||||
description: "Terraform registry namespace (e.g. 'nubes')"
|
||||
version:
|
||||
type: string
|
||||
description: "Semantic version without v (e.g. '1.0.1')"
|
||||
gitRepo:
|
||||
type: string
|
||||
description: "Git repository URL to clone"
|
||||
gitRef:
|
||||
type: string
|
||||
description: "Git tag, commit SHA or branch"
|
||||
platforms:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
default: ["linux_amd64", "windows_amd64"]
|
||||
description: "Target platforms for build"
|
||||
status:
|
||||
type: object
|
||||
properties:
|
||||
phase:
|
||||
type: string
|
||||
description: "Current phase: Pending, Building, Publishing, Ready, Failed"
|
||||
downloadUrl:
|
||||
type: string
|
||||
conditions:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
status:
|
||||
type: string
|
||||
lastTransitionTime:
|
||||
type: string
|
||||
format: date-time
|
||||
reason:
|
||||
type: string
|
||||
message:
|
||||
type: string
|
||||
scope: Namespaced
|
||||
names:
|
||||
plural: providerreleases
|
||||
singular: providerrelease
|
||||
kind: TerraformProviderRelease
|
||||
shortNames:
|
||||
- tpr
|
||||
@@ -0,0 +1,75 @@
|
||||
apiVersion: v1
|
||||
kind: ConfigMap
|
||||
metadata:
|
||||
name: builder-script
|
||||
namespace: terra
|
||||
data:
|
||||
build.sh: |
|
||||
#!/bin/sh
|
||||
set -e
|
||||
|
||||
echo ">>> Starting Build for $PROVIDER_NAME $VERSION"
|
||||
|
||||
# 1. Setup Environment
|
||||
# Inputs: GIT_REPO, GIT_REF, PROVIDER_NAME, VERSION
|
||||
WORK_DIR="/workspace"
|
||||
ARTIFACTS_DIR="/artifacts"
|
||||
mkdir -p $WORK_DIR $ARTIFACTS_DIR
|
||||
|
||||
# 2. Clone Repository
|
||||
echo ">>> Cloning $GIT_REPO ($GIT_REF)..."
|
||||
git clone $GIT_REPO $WORK_DIR/src
|
||||
cd $WORK_DIR/src
|
||||
git checkout $GIT_REF
|
||||
|
||||
# 3. Build for Platforms
|
||||
# Hardcoded platforms for MVP: linux/amd64, windows/amd64
|
||||
# Naming convention: terraform-provider-NAME_vVERSION_OS_ARCH.zip
|
||||
|
||||
echo ">>> Building binaries..."
|
||||
|
||||
# Linux amd64
|
||||
echo "--> linux/amd64"
|
||||
GOOS=linux GOARCH=amd64 CGO_ENABLED=0 go build -o terraform-provider-${PROVIDER_NAME}_v${VERSION}
|
||||
zip ${ARTIFACTS_DIR}/terraform-provider-${PROVIDER_NAME}_${VERSION}_linux_amd64.zip terraform-provider-${PROVIDER_NAME}_v${VERSION}
|
||||
rm terraform-provider-${PROVIDER_NAME}_v${VERSION}
|
||||
|
||||
# Windows amd64
|
||||
echo "--> windows/amd64"
|
||||
GOOS=windows GOARCH=amd64 CGO_ENABLED=0 go build -o terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
|
||||
zip ${ARTIFACTS_DIR}/terraform-provider-${PROVIDER_NAME}_${VERSION}_windows_amd64.zip terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
|
||||
rm terraform-provider-${PROVIDER_NAME}_v${VERSION}.exe
|
||||
|
||||
# 4. Checksums
|
||||
cd $ARTIFACTS_DIR
|
||||
sha256sum *.zip > terraform-provider-${PROVIDER_NAME}_${VERSION}_SHA256SUMS
|
||||
|
||||
# 5. GPG Sign (Mock for MVP - empty sig or skip if client allows, but Terraform strict.
|
||||
# For now we will generate a dummy signature to satisfy file existence, real signing requires GPG private key in env)
|
||||
# Ideally: gpg --detach-sign --armor terraform-provider-..._SHA256SUMS
|
||||
touch terraform-provider-${PROVIDER_NAME}_${VERSION}_SHA256SUMS.sig
|
||||
|
||||
# 6. Upload to S3
|
||||
# Layout: /terraform-providers/hostname/namespace/name/version/
|
||||
# We will use 'REGISTRY_HOSTNAME' env var if set, else default.
|
||||
|
||||
if [ -z "$REGISTRY_HOSTNAME" ]; then
|
||||
REGISTRY_HOSTNAME="terra.k8c.ru"
|
||||
fi
|
||||
|
||||
TARGET_PATH="terraform-providers/$REGISTRY_HOSTNAME/${NAMESPACE}/${PROVIDER_NAME}/${VERSION}"
|
||||
|
||||
echo ">>> Uploading to S3 path: $TARGET_PATH"
|
||||
|
||||
# Configure mc using S3 credentials (S3_ENDPOINT, S3_ACCESS_KEY, S3_SECRET_KEY)
|
||||
mc alias set nubes_s3 https://$S3_ENDPOINT $S3_ACCESS_KEY $S3_SECRET_KEY
|
||||
|
||||
# Upload files
|
||||
mc cp --recursive $ARTIFACTS_DIR/ nubes_s3/$TARGET_PATH/
|
||||
|
||||
# 7. Generate shasums file for download (Terraform download protocol requires a checksums.txt usually or just the sums file)
|
||||
# We are following the provider registry protocol loosely.
|
||||
# The most important part for discovery is the 'index.json' or 'versions' endpoint, but we are static hosting.
|
||||
# We rely on the directory structure.
|
||||
|
||||
echo ">>> Build Complete!"
|
||||
@@ -0,0 +1,27 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: terraform-operator
|
||||
namespace: terra
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: terraform-operator
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: terraform-operator
|
||||
spec:
|
||||
serviceAccountName: terraform-operator
|
||||
containers:
|
||||
- name: operator
|
||||
image: naeel/terraform-registry-operator:latest
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: REGISTRY_HOSTNAME
|
||||
value: "terra.k8c.ru"
|
||||
resources:
|
||||
limits:
|
||||
memory: "128Mi"
|
||||
cpu: "500m"
|
||||
@@ -0,0 +1,81 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: registry-server
|
||||
namespace: terra
|
||||
labels:
|
||||
app: registry-server
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: registry-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: registry-server
|
||||
spec:
|
||||
containers:
|
||||
- name: registry-server
|
||||
image: naeel/terraform-registry-server:gpg-key-20260208
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: REGISTRY_HOSTNAME
|
||||
value: "terra.k8c.ru"
|
||||
- name: S3_ENDPOINT
|
||||
value: "s3.msk-1.ngcloud.ru"
|
||||
- name: S3_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: access-key
|
||||
name: s3-credentials
|
||||
- name: S3_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: secret-key
|
||||
name: s3-credentials
|
||||
- name: S3_BUCKET
|
||||
value: "terraform-registry"
|
||||
- name: S3_USE_SSL
|
||||
value: "true"
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
|
||||
---
|
||||
apiVersion: v1
|
||||
kind: Service
|
||||
metadata:
|
||||
name: registry-server
|
||||
namespace: terra
|
||||
spec:
|
||||
selector:
|
||||
app: registry-server
|
||||
ports:
|
||||
- port: 80
|
||||
targetPort: 8080
|
||||
---
|
||||
apiVersion: networking.k8s.io/v1
|
||||
kind: Ingress
|
||||
metadata:
|
||||
name: registry-ingress
|
||||
namespace: terra
|
||||
annotations:
|
||||
nginx.ingress.kubernetes.io/rewrite-target: /
|
||||
cert-manager.io/cluster-issuer: letsencrypt-prod
|
||||
spec:
|
||||
ingressClassName: nginx
|
||||
tls:
|
||||
- hosts:
|
||||
- terra.k8c.ru
|
||||
secretName: registry-tls
|
||||
rules:
|
||||
- host: terra.k8c.ru
|
||||
http:
|
||||
paths:
|
||||
- path: /
|
||||
pathType: Prefix
|
||||
backend:
|
||||
service:
|
||||
name: registry-server
|
||||
port:
|
||||
number: 80
|
||||
@@ -0,0 +1,42 @@
|
||||
apiVersion: apps/v1
|
||||
kind: Deployment
|
||||
metadata:
|
||||
name: registry-server
|
||||
namespace: terra
|
||||
labels:
|
||||
app: registry-server
|
||||
spec:
|
||||
replicas: 1
|
||||
selector:
|
||||
matchLabels:
|
||||
app: registry-server
|
||||
template:
|
||||
metadata:
|
||||
labels:
|
||||
app: registry-server
|
||||
spec:
|
||||
containers:
|
||||
- name: registry-server
|
||||
image: naeel/terraform-registry-server:docs-dev-fixed
|
||||
imagePullPolicy: Always
|
||||
env:
|
||||
- name: REGISTRY_HOSTNAME
|
||||
value: "terra.k8c.ru"
|
||||
- name: S3_ENDPOINT
|
||||
value: "s3.msk-1.ngcloud.ru"
|
||||
- name: S3_ACCESS_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: access-key
|
||||
name: s3-credentials
|
||||
- name: S3_SECRET_KEY
|
||||
valueFrom:
|
||||
secretKeyRef:
|
||||
key: secret-key
|
||||
name: s3-credentials
|
||||
- name: S3_BUCKET
|
||||
value: "terraform-registry"
|
||||
- name: S3_USE_SSL
|
||||
value: "true"
|
||||
ports:
|
||||
- containerPort: 8080
|
||||
@@ -0,0 +1,5 @@
|
||||
module terraform-registry-keys
|
||||
|
||||
go 1.24.0
|
||||
|
||||
toolchain go1.24.12
|
||||
@@ -0,0 +1,59 @@
|
||||
package registrykeys
|
||||
|
||||
const KeyID = "3EC4673EB798238A"
|
||||
|
||||
const ASCIIArmor = `-----BEGIN PGP PUBLIC KEY BLOCK-----
|
||||
|
||||
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
|
||||
6cZ0NQ==
|
||||
=3Ea2
|
||||
-----END PGP PUBLIC KEY BLOCK-----
|
||||
`
|
||||
Reference in New Issue
Block a user