feat(business-line): auto bind Wayne namespace on create

This commit is contained in:
mac
2026-07-22 14:59:54 +08:00
parent 7f6f6fa41e
commit d150d0f0dd
3 changed files with 214 additions and 1 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
}
@@ -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