186 lines
5.0 KiB
Go
186 lines
5.0 KiB
Go
package serviceadapter
|
|
|
|
import (
|
|
"crypto/tls"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"net/http"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Loader watches ServiceAdapter CRs via the Kubernetes REST API and populates the registry.
|
|
// No client-go dependency — uses the in-cluster service account token directly.
|
|
type Loader struct {
|
|
registry *Registry
|
|
namespace string
|
|
client *http.Client
|
|
token string
|
|
baseURL string
|
|
stopChan chan struct{}
|
|
once sync.Once
|
|
}
|
|
|
|
// NewLoader creates a loader that reads ServiceAdapters from the k8s API.
|
|
// Returns an error if not running inside a cluster (missing SA token/ca).
|
|
func NewLoader(registry *Registry, namespace string) (*Loader, error) {
|
|
if registry == nil {
|
|
return nil, fmt.Errorf("registry cannot be nil")
|
|
}
|
|
if namespace == "" {
|
|
namespace = "api"
|
|
}
|
|
|
|
tokenBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/token")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("not in cluster: %w", err)
|
|
}
|
|
|
|
caBytes, err := os.ReadFile("/var/run/secrets/kubernetes.io/serviceaccount/ca.crt")
|
|
if err != nil {
|
|
return nil, fmt.Errorf("missing CA cert: %w", err)
|
|
}
|
|
|
|
pool := x509.NewCertPool()
|
|
pool.AppendCertsFromPEM(caBytes)
|
|
|
|
// Use kube-api-proxy (nginx) in the same namespace to reach the API server.
|
|
// This avoids needing direct egress to the API server ClusterIP which
|
|
// standard NetworkPolicy can't allow through Cilium.
|
|
proxyHost := os.Getenv("KUBE_API_PROXY_URL")
|
|
if proxyHost == "" {
|
|
proxyHost = "https://kube-api-proxy.api.svc.cluster.local:8443"
|
|
}
|
|
|
|
return &Loader{
|
|
registry: registry,
|
|
namespace: namespace,
|
|
token: string(tokenBytes),
|
|
baseURL: proxyHost,
|
|
client: &http.Client{
|
|
Timeout: 10 * time.Second,
|
|
Transport: &http.Transport{
|
|
TLSClientConfig: &tls.Config{RootCAs: pool, InsecureSkipVerify: true},
|
|
},
|
|
},
|
|
stopChan: make(chan struct{}),
|
|
}, nil
|
|
}
|
|
|
|
// Start begins loading ServiceAdapters in the background. Non-blocking.
|
|
// Retries on failure so the gateway can start serving immediately.
|
|
func (l *Loader) Start(interval time.Duration) {
|
|
go func() {
|
|
// Initial load with retries (API server may be slow on startup)
|
|
for attempt := 1; ; attempt++ {
|
|
if err := l.load(); err != nil {
|
|
backoff := time.Duration(min(attempt*5, 30)) * time.Second
|
|
log.Printf("serviceadapter loader: attempt %d failed (%v), retry in %s", attempt, err, backoff)
|
|
time.Sleep(backoff)
|
|
continue
|
|
}
|
|
break
|
|
}
|
|
|
|
// Poll for changes
|
|
ticker := time.NewTicker(interval)
|
|
defer ticker.Stop()
|
|
for {
|
|
select {
|
|
case <-ticker.C:
|
|
if err := l.load(); err != nil {
|
|
log.Printf("serviceadapter loader poll error: %v", err)
|
|
}
|
|
case <-l.stopChan:
|
|
return
|
|
}
|
|
}
|
|
}()
|
|
}
|
|
|
|
// Stop stops the background poll.
|
|
func (l *Loader) Stop() {
|
|
l.once.Do(func() { close(l.stopChan) })
|
|
}
|
|
|
|
// load fetches all ServiceAdapters from the k8s API and syncs the registry.
|
|
func (l *Loader) load() error {
|
|
url := fmt.Sprintf("%s/apis/gateway.riotpiao.com/v1/namespaces/%s/serviceadapters", l.baseURL, l.namespace)
|
|
req, err := http.NewRequest("GET", url, nil)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req.Header.Set("Authorization", "Bearer "+l.token)
|
|
req.Header.Set("Accept", "application/json")
|
|
|
|
resp, err := l.client.Do(req)
|
|
if err != nil {
|
|
return fmt.Errorf("k8s API request: %w", err)
|
|
}
|
|
defer resp.Body.Close()
|
|
|
|
if resp.StatusCode == 429 {
|
|
return fmt.Errorf("k8s API: storage reinitializing (429)")
|
|
}
|
|
if resp.StatusCode != 200 {
|
|
body, _ := io.ReadAll(resp.Body)
|
|
return fmt.Errorf("k8s API: %d %s", resp.StatusCode, string(body[:min(len(body), 200)]))
|
|
}
|
|
|
|
var list crList
|
|
if err := json.NewDecoder(resp.Body).Decode(&list); err != nil {
|
|
return fmt.Errorf("decoding response: %w", err)
|
|
}
|
|
|
|
// Sync: add/update all found, registry handles dedup
|
|
for i := range list.Items {
|
|
item := &list.Items[i]
|
|
adapter := &ServiceAdapter{
|
|
Name: item.Metadata.Name,
|
|
Namespace: item.Metadata.Namespace,
|
|
ServiceName: item.Spec.ServiceName,
|
|
CreatedAt: time.Now(),
|
|
}
|
|
adapter.Spec.Upstream.URL = item.Spec.Upstream.URL
|
|
adapter.Spec.Upstream.TimeoutSeconds = item.Spec.Upstream.TimeoutSeconds
|
|
adapter.Spec.Auth.Required = item.Spec.Auth.Required
|
|
adapter.Spec.Auth.Capability = item.Spec.Auth.Capability
|
|
|
|
if existing := l.registry.Get(item.Spec.ServiceName); existing != nil {
|
|
l.registry.Update(adapter)
|
|
} else {
|
|
l.registry.Add(adapter)
|
|
log.Printf("serviceadapter loaded: %s → %s", item.Spec.ServiceName, item.Spec.Upstream.URL)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Minimal JSON structs for the k8s list response — no client-go needed.
|
|
type crList struct {
|
|
Items []crItem `json:"items"`
|
|
}
|
|
|
|
type crItem struct {
|
|
Metadata struct {
|
|
Name string `json:"name"`
|
|
Namespace string `json:"namespace"`
|
|
} `json:"metadata"`
|
|
Spec struct {
|
|
ServiceName string `json:"serviceName"`
|
|
Upstream struct {
|
|
URL string `json:"url"`
|
|
TimeoutSeconds int32 `json:"timeoutSeconds"`
|
|
} `json:"upstream"`
|
|
Auth struct {
|
|
Required bool `json:"required"`
|
|
Capability string `json:"capability"`
|
|
} `json:"auth"`
|
|
} `json:"spec"`
|
|
}
|