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()})
|
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
|
||||||
return
|
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}
|
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()})
|
c.JSON(http.StatusConflict, gin.H{"error": err.Error()})
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,13 @@ type WayneRoleGroup struct {
|
|||||||
Type int `json:"type"`
|
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 {
|
type WayneOperatorPermissions struct {
|
||||||
Create bool `json:"create"`
|
Create bool `json:"create"`
|
||||||
Update bool `json:"update"`
|
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)
|
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) {
|
func (s *WayneRoleBindingService) ListGroups(ctx context.Context, groupType *int) (*WayneRoleBindingResult, error) {
|
||||||
path := "/api/v1/groups"
|
path := "/api/v1/groups"
|
||||||
if groupType != nil {
|
if groupType != nil {
|
||||||
@@ -615,6 +693,28 @@ func parseWayneResourceIDs(body []byte) ([]uint64, error) {
|
|||||||
return resourceIDsFromItems(wrapped.Data.List), nil
|
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 {
|
func resourceIDsFromItems(items []struct {
|
||||||
ID uint64 `json:"id"`
|
ID uint64 `json:"id"`
|
||||||
}) []uint64 {
|
}) []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) {
|
func TestWayneRoleBindingServiceCreateAndDeleteNativeBindings(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user