feat: share proxmox cluster client with worker
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
|
|
||||||
## Unreleased
|
## Unreleased
|
||||||
|
|
||||||
|
- Cluster-, Krypto- und Proxmox-Client-Bausteine nach `platform/` verschoben und im Worker fuer echte UPID-Polls verdrahtet.
|
||||||
- UPID-Polling-Job fuer Proxmox-Tasks mit VM-Status-Update und Audit-Writer angelegt.
|
- UPID-Polling-Job fuer Proxmox-Tasks mit VM-Status-Update und Audit-Writer angelegt.
|
||||||
- Worker-Grundgeruest mit asynq, Redis-Anbindung, DB-Ping und Dummy-Job-Handler angelegt.
|
- Worker-Grundgeruest mit asynq, Redis-Anbindung, DB-Ping und Dummy-Job-Handler angelegt.
|
||||||
- Interne Operator-Endpunkte fuer Cluster-Anlage, Token-Rotation und Status-Updates ohne Token-Leak angelegt.
|
- Interne Operator-Endpunkte fuer Cluster-Anlage, Token-Rotation und Status-Updates ohne Token-Leak angelegt.
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ Der Proxmox-Client nutzt HTTPS mit normaler Zertifikatsvalidierung plus SHA-256-
|
|||||||
|
|
||||||
Interne Cluster-Admin-Routen unter `/internal/clusters` sind nicht tenant-gebunden. Sie erfordern den Header `X-ProxUI-Operator-Token` mit `OPERATOR_TOKEN` und die isolierte Rolle `operator`; Responses enthalten nie `token_secret`.
|
Interne Cluster-Admin-Routen unter `/internal/clusters` sind nicht tenant-gebunden. Sie erfordern den Header `X-ProxUI-Operator-Token` mit `OPERATOR_TOKEN` und die isolierte Rolle `operator`; Responses enthalten nie `token_secret`.
|
||||||
|
|
||||||
Der Worker nutzt `asynq` gegen `REDIS_ADDR`, pingt beim Start dieselbe Supabase-DB ueber `DATABASE_URL` und registriert aktuell den Dummy-Job `proxui.dummy` als Smoke-Test fuer die Queue-Verarbeitung.
|
Der Worker nutzt `asynq` gegen `REDIS_ADDR`, pingt beim Start dieselbe Supabase-DB ueber `DATABASE_URL`, entschluesselt Cluster-Credentials mit `MASTER_KEY_BASE64` und nutzt den gemeinsamen Proxmox-Client aus `platform/proxmox` fuer UPID-Polls.
|
||||||
|
|
||||||
Lokale Dienste:
|
Lokale Dienste:
|
||||||
|
|
||||||
@@ -65,7 +65,7 @@ Aktuelle Targets:
|
|||||||
Worker-Jobs:
|
Worker-Jobs:
|
||||||
|
|
||||||
- `proxui.dummy`: Dummy-Job mit Payload `{"message":"..."}` fuer Smoke-Tests der asynq-Verarbeitung
|
- `proxui.dummy`: Dummy-Job mit Payload `{"message":"..."}` fuer Smoke-Tests der asynq-Verarbeitung
|
||||||
- `proxmox.task.poll`: Pollt einen Proxmox-UPID, setzt VM-Status auf Erfolgs- oder Fehlerstatus und schreibt Audit-Metadaten. Die echte Cluster-Client-Aufloesung wird im naechsten Integrationsschritt angeschlossen.
|
- `proxmox.task.poll`: Pollt einen Proxmox-UPID ueber den gespeicherten Cluster, setzt VM-Status auf Erfolgs- oder Fehlerstatus und schreibt Audit-Metadaten.
|
||||||
|
|
||||||
Backend-Endpunkte:
|
Backend-Endpunkte:
|
||||||
|
|
||||||
|
|||||||
@@ -132,7 +132,12 @@ Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsda
|
|||||||
- [x] Polling-Handler mit Proxmox-Status-Client-Interface angelegt
|
- [x] Polling-Handler mit Proxmox-Status-Client-Interface angelegt
|
||||||
- [x] VM-Status-Update und Audit-Writer fuer finale Task-Ergebnisse angelegt
|
- [x] VM-Status-Update und Audit-Writer fuer finale Task-Ergebnisse angelegt
|
||||||
- [x] Mock-Proxmox-Test fuer `running` -> `stopped/OK` und Fehlerstatus angelegt
|
- [x] Mock-Proxmox-Test fuer `running` -> `stopped/OK` und Fehlerstatus angelegt
|
||||||
- [x] Echte Cluster-Client-Aufloesung als separater Integrationspunkt gekapselt
|
- [x] Echte Cluster-Client-Aufloesung ueber gemeinsames Cluster-Repository und Proxmox-Client verdrahtet
|
||||||
|
- [x] Shared-Plattform-Follow-up fuer Worker-Integration
|
||||||
|
- [x] Cluster-Repository von `backend/internal` nach `platform/cluster` verschoben
|
||||||
|
- [x] Krypto-Layer nach `platform/encryption` verschoben
|
||||||
|
- [x] Proxmox-Client nach `platform/proxmox` verschoben
|
||||||
|
- [x] Backend und Worker nutzen dieselben Implementierungen
|
||||||
|
|
||||||
## MVP-Backlog
|
## MVP-Backlog
|
||||||
|
|
||||||
@@ -192,3 +197,4 @@ Arbeitsliste auf Basis von `proxmox-console-entwicklungsplan.md`. Die Entwurfsda
|
|||||||
- 2026-06-11: Interne Operator-Endpunkte fuer Cluster-Anlage, Token-Rotation und Status-Updates angelegt.
|
- 2026-06-11: Interne Operator-Endpunkte fuer Cluster-Anlage, Token-Rotation und Status-Updates angelegt.
|
||||||
- 2026-06-11: Worker-Grundgeruest mit asynq, Redis-Anbindung, DB-Ping und Dummy-Job-Tests angelegt.
|
- 2026-06-11: Worker-Grundgeruest mit asynq, Redis-Anbindung, DB-Ping und Dummy-Job-Tests angelegt.
|
||||||
- 2026-06-11: UPID-Polling-Job mit VM-Status-Update, Audit-Writer und Mock-Proxmox-Tests angelegt.
|
- 2026-06-11: UPID-Polling-Job mit VM-Status-Update, Audit-Writer und Mock-Proxmox-Tests angelegt.
|
||||||
|
- 2026-06-11: Shared Cluster-/Krypto-/Proxmox-Packages nach `platform/` verschoben und Worker-UPID-Polling an echte Cluster-Aufloesung angeschlossen.
|
||||||
|
|||||||
@@ -12,15 +12,15 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/cluster"
|
||||||
"forgejo.digital-droplets.de/philschlo/proxui/platform/config"
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/config"
|
||||||
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/encryption"
|
||||||
"forgejo.digital-droplets.de/philschlo/proxui/platform/logging"
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/logging"
|
||||||
_ "github.com/jackc/pgx/v5/stdlib"
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
|
|
||||||
"proxui/backend/internal/auth"
|
"proxui/backend/internal/auth"
|
||||||
"proxui/backend/internal/authorization"
|
"proxui/backend/internal/authorization"
|
||||||
"proxui/backend/internal/cluster"
|
|
||||||
"proxui/backend/internal/clusteradmin"
|
"proxui/backend/internal/clusteradmin"
|
||||||
"proxui/backend/internal/encryption"
|
|
||||||
"proxui/backend/internal/membership"
|
"proxui/backend/internal/membership"
|
||||||
"proxui/backend/internal/operator"
|
"proxui/backend/internal/operator"
|
||||||
"proxui/backend/internal/profile"
|
"proxui/backend/internal/profile"
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"proxui/backend/internal/cluster"
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/cluster"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Repository interface {
|
type Repository interface {
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
|
||||||
"proxui/backend/internal/cluster"
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/cluster"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestCreateClusterDoesNotReturnTokenSecret(t *testing.T) {
|
func TestCreateClusterDoesNotReturnTokenSecret(t *testing.T) {
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"proxui/backend/internal/encryption"
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/encryption"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Cluster struct {
|
type Cluster struct {
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"proxui/backend/internal/encryption"
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/encryption"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestRepositoryEncryptsStoredTokenAndDecryptsOnLoad(t *testing.T) {
|
func TestRepositoryEncryptsStoredTokenAndDecryptsOnLoad(t *testing.T) {
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -15,7 +16,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"proxui/backend/internal/cluster"
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/cluster"
|
||||||
)
|
)
|
||||||
|
|
||||||
const defaultTimeout = 15 * time.Second
|
const defaultTimeout = 15 * time.Second
|
||||||
@@ -125,6 +126,43 @@ func (c *Client) Delete(ctx context.Context, path string) (*http.Response, error
|
|||||||
return c.Do(ctx, http.MethodDelete, path, nil)
|
return c.Do(ctx, http.MethodDelete, path, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type TaskStatus struct {
|
||||||
|
Status string
|
||||||
|
ExitStatus string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Client) GetTaskStatus(ctx context.Context, node string, upid string) (TaskStatus, error) {
|
||||||
|
response, err := c.Get(ctx, fmt.Sprintf(
|
||||||
|
"/nodes/%s/tasks/%s/status",
|
||||||
|
url.PathEscape(node),
|
||||||
|
url.PathEscape(upid),
|
||||||
|
))
|
||||||
|
if err != nil {
|
||||||
|
return TaskStatus{}, err
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
if response.StatusCode >= http.StatusBadRequest {
|
||||||
|
_, _ = io.Copy(io.Discard, response.Body)
|
||||||
|
return TaskStatus{}, fmt.Errorf("proxmox returned %s", response.Status)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body struct {
|
||||||
|
Data struct {
|
||||||
|
Status string `json:"status"`
|
||||||
|
ExitStatus string `json:"exitstatus"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(response.Body).Decode(&body); err != nil {
|
||||||
|
return TaskStatus{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return TaskStatus{
|
||||||
|
Status: body.Data.Status,
|
||||||
|
ExitStatus: body.Data.ExitStatus,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Client) Do(ctx context.Context, method string, path string, body []byte) (*http.Response, error) {
|
func (c *Client) Do(ctx context.Context, method string, path string, body []byte) (*http.Response, error) {
|
||||||
var lastErr error
|
var lastErr error
|
||||||
attempts := c.retries + 1
|
attempts := c.retries + 1
|
||||||
@@ -12,7 +12,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"proxui/backend/internal/cluster"
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/cluster"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestClientAcceptsMatchingFingerprintAndSendsTokenHeader(t *testing.T) {
|
func TestClientAcceptsMatchingFingerprintAndSendsTokenHeader(t *testing.T) {
|
||||||
@@ -85,6 +85,29 @@ func TestClientRetriesServerErrors(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestClientGetsTaskStatus(t *testing.T) {
|
||||||
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if r.URL.Path != "/api2/json/nodes/pve/tasks/UPID:pve:1/status" {
|
||||||
|
t.Fatalf("path = %q", r.URL.Path)
|
||||||
|
}
|
||||||
|
_, _ = w.Write([]byte(`{"data":{"status":"stopped","exitstatus":"OK"}}`))
|
||||||
|
}))
|
||||||
|
defer server.Close()
|
||||||
|
|
||||||
|
client := newTestClient(t, server, fingerprintForServer(server))
|
||||||
|
status, err := client.GetTaskStatus(context.Background(), "pve", "UPID:pve:1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetTaskStatus() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if status.Status != "stopped" {
|
||||||
|
t.Fatalf("Status = %q, want stopped", status.Status)
|
||||||
|
}
|
||||||
|
if status.ExitStatus != "OK" {
|
||||||
|
t.Fatalf("ExitStatus = %q, want OK", status.ExitStatus)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestNewClientRejectsInvalidFingerprint(t *testing.T) {
|
func TestNewClientRejectsInvalidFingerprint(t *testing.T) {
|
||||||
_, err := NewClient(cluster.Cluster{
|
_, err := NewClient(cluster.Cluster{
|
||||||
APIEndpoint: "https://pve.example.test:8006/api2/json",
|
APIEndpoint: "https://pve.example.test:8006/api2/json",
|
||||||
@@ -11,7 +11,9 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/cluster"
|
||||||
"forgejo.digital-droplets.de/philschlo/proxui/platform/config"
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/config"
|
||||||
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/encryption"
|
||||||
"forgejo.digital-droplets.de/philschlo/proxui/platform/logging"
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/logging"
|
||||||
"github.com/hibiken/asynq"
|
"github.com/hibiken/asynq"
|
||||||
_ "github.com/jackc/pgx/v5/stdlib"
|
_ "github.com/jackc/pgx/v5/stdlib"
|
||||||
@@ -28,6 +30,12 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
logger := logging.New("worker", cfg.AppEnv, cfg.LogLevel)
|
logger := logging.New("worker", cfg.AppEnv, cfg.LogLevel)
|
||||||
|
tokenCipher, err := encryption.NewFromBase64(cfg.MasterKeyBase64)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("failed to initialize cluster token encryption", "error", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
db, err := openDatabase(cfg.DatabaseURL)
|
db, err := openDatabase(cfg.DatabaseURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Error("failed to connect database", "error", err)
|
logger.Error("failed to connect database", "error", err)
|
||||||
@@ -43,7 +51,8 @@ func main() {
|
|||||||
Concurrency: cfg.WorkerConcurrency,
|
Concurrency: cfg.WorkerConcurrency,
|
||||||
})
|
})
|
||||||
mux := asynq.NewServeMux()
|
mux := asynq.NewServeMux()
|
||||||
registerHandlers(mux, db, logger)
|
clusterRepository := cluster.NewRepository(db, tokenCipher)
|
||||||
|
registerHandlers(mux, db, clusterRepository, logger)
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
logger.Info("worker started", "concurrency", cfg.WorkerConcurrency, "redis_addr", cfg.RedisAddr)
|
logger.Info("worker started", "concurrency", cfg.WorkerConcurrency, "redis_addr", cfg.RedisAddr)
|
||||||
@@ -58,12 +67,12 @@ func main() {
|
|||||||
logger.Info("worker stopped")
|
logger.Info("worker stopped")
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerHandlers(mux *asynq.ServeMux, db *sql.DB, logger *slog.Logger) {
|
func registerHandlers(mux *asynq.ServeMux, db *sql.DB, clusters cluster.Repository, logger *slog.Logger) {
|
||||||
mux.Handle(tasks.TypeDummy, tasks.NewDummyHandler(logger))
|
mux.Handle(tasks.TypeDummy, tasks.NewDummyHandler(logger))
|
||||||
mux.Handle(
|
mux.Handle(
|
||||||
tasks.TypeProxmoxTaskPoll,
|
tasks.TypeProxmoxTaskPoll,
|
||||||
tasks.NewProxmoxTaskPollHandler(
|
tasks.NewProxmoxTaskPollHandler(
|
||||||
tasks.UnconfiguredProxmoxTaskClientResolver{},
|
tasks.NewDefaultClusterTaskClientResolver(clusters),
|
||||||
tasks.NewSQLVMStatusStore(db),
|
tasks.NewSQLVMStatusStore(db),
|
||||||
tasks.NewSQLAuditWriter(db),
|
tasks.NewSQLAuditWriter(db),
|
||||||
logger,
|
logger,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"database/sql"
|
"database/sql"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"log/slog"
|
"log/slog"
|
||||||
"time"
|
"time"
|
||||||
@@ -13,6 +14,8 @@ import (
|
|||||||
|
|
||||||
const TypeProxmoxTaskPoll = "proxmox.task.poll"
|
const TypeProxmoxTaskPoll = "proxmox.task.poll"
|
||||||
|
|
||||||
|
var ErrClusterNotFound = errors.New("cluster not found")
|
||||||
|
|
||||||
type ProxmoxTaskPollPayload struct {
|
type ProxmoxTaskPollPayload struct {
|
||||||
ClusterID string `json:"cluster_id"`
|
ClusterID string `json:"cluster_id"`
|
||||||
Node string `json:"node"`
|
Node string `json:"node"`
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/cluster"
|
||||||
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/proxmox"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClusterRepository interface {
|
||||||
|
GetCluster(ctx context.Context, id string) (cluster.Cluster, bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProxmoxTaskClientFactory func(cluster cluster.Cluster) (PlatformProxmoxTaskClient, error)
|
||||||
|
|
||||||
|
type PlatformProxmoxTaskClient interface {
|
||||||
|
GetTaskStatus(ctx context.Context, node string, upid string) (proxmox.TaskStatus, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClusterTaskClientResolver struct {
|
||||||
|
clusters ClusterRepository
|
||||||
|
factory ProxmoxTaskClientFactory
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClusterTaskClientResolver(clusters ClusterRepository, factory ProxmoxTaskClientFactory) ClusterTaskClientResolver {
|
||||||
|
return ClusterTaskClientResolver{
|
||||||
|
clusters: clusters,
|
||||||
|
factory: factory,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultClusterTaskClientResolver(clusters ClusterRepository) ClusterTaskClientResolver {
|
||||||
|
return NewClusterTaskClientResolver(clusters, func(cluster cluster.Cluster) (PlatformProxmoxTaskClient, error) {
|
||||||
|
return proxmox.NewClient(cluster)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r ClusterTaskClientResolver) ResolveTaskClient(ctx context.Context, clusterID string) (ProxmoxTaskClient, error) {
|
||||||
|
cluster, found, err := r.clusters.GetCluster(ctx, clusterID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if !found {
|
||||||
|
return nil, ErrClusterNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
client, err := r.factory(cluster)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return platformTaskClientAdapter{client: client}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type platformTaskClientAdapter struct {
|
||||||
|
client PlatformProxmoxTaskClient
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a platformTaskClientAdapter) GetTaskStatus(ctx context.Context, node string, upid string) (ProxmoxTaskStatus, error) {
|
||||||
|
status, err := a.client.GetTaskStatus(ctx, node, upid)
|
||||||
|
if err != nil {
|
||||||
|
return ProxmoxTaskStatus{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return ProxmoxTaskStatus{
|
||||||
|
Status: status.Status,
|
||||||
|
ExitStatus: status.ExitStatus,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
package tasks
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/cluster"
|
||||||
|
"forgejo.digital-droplets.de/philschlo/proxui/platform/proxmox"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestClusterTaskClientResolverLoadsClusterAndAdaptsStatus(t *testing.T) {
|
||||||
|
repository := &stubClusterRepository{
|
||||||
|
cluster: cluster.Cluster{
|
||||||
|
ID: "cluster-1",
|
||||||
|
APIEndpoint: "https://pve.example.test:8006",
|
||||||
|
TLSFingerprint: "fingerprint",
|
||||||
|
TokenID: "root@pam!proxui",
|
||||||
|
TokenSecret: "secret-token",
|
||||||
|
},
|
||||||
|
found: true,
|
||||||
|
}
|
||||||
|
factoryClient := &stubPlatformTaskClient{
|
||||||
|
status: proxmox.TaskStatus{Status: "stopped", ExitStatus: "OK"},
|
||||||
|
}
|
||||||
|
resolver := NewClusterTaskClientResolver(repository, func(cluster cluster.Cluster) (PlatformProxmoxTaskClient, error) {
|
||||||
|
if cluster.ID != "cluster-1" {
|
||||||
|
t.Fatalf("cluster ID = %q, want cluster-1", cluster.ID)
|
||||||
|
}
|
||||||
|
return factoryClient, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
client, err := resolver.ResolveTaskClient(context.Background(), "cluster-1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ResolveTaskClient() error = %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status, err := client.GetTaskStatus(context.Background(), "pve", "UPID:pve:1")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("GetTaskStatus() error = %v", err)
|
||||||
|
}
|
||||||
|
if status.Status != "stopped" || status.ExitStatus != "OK" {
|
||||||
|
t.Fatalf("status = %+v, want stopped/OK", status)
|
||||||
|
}
|
||||||
|
if repository.id != "cluster-1" {
|
||||||
|
t.Fatalf("repository id = %q, want cluster-1", repository.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClusterTaskClientResolverReturnsClusterNotFound(t *testing.T) {
|
||||||
|
resolver := NewClusterTaskClientResolver(&stubClusterRepository{}, nil)
|
||||||
|
|
||||||
|
if _, err := resolver.ResolveTaskClient(context.Background(), "missing"); !errors.Is(err, ErrClusterNotFound) {
|
||||||
|
t.Fatalf("ResolveTaskClient() error = %v, want ErrClusterNotFound", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubClusterRepository struct {
|
||||||
|
id string
|
||||||
|
cluster cluster.Cluster
|
||||||
|
found bool
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubClusterRepository) GetCluster(_ context.Context, id string) (cluster.Cluster, bool, error) {
|
||||||
|
s.id = id
|
||||||
|
return s.cluster, s.found, s.err
|
||||||
|
}
|
||||||
|
|
||||||
|
type stubPlatformTaskClient struct {
|
||||||
|
status proxmox.TaskStatus
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *stubPlatformTaskClient) GetTaskStatus(context.Context, string, string) (proxmox.TaskStatus, error) {
|
||||||
|
return s.status, s.err
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user