Merge branch 'main' into feat/mysql-service-delivery

This commit is contained in:
ztkkOip
2026-07-23 14:21:06 +08:00
committed by GitHub
11 changed files with 1050 additions and 6 deletions
+45 -1
View File
@@ -144,9 +144,53 @@ func (h *BusinessLineHandler) Create(c *gin.Context) {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
req.Name = strings.TrimSpace(req.Name)
if req.Name == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "business line name is required"})
return
}
var existing model.BusinessLine
if err := h.db.Where("name = ?", req.Name).First(&existing).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "business line already exists"})
return
} else if !errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
if h.wayne == nil {
c.JSON(http.StatusServiceUnavailable, gin.H{"error": "wayne native api is not configured"})
return
}
namespace, err := h.wayne.EnsureNamespace(c.Request.Context(), req.Name)
if err != nil {
writeWayneRoleBindingError(c, nil, err)
return
}
if namespace == nil || namespace.ID == 0 {
c.JSON(http.StatusBadGateway, gin.H{"error": "wayne namespace response is invalid"})
return
}
if strings.TrimSpace(namespace.Name) == "" {
namespace.Name = req.Name
}
if strings.TrimSpace(namespace.KubeNamespace) == "" {
namespace.KubeNamespace = req.Name
}
item := model.BusinessLine{Name: req.Name}
if err := h.db.Create(&item).Error; err != nil {
if err := h.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Create(&item).Error; err != nil {
return err
}
return tx.Create(&model.BusinessLineWayneNamespace{
BusinessLineID: item.ID,
WayneNamespaceID: namespace.ID,
WayneNamespaceName: namespace.Name,
KubeNamespace: namespace.KubeNamespace,
}).Error
}); err != nil {
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
return
}
@@ -0,0 +1,109 @@
package handler
import (
"errors"
"net/http"
"github.com/1024XEngineer/xinfra/server/internal/auth"
"github.com/1024XEngineer/xinfra/server/internal/model"
"github.com/1024XEngineer/xinfra/server/internal/service"
"github.com/gin-gonic/gin"
"gorm.io/gorm"
)
type ContainerServiceHandler struct {
db *gorm.DB
wayne *service.WayneRoleBindingService
}
func NewContainerServiceHandler(db *gorm.DB, wayne *service.WayneRoleBindingService) *ContainerServiceHandler {
return &ContainerServiceHandler{db: db, wayne: wayne}
}
func (h *ContainerServiceHandler) Summary(c *gin.Context) {
data, ok := h.loadData(c)
if !ok {
return
}
c.JSON(http.StatusOK, data.Summary)
}
func (h *ContainerServiceHandler) Workloads(c *gin.Context) {
data, ok := h.loadData(c)
if !ok {
return
}
c.JSON(http.StatusOK, gin.H{"items": data.Workloads, "summary": data.Summary})
}
func (h *ContainerServiceHandler) loadData(c *gin.Context) (*service.ContainerServiceData, bool) {
claims, ok := CurrentClaims(c)
if !ok {
c.JSON(http.StatusUnauthorized, gin.H{"error": "missing current user"})
return nil, false
}
businessLineID, ok := parseBusinessLineID(c)
if !ok {
return nil, false
}
businessLine, ok := h.getBusinessLine(c, businessLineID)
if !ok {
return nil, false
}
if !h.canReadBusinessLine(c, claims, businessLineID) {
return nil, false
}
namespaces, ok := h.listBusinessLineNamespaces(c, businessLineID)
if !ok {
return nil, false
}
data, err := h.wayne.ContainerServiceData(c.Request.Context(), service.ContainerBusinessLineQuery{
BusinessLineID: businessLineID,
BusinessLineName: businessLine.Name,
Namespaces: namespaces,
})
if err != nil {
writeWayneRoleBindingError(c, nil, err)
return nil, false
}
return data, true
}
func (h *ContainerServiceHandler) getBusinessLine(c *gin.Context, id uint64) (model.BusinessLine, bool) {
var businessLine model.BusinessLine
if err := h.db.First(&businessLine, "id = ?", id).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusNotFound, gin.H{"error": "business line not found"})
return businessLine, false
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return businessLine, false
}
return businessLine, true
}
func (h *ContainerServiceHandler) canReadBusinessLine(c *gin.Context, claims *auth.Claims, businessLineID uint64) bool {
if claims.IsAdmin {
return true
}
var binding model.BusinessLineUser
if err := h.db.Where("business_line_id = ? AND user_id = ?", businessLineID, claims.UserID).First(&binding).Error; err != nil {
if errors.Is(err, gorm.ErrRecordNotFound) {
c.JSON(http.StatusForbidden, gin.H{"error": "current user is not bound to current business line"})
return false
}
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return false
}
return true
}
func (h *ContainerServiceHandler) listBusinessLineNamespaces(c *gin.Context, businessLineID uint64) ([]model.BusinessLineWayneNamespace, bool) {
var rows []model.BusinessLineWayneNamespace
if err := h.db.Where("business_line_id = ?", businessLineID).Order("wayne_namespace_id ASC").Find(&rows).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return nil, false
}
return rows, true
}
+3
View File
@@ -90,6 +90,7 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
deploymentHandler := handler.NewDeploymentHandler(deps.Config, deps.DB, deploymentService)
deliveryHandler := handler.NewDeliveryHandler(deliveryService)
executionHandler := handler.NewExecutionHandler(deliveryService, deps.Config.DeliveryServiceToken)
containerServiceHandler := handler.NewContainerServiceHandler(deps.DB, wayneRoleBindingService)
r.GET("/healthz", healthHandler.Healthz)
r.GET("/readyz", healthHandler.Readyz)
@@ -142,6 +143,8 @@ func registerAuthServerRoutes(r *gin.Engine, deps Dependencies) {
protected.PUT("/subsystem-auth/wayne/business-lines/:id/namespaces/:namespaceid/users/:username/roles", subsystemAuthHandler.BindWayneNamespaceRoles)
protected.DELETE("/subsystem-auth/wayne/business-lines/:id/namespaces/:namespaceid/users/:username/roles", subsystemAuthHandler.UnbindWayneNamespaceRoles)
protected.POST("/subsystem-auth/wayne/business-lines/:id/users/:userid/init", subsystemAuthHandler.InitWayneBusinessLineUser)
protected.GET("/container-services/business-lines/:id/summary", containerServiceHandler.Summary)
protected.GET("/container-services/business-lines/:id/workloads", containerServiceHandler.Workloads)
protected.POST("/deployments", deploymentHandler.Create)
protected.GET("/deployments/:id", deploymentHandler.Get)
protected.GET("/deployments/:id/events", deploymentHandler.Events)
@@ -0,0 +1,420 @@
package service
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"github.com/1024XEngineer/xinfra/server/internal/model"
)
type ContainerServiceSummary struct {
BusinessLineID uint64 `json:"businessLineId"`
BusinessLineName string `json:"businessLineName"`
Namespaces int `json:"namespaces"`
Clusters int `json:"clusters"`
Nodes int `json:"nodes"`
ReadyNodes int `json:"readyNodes"`
SchedulableNodes int `json:"schedulableNodes"`
Workloads int `json:"workloads"`
Pods int `json:"pods"`
ReadyPods int `json:"readyPods"`
CPUUsed float64 `json:"cpuUsed"`
CPUTotal float64 `json:"cpuTotal"`
CPUPercent int `json:"cpuPercent"`
MemoryUsed float64 `json:"memoryUsed"`
MemoryTotal float64 `json:"memoryTotal"`
MemoryPercent int `json:"memoryPercent"`
Errors []string `json:"errors,omitempty"`
}
type ContainerWorkload struct {
Name string `json:"name"`
Cluster string `json:"cluster"`
Namespace string `json:"namespace"`
Workload string `json:"workload"`
Ready string `json:"ready"`
ReadyPods int `json:"readyPods"`
Pods int `json:"pods"`
Image string `json:"image"`
BusinessLine string `json:"biz"`
Status string `json:"status"`
StatusClass string `json:"statusClass"`
}
type ContainerServiceData struct {
Summary ContainerServiceSummary `json:"summary"`
Workloads []ContainerWorkload `json:"items"`
}
type ContainerBusinessLineQuery struct {
BusinessLineID uint64
BusinessLineName string
Namespaces []model.BusinessLineWayneNamespace
}
func (s *WayneRoleBindingService) ContainerServiceData(ctx context.Context, query ContainerBusinessLineQuery) (*ContainerServiceData, error) {
summary := ContainerServiceSummary{
BusinessLineID: query.BusinessLineID,
BusinessLineName: query.BusinessLineName,
Namespaces: len(query.Namespaces),
}
workloads := make([]ContainerWorkload, 0)
clusterNodes := map[string]wayneNodeSummary{}
seenWorkloads := map[string]struct{}{}
errors := make([]string, 0)
for _, binding := range query.Namespaces {
namespaceDetail, err := s.getWayneNamespace(ctx, binding.WayneNamespaceID)
if err != nil {
errors = append(errors, fmt.Sprintf("namespace %d: %v", binding.WayneNamespaceID, err))
continue
}
kubeNamespace := strings.TrimSpace(binding.KubeNamespace)
if kubeNamespace == "" {
kubeNamespace = namespaceDetail.KubeNamespace
}
clusters := namespaceDetail.Clusters()
if len(clusters) == 0 {
errors = append(errors, fmt.Sprintf("namespace %d has no clusterMeta", binding.WayneNamespaceID))
continue
}
apps, err := s.listWayneNamespaceApps(ctx, binding.WayneNamespaceID)
if err != nil {
errors = append(errors, fmt.Sprintf("namespace %d apps: %v", binding.WayneNamespaceID, err))
continue
}
if len(apps) == 0 {
errors = append(errors, fmt.Sprintf("namespace %d has no apps for deployment query", binding.WayneNamespaceID))
continue
}
appID := apps[0].ID
for _, cluster := range clusters {
if _, ok := clusterNodes[cluster]; !ok {
nodes, err := s.getWayneNodes(ctx, cluster)
if err != nil {
errors = append(errors, fmt.Sprintf("cluster %s nodes: %v", cluster, err))
} else {
clusterNodes[cluster] = nodes
}
}
items, err := s.listWayneDeployments(ctx, appID, kubeNamespace, cluster)
if err != nil {
errors = append(errors, fmt.Sprintf("namespace %d cluster %s app %d deployments: %v", binding.WayneNamespaceID, cluster, appID, err))
continue
}
for _, item := range items {
workload := containerWorkloadFromMap(item, cluster, kubeNamespace, query.BusinessLineName)
if workload.Name == "" {
continue
}
key := strings.Join([]string{workload.Cluster, workload.Namespace, workload.Workload, workload.Name}, "\x00")
if _, ok := seenWorkloads[key]; ok {
continue
}
seenWorkloads[key] = struct{}{}
workloads = append(workloads, workload)
summary.Pods += workload.Pods
summary.ReadyPods += workload.ReadyPods
}
}
}
for _, nodes := range clusterNodes {
summary.Nodes += nodes.Total
summary.ReadyNodes += nodes.Ready
summary.SchedulableNodes += nodes.Schedulable
summary.CPUUsed += nodes.CPUUsed
summary.CPUTotal += nodes.CPUTotal
summary.MemoryUsed += nodes.MemoryUsed
summary.MemoryTotal += nodes.MemoryTotal
}
summary.Clusters = len(clusterNodes)
summary.Workloads = len(workloads)
summary.CPUPercent = ratioPercent(summary.CPUUsed, summary.CPUTotal)
summary.MemoryPercent = ratioPercent(summary.MemoryUsed, summary.MemoryTotal)
summary.Errors = errors
return &ContainerServiceData{Summary: summary, Workloads: workloads}, nil
}
func (s *WayneRoleBindingService) getWayneNamespace(ctx context.Context, id uint64) (wayneNamespaceDetail, error) {
result, err := s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/namespaces/%d", id), nil)
if err != nil {
return wayneNamespaceDetail{}, err
}
return parseWayneNamespaceDetail(result.Body)
}
func (s *WayneRoleBindingService) listWayneNamespaceApps(ctx context.Context, namespaceID uint64) ([]wayneApp, error) {
values := url.Values{}
values.Set("pageNo", "1")
values.Set("pageSize", "500")
result, err := s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/namespaces/%d/apps?%s", namespaceID, values.Encode()), nil)
if err != nil {
return nil, err
}
return parseWayneApps(result.Body)
}
func (s *WayneRoleBindingService) getWayneNodes(ctx context.Context, cluster string) (wayneNodeSummary, error) {
result, err := s.callRaw(ctx, http.MethodGet, fmt.Sprintf("/api/v1/kubernetes/nodes/clusters/%s", url.PathEscape(cluster)), nil)
if err != nil {
return wayneNodeSummary{}, err
}
return parseWayneNodeSummary(result.Body)
}
func (s *WayneRoleBindingService) listWayneDeployments(ctx context.Context, appID uint64, namespace string, cluster string) ([]map[string]any, error) {
values := url.Values{}
values.Set("pageNo", "1")
values.Set("pageSize", "500")
path := fmt.Sprintf(
"/api/v1/kubernetes/apps/%d/deployments/namespaces/%s/clusters/%s?%s",
appID,
url.PathEscape(namespace),
url.PathEscape(cluster),
values.Encode(),
)
result, err := s.callRaw(ctx, http.MethodGet, path, nil)
if err != nil {
return nil, err
}
return parseWayneMapList(result.Body)
}
type wayneNamespaceDetail struct {
ID uint64 `json:"id"`
Name string `json:"name"`
KubeNamespace string `json:"kubeNamespace"`
MetaData string `json:"metaData"`
}
func (n wayneNamespaceDetail) Clusters() []string {
var meta struct {
ClusterMeta map[string]json.RawMessage `json:"clusterMeta"`
}
if err := json.Unmarshal([]byte(n.MetaData), &meta); err != nil {
return nil
}
clusters := make([]string, 0, len(meta.ClusterMeta))
for cluster := range meta.ClusterMeta {
if strings.TrimSpace(cluster) != "" {
clusters = append(clusters, cluster)
}
}
return clusters
}
type wayneApp struct {
ID uint64 `json:"id"`
Name string `json:"name"`
}
type wayneNodeSummary struct {
Total int
Ready int
Schedulable int
CPUUsed float64
CPUTotal float64
MemoryUsed float64
MemoryTotal float64
}
func parseWayneNamespaceDetail(body []byte) (wayneNamespaceDetail, error) {
var wrapped struct {
Data wayneNamespaceDetail `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return wayneNamespaceDetail{}, err
}
return wrapped.Data, nil
}
func parseWayneApps(body []byte) ([]wayneApp, error) {
var wrapped struct {
Data struct {
List []wayneApp `json:"list"`
} `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return nil, err
}
return wrapped.Data.List, nil
}
func parseWayneMapList(body []byte) ([]map[string]any, error) {
var wrapped struct {
Data struct {
List []map[string]any `json:"list"`
} `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return nil, err
}
return wrapped.Data.List, nil
}
func parseWayneNodeSummary(body []byte) (wayneNodeSummary, error) {
var wrapped struct {
Data struct {
NodeSummary struct {
Total int `json:"total"`
Ready int `json:"ready"`
Schedulable int `json:"schedulable"`
} `json:"nodeSummary"`
CPUSummary struct {
Total float64 `json:"total"`
Used float64 `json:"used"`
} `json:"cpuSummary"`
MemorySummary struct {
Total float64 `json:"total"`
Used float64 `json:"used"`
} `json:"memorySummary"`
} `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return wayneNodeSummary{}, err
}
return wayneNodeSummary{
Total: wrapped.Data.NodeSummary.Total,
Ready: wrapped.Data.NodeSummary.Ready,
Schedulable: wrapped.Data.NodeSummary.Schedulable,
CPUUsed: wrapped.Data.CPUSummary.Used,
CPUTotal: wrapped.Data.CPUSummary.Total,
MemoryUsed: wrapped.Data.MemorySummary.Used,
MemoryTotal: wrapped.Data.MemorySummary.Total,
}, nil
}
func containerWorkloadFromMap(item map[string]any, cluster string, namespace string, businessLineName string) ContainerWorkload {
name := firstString(item, "name", "objectMeta.name", "metadata.name")
ready := firstInt(item, "pods.current", "readyReplicas", "status.readyReplicas", "availableReplicas", "status.availableReplicas")
total := firstInt(item, "pods.desired", "replicas", "status.replicas", "desiredReplicas", "spec.replicas")
if total == 0 && ready > 0 {
total = ready
}
status := "健康"
statusClass := "ok"
if total == 0 {
status = "未就绪"
statusClass = "warn"
} else if ready < total {
status = "部分异常"
statusClass = "warn"
}
return ContainerWorkload{
Name: name,
Cluster: cluster,
Namespace: namespace,
Workload: "Deployment",
Ready: strconv.Itoa(ready) + " / " + strconv.Itoa(total),
ReadyPods: ready,
Pods: total,
Image: firstImage(item),
BusinessLine: businessLineName,
Status: status,
StatusClass: statusClass,
}
}
func firstImage(item map[string]any) string {
if value := firstString(item, "image", "containerImage"); value != "" {
return value
}
for _, path := range []string{"containers", "images", "containerImages"} {
value, ok := nestedValue(item, path)
if !ok {
continue
}
if images, ok := value.([]any); ok && len(images) > 0 {
if image, ok := images[0].(string); ok {
return image
}
}
}
if containers, ok := nestedValue(item, "spec.template.spec.containers"); ok {
if rows, ok := containers.([]any); ok && len(rows) > 0 {
if row, ok := rows[0].(map[string]any); ok {
return stringFromAny(row["image"])
}
}
}
return ""
}
func firstString(item map[string]any, paths ...string) string {
for _, path := range paths {
value, ok := nestedValue(item, path)
if !ok {
continue
}
if text := strings.TrimSpace(stringFromAny(value)); text != "" {
return text
}
}
return ""
}
func firstInt(item map[string]any, paths ...string) int {
for _, path := range paths {
value, ok := nestedValue(item, path)
if !ok {
continue
}
switch typed := value.(type) {
case float64:
return int(typed)
case int:
return typed
case json.Number:
intValue, _ := typed.Int64()
return int(intValue)
case string:
intValue, _ := strconv.Atoi(strings.TrimSpace(typed))
return intValue
}
}
return 0
}
func nestedValue(item map[string]any, path string) (any, bool) {
current := any(item)
for _, part := range strings.Split(path, ".") {
row, ok := current.(map[string]any)
if !ok {
return nil, false
}
current, ok = row[part]
if !ok {
return nil, false
}
}
return current, true
}
func stringFromAny(value any) string {
switch typed := value.(type) {
case string:
return typed
case fmt.Stringer:
return typed.String()
default:
if value == nil {
return ""
}
return fmt.Sprintf("%v", value)
}
}
func ratioPercent(used, total float64) int {
if total <= 0 {
return 0
}
return int(used * 100 / total)
}
@@ -53,6 +53,13 @@ type WayneRoleGroup struct {
Type int `json:"type"`
}
type WayneNamespace struct {
ID uint64 `json:"id"`
Name string `json:"name"`
KubeNamespace string `json:"kubeNamespace"`
MetaData string `json:"metaData,omitempty"`
}
type WayneOperatorPermissions struct {
Create bool `json:"create"`
Update bool `json:"update"`
@@ -115,6 +122,77 @@ func (s *WayneRoleBindingService) ListNamespaces(ctx context.Context) (*WayneRol
return s.callRaw(ctx, http.MethodGet, "/api/v1/namespaces", nil)
}
func (s *WayneRoleBindingService) EnsureNamespace(ctx context.Context, name string) (*WayneNamespace, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("wayne namespace name is required")
}
namespace, err := s.FindNamespaceByName(ctx, name)
if err != nil {
return nil, err
}
if namespace != nil {
return namespace, nil
}
return s.CreateNamespace(ctx, WayneNamespace{
Name: name,
KubeNamespace: name,
MetaData: "{}",
})
}
func (s *WayneRoleBindingService) FindNamespaceByName(ctx context.Context, name string) (*WayneNamespace, error) {
name = strings.TrimSpace(name)
if name == "" {
return nil, fmt.Errorf("wayne namespace name is required")
}
values := url.Values{}
values.Set("name", name)
values.Set("pageNo", "1")
values.Set("pageSize", "500")
result, err := s.callRaw(ctx, http.MethodGet, "/api/v1/namespaces?"+values.Encode(), nil)
if err != nil {
return nil, err
}
namespaces, err := parseWayneNamespaces(result.Body)
if err != nil {
return nil, err
}
for _, namespace := range namespaces {
if strings.EqualFold(strings.TrimSpace(namespace.Name), name) {
return &namespace, nil
}
}
return nil, nil
}
func (s *WayneRoleBindingService) CreateNamespace(ctx context.Context, namespace WayneNamespace) (*WayneNamespace, error) {
namespace.Name = strings.TrimSpace(namespace.Name)
namespace.KubeNamespace = strings.TrimSpace(namespace.KubeNamespace)
if namespace.Name == "" {
return nil, fmt.Errorf("wayne namespace name is required")
}
if namespace.KubeNamespace == "" {
namespace.KubeNamespace = namespace.Name
}
if strings.TrimSpace(namespace.MetaData) == "" {
namespace.MetaData = "{}"
}
body, err := json.Marshal(namespace)
if err != nil {
return nil, err
}
result, err := s.callRaw(ctx, http.MethodPost, "/api/v1/namespaces", body)
if err != nil {
return nil, err
}
created, err := parseWayneNamespace(result.Body)
if err != nil {
return nil, err
}
return &created, nil
}
func (s *WayneRoleBindingService) ListGroups(ctx context.Context, groupType *int) (*WayneRoleBindingResult, error) {
path := "/api/v1/groups"
if groupType != nil {
@@ -615,6 +693,28 @@ func parseWayneResourceIDs(body []byte) ([]uint64, error) {
return resourceIDsFromItems(wrapped.Data.List), nil
}
func parseWayneNamespace(body []byte) (WayneNamespace, error) {
var wrapped struct {
Data WayneNamespace `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return WayneNamespace{}, err
}
return wrapped.Data, nil
}
func parseWayneNamespaces(body []byte) ([]WayneNamespace, error) {
var wrapped struct {
Data struct {
List []WayneNamespace `json:"list"`
} `json:"data"`
}
if err := json.Unmarshal(body, &wrapped); err != nil {
return nil, err
}
return wrapped.Data.List, nil
}
func resourceIDsFromItems(items []struct {
ID uint64 `json:"id"`
}) []uint64 {
@@ -81,6 +81,75 @@ func TestParseWayneUsersSupportsNativePageList(t *testing.T) {
}
}
func TestWayneRoleBindingServiceEnsureNamespaceUsesExisting(t *testing.T) {
var got []string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
got = append(got, r.Method+" "+r.URL.RequestURI())
switch r.URL.RequestURI() {
case "/login/db":
_, _ = w.Write([]byte(`{"data":{"token":"` + testJWT(time.Now().Add(time.Hour)) + `"}}`))
case "/api/v1/namespaces?name=kodo&pageNo=1&pageSize=500":
assertBearer(t, r)
_, _ = w.Write([]byte(`{"data":{"list":[{"id":12,"name":"kodo","kubeNamespace":"kodo-prod"}]}}`))
default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.RequestURI())
}
}))
defer server.Close()
svc := NewWayneRoleBindingService(testWayneConfig(server.URL), nil)
namespace, err := svc.EnsureNamespace(context.Background(), "kodo")
if err != nil {
t.Fatalf("EnsureNamespace() error = %v", err)
}
if namespace.ID != 12 || namespace.Name != "kodo" || namespace.KubeNamespace != "kodo-prod" {
t.Fatalf("namespace = %#v", namespace)
}
if strings.Join(got, "\n") != strings.Join([]string{
"POST /login/db",
"GET /api/v1/namespaces?name=kodo&pageNo=1&pageSize=500",
}, "\n") {
t.Fatalf("requests:\n%s", strings.Join(got, "\n"))
}
}
func TestWayneRoleBindingServiceEnsureNamespaceCreatesMissing(t *testing.T) {
var createBody WayneNamespace
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.RequestURI() {
case "/login/db":
_, _ = w.Write([]byte(`{"data":{"token":"` + testJWT(time.Now().Add(time.Hour)) + `"}}`))
case "/api/v1/namespaces?name=las&pageNo=1&pageSize=500":
assertBearer(t, r)
_, _ = w.Write([]byte(`{"data":{"list":[]}}`))
case "/api/v1/namespaces":
if r.Method != http.MethodPost {
t.Fatalf("method = %s, want POST", r.Method)
}
assertBearer(t, r)
if err := json.Unmarshal(readTestBody(t, r), &createBody); err != nil {
t.Fatalf("invalid create body: %v", err)
}
_, _ = w.Write([]byte(`{"data":{"id":21,"name":"las","kubeNamespace":"las"}}`))
default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.RequestURI())
}
}))
defer server.Close()
svc := NewWayneRoleBindingService(testWayneConfig(server.URL), nil)
namespace, err := svc.EnsureNamespace(context.Background(), "las")
if err != nil {
t.Fatalf("EnsureNamespace() error = %v", err)
}
if namespace.ID != 21 || namespace.Name != "las" || namespace.KubeNamespace != "las" {
t.Fatalf("namespace = %#v", namespace)
}
if createBody.Name != "las" || createBody.KubeNamespace != "las" || createBody.MetaData != "{}" {
t.Fatalf("create body = %#v", createBody)
}
}
func TestWayneRoleBindingServiceCreateAndDeleteNativeBindings(t *testing.T) {
tests := []struct {
name string