chore: establish local platform baseline
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
package httpapi
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"data-terminal-cloud/internal/concurrency"
|
||||
"data-terminal-cloud/internal/idempotency"
|
||||
"data-terminal-cloud/internal/identity"
|
||||
"data-terminal-cloud/internal/opsconfig"
|
||||
)
|
||||
|
||||
func CreateOpsConfigVersionHandler(service *opsconfig.Service) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
principal, ok := webPrincipal(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tenantID := strings.TrimSpace(r.Header.Get("X-Tenant-Id"))
|
||||
key := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
||||
if !validUUID(tenantID) || len(key) < 16 || len(key) > 128 {
|
||||
writeValidation(w, r, "运营配置请求参数不正确", FieldError{Field: "headers", Message: "X-Tenant-Id和16至128字符的Idempotency-Key均为必填"})
|
||||
return
|
||||
}
|
||||
var body struct {
|
||||
ConfigKey string `json:"config_key"`
|
||||
DisplayName string `json:"display_name"`
|
||||
ConfigKind string `json:"config_kind"`
|
||||
Payload map[string]any `json:"payload"`
|
||||
SecretRefs map[string]string `json:"secret_refs"`
|
||||
ChangeSummary string `json:"change_summary"`
|
||||
}
|
||||
if !decodeWebJSON(w, r, &body, "运营配置草稿参数不正确") {
|
||||
return
|
||||
}
|
||||
item, created, err := service.Create(r.Context(), principal, key, RequestID(r.Context()), opsconfig.CreateInput{TenantID: tenantID, ConfigKey: body.ConfigKey, DisplayName: body.DisplayName, ConfigKind: body.ConfigKind, Payload: body.Payload, SecretRefs: body.SecretRefs, ChangeSummary: body.ChangeSummary})
|
||||
if err != nil {
|
||||
writeOpsConfigError(w, r, err)
|
||||
return
|
||||
}
|
||||
status := http.StatusCreated
|
||||
if !created {
|
||||
status = http.StatusOK
|
||||
w.Header().Set("Idempotency-Replayed", "true")
|
||||
}
|
||||
w.Header().Set("ETag", concurrency.FormatETag(item.RowVersion))
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
WriteJSON(w, r, status, item)
|
||||
})
|
||||
}
|
||||
|
||||
func ListOpsConfigVersionsHandler(service *opsconfig.Service) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
principal, ok := webPrincipal(w, r)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tenantID := strings.TrimSpace(r.Header.Get("X-Tenant-Id"))
|
||||
if !validUUID(tenantID) {
|
||||
writeValidation(w, r, "运营配置查询参数不正确", FieldError{Field: "X-Tenant-Id", Message: "必须是有效UUID"})
|
||||
return
|
||||
}
|
||||
items, err := service.List(r.Context(), principal, tenantID, r.URL.Query().Get("config_key"))
|
||||
if err != nil {
|
||||
writeOpsConfigError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
WriteJSON(w, r, http.StatusOK, map[string]any{"items": items})
|
||||
})
|
||||
}
|
||||
|
||||
func SubmitOpsConfigVersionHandler(service *opsconfig.Service) http.Handler {
|
||||
return opsConfigTransitionHandler(func(r *http.Request, principalInput opsPrincipalInput, expected int64) (opsconfig.Version, error) {
|
||||
return service.Submit(r.Context(), principalInput.principal, principalInput.tenantID, r.PathValue("id"), RequestID(r.Context()), expected)
|
||||
})
|
||||
}
|
||||
|
||||
func PublishOpsConfigVersionHandler(service *opsconfig.Service) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
principal, ok := resourcePrincipal(w, r, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tenantID := strings.TrimSpace(r.Header.Get("X-Tenant-Id"))
|
||||
if !validUUID(tenantID) {
|
||||
writeValidation(w, r, "运营配置发布参数不正确", FieldError{Field: "X-Tenant-Id", Message: "必须是有效UUID"})
|
||||
return
|
||||
}
|
||||
expected, err := concurrency.ParseIfMatch(r)
|
||||
if err != nil {
|
||||
writeETagError(w, r, err)
|
||||
return
|
||||
}
|
||||
var body opsconfig.PublishInput
|
||||
if !decodeWebJSON(w, r, &body, "配置发布参数不正确") {
|
||||
return
|
||||
}
|
||||
item, err := service.Publish(r.Context(), principal, tenantID, r.PathValue("id"), RequestID(r.Context()), body, expected)
|
||||
if err != nil {
|
||||
writeOpsConfigError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("ETag", concurrency.FormatETag(item.RowVersion))
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
WriteJSON(w, r, http.StatusOK, item)
|
||||
})
|
||||
}
|
||||
|
||||
func RollbackOpsConfigVersionHandler(service *opsconfig.Service) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
principal, ok := resourcePrincipal(w, r, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tenantID := strings.TrimSpace(r.Header.Get("X-Tenant-Id"))
|
||||
if !validUUID(tenantID) {
|
||||
writeValidation(w, r, "运营配置回滚参数不正确", FieldError{Field: "X-Tenant-Id", Message: "必须是有效UUID"})
|
||||
return
|
||||
}
|
||||
key := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
|
||||
if len(key) < 16 || len(key) > 128 {
|
||||
writeValidation(w, r, "运营配置回滚参数不正确", FieldError{Field: "Idempotency-Key", Message: "长度必须为16至128字符"})
|
||||
return
|
||||
}
|
||||
var body opsconfig.PublishInput
|
||||
if !decodeWebJSON(w, r, &body, "配置回滚参数不正确") {
|
||||
return
|
||||
}
|
||||
item, created, err := service.Rollback(r.Context(), principal, tenantID, r.PathValue("id"), key, RequestID(r.Context()), body)
|
||||
if err != nil {
|
||||
writeOpsConfigError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("ETag", concurrency.FormatETag(item.RowVersion))
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
status := http.StatusCreated
|
||||
if !created {
|
||||
status = http.StatusOK
|
||||
w.Header().Set("Idempotency-Replayed", "true")
|
||||
}
|
||||
WriteJSON(w, r, status, item)
|
||||
})
|
||||
}
|
||||
|
||||
type opsPrincipalInput struct {
|
||||
principal interfacePrincipal
|
||||
tenantID string
|
||||
}
|
||||
|
||||
// interfacePrincipal is aliased below through a concrete adapter to keep the transition
|
||||
// helper small without weakening the service's identity type.
|
||||
type interfacePrincipal = identity.Principal
|
||||
|
||||
func opsConfigTransitionHandler(transition func(*http.Request, opsPrincipalInput, int64) (opsconfig.Version, error)) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
principal, ok := resourcePrincipal(w, r, "id")
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
tenantID := strings.TrimSpace(r.Header.Get("X-Tenant-Id"))
|
||||
if !validUUID(tenantID) {
|
||||
writeValidation(w, r, "运营配置状态变更参数不正确", FieldError{Field: "X-Tenant-Id", Message: "必须是有效UUID"})
|
||||
return
|
||||
}
|
||||
expected, err := concurrency.ParseIfMatch(r)
|
||||
if err != nil {
|
||||
writeETagError(w, r, err)
|
||||
return
|
||||
}
|
||||
item, err := transition(r, opsPrincipalInput{principal: principal, tenantID: tenantID}, expected)
|
||||
if err != nil {
|
||||
writeOpsConfigError(w, r, err)
|
||||
return
|
||||
}
|
||||
w.Header().Set("ETag", concurrency.FormatETag(item.RowVersion))
|
||||
w.Header().Set("Cache-Control", "no-store")
|
||||
WriteJSON(w, r, http.StatusOK, item)
|
||||
})
|
||||
}
|
||||
|
||||
func writeOpsConfigError(w http.ResponseWriter, r *http.Request, err error) {
|
||||
switch {
|
||||
case errors.Is(err, opsconfig.ErrInvalid):
|
||||
writeValidation(w, r, "运营配置参数或结构校验未通过", FieldError{Field: "config", Message: "字段、资源引用、密钥引用或配置结构不符合runtime-config-v1"})
|
||||
case errors.Is(err, opsconfig.ErrAccessDenied):
|
||||
WriteError(w, r, http.StatusForbidden, "OPS_ACCESS_DENIED", "当前账号没有所需的内部运营权限")
|
||||
case errors.Is(err, opsconfig.ErrNotFound):
|
||||
WriteError(w, r, http.StatusNotFound, "RESOURCE_NOT_FOUND", "运营配置版本不存在")
|
||||
case errors.Is(err, opsconfig.ErrVersionConflict):
|
||||
WriteError(w, r, http.StatusPreconditionFailed, "VERSION_CONFLICT", "配置版本已变化,请重新读取后再操作")
|
||||
case errors.Is(err, opsconfig.ErrSelfApproval):
|
||||
WriteError(w, r, http.StatusConflict, "SELF_APPROVAL_FORBIDDEN", "配置创建人不能审批并发布自己的变更")
|
||||
case errors.Is(err, opsconfig.ErrStateConflict):
|
||||
WriteError(w, r, http.StatusConflict, "CONFIG_STATE_CONFLICT", "当前配置状态不允许执行该操作")
|
||||
case errors.Is(err, opsconfig.ErrPublishGate):
|
||||
WriteError(w, r, http.StatusConflict, "CONFIG_PUBLISH_GATE_FAILED", "配置缺少有效评测,或组件版本、资源与密钥引用未通过有效性检查")
|
||||
case errors.Is(err, idempotency.ErrKeyConflict):
|
||||
WriteError(w, r, http.StatusConflict, "IDEMPOTENCY_KEY_CONFLICT", "重复请求识别码已被不同请求使用")
|
||||
case errors.Is(err, idempotency.ErrInProgress):
|
||||
WriteError(w, r, http.StatusConflict, "IDEMPOTENCY_IN_PROGRESS", "相同请求正在处理中")
|
||||
default:
|
||||
WriteError(w, r, http.StatusInternalServerError, "INTERNAL_ERROR", "运营配置处理失败")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user