feat: share proxmox cluster client with worker

This commit is contained in:
Philipp
2026-06-11 10:24:36 +02:00
parent 7c818cf92e
commit bf31a8db37
16 changed files with 240 additions and 14 deletions
+250
View File
@@ -0,0 +1,250 @@
package cluster
import (
"context"
"database/sql"
"errors"
"fmt"
"strings"
"time"
"forgejo.digital-droplets.de/philschlo/proxui/platform/encryption"
)
type Cluster struct {
ID string
Name string
APIEndpoint string
TLSFingerprint string
TokenID string
TokenSecret string
Status string
CreatedAt time.Time
}
type StoredCluster struct {
ID string
Name string
APIEndpoint string
TLSFingerprint string
EncryptedToken []byte
TokenID string
Status string
CreatedAt time.Time
}
type Storage interface {
Get(ctx context.Context, id string) (StoredCluster, bool, error)
Upsert(ctx context.Context, cluster StoredCluster) (StoredCluster, error)
SetStatus(ctx context.Context, id string, status string) (StoredCluster, bool, error)
}
type Repository struct {
storage Storage
cipher encryption.Cipher
}
func NewRepository(db *sql.DB, cipher encryption.Cipher) Repository {
return Repository{
storage: SQLStorage{db: db},
cipher: cipher,
}
}
func NewRepositoryWithStorage(storage Storage, cipher encryption.Cipher) Repository {
return Repository{
storage: storage,
cipher: cipher,
}
}
func (r Repository) GetCluster(ctx context.Context, id string) (Cluster, bool, error) {
stored, found, err := r.storage.Get(ctx, id)
if err != nil || !found {
return Cluster{}, found, err
}
tokenSecret, err := r.cipher.Decrypt(stored.EncryptedToken)
if err != nil {
return Cluster{}, false, err
}
return Cluster{
ID: stored.ID,
Name: stored.Name,
APIEndpoint: stored.APIEndpoint,
TLSFingerprint: stored.TLSFingerprint,
TokenID: stored.TokenID,
TokenSecret: string(tokenSecret),
Status: stored.Status,
CreatedAt: stored.CreatedAt,
}, true, nil
}
func (r Repository) UpsertCluster(ctx context.Context, cluster Cluster) (Cluster, error) {
if strings.TrimSpace(cluster.TokenSecret) == "" {
return Cluster{}, fmt.Errorf("token secret is required")
}
encryptedToken, err := r.cipher.Encrypt([]byte(cluster.TokenSecret))
if err != nil {
return Cluster{}, err
}
status := strings.TrimSpace(cluster.Status)
if status == "" {
status = "active"
}
stored, err := r.storage.Upsert(ctx, StoredCluster{
ID: strings.TrimSpace(cluster.ID),
Name: cluster.Name,
APIEndpoint: cluster.APIEndpoint,
TLSFingerprint: cluster.TLSFingerprint,
EncryptedToken: encryptedToken,
TokenID: cluster.TokenID,
Status: status,
})
if err != nil {
return Cluster{}, err
}
return Cluster{
ID: stored.ID,
Name: stored.Name,
APIEndpoint: stored.APIEndpoint,
TLSFingerprint: stored.TLSFingerprint,
TokenID: stored.TokenID,
TokenSecret: cluster.TokenSecret,
Status: stored.Status,
CreatedAt: stored.CreatedAt,
}, nil
}
func (r Repository) SetClusterStatus(ctx context.Context, id string, status string) (Cluster, bool, error) {
stored, found, err := r.storage.SetStatus(ctx, strings.TrimSpace(id), strings.TrimSpace(status))
if err != nil || !found {
return Cluster{}, found, err
}
return Cluster{
ID: stored.ID,
Name: stored.Name,
APIEndpoint: stored.APIEndpoint,
TLSFingerprint: stored.TLSFingerprint,
TokenID: stored.TokenID,
Status: stored.Status,
CreatedAt: stored.CreatedAt,
}, true, nil
}
type SQLStorage struct {
db *sql.DB
}
func (s SQLStorage) Get(ctx context.Context, id string) (StoredCluster, bool, error) {
var cluster StoredCluster
err := s.db.QueryRowContext(ctx, `
select id::text, name, api_endpoint, tls_fingerprint, encrypted_token, token_id, status, created_at
from public.clusters
where id = $1
`, id).Scan(
&cluster.ID,
&cluster.Name,
&cluster.APIEndpoint,
&cluster.TLSFingerprint,
&cluster.EncryptedToken,
&cluster.TokenID,
&cluster.Status,
&cluster.CreatedAt,
)
if errors.Is(err, sql.ErrNoRows) {
return StoredCluster{}, false, nil
}
if err != nil {
return StoredCluster{}, false, err
}
return cluster, true, nil
}
func (s SQLStorage) Upsert(ctx context.Context, cluster StoredCluster) (StoredCluster, error) {
var stored StoredCluster
err := s.db.QueryRowContext(ctx, `
insert into public.clusters (
id,
name,
api_endpoint,
tls_fingerprint,
encrypted_token,
token_id,
status
)
values (
coalesce(nullif($1, '')::uuid, gen_random_uuid()),
$2,
$3,
$4,
$5,
$6,
$7
)
on conflict (id) do update
set name = excluded.name,
api_endpoint = excluded.api_endpoint,
tls_fingerprint = excluded.tls_fingerprint,
encrypted_token = excluded.encrypted_token,
token_id = excluded.token_id,
status = excluded.status
returning id::text, name, api_endpoint, tls_fingerprint, encrypted_token, token_id, status, created_at
`,
cluster.ID,
cluster.Name,
cluster.APIEndpoint,
cluster.TLSFingerprint,
cluster.EncryptedToken,
cluster.TokenID,
cluster.Status,
).Scan(
&stored.ID,
&stored.Name,
&stored.APIEndpoint,
&stored.TLSFingerprint,
&stored.EncryptedToken,
&stored.TokenID,
&stored.Status,
&stored.CreatedAt,
)
if err != nil {
return StoredCluster{}, err
}
return stored, nil
}
func (s SQLStorage) SetStatus(ctx context.Context, id string, status string) (StoredCluster, bool, error) {
var stored StoredCluster
err := s.db.QueryRowContext(ctx, `
update public.clusters
set status = $2
where id = $1
returning id::text, name, api_endpoint, tls_fingerprint, encrypted_token, token_id, status, created_at
`, id, status).Scan(
&stored.ID,
&stored.Name,
&stored.APIEndpoint,
&stored.TLSFingerprint,
&stored.EncryptedToken,
&stored.TokenID,
&stored.Status,
&stored.CreatedAt,
)
if errors.Is(err, sql.ErrNoRows) {
return StoredCluster{}, false, nil
}
if err != nil {
return StoredCluster{}, false, err
}
return stored, true, nil
}
+167
View File
@@ -0,0 +1,167 @@
package cluster
import (
"bytes"
"context"
"fmt"
"testing"
"time"
"forgejo.digital-droplets.de/philschlo/proxui/platform/encryption"
)
func TestRepositoryEncryptsStoredTokenAndDecryptsOnLoad(t *testing.T) {
cipher := testCipher(t, 1)
storage := newMemoryStorage()
repository := NewRepositoryWithStorage(storage, cipher)
saved, err := repository.UpsertCluster(context.Background(), Cluster{
ID: "cluster-1",
Name: "Lab",
APIEndpoint: "https://pve.example.test:8006",
TLSFingerprint: "AA:BB",
TokenID: "root@pam!proxui",
TokenSecret: "secret-token",
})
if err != nil {
t.Fatalf("UpsertCluster() error = %v", err)
}
stored := storage.records[saved.ID]
if bytes.Contains(stored.EncryptedToken, []byte("secret-token")) {
t.Fatal("stored encrypted token contains plaintext")
}
got, found, err := repository.GetCluster(context.Background(), saved.ID)
if err != nil {
t.Fatalf("GetCluster() error = %v", err)
}
if !found {
t.Fatal("GetCluster() found = false, want true")
}
if got.TokenSecret != "secret-token" {
t.Fatalf("TokenSecret = %q, want secret-token", got.TokenSecret)
}
if got.TokenID != "root@pam!proxui" {
t.Fatalf("TokenID = %q, want root@pam!proxui", got.TokenID)
}
}
func TestRepositoryRejectsMissingTokenSecret(t *testing.T) {
repository := NewRepositoryWithStorage(newMemoryStorage(), testCipher(t, 1))
_, err := repository.UpsertCluster(context.Background(), Cluster{
ID: "cluster-1",
Name: "Lab",
APIEndpoint: "https://pve.example.test:8006",
TokenID: "root@pam!proxui",
})
if err == nil {
t.Fatal("UpsertCluster() error = nil, want error")
}
}
func TestRepositoryReturnsErrorWhenStoredTokenUsesWrongKey(t *testing.T) {
storage := newMemoryStorage()
writer := NewRepositoryWithStorage(storage, testCipher(t, 1))
reader := NewRepositoryWithStorage(storage, testCipher(t, 2))
saved, err := writer.UpsertCluster(context.Background(), Cluster{
ID: "cluster-1",
Name: "Lab",
APIEndpoint: "https://pve.example.test:8006",
TLSFingerprint: "AA:BB",
TokenID: "root@pam!proxui",
TokenSecret: "secret-token",
})
if err != nil {
t.Fatalf("UpsertCluster() error = %v", err)
}
if _, _, err := reader.GetCluster(context.Background(), saved.ID); err == nil {
t.Fatal("GetCluster() error = nil, want error")
}
}
func TestRepositorySetsClusterStatusWithoutDecryptingToken(t *testing.T) {
storage := newMemoryStorage()
repository := NewRepositoryWithStorage(storage, testCipher(t, 1))
_, err := repository.UpsertCluster(context.Background(), Cluster{
ID: "cluster-1",
Name: "Lab",
APIEndpoint: "https://pve.example.test:8006",
TLSFingerprint: "AA:BB",
TokenID: "root@pam!proxui",
TokenSecret: "secret-token",
})
if err != nil {
t.Fatalf("UpsertCluster() error = %v", err)
}
updated, found, err := repository.SetClusterStatus(context.Background(), "cluster-1", "disabled")
if err != nil {
t.Fatalf("SetClusterStatus() error = %v", err)
}
if !found {
t.Fatal("SetClusterStatus() found = false, want true")
}
if updated.Status != "disabled" {
t.Fatalf("Status = %q, want disabled", updated.Status)
}
if updated.TokenSecret != "" {
t.Fatal("SetClusterStatus() returned token secret")
}
}
func testCipher(t *testing.T, value byte) encryption.Cipher {
t.Helper()
cipher, err := encryption.New(bytes.Repeat([]byte{value}, 32))
if err != nil {
t.Fatalf("encryption.New() error = %v", err)
}
return cipher
}
type memoryStorage struct {
records map[string]StoredCluster
nextID int
}
func newMemoryStorage() *memoryStorage {
return &memoryStorage{
records: make(map[string]StoredCluster),
}
}
func (s *memoryStorage) Get(_ context.Context, id string) (StoredCluster, bool, error) {
cluster, ok := s.records[id]
return cluster, ok, nil
}
func (s *memoryStorage) Upsert(_ context.Context, cluster StoredCluster) (StoredCluster, error) {
if cluster.ID == "" {
s.nextID++
cluster.ID = fmt.Sprintf("cluster-%d", s.nextID)
}
if cluster.Status == "" {
cluster.Status = "active"
}
if cluster.CreatedAt.IsZero() {
cluster.CreatedAt = time.Now()
}
s.records[cluster.ID] = cluster
return cluster, nil
}
func (s *memoryStorage) SetStatus(_ context.Context, id string, status string) (StoredCluster, bool, error) {
cluster, ok := s.records[id]
if !ok {
return StoredCluster{}, false, nil
}
cluster.Status = status
s.records[id] = cluster
return cluster, true, nil
}