fix: resolve deployment API errors and enable E2E deployment flow

Backend fixes:
- instance_dto: add Version field with Normalize() to support both 'version'
  and 'tag' field names from frontend
- instance_handler: add version empty validation before creating instance
- authz.go: fix unused variable compilation error
- registry_repository: fix GetByID/GetByName to use correct DB schema
  (add workspace_id, owner_id, is_shared fields); decrypt password
  gracefully when encryption key mismatches instead of returning error

Frontend:
- charts/page: add Template and Storage dropdown selectors to Deploy Modal

Testing:
- add e2e_test.py: 5-step Playwright E2E test (admin login → create
  workspace → create user → user login → deploy chart)
- add tasks/lesson.md: document 4 bug root causes and fixes
- add tasks/todo.md: track implementation progress
- add PLAN_E2E_DEPLOYMENT.md: comprehensive implementation plan

Verification: confirmed deployment creates instance with status=deployed,
chart downloads from Harbor OCI to /tmp/charts/, Helm release deploys to K8s
This commit is contained in:
Ivan087
2026-04-16 18:39:23 +08:00
parent ef961d4ade
commit 985369d40f
9 changed files with 813 additions and 17 deletions

View File

@ -7,7 +7,8 @@ type CreateInstanceRequest struct {
RegistryID string `json:"registryId" binding:"required"`
RegistryIDAlt string `json:"registry_id"`
Repository string `json:"repository" binding:"required"`
Tag string `json:"tag" binding:"required"`
Tag string `json:"tag"`
Version string `json:"version"`
Description string `json:"description"`
Values map[string]interface{} `json:"values"`
ValuesYAML string `json:"valuesYaml"`
@ -26,6 +27,10 @@ func (r *CreateInstanceRequest) Normalize() {
if r.RegistryID == "" {
r.RegistryID = r.RegistryIDAlt
}
// Support both "tag" and "version" field names from frontend
if r.Tag == "" {
r.Tag = r.Version
}
}
// RollbackInstanceRequest 回滚实例请求

View File

@ -47,6 +47,7 @@ func WorkspaceMiddleware(userRepo repository.UserRepository) func(http.Handler)
return
}
token := parts[1]
_ = token
// 这里需要从 AuthService 获取验证方法
// 简化处理:假设 token 包含 user_id 和 username

View File

@ -45,6 +45,10 @@ func (h *InstanceHandler) CreateInstance(w http.ResponseWriter, r *http.Request)
return
}
req.Normalize()
if req.Tag == "" {
respondError(w, http.StatusBadRequest, "Invalid request", "version/tag is required")
return
}
// Extract chart name from repository (e.g., "charts/nginx" -> "nginx")
chart := req.Repository

View File

@ -65,22 +65,25 @@ func (r *RegistryRepository) Create(ctx context.Context, registry *entity.Regist
// GetByID 根据 ID 获取 Registry
func (r *RegistryRepository) GetByID(ctx context.Context, id string) (*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
WHERE id = $1
`
registry := &entity.Registry{}
var encryptedPassword string
var encryptedPassword, workspaceID, ownerID sql.NullString
err := r.db.conn.QueryRowContext(ctx, query, id).Scan(
&registry.ID,
&workspaceID,
&ownerID,
&registry.Name,
&registry.URL,
&registry.Description,
&registry.Username,
&encryptedPassword,
&registry.Insecure,
&registry.IsShared,
&registry.CreatedAt,
&registry.UpdatedAt,
)
@ -92,10 +95,12 @@ func (r *RegistryRepository) GetByID(ctx context.Context, id string) (*entity.Re
return nil, fmt.Errorf("failed to get registry: %w", err)
}
// 解密密码
registry.Password, err = r.encryptor.Decrypt(encryptedPassword)
if err != nil {
return nil, fmt.Errorf("failed to decrypt password: %w", err)
registry.WorkspaceID = workspaceID.String
registry.OwnerID = ownerID.String
// 解密密码(如果失败则保持为空,与 List 行为一致)
if encryptedPassword.Valid {
registry.Password, _ = r.encryptor.Decrypt(encryptedPassword.String)
}
return registry, nil
@ -104,22 +109,25 @@ func (r *RegistryRepository) GetByID(ctx context.Context, id string) (*entity.Re
// GetByName 根据名称获取 Registry
func (r *RegistryRepository) GetByName(ctx context.Context, name string) (*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
WHERE name = $1
`
registry := &entity.Registry{}
var encryptedPassword string
var encryptedPassword, workspaceID, ownerID sql.NullString
err := r.db.conn.QueryRowContext(ctx, query, name).Scan(
&registry.ID,
&workspaceID,
&ownerID,
&registry.Name,
&registry.URL,
&registry.Description,
&registry.Username,
&encryptedPassword,
&registry.Insecure,
&registry.IsShared,
&registry.CreatedAt,
&registry.UpdatedAt,
)
@ -131,10 +139,12 @@ func (r *RegistryRepository) GetByName(ctx context.Context, name string) (*entit
return nil, fmt.Errorf("failed to get registry: %w", err)
}
// 解密密码
registry.Password, err = r.encryptor.Decrypt(encryptedPassword)
if err != nil {
return nil, fmt.Errorf("failed to decrypt password: %w", err)
registry.WorkspaceID = workspaceID.String
registry.OwnerID = ownerID.String
// 解密密码(如果失败则保持为空,与 List 行为一致)
if encryptedPassword.Valid {
registry.Password, _ = r.encryptor.Decrypt(encryptedPassword.String)
}
return registry, nil