refactor: full-stack restructure with multi-tenancy, workspace management, and K8s diagnostics
- Add Workspace domain (entity, repository, service, handler, DTO) - Add multi-tenant K8s client with tenant binding and quota management - Add K8s diagnostics client (instance diagnostics) - Add authorization middleware (authz package) - Restructure frontend to feature-based architecture (features/) - Add User Management page in configuration - Add AccessDenied page and route guards - Refactor shared components (form inputs, layout, UI) - Update Tailwind config for new design system - Add comprehensive documentation (docs/, tasks/, plans) - Improve cluster service with better kubeconfig handling - Add tests for crypto, config, helm client, tenant binding
This commit is contained in:
@ -12,54 +12,33 @@ import (
|
||||
"github.com/ocdp/cluster-service/internal/pkg/crypto"
|
||||
)
|
||||
|
||||
// ClusterRepository PostgreSQL 集群仓储实现
|
||||
type ClusterRepository struct {
|
||||
db *DB
|
||||
encryptor crypto.Encryptor
|
||||
}
|
||||
|
||||
// NewClusterRepository 创建 PostgreSQL 集群仓储
|
||||
func NewClusterRepository(db *DB, encryptor crypto.Encryptor) repository.ClusterRepository {
|
||||
return &ClusterRepository{
|
||||
db: db,
|
||||
encryptor: encryptor,
|
||||
}
|
||||
return &ClusterRepository{db: db, encryptor: encryptor}
|
||||
}
|
||||
|
||||
// Create 创建集群
|
||||
func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster) error {
|
||||
if cluster.ID == "" {
|
||||
cluster.ID = uuid.New().String()
|
||||
}
|
||||
|
||||
// 加密敏感数据
|
||||
encryptedCAData, err := r.encryptor.Encrypt(cluster.CAData)
|
||||
encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken, err := r.encryptClusterSecrets(cluster)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt CA data: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
encryptedCertData, err := r.encryptor.Encrypt(cluster.CertData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt cert data: %w", err)
|
||||
}
|
||||
|
||||
encryptedKeyData, err := r.encryptor.Encrypt(cluster.KeyData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt key data: %w", err)
|
||||
}
|
||||
|
||||
encryptedToken, err := r.encryptor.Encrypt(cluster.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt token: %w", err)
|
||||
}
|
||||
|
||||
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, visibility, name, host, ca_data, cert_data, key_data, token, description, default_namespace, created_at, updated_at)
|
||||
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14)
|
||||
`
|
||||
|
||||
_, err = r.db.conn.ExecContext(ctx, query,
|
||||
cluster.ID,
|
||||
cluster.WorkspaceID,
|
||||
cluster.OwnerID,
|
||||
cluster.Visibility,
|
||||
cluster.Name,
|
||||
cluster.Host,
|
||||
encryptedCAData,
|
||||
@ -67,160 +46,62 @@ func (r *ClusterRepository) Create(ctx context.Context, cluster *entity.Cluster)
|
||||
encryptedKeyData,
|
||||
encryptedToken,
|
||||
cluster.Description,
|
||||
cluster.DefaultNamespace,
|
||||
cluster.CreatedAt,
|
||||
cluster.UpdatedAt,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to create cluster: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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
|
||||
FROM clusters
|
||||
WHERE id = $1
|
||||
`
|
||||
|
||||
cluster := &entity.Cluster{}
|
||||
var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken string
|
||||
|
||||
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
|
||||
&cluster.ID,
|
||||
&cluster.Name,
|
||||
&cluster.Host,
|
||||
&encryptedCAData,
|
||||
&encryptedCertData,
|
||||
&encryptedKeyData,
|
||||
&encryptedToken,
|
||||
&cluster.Description,
|
||||
&cluster.CreatedAt,
|
||||
&cluster.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrClusterNotFound
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cluster: %w", err)
|
||||
}
|
||||
|
||||
// 解密敏感数据
|
||||
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)
|
||||
}
|
||||
|
||||
return cluster, nil
|
||||
return r.get(ctx, "id = $1", id)
|
||||
}
|
||||
|
||||
// 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
|
||||
return r.get(ctx, "name = $1", name)
|
||||
}
|
||||
|
||||
func (r *ClusterRepository) get(ctx context.Context, where string, arg interface{}) (*entity.Cluster, error) {
|
||||
query := fmt.Sprintf(`
|
||||
SELECT id, workspace_id, owner_id, visibility, name, host, ca_data, cert_data, key_data, token, description, default_namespace, created_at, updated_at
|
||||
FROM clusters
|
||||
WHERE name = $1
|
||||
`
|
||||
|
||||
cluster := &entity.Cluster{}
|
||||
var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken string
|
||||
|
||||
err := r.db.conn.QueryRowContext(ctx, query, name).Scan(
|
||||
&cluster.ID,
|
||||
&cluster.Name,
|
||||
&cluster.Host,
|
||||
&encryptedCAData,
|
||||
&encryptedCertData,
|
||||
&encryptedKeyData,
|
||||
&encryptedToken,
|
||||
&cluster.Description,
|
||||
&cluster.CreatedAt,
|
||||
&cluster.UpdatedAt,
|
||||
)
|
||||
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, entity.ErrClusterNotFound
|
||||
}
|
||||
WHERE %s
|
||||
`, where)
|
||||
rows, err := r.db.conn.QueryContext(ctx, query, arg)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get cluster: %w", err)
|
||||
}
|
||||
|
||||
// 解密敏感数据
|
||||
cluster.CAData, err = r.encryptor.Decrypt(encryptedCAData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt CA data: %w", err)
|
||||
defer rows.Close()
|
||||
if !rows.Next() {
|
||||
return nil, entity.ErrClusterNotFound
|
||||
}
|
||||
|
||||
cluster.CertData, err = r.encryptor.Decrypt(encryptedCertData)
|
||||
cluster, err := r.scanCluster(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt cert data: %w", err)
|
||||
return nil, 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)
|
||||
}
|
||||
|
||||
return cluster, nil
|
||||
}
|
||||
|
||||
// Update 更新集群
|
||||
func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster) error {
|
||||
cluster.UpdatedAt = time.Now()
|
||||
|
||||
// 加密敏感数据
|
||||
encryptedCAData, err := r.encryptor.Encrypt(cluster.CAData)
|
||||
encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken, err := r.encryptClusterSecrets(cluster)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt CA data: %w", err)
|
||||
return err
|
||||
}
|
||||
|
||||
encryptedCertData, err := r.encryptor.Encrypt(cluster.CertData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt cert data: %w", err)
|
||||
}
|
||||
|
||||
encryptedKeyData, err := r.encryptor.Encrypt(cluster.KeyData)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt key data: %w", err)
|
||||
}
|
||||
|
||||
encryptedToken, err := r.encryptor.Encrypt(cluster.Token)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to encrypt token: %w", err)
|
||||
}
|
||||
|
||||
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 workspace_id = $1, owner_id = $2, visibility = $3, name = $4, host = $5,
|
||||
ca_data = $6, cert_data = $7, key_data = $8, token = $9, description = $10,
|
||||
default_namespace = $11, updated_at = $12
|
||||
WHERE id = $13
|
||||
`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query,
|
||||
cluster.WorkspaceID,
|
||||
cluster.OwnerID,
|
||||
cluster.Visibility,
|
||||
cluster.Name,
|
||||
cluster.Host,
|
||||
encryptedCAData,
|
||||
@ -228,110 +109,134 @@ func (r *ClusterRepository) Update(ctx context.Context, cluster *entity.Cluster)
|
||||
encryptedKeyData,
|
||||
encryptedToken,
|
||||
cluster.Description,
|
||||
cluster.DefaultNamespace,
|
||||
cluster.UpdatedAt,
|
||||
cluster.ID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to update cluster: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrClusterNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete 删除集群
|
||||
func (r *ClusterRepository) Delete(ctx context.Context, id string) error {
|
||||
query := `DELETE FROM clusters WHERE id = $1`
|
||||
|
||||
result, err := r.db.conn.ExecContext(ctx, query, id)
|
||||
result, err := r.db.conn.ExecContext(ctx, `DELETE FROM clusters WHERE id = $1`, id)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to delete cluster: %w", err)
|
||||
}
|
||||
|
||||
rows, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get affected rows: %w", err)
|
||||
}
|
||||
|
||||
if rows == 0 {
|
||||
return entity.ErrClusterNotFound
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 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, visibility, name, host, ca_data, cert_data, key_data, token, description, default_namespace, created_at, updated_at
|
||||
FROM clusters
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
rows, err := r.db.conn.QueryContext(ctx, query)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to list clusters: %w", err)
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
clusters := make([]*entity.Cluster, 0)
|
||||
for rows.Next() {
|
||||
cluster := &entity.Cluster{}
|
||||
var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken string
|
||||
|
||||
err := rows.Scan(
|
||||
&cluster.ID,
|
||||
&cluster.Name,
|
||||
&cluster.Host,
|
||||
&encryptedCAData,
|
||||
&encryptedCertData,
|
||||
&encryptedKeyData,
|
||||
&encryptedToken,
|
||||
&cluster.Description,
|
||||
&cluster.CreatedAt,
|
||||
&cluster.UpdatedAt,
|
||||
)
|
||||
cluster, err := r.scanCluster(rows)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan cluster: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 解密敏感数据
|
||||
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)
|
||||
}
|
||||
|
||||
clusters = append(clusters, cluster)
|
||||
}
|
||||
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf("rows iteration error: %w", err)
|
||||
}
|
||||
|
||||
return clusters, nil
|
||||
}
|
||||
|
||||
type clusterScanner interface {
|
||||
Scan(dest ...interface{}) error
|
||||
}
|
||||
|
||||
func (r *ClusterRepository) scanCluster(scanner clusterScanner) (*entity.Cluster, error) {
|
||||
cluster := &entity.Cluster{}
|
||||
var encryptedCAData, encryptedCertData, encryptedKeyData, encryptedToken sql.NullString
|
||||
var defaultNamespace sql.NullString
|
||||
err := scanner.Scan(
|
||||
&cluster.ID,
|
||||
&cluster.WorkspaceID,
|
||||
&cluster.OwnerID,
|
||||
&cluster.Visibility,
|
||||
&cluster.Name,
|
||||
&cluster.Host,
|
||||
&encryptedCAData,
|
||||
&encryptedCertData,
|
||||
&encryptedKeyData,
|
||||
&encryptedToken,
|
||||
&cluster.Description,
|
||||
&defaultNamespace,
|
||||
&cluster.CreatedAt,
|
||||
&cluster.UpdatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to scan cluster: %w", err)
|
||||
}
|
||||
cluster.DefaultNamespace = defaultNamespace.String
|
||||
var decryptErr error
|
||||
cluster.CAData, decryptErr = decryptMaybe(r.encryptor, encryptedCAData.String)
|
||||
if decryptErr != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt CA data: %w", decryptErr)
|
||||
}
|
||||
cluster.CertData, decryptErr = decryptMaybe(r.encryptor, encryptedCertData.String)
|
||||
if decryptErr != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt cert data: %w", decryptErr)
|
||||
}
|
||||
cluster.KeyData, decryptErr = decryptMaybe(r.encryptor, encryptedKeyData.String)
|
||||
if decryptErr != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt key data: %w", decryptErr)
|
||||
}
|
||||
cluster.Token, decryptErr = decryptMaybe(r.encryptor, encryptedToken.String)
|
||||
if decryptErr != nil {
|
||||
return nil, fmt.Errorf("failed to decrypt token: %w", decryptErr)
|
||||
}
|
||||
return cluster, nil
|
||||
}
|
||||
|
||||
func (r *ClusterRepository) encryptClusterSecrets(cluster *entity.Cluster) (string, string, string, string, error) {
|
||||
ca, err := r.encryptor.Encrypt(cluster.CAData)
|
||||
if err != nil {
|
||||
return "", "", "", "", fmt.Errorf("failed to encrypt CA data: %w", err)
|
||||
}
|
||||
cert, err := r.encryptor.Encrypt(cluster.CertData)
|
||||
if err != nil {
|
||||
return "", "", "", "", fmt.Errorf("failed to encrypt cert data: %w", err)
|
||||
}
|
||||
key, err := r.encryptor.Encrypt(cluster.KeyData)
|
||||
if err != nil {
|
||||
return "", "", "", "", fmt.Errorf("failed to encrypt key data: %w", err)
|
||||
}
|
||||
token, err := r.encryptor.Encrypt(cluster.Token)
|
||||
if err != nil {
|
||||
return "", "", "", "", fmt.Errorf("failed to encrypt token: %w", err)
|
||||
}
|
||||
return ca, cert, key, token, nil
|
||||
}
|
||||
|
||||
func decryptMaybe(encryptor crypto.Encryptor, value string) (string, error) {
|
||||
if value == "" {
|
||||
return "", nil
|
||||
}
|
||||
return encryptor.Decrypt(value)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user