feat(business-line): auto bind Wayne namespace on create
This commit is contained in:
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user