feat(delivery): refine base service integrations
This commit is contained in:
@@ -18,6 +18,7 @@ func AutoMigrate(db *gorm.DB) error {
|
||||
&model.BusinessLine{},
|
||||
&model.BusinessLineUser{},
|
||||
&model.BusinessLineWayneNamespace{},
|
||||
&model.BusinessLineSinaOrganization{},
|
||||
&model.AccessToken{},
|
||||
&model.WayneToken{},
|
||||
&model.AuditLog{},
|
||||
|
||||
@@ -1,12 +1,19 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/1024XEngineer/xinfra/server/internal/config"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/model"
|
||||
"github.com/1024XEngineer/xinfra/server/internal/service"
|
||||
|
||||
@@ -15,8 +22,10 @@ import (
|
||||
)
|
||||
|
||||
type BusinessLineHandler struct {
|
||||
db *gorm.DB
|
||||
wayne *service.WayneRoleBindingService
|
||||
cfg config.Config
|
||||
db *gorm.DB
|
||||
wayne *service.WayneRoleBindingService
|
||||
httpClient *http.Client
|
||||
}
|
||||
|
||||
type BusinessLineWithPermission struct {
|
||||
@@ -47,8 +56,24 @@ type WayneNamespaceBindingItem struct {
|
||||
KubeNamespace string `json:"kubeNamespace"`
|
||||
}
|
||||
|
||||
func NewBusinessLineHandler(db *gorm.DB, wayne *service.WayneRoleBindingService) *BusinessLineHandler {
|
||||
return &BusinessLineHandler{db: db, wayne: wayne}
|
||||
type SinaOrganizationBindingPayload struct {
|
||||
Organizations []SinaOrganizationBindingItem `json:"organizations"`
|
||||
}
|
||||
|
||||
type SinaOrganizationBindingItem struct {
|
||||
ID string `json:"id" binding:"required"`
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func NewBusinessLineHandler(cfg config.Config, db *gorm.DB, wayne *service.WayneRoleBindingService) *BusinessLineHandler {
|
||||
return &BusinessLineHandler{
|
||||
cfg: cfg,
|
||||
db: db,
|
||||
wayne: wayne,
|
||||
httpClient: &http.Client{
|
||||
Timeout: 10 * time.Second,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListCurrentUserBusinessLines(c *gin.Context) {
|
||||
@@ -405,6 +430,250 @@ func (h *BusinessLineHandler) ReplaceWayneNamespaces(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListSinaOrganizations(c *gin.Context) {
|
||||
businessLineID, ok := parseBusinessLineID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.canManageBusinessLine(c, businessLineID) {
|
||||
return
|
||||
}
|
||||
|
||||
token, err := h.loginSina(c.Request.Context())
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
items, err := h.fetchSinaOrganizations(c.Request.Context(), token, c.Query("keyword"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadGateway, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ListSinaOrganizationMappings(c *gin.Context) {
|
||||
businessLineID, ok := parseBusinessLineID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.canManageBusinessLine(c, businessLineID) {
|
||||
return
|
||||
}
|
||||
|
||||
var rows []model.BusinessLineSinaOrganization
|
||||
if err := h.db.Where("business_line_id = ?", businessLineID).Order("sina_organization_name ASC").Find(&rows).Error; err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
items := make([]gin.H, 0, len(rows))
|
||||
for _, row := range rows {
|
||||
items = append(items, gin.H{
|
||||
"id": row.SinaOrganizationID,
|
||||
"name": row.SinaOrganizationName,
|
||||
})
|
||||
}
|
||||
c.JSON(http.StatusOK, gin.H{"items": items})
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) ReplaceSinaOrganizationMappings(c *gin.Context) {
|
||||
businessLineID, ok := parseBusinessLineID(c)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if !h.canManageBusinessLine(c, businessLineID) {
|
||||
return
|
||||
}
|
||||
|
||||
var req SinaOrganizationBindingPayload
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.db.Transaction(func(tx *gorm.DB) error {
|
||||
if err := tx.Where("business_line_id = ?", businessLineID).Delete(&model.BusinessLineSinaOrganization{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
seen := map[string]struct{}{}
|
||||
for _, item := range req.Organizations {
|
||||
id := strings.TrimSpace(item.ID)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
row := model.BusinessLineSinaOrganization{
|
||||
BusinessLineID: businessLineID,
|
||||
SinaOrganizationID: id,
|
||||
SinaOrganizationName: strings.TrimSpace(item.Name),
|
||||
}
|
||||
if err := tx.Create(&row).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}); err != nil {
|
||||
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"ok": true})
|
||||
}
|
||||
|
||||
type sinaBusinessLoginResp struct {
|
||||
Success bool `json:"success"`
|
||||
Result map[string]interface{} `json:"result"`
|
||||
Data map[string]interface{} `json:"data"`
|
||||
Token string `json:"token"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
type sinaOrganizationListResp struct {
|
||||
Success bool `json:"success"`
|
||||
Result struct {
|
||||
Items []map[string]interface{} `json:"items"`
|
||||
Count int64 `json:"count"`
|
||||
} `json:"result"`
|
||||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) loginSina(ctx context.Context) (string, error) {
|
||||
username := strings.TrimSpace(h.cfg.SINAUsername)
|
||||
password := strings.TrimSpace(h.cfg.SINAPassword)
|
||||
if username == "" || password == "" {
|
||||
return "", errors.New("SINA_USERNAME or SINA_PASSWORD is not configured")
|
||||
}
|
||||
payload, err := json.Marshal(map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, h.cfg.SINABaseURL+"/sinai/v1/login", bytes.NewReader(payload))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("content-type", "application/json")
|
||||
|
||||
resp, err := h.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("sina login failed: status=%d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
var parsed sinaBusinessLoginResp
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !parsed.Success {
|
||||
return "", fmt.Errorf("sina login failed: %s", parsed.Message)
|
||||
}
|
||||
token := firstNonEmptyString(
|
||||
parsed.Token,
|
||||
sinaStringValue(parsed.Result["token"]),
|
||||
sinaStringValue(parsed.Result["access_token"]),
|
||||
sinaStringValue(parsed.Data["token"]),
|
||||
sinaStringValue(parsed.Data["access_token"]),
|
||||
)
|
||||
if token == "" {
|
||||
return "", errors.New("sina login response missing token")
|
||||
}
|
||||
return token, nil
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) fetchSinaOrganizations(ctx context.Context, token string, keyword string) ([]SinaOrganizationBindingItem, error) {
|
||||
const size = 100
|
||||
page := 1
|
||||
items := make([]SinaOrganizationBindingItem, 0)
|
||||
for {
|
||||
values := url.Values{}
|
||||
values.Set("ciClsName", "zion_organization")
|
||||
values.Set("keyword", keyword)
|
||||
values.Set("page", strconv.Itoa(page))
|
||||
values.Set("size", strconv.Itoa(size))
|
||||
values.Set("isAccurate", "false")
|
||||
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, h.cfg.SINABaseURL+"/sinai/v1/ci?"+values.Encode(), nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
req.Header.Set("Authorization", token)
|
||||
req.Header.Set("Accept", "application/json")
|
||||
|
||||
resp, err := h.httpClient.Do(req)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
body, readErr := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
if readErr != nil {
|
||||
return nil, readErr
|
||||
}
|
||||
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
||||
return nil, fmt.Errorf("sina organization list failed: status=%d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var parsed sinaOrganizationListResp
|
||||
if err := json.Unmarshal(body, &parsed); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !parsed.Success {
|
||||
return nil, fmt.Errorf("sina organization list failed: %s", parsed.Message)
|
||||
}
|
||||
for _, row := range parsed.Result.Items {
|
||||
id := firstNonEmptyString(sinaStringValue(row["id"]), sinaStringValue(row["ciId"]))
|
||||
name := firstNonEmptyString(
|
||||
sinaStringValue(row["name"]),
|
||||
sinaStringValue(row["org_name"]),
|
||||
sinaStringValue(row["title"]),
|
||||
id,
|
||||
)
|
||||
if id == "" {
|
||||
continue
|
||||
}
|
||||
items = append(items, SinaOrganizationBindingItem{ID: id, Name: name})
|
||||
}
|
||||
if len(parsed.Result.Items) < size || int64(len(items)) >= parsed.Result.Count {
|
||||
break
|
||||
}
|
||||
page++
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func sinaStringValue(value interface{}) string {
|
||||
switch v := value.(type) {
|
||||
case string:
|
||||
return strings.TrimSpace(v)
|
||||
case fmt.Stringer:
|
||||
return strings.TrimSpace(v.String())
|
||||
case nil:
|
||||
return ""
|
||||
default:
|
||||
return strings.TrimSpace(fmt.Sprint(v))
|
||||
}
|
||||
}
|
||||
|
||||
func firstNonEmptyString(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (h *BusinessLineHandler) canManageBusinessLine(c *gin.Context, businessLineID uint64) bool {
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
|
||||
@@ -171,7 +171,7 @@ func (h *DeliveryHandler) RevealCredentials(c *gin.Context) {
|
||||
}
|
||||
items, err := h.service.RevealDeploymentCredentials(c.Request.Context(), c.Param("id"), claims.UserID, claims.IsAdmin)
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "credentials not found or already viewed"})
|
||||
c.JSON(http.StatusNotFound, gin.H{"error": "credentials not found or already claimed"})
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
@@ -357,7 +357,7 @@ func (h *DeliveryHandler) TargetHostMountPaths(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid target_id"})
|
||||
return
|
||||
}
|
||||
items, err := h.service.ListHostMountPaths(c.Request.Context(), targetID, c.Param("host"))
|
||||
items, err := h.service.ListHostMountPaths(c.Request.Context(), targetID, c.Param("host"), c.Query("prefix"))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||
return
|
||||
|
||||
@@ -18,7 +18,17 @@ func NewMachineHandler(machines *service.MachineService) *MachineHandler {
|
||||
}
|
||||
|
||||
func (h *MachineHandler) Overview(c *gin.Context) {
|
||||
overview, err := h.machines.Overview(c.Request.Context())
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return
|
||||
}
|
||||
businessLineID, ok := queryUint(c, "business_line_id")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "business_line_id is required"})
|
||||
return
|
||||
}
|
||||
overview, err := h.machines.Overview(c.Request.Context(), claims.UserID, claims.IsAdmin, businessLineID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
@@ -27,18 +37,28 @@ func (h *MachineHandler) Overview(c *gin.Context) {
|
||||
}
|
||||
|
||||
func (h *MachineHandler) List(c *gin.Context) {
|
||||
resources, err := h.machines.List(c.Request.Context(), service.MachineListQuery{
|
||||
Page: queryInt(c, "page", 1),
|
||||
Size: queryInt(c, "size", 20),
|
||||
Hostname: c.Query("hostname"),
|
||||
AssetNumber: c.Query("assetNumber"),
|
||||
Type: c.Query("type"),
|
||||
Location: c.Query("location"),
|
||||
IP: c.Query("ip"),
|
||||
Spec: c.Query("spec"),
|
||||
BusinessLine: c.Query("businessLine"),
|
||||
Source: c.Query("source"),
|
||||
Status: c.Query("status"),
|
||||
claims, ok := CurrentClaims(c)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
|
||||
return
|
||||
}
|
||||
businessLineID, ok := queryUint(c, "business_line_id")
|
||||
if !ok {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "business_line_id is required"})
|
||||
return
|
||||
}
|
||||
resources, err := h.machines.List(c.Request.Context(), claims.UserID, claims.IsAdmin, service.MachineListQuery{
|
||||
BusinessLineID: businessLineID,
|
||||
Page: queryInt(c, "page", 1),
|
||||
Size: queryInt(c, "size", 20),
|
||||
Hostname: c.Query("hostname"),
|
||||
AssetNumber: c.Query("assetNumber"),
|
||||
Type: c.Query("type"),
|
||||
Location: c.Query("location"),
|
||||
IP: c.Query("ip"),
|
||||
Spec: c.Query("spec"),
|
||||
Source: c.Query("source"),
|
||||
Status: c.Query("status"),
|
||||
})
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
@@ -67,3 +87,15 @@ func queryInt(c *gin.Context, key string, fallback int) int {
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func queryUint(c *gin.Context, key string) (uint64, bool) {
|
||||
value := c.Query(key)
|
||||
if value == "" {
|
||||
return 0, false
|
||||
}
|
||||
n, err := strconv.ParseUint(value, 10, 64)
|
||||
if err != nil || n == 0 {
|
||||
return 0, false
|
||||
}
|
||||
return n, true
|
||||
}
|
||||
|
||||
@@ -34,26 +34,27 @@ type ResourceQuota struct {
|
||||
}
|
||||
|
||||
type DeliveryTask struct {
|
||||
ID string `gorm:"size:36;primaryKey" json:"id"`
|
||||
BusinessLineID uint64 `gorm:"not null;index" json:"business_line_id"`
|
||||
RequestedBy uint64 `gorm:"not null;index" json:"requested_by"`
|
||||
Component string `gorm:"size:32;not null;default:mysql;index" json:"component"`
|
||||
TargetType string `gorm:"size:32;not null" json:"target_type"`
|
||||
TargetID uint64 `gorm:"not null;index" json:"target_id"`
|
||||
Namespace string `gorm:"size:63;not null;index" json:"namespace"`
|
||||
InstanceName string `gorm:"size:63;not null" json:"instance_name"`
|
||||
TargetHost string `gorm:"size:128" json:"target_host,omitempty"`
|
||||
TargetHostIP string `gorm:"size:64" json:"target_host_ip,omitempty"`
|
||||
MySQLPort int `gorm:"column:mysql_port;not null;default:3307" json:"mysql_port"`
|
||||
Status string `gorm:"size:32;not null;index" json:"status"`
|
||||
ImmutablePayload string `gorm:"type:json;not null" json:"immutable_payload"`
|
||||
PayloadHash string `gorm:"size:64;not null" json:"payload_hash"`
|
||||
IdempotencyKey string `gorm:"size:128;not null;uniqueIndex" json:"idempotency_key"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message,omitempty"`
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||
ID string `gorm:"size:36;primaryKey" json:"id"`
|
||||
BusinessLineID uint64 `gorm:"not null;index" json:"business_line_id"`
|
||||
RequestedBy uint64 `gorm:"not null;index" json:"requested_by"`
|
||||
Component string `gorm:"size:32;not null;default:mysql;index" json:"component"`
|
||||
TargetType string `gorm:"size:32;not null" json:"target_type"`
|
||||
TargetID uint64 `gorm:"not null;index" json:"target_id"`
|
||||
Namespace string `gorm:"size:63;not null;index" json:"namespace"`
|
||||
InstanceName string `gorm:"size:63;not null" json:"instance_name"`
|
||||
TargetHost string `gorm:"size:128" json:"target_host,omitempty"`
|
||||
TargetHostIP string `gorm:"size:64" json:"target_host_ip,omitempty"`
|
||||
MySQLPort int `gorm:"column:mysql_port;not null;default:3307" json:"mysql_port"`
|
||||
Status string `gorm:"size:32;not null;index" json:"status"`
|
||||
ImmutablePayload string `gorm:"type:json;not null" json:"immutable_payload"`
|
||||
PayloadHash string `gorm:"size:64;not null" json:"payload_hash"`
|
||||
IdempotencyKey string `gorm:"size:128;not null;uniqueIndex" json:"idempotency_key"`
|
||||
ErrorMessage string `gorm:"type:text" json:"error_message,omitempty"`
|
||||
CreatedAt time.Time `gorm:"index" json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
StartedAt *time.Time `json:"started_at,omitempty"`
|
||||
FinishedAt *time.Time `json:"finished_at,omitempty"`
|
||||
CredentialAvailable bool `gorm:"-" json:"credential_available"`
|
||||
}
|
||||
|
||||
type ResourceReservation struct {
|
||||
|
||||
@@ -58,6 +58,15 @@ type BusinessLineWayneNamespace struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type BusinessLineSinaOrganization struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
BusinessLineID uint64 `gorm:"not null;uniqueIndex:idx_business_line_sina_orgs_unique,priority:1;index" json:"business_line_id"`
|
||||
SinaOrganizationID string `gorm:"size:128;not null;uniqueIndex:idx_business_line_sina_orgs_unique,priority:2" json:"sina_organization_id"`
|
||||
SinaOrganizationName string `gorm:"size:255;not null;default:''" json:"sina_organization_name"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
type AccessToken struct {
|
||||
ID uint64 `gorm:"primaryKey" json:"id"`
|
||||
UserID uint64 `gorm:"not null;index" json:"user_id"`
|
||||
|
||||
@@ -81,7 +81,7 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
||||
healthHandler := handler.NewHealthHandler(deps.DB)
|
||||
authHandler := handler.NewAuthHandler(deps.Config, authService)
|
||||
userHandler := handler.NewUserHandler(deps.DB)
|
||||
businessLineHandler := handler.NewBusinessLineHandler(deps.DB, wayneRoleBindingService)
|
||||
businessLineHandler := handler.NewBusinessLineHandler(deps.Config, deps.DB, wayneRoleBindingService)
|
||||
wayenHandler := handler.NewWayenHandler(deps.DB, wayenService, auditService)
|
||||
wayneRoleBindingHandler := handler.NewWayneRoleBindingHandler(wayneRoleBindingService, auditService)
|
||||
subsystemAuthHandler := handler.NewSubsystemAuthHandler(deps.DB, wayneRoleBindingService, auditService)
|
||||
@@ -125,6 +125,9 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
|
||||
protected.POST("/business-lines/authorizations", businessLineHandler.GrantPermission)
|
||||
protected.GET("/business-lines/:id/wayne-namespaces", businessLineHandler.ListWayneNamespaces)
|
||||
protected.PUT("/business-lines/:id/wayne-namespaces", businessLineHandler.ReplaceWayneNamespaces)
|
||||
protected.GET("/business-lines/:id/sina-organizations", businessLineHandler.ListSinaOrganizations)
|
||||
protected.GET("/business-lines/:id/sina-organization-mappings", businessLineHandler.ListSinaOrganizationMappings)
|
||||
protected.PUT("/business-lines/:id/sina-organization-mappings", businessLineHandler.ReplaceSinaOrganizationMappings)
|
||||
protected.GET("/wayen/login", wayenHandler.Login)
|
||||
protected.GET("/wayen/credential", wayenHandler.GetCredential)
|
||||
protected.PUT("/wayen/credential", wayenHandler.SaveCredential)
|
||||
|
||||
@@ -129,9 +129,13 @@ type MySQLServiceLedgerItem struct {
|
||||
}
|
||||
|
||||
type DeploymentCredentialView struct {
|
||||
Username string `json:"username"`
|
||||
Host string `json:"host"`
|
||||
Password string `json:"password"`
|
||||
Service string `json:"service"`
|
||||
InstanceName string `json:"instance_name"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Username string `json:"username"`
|
||||
AccountHost string `json:"account_host"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
// targetMetadata describes the native VM候选节点池以及部署形态,由 AWX inventory hosts 动态组装。
|
||||
@@ -397,13 +401,19 @@ func (s *DeliveryService) getTarget(ctx context.Context, templateID uint64) (Del
|
||||
return s.awxDeliveryTarget(ctx, *template)
|
||||
}
|
||||
|
||||
func (s *DeliveryService) ListHostMountPaths(ctx context.Context, targetID uint64, hostName string) ([]DeliveryMountPath, error) {
|
||||
func (s *DeliveryService) ListHostMountPaths(ctx context.Context, targetID uint64, hostName string, prefix string) ([]DeliveryMountPath, error) {
|
||||
if targetID == 0 {
|
||||
return nil, fmt.Errorf("target_id is required")
|
||||
}
|
||||
if hostName == "" || len(hostName) > 253 || !hostNamePattern.MatchString(hostName) {
|
||||
return nil, fmt.Errorf("host must be a valid inventory host name")
|
||||
}
|
||||
prefix = strings.TrimSpace(prefix)
|
||||
if prefix != "" {
|
||||
if err := validateDirectoryLookupPrefix(prefix); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
template, err := s.awx.GetJobTemplate(ctx, targetID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("deployment target is unavailable: %w", err)
|
||||
@@ -423,9 +433,16 @@ func (s *DeliveryService) ListHostMountPaths(ctx context.Context, targetID uint6
|
||||
return nil, fmt.Errorf("host %q is not in the deployment target inventory", hostName)
|
||||
}
|
||||
if s.cfg.AWXFactsTemplateID != 0 {
|
||||
if err := s.refreshHostFacts(ctx, hostName); err != nil {
|
||||
stdout, err := s.refreshHostFacts(ctx, hostName, prefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if prefix != "" {
|
||||
return directoryPathsFromAWXStdout(stdout), nil
|
||||
}
|
||||
}
|
||||
if prefix != "" {
|
||||
return []DeliveryMountPath{}, nil
|
||||
}
|
||||
facts, err := s.awx.GetHostFacts(ctx, matched.ID)
|
||||
if err != nil {
|
||||
@@ -438,21 +455,86 @@ func (s *DeliveryService) ListHostMountPaths(ctx context.Context, targetID uint6
|
||||
return items, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) refreshHostFacts(ctx context.Context, hostName string) error {
|
||||
func validateDirectoryLookupPrefix(prefix string) error {
|
||||
if prefix == "" {
|
||||
return nil
|
||||
}
|
||||
if len(prefix) > 512 {
|
||||
return fmt.Errorf("path prefix is too long")
|
||||
}
|
||||
if !strings.HasPrefix(prefix, "/") {
|
||||
return fmt.Errorf("path prefix must be an absolute path")
|
||||
}
|
||||
if strings.ContainsRune(prefix, 0) {
|
||||
return fmt.Errorf("path prefix contains invalid characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func directoryPathsFromAWXStdout(stdout string) []DeliveryMountPath {
|
||||
const marker = "XINFRA_PATH_COMPLETIONS_JSON="
|
||||
for _, line := range strings.Split(stdout, "\n") {
|
||||
line = strings.TrimSpace(line)
|
||||
idx := strings.Index(line, marker)
|
||||
if idx < 0 {
|
||||
continue
|
||||
}
|
||||
if items, ok := parseDirectoryCompletionJSON(line[idx+len(marker):]); ok {
|
||||
sort.Slice(items, func(i, j int) bool {
|
||||
return items[i].Path < items[j].Path
|
||||
})
|
||||
return items
|
||||
}
|
||||
}
|
||||
return []DeliveryMountPath{}
|
||||
}
|
||||
|
||||
func parseDirectoryCompletionJSON(raw string) ([]DeliveryMountPath, bool) {
|
||||
raw = strings.TrimSpace(raw)
|
||||
candidates := []string{raw}
|
||||
if strings.Contains(raw, `\"`) {
|
||||
candidates = append(candidates, strings.ReplaceAll(raw, `\"`, `"`))
|
||||
}
|
||||
for _, candidate := range candidates {
|
||||
start := strings.Index(candidate, "[")
|
||||
end := strings.LastIndex(candidate, "]")
|
||||
if start < 0 || end < start {
|
||||
continue
|
||||
}
|
||||
var items []DeliveryMountPath
|
||||
if err := json.Unmarshal([]byte(candidate[start:end+1]), &items); err == nil {
|
||||
return items, true
|
||||
}
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
|
||||
func (s *DeliveryService) refreshHostFacts(ctx context.Context, hostName string, lookupPath string) (string, error) {
|
||||
extraVars := map[string]any{
|
||||
"target_hosts": hostName,
|
||||
}
|
||||
if lookupPath != "" {
|
||||
extraVars["lookup_path"] = lookupPath
|
||||
}
|
||||
job, err := s.awx.Launch(ctx, s.cfg.AWXFactsTemplateID, AWXLaunchRequest{
|
||||
Limit: hostName,
|
||||
Limit: hostName,
|
||||
ExtraVars: extraVars,
|
||||
})
|
||||
if err != nil {
|
||||
return fmt.Errorf("launch AWX facts job: %w", err)
|
||||
return "", fmt.Errorf("launch AWX facts job: %w", err)
|
||||
}
|
||||
done, err := s.awx.WaitJob(ctx, strconv.FormatUint(job.ID, 10), time.Duration(s.cfg.AWXFactsTimeoutSeconds)*time.Second)
|
||||
if err != nil {
|
||||
return err
|
||||
return "", err
|
||||
}
|
||||
if done.Status != "successful" || done.Failed {
|
||||
return fmt.Errorf("AWX facts job %d finished with status %s", done.ID, done.Status)
|
||||
return "", fmt.Errorf("AWX facts job %d finished with status %s", done.ID, done.Status)
|
||||
}
|
||||
return nil
|
||||
stdout, err := s.awx.JobStdout(ctx, strconv.FormatUint(job.ID, 10))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return stdout, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) awxDeliveryTarget(ctx context.Context, template AWXJobTemplate) (DeliveryTarget, error) {
|
||||
@@ -503,6 +585,9 @@ func (s *DeliveryService) CreateTask(ctx context.Context, userID uint64, isAdmin
|
||||
if len(credentialInput["root@localhost"]) < 16 || len(credentialInput["xinfra_admin@%"]) < 16 {
|
||||
return nil, false, fmt.Errorf("mysql passwords must be at least 16 characters")
|
||||
}
|
||||
if !mysqlPasswordPattern.MatchString(credentialInput["root@localhost"]) || !mysqlPasswordPattern.MatchString(credentialInput["xinfra_admin@%"]) {
|
||||
return nil, false, fmt.Errorf("mysql passwords may only contain letters and digits")
|
||||
}
|
||||
}
|
||||
input.MySQLRootPassword = ""
|
||||
input.MySQLAdminPassword = ""
|
||||
@@ -657,6 +742,8 @@ var (
|
||||
// timezone 仅接受偏移量(±HH:MM)、SYSTEM 或命名时区(如 Asia/Shanghai)。
|
||||
var timezonePattern = regexp.MustCompile(`^([+-](0\d|1[0-4]):[0-5]\d|SYSTEM|[A-Za-z]+(?:/[A-Za-z0-9_+-]+)+)$`)
|
||||
|
||||
var mysqlPasswordPattern = regexp.MustCompile(`^[A-Za-z0-9]+$`)
|
||||
|
||||
func validateDeliveryInput(input MySQLDeliveryInput, _ []string) error {
|
||||
if len(input.Namespace) > 63 || !dnsLabelPattern.MatchString(input.Namespace) {
|
||||
return fmt.Errorf("namespace must be a valid Kubernetes DNS label")
|
||||
@@ -773,7 +860,13 @@ func (s *DeliveryService) ListTasks(ctx context.Context, userID uint64, isAdmin
|
||||
query = query.Where("business_line_id IN (?)", s.db.Model(&model.BusinessLineUser{}).Select("business_line_id").Where("user_id = ?", userID))
|
||||
}
|
||||
var tasks []model.DeliveryTask
|
||||
return tasks, query.Find(&tasks).Error
|
||||
if err := query.Find(&tasks).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.fillCredentialAvailability(ctx, tasks); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return tasks, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) ListMySQLServiceLedger(ctx context.Context, userID uint64, isAdmin bool, businessLineID uint64) ([]MySQLServiceLedgerItem, error) {
|
||||
@@ -916,6 +1009,11 @@ func (s *DeliveryService) GetTask(ctx context.Context, taskID string, userID uin
|
||||
if err := query.First(&task).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
taskItems := []model.DeliveryTask{task}
|
||||
if err := s.fillCredentialAvailability(ctx, taskItems); err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
task = taskItems[0]
|
||||
var events []model.TaskEvent
|
||||
if err := s.db.WithContext(ctx).Where("task_id = ?", taskID).Order("id ASC").Find(&events).Error; err != nil {
|
||||
return nil, nil, err
|
||||
@@ -923,6 +1021,41 @@ func (s *DeliveryService) GetTask(ctx context.Context, taskID string, userID uin
|
||||
return &task, events, nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) fillCredentialAvailability(ctx context.Context, tasks []model.DeliveryTask) error {
|
||||
if len(tasks) == 0 {
|
||||
return nil
|
||||
}
|
||||
taskIDs := make([]string, 0, len(tasks))
|
||||
for i := range tasks {
|
||||
if tasks[i].Status == model.TaskFinished || tasks[i].Status == model.TaskRegisterFailed {
|
||||
taskIDs = append(taskIDs, tasks[i].ID)
|
||||
}
|
||||
}
|
||||
if len(taskIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
var rows []struct {
|
||||
TaskID string
|
||||
Count int64
|
||||
}
|
||||
if err := s.db.WithContext(ctx).
|
||||
Model(&model.DeploymentCredential{}).
|
||||
Select("task_id, count(*) as count").
|
||||
Where("task_id IN ? AND status = ?", taskIDs, "available").
|
||||
Group("task_id").
|
||||
Scan(&rows).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
available := make(map[string]bool, len(rows))
|
||||
for _, row := range rows {
|
||||
available[row.TaskID] = row.Count > 0
|
||||
}
|
||||
for i := range tasks {
|
||||
tasks[i].CredentialAvailable = available[tasks[i].ID]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *DeliveryService) RevealDeploymentCredentials(ctx context.Context, taskID string, userID uint64, isAdmin bool) ([]DeploymentCredentialView, error) {
|
||||
task, _, err := s.GetTask(ctx, taskID, userID, isAdmin)
|
||||
if err != nil {
|
||||
@@ -933,6 +1066,10 @@ func (s *DeliveryService) RevealDeploymentCredentials(ctx context.Context, taskI
|
||||
}
|
||||
var out []DeploymentCredentialView
|
||||
err = s.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var result model.DeploymentResult
|
||||
if err := tx.Where("task_id = ?", taskID).First(&result).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return err
|
||||
}
|
||||
var credentials []model.DeploymentCredential
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).
|
||||
Where("task_id = ? AND status = ?", taskID, "available").
|
||||
@@ -949,22 +1086,20 @@ func (s *DeliveryService) RevealDeploymentCredentials(ctx context.Context, taskI
|
||||
return err
|
||||
}
|
||||
out = append(out, DeploymentCredentialView{
|
||||
Username: credential.Username,
|
||||
Host: credential.AccountHost,
|
||||
Password: password,
|
||||
Service: firstNonEmptyDeliveryValue(result.Component, credential.Component, task.Component, "mysql"),
|
||||
InstanceName: firstNonEmptyDeliveryValue(result.InstanceName, credential.InstanceName, task.InstanceName),
|
||||
Host: firstNonEmptyDeliveryValue(result.Host, task.TargetHostIP),
|
||||
Port: firstNonZero(result.Port, task.MySQLPort),
|
||||
Username: credential.Username,
|
||||
AccountHost: credential.AccountHost,
|
||||
Password: password,
|
||||
})
|
||||
}
|
||||
ids := make([]uint64, 0, len(credentials))
|
||||
for _, credential := range credentials {
|
||||
ids = append(ids, credential.ID)
|
||||
}
|
||||
now := time.Now()
|
||||
return tx.Model(&model.DeploymentCredential{}).Where("id IN ?", ids).Updates(map[string]any{
|
||||
"status": "viewed",
|
||||
"viewed_by": userID,
|
||||
"viewed_at": now,
|
||||
"updated_at": now,
|
||||
}).Error
|
||||
return tx.Unscoped().Where("id IN ?", ids).Delete(&model.DeploymentCredential{}).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -972,6 +1107,24 @@ func (s *DeliveryService) RevealDeploymentCredentials(ctx context.Context, taskI
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func firstNonEmptyDeliveryValue(values ...string) string {
|
||||
for _, value := range values {
|
||||
if strings.TrimSpace(value) != "" {
|
||||
return strings.TrimSpace(value)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func firstNonZero(values ...int) int {
|
||||
for _, value := range values {
|
||||
if value != 0 {
|
||||
return value
|
||||
}
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *DeliveryService) SubscribeTask(taskID string) (<-chan DeliveryTaskSnapshot, func()) {
|
||||
ch := make(chan DeliveryTaskSnapshot, 8)
|
||||
s.streamMu.Lock()
|
||||
@@ -1170,7 +1323,7 @@ func (s *DeliveryService) claimAndReserve(ctx context.Context) (*model.DeliveryT
|
||||
return s.failInTransaction(tx, &task, model.TaskValidationFailed, "deployment target has no candidate hosts")
|
||||
}
|
||||
var occupied []string
|
||||
occupiedExclude := []string{model.TaskExecutionFailed, model.TaskValidationFailed, model.TaskCanceled}
|
||||
occupiedExclude := allocationReleasedTaskStatuses()
|
||||
if err := tx.Model(&model.DeliveryTask{}).Where("target_id = ? AND target_host <> ? AND status NOT IN ?", task.TargetID, "", occupiedExclude).Pluck("target_host", &occupied).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -1641,6 +1794,18 @@ func terminalTaskStatuses() []string {
|
||||
}
|
||||
}
|
||||
|
||||
func allocationReleasedTaskStatuses() []string {
|
||||
return []string{
|
||||
model.TaskFinished,
|
||||
model.TaskExecutionFailed,
|
||||
model.TaskValidationFailed,
|
||||
model.TaskRegisterFailed,
|
||||
model.TaskCanceled,
|
||||
model.TaskRolledBack,
|
||||
model.TaskRollbackAck,
|
||||
}
|
||||
}
|
||||
|
||||
func awxNotificationMessage(input AWXJobNotificationInput) string {
|
||||
status := strings.TrimSpace(input.Status)
|
||||
name := strings.TrimSpace(input.Name)
|
||||
|
||||
@@ -143,6 +143,35 @@ func TestRollbackExtraVarsTargetsOnlyTheAllocatedInstance(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirectoryPathsFromAWXStdout(t *testing.T) {
|
||||
stdout := `
|
||||
TASK [Show directory completions] **********************************************
|
||||
ok: [db-01] => {
|
||||
"stdout": "XINFRA_PATH_COMPLETIONS_JSON=[{\"path\":\"/a/bc\",\"available_gi\":12},{\"path\":\"/a/bb\",\"available_gi\":8}]"
|
||||
}
|
||||
`
|
||||
items := directoryPathsFromAWXStdout(stdout)
|
||||
if len(items) != 2 {
|
||||
t.Fatalf("expected two completion items, got %#v", items)
|
||||
}
|
||||
if items[0].Path != "/a/bb" || items[1].Path != "/a/bc" {
|
||||
t.Fatalf("items should be parsed and sorted by path: %#v", items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateDirectoryLookupPrefix(t *testing.T) {
|
||||
for _, path := range []string{"/", "/a", "/a/b", "/lib/data"} {
|
||||
if err := validateDirectoryLookupPrefix(path); err != nil {
|
||||
t.Fatalf("valid path prefix %q rejected: %v", path, err)
|
||||
}
|
||||
}
|
||||
for _, path := range []string{"a", "relative/path"} {
|
||||
if err := validateDirectoryLookupPrefix(path); err == nil {
|
||||
t.Fatalf("invalid path prefix %q was accepted", path)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRegisterFailedIsProtectedFromRollback(t *testing.T) {
|
||||
if !rollbackProtectedStatus(model.TaskRegisterFailed) {
|
||||
t.Fatal("register_failed must preserve the healthy instance and resource usage")
|
||||
|
||||
@@ -37,17 +37,17 @@ type MachineService struct {
|
||||
}
|
||||
|
||||
type MachineListQuery struct {
|
||||
Page int
|
||||
Size int
|
||||
Hostname string
|
||||
AssetNumber string
|
||||
Type string
|
||||
Location string
|
||||
IP string
|
||||
Spec string
|
||||
BusinessLine string
|
||||
Source string
|
||||
Status string
|
||||
BusinessLineID uint64
|
||||
Page int
|
||||
Size int
|
||||
Hostname string
|
||||
AssetNumber string
|
||||
Type string
|
||||
Location string
|
||||
IP string
|
||||
Spec string
|
||||
Source string
|
||||
Status string
|
||||
}
|
||||
|
||||
type MachineResourceItem struct {
|
||||
@@ -189,25 +189,29 @@ func (s *MachineService) SyncNow(ctx context.Context) (*model.MachineSyncState,
|
||||
return s.syncState(ctx)
|
||||
}
|
||||
|
||||
func (s *MachineService) Overview(ctx context.Context) (*MachineOverview, error) {
|
||||
var total, physical int64
|
||||
if err := s.db.WithContext(ctx).Model(&model.MachineResource{}).Count(&total).Error; err != nil {
|
||||
func (s *MachineService) Overview(ctx context.Context, userID uint64, isAdmin bool, businessLineID uint64) (*MachineOverview, error) {
|
||||
baseDB, err := s.scopedMachineDB(ctx, userID, isAdmin, businessLineID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Model(&model.MachineResource{}).Where("resource_type = ?", "physical").Count(&physical).Error; err != nil {
|
||||
var total, physical int64
|
||||
if err := baseDB.Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := baseDB.Session(&gorm.Session{}).Where("resource_type = ?", "physical").Count(&physical).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
sourceCounts, err := s.sourceCounts(ctx)
|
||||
sourceCounts, err := s.sourceCounts(baseDB.Session(&gorm.Session{}))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cloudSources := []string{"aliyun", "ali", "alicloud", "aws", "qiniu"}
|
||||
cmdbTotal, err := s.countExcludingSources(ctx, cloudSources)
|
||||
cmdbTotal, err := s.countExcludingSources(baseDB.Session(&gorm.Session{}), cloudSources)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cmdbPhysical, err := s.countExcludingSourcesAndType(ctx, cloudSources, "physical")
|
||||
cmdbPhysical, err := s.countExcludingSourcesAndType(baseDB.Session(&gorm.Session{}), cloudSources, "physical")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -239,7 +243,7 @@ func (s *MachineService) Overview(ctx context.Context) (*MachineOverview, error)
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *MachineService) List(ctx context.Context, query MachineListQuery) (*MachineResourceList, error) {
|
||||
func (s *MachineService) List(ctx context.Context, userID uint64, isAdmin bool, query MachineListQuery) (*MachineResourceList, error) {
|
||||
if query.Page <= 0 {
|
||||
query.Page = 1
|
||||
}
|
||||
@@ -250,7 +254,10 @@ func (s *MachineService) List(ctx context.Context, query MachineListQuery) (*Mac
|
||||
query.Size = 200
|
||||
}
|
||||
|
||||
db := s.db.WithContext(ctx).Model(&model.MachineResource{})
|
||||
db, err := s.scopedMachineDB(ctx, userID, isAdmin, query.BusinessLineID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
db = applyMachineFilters(db, query)
|
||||
|
||||
var total int64
|
||||
@@ -270,6 +277,37 @@ func (s *MachineService) List(ctx context.Context, query MachineListQuery) (*Mac
|
||||
return &MachineResourceList{Total: total, Items: items}, nil
|
||||
}
|
||||
|
||||
func (s *MachineService) scopedMachineDB(ctx context.Context, userID uint64, isAdmin bool, businessLineID uint64) (*gorm.DB, error) {
|
||||
if businessLineID == 0 {
|
||||
return nil, errors.New("business_line_id is required")
|
||||
}
|
||||
if !isAdmin {
|
||||
var binding model.BusinessLineUser
|
||||
err := s.db.WithContext(ctx).Where("business_line_id = ? AND user_id = ?", businessLineID, userID).First(&binding).Error
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("current user does not have business line permission")
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
var mappings []model.BusinessLineSinaOrganization
|
||||
if err := s.db.WithContext(ctx).Where("business_line_id = ?", businessLineID).Order("sina_organization_name ASC").Find(&mappings).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
names := make([]string, 0, len(mappings))
|
||||
for _, mapping := range mappings {
|
||||
if name := strings.TrimSpace(mapping.SinaOrganizationName); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
db := s.db.WithContext(ctx).Model(&model.MachineResource{})
|
||||
if len(names) == 0 {
|
||||
return db.Where("1 = 0"), nil
|
||||
}
|
||||
return db.Where("business_line IN ?", names), nil
|
||||
}
|
||||
|
||||
func (s *MachineService) acquireSync() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
@@ -527,18 +565,15 @@ func applyMachineFilters(db *gorm.DB, query MachineListQuery) *gorm.DB {
|
||||
if query.Spec != "" {
|
||||
db = db.Where("spec LIKE ?", "%"+query.Spec+"%")
|
||||
}
|
||||
if query.BusinessLine != "" {
|
||||
db = db.Where("business_line LIKE ?", "%"+query.BusinessLine+"%")
|
||||
}
|
||||
return db
|
||||
}
|
||||
|
||||
func (s *MachineService) sourceCounts(ctx context.Context) (map[string]int64, error) {
|
||||
func (s *MachineService) sourceCounts(db *gorm.DB) (map[string]int64, error) {
|
||||
var rows []struct {
|
||||
Source string
|
||||
Count int64
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Model(&model.MachineResource{}).Select("source, count(*) as count").Group("source").Scan(&rows).Error; err != nil {
|
||||
if err := db.Select("source, count(*) as count").Group("source").Scan(&rows).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
out := map[string]int64{}
|
||||
@@ -549,15 +584,15 @@ func (s *MachineService) sourceCounts(ctx context.Context) (map[string]int64, er
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (s *MachineService) countExcludingSources(ctx context.Context, sources []string) (int64, error) {
|
||||
func (s *MachineService) countExcludingSources(db *gorm.DB, sources []string) (int64, error) {
|
||||
var count int64
|
||||
err := s.db.WithContext(ctx).Model(&model.MachineResource{}).Where("source NOT IN ?", sources).Count(&count).Error
|
||||
err := db.Where("source NOT IN ?", sources).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
func (s *MachineService) countExcludingSourcesAndType(ctx context.Context, sources []string, resourceType string) (int64, error) {
|
||||
func (s *MachineService) countExcludingSourcesAndType(db *gorm.DB, sources []string, resourceType string) (int64, error) {
|
||||
var count int64
|
||||
err := s.db.WithContext(ctx).Model(&model.MachineResource{}).Where("source NOT IN ? AND resource_type = ?", sources, resourceType).Count(&count).Error
|
||||
err := db.Where("source NOT IN ? AND resource_type = ?", sources, resourceType).Count(&count).Error
|
||||
return count, err
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user