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:
90
backend/internal/domain/entity/audit_log.go
Normal file
90
backend/internal/domain/entity/audit_log.go
Normal file
@ -0,0 +1,90 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AuditAction 审计操作类型
|
||||
type AuditAction string
|
||||
|
||||
const (
|
||||
AuditActionCreate AuditAction = "create"
|
||||
AuditActionUpdate AuditAction = "update"
|
||||
AuditActionDelete AuditAction = "delete"
|
||||
AuditActionDeploy AuditAction = "deploy"
|
||||
AuditActionScale AuditAction = "scale"
|
||||
AuditActionLogin AuditAction = "login"
|
||||
AuditActionLogout AuditAction = "logout"
|
||||
AuditActionChangePassword AuditAction = "change_password"
|
||||
)
|
||||
|
||||
// AuditResourceType 审计资源类型
|
||||
type AuditResourceType string
|
||||
|
||||
const (
|
||||
AuditResourceUser AuditResourceType = "user"
|
||||
AuditResourceWorkspace AuditResourceType = "workspace"
|
||||
AuditResourceQuota AuditResourceType = "quota"
|
||||
AuditResourceCluster AuditResourceType = "cluster"
|
||||
AuditResourceRegistry AuditResourceType = "registry"
|
||||
AuditResourceInstance AuditResourceType = "instance"
|
||||
AuditResourceStorage AuditResourceType = "storage"
|
||||
AuditResourceTemplate AuditResourceType = "template"
|
||||
)
|
||||
|
||||
// AuditLog 审计日志实体
|
||||
type AuditLog struct {
|
||||
ID string
|
||||
WorkspaceID string
|
||||
UserID string
|
||||
Action AuditAction
|
||||
ResourceType AuditResourceType
|
||||
ResourceID string
|
||||
ResourceName string
|
||||
Details map[string]interface{}
|
||||
IPAddress string
|
||||
UserAgent string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// NewAuditLog 创建新审计日志
|
||||
func NewAuditLog(workspaceID, userID string, action AuditAction, resourceType AuditResourceType) *AuditLog {
|
||||
now := time.Now()
|
||||
return &AuditLog{
|
||||
WorkspaceID: workspaceID,
|
||||
UserID: userID,
|
||||
Action: action,
|
||||
ResourceType: resourceType,
|
||||
CreatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// SetResource 设置关联资源
|
||||
func (a *AuditLog) SetResource(resourceID, resourceName string) {
|
||||
a.ResourceID = resourceID
|
||||
a.ResourceName = resourceName
|
||||
}
|
||||
|
||||
// SetDetails 设置详细信息
|
||||
func (a *AuditLog) SetDetails(details map[string]interface{}) {
|
||||
a.Details = details
|
||||
}
|
||||
|
||||
// SetClientInfo 设置客户端信息
|
||||
func (a *AuditLog) SetClientInfo(ipAddress, userAgent string) {
|
||||
a.IPAddress = ipAddress
|
||||
a.UserAgent = userAgent
|
||||
}
|
||||
|
||||
// DetailsJSON 将详情转为 JSON 字符串
|
||||
func (a *AuditLog) DetailsJSON() (string, error) {
|
||||
if a.Details == nil {
|
||||
return "{}", nil
|
||||
}
|
||||
data, err := json.Marshal(a.Details)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
41
backend/internal/domain/entity/chart_reference.go
Normal file
41
backend/internal/domain/entity/chart_reference.go
Normal file
@ -0,0 +1,41 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ChartReference Chart 引用实体
|
||||
type ChartReference struct {
|
||||
ID string
|
||||
WorkspaceID string
|
||||
RegistryID string
|
||||
Repository string
|
||||
ChartName string
|
||||
Description string
|
||||
IsEnabled bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewChartReference 创建新 Chart 引用
|
||||
func NewChartReference(workspaceID, registryID, repository, chartName, description string) *ChartReference {
|
||||
now := time.Now()
|
||||
return &ChartReference{
|
||||
WorkspaceID: workspaceID,
|
||||
RegistryID: registryID,
|
||||
Repository: repository,
|
||||
ChartName: chartName,
|
||||
Description: description,
|
||||
IsEnabled: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证 Chart 引用数据
|
||||
func (c *ChartReference) Validate() error {
|
||||
if c.Repository == "" {
|
||||
return ErrInvalidChart
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@ -4,28 +4,49 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// IsolationMode 集群隔离模式
|
||||
type IsolationMode string
|
||||
|
||||
const (
|
||||
IsolationModeNamespace IsolationMode = "namespace" // 共享集群模式,多 workspace 使用不同 namespace
|
||||
IsolationModeCluster IsolationMode = "cluster" // 私有集群模式,每个 workspace 独立集群
|
||||
)
|
||||
|
||||
// Cluster Kubernetes 集群领域实体
|
||||
type Cluster struct {
|
||||
ID string
|
||||
Name string
|
||||
Host string // Kubernetes API Server URL
|
||||
CAData string // Base64 encoded CA certificate
|
||||
CertData string // Base64 encoded client certificate
|
||||
KeyData string // Base64 encoded client key
|
||||
Token string // Bearer token (alternative to cert auth)
|
||||
Description string
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID string
|
||||
WorkspaceID string // 所属 workspace,NULL 表示全局共享
|
||||
OwnerID string // 创建者用户 ID
|
||||
Name string
|
||||
Host string // Kubernetes API Server URL
|
||||
CAData string // Base64 encoded CA certificate
|
||||
CertData string // Base64 encoded client certificate
|
||||
KeyData string // Base64 encoded client key
|
||||
Token string // Bearer token (alternative to cert auth)
|
||||
Description string
|
||||
|
||||
// 隔离模式
|
||||
IsolationMode IsolationMode // 'namespace' | 'cluster'
|
||||
DefaultNamespace string // 当 isolation_mode=namespace 时的默认 namespace 前缀
|
||||
|
||||
IsShared bool // 是否为共享集群(admin 创建供多 workspace 使用)
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewCluster 创建新集群
|
||||
func NewCluster(name, host string) *Cluster {
|
||||
func NewCluster(workspaceID, ownerID, name, host string) *Cluster {
|
||||
now := time.Now()
|
||||
return &Cluster{
|
||||
Name: name,
|
||||
Host: host,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
WorkspaceID: workspaceID,
|
||||
OwnerID: ownerID,
|
||||
Name: name,
|
||||
Host: host,
|
||||
IsolationMode: IsolationModeNamespace, // 默认 namespace 隔离模式
|
||||
DefaultNamespace: workspaceID, // 默认使用 workspace ID 作为 namespace 前缀
|
||||
IsShared: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
@ -63,11 +84,35 @@ func (c *Cluster) Validate() error {
|
||||
if c.Host == "" {
|
||||
return ErrInvalidClusterHost
|
||||
}
|
||||
// 必须有认证方式:证书或 Token
|
||||
if (c.CertData == "" || c.KeyData == "") && c.Token == "" {
|
||||
return ErrInvalidClusterAuth
|
||||
|
||||
// 检查是否有 kubeconfig 格式(完整的 kubeconfig 在 CAData 中)
|
||||
hasKubeconfig := len(c.CAData) > 100 && (c.CAData[:11] == "apiVersion:" || c.CAData[:5] == "kind:")
|
||||
|
||||
// 认证方式:证书、Token、kubeconfig 或空(使用本地 kubeconfig)
|
||||
hasCertAuth := c.CertData != "" && c.KeyData != ""
|
||||
hasToken := c.Token != ""
|
||||
hasNoAuth := c.CertData == "" && c.KeyData == "" && c.Token == ""
|
||||
|
||||
// 如果有 kubeconfig 格式,或有证书,或有 token,或没有凭证(依赖 TestConnection 使用本地 kubeconfig),都是有效的
|
||||
if hasKubeconfig || hasCertAuth || hasToken || hasNoAuth {
|
||||
return nil
|
||||
}
|
||||
return nil
|
||||
|
||||
return ErrInvalidClusterAuth
|
||||
}
|
||||
|
||||
// GetNamespace 获取部署用的 namespace
|
||||
// namespace 隔离模式: {workspace_id}-{instance_name} 或 {default_namespace}-{username}
|
||||
// cluster 隔离模式: 使用 workspace 的默认 namespace
|
||||
func (c *Cluster) GetNamespace(workspaceName, instanceName string) string {
|
||||
if c.IsolationMode == IsolationModeCluster {
|
||||
return c.DefaultNamespace
|
||||
}
|
||||
// namespace 隔离模式
|
||||
if c.DefaultNamespace != "" {
|
||||
return c.DefaultNamespace + "-" + instanceName
|
||||
}
|
||||
return workspaceName + "-" + instanceName
|
||||
}
|
||||
|
||||
// GetKubeConfig 生成 kubeconfig 内容
|
||||
|
||||
@ -37,4 +37,32 @@ var (
|
||||
ErrArtifactNotFound = errors.New("artifact not found")
|
||||
ErrRepositoryNotFound = errors.New("repository not found")
|
||||
ErrValuesSchemaNotFound = errors.New("values schema not found")
|
||||
ErrValuesNotFound = errors.New("values not found")
|
||||
|
||||
// Workspace errors
|
||||
ErrInvalidWorkspaceName = errors.New("invalid workspace name")
|
||||
ErrWorkspaceNotFound = errors.New("workspace not found")
|
||||
ErrWorkspaceExists = errors.New("workspace already exists")
|
||||
|
||||
// Quota errors
|
||||
ErrQuotaExceeded = errors.New("quota exceeded")
|
||||
ErrInvalidQuota = errors.New("invalid quota")
|
||||
|
||||
// Storage errors
|
||||
ErrInvalidStorageName = errors.New("invalid storage name")
|
||||
ErrStorageNotFound = errors.New("storage not found")
|
||||
ErrStorageExists = errors.New("storage already exists")
|
||||
|
||||
// Chart Reference errors
|
||||
ErrInvalidChartReferenceName = errors.New("invalid chart reference name")
|
||||
ErrChartReferenceNotFound = errors.New("chart reference not found")
|
||||
ErrChartReferenceExists = errors.New("chart reference already exists")
|
||||
|
||||
// Template errors
|
||||
ErrInvalidTemplateName = errors.New("invalid template name")
|
||||
ErrTemplateNotFound = errors.New("template not found")
|
||||
ErrTemplateExists = errors.New("template already exists")
|
||||
|
||||
// Permission errors
|
||||
ErrPermissionDenied = errors.New("permission denied")
|
||||
)
|
||||
|
||||
@ -1,7 +1,9 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"time"
|
||||
"unicode"
|
||||
)
|
||||
|
||||
// InstanceStatus 实例状态
|
||||
@ -33,43 +35,65 @@ const (
|
||||
|
||||
// Instance Helm 应用实例领域实体
|
||||
type Instance struct {
|
||||
ID string
|
||||
ClusterID string
|
||||
Name string // Helm Release Name
|
||||
Namespace string
|
||||
RegistryID string
|
||||
Repository string // OCI Repository (e.g., charts/app)
|
||||
Chart string // Chart Name
|
||||
Version string // Chart Version
|
||||
Description string
|
||||
Values map[string]interface{} // Helm Values (JSON)
|
||||
ValuesYAML string // Helm Values (YAML format)
|
||||
Status InstanceStatus
|
||||
StatusReason string
|
||||
LastOperation InstanceOperation
|
||||
LastError string
|
||||
Revision int // Helm Release Revision
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID string
|
||||
WorkspaceID string // 所属 workspace
|
||||
OwnerID string // 创建者用户 ID
|
||||
ClusterID string
|
||||
RegistryID string
|
||||
ChartReferenceID string // 引用的 Chart 引用
|
||||
ValuesTemplateID string // 使用的 Values 模板
|
||||
|
||||
Name string // Helm Release Name
|
||||
Namespace string
|
||||
Repository string // OCI Repository (e.g., charts/app)
|
||||
Chart string // Chart Name
|
||||
Version string // Chart Version
|
||||
Description string
|
||||
Values map[string]interface{} // Helm Values (JSON)
|
||||
ValuesYAML string // Helm Values (YAML format)
|
||||
UserOverrideYAML string // 用户额外覆盖的配置
|
||||
|
||||
Status InstanceStatus
|
||||
StatusReason string
|
||||
LastOperation InstanceOperation
|
||||
LastError string
|
||||
Revision int // Helm Release Revision
|
||||
|
||||
// 资源使用统计(Helm 安装时从集群获取并更新)
|
||||
CPURequested float64 // CPU 请求量 (cores)
|
||||
MemoryRequested string // 内存请求量 (e.g., "2Gi")
|
||||
GPURequested float64 // GPU 请求量 (cards)
|
||||
GPUMemoryRequested string // GPU 内存请求量 (e.g., "16Gi")
|
||||
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewInstance 创建新实例
|
||||
func NewInstance(clusterID, name, namespace, registryID, repository, chart, version string) *Instance {
|
||||
func NewInstance(workspaceID, ownerID, clusterID, registryID, chartReferenceID, valuesTemplateID, name, namespace, repository, chart, version string) *Instance {
|
||||
now := time.Now()
|
||||
return &Instance{
|
||||
ClusterID: clusterID,
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
RegistryID: registryID,
|
||||
Repository: repository,
|
||||
Chart: chart,
|
||||
Version: version,
|
||||
Status: StatusPending,
|
||||
StatusReason: "Pending install",
|
||||
LastOperation: OperationInstall,
|
||||
Revision: 1,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
WorkspaceID: workspaceID,
|
||||
OwnerID: ownerID,
|
||||
ClusterID: clusterID,
|
||||
RegistryID: registryID,
|
||||
ChartReferenceID: chartReferenceID,
|
||||
ValuesTemplateID: valuesTemplateID,
|
||||
Name: name,
|
||||
Namespace: namespace,
|
||||
Repository: repository,
|
||||
Chart: chart,
|
||||
Version: version,
|
||||
Status: StatusPending,
|
||||
StatusReason: "Pending install",
|
||||
LastOperation: OperationInstall,
|
||||
Revision: 1,
|
||||
CPURequested: 0,
|
||||
MemoryRequested: "0Mi",
|
||||
GPURequested: 0,
|
||||
GPUMemoryRequested: "0Mi",
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
@ -154,13 +178,43 @@ func (i *Instance) Upgrade(version string, values map[string]interface{}) {
|
||||
i.BeginOperation(OperationUpgrade, "Pending upgrade")
|
||||
}
|
||||
|
||||
// ValidateReleaseName 验证 Helm Release 名称是否符合 RFC 1123 DNS 子域名规范
|
||||
// Helm release 名称必须:
|
||||
// - 只能包含小写字母(a-z)、数字(0-9)和连字符(-)
|
||||
// - 不能以连字符开头或结尾
|
||||
// - 长度不超过 53 个字符
|
||||
func ValidateReleaseName(name string) error {
|
||||
if name == "" {
|
||||
return ErrInvalidInstanceName
|
||||
}
|
||||
|
||||
// 检查长度(RFC 1123 DNS 子域名最大长度为 63,但 Helm 限制为 53)
|
||||
if len(name) > 53 {
|
||||
return ErrInvalidInstanceName
|
||||
}
|
||||
|
||||
// 不能以连字符开头或结尾
|
||||
if strings.HasPrefix(name, "-") || strings.HasSuffix(name, "-") {
|
||||
return ErrInvalidInstanceName
|
||||
}
|
||||
|
||||
// 只能包含小写字母、数字和连字符
|
||||
for _, r := range name {
|
||||
if !(unicode.IsLower(r) || unicode.IsDigit(r) || r == '-') {
|
||||
return ErrInvalidInstanceName
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Validate 验证实例配置
|
||||
func (i *Instance) Validate() error {
|
||||
if i.ClusterID == "" {
|
||||
return ErrInvalidClusterID
|
||||
}
|
||||
if i.Name == "" {
|
||||
return ErrInvalidInstanceName
|
||||
if err := ValidateReleaseName(i.Name); err != nil {
|
||||
return err
|
||||
}
|
||||
if i.Namespace == "" {
|
||||
return ErrInvalidNamespace
|
||||
|
||||
79
backend/internal/domain/entity/quota.go
Normal file
79
backend/internal/domain/entity/quota.go
Normal file
@ -0,0 +1,79 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ResourceType 资源类型
|
||||
type ResourceType string
|
||||
|
||||
const (
|
||||
ResourceCPU ResourceType = "cpu"
|
||||
ResourceGPU ResourceType = "gpu"
|
||||
ResourceGPUMemory ResourceType = "gpu_memory"
|
||||
)
|
||||
|
||||
// WorkspaceQuota 工作空间配额实体
|
||||
type WorkspaceQuota struct {
|
||||
ID string
|
||||
WorkspaceID string
|
||||
ResourceType ResourceType
|
||||
HardLimit float64 // 硬限制(0表示无限制)
|
||||
SoftLimit float64 // 软限制(警告阈值)
|
||||
Used float64 // 当前使用量
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewWorkspaceQuota 创建新配额
|
||||
func NewWorkspaceQuota(workspaceID string, resourceType ResourceType, hardLimit, softLimit float64) *WorkspaceQuota {
|
||||
now := time.Now()
|
||||
return &WorkspaceQuota{
|
||||
WorkspaceID: workspaceID,
|
||||
ResourceType: resourceType,
|
||||
HardLimit: hardLimit,
|
||||
SoftLimit: softLimit,
|
||||
Used: 0,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// CanAllocate 检查是否可以分配指定资源量
|
||||
func (q *WorkspaceQuota) CanAllocate(amount float64) bool {
|
||||
if q.HardLimit == 0 {
|
||||
return true // 无限制
|
||||
}
|
||||
return q.Used+amount <= q.HardLimit
|
||||
}
|
||||
|
||||
// Allocate 分配资源
|
||||
func (q *WorkspaceQuota) Allocate(amount float64) {
|
||||
q.Used += amount
|
||||
q.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// Release 释放资源
|
||||
func (q *WorkspaceQuota) Release(amount float64) {
|
||||
q.Used -= amount
|
||||
if q.Used < 0 {
|
||||
q.Used = 0
|
||||
}
|
||||
q.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// IsOverLimit 检查是否超过硬限制
|
||||
func (q *WorkspaceQuota) IsOverLimit() bool {
|
||||
if q.HardLimit == 0 {
|
||||
return false
|
||||
}
|
||||
return q.Used > q.HardLimit
|
||||
}
|
||||
|
||||
// IsOverSoftLimit 检查是否超过软限制
|
||||
func (q *WorkspaceQuota) IsOverSoftLimit() bool {
|
||||
if q.SoftLimit == 0 {
|
||||
return false
|
||||
}
|
||||
return q.Used > q.SoftLimit
|
||||
}
|
||||
@ -7,24 +7,30 @@ import (
|
||||
// Registry OCI Registry 领域实体
|
||||
type Registry struct {
|
||||
ID string
|
||||
WorkspaceID string // 所属 workspace,NULL 表示全局共享
|
||||
OwnerID string // 创建者用户 ID
|
||||
Name string
|
||||
URL string
|
||||
Description string
|
||||
Username string
|
||||
Password string
|
||||
Insecure bool // 是否跳过 TLS 验证
|
||||
Insecure bool // 是否跳过 TLS 验证
|
||||
IsShared bool // 是否为共享 Registry(admin 创建供多 workspace 使用)
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewRegistry 创建新 Registry
|
||||
func NewRegistry(name, url string) *Registry {
|
||||
func NewRegistry(workspaceID, ownerID, name, url string) *Registry {
|
||||
now := time.Now()
|
||||
return &Registry{
|
||||
Name: name,
|
||||
URL: url,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
WorkspaceID: workspaceID,
|
||||
OwnerID: ownerID,
|
||||
Name: name,
|
||||
URL: url,
|
||||
IsShared: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
98
backend/internal/domain/entity/storage.go
Normal file
98
backend/internal/domain/entity/storage.go
Normal file
@ -0,0 +1,98 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"time"
|
||||
)
|
||||
|
||||
// StorageType 存储类型
|
||||
type StorageType string
|
||||
|
||||
const (
|
||||
StorageTypeNFS StorageType = "nfs"
|
||||
StorageTypePV StorageType = "pv"
|
||||
StorageTypeHostPath StorageType = "hostPath"
|
||||
)
|
||||
|
||||
// StorageConfig 存储配置
|
||||
type StorageConfig struct {
|
||||
NFS *NFSConfig `json:"nfs,omitempty"`
|
||||
PV *PVConfig `json:"pv,omitempty"`
|
||||
HostPath *HostPathConfig `json:"hostPath,omitempty"`
|
||||
}
|
||||
|
||||
// NFSConfig NFS 配置
|
||||
type NFSConfig struct {
|
||||
Server string `json:"server"`
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// PVConfig PV 配置
|
||||
type PVConfig struct {
|
||||
StorageClassName string `json:"storageClassName"`
|
||||
Capacity string `json:"capacity"`
|
||||
AccessModes []string `json:"accessModes"`
|
||||
}
|
||||
|
||||
// HostPathConfig HostPath 配置
|
||||
type HostPathConfig struct {
|
||||
Path string `json:"path"`
|
||||
}
|
||||
|
||||
// StorageBackend 存储后端实体
|
||||
type StorageBackend struct {
|
||||
ID string
|
||||
WorkspaceID string
|
||||
OwnerID string
|
||||
Name string
|
||||
Type StorageType
|
||||
Config StorageConfig
|
||||
Description string
|
||||
IsDefault bool
|
||||
IsShared bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewStorageBackend 创建新存储后端
|
||||
func NewStorageBackend(workspaceID, ownerID, name string, storageType StorageType, config StorageConfig) *StorageBackend {
|
||||
now := time.Now()
|
||||
return &StorageBackend{
|
||||
WorkspaceID: workspaceID,
|
||||
OwnerID: ownerID,
|
||||
Name: name,
|
||||
Type: storageType,
|
||||
Config: config,
|
||||
IsDefault: false,
|
||||
IsShared: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证存储后端数据
|
||||
func (s *StorageBackend) Validate() error {
|
||||
if s.Name == "" {
|
||||
return ErrInvalidStorageName
|
||||
}
|
||||
if s.Type == "" {
|
||||
return ErrInvalidStorageName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// ConfigJSON 将配置转为 JSON 字符串
|
||||
func (s *StorageBackend) ConfigJSON() (string, error) {
|
||||
data, err := json.Marshal(s.Config)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return string(data), nil
|
||||
}
|
||||
|
||||
// ParseConfigJSON 从 JSON 解析配置
|
||||
func ParseConfigJSON(jsonStr string) (*StorageConfig, error) {
|
||||
var config StorageConfig
|
||||
err := json.Unmarshal([]byte(jsonStr), &config)
|
||||
return &config, err
|
||||
}
|
||||
@ -4,30 +4,58 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// UserRole 用户角色
|
||||
type UserRole string
|
||||
|
||||
const (
|
||||
RoleAdmin UserRole = "admin"
|
||||
RoleUser UserRole = "user"
|
||||
)
|
||||
|
||||
// User 用户领域实体
|
||||
type User struct {
|
||||
ID string
|
||||
Username string
|
||||
PasswordHash string
|
||||
Email string
|
||||
RevokedAfter time.Time // 全局 Token 撤销时间
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
ID string
|
||||
Username string
|
||||
PasswordHash string
|
||||
Email string
|
||||
Role UserRole // 用户角色: admin, user
|
||||
WorkspaceID string // 所属工作空间,admin 为空表示全局
|
||||
IsActive bool // 账户是否激活
|
||||
MustChangePassword bool // 首次登录必须修改密码
|
||||
RevokedAfter time.Time // 全局 Token 撤销时间
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewUser 创建新用户
|
||||
func NewUser(username, passwordHash, email string) *User {
|
||||
now := time.Now()
|
||||
return &User{
|
||||
Username: username,
|
||||
PasswordHash: passwordHash,
|
||||
Email: email,
|
||||
RevokedAfter: time.Unix(0, 0), // 初始值:1970-01-01
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Username: username,
|
||||
PasswordHash: passwordHash,
|
||||
Email: email,
|
||||
Role: RoleUser, // 默认普通用户
|
||||
IsActive: true,
|
||||
MustChangePassword: true, // 首次登录必须修改密码
|
||||
RevokedAfter: time.Unix(0, 0), // 初始值:1970-01-01
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// IsAdmin 判断是否为管理员
|
||||
func (u *User) IsAdmin() bool {
|
||||
return u.Role == RoleAdmin
|
||||
}
|
||||
|
||||
// CanAccessWorkspace 检查是否可以访问指定工作空间
|
||||
func (u *User) CanAccessWorkspace(workspaceID string) bool {
|
||||
if u.IsAdmin() {
|
||||
return true // Admin 可以访问所有工作空间
|
||||
}
|
||||
return u.WorkspaceID == workspaceID
|
||||
}
|
||||
|
||||
// UpdatePassword 更新密码(会触发全局登出)
|
||||
func (u *User) UpdatePassword(newPasswordHash string) {
|
||||
u.PasswordHash = newPasswordHash
|
||||
|
||||
83
backend/internal/domain/entity/values_template.go
Normal file
83
backend/internal/domain/entity/values_template.go
Normal file
@ -0,0 +1,83 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// ValuesTemplate Values 模板实体(带版本管理)
|
||||
type ValuesTemplate struct {
|
||||
ID string
|
||||
WorkspaceID string
|
||||
OwnerID string
|
||||
ChartReferenceID string
|
||||
Name string
|
||||
Description string
|
||||
ValuesYAML string
|
||||
Version int // 模板版本号
|
||||
IsDefault bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewValuesTemplate 创建新 Values 模板
|
||||
func NewValuesTemplate(workspaceID, ownerID, chartReferenceID, name, valuesYAML string) *ValuesTemplate {
|
||||
now := time.Now()
|
||||
return &ValuesTemplate{
|
||||
WorkspaceID: workspaceID,
|
||||
OwnerID: ownerID,
|
||||
ChartReferenceID: chartReferenceID,
|
||||
Name: name,
|
||||
ValuesYAML: valuesYAML,
|
||||
Version: 1,
|
||||
IsDefault: false,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证 Values 模板数据
|
||||
func (v *ValuesTemplate) Validate() error {
|
||||
if v.Name == "" {
|
||||
return ErrInvalidTemplateName
|
||||
}
|
||||
if v.ValuesYAML == "" {
|
||||
return ErrInvalidTemplateName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// IncrementVersion 递增版本号
|
||||
func (v *ValuesTemplate) IncrementVersion() {
|
||||
v.Version++
|
||||
v.UpdatedAt = time.Now()
|
||||
}
|
||||
|
||||
// UserConfigOverride 用户配置覆盖实体
|
||||
type UserConfigOverride struct {
|
||||
ID string
|
||||
WorkspaceID string
|
||||
UserID string
|
||||
TargetType string // 'storage', 'template', 'global'
|
||||
TargetID string
|
||||
Config map[string]interface{}
|
||||
Priority int
|
||||
IsActive bool
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewUserConfigOverride 创建新用户配置覆盖
|
||||
func NewUserConfigOverride(workspaceID, userID, targetType, targetID string, config map[string]interface{}) *UserConfigOverride {
|
||||
now := time.Now()
|
||||
return &UserConfigOverride{
|
||||
WorkspaceID: workspaceID,
|
||||
UserID: userID,
|
||||
TargetType: targetType,
|
||||
TargetID: targetID,
|
||||
Config: config,
|
||||
Priority: 0,
|
||||
IsActive: true,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
35
backend/internal/domain/entity/workspace.go
Normal file
35
backend/internal/domain/entity/workspace.go
Normal file
@ -0,0 +1,35 @@
|
||||
package entity
|
||||
|
||||
import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Workspace 工作空间实体
|
||||
type Workspace struct {
|
||||
ID string
|
||||
Name string
|
||||
Description string
|
||||
CreatedBy string // 创建者用户 ID
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
|
||||
// NewWorkspace 创建新工作空间
|
||||
func NewWorkspace(name, description, createdBy string) *Workspace {
|
||||
now := time.Now()
|
||||
return &Workspace{
|
||||
Name: name,
|
||||
Description: description,
|
||||
CreatedBy: createdBy,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
}
|
||||
|
||||
// Validate 验证工作空间数据
|
||||
func (w *Workspace) Validate() error {
|
||||
if w.Name == "" {
|
||||
return ErrInvalidWorkspaceName
|
||||
}
|
||||
return nil
|
||||
}
|
||||
Reference in New Issue
Block a user