feat: register MySQL instances in CloudDM

This commit is contained in:
2026-07-29 18:24:36 +08:00
parent 33449b60b2
commit 0aac99c359
2 changed files with 126 additions and 6 deletions
+93 -6
View File
@@ -37,6 +37,7 @@ type MySQLDeliveryInput struct {
TargetID uint64 `json:"target_id" binding:"required"`
Namespace string `json:"namespace" binding:"required"`
InstanceName string `json:"instance_name" binding:"required"`
InstanceDesc string `json:"instance_desc,omitempty"`
MySQLVersion string `json:"mysql_version"`
Topology string `json:"topology"`
MySQLPort int `json:"mysql_port"`
@@ -815,6 +816,7 @@ func validateDeliveryInput(input MySQLDeliveryInput, _ []string) error {
func normalizeMySQLDeliveryInput(input *MySQLDeliveryInput) {
input.Namespace = normalizeDNSLabel(input.Namespace)
input.InstanceName = normalizeDNSLabel(input.InstanceName)
input.InstanceDesc = strings.TrimSpace(input.InstanceDesc)
input.TargetHost = strings.TrimSpace(input.TargetHost)
input.DataDisk = strings.TrimSpace(input.DataDisk)
if input.MySQLVersion == "" {
@@ -1971,24 +1973,109 @@ func (s *DeliveryService) RetryCloudDMRegistration(ctx context.Context, taskID s
return s.transition(ctx, &task, model.TaskFinished, "CloudDM registration completed", "")
}
type cloudDMRegisterRequest struct {
SourceSystem string `json:"sourceSystem"`
ResourceType string `json:"resourceType"`
ExternalResourceID string `json:"externalResourceId"`
DataSource cloudDMDataSource `json:"dataSource"`
}
type cloudDMDataSource struct {
InstanceName string `json:"instanceName"`
InstanceDesc string `json:"instanceDesc"`
DSType string `json:"dsType"`
Host string `json:"host"`
SecurityType string `json:"securityType"`
UserName string `json:"userName"`
Password string `json:"password"`
DefaultSchema any `json:"defaultSchema"`
ClientTimeZone string `json:"clientTimeZone"`
ConnectTimeoutMs int `json:"connectTimeoutMs"`
SocketTimeoutSecs int `json:"socketTimeoutSeconds"`
ConnectionCharset string `json:"connectionCharset"`
}
func cloudDMExternalResourceID(instanceID uint64) string {
return "mysql-instance:" + strconv.FormatUint(instanceID, 10)
}
func cloudDMClientTimeZone(value string) string {
value = strings.TrimSpace(value)
if value == "" {
return "Asia/Shanghai"
}
return value
}
func buildCloudDMRegisterRequest(instance model.DeploymentResult, payload deliveryPayload, password string) cloudDMRegisterRequest {
description := strings.TrimSpace(payload.InstanceDesc)
if description == "" {
description = instance.InstanceName
}
return cloudDMRegisterRequest{
SourceSystem: "xinfra",
ResourceType: "MYSQL_INSTANCE",
ExternalResourceID: cloudDMExternalResourceID(instance.ID),
DataSource: cloudDMDataSource{
InstanceName: instance.InstanceName,
InstanceDesc: description,
DSType: "MySQL",
Host: net.JoinHostPort(instance.Host, strconv.Itoa(instance.Port)),
SecurityType: "USER_PASSWD",
UserName: "root",
Password: password,
DefaultSchema: nil,
ClientTimeZone: cloudDMClientTimeZone(payload.Timezone),
ConnectTimeoutMs: 5000,
SocketTimeoutSecs: 10,
ConnectionCharset: "utf8",
},
}
}
func (s *DeliveryService) RegisterCloudDM(ctx context.Context, taskID string) error {
if s.cfg.CloudDMRegisterURL == "" {
if strings.TrimSpace(s.cfg.CloudDMRegisterURL) == "" {
return nil
}
if strings.TrimSpace(s.cfg.CloudDMAPIToken) == "" {
return fmt.Errorf("CloudDM registration is enabled but CLOUDDM_API_TOKEN is not configured")
}
var task model.DeliveryTask
if err := s.db.WithContext(ctx).First(&task, "id = ?", taskID).Error; err != nil {
return err
}
var payload deliveryPayload
if err := json.Unmarshal([]byte(task.ImmutablePayload), &payload); err != nil {
return fmt.Errorf("decode delivery payload: %w", err)
}
var instance model.DeploymentResult
if err := s.db.WithContext(ctx).Where("task_id = ? AND component = ? AND service_type = ?", taskID, "mysql", "database").First(&instance).Error; err != nil {
return err
}
body := map[string]any{"name": instance.InstanceName, "host": instance.Host, "port": instance.Port, "username": "root", "database_type": "mysql"}
raw, _ := json.Marshal(body)
var credential model.DeploymentCredential
if err := s.db.WithContext(ctx).
Where("task_id = ? AND username = ? AND account_host = ? AND status IN ?", taskID, "root", "localhost", []string{"pending", "available"}).
First(&credential).Error; err != nil {
return fmt.Errorf("CloudDM registration requires the MySQL root credential: %w", err)
}
password, err := decryptCredential(s.cfg.DeliveryCredentialSecret, credential.Ciphertext, credential.Nonce)
if err != nil {
return fmt.Errorf("decrypt MySQL root credential for CloudDM registration: %w", err)
}
body := buildCloudDMRegisterRequest(instance, payload, password)
raw, err := json.Marshal(body)
if err != nil {
return fmt.Errorf("encode CloudDM registration request: %w", err)
}
req, err := http.NewRequestWithContext(ctx, http.MethodPost, s.cfg.CloudDMRegisterURL, bytes.NewReader(raw))
if err != nil {
return err
}
// X-Request-ID is a trace identifier, so generate a fresh UUID for every
// HTTP call. externalResourceId above is the stable business idempotency key.
req.Header.Set("Authorization", "Bearer "+strings.TrimSpace(s.cfg.CloudDMAPIToken))
req.Header.Set("Content-Type", "application/json")
if s.cfg.CloudDMAPIToken != "" {
req.Header.Set("Authorization", "Bearer "+s.cfg.CloudDMAPIToken)
}
req.Header.Set("X-Request-ID", randomUUID())
resp, err := http.DefaultClient.Do(req)
if err != nil {
return err