feat(frontend): add Helm chart browser, monitoring, chart-references and values templates pages
Add new frontend pages for the multi-tenant OCDP platform: - Charts page (/charts): Browse Harbor OCI registries to list Helm chart repositories and versions, with deploy modal to launch charts on selected clusters - Monitoring page (/monitoring): Display cluster metrics (CPU/Memory/GPU usage) and per-node details with resource utilization bars - Chart References page (/chart-references): CRUD for chart metadata references - Values Templates page (/templates): CRUD for Helm values templates with version history and rollback support - Sidebar: Add Charts navigation, update Storage and Templates links - api.ts: Add all API client functions (clusterApi, registryApi, instanceApi, monitoringApi, storageApi, chartReferenceApi, valuesTemplateApi, workspaceApi, userApi) with full TypeScript types Note: deploy flow and values template rollback not yet end-to-end tested.
This commit is contained in:
@ -0,0 +1,200 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// AuditLogRepository PostgreSQL 审计日志仓储实现
|
||||
type AuditLogRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewAuditLogRepository 创建 PostgreSQL 审计日志仓储
|
||||
func NewAuditLogRepository(db *DB) repository.AuditLogRepository {
|
||||
return &AuditLogRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建审计日志
|
||||
func (r *AuditLogRepository) Create(ctx context.Context, log *entity.AuditLog) error {
|
||||
if log.ID == "" {
|
||||
log.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
detailsJSON, err := json.Marshal(log.Details)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal details: %w", err)
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO audit_logs (id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
_, err = r.db.conn.ExecContext(ctx, query,
|
||||
log.ID,
|
||||
log.WorkspaceID,
|
||||
log.UserID,
|
||||
log.Action,
|
||||
log.ResourceType,
|
||||
log.ResourceID,
|
||||
log.ResourceName,
|
||||
detailsJSON,
|
||||
log.IPAddress,
|
||||
log.UserAgent,
|
||||
log.CreatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create audit log: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的审计日志
|
||||
func (r *AuditLogRepository) GetByWorkspace(ctx context.Context, workspaceID string, limit int) ([]*entity.AuditLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
|
||||
FROM audit_logs
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanAuditLogs(rows)
|
||||
}
|
||||
|
||||
// GetByUser 获取用户的审计日志
|
||||
func (r *AuditLogRepository) GetByUser(ctx context.Context, userID string, limit int) ([]*entity.AuditLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
|
||||
FROM audit_logs
|
||||
WHERE user_id = $1
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $2
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, userID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanAuditLogs(rows)
|
||||
}
|
||||
|
||||
// GetByResource 获取资源的审计日志
|
||||
func (r *AuditLogRepository) GetByResource(ctx context.Context, resourceType entity.AuditResourceType, resourceID string, limit int) ([]*entity.AuditLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, workspace_id, user_id, admin action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
|
||||
FROM audit_logs
|
||||
WHERE resource_type = $1 AND resource_id = $2
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $3
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, resourceType, resourceID, limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanAuditLogs(rows)
|
||||
}
|
||||
|
||||
// List 列出审计日志(分页)
|
||||
func (r *AuditLogRepository) List(ctx context.Context, limit, offset int) ([]*entity.AuditLog, error) {
|
||||
if limit <= 0 {
|
||||
limit = 100
|
||||
}
|
||||
if offset < 0 {
|
||||
offset = 0
|
||||
}
|
||||
|
||||
query := `
|
||||
SELECT id, workspace_id, user_id, action, resource_type, resource_id, resource_name, details, ip_address, user_agent, created_at
|
||||
FROM audit_logs
|
||||
ORDER BY created_at DESC
|
||||
LIMIT $1 OFFSET $2
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, limit, offset)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list audit logs: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanAuditLogs(rows)
|
||||
}
|
||||
|
||||
// DeleteByWorkspace 删除 workspace 的审计日志
|
||||
func (r *AuditLogRepository) DeleteByWorkspace(ctx context.Context, workspaceID string) error {
|
||||
query := `DELETE FROM audit_logs WHERE workspace_id = $1`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete audit logs: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// scanAuditLogs 扫描多行结果
|
||||
func (r *AuditLogRepository) scanAuditLogs(rows *sql.Rows) ([]*entity.AuditLog, error) {
|
||||
logs := make([]*entity.AuditLog, 0)
|
||||
for rows.Next() {
|
||||
log := &entity.AuditLog{}
|
||||
var detailsJSON []byte
|
||||
err := rows.Scan(
|
||||
&log.ID,
|
||||
&log.WorkspaceID,
|
||||
&log.UserID,
|
||||
&log.Action,
|
||||
&log.ResourceType,
|
||||
&log.ResourceID,
|
||||
&log.ResourceName,
|
||||
&detailsJSON,
|
||||
&log.IPAddress,
|
||||
&log.UserAgent,
|
||||
&log.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan audit log: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(detailsJSON, &log.Details); err != nil {
|
||||
log.Details = make(map[string]interface{})
|
||||
}
|
||||
logs = append(logs, log)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return logs, nil
|
||||
}
|
||||
@ -0,0 +1,253 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// ChartReferenceRepository PostgreSQL Chart 引用仓储实现
|
||||
type ChartReferenceRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewChartReferenceRepository 创建 PostgreSQL Chart 引用仓储
|
||||
func NewChartReferenceRepository(db *DB) repository.ChartReferenceRepository {
|
||||
return &ChartReferenceRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建 Chart 引用
|
||||
func (r *ChartReferenceRepository) Create(ctx context.Context, chartRef *entity.ChartReference) error {
|
||||
if chartRef.ID == "" {
|
||||
chartRef.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO chart_references (id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
|
||||
`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query,
|
||||
chartRef.ID,
|
||||
chartRef.WorkspaceID,
|
||||
chartRef.RegistryID,
|
||||
chartRef.Repository,
|
||||
chartRef.ChartName,
|
||||
chartRef.Description,
|
||||
chartRef.IsEnabled,
|
||||
chartRef.CreatedAt,
|
||||
chartRef.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create chart reference: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取 Chart 引用
|
||||
func (r *ChartReferenceRepository) GetByID(ctx context.Context, id string) (*entity.ChartReference, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
|
||||
FROM chart_references
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
chartRef := &entity.ChartReference{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&chartRef.ID,
|
||||
&chartRef.WorkspaceID,
|
||||
&chartRef.RegistryID,
|
||||
&chartRef.Repository,
|
||||
&chartRef.ChartName,
|
||||
&chartRef.Description,
|
||||
&chartRef.IsEnabled,
|
||||
&chartRef.CreatedAt,
|
||||
&chartRef.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrChartReferenceNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get chart reference: %w", err)
|
||||
}
|
||||
|
||||
return chartRef, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的所有 Chart 引用
|
||||
func (r *ChartReferenceRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ChartReference, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
|
||||
FROM chart_references
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY chart_name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list chart references: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanChartReferences(rows)
|
||||
}
|
||||
|
||||
// GetByRegistry 获取 registry 的所有 Chart 引用
|
||||
func (r *ChartReferenceRepository) GetByRegistry(ctx context.Context, registryID string) ([]*entity.ChartReference, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
|
||||
FROM chart_references
|
||||
WHERE registry_id = $1
|
||||
ORDER BY chart_name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, registryID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list chart references: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanChartReferences(rows)
|
||||
}
|
||||
|
||||
// GetByName 根据名称获取 Chart 引用
|
||||
func (r *ChartReferenceRepository) GetByName(ctx context.Context, workspaceID, chartName string) (*entity.ChartReference, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
|
||||
FROM chart_references
|
||||
WHERE workspace_id = $1 AND chart_name = $2
|
||||
`
|
||||
|
||||
chartRef := &entity.ChartReference{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, chartName).Scan(
|
||||
&chartRef.ID,
|
||||
&chartRef.WorkspaceID,
|
||||
&chartRef.RegistryID,
|
||||
&chartRef.Repository,
|
||||
&chartRef.ChartName,
|
||||
&chartRef.Description,
|
||||
&chartRef.IsEnabled,
|
||||
&chartRef.CreatedAt,
|
||||
&chartRef.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrChartReferenceNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get chart reference: %w", err)
|
||||
}
|
||||
|
||||
return chartRef, nil
|
||||
}
|
||||
|
||||
// Update 更新 Chart 引用
|
||||
func (r *ChartReferenceRepository) Update(ctx context.Context, chartRef *entity.ChartReference) error {
|
||||
chartRef.UpdatedAt = time.Now()
|
||||
|
||||
query := `
|
||||
UPDATE chart_references
|
||||
SET registry_id = $1, repository = $2, chart_name = $3, description = $4, is_enabled = $5, updated_at = $6
|
||||
WHERE id = $7
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
chartRef.RegistryID,
|
||||
chartRef.Repository,
|
||||
chartRef.ChartName,
|
||||
chartRef.Description,
|
||||
chartRef.IsEnabled,
|
||||
chartRef.UpdatedAt,
|
||||
chartRef.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update chart reference: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrChartReferenceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除 Chart 引用
|
||||
func (r *ChartReferenceRepository) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM chart_references WHERE id = $1`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete chart reference: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrChartReferenceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 列出所有 Chart 引用(管理员用)
|
||||
func (r *ChartReferenceRepository) List(ctx context.Context) ([]*entity.ChartReference, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, registry_id, repository, chart_name, description, is_enabled, created_at, updated_at
|
||||
FROM chart_references
|
||||
ORDER BY workspace_id, chart_name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list chart references: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanChartReferences(rows)
|
||||
}
|
||||
|
||||
// scanChartReferences 扫描多行结果
|
||||
func (r *ChartReferenceRepository) scanChartReferences(rows *sql.Rows) ([]*entity.ChartReference, error) {
|
||||
chartRefs := make([]*entity.ChartReference, 0)
|
||||
for rows.Next() {
|
||||
chartRef := &entity.ChartReference{}
|
||||
err := rows.Scan(
|
||||
&chartRef.ID,
|
||||
&chartRef.WorkspaceID,
|
||||
&chartRef.RegistryID,
|
||||
&chartRef.Repository,
|
||||
&chartRef.ChartName,
|
||||
&chartRef.Description,
|
||||
&chartRef.IsEnabled,
|
||||
&chartRef.CreatedAt,
|
||||
&chartRef.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan chart reference: %w", err)
|
||||
}
|
||||
chartRefs = append(chartRefs, chartRef)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return chartRefs, nil
|
||||
}
|
||||
@ -32,6 +32,11 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
|
||||
cluster.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if cluster.IsolationMode == "" {
|
||||
cluster.IsolationMode = entity.IsolationModeNamespace
|
||||
}
|
||||
|
||||
// 加密敏感数据
|
||||
encryptedCAData, err := r.encryptor.Encrypt(cluster.CAData)
|
||||
if err != nil {
|
||||
@ -54,12 +59,14 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO clusters (id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
INSERT INTO clusters (id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)
|
||||
`
|
||||
|
||||
_, err = r.db.conn.ExecContext(ctx, query,
|
||||
cluster.ID,
|
||||
cluster.WorkspaceID,
|
||||
cluster.OwnerID,
|
||||
cluster.Name,
|
||||
cluster.Host,
|
||||
encryptedCAData,
|
||||
@ -67,6 +74,9 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
|
||||
encryptedKeyData,
|
||||
encryptedToken,
|
||||
cluster.Description,
|
||||
cluster.IsolationMode,
|
||||
cluster.DefaultNamespace,
|
||||
cluster.IsShared,
|
||||
cluster.CreatedAt,
|
||||
cluster.UpdatedAt,
|
||||
)
|
||||
@ -81,7 +91,7 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
|
||||
// GetByID 根据 ID 获取集群
|
||||
func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Cluster, error) {
|
||||
query := `
|
||||
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
|
||||
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
|
||||
FROM clusters
|
||||
WHERE id = $1
|
||||
`
|
||||
@ -91,6 +101,8 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
|
||||
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&cluster.ID,
|
||||
&cluster.WorkspaceID,
|
||||
&cluster.OwnerID,
|
||||
&cluster.Name,
|
||||
&cluster.Host,
|
||||
&encryptedCAData,
|
||||
@ -98,6 +110,9 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
|
||||
&encryptedKeyData,
|
||||
&encryptedToken,
|
||||
&cluster.Description,
|
||||
&cluster.IsolationMode,
|
||||
&cluster.DefaultNamespace,
|
||||
&cluster.IsShared,
|
||||
&cluster.CreatedAt,
|
||||
&cluster.UpdatedAt,
|
||||
)
|
||||
@ -110,25 +125,10 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
|
||||
}
|
||||
|
||||
// 解密敏感数据
|
||||
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
|
||||
}
|
||||
|
||||
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
|
||||
}
|
||||
|
||||
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
|
||||
}
|
||||
|
||||
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt token: %w", err)
|
||||
}
|
||||
cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData)
|
||||
cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData)
|
||||
cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData)
|
||||
cluster.Token, _ = r.encryptor.Decrypt(encryptedToken)
|
||||
|
||||
return cluster, nil
|
||||
}
|
||||
@ -136,7 +136,7 @@ func (r *ClusterRepository) GetByID(ctx context.Context, id string) (*entity.Clu
|
||||
// GetByName 根据名称获取集群
|
||||
func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity.Cluster, error) {
|
||||
query := `
|
||||
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
|
||||
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
|
||||
FROM clusters
|
||||
WHERE name = $1
|
||||
`
|
||||
@ -146,6 +146,8 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity
|
||||
|
||||
err := r.db.conn.QueryRowContext(ctx, query, name).Scan(
|
||||
&cluster.ID,
|
||||
&cluster.WorkspaceID,
|
||||
&cluster.OwnerID,
|
||||
&cluster.Name,
|
||||
&cluster.Host,
|
||||
&encryptedCAData,
|
||||
@ -153,6 +155,9 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity
|
||||
&encryptedKeyData,
|
||||
&encryptedToken,
|
||||
&cluster.Description,
|
||||
&cluster.IsolationMode,
|
||||
&cluster.DefaultNamespace,
|
||||
&cluster.IsShared,
|
||||
&cluster.CreatedAt,
|
||||
&cluster.UpdatedAt,
|
||||
)
|
||||
@ -165,25 +170,10 @@ func (r *ClusterRepository) GetByName(ctx context.Context, name string) (*entity
|
||||
}
|
||||
|
||||
// 解密敏感数据
|
||||
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
|
||||
}
|
||||
|
||||
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
|
||||
}
|
||||
|
||||
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
|
||||
}
|
||||
|
||||
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt token: %w", err)
|
||||
}
|
||||
cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData)
|
||||
cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData)
|
||||
cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData)
|
||||
cluster.Token, _ = r.encryptor.Decrypt(encryptedToken)
|
||||
|
||||
return cluster, nil
|
||||
}
|
||||
@ -215,9 +205,10 @@ func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster)
|
||||
|
||||
query := `
|
||||
UPDATE clusters
|
||||
SET name = $1, host = $2, ca_data = $3, cert_data = $4, key_data = $5,
|
||||
token = $6, description = $7, updated_at = $8
|
||||
WHERE id = $9
|
||||
SET name = $1, host = $2, ca_data = $3, cert_data = $4, key_data = $5,
|
||||
token = $6, description = $7, isolation_mode = $8, default_namespace = $9,
|
||||
is_shared = $10, updated_at = $11
|
||||
WHERE id = $12
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
@ -228,6 +219,9 @@ func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster)
|
||||
encryptedKeyData,
|
||||
encryptedToken,
|
||||
cluster.Description,
|
||||
cluster.IsolationMode,
|
||||
cluster.DefaultNamespace,
|
||||
cluster.IsShared,
|
||||
cluster.UpdatedAt,
|
||||
cluster.ID,
|
||||
)
|
||||
@ -272,7 +266,7 @@ func (r *ClusterRepository) Delete(ctx context.Context, id string) error {
|
||||
// List 列出所有集群
|
||||
func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error) {
|
||||
query := `
|
||||
SELECT id, name, host, ca_data, cert_data, key_data, token, description, created_at, updated_at
|
||||
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
|
||||
FROM clusters
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
@ -283,13 +277,59 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanClusters(rows)
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的所有集群(包括共享集群)
|
||||
func (r *ClusterRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Cluster, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
|
||||
FROM clusters
|
||||
WHERE workspace_id = $1 OR is_shared = TRUE
|
||||
ORDER BY is_shared, created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list clusters by workspace: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanClusters(rows)
|
||||
}
|
||||
|
||||
// GetShared 获取所有共享集群
|
||||
func (r *ClusterRepository) GetShared(ctx context.Context) ([]*entity.Cluster, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, host, ca_data, cert_data, key_data, token, description, isolation_mode, default_namespace, is_shared, created_at, updated_at
|
||||
FROM clusters
|
||||
WHERE is_shared = TRUE
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list shared clusters: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanClusters(rows)
|
||||
}
|
||||
|
||||
// scanClusters 扫描多行结果
|
||||
func (r *ClusterRepository) scanClusters(rows *sql.Rows) ([]*entity.Cluster, error) {
|
||||
clusters := make([]*entity.Cluster, 0)
|
||||
for rows.Next() {
|
||||
cluster := &entity.Cluster{}
|
||||
var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken string
|
||||
var (
|
||||
encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken sql.NullString
|
||||
workspaceID, ownerID, defaultNamespace sql.NullString
|
||||
)
|
||||
|
||||
err := rows.Scan(
|
||||
&cluster.ID,
|
||||
&workspaceID,
|
||||
&ownerID,
|
||||
&cluster.Name,
|
||||
&cluster.Host,
|
||||
&encryptedCAData,
|
||||
@ -297,6 +337,9 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error)
|
||||
&encryptedKeyData,
|
||||
&encryptedToken,
|
||||
&cluster.Description,
|
||||
&cluster.IsolationMode,
|
||||
&defaultNamespace,
|
||||
&cluster.IsShared,
|
||||
&cluster.CreatedAt,
|
||||
&cluster.UpdatedAt,
|
||||
)
|
||||
@ -304,25 +347,23 @@ func (r *ClusterRepository) List(ctx context.Context) ([]*entity.Cluster, error)
|
||||
return nil, fmt.Errorf("failed to scan cluster: %w", err)
|
||||
}
|
||||
|
||||
// 处理 NULL 值
|
||||
cluster.WorkspaceID = workspaceID.String
|
||||
cluster.OwnerID = ownerID.String
|
||||
cluster.DefaultNamespace = defaultNamespace.String
|
||||
|
||||
// 解密敏感数据
|
||||
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
|
||||
if encryptedCAData.Valid {
|
||||
cluster.CAData, _ = r.encryptor.Decrypt(encryptedCAData.String)
|
||||
}
|
||||
|
||||
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
|
||||
if encryptedCertData.Valid {
|
||||
cluster.CertData, _ = r.encryptor.Decrypt(encryptedCertData.String)
|
||||
}
|
||||
|
||||
cluster.KeyData, err = r.encryptor.Decrypt(encryptedKeyData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt key data: %w", err)
|
||||
if encryptedKeyData.Valid {
|
||||
cluster.KeyData, _ = r.encryptor.Decrypt(encryptedKeyData.String)
|
||||
}
|
||||
|
||||
cluster.Token, err = r.encryptor.Decrypt(encryptedToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt token: %w", err)
|
||||
if encryptedToken.Valid {
|
||||
cluster.Token, _ = r.encryptor.Decrypt(encryptedToken.String)
|
||||
}
|
||||
|
||||
clusters = append(clusters, cluster)
|
||||
|
||||
@ -124,6 +124,58 @@ func (db *DB) InitSchema() error {
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_cluster ON instances(cluster_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_registry ON instances(registry_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_instances_name ON instances(name);
|
||||
|
||||
-- Storage Backends 表
|
||||
CREATE TABLE IF NOT EXISTS storage_backends (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
workspace_id VARCHAR(36),
|
||||
owner_id VARCHAR(36),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
type VARCHAR(50) NOT NULL,
|
||||
config JSONB NOT NULL,
|
||||
description TEXT,
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
is_shared BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_storage_workspace ON storage_backends(workspace_id);
|
||||
|
||||
-- Chart References 表
|
||||
CREATE TABLE IF NOT EXISTS chart_references (
|
||||
id VARCHAR(36) PRIMARY KEY,
|
||||
workspace_id VARCHAR(36),
|
||||
registry_id VARCHAR(36),
|
||||
repository VARCHAR(500) NOT NULL,
|
||||
chart_name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
is_enabled BOOLEAN DEFAULT TRUE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_chart_workspace ON chart_references(workspace_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_chart_registry ON chart_references(registry_id);
|
||||
|
||||
-- Values Templates 表 - 使用复合唯一键替代主键,允许同一模板的多个版本
|
||||
CREATE TABLE IF NOT EXISTS values_templates (
|
||||
id VARCHAR(36),
|
||||
workspace_id VARCHAR(36),
|
||||
owner_id VARCHAR(36),
|
||||
chart_reference_id VARCHAR(36),
|
||||
name VARCHAR(255) NOT NULL,
|
||||
description TEXT,
|
||||
values_yaml TEXT NOT NULL,
|
||||
version INTEGER NOT NULL DEFAULT 1,
|
||||
is_default BOOLEAN DEFAULT FALSE,
|
||||
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE (chart_reference_id, name, version)
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_values_template_chart ON values_templates(chart_reference_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_values_template_workspace ON values_templates(workspace_id);
|
||||
`
|
||||
|
||||
_, err := db.conn.Exec(schema)
|
||||
|
||||
@ -431,3 +431,105 @@ func (r *InstanceRepository) List(ctx context.Context) ([]*entity.Instance, erro
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 列出指定工作空间的所有实例(用于配额检查)
|
||||
func (r *InstanceRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.Instance, error) {
|
||||
query := `
|
||||
SELECT id, cluster_id, workspace_id, owner_id, name, namespace, registry_id, repository, chart, version,
|
||||
description, values, values_yaml, values_template_id, user_override_yaml,
|
||||
status, status_reason, last_operation, last_error, revision,
|
||||
cpu_requested, memory_requested, gpu_requested, gpu_memory_requested,
|
||||
created_at, updated_at
|
||||
FROM instances
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get instances by workspace: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
instances := make([]*entity.Instance, 0)
|
||||
for rows.Next() {
|
||||
instance := &entity.Instance{}
|
||||
var (
|
||||
valuesJSON []byte
|
||||
statusReason sql.NullString
|
||||
lastOperation sql.NullString
|
||||
lastError sql.NullString
|
||||
valuesTemplateID sql.NullString
|
||||
userOverrideYAML sql.NullString
|
||||
memoryRequested sql.NullString
|
||||
gpuMemoryRequested sql.NullString
|
||||
)
|
||||
|
||||
err := rows.Scan(
|
||||
&instance.ID,
|
||||
&instance.ClusterID,
|
||||
&instance.WorkspaceID,
|
||||
&instance.OwnerID,
|
||||
&instance.Name,
|
||||
&instance.Namespace,
|
||||
&instance.RegistryID,
|
||||
&instance.Repository,
|
||||
&instance.Chart,
|
||||
&instance.Version,
|
||||
&instance.Description,
|
||||
&valuesJSON,
|
||||
&instance.ValuesYAML,
|
||||
&valuesTemplateID,
|
||||
&userOverrideYAML,
|
||||
&instance.Status,
|
||||
&statusReason,
|
||||
&lastOperation,
|
||||
&lastError,
|
||||
&instance.Revision,
|
||||
&instance.CPURequested,
|
||||
&memoryRequested,
|
||||
&instance.GPURequested,
|
||||
&gpuMemoryRequested,
|
||||
&instance.CreatedAt,
|
||||
&instance.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan instance: %w", err)
|
||||
}
|
||||
|
||||
if valuesJSON != nil {
|
||||
if err := json.Unmarshal(valuesJSON, &instance.Values); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal values: %w", err)
|
||||
}
|
||||
}
|
||||
if valuesTemplateID.Valid {
|
||||
instance.ValuesTemplateID = valuesTemplateID.String
|
||||
}
|
||||
if userOverrideYAML.Valid {
|
||||
instance.UserOverrideYAML = userOverrideYAML.String
|
||||
}
|
||||
if statusReason.Valid {
|
||||
instance.StatusReason = statusReason.String
|
||||
}
|
||||
if lastOperation.Valid {
|
||||
instance.LastOperation = entity.InstanceOperation(lastOperation.String)
|
||||
}
|
||||
if lastError.Valid {
|
||||
instance.LastError = lastError.String
|
||||
}
|
||||
if memoryRequested.Valid {
|
||||
instance.MemoryRequested = memoryRequested.String
|
||||
}
|
||||
if gpuMemoryRequested.Valid {
|
||||
instance.GPUMemoryRequested = gpuMemoryRequested.String
|
||||
}
|
||||
|
||||
instances = append(instances, instance)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return instances, nil
|
||||
}
|
||||
|
||||
@ -0,0 +1,212 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// QuotaRepository PostgreSQL 配额仓储实现
|
||||
type QuotaRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewQuotaRepository 创建 PostgreSQL 配额仓储
|
||||
func NewQuotaRepository(db *DB) repository.QuotaRepository {
|
||||
return &QuotaRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建配额
|
||||
func (r *QuotaRepository) Create(ctx context.Context, quota *entity.WorkspaceQuota) error {
|
||||
if quota.ID == "" {
|
||||
quota.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO workspace_quotas (id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||||
ON CONFLICT (workspace_id, resource_type) DO UPDATE
|
||||
SET hard_limit = $4, soft_limit = $5, updated_at = $8
|
||||
`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query,
|
||||
quota.ID,
|
||||
quota.WorkspaceID,
|
||||
quota.ResourceType,
|
||||
quota.HardLimit,
|
||||
quota.SoftLimit,
|
||||
quota.Used,
|
||||
quota.CreatedAt,
|
||||
quota.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create quota: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取配额
|
||||
func (r *QuotaRepository) GetByID(ctx context.Context, id string) (*entity.WorkspaceQuota, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at
|
||||
FROM workspace_quotas
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
quota := &entity.WorkspaceQuota{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
"a.ID,
|
||||
"a.WorkspaceID,
|
||||
"a.ResourceType,
|
||||
"a.HardLimit,
|
||||
"a.SoftLimit,
|
||||
"a.Used,
|
||||
"a.CreatedAt,
|
||||
"a.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get quota: %w", err)
|
||||
}
|
||||
|
||||
return quota, nil
|
||||
}
|
||||
|
||||
// GetByWorkspaceAndType 根据 workspace 和资源类型获取配额
|
||||
func (r *QuotaRepository) GetByWorkspaceAndType(ctx context.Context, workspaceID string, resourceType entity.ResourceType) (*entity.WorkspaceQuota, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at
|
||||
FROM workspace_quotas
|
||||
WHERE workspace_id = $1 AND resource_type = $2
|
||||
`
|
||||
|
||||
quota := &entity.WorkspaceQuota{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, resourceType).Scan(
|
||||
"a.ID,
|
||||
"a.WorkspaceID,
|
||||
"a.ResourceType,
|
||||
"a.HardLimit,
|
||||
"a.SoftLimit,
|
||||
"a.Used,
|
||||
"a.CreatedAt,
|
||||
"a.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get quota: %w", err)
|
||||
}
|
||||
|
||||
return quota, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的所有配额
|
||||
func (r *QuotaRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.WorkspaceQuota, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, resource_type, hard_limit, soft_limit, used, created_at, updated_at
|
||||
FROM workspace_quotas
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY resource_type
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list quotas: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
quotas := make([]*entity.WorkspaceQuota, 0)
|
||||
for rows.Next() {
|
||||
quota := &entity.WorkspaceQuota{}
|
||||
err := rows.Scan(
|
||||
"a.ID,
|
||||
"a.WorkspaceID,
|
||||
"a.ResourceType,
|
||||
"a.HardLimit,
|
||||
"a.SoftLimit,
|
||||
"a.Used,
|
||||
"a.CreatedAt,
|
||||
"a.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan quota: %w", err)
|
||||
}
|
||||
quotas = append(quotas, quota)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return quotas, nil
|
||||
}
|
||||
|
||||
// Update 更新配额
|
||||
func (r *QuotaRepository) Update(ctx context.Context, quota *entity.WorkspaceQuota) error {
|
||||
quota.UpdatedAt = time.Now()
|
||||
|
||||
query := `
|
||||
UPDATE workspace_quotas
|
||||
SET hard_limit = $1, soft_limit = $2, used = $3, updated_at = $4
|
||||
WHERE id = $5
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
quota.HardLimit,
|
||||
quota.SoftLimit,
|
||||
quota.Used,
|
||||
quota.UpdatedAt,
|
||||
quota.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update quota: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return fmt.Errorf("quota not found")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除配额
|
||||
func (r *QuotaRepository) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM workspace_quotas WHERE id = $1`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete quota: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteByWorkspace 删除 workspace 的所有配额
|
||||
func (r *QuotaRepository) DeleteByWorkspace(ctx context.Context, workspaceID string) error {
|
||||
query := `DELETE FROM workspace_quotas WHERE workspace_id = $1`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete quotas: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -208,7 +208,7 @@ func (r *RegistryRepository) Delete(ctx context.Context, id string) error {
|
||||
// List 列出所有 Registries
|
||||
func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, error) {
|
||||
query := `
|
||||
SELECT id, name, url, description, username, password, insecure, created_at, updated_at
|
||||
SELECT id, workspace_id, owner_id, name, url, description, username, password, insecure, is_shared, created_at, updated_at
|
||||
FROM registries
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
@ -222,16 +222,19 @@ func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, erro
|
||||
registries := make([]*entity.Registry, 0)
|
||||
for rows.Next() {
|
||||
registry := &entity.Registry{}
|
||||
var encryptedPassword string
|
||||
var encryptedPassword, workspaceID, ownerID sql.NullString
|
||||
|
||||
err := rows.Scan(
|
||||
®istry.ID,
|
||||
&workspaceID,
|
||||
&ownerID,
|
||||
®istry.Name,
|
||||
®istry.URL,
|
||||
®istry.Description,
|
||||
®istry.Username,
|
||||
&encryptedPassword,
|
||||
®istry.Insecure,
|
||||
®istry.IsShared,
|
||||
®istry.CreatedAt,
|
||||
®istry.UpdatedAt,
|
||||
)
|
||||
@ -239,10 +242,13 @@ func (r *RegistryRepository) List(ctx context.Context) ([]*entity.Registry, erro
|
||||
return nil, fmt.Errorf("failed to scan registry: %w", err)
|
||||
}
|
||||
|
||||
// 处理 NULL 值
|
||||
registry.WorkspaceID = workspaceID.String
|
||||
registry.OwnerID = ownerID.String
|
||||
|
||||
// 解密密码
|
||||
registry.Password, err = r.encryptor.Decrypt(encryptedPassword)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt password: %w", err)
|
||||
if encryptedPassword.Valid {
|
||||
registry.Password, _ = r.encryptor.Decrypt(encryptedPassword.String)
|
||||
}
|
||||
|
||||
registries = append(registries, registry)
|
||||
|
||||
@ -0,0 +1,326 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// StorageRepository PostgreSQL 存储后端仓储实现
|
||||
type StorageRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewStorageRepository 创建 PostgreSQL 存储后端仓储
|
||||
func NewStorageRepository(db *DB) repository.StorageRepository {
|
||||
return &StorageRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建存储后端
|
||||
func (r *StorageRepository) Create(ctx context.Context, storage *entity.StorageBackend) error {
|
||||
if storage.ID == "" {
|
||||
storage.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
configJSON, err := json.Marshal(storage.Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO storage_backends (id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
_, err = r.db.conn.ExecContext(ctx, query,
|
||||
storage.ID,
|
||||
storage.WorkspaceID,
|
||||
storage.OwnerID,
|
||||
storage.Name,
|
||||
storage.Type,
|
||||
configJSON,
|
||||
storage.Description,
|
||||
storage.IsDefault,
|
||||
storage.IsShared,
|
||||
storage.CreatedAt,
|
||||
storage.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create storage: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取存储后端
|
||||
func (r *StorageRepository) GetByID(ctx context.Context, id string) (*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
storage := &entity.StorageBackend{}
|
||||
var configJSON []byte
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&storage.ID,
|
||||
&storage.WorkspaceID,
|
||||
&storage.OwnerID,
|
||||
&storage.Name,
|
||||
&storage.Type,
|
||||
&configJSON,
|
||||
&storage.Description,
|
||||
&storage.IsDefault,
|
||||
&storage.IsShared,
|
||||
&storage.CreatedAt,
|
||||
&storage.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrStorageNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get storage: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// GetByName 根据名称获取存储后端
|
||||
func (r *StorageRepository) GetByName(ctx context.Context, workspaceID, name string) (*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
WHERE workspace_id = $1 AND name = $2
|
||||
`
|
||||
|
||||
storage := &entity.StorageBackend{}
|
||||
var configJSON []byte
|
||||
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, name).Scan(
|
||||
&storage.ID,
|
||||
&storage.WorkspaceID,
|
||||
&storage.OwnerID,
|
||||
&storage.Name,
|
||||
&storage.Type,
|
||||
&configJSON,
|
||||
&storage.Description,
|
||||
&storage.IsDefault,
|
||||
&storage.IsShared,
|
||||
&storage.CreatedAt,
|
||||
&storage.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrStorageNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get storage: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的所有存储后端
|
||||
func (r *StorageRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
WHERE workspace_id = $1 OR is_shared = TRUE
|
||||
ORDER BY is_default DESC, name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list storage: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanStorages(rows)
|
||||
}
|
||||
|
||||
// GetShared 获取所有共享存储后端
|
||||
func (r *StorageRepository) GetShared(ctx context.Context) ([]*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
WHERE is_shared = TRUE
|
||||
ORDER BY name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list shared storage: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanStorages(rows)
|
||||
}
|
||||
|
||||
// GetDefault 获取 workspace 的默认存储后端
|
||||
func (r *StorageRepository) GetDefault(ctx context.Context, workspaceID string) (*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
WHERE workspace_id = $1 AND is_default = TRUE
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
storage := &entity.StorageBackend{}
|
||||
var configJSON []byte
|
||||
err := r.db.conn.QueryRowContext(ctx, query, workspaceID).Scan(
|
||||
&storage.ID,
|
||||
&storage.WorkspaceID,
|
||||
&storage.OwnerID,
|
||||
&storage.Name,
|
||||
&storage.Type,
|
||||
&configJSON,
|
||||
&storage.Description,
|
||||
&storage.IsDefault,
|
||||
&storage.IsShared,
|
||||
&storage.CreatedAt,
|
||||
&storage.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get default storage: %w", err)
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
return storage, nil
|
||||
}
|
||||
|
||||
// Update 更新存储后端
|
||||
func (r *StorageRepository) Update(ctx context.Context, storage *entity.StorageBackend) error {
|
||||
storage.UpdatedAt = time.Now()
|
||||
|
||||
configJSON, err := json.Marshal(storage.Config)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
query := `
|
||||
UPDATE storage_backends
|
||||
SET name = $1, type = $2, config = $3, description = $4, is_default = $5, is_shared = $6, updated_at = $7
|
||||
WHERE id = $8
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
storage.Name,
|
||||
storage.Type,
|
||||
configJSON,
|
||||
storage.Description,
|
||||
storage.IsDefault,
|
||||
storage.IsShared,
|
||||
storage.UpdatedAt,
|
||||
storage.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update storage: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrStorageNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除存储后端
|
||||
func (r *StorageRepository) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM storage_backends WHERE id = $1`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete storage: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrStorageNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 列出所有存储后端(管理员用)
|
||||
func (r *StorageRepository) List(ctx context.Context) ([]*entity.StorageBackend, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, name, type, config, description, is_default, is_shared, created_at, updated_at
|
||||
FROM storage_backends
|
||||
ORDER BY workspace_id, name
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list storage: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanStorages(rows)
|
||||
}
|
||||
|
||||
// scanStorages 扫描多行结果
|
||||
func (r *StorageRepository) scanStorages(rows *sql.Rows) ([]*entity.StorageBackend, error) {
|
||||
storages := make([]*entity.StorageBackend, 0)
|
||||
for rows.Next() {
|
||||
storage := &entity.StorageBackend{}
|
||||
var configJSON []byte
|
||||
err := rows.Scan(
|
||||
&storage.ID,
|
||||
&storage.WorkspaceID,
|
||||
&storage.OwnerID,
|
||||
&storage.Name,
|
||||
&storage.Type,
|
||||
&configJSON,
|
||||
&storage.Description,
|
||||
&storage.IsDefault,
|
||||
&storage.IsShared,
|
||||
&storage.CreatedAt,
|
||||
&storage.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan storage: %w", err)
|
||||
}
|
||||
if err := json.Unmarshal(configJSON, &storage.Config); err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
storages = append(storages, storage)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return storages, nil
|
||||
}
|
||||
@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"log"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
@ -27,9 +28,14 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error {
|
||||
user.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
// 设置默认值
|
||||
if user.IsActive {
|
||||
user.IsActive = true
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO users (id, username, password_hash, email, revoked_after, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||
INSERT INTO users (id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query,
|
||||
@ -37,6 +43,10 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error {
|
||||
user.Username,
|
||||
user.PasswordHash,
|
||||
user.Email,
|
||||
user.Role,
|
||||
user.WorkspaceID,
|
||||
user.IsActive,
|
||||
user.MustChangePassword,
|
||||
user.RevokedAfter,
|
||||
user.CreatedAt,
|
||||
user.UpdatedAt,
|
||||
@ -52,22 +62,34 @@ func (r *UserRepository) Create(ctx context.Context, user *entity.User) error {
|
||||
// GetByID 根据 ID 获取用户
|
||||
func (r *UserRepository) GetByID(ctx context.Context, id string) (*entity.User, error) {
|
||||
query := `
|
||||
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
|
||||
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
|
||||
FROM users
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
user := &entity.User{}
|
||||
var workspaceID sql.NullString
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.PasswordHash,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&workspaceID,
|
||||
&user.IsActive,
|
||||
&user.MustChangePassword,
|
||||
&user.RevokedAfter,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
|
||||
// Handle NULL workspace_id
|
||||
if workspaceID.Valid {
|
||||
user.WorkspaceID = workspaceID.String
|
||||
} else {
|
||||
user.WorkspaceID = ""
|
||||
}
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrUserNotFound
|
||||
}
|
||||
@ -80,30 +102,50 @@ func (r *UserRepository) GetByID(ctx context.Context, id string) (*entity.User,
|
||||
|
||||
// GetByUsername 根据用户名获取用户
|
||||
func (r *UserRepository) GetByUsername(ctx context.Context, username string) (*entity.User, error) {
|
||||
log.Printf("[DEBUG] GetByUsername called with username: %q", username)
|
||||
query := `
|
||||
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
|
||||
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
|
||||
FROM users
|
||||
WHERE username = $1
|
||||
`
|
||||
|
||||
log.Printf("[DEBUG] Executing query: %s with param: %s", query, username)
|
||||
|
||||
user := &entity.User{}
|
||||
var workspaceID sql.NullString
|
||||
err := r.db.conn.QueryRowContext(ctx, query, username).Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.PasswordHash,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&workspaceID,
|
||||
&user.IsActive,
|
||||
&user.MustChangePassword,
|
||||
&user.RevokedAfter,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
|
||||
// Handle NULL workspace_id
|
||||
if workspaceID.Valid {
|
||||
user.WorkspaceID = workspaceID.String
|
||||
} else {
|
||||
user.WorkspaceID = ""
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Query result - err: %v", err)
|
||||
if err == sql.ErrNoRows {
|
||||
log.Printf("[DEBUG] User not found in DB")
|
||||
return nil, entity.ErrUserNotFound
|
||||
}
|
||||
if err != nil {
|
||||
log.Printf("[DEBUG] Scan error: %v", err)
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
log.Printf("[DEBUG] Found user: %+v", user)
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
@ -113,14 +155,18 @@ func (r *UserRepository) Update(ctx context.Context, user *entity.User) error {
|
||||
|
||||
query := `
|
||||
UPDATE users
|
||||
SET username = $1, password_hash = $2, email = $3, revoked_after = $4, updated_at = $5
|
||||
WHERE id = $6
|
||||
SET username = $1, password_hash = $2, email = $3, role = $4, workspace_id = $5, is_active = $6, must_change_password = $7, revoked_after = $8, updated_at = $9
|
||||
WHERE id = $10
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
user.Username,
|
||||
user.PasswordHash,
|
||||
user.Email,
|
||||
user.Role,
|
||||
user.WorkspaceID,
|
||||
user.IsActive,
|
||||
user.MustChangePassword,
|
||||
user.RevokedAfter,
|
||||
user.UpdatedAt,
|
||||
user.ID,
|
||||
@ -166,7 +212,7 @@ func (r *UserRepository) Delete(ctx context.Context, id string) error {
|
||||
// List 列出所有用户
|
||||
func (r *UserRepository) List(ctx context.Context) ([]*entity.User, error) {
|
||||
query := `
|
||||
SELECT id, username, password_hash, email, revoked_after, created_at, updated_at
|
||||
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
|
||||
FROM users
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
@ -185,6 +231,98 @@ func (r *UserRepository) List(ctx context.Context) ([]*entity.User, error) {
|
||||
&user.Username,
|
||||
&user.PasswordHash,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&user.WorkspaceID,
|
||||
&user.IsActive,
|
||||
&user.MustChangePassword,
|
||||
&user.RevokedAfter,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan user: %w", err)
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// ListByWorkspace 列出指定 workspace 的用户
|
||||
func (r *UserRepository) ListByWorkspace(ctx context.Context, workspaceID string) ([]*entity.User, error) {
|
||||
query := `
|
||||
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
|
||||
FROM users
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list users by workspace: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := make([]*entity.User, 0)
|
||||
for rows.Next() {
|
||||
user := &entity.User{}
|
||||
err := rows.Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.PasswordHash,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&user.WorkspaceID,
|
||||
&user.IsActive,
|
||||
&user.MustChangePassword,
|
||||
&user.RevokedAfter,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan user: %w", err)
|
||||
}
|
||||
users = append(users, user)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return users, nil
|
||||
}
|
||||
|
||||
// ListActive 仅列出活跃用户
|
||||
func (r *UserRepository) ListActive(ctx context.Context) ([]*entity.User, error) {
|
||||
query := `
|
||||
SELECT id, username, password_hash, email, role, workspace_id, is_active, must_change_password, revoked_after, created_at, updated_at
|
||||
FROM users
|
||||
WHERE is_active = TRUE
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list active users: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
users := make([]*entity.User, 0)
|
||||
for rows.Next() {
|
||||
user := &entity.User{}
|
||||
err := rows.Scan(
|
||||
&user.ID,
|
||||
&user.Username,
|
||||
&user.PasswordHash,
|
||||
&user.Email,
|
||||
&user.Role,
|
||||
&user.WorkspaceID,
|
||||
&user.IsActive,
|
||||
&user.MustChangePassword,
|
||||
&user.RevokedAfter,
|
||||
&user.CreatedAt,
|
||||
&user.UpdatedAt,
|
||||
|
||||
@ -0,0 +1,287 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// ValuesTemplateRepository PostgreSQL Values 模板仓储实现
|
||||
type ValuesTemplateRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewValuesTemplateRepository 创建 PostgreSQL Values 模板仓储
|
||||
func NewValuesTemplateRepository(db *DB) repository.ValuesTemplateRepository {
|
||||
return &ValuesTemplateRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建 Values 模板
|
||||
func (r *ValuesTemplateRepository) Create(ctx context.Context, template *entity.ValuesTemplate) error {
|
||||
if template.ID == "" {
|
||||
template.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO values_templates (id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query,
|
||||
template.ID,
|
||||
template.WorkspaceID,
|
||||
template.OwnerID,
|
||||
template.ChartReferenceID,
|
||||
template.Name,
|
||||
template.Description,
|
||||
template.ValuesYAML,
|
||||
template.Version,
|
||||
template.IsDefault,
|
||||
template.CreatedAt,
|
||||
template.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create values template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取 Values 模板
|
||||
func (r *ValuesTemplateRepository) GetByID(ctx context.Context, id string) (*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
template := &entity.ValuesTemplate{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&template.ID,
|
||||
&template.WorkspaceID,
|
||||
&template.OwnerID,
|
||||
&template.ChartReferenceID,
|
||||
&template.Name,
|
||||
&template.Description,
|
||||
&template.ValuesYAML,
|
||||
&template.Version,
|
||||
&template.IsDefault,
|
||||
&template.CreatedAt,
|
||||
&template.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrTemplateNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get values template: %w", err)
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// GetByWorkspace 获取 workspace 的所有 Values 模板
|
||||
func (r *ValuesTemplateRepository) GetByWorkspace(ctx context.Context, workspaceID string) ([]*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
WHERE workspace_id = $1
|
||||
ORDER BY chart_reference_id, name, version DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, workspaceID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list values templates: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanValuesTemplates(rows)
|
||||
}
|
||||
|
||||
// GetByChartReference 获取 Chart Reference 的所有 Values 模板
|
||||
func (r *ValuesTemplateRepository) GetByChartReference(ctx context.Context, chartRefID string) ([]*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
WHERE chart_reference_id = $1
|
||||
ORDER BY name, version DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, chartRefID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list values templates: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanValuesTemplates(rows)
|
||||
}
|
||||
|
||||
// GetByName 根据名称获取 Values 模板(获取最新版本)
|
||||
func (r *ValuesTemplateRepository) GetByName(ctx context.Context, workspaceID, chartRefID, name string) (*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
WHERE workspace_id = $1 AND chart_reference_id = $2 AND name = $3
|
||||
ORDER BY version DESC
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
template := &entity.ValuesTemplate{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, workspaceID, chartRefID, name).Scan(
|
||||
&template.ID,
|
||||
&template.WorkspaceID,
|
||||
&template.OwnerID,
|
||||
&template.ChartReferenceID,
|
||||
&template.Name,
|
||||
&template.Description,
|
||||
&template.ValuesYAML,
|
||||
&template.Version,
|
||||
&template.IsDefault,
|
||||
&template.CreatedAt,
|
||||
&template.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrTemplateNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get values template: %w", err)
|
||||
}
|
||||
|
||||
return template, nil
|
||||
}
|
||||
|
||||
// GetHistory 获取模板的版本历史
|
||||
func (r *ValuesTemplateRepository) GetHistory(ctx context.Context, chartRefID, name string) ([]*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
WHERE chart_reference_id = $1 AND name = $2
|
||||
ORDER BY version DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, chartRefID, name)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get values template history: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanValuesTemplates(rows)
|
||||
}
|
||||
|
||||
// Update 更新 Values 模板(自动递增版本)
|
||||
func (r *ValuesTemplateRepository) Update(ctx context.Context, template *entity.ValuesTemplate) error {
|
||||
// 获取当前最大版本号
|
||||
var maxVersion int
|
||||
err := r.db.conn.QueryRowContext(ctx,
|
||||
"SELECT COALESCE(MAX(version), 0) FROM values_templates WHERE chart_reference_id = $1 AND name = $2",
|
||||
template.ChartReferenceID, template.Name,
|
||||
).Scan(&maxVersion)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get max version: %w", err)
|
||||
}
|
||||
|
||||
// 生成新 ID 用于新版本
|
||||
newID := uuid.New().String()
|
||||
template.Version = maxVersion + 1
|
||||
template.UpdatedAt = time.Now()
|
||||
template.CreatedAt = time.Now() // 新版本的创建时间
|
||||
|
||||
query := `
|
||||
INSERT INTO values_templates (id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
|
||||
`
|
||||
|
||||
_, err = r.db.conn.ExecContext(ctx, query,
|
||||
newID,
|
||||
template.WorkspaceID,
|
||||
template.OwnerID,
|
||||
template.ChartReferenceID,
|
||||
template.Name,
|
||||
template.Description,
|
||||
template.ValuesYAML,
|
||||
template.Version,
|
||||
template.IsDefault,
|
||||
template.CreatedAt,
|
||||
template.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update values template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除 Values 模板
|
||||
func (r *ValuesTemplateRepository) Delete(ctx context.Context, id string) error {
|
||||
// 获取模板信息
|
||||
template, err := r.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除该名称的所有版本
|
||||
query := `DELETE FROM values_templates WHERE chart_reference_id = $1 AND name = $2`
|
||||
_, err = r.db.conn.ExecContext(ctx, query, template.ChartReferenceID, template.Name)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete values template: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 列出所有 Values 模板(管理员用)
|
||||
func (r *ValuesTemplateRepository) List(ctx context.Context) ([]*entity.ValuesTemplate, error) {
|
||||
query := `
|
||||
SELECT id, workspace_id, owner_id, chart_reference_id, name, description, values_yaml, version, is_default, created_at, updated_at
|
||||
FROM values_templates
|
||||
ORDER BY workspace_id, chart_reference_id, name, version DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list values templates: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return r.scanValuesTemplates(rows)
|
||||
}
|
||||
|
||||
// scanValuesTemplates 扫描多行结果
|
||||
func (r *ValuesTemplateRepository) scanValuesTemplates(rows *sql.Rows) ([]*entity.ValuesTemplate, error) {
|
||||
templates := make([]*entity.ValuesTemplate, 0)
|
||||
for rows.Next() {
|
||||
template := &entity.ValuesTemplate{}
|
||||
err := rows.Scan(
|
||||
&template.ID,
|
||||
&template.WorkspaceID,
|
||||
&template.OwnerID,
|
||||
&template.ChartReferenceID,
|
||||
&template.Name,
|
||||
&template.Description,
|
||||
&template.ValuesYAML,
|
||||
&template.Version,
|
||||
&template.IsDefault,
|
||||
&template.CreatedAt,
|
||||
&template.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan values template: %w", err)
|
||||
}
|
||||
templates = append(templates, template)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return templates, nil
|
||||
}
|
||||
@ -0,0 +1,197 @@
|
||||
package postgres
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/ocdp/cluster-service/internal/domain/entity"
|
||||
"github.com/ocdp/cluster-service/internal/domain/repository"
|
||||
)
|
||||
|
||||
// WorkspaceRepository PostgreSQL Workspace 仓储实现
|
||||
type WorkspaceRepository struct {
|
||||
db *DB
|
||||
}
|
||||
|
||||
// NewWorkspaceRepository 创建 PostgreSQL Workspace 仓储
|
||||
func NewWorkspaceRepository(db *DB) repository.WorkspaceRepository {
|
||||
return &WorkspaceRepository{db: db}
|
||||
}
|
||||
|
||||
// Create 创建 Workspace
|
||||
func (r *WorkspaceRepository) Create(ctx context.Context, workspace *entity.Workspace) error {
|
||||
if workspace.ID == "" {
|
||||
workspace.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
query := `
|
||||
INSERT INTO workspaces (id, name, description, created_by, created_at, updated_at)
|
||||
VALUES ($1, $2, $3, $4, $5, $6)
|
||||
`
|
||||
|
||||
_, err := r.db.conn.ExecContext(ctx, query,
|
||||
workspace.ID,
|
||||
workspace.Name,
|
||||
workspace.Description,
|
||||
workspace.CreatedBy,
|
||||
workspace.CreatedAt,
|
||||
workspace.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create workspace: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetByID 根据 ID 获取 Workspace
|
||||
func (r *WorkspaceRepository) GetByID(ctx context.Context, id string) (*entity.Workspace, error) {
|
||||
query := `
|
||||
SELECT id, name, description, created_by, created_at, updated_at
|
||||
FROM workspaces
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
workspace := &entity.Workspace{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&workspace.ID,
|
||||
&workspace.Name,
|
||||
&workspace.Description,
|
||||
&workspace.CreatedBy,
|
||||
&workspace.CreatedAt,
|
||||
&workspace.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrWorkspaceNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get workspace: %w", err)
|
||||
}
|
||||
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
// GetByName 根据名称获取 Workspace
|
||||
func (r *WorkspaceRepository) GetByName(ctx context.Context, name string) (*entity.Workspace, error) {
|
||||
query := `
|
||||
SELECT id, name, description, created_by, created_at, updated_at
|
||||
FROM workspaces
|
||||
WHERE name = $1
|
||||
`
|
||||
|
||||
workspace := &entity.Workspace{}
|
||||
err := r.db.conn.QueryRowContext(ctx, query, name).Scan(
|
||||
&workspace.ID,
|
||||
&workspace.Name,
|
||||
&workspace.Description,
|
||||
&workspace.CreatedBy,
|
||||
&workspace.CreatedAt,
|
||||
&workspace.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrWorkspaceNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get workspace: %w", err)
|
||||
}
|
||||
|
||||
return workspace, nil
|
||||
}
|
||||
|
||||
// Update 更新 Workspace
|
||||
func (r *WorkspaceRepository) Update(ctx context.Context, workspace *entity.Workspace) error {
|
||||
workspace.UpdatedAt = time.Now()
|
||||
|
||||
query := `
|
||||
UPDATE workspaces
|
||||
SET name = $1, description = $2, updated_at = $3
|
||||
WHERE id = $4
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
workspace.Name,
|
||||
workspace.Description,
|
||||
workspace.UpdatedAt,
|
||||
workspace.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update workspace: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrWorkspaceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除 Workspace
|
||||
func (r *WorkspaceRepository) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM workspaces WHERE id = $1`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete workspace: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrWorkspaceNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// List 列出所有 Workspace
|
||||
func (r *WorkspaceRepository) List(ctx context.Context) ([]*entity.Workspace, error) {
|
||||
query := `
|
||||
SELECT id, name, description, created_by, created_at, updated_at
|
||||
FROM workspaces
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list workspaces: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
workspaces := make([]*entity.Workspace, 0)
|
||||
for rows.Next() {
|
||||
workspace := &entity.Workspace{}
|
||||
err := rows.Scan(
|
||||
&workspace.ID,
|
||||
&workspace.Name,
|
||||
&workspace.Description,
|
||||
&workspace.CreatedBy,
|
||||
&workspace.CreatedAt,
|
||||
&workspace.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan workspace: %w", err)
|
||||
}
|
||||
workspaces = append(workspaces, workspace)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return workspaces, nil
|
||||
}
|
||||
Reference in New Issue
Block a user