feat(rbac): 集群级别rbac重构

This commit is contained in:
chenyang
2021-07-23 17:05:53 +08:00
parent 6928299a0a
commit 44247f934b
54 changed files with 3177 additions and 2020 deletions

View File

@@ -6,12 +6,14 @@ import (
"github.com/KubeOperator/ekko/pkg/kubernetes" "github.com/KubeOperator/ekko/pkg/kubernetes"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"strings"
) )
func (h *Handler) ListApiGroups() iris.Handler { func (h *Handler) ListApiGroups() iris.Handler {
return func(ctx *context.Context) { return func(ctx *context.Context) {
name := ctx.Params().GetString("name") name := ctx.Params().GetString("name")
c, err := h.clusterService.Get(name,common.DBOptions{}) c, err := h.clusterService.Get(name, common.DBOptions{})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
@@ -37,8 +39,8 @@ func (h *Handler) ListApiGroups() iris.Handler {
func (h *Handler) ListApiGroupResources() iris.Handler { func (h *Handler) ListApiGroupResources() iris.Handler {
return func(ctx *context.Context) { return func(ctx *context.Context) {
name := ctx.Params().GetString("name") name := ctx.Params().GetString("name")
groupVersion := ctx.Params().GetString("group") groupName := ctx.Params().GetString("group")
c, err := h.clusterService.Get(name,common.DBOptions{}) c, err := h.clusterService.Get(name, common.DBOptions{})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
@@ -51,13 +53,39 @@ func (h *Handler) ListApiGroupResources() iris.Handler {
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
return return
} }
apiResources, err := client.ServerResourcesForGroupVersion(groupVersion) gss, rss, err := client.ServerGroupsAndResources()
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
return return
} }
ctx.Values().Set("data", apiResources.APIResources) var groupVersions []v1.GroupVersionForDiscovery
resourceSet := map[string]struct{}{}
if groupName == "core" {
groupName = ""
}
for i := range gss {
if gss[i].Name == groupName {
groupVersions = append(groupVersions, gss[i].Versions...)
}
}
for i := range rss {
for j := range groupVersions {
if groupVersions[j].GroupVersion == rss[i].GroupVersion {
for k := range rss[i].APIResources {
if !strings.Contains(rss[i].APIResources[k].Name, "/") {
resourceSet[rss[i].APIResources[k].Name] = struct{}{}
}
}
}
}
}
var resources []string
for k := range resourceSet {
resources = append(resources, k)
}
ctx.Values().Set("data", resources)
} }
} }

View File

@@ -10,6 +10,7 @@ import (
"github.com/KubeOperator/ekko/internal/service/v1/cluster" "github.com/KubeOperator/ekko/internal/service/v1/cluster"
"github.com/KubeOperator/ekko/internal/service/v1/clusterbinding" "github.com/KubeOperator/ekko/internal/service/v1/clusterbinding"
"github.com/KubeOperator/ekko/internal/service/v1/common" "github.com/KubeOperator/ekko/internal/service/v1/common"
"github.com/KubeOperator/ekko/internal/service/v1/project"
pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1" pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1"
"github.com/KubeOperator/ekko/pkg/certificate" "github.com/KubeOperator/ekko/pkg/certificate"
"github.com/KubeOperator/ekko/pkg/kubernetes" "github.com/KubeOperator/ekko/pkg/kubernetes"
@@ -26,12 +27,14 @@ import (
type Handler struct { type Handler struct {
clusterService cluster.Service clusterService cluster.Service
clusterBindingService clusterbinding.Service clusterBindingService clusterbinding.Service
projectService project.Service
} }
func NewHandler() *Handler { func NewHandler() *Handler {
return &Handler{ return &Handler{
clusterService: cluster.NewService(), clusterService: cluster.NewService(),
clusterBindingService: clusterbinding.NewService(), clusterBindingService: clusterbinding.NewService(),
projectService: project.NewService(),
} }
} }
@@ -131,7 +134,6 @@ func (h *Handler) CreateCluster() iris.Handler {
txOptions := common.DBOptions{DB: tx} txOptions := common.DBOptions{DB: tx}
req.CreatedBy = profile.Name req.CreatedBy = profile.Name
if err := client.CreateDefaultClusterRoles(); err != nil { if err := client.CreateDefaultClusterRoles(); err != nil {
_ = tx.Rollback() _ = tx.Rollback()
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
@@ -152,12 +154,9 @@ func (h *Handler) CreateCluster() iris.Handler {
CreatedBy: profile.Name, CreatedBy: profile.Name,
}, },
Metadata: v1.Metadata{ Metadata: v1.Metadata{
Name: fmt.Sprintf("%s-%s-cluster-binding-", req.Name, profile.Name), Name: fmt.Sprintf("%s-%s-cluster-binding", req.Name, profile.Name),
},
Subject: v1Cluster.Subject{
Name: profile.Name,
Kind: "User",
}, },
UserRef: profile.Name,
ClusterRef: req.Name, ClusterRef: req.Name,
} }
@@ -186,11 +185,24 @@ func (h *Handler) CreateCluster() iris.Handler {
return return
} }
roleBindingName := "ekko:admin-cluster"
obj, err := kc.RbacV1().ClusterRoleBindings().Get(goContext.TODO(), roleBindingName, metav1.GetOptions{})
if err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "not found") {
_ = tx.Rollback()
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error())
}
}
if obj == nil || obj.Name == "" {
if _, err := kc.RbacV1().ClusterRoleBindings().Create(goContext.TODO(), &rbacV1.ClusterRoleBinding{ if _, err := kc.RbacV1().ClusterRoleBindings().Create(goContext.TODO(), &rbacV1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("ekko:cluster-admin-%s", profile.Name), Name: roleBindingName,
Annotations: map[string]string{ Annotations: map[string]string{
"created-by": profile.Name, "builtin": "true",
},
Labels: map[string]string{
"user-name": profile.Name,
}, },
}, },
Subjects: []rbacV1.Subject{ Subjects: []rbacV1.Subject{
@@ -200,7 +212,7 @@ func (h *Handler) CreateCluster() iris.Handler {
}, },
}, },
RoleRef: rbacV1.RoleRef{ RoleRef: rbacV1.RoleRef{
Name: "cluster-admin", Name: "Admin Cluster",
Kind: "ClusterRole", Kind: "ClusterRole",
}, },
}, metav1.CreateOptions{}); err != nil { }, metav1.CreateOptions{}); err != nil {
@@ -209,8 +221,8 @@ func (h *Handler) CreateCluster() iris.Handler {
ctx.Values().Set("message", err.Error()) ctx.Values().Set("message", err.Error())
return return
} }
}
_ = tx.Commit() _ = tx.Commit()
ctx.Values().Set("data", &req) ctx.Values().Set("data", &req)
} }
} }
@@ -236,6 +248,18 @@ func (h *Handler) SearchClusters() iris.Handler {
ctx.Values().Set("data", pkgV1.Page{Items: clusters, Total: total}) ctx.Values().Set("data", pkgV1.Page{Items: clusters, Total: total})
} }
} }
func (h *Handler) GetCluster() iris.Handler {
return func(ctx *context.Context) {
name := ctx.Params().GetString("name")
c, err := h.clusterService.Get(name, common.DBOptions{})
if err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", fmt.Sprintf("get clusters failed: %s", err.Error()))
return
}
ctx.Values().Set("data", c)
}
}
func (h *Handler) ListClusters() iris.Handler { func (h *Handler) ListClusters() iris.Handler {
return func(ctx *context.Context) { return func(ctx *context.Context) {
@@ -266,7 +290,7 @@ func (h *Handler) ListClusters() iris.Handler {
return return
} }
for j := range mbs { for j := range mbs {
if mbs[j].Subject.Kind == "User" && mbs[j].Subject.Name == profile.Name { if mbs[j].UserRef == profile.Name {
resultClusters = append(resultClusters, clusters[i]) resultClusters = append(resultClusters, clusters[i])
} }
} }
@@ -323,18 +347,19 @@ func Install(parent iris.Party) {
sp := parent.Party("/clusters") sp := parent.Party("/clusters")
sp.Post("", handler.CreateCluster()) sp.Post("", handler.CreateCluster())
sp.Get("", handler.ListClusters()) sp.Get("", handler.ListClusters())
sp.Get("/:name", handler.GetCluster())
sp.Delete("/:name", handler.DeleteCluster()) sp.Delete("/:name", handler.DeleteCluster())
sp.Post("/search", handler.SearchClusters()) sp.Post("/search", handler.SearchClusters())
sp.Get("/:name/members", handler.GetClusterMembers()) sp.Get("/:name/members", handler.ListClusterMembers())
sp.Post("/:name/members", handler.CreateClusterMember()) sp.Post("/:name/members", handler.CreateClusterMember())
sp.Delete("/:name/members/:member", handler.DeleteClusterMember()) sp.Delete("/:name/members/:member", handler.DeleteClusterMember())
sp.Put("/:name/members/:member", handler.UpdateClusterMember()) sp.Put("/:name/members/:member", handler.UpdateClusterMember())
sp.Get("/:name/members/:member", handler.GetClusterMember()) sp.Get("/:name/members/:member", handler.GetClusterMember())
sp.Get("/:name/clusterroles", handler.GetClusterRoles()) sp.Get("/:name/clusterroles", handler.ListClusterRoles())
sp.Post("/:name/clusterroles", handler.CreateClusterRole()) sp.Post("/:name/clusterroles", handler.CreateClusterRole())
sp.Put("/:name/clusterroles/:clusterrole", handler.UpdateClusterRole()) sp.Put("/:name/clusterroles/:clusterrole", handler.UpdateClusterRole())
sp.Delete("/:name/clusterroles/:clusterrole", handler.DeleteClusterRole()) sp.Delete("/:name/clusterroles/:clusterrole", handler.DeleteClusterRole())
sp.Get("/:name/apigroups", handler.ListApiGroups()) sp.Get("/:name/apigroups", handler.ListApiGroups())
sp.Get("/:name/apigroups/{group:path}", handler.ListApiGroupResources()) sp.Get("/:name/apigroups/{group:path}", handler.ListApiGroupResources())
sp.Get("/:name/namespaces", handler.ListNamespace())
} }

View File

@@ -3,7 +3,6 @@ package cluster
import ( import (
goContext "context" goContext "context"
"fmt" "fmt"
"github.com/KubeOperator/ekko/internal/api/v1/session"
"github.com/KubeOperator/ekko/internal/service/v1/common" "github.com/KubeOperator/ekko/internal/service/v1/common"
"github.com/KubeOperator/ekko/pkg/kubernetes" "github.com/KubeOperator/ekko/pkg/kubernetes"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
@@ -24,6 +23,13 @@ func (h *Handler) UpdateClusterRole() iris.Handler {
ctx.Values().Set("message", fmt.Sprintf("delete cluster failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("delete cluster failed: %s", err.Error()))
return return
} }
for i := range req.Rules {
for j := range req.Rules[i].APIGroups {
if req.Rules[i].APIGroups[j] == "core" {
req.Rules[i].APIGroups[j] = ""
}
}
}
c, err := h.clusterService.Get(name, common.DBOptions{}) c, err := h.clusterService.Get(name, common.DBOptions{})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
@@ -63,6 +69,15 @@ func (h *Handler) CreateClusterRole() iris.Handler {
ctx.Values().Set("message", fmt.Sprintf("delete cluster failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("delete cluster failed: %s", err.Error()))
return return
} }
for i := range req.Rules {
for j := range req.Rules[i].APIGroups {
if req.Rules[i].APIGroups[j] == "core" {
req.Rules[i].APIGroups[j] = ""
}
}
}
c, err := h.clusterService.Get(name, common.DBOptions{}) c, err := h.clusterService.Get(name, common.DBOptions{})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
@@ -76,16 +91,11 @@ func (h *Handler) CreateClusterRole() iris.Handler {
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
return return
} }
u := ctx.Values().Get("profile")
profile := u.(session.UserProfile)
req.Annotations = map[string]string{ req.Annotations = map[string]string{
"ekko-i18n": "none", "builtin": "false",
"created-by": profile.Name,
"created-at": time.Now().Format("2006-01-02 15:04:05"), "created-at": time.Now().Format("2006-01-02 15:04:05"),
} }
req.Labels = map[string]string{ req.Labels[kubernetes.LabelManageKey] = "ekko"
"manage": "ekko",
}
resp, err := client.RbacV1().ClusterRoles().Create(goContext.TODO(), &req, metav1.CreateOptions{}) resp, err := client.RbacV1().ClusterRoles().Create(goContext.TODO(), &req, metav1.CreateOptions{})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
@@ -134,7 +144,7 @@ func (h *Handler) DeleteClusterRole() iris.Handler {
} }
} }
func (h *Handler) GetClusterRoles() iris.Handler { func (h *Handler) ListClusterRoles() iris.Handler {
return func(ctx *context.Context) { return func(ctx *context.Context) {
name := ctx.Params().GetString("name") name := ctx.Params().GetString("name")
c, err := h.clusterService.Get(name, common.DBOptions{}) c, err := h.clusterService.Get(name, common.DBOptions{})
@@ -150,7 +160,7 @@ func (h *Handler) GetClusterRoles() iris.Handler {
ctx.Values().Set("message", fmt.Sprintf("get kubernetes client failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get kubernetes client failed: %s", err.Error()))
return return
} }
items, err := client.RbacV1().ClusterRoles().List(goContext.TODO(), metav1.ListOptions{LabelSelector: "manage=ekko"}) items, err := client.RbacV1().ClusterRoles().List(goContext.TODO(), metav1.ListOptions{LabelSelector: "kubeoperator.io/manage=ekko"})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster roles failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get cluster roles failed: %s", err.Error()))

View File

@@ -16,7 +16,6 @@ import (
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
rbacV1 "k8s.io/api/rbac/v1" rbacV1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"strings"
"time" "time"
) )
@@ -46,10 +45,7 @@ func (h *Handler) UpdateClusterMember() iris.Handler {
return return
} }
binding, err := h.clusterBindingService.GetBindingByClusterNameAndSubject(name, v1Cluster.Subject{ binding, err := h.clusterBindingService.GetBindingByClusterNameAndUserName(name, req.Name, common.DBOptions{})
Kind: req.Kind,
Name: req.Name,
}, common.DBOptions{})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster binding failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get cluster binding failed: %s", err.Error()))
@@ -57,7 +53,7 @@ func (h *Handler) UpdateClusterMember() iris.Handler {
} }
clusterRoleBindings, err := client.RbacV1().ClusterRoleBindings().List(goContext.TODO(), metav1.ListOptions{ clusterRoleBindings, err := client.RbacV1().ClusterRoleBindings().List(goContext.TODO(), metav1.ListOptions{
LabelSelector: fmt.Sprintf("subject-name=%s,subject-kind=%s", binding.Subject.Name, binding.Subject.Kind), LabelSelector: fmt.Sprintf("user-name=%s", binding.UserRef),
}) })
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
@@ -96,25 +92,22 @@ func (h *Handler) UpdateClusterMember() iris.Handler {
return return
} }
} }
u := ctx.Values().Get("profile")
profile := u.(session.UserProfile)
for i := range forCreate { for i := range forCreate {
b := rbacV1.ClusterRoleBinding{ b := rbacV1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s-%s", name, req.Name, forCreate[i]), Name: fmt.Sprintf("%s-%s-%s", name, req.Name, forCreate[i]),
Labels: map[string]string{ Labels: map[string]string{
"manage": "ekko", "kubeoperator.io/manage": "ekko",
"subject-name": req.Name, "user-name": req.Name,
"subject-kind": req.Kind,
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"created-by": profile.Name, "builtin": "false",
"created-at": time.Now().Format("2006-01-02 15:04:05"), "created-at": time.Now().Format("2006-01-02 15:04:05"),
}, },
}, },
Subjects: []rbacV1.Subject{ Subjects: []rbacV1.Subject{
{ {
Kind: req.Kind, Kind: "User",
Name: req.Name, Name: req.Name,
}, },
}, },
@@ -140,27 +133,6 @@ func (h *Handler) GetClusterMember() iris.Handler {
name := ctx.Params().GetString("name") name := ctx.Params().GetString("name")
memberName := ctx.Params().Get("member") memberName := ctx.Params().Get("member")
var kind string
if ctx.URLParamExists("kind") {
kind = ctx.URLParam("kind")
}
subject := v1Cluster.Subject{
Name: memberName,
}
if kind != "" {
switch strings.ToLower(kind) {
case "user":
subject.Kind = "User"
case "group":
subject.Kind = "Group"
default:
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", fmt.Errorf("invalid kind %s", kind)))
return
}
subject.Kind = kind
}
c, err := h.clusterService.Get(name, common.DBOptions{}) c, err := h.clusterService.Get(name, common.DBOptions{})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
@@ -175,7 +147,7 @@ func (h *Handler) GetClusterMember() iris.Handler {
return return
} }
binding, err := h.clusterBindingService.GetBindingByClusterNameAndSubject(name, subject, common.DBOptions{}) binding, err := h.clusterBindingService.GetBindingByClusterNameAndUserName(name, memberName, common.DBOptions{})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster binding failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get cluster binding failed: %s", err.Error()))
@@ -183,7 +155,7 @@ func (h *Handler) GetClusterMember() iris.Handler {
} }
clusterRoleBindings, err := client.RbacV1().ClusterRoleBindings().List(goContext.TODO(), metav1.ListOptions{ clusterRoleBindings, err := client.RbacV1().ClusterRoleBindings().List(goContext.TODO(), metav1.ListOptions{
LabelSelector: fmt.Sprintf("subject-name=%s,subject-kind=%s", binding.Subject.Name, binding.Subject.Kind), LabelSelector: fmt.Sprintf("user-name=%s", binding.UserRef),
}) })
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
@@ -193,8 +165,7 @@ func (h *Handler) GetClusterMember() iris.Handler {
var member Member var member Member
member.ClusterRoles = make([]string, 0) member.ClusterRoles = make([]string, 0)
member.Kind = binding.Subject.Kind member.Name = binding.UserRef
member.Name = binding.Subject.Name
set := collectons.NewStringSet() set := collectons.NewStringSet()
for i := range clusterRoleBindings.Items { for i := range clusterRoleBindings.Items {
set.Add(clusterRoleBindings.Items[i].RoleRef.Name) set.Add(clusterRoleBindings.Items[i].RoleRef.Name)
@@ -205,7 +176,7 @@ func (h *Handler) GetClusterMember() iris.Handler {
} }
func (h *Handler) GetClusterMembers() iris.Handler { func (h *Handler) ListClusterMembers() iris.Handler {
return func(ctx *context.Context) { return func(ctx *context.Context) {
name := ctx.Params().GetString("name") name := ctx.Params().GetString("name")
bindings, err := h.clusterBindingService.GetClusterBindingByClusterName(name, common.DBOptions{}) bindings, err := h.clusterBindingService.GetClusterBindingByClusterName(name, common.DBOptions{})
@@ -214,17 +185,12 @@ func (h *Handler) GetClusterMembers() iris.Handler {
ctx.Values().Set("message", err.Error()) ctx.Values().Set("message", err.Error())
return return
} }
subjectMap := map[v1Cluster.Subject]v1Cluster.Binding{} var members []Member
for i := range bindings { for i := range bindings {
subjectMap[bindings[i].Subject] = bindings[i]
}
members := make([]Member, 0)
for key := range subjectMap {
members = append(members, Member{ members = append(members, Member{
Name: key.Name, Name: bindings[i].UserRef,
Kind: key.Kind, BindingName: bindings[i].Name,
BindingName: subjectMap[key].Name, CreateAt: bindings[i].CreateAt,
CreateAt: subjectMap[key].CreateAt,
}) })
} }
ctx.Values().Set("data", members) ctx.Values().Set("data", members)
@@ -238,7 +204,7 @@ func (h *Handler) CreateClusterMember() iris.Handler {
err := ctx.ReadJSON(&req) err := ctx.ReadJSON(&req)
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusBadRequest) ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", fmt.Sprintf("delete cluster failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("create cluster member failed: %s", err.Error()))
return return
} }
u := ctx.Values().Get("profile") u := ctx.Values().Get("profile")
@@ -249,12 +215,9 @@ func (h *Handler) CreateClusterMember() iris.Handler {
CreatedBy: profile.Name, CreatedBy: profile.Name,
}, },
Metadata: v1.Metadata{ Metadata: v1.Metadata{
Name: fmt.Sprintf("%s-%s-cluster-binding-", name, req.Name), Name: fmt.Sprintf("%s-%s-cluster-binding", name, req.Name),
},
Subject: v1Cluster.Subject{
Name: req.Name,
Kind: req.Kind,
}, },
UserRef: req.Name,
ClusterRef: name, ClusterRef: name,
} }
c, err := h.clusterService.Get(name, common.DBOptions{}) c, err := h.clusterService.Get(name, common.DBOptions{})
@@ -264,7 +227,6 @@ func (h *Handler) CreateClusterMember() iris.Handler {
return return
} }
k := kubernetes.NewKubernetes(*c) k := kubernetes.NewKubernetes(*c)
if req.Kind == "User" {
cert, err := k.CreateCommonUser(req.Name) cert, err := k.CreateCommonUser(req.Name)
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
@@ -272,7 +234,6 @@ func (h *Handler) CreateClusterMember() iris.Handler {
return return
} }
binding.Certificate = cert binding.Certificate = cert
}
if err := h.clusterBindingService.CreateClusterBinding(&binding, common.DBOptions{}); err != nil { if err := h.clusterBindingService.CreateClusterBinding(&binding, common.DBOptions{}); err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error()) ctx.Values().Set("message", err.Error())
@@ -284,25 +245,23 @@ func (h *Handler) CreateClusterMember() iris.Handler {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get k8s client failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get k8s client failed: %s", err.Error()))
return return
} }
for i := range req.ClusterRoles { for i := range req.ClusterRoles {
binding := rbacV1.ClusterRoleBinding{ binding := rbacV1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{ ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s-%s", name, req.Name, req.ClusterRoles[i]), Name: fmt.Sprintf("%s-%s", req.Name, req.ClusterRoles[i]),
Labels: map[string]string{ Labels: map[string]string{
"manage": "ekko", "kubeoperator.io/manage": "ekko",
"subject-name": req.Name, "user-name": req.Name,
"subject-kind": req.Kind,
}, },
Annotations: map[string]string{ Annotations: map[string]string{
"created-by": profile.Name, "builtin": "false",
"created-at": time.Now().Format("2006-01-02 15:04:05"), "created-at": time.Now().Format("2006-01-02 15:04:05"),
}, },
}, },
Subjects: []rbacV1.Subject{ Subjects: []rbacV1.Subject{
{ {
Kind: req.Kind, Kind: "User",
Name: req.Name, Name: req.Name,
}, },
}, },
@@ -318,6 +277,42 @@ func (h *Handler) CreateClusterMember() iris.Handler {
return return
} }
} }
// 创建Rolebinding
for i := range req.NamespaceRoles {
for j := range req.NamespaceRoles[i].Roles {
b := rbacV1.RoleBinding{
ObjectMeta: metav1.ObjectMeta{
Namespace: req.NamespaceRoles[i].Namespace,
Name: fmt.Sprintf("%s-%s", req.Name, req.NamespaceRoles[i].Roles[j]),
Labels: map[string]string{
"kubeoperator.io/manage": "ekko",
"user-name": req.Name,
},
Annotations: map[string]string{
"builtin": "false",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
},
Subjects: []rbacV1.Subject{
{
Kind: "User",
Name: req.Name,
},
},
RoleRef: rbacV1.RoleRef{
Kind: "ClusterRole",
Name: req.NamespaceRoles[i].Roles[j],
},
}
_, err := client.RbacV1().RoleBindings(req.NamespaceRoles[i].Namespace).Create(goContext.TODO(), &b, metav1.CreateOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("create role binding failed: %s", err.Error()))
return
}
}
}
ctx.Values().Set("data", req) ctx.Values().Set("data", req)
} }
} }
@@ -325,11 +320,7 @@ func (h *Handler) CreateClusterMember() iris.Handler {
func (h *Handler) DeleteClusterMember() iris.Handler { func (h *Handler) DeleteClusterMember() iris.Handler {
return func(ctx *context.Context) { return func(ctx *context.Context) {
name := ctx.Params().GetString("name") name := ctx.Params().GetString("name")
member := ctx.Params().GetString("member") memberName := ctx.Params().GetString("member")
var kind string
if ctx.URLParamExists("kind") {
kind = ctx.URLParam("kind")
}
u := ctx.Values().Get("profile") u := ctx.Values().Get("profile")
profile := u.(session.UserProfile) profile := u.(session.UserProfile)
c, err := h.clusterService.Get(name, common.DBOptions{}) c, err := h.clusterService.Get(name, common.DBOptions{})
@@ -338,29 +329,13 @@ func (h *Handler) DeleteClusterMember() iris.Handler {
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
return return
} }
if c.CreatedBy == profile.Name { if c.CreatedBy == memberName {
ctx.StatusCode(iris.StatusBadRequest) ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", fmt.Sprintf("can not delete cluster importer %s", profile.Name)) ctx.Values().Set("message", fmt.Sprintf("can not delete cluster importer %s", profile.Name))
return return
} }
subject := v1Cluster.Subject{ binding, err := h.clusterBindingService.GetBindingByClusterNameAndUserName(c.Name, memberName, common.DBOptions{})
Name: member,
}
if kind != "" {
switch strings.ToLower(kind) {
case "user":
subject.Kind = "User"
case "group":
subject.Kind = "Group"
default:
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", fmt.Errorf("invalid kind %s", kind)))
return
}
subject.Kind = kind
}
binding, err := h.clusterBindingService.GetBindingByClusterNameAndSubject(c.Name, subject, common.DBOptions{})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
@@ -388,7 +363,7 @@ func (h *Handler) DeleteClusterMember() iris.Handler {
if err = client.RbacV1().ClusterRoleBindings().DeleteCollection(goContext.TODO(), if err = client.RbacV1().ClusterRoleBindings().DeleteCollection(goContext.TODO(),
metav1.DeleteOptions{}, metav1.DeleteOptions{},
metav1.ListOptions{ metav1.ListOptions{
LabelSelector: fmt.Sprintf("subject-name=%s,subject-kind=%s", subject.Name, subject.Kind), LabelSelector: fmt.Sprintf("user-name=%s", memberName),
}); err != nil { }); err != nil {
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("delete cluster role failed: %s", err.Error())) ctx.Values().Set("message", fmt.Sprintf("delete cluster role failed: %s", err.Error()))

View File

@@ -0,0 +1,39 @@
package cluster
import (
goContext "context"
"fmt"
"github.com/KubeOperator/ekko/internal/service/v1/common"
"github.com/KubeOperator/ekko/pkg/kubernetes"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
func (h *Handler) ListNamespace() iris.Handler {
return func(ctx *context.Context) {
name := ctx.Params().GetString("name")
c, err := h.clusterService.Get(name, common.DBOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
return
}
k := kubernetes.NewKubernetes(*c)
client, err := k.Client()
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get k8s client failed: %s", err.Error()))
return
}
ns, err := client.CoreV1().Namespaces().List(goContext.TODO(), metav1.ListOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get k8s client failed: %s", err.Error()))
return
}
ctx.Values().Set("data", ns.Items)
}
}

View File

@@ -0,0 +1,63 @@
package cluster
import (
"errors"
"fmt"
v1Project "github.com/KubeOperator/ekko/internal/model/v1/project"
"github.com/KubeOperator/ekko/internal/service/v1/common"
"github.com/KubeOperator/ekko/pkg/kubernetes"
"github.com/asdine/storm/v3"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
)
func (h *Handler) CreateProject() iris.Handler {
return func(ctx *context.Context) {
name := ctx.Params().GetString("name")
var req v1Project.Project
if err := ctx.ReadJSON(&req); err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", err)
return
}
c, err := h.clusterService.Get(name, common.DBOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
return
}
k := kubernetes.NewKubernetes(*c)
cert, err := k.CreateCommonUser(req.Name)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err)
return
}
req.Certificate = cert
if err := h.projectService.Create(c.Name, &req, common.DBOptions{}); err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err)
return
}
ctx.Values().Set("data", req)
}
}
func (h *Handler) ListProject() iris.Handler {
return func(ctx *context.Context) {
name := ctx.Params().GetString("name")
c, err := h.clusterService.Get(name, common.DBOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
return
}
ps, err := h.projectService.List(c.Name, common.DBOptions{})
if err != nil && !errors.Is(err, storm.ErrNotFound) {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err)
return
}
ctx.Values().Set("data", ps)
}
}

View File

@@ -12,10 +12,15 @@ type Cluster struct {
CaDataStr string `json:"caDataStr"` CaDataStr string `json:"caDataStr"`
} }
type NamespaceRoles struct {
Namespace string `json:"namespace"`
Roles []string `json:"roles"`
}
type Member struct { type Member struct {
Name string `json:"name"` Name string `json:"name"`
Kind string `json:"kind"`
ClusterRoles []string `json:"clusterRoles"` ClusterRoles []string `json:"clusterRoles"`
CreateAt time.Time `json:"createAt"`
BindingName string `json:"bindingName"` BindingName string `json:"bindingName"`
CreateAt time.Time `json:"createAt"`
NamespaceRoles []NamespaceRoles `json:"namespaceRoles"`
} }

View File

@@ -4,7 +4,6 @@ import (
"encoding/pem" "encoding/pem"
"fmt" "fmt"
"github.com/KubeOperator/ekko/internal/api/v1/session" "github.com/KubeOperator/ekko/internal/api/v1/session"
v1Cluster "github.com/KubeOperator/ekko/internal/model/v1/cluster"
"github.com/KubeOperator/ekko/internal/service/v1/cluster" "github.com/KubeOperator/ekko/internal/service/v1/cluster"
"github.com/KubeOperator/ekko/internal/service/v1/clusterbinding" "github.com/KubeOperator/ekko/internal/service/v1/clusterbinding"
"github.com/KubeOperator/ekko/internal/service/v1/common" "github.com/KubeOperator/ekko/internal/service/v1/common"
@@ -41,10 +40,7 @@ func (h *Handler) KubernetesAPIProxy() iris.Handler {
} }
u := ctx.Values().Get("profile") u := ctx.Values().Get("profile")
profile := u.(session.UserProfile) profile := u.(session.UserProfile)
binding, err := h.clusterBindingService.GetBindingByClusterNameAndSubject(name, v1Cluster.Subject{ binding, err := h.clusterBindingService.GetBindingByClusterNameAndUserName(name, profile.Name, common.DBOptions{})
Kind: "User",
Name: profile.Name,
}, common.DBOptions{})
if err != nil { if err != nil {
ctx.StatusCode(iris.StatusForbidden) ctx.StatusCode(iris.StatusForbidden)
ctx.Values().Set("message", fmt.Sprintf("user %s not cluster %s member ", profile.Name, name)) ctx.Values().Set("message", fmt.Sprintf("user %s not cluster %s member ", profile.Name, name))

View File

@@ -1,30 +1,37 @@
package session package session
import ( import (
goContext "context"
"errors" "errors"
"fmt" "fmt"
v1Role "github.com/KubeOperator/ekko/internal/model/v1/role" v1Role "github.com/KubeOperator/ekko/internal/model/v1/role"
"github.com/KubeOperator/ekko/internal/service/v1/cluster"
"github.com/KubeOperator/ekko/internal/service/v1/common" "github.com/KubeOperator/ekko/internal/service/v1/common"
"github.com/KubeOperator/ekko/internal/service/v1/role" "github.com/KubeOperator/ekko/internal/service/v1/role"
"github.com/KubeOperator/ekko/internal/service/v1/rolebinding" "github.com/KubeOperator/ekko/internal/service/v1/rolebinding"
"github.com/KubeOperator/ekko/internal/service/v1/user" "github.com/KubeOperator/ekko/internal/service/v1/user"
pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1" pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1"
"github.com/KubeOperator/ekko/pkg/collectons" "github.com/KubeOperator/ekko/pkg/collectons"
"github.com/KubeOperator/ekko/pkg/kubernetes"
"github.com/asdine/storm/v3" "github.com/asdine/storm/v3"
"github.com/kataras/iris/v12" "github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context" "github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/sessions" "github.com/kataras/iris/v12/sessions"
"golang.org/x/crypto/bcrypt" "golang.org/x/crypto/bcrypt"
v1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
) )
type Handler struct { type Handler struct {
userService user.Service userService user.Service
roleService role.Service roleService role.Service
clusterService cluster.Service
rolebindingService rolebinding.Service rolebindingService rolebinding.Service
} }
func NewHandler() *Handler { func NewHandler() *Handler {
return &Handler{ return &Handler{
clusterService: cluster.NewService(),
userService: user.NewService(), userService: user.NewService(),
roleService: role.NewService(), roleService: role.NewService(),
rolebindingService: rolebinding.NewService(), rolebindingService: rolebinding.NewService(),
@@ -184,11 +191,106 @@ func (h *Handler) GetProfile() iris.Handler {
} }
} }
func (h *Handler) ListUserNamespace() iris.Handler {
return func(ctx *context.Context) {
name := ctx.Params().GetString("cluster_name")
c, err := h.clusterService.Get(name, common.DBOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
return
}
session := sessions.Get(ctx)
u := session.Get("profile")
profile := u.(UserProfile)
k := kubernetes.NewKubernetes(*c)
client, err := k.Client()
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get k8s client failed: %s", err.Error()))
return
}
rbs, err := client.RbacV1().RoleBindings("").List(goContext.TODO(), metav1.ListOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get k8s client failed: %s", err.Error()))
return
}
namespaceSet := collectons.NewStringSet()
for i := range rbs.Items {
for j := range rbs.Items[i].Subjects {
if rbs.Items[i].Subjects[j].Kind == "User" && rbs.Items[i].Subjects[j].Name == profile.Name {
namespaceSet.Add(rbs.Items[i].Namespace)
}
}
}
ctx.Values().Set("data", namespaceSet.ToSlice())
}
}
func (h *Handler) GetClusterProfile() iris.Handler {
return func(ctx *context.Context) {
session := sessions.Get(ctx)
clusterName := ctx.Params().GetString("cluster_name")
c, err := h.clusterService.Get(clusterName, common.DBOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error())
return
}
k := kubernetes.NewKubernetes(*c)
client, err := k.Client()
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get k8s client failed: %s", err.Error()))
return
}
u := session.Get("profile")
profile := u.(UserProfile)
clusterRoleBindings, err := client.RbacV1().ClusterRoleBindings().List(goContext.TODO(), metav1.ListOptions{
LabelSelector: fmt.Sprintf("user-name=%s", profile.Name),
})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster-role-binding failed: %s", err.Error()))
return
}
roleSet := map[string]struct{}{}
for i := range clusterRoleBindings.Items {
for j := range clusterRoleBindings.Items[i].Subjects {
if clusterRoleBindings.Items[i].Subjects[j].Kind == "User" {
roleSet[clusterRoleBindings.Items[i].RoleRef.Name] = struct{}{}
}
}
}
var roles []v1.ClusterRole
for key := range roleSet {
r, err := client.RbacV1().ClusterRoles().Get(goContext.TODO(), key, metav1.GetOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster-role failed: %s", err.Error()))
return
}
roles = append(roles, *r)
}
crp := ClusterUserProfile{
UserProfile: profile,
ClusterRoles: roles,
}
ctx.Values().Set("data", &crp)
}
}
func Install(parent iris.Party) { func Install(parent iris.Party) {
handler := NewHandler() handler := NewHandler()
sp := parent.Party("/sessions") sp := parent.Party("/sessions")
sp.Post("", handler.Login()) sp.Post("", handler.Login())
sp.Delete("", handler.Logout()) sp.Delete("", handler.Logout())
sp.Get("", handler.GetProfile()) sp.Get("", handler.GetProfile())
sp.Get("/:cluster_name", handler.GetClusterProfile())
sp.Get("/status", handler.IsLogin()) sp.Get("/status", handler.IsLogin())
sp.Get("/:cluster_name/namespaces", handler.ListUserNamespace())
} }

View File

@@ -1,5 +1,7 @@
package session package session
import v1 "k8s.io/api/rbac/v1"
type LoginCredential struct { type LoginCredential struct {
Username string `json:"username"` Username string `json:"username"`
Email string `json:"email"` Email string `json:"email"`
@@ -12,3 +14,8 @@ type UserProfile struct {
Email string `json:"email"` Email string `json:"email"`
ResourcePermissions map[string][]string `json:"resourcePermissions"` ResourcePermissions map[string][]string `json:"resourcePermissions"`
} }
type ClusterUserProfile struct {
UserProfile
ClusterRoles []v1.ClusterRole `json:"clusterRoles"`
}

View File

@@ -5,7 +5,6 @@ import (
"fmt" "fmt"
"github.com/KubeOperator/ekko/internal/api/v1/session" "github.com/KubeOperator/ekko/internal/api/v1/session"
v1 "github.com/KubeOperator/ekko/internal/model/v1" v1 "github.com/KubeOperator/ekko/internal/model/v1"
v1Cluster "github.com/KubeOperator/ekko/internal/model/v1/cluster"
v1Role "github.com/KubeOperator/ekko/internal/model/v1/role" v1Role "github.com/KubeOperator/ekko/internal/model/v1/role"
"github.com/KubeOperator/ekko/internal/server" "github.com/KubeOperator/ekko/internal/server"
"github.com/KubeOperator/ekko/internal/service/v1/clusterbinding" "github.com/KubeOperator/ekko/internal/service/v1/clusterbinding"
@@ -136,10 +135,7 @@ func (h *Handler) DeleteUser() iris.Handler {
return return
} }
} }
cbs, err := h.clusterBindingService.GetBindingsBySubject(v1Cluster.Subject{ cbs, err := h.clusterBindingService.GetBindingsByUserName(userName, txOptions)
Kind: "User",
Name: userName,
}, txOptions)
if err != nil && !errors.As(err, &storm.ErrNotFound) { if err != nil && !errors.As(err, &storm.ErrNotFound) {
_ = tx.Rollback() _ = tx.Rollback()
ctx.StatusCode(iris.StatusInternalServerError) ctx.StatusCode(iris.StatusInternalServerError)

View File

@@ -2,15 +2,10 @@ package cluster
import v1 "github.com/KubeOperator/ekko/internal/model/v1" import v1 "github.com/KubeOperator/ekko/internal/model/v1"
type Subject struct {
Kind string `json:"kind"`
Name string `json:"name"`
}
type Binding struct { type Binding struct {
v1.BaseModel `storm:"inline"` v1.BaseModel `storm:"inline"`
v1.Metadata `storm:"inline"` v1.Metadata `storm:"inline"`
Subject Subject `json:"subject" storm:"inline"` UserRef string `json:"UserRef" storm:"inline"`
ClusterRef string `json:"clusterRef" storm:"index"` ClusterRef string `json:"clusterRef" storm:"index"`
Certificate []byte `json:"certificate"` Certificate []byte `json:"certificate"`
} }

View File

@@ -0,0 +1,12 @@
package project
import v1 "github.com/KubeOperator/ekko/internal/model/v1"
type Project struct {
v1.BaseModel `storm:"inline"`
v1.Metadata `storm:"inline"`
Users []string `json:"users"`
ClusterRef string `json:"clusterRef" storm:"index"`
ProjectName string `json:"projectName"`
Certificate []byte `json:"certificate"`
}

View File

@@ -14,10 +14,9 @@ type Service interface {
GetClusterBindingByClusterName(clusterName string, options common.DBOptions) ([]v1Cluster.Binding, error) GetClusterBindingByClusterName(clusterName string, options common.DBOptions) ([]v1Cluster.Binding, error)
CreateClusterBinding(binding *v1Cluster.Binding, options common.DBOptions) error CreateClusterBinding(binding *v1Cluster.Binding, options common.DBOptions) error
UpdateClusterBinding(name string, binding *v1Cluster.Binding, options common.DBOptions) error UpdateClusterBinding(name string, binding *v1Cluster.Binding, options common.DBOptions) error
GetBindingByClusterNameAndUserName(clusterName string, userName string, options common.DBOptions) (*v1Cluster.Binding, error)
GetBindingsByUserName(userName string, options common.DBOptions) ([]v1Cluster.Binding, error)
Delete(name string, options common.DBOptions) error Delete(name string, options common.DBOptions) error
GetBindingByClusterNameAndSubject(clusterName string, subject v1Cluster.Subject, options common.DBOptions) (*v1Cluster.Binding, error)
GetBindingsBySubject(subject v1Cluster.Subject, options common.DBOptions) ([]v1Cluster.Binding, error)
} }
func NewService() Service { func NewService() Service {
@@ -45,9 +44,9 @@ func (s *service) UpdateClusterBinding(name string, binding *v1Cluster.Binding,
return db.Update(binding) return db.Update(binding)
} }
func (s *service) GetBindingsBySubject(subject v1Cluster.Subject, options common.DBOptions) ([]v1Cluster.Binding, error) { func (s *service) GetBindingsByUserName(userName string, options common.DBOptions) ([]v1Cluster.Binding, error) {
db := s.GetDB(options) db := s.GetDB(options)
query := db.Select(q.Eq("Subject", subject)) query := db.Select(q.Eq("UserRef", userName))
var rbs []v1Cluster.Binding var rbs []v1Cluster.Binding
if err := query.Find(&rbs); err != nil { if err := query.Find(&rbs); err != nil {
return rbs, err return rbs, err
@@ -55,9 +54,9 @@ func (s *service) GetBindingsBySubject(subject v1Cluster.Subject, options common
return rbs, nil return rbs, nil
} }
func (s *service) GetBindingByClusterNameAndSubject(clusterName string, subject v1Cluster.Subject, options common.DBOptions) (*v1Cluster.Binding, error) { func (s *service) GetBindingByClusterNameAndUserName(clusterName string, userName string, options common.DBOptions) (*v1Cluster.Binding, error) {
db := s.GetDB(options) db := s.GetDB(options)
query := db.Select(q.And(q.Eq("ClusterRef", clusterName), q.Eq("Subject", subject))) query := db.Select(q.And(q.Eq("ClusterRef", clusterName), q.Eq("UserRef", userName)))
var rb v1Cluster.Binding var rb v1Cluster.Binding
if err := query.First(&rb); err != nil { if err := query.First(&rb); err != nil {
return nil, err return nil, err

View File

@@ -0,0 +1,71 @@
package project
import (
"errors"
v1Project "github.com/KubeOperator/ekko/internal/model/v1/project"
"github.com/KubeOperator/ekko/internal/service/v1/common"
"github.com/google/uuid"
"time"
)
type Service interface {
common.DBService
Create(clusterName string, r *v1Project.Project, options common.DBOptions) error
Get(name string, options common.DBOptions) (*v1Project.Project, error)
List(clusterName string, options common.DBOptions) ([]v1Project.Project, error)
Delete(name string, options common.DBOptions) error
Update(name string, role *v1Project.Project, options common.DBOptions) error
}
func NewService() Service {
return &service{}
}
type service struct {
common.DefaultDBService
}
func (s service) Create(clusterName string, p *v1Project.Project, options common.DBOptions) error {
db := s.GetDB(options)
p.UUID = uuid.New().String()
p.CreateAt = time.Now()
p.UpdateAt = time.Now()
p.ClusterRef = clusterName
return db.Save(p)
}
func (s service) Get(name string, options common.DBOptions) (*v1Project.Project, error) {
db := s.GetDB(options)
var p v1Project.Project
if err := db.One("Name", name, &p); err != nil {
return nil, err
}
return &p, nil
}
func (s service) List(clusterName string, options common.DBOptions) ([]v1Project.Project, error) {
db := s.GetDB(options)
rs := make([]v1Project.Project, 0)
if err := db.Find("ClusterRef", clusterName, &rs); err != nil {
return rs, err
}
return rs, nil
}
func (s service) Delete(name string, options common.DBOptions) error {
db := s.GetDB(options)
item, err := s.Get(name, options)
if err != nil {
return err
}
if item.BuiltIn {
return errors.New("can not delete this resource,because it created by system")
}
return db.DeleteStruct(item)
}
func (s service) Update(name string, role *v1Project.Project, options common.DBOptions) error {
panic("implement me")
}

419
pkg/kubernetes/constants.go Normal file
View File

@@ -0,0 +1,419 @@
package kubernetes
import (
rbacV1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"time"
)
const (
LabelManageKey = "kubeoperator.io/manage"
LabelRoleTypeKey = "kubeoperator.io/role-type"
RoleTypeCluster = "cluster"
RoleTypeNamespace = "namespace"
)
var initClusterRoles = []rbacV1.ClusterRole{
{
ObjectMeta: metav1.ObjectMeta{
Name: "Admin Cluster",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeCluster,
},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{"*"},
Resources: []string{"*"},
Verbs: []string{"*"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "View Cluster",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeCluster},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{"*"},
Resources: []string{"*"},
Verbs: []string{"list", "get", "watch"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "Manage Namespaces",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeCluster},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"namespaces"},
Verbs: []string{"*"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "View Namespaces",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeCluster},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"namespaces"},
Verbs: []string{"list", "get", "watch"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "View Nodes",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeCluster},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"nodes"},
Verbs: []string{"*"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "View Nodes",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeCluster},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"nodes"},
Verbs: []string{"list", "get", "watch"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "View Events",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeCluster},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"events"},
Verbs: []string{"list", "get", "watch"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "Manage Network",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeNamespace,
},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"services", "endpoints"},
Verbs: []string{"*"},
},
{
APIGroups: []string{"networking.k8s.io"},
Resources: []string{"ingresses", "networkpolicies"},
Verbs: []string{"*"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "View Network",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeNamespace},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"services", "endpoints"},
Verbs: []string{"list", "get", "watch"},
},
{
APIGroups: []string{"networking.k8s.io"},
Resources: []string{"ingresses", "networkpolicies"},
Verbs: []string{"list", "get", "watch"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "Manage Config",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeNamespace},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"configmaps,secrets,resourcequotas,limitranges"},
Verbs: []string{"*"},
},
{
APIGroups: []string{"autoscaling"},
Resources: []string{"horizontalpodautoscalers"},
Verbs: []string{"*"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "View Config",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeNamespace},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"configmaps,secrets,resourcequotas,limitranges"},
Verbs: []string{"list", "get", "watch"},
},
{
APIGroups: []string{"autoscaling"},
Resources: []string{"horizontalpodautoscalers"},
Verbs: []string{"list", "get", "watch"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "Manage Storage",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeNamespace,
},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"persistentvolumes", "persistentvolumeclaims"},
Verbs: []string{"*"},
},
{
APIGroups: []string{"storage.k8s.io"},
Resources: []string{"storageclasses"},
Verbs: []string{"*"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "View Storage",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeNamespace,
},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"persistentvolumes", "persistentvolumeclaims"},
Verbs: []string{"list", "get", "watch"},
},
{
APIGroups: []string{"storage.k8s.io"},
Resources: []string{"storageclasses"},
Verbs: []string{"list", "get", "watch"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "Manage Workload",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeNamespace,
},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"pods", "containers"},
Verbs: []string{"*"},
},
{
APIGroups: []string{"apps"},
Resources: []string{"deployments", "daemonsets", "replicasets", "statefulsets"},
Verbs: []string{"*"},
},
{
APIGroups: []string{"batch"},
Resources: []string{"jobs", "cronjobs"},
Verbs: []string{"*"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "View Workload",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeNamespace,
},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{""},
Resources: []string{"pods", "containers"},
Verbs: []string{"list", "get", "watch"},
},
{
APIGroups: []string{"apps"},
Resources: []string{"deployments", "daemonsets", "replicasets", "statefulsets"},
Verbs: []string{"list", "get", "watch"},
},
{
APIGroups: []string{"batch"},
Resources: []string{"jobs", "cronjobs"},
Verbs: []string{"list", "get", "watch"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "Mange RBAC",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeNamespace,
},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{"rbac.authorization.k8s.io"},
Resources: []string{"clusterroles", "clusterrolebindings", "roles", "rolebindings"},
Verbs: []string{"*"},
},
{
APIGroups: []string{""},
Resources: []string{"serviceaccounts"},
Verbs: []string{"*"},
},
{
APIGroups: []string{"policy"},
Resources: []string{"podsecuritypolicies"},
Verbs: []string{"*"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "Mange RBAC",
Annotations: map[string]string{
"builtin": "true",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
LabelManageKey: "ekko",
LabelRoleTypeKey: RoleTypeNamespace,
},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{"rbac.authorization.k8s.io"},
Resources: []string{"clusterroles", "clusterrolebindings", "roles", "rolebindings"},
Verbs: []string{"list", "get", "watch"},
},
{
APIGroups: []string{""},
Resources: []string{"serviceaccounts"},
Verbs: []string{"list", "get", "watch"},
},
{
APIGroups: []string{"policy"},
Resources: []string{"podsecuritypolicies"},
Verbs: []string{"list", "get", "watch"},
},
},
},
}

View File

@@ -8,7 +8,6 @@ import (
"github.com/KubeOperator/ekko/pkg/certificate" "github.com/KubeOperator/ekko/pkg/certificate"
v1 "k8s.io/api/authorization/v1" v1 "k8s.io/api/authorization/v1"
certv1 "k8s.io/api/certificates/v1" certv1 "k8s.io/api/certificates/v1"
rbacV1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/version" "k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/kubernetes" "k8s.io/client-go/kubernetes"
@@ -40,69 +39,20 @@ func NewKubernetes(cluster v1Cluster.Cluster) Interface {
} }
func (k *Kubernetes) CreateDefaultClusterRoles() error { func (k *Kubernetes) CreateDefaultClusterRoles() error {
defaultRoles := []rbacV1.ClusterRole{
{
ObjectMeta: metav1.ObjectMeta{
Name: "ekko-admin",
Annotations: map[string]string{
"ekko-i18n": "cluster_administrator",
"created-by": "system",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
"manage": "ekko",
},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{"*"},
Resources: []string{"*"},
Verbs: []string{"*"},
},
{
NonResourceURLs: []string{"*"},
Verbs: []string{"*"},
},
},
},
{
ObjectMeta: metav1.ObjectMeta{
Name: "ekko-viewer",
Annotations: map[string]string{
"ekko-i18n": "cluster_viewer",
"created-by": "system",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
Labels: map[string]string{
"manage": "ekko",
},
},
Rules: []rbacV1.PolicyRule{
{
APIGroups: []string{"*"},
Resources: []string{"*"},
Verbs: []string{"list", "get"},
},
{
NonResourceURLs: []string{"*"},
Verbs: []string{"list", "get"},
},
},
},
}
client, err := k.Client() client, err := k.Client()
if err != nil { if err != nil {
return err return err
} }
for i := range defaultRoles { for i := range initClusterRoles {
instance, err := client.RbacV1().ClusterRoles().Get(context.TODO(), defaultRoles[i].Name, metav1.GetOptions{}) instance, err := client.RbacV1().ClusterRoles().Get(context.TODO(), initClusterRoles[i].Name, metav1.GetOptions{})
if err != nil { if err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "not found") { if !strings.Contains(strings.ToLower(err.Error()), "not found") {
return err return err
} }
} }
if instance == nil { if instance == nil || instance.Name == "" {
_, err = client.RbacV1().ClusterRoles().Create(context.TODO(), &defaultRoles[i], metav1.CreateOptions{}) _, err = client.RbacV1().ClusterRoles().Create(context.TODO(), &initClusterRoles[i], metav1.CreateOptions{})
if err != nil { if err != nil {
return err return err
} }

View File

@@ -14,6 +14,15 @@ export function getCurrentUser() {
return get(authUrl) return get(authUrl)
} }
export function getCurrentClusterUser(clusterName) {
return get(`${authUrl}/${clusterName}`)
}
export function isLogin() { export function isLogin() {
return get(`${authUrl}/status`) return get(`${authUrl}/status`)
} }
export function getNamespaces(clusterName) {
return get(`${authUrl}/${clusterName}/namespaces`)
}

View File

@@ -1,19 +1,7 @@
import {post, get, del} from "@/plugins/request" import {get} from "@/plugins/request"
const authUrl = "/api/v1/clusters" const authUrl = "/api/v1/clusters"
export function create(data) { export function getCluster(name) {
return post(authUrl, data) return get(`${authUrl}/${name}`)
}
export function listAll(){
return get(authUrl)
}
export function deleteBy(name){
return del(`${authUrl}/${name}`)
}
export function searchCluster(page,size) {
return post(`${authUrl}/search?pageNum=${page}&pageSize=${size}`)
} }

View File

@@ -0,0 +1,56 @@
<template>
<el-dropdown trigger="click" @command="handleProjectSwitch">
<span class="el-dropdown-link">
<i class="iconfont iconnamesapce" style="color: #3884c5;margin-right: 3px" :icon="['fas', 'globe']"/>
<span>{{ getProjectName }}</span>
<i class="el-icon-arrow-down el-icon--right"></i>
</span>
<el-dropdown-menu slot="dropdown">
<el-dropdown-item command="">全部</el-dropdown-item>
<el-dropdown-item disabled divided></el-dropdown-item>
<el-dropdown-item v-for="(value,key) in namespaceOptions" :key="key" :command="value">{{ value }}
</el-dropdown-item>
</el-dropdown-menu>
</el-dropdown>
</template>
<script>
import {getNamespaces} from '@/api/auth'
export default {
name: "ProjectSwitch",
data() {
return {
namespaceOptions: [],
}
},
computed: {
getProjectName() {
const p = sessionStorage.getItem("project")
return (p === null) ? "Global" : p
}
},
methods: {
handleProjectSwitch(command) {
if (!command) {
sessionStorage.removeItem("project")
} else {
sessionStorage.setItem("project", command)
}
location.reload()
}
},
created() {
const cluster = this.$store.getters.cluster
getNamespaces(cluster).then((data) => {
this.namespaceOptions = data.data
})
}
}
</script>
<style scoped>
</style>

View File

@@ -2,7 +2,7 @@
<el-dropdown trigger="click" @command="handleCommand"> <el-dropdown trigger="click" @command="handleCommand">
<span class="el-dropdown-link"> <span class="el-dropdown-link">
<i class="el-icon-user-solid" style="margin-right: 3px"></i> <i class="el-icon-user-solid" style="margin-right: 3px"></i>
<span>{{ name }}</span> <span>{{ nickName }}</span>
<i class="el-icon-arrow-down el-icon--right"></i> <i class="el-icon-arrow-down el-icon--right"></i>
</span> </span>
<el-dropdown-menu slot="dropdown"> <el-dropdown-menu slot="dropdown">
@@ -22,7 +22,7 @@
name: "UserSetting", name: "UserSetting",
computed: { computed: {
...mapGetters([ ...mapGetters([
"name" "nickName"
]) ])
}, },
methods: { methods: {

View File

@@ -5,7 +5,8 @@
<home></home> <home></home>
</div> </div>
<div class="header-right"> <div class="header-right">
<user-setting></user-setting> <project-switch></project-switch>
<user-setting style="margin-left: 20px"></user-setting>
</div> </div>
</div> </div>
</template> </template>
@@ -14,16 +15,18 @@
import SidebarToggleButton from "@/components/layout/sidebar/SidebarToggleButton" import SidebarToggleButton from "@/components/layout/sidebar/SidebarToggleButton"
import UserSetting from "@/business/app-layout/header-components/UserSetting" import UserSetting from "@/business/app-layout/header-components/UserSetting"
import Home from "@/business/app-layout/header-components/Home" import Home from "@/business/app-layout/header-components/Home"
import ProjectSwitch from "@/business/app-layout/header-components/ProjectSwitch";
export default { export default {
name: "HorizontalHeader", name: "HorizontalHeader",
components: { Home, UserSetting, SidebarToggleButton } components: {ProjectSwitch, Home, UserSetting, SidebarToggleButton}
} }
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@import "~@/styles/common/mixins"; @import "~@/styles/common/mixins";
.horizontal-header { .horizontal-header {
@include flex-row(flex-start, center); @include flex-row(flex-start, center);
position: relative; position: relative;
height: 100%; height: 100%;
@@ -50,5 +53,5 @@ export default {
margin-left: 20px; margin-left: 20px;
} }
} }
} }
</style> </style>

View File

@@ -3,10 +3,10 @@
<complex-table :selects.sync="selects" :data="data" :pagination-config="page" @search="search()"> <complex-table :selects.sync="selects" :data="data" :pagination-config="page" @search="search()">
<template #header> <template #header>
<el-button-group> <el-button-group>
<el-button type="primary" size="small" @click="onCreate"> <el-button v-has-permissions="{apiGroup:'',resource:'namespaces',verb:'create'}" type="primary" size="small" @click="onCreate">
{{ $t("commons.button.create") }} {{ $t("commons.button.create") }}
</el-button> </el-button>
<el-button type="primary" size="small" :disabled="selects.length===0" @click="onDelete()"> <el-button v-has-permissions="{apiGroup:'',resource:'namespaces',verb:'delete'}" type="primary" size="small" :disabled="selects.length===0" @click="onDelete()">
{{ $t("commons.button.delete") }} {{ $t("commons.button.delete") }}
</el-button> </el-button>
</el-button-group> </el-button-group>
@@ -58,6 +58,8 @@ import ComplexTable from "@/components/complex-table"
import {listNamespace, deleteNamespace} from "@/api/namespaces" import {listNamespace, deleteNamespace} from "@/api/namespaces"
import KoTableOperations from "@/components/ko-table-operations" import KoTableOperations from "@/components/ko-table-operations"
import {downloadYaml} from "@/utils/actions" import {downloadYaml} from "@/utils/actions"
import {checkPermissions} from "@/utils/permission"
export default { export default {
name: "NamespaceList", name: "NamespaceList",
@@ -73,21 +75,31 @@ export default {
path: "/namespaces/edit/"+row.metadata.name , path: "/namespaces/edit/"+row.metadata.name ,
query: { yamlShow: false } query: { yamlShow: false }
}) })
},
disabled:()=>{
return !checkPermissions({apiGroup:"",resource:"namespaces",verb:"update"})
} }
}, },
{ {
label: this.$t("commons.button.edit_yaml"), label: this.$t("commons.button.edit_yaml"),
icon: "el-icon-edit", icon: "el-icon-edit",
disabled:()=>{
return !checkPermissions({apiGroup:"",resource:"namespaces",verb:"update"})
},
click: (row) => { click: (row) => {
this.$router.push({ this.$router.push({
path: "/namespaces/edit/"+row.metadata.name , path: "/namespaces/edit/"+row.metadata.name ,
query: { yamlShow: true } query: { yamlShow: true }
}) })
} }
}, },
{ {
label: this.$t("commons.button.download_yaml"), label: this.$t("commons.button.download_yaml"),
icon: "el-icon-download", icon: "el-icon-download",
disabled:()=>{
return !checkPermissions({apiGroup:"",resource:"namespaces",verb:"get"})
},
click: (row) => { click: (row) => {
downloadYaml(row.metadata.name + ".yml", row) downloadYaml(row.metadata.name + ".yml", row)
} }
@@ -95,6 +107,9 @@ export default {
{ {
label: this.$t("commons.button.delete"), label: this.$t("commons.button.delete"),
icon: "el-icon-delete", icon: "el-icon-delete",
disabled:()=>{
return !checkPermissions({apiGroup:"",resource:"namespaces",verb:"delete"})
},
click: (row) => { click: (row) => {
this.onDelete(row) this.onDelete(row)
} }

View File

@@ -2,33 +2,26 @@
<layout-content :header="$t('business.dashboard.dashboard')"> <layout-content :header="$t('business.dashboard.dashboard')">
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="24"> <el-col :span="24">
<el-card shadow="always" style="background-color: #243441;height: 120px"> <el-card v-if="cluster" shadow="always" style="background-color: #243441;height: 120px">
<el-row :gutter="24"> <el-row :gutter="24">
<el-col :span="6"> <el-col :span="8">
<span class="title">{{ $t("commons.table.name") }}</span> <span class="title">{{ $t("commons.table.name") }}</span>
<div style="text-align: center"> <div style="text-align: center">
<h1>测试</h1> <h1>{{cluster.name}}</h1>
</div> </div>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="8">
<span class="title">{{ $t("business.cluster.version") }}</span> <span class="title">{{ $t("business.cluster.version") }}</span>
<div class="line"></div> <div class="line"></div>
<div style="text-align: center"> <div style="text-align: center">
<h1>v1.18.0</h1> <h1>{{cluster.status.version}}</h1>
</div> </div>
</el-col> </el-col>
<el-col :span="6"> <el-col :span="8">
<span class="title">{{ $t("business.cluster.nodes") }}</span>
<div class="line"></div>
<div style="text-align: center">
<h1>2</h1>
</div>
</el-col>
<el-col :span="6">
<span class="title">{{ $t("commons.table.created_time") }}</span> <span class="title">{{ $t("commons.table.created_time") }}</span>
<div class="line"></div> <div class="line"></div>
<div style="text-align: center"> <div style="text-align: center">
<h1>16 days ago</h1> <h1>{{fromNow(cluster.createAt)}} Days</h1>
</div> </div>
</el-col> </el-col>
</el-row> </el-row>
@@ -55,7 +48,9 @@
</el-card> </el-card>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="24"> <el-row :gutter="24" v-has-permissions="{apiGroup:'',resource:'events',verb:'list'}">
<!-- <el-row :gutter="24">-->
<h3>Events</h3> <h3>Events</h3>
<complex-table :pagination-config="page" :data="events" @search="search()" v-loading="loading"> <complex-table :pagination-config="page" :data="events" @search="search()" v-loading="loading">
<el-table-column label="Reason" prop="reason" fix max-width="50px"> <el-table-column label="Reason" prop="reason" fix max-width="50px">
@@ -89,24 +84,28 @@
</template> </template>
<script> <script>
import LayoutContent from "@/components/layout/LayoutContent" import LayoutContent from "@/components/layout/LayoutContent"
import KoCharts from "@/components/ko-charts" import KoCharts from "@/components/ko-charts"
import {listNamespace} from "@/api/namespaces" import {listNamespace} from "@/api/namespaces"
import {listIngresses} from "@/api/ingress" import {listIngresses} from "@/api/ingress"
import {listPvs} from "@/api/pv" import {listPvs} from "@/api/pv"
import {listDeployments} from "@/api/deployments" import {listDeployments} from "@/api/deployments"
import {listStatefulSets} from "@/api/statefulsets" import {listStatefulSets} from "@/api/statefulsets"
import {listJobs} from "@/api/jobs" import {listJobs} from "@/api/jobs"
import {listDaemonSets} from "@/api/daemonsets" import {listDaemonSets} from "@/api/daemonsets"
import {listServices} from "@/api/services" import {listServices} from "@/api/services"
import ComplexTable from "@/components/complex-table" import {listNodes} from "@/api/nodes"
import {listEvents} from "@/api/events" import ComplexTable from "@/components/complex-table"
import {listEvents} from "@/api/events"
import {getCluster} from "@/api/clusters"
import {checkPermissions} from "@/utils/permission"
export default { export default {
name: "Dashboard", name: "Dashboard",
components: { ComplexTable, KoCharts, LayoutContent }, components: {ComplexTable, KoCharts, LayoutContent},
data () { data() {
return { return {
cluster: null,
clusterName: "", clusterName: "",
resources: [], resources: [],
page: { page: {
@@ -118,7 +117,27 @@ export default {
} }
}, },
methods: { methods: {
listResources () {
fromNow(date) {
const a = new Date(date)
const b = new Date()
return parseInt(Math.abs(b - a) / 1000 / 60 / 60 / 24)
},
listResources() {
getCluster(this.clusterName).then(res => {
this.cluster = res.data;
})
if (checkPermissions({apiGroup: "", resource: "nodes", verb: "list"})) {
listNodes(this.clusterName).then(res => {
const nodes = {
name: "Nodes",
count: res.items.length,
data: this.getData(res.items, "status.phase")
}
this.resources.push(nodes)
})
}
if (checkPermissions({apiGroup: "", resource: "namespaces", verb: "list"})) {
listNamespace(this.clusterName).then(res => { listNamespace(this.clusterName).then(res => {
const namespaces = { const namespaces = {
name: "Namespaces", name: "Namespaces",
@@ -127,6 +146,8 @@ export default {
} }
this.resources.push(namespaces) this.resources.push(namespaces)
}) })
}
if (checkPermissions({apiGroup: "networking.k8s.io", resource: "ingresses", verb: "list"})) {
listIngresses(this.clusterName).then(res => { listIngresses(this.clusterName).then(res => {
const ingresses = { const ingresses = {
name: "Ingresses", name: "Ingresses",
@@ -138,6 +159,8 @@ export default {
} }
this.resources.push(ingresses) this.resources.push(ingresses)
}) })
}
if (checkPermissions({apiGroup: "", resource: "persistentvolumes", verb: "list"})) {
listPvs(this.clusterName).then(res => { listPvs(this.clusterName).then(res => {
const persistentVolumes = { const persistentVolumes = {
name: "PersistentVolumes", name: "PersistentVolumes",
@@ -146,6 +169,8 @@ export default {
} }
this.resources.push(persistentVolumes) this.resources.push(persistentVolumes)
}) })
}
if (checkPermissions({apiGroup: "apps", resource: "deployments", verb: "list"})) {
listDeployments(this.clusterName).then(res => { listDeployments(this.clusterName).then(res => {
const deployments = { const deployments = {
name: "Deployments", name: "Deployments",
@@ -154,6 +179,8 @@ export default {
} }
this.resources.push(deployments) this.resources.push(deployments)
}) })
}
if (checkPermissions({apiGroup: "apps", resource: "statefulsets", verb: "list"})) {
listStatefulSets(this.clusterName).then(res => { listStatefulSets(this.clusterName).then(res => {
const statefulSets = { const statefulSets = {
name: "StatefulSets", name: "StatefulSets",
@@ -162,6 +189,8 @@ export default {
} }
this.resources.push(statefulSets) this.resources.push(statefulSets)
}) })
}
if (checkPermissions({apiGroup: "batch", resource: "jobs", verb: "list"})) {
listJobs(this.clusterName).then(res => { listJobs(this.clusterName).then(res => {
const jobs = { const jobs = {
name: "Jobs", name: "Jobs",
@@ -170,6 +199,8 @@ export default {
} }
this.resources.push(jobs) this.resources.push(jobs)
}) })
}
if (checkPermissions({apiGroup: "apps", resource: "daemonsets", verb: "list"})) {
listDaemonSets(this.clusterName).then(res => { listDaemonSets(this.clusterName).then(res => {
const daemonSets = { const daemonSets = {
name: "DaemonSets", name: "DaemonSets",
@@ -178,6 +209,8 @@ export default {
} }
this.resources.push(daemonSets) this.resources.push(daemonSets)
}) })
}
if (checkPermissions({apiGroup: "", resource: "services", verb: "list"})) {
listServices(this.clusterName).then(res => { listServices(this.clusterName).then(res => {
const services = { const services = {
name: "Services", name: "Services",
@@ -186,17 +219,20 @@ export default {
} }
this.resources.push(services) this.resources.push(services)
}) })
}
this.search() this.search()
}, },
search () { search() {
this.loading = true this.loading = true
if (checkPermissions({apiGroup: "", resource: "events", verb: "list"})) {
listEvents(this.clusterName, this.page.pageSize, this.page.nextToken).then(res => { listEvents(this.clusterName, this.page.pageSize, this.page.nextToken).then(res => {
this.loading = false this.loading = false
this.events = res.items this.events = res.items
this.page.nextToken = res.metadata["continue"] ? res.metadata["continue"] : "" this.page.nextToken = res.metadata["continue"] ? res.metadata["continue"] : ""
}) })
}
}, },
getData (items, keys) { getData(items, keys) {
let key = [] let key = []
let result = [] let result = []
for (const item of items) { for (const item of items) {
@@ -219,7 +255,7 @@ export default {
} }
return result return result
}, },
traverse (obj, keys) { traverse(obj, keys) {
if (keys === "status.conditions.type") { if (keys === "status.conditions.type") {
if (obj.status.conditions[0].type && obj.status.conditions[0].status === "True") { if (obj.status.conditions[0].type && obj.status.conditions[0].status === "True") {
return obj.status.conditions[0].type return obj.status.conditions[0].type
@@ -243,11 +279,11 @@ export default {
} }
} }
}, },
created () { created() {
this.clusterName = this.$route.query.cluster this.clusterName = this.$route.query.cluster
this.listResources() this.listResources()
} }
} }
</script> </script>
<style scoped> <style scoped>

View File

@@ -101,7 +101,6 @@
if (valid) { if (valid) {
this.loading = true this.loading = true
this.$store.dispatch("user/login", this.form).then(() => { this.$store.dispatch("user/login", this.form).then(() => {
console.log("123")
this.$router.push({path: this.redirect || "/", query: this.otherQuery}) this.$router.push({path: this.redirect || "/", query: this.otherQuery})
this.loading = false this.loading = false
}).catch(() => { }).catch(() => {

View File

@@ -3,7 +3,7 @@
:class="{'collapse':collapse}"> :class="{'collapse':collapse}">
<transition name="sidebar-logo-fade" <transition name="sidebar-logo-fade"
mode="out-in"> mode="out-in">
<router-link v-if="collapse" <div v-if="collapse"
key="collapse" key="collapse"
class="sidebar-logo-link" class="sidebar-logo-link"
to="/"> to="/">
@@ -11,8 +11,8 @@
:src="collapseLogo" :src="collapseLogo"
class="sidebar-logo" class="sidebar-logo"
alt="Sidebar Logo"> alt="Sidebar Logo">
</router-link> </div>
<router-link v-else <div v-else
key="expand" key="expand"
class="sidebar-logo-link" class="sidebar-logo-link"
to="/"> to="/">
@@ -20,7 +20,7 @@
:src="logo" :src="logo"
class="sidebar-logo" class="sidebar-logo"
alt="Sidebar Logo"> alt="Sidebar Logo">
</router-link> </div>
</transition> </transition>
</div> </div>
</template> </template>
@@ -48,9 +48,9 @@ export default {
</script> </script>
<style lang="scss"> <style lang="scss">
@import "~@/styles/common/variables"; @import "~@/styles/common/variables";
.sidebar-logo-container { .sidebar-logo-container {
position: relative; position: relative;
height: $header-height; height: $header-height;
line-height: $header-height; line-height: $header-height;
@@ -88,19 +88,19 @@ export default {
margin: auto; margin: auto;
} }
} }
} }
.sidebar-logo-fade-enter-active { .sidebar-logo-fade-enter-active {
transition: opacity 0.1s; transition: opacity 0.1s;
transition-delay: 0.1s; transition-delay: 0.1s;
} }
.sidebar-logo-fade-leave-active { .sidebar-logo-fade-leave-active {
opacity: 0; opacity: 0;
} }
.sidebar-logo-fade-enter, .sidebar-logo-fade-enter,
.sidebar-logo-fade-leave-to { .sidebar-logo-fade-leave-to {
opacity: 0; opacity: 0;
} }
</style> </style>

View File

@@ -60,6 +60,9 @@ export default {
}, },
methods: { methods: {
hasOneShowingChild(children = [], parent) { hasOneShowingChild(children = [], parent) {
if (parent.parent){
return false
}
const showingChildren = children.filter(item => { const showingChildren = children.filter(item => {
if (item.hidden) { if (item.hidden) {
return false return false

View File

@@ -48,7 +48,6 @@
}, },
}, },
created() { created() {
console.log(this.permission_routes)
} }
} }
</script> </script>

View File

@@ -0,0 +1,8 @@
import Permission from "./permission";
export default {
install(Vue) {
Vue.directive('has-permissions', Permission.hasPermissions);
}
}

View File

@@ -0,0 +1,26 @@
import {checkPermissions} from '@/utils/permission'
function hasPermissions(el, binding) {
const {value, modifiers} = binding
const hasPermissions = checkPermissions(value)
if (!hasPermissions) {
if (modifiers && modifiers.disable) {
el.setAttribute("disabled", true)
} else {
el.parentNode && el.parentNode.removeChild(el)
}
}
}
export default {
hasPermissions: {
inserted(el, binding) {
hasPermissions(el, binding)
},
update(el, binding) {
hasPermissions(el, binding)
}
}
}

View File

@@ -19,6 +19,8 @@ import "./permission"
import "@/styles/common/ekko.css" import "@/styles/common/ekko.css"
import filters from "./filters"; import filters from "./filters";
import JsonViewer from 'vue-json-viewer' import JsonViewer from 'vue-json-viewer'
import directives from "./directive";
Vue.config.productionTip = false Vue.config.productionTip = false
@@ -34,6 +36,7 @@ library.add(fas, far, fab)
Vue.use(icons); Vue.use(icons);
Vue.use(filters); Vue.use(filters);
Vue.use(directives)
Vue.use(JsonViewer) Vue.use(JsonViewer)
new Vue({ new Vue({

View File

@@ -2,21 +2,31 @@ import router from "./router"
import NProgress from "nprogress" import NProgress from "nprogress"
import "nprogress/nprogress.css" import "nprogress/nprogress.css"
import store from "./store" import store from "./store"
import Layout from "@/business/app-layout/horizontal-layout"
NProgress.configure({ showSpinner: false }) // NProgress Configuration
NProgress.configure({showSpinner: false}) // NProgress Configuration
const whiteList = ["/login"] // no redirect whitelist const whiteList = ["/login"] // no redirect whitelist
const generateRoutes = async (to, from, next) => { const generateRoutes = async (to, from, next) => {
const hasRoles = store.getters.roles && store.getters.roles.length > 0 const hasRoles = store.getters.clusterRoles && store.getters.clusterRoles.length > 0
if (hasRoles) { if (hasRoles) {
next() next()
} else { } else {
try { try {
const user = await store.dispatch("user/getCurrentUser") const {clusterRoles} = await store.dispatch("user/getCurrentUser")
const accessRoutes = await store.dispatch("permission/generateRoutes", user) const accessRoutes = await store.dispatch("permission/generateRoutes", clusterRoles)
if (accessRoutes.length > 0) {
const root = {
path: "/",
component: Layout,
redirect: accessRoutes[0].path,
}
router.addRoute(root)
}
router.addRoutes(accessRoutes) router.addRoutes(accessRoutes)
next({ ...to, replace: true }) next({...to, replace: true})
} catch (error) { } catch (error) {
NProgress.done() NProgress.done()
} }
@@ -26,22 +36,24 @@ const generateRoutes = async (to, from, next) => {
//路由前置钩子,根据实际需求修改 //路由前置钩子,根据实际需求修改
router.beforeEach(async (to, from, next) => { router.beforeEach(async (to, from, next) => {
NProgress.start() NProgress.start()
if (!to.query["cluster"]) {
if (from.query["cluster"]) {
const q = to.query
q["cluster"] = from.query["cluster"]
next({ path: to.path, query: q })
NProgress.done()
} else {
console.log("no cluster")
}
}
const isLogin = await store.dispatch("user/isLogin") const isLogin = await store.dispatch("user/isLogin")
if (isLogin) { if (isLogin) {
if (to.path === "/login") { if (to.path === "/login") {
next({ path: "/" }) next({path: "/"})
NProgress.done() NProgress.done()
} }
if (!to.query["cluster"]) {
if (from.query["cluster"]) {
await store.dispatch("user/setCurrentCluster", from.query["cluster"])
const q = to.query
q["cluster"] = from.query["cluster"]
next({path: to.path, query: q})
} else {
console.log("no cluster")
}
} else {
await store.dispatch("user/setCurrentCluster", to.query["cluster"])
}
await generateRoutes(to, from, next) await generateRoutes(to, from, next)
} else { } else {
/* has not login*/ /* has not login*/

View File

@@ -76,11 +76,6 @@ const promise = (request, loading = {}) => {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
loading.status = true loading.status = true
request.then(response => { request.then(response => {
// if (response.data.success) {
// resolve(response.data)
// } else {
// reject(response.message)
// }
resolve(response.data) resolve(response.data)
loading.status = false loading.status = false
}).catch(error => { }).catch(error => {

View File

@@ -7,7 +7,7 @@ const modules = require.context("./modules", true, /\.js$/)
// 修复路由变更后报错的问题 // 修复路由变更后报错的问题
const routerPush = Router.prototype.push const routerPush = Router.prototype.push
Router.prototype.push = function push (location) { Router.prototype.push = function push(location) {
return routerPush.call(this, location).catch(error => error) return routerPush.call(this, location).catch(error => error)
} }
@@ -47,12 +47,12 @@ export const rolesRoutes = [
r2.sort ??= Number.MAX_VALUE r2.sort ??= Number.MAX_VALUE
return r1.sort - r2.sort return r1.sort - r2.sort
}), }),
{ path: "*", redirect: "/", hidden: true } {path: "*", redirect: "/", hidden: true}
] ]
const createRouter = () => new Router({ const createRouter = () => new Router({
mode: 'history', mode: 'history',
scrollBehavior: () => ({ y: 0 }), scrollBehavior: () => ({y: 0}),
routes: constantRoutes, routes: constantRoutes,
base: "dashboard" base: "dashboard"
}) })
@@ -60,7 +60,7 @@ const createRouter = () => new Router({
const router = createRouter() const router = createRouter()
export function resetRouter () { export function resetRouter() {
const newRouter = createRouter() const newRouter = createRouter()
router.matcher = newRouter.matcher // reset router router.matcher = newRouter.matcher // reset router
} }

View File

@@ -2,6 +2,7 @@ import Layout from "@/business/app-layout/horizontal-layout"
const AccessControl = { const AccessControl = {
path: "/accesscontrol", path: "/accesscontrol",
parent: true,
sort: 5, sort: 5,
component: Layout, component: Layout,
name: "Access Control", name: "Access Control",
@@ -13,6 +14,11 @@ const AccessControl = {
children: [ children: [
{ {
path: "/serviceaccounts", path: "/serviceaccounts",
requirePermission:{
apiGroup:"",
resource:"serviceaccounts",
verb:"list",
},
component: () => import("@/business/access-control/service-accounts"), component: () => import("@/business/access-control/service-accounts"),
name: "ServiceAccounts", name: "ServiceAccounts",
meta: { meta: {
@@ -22,6 +28,11 @@ const AccessControl = {
}, },
{ {
path: "/rolebindings", path: "/rolebindings",
requirePermission:{
apiGroup:"rbac.authorization.k8s.io",
resource:"rolebindings",
verb:"list",
},
component: () => import("@/business/access-control/role-bindings"), component: () => import("@/business/access-control/role-bindings"),
name: "RoleBindings", name: "RoleBindings",
meta: { meta: {
@@ -31,6 +42,11 @@ const AccessControl = {
}, },
{ {
path: "/roles", path: "/roles",
requirePermission:{
apiGroup:"rbac.authorization.k8s.io",
resource:"roles",
verb:"list",
},
component: () => import("@/business/access-control/roles"), component: () => import("@/business/access-control/roles"),
name: "Roles", name: "Roles",
meta: { meta: {
@@ -40,6 +56,11 @@ const AccessControl = {
}, },
{ {
path: "/podsecuritypolicy", path: "/podsecuritypolicy",
requirePermission:{
apiGroup:"policy",
resource:"podsecuritypolicies",
verb:"list",
},
component: () => import("@/business/access-control/pod-security-policies"), component: () => import("@/business/access-control/pod-security-policies"),
name: "PodSecurityPolicy", name: "PodSecurityPolicy",
meta: { meta: {

View File

@@ -2,6 +2,8 @@ import Layout from "@/business/app-layout/horizontal-layout"
const Clusters = { const Clusters = {
path: "/cluster", path: "/cluster",
parent: true,
global: true,
sort: 1, sort: 1,
component: Layout, component: Layout,
name: "Cluster", name: "Cluster",
@@ -12,6 +14,11 @@ const Clusters = {
children: [ children: [
{ {
path: "/nodes", path: "/nodes",
requirePermission: {
apiGroup: "",
resource: "nodes",
verb: "list",
},
component: () => import("@/business/cluster/nodes"), component: () => import("@/business/cluster/nodes"),
name: "Nodes", name: "Nodes",
meta: { meta: {
@@ -32,6 +39,11 @@ const Clusters = {
path: "/namespaces", path: "/namespaces",
component: () => import("@/business/cluster/namespaces"), component: () => import("@/business/cluster/namespaces"),
name: "Namespaces", name: "Namespaces",
requirePermission: {
apiGroup: "",
resource: "namespaces",
verb: "list",
},
meta: { meta: {
title: "Namespaces" title: "Namespaces"
} }
@@ -69,6 +81,11 @@ const Clusters = {
path: "/events", path: "/events",
component: () => import("@/business/cluster/events"), component: () => import("@/business/cluster/events"),
name: "events", name: "events",
requirePermission: {
apiGroup: "",
resource: "events",
verb: "list",
},
meta: { meta: {
title: "Events", title: "Events",
} }

View File

@@ -2,6 +2,7 @@ import Layout from "@/business/app-layout/horizontal-layout"
const Configuration = { const Configuration = {
path: "/configuration", path: "/configuration",
parent: true,
sort: 2, sort: 2,
component: Layout, component: Layout,
name: "Configuration", name: "Configuration",

View File

@@ -3,6 +3,7 @@ import Layout from "@/business/app-layout/horizontal-layout"
const Dashboard = { const Dashboard = {
path: "/dashboard", path: "/dashboard",
sort: 0, sort: 0,
global: true,
component: Layout, component: Layout,
name: "Dashboard", name: "Dashboard",
children: [ children: [

View File

@@ -12,6 +12,11 @@ const Network = {
children: [ children: [
{ {
path: "/services", path: "/services",
requirePermission: {
apiGroup: "",
resource: "services",
verb: "list",
},
component: () => import("@/business/network/services"), component: () => import("@/business/network/services"),
name: "Services", name: "Services",
meta: { meta: {
@@ -49,6 +54,11 @@ const Network = {
}, },
{ {
path: "/ingresses", path: "/ingresses",
requirePermission: {
apiGroup: "networking.k8s.io",
resource: "ingresses",
verb: "list",
},
component: () => import("@/business/network/ingresses"), component: () => import("@/business/network/ingresses"),
name: "Ingresses", name: "Ingresses",
meta: { meta: {
@@ -67,6 +77,11 @@ const Network = {
}, },
{ {
path: "/endpoints", path: "/endpoints",
requirePermission: {
apiGroup: "",
resource: "endpoints",
verb: "list",
},
component: () => import("@/business/network/endpoints"), component: () => import("@/business/network/endpoints"),
name: "Endpoints", name: "Endpoints",
meta: { meta: {
@@ -75,6 +90,11 @@ const Network = {
}, },
{ {
path: "/networkpolicies", path: "/networkpolicies",
requirePermission: {
apiGroup: "",
resource: "networkpolicies",
verb: "list",
},
component: () => import("@/business/network/network-policies"), component: () => import("@/business/network/network-policies"),
name: "NetworkPolicies", name: "NetworkPolicies",
meta: { meta: {

View File

@@ -3,6 +3,7 @@ import Layout from "@/business/app-layout/horizontal-layout"
const Storage = { const Storage = {
path: "/storage", path: "/storage",
sort: 3, sort: 3,
parent: true,
component: Layout, component: Layout,
name: "Storage", name: "Storage",
meta: { meta: {
@@ -11,7 +12,12 @@ const Storage = {
}, },
children: [ children: [
{ {
path: "/pv", path: "/persistentvolumes",
requirePermission: {
apiGroup: "",
resource: "persistentvolumes",
verb: "list",
},
component: () => import("@/business/storage/pvs"), component: () => import("@/business/storage/pvs"),
name: "Pvs", name: "Pvs",
meta: { meta: {
@@ -19,7 +25,12 @@ const Storage = {
} }
}, },
{ {
path: "/pvcs", path: "/persistentvolumeclaims",
requirePermission: {
apiGroup: "",
resource: "persistentvolumeclaims",
verb: "list",
},
component: () => import("@/business/storage/pvcs"), component: () => import("@/business/storage/pvcs"),
name: "Pvcs", name: "Pvcs",
meta: { meta: {
@@ -28,6 +39,11 @@ const Storage = {
}, },
{ {
path: "/storageclasses", path: "/storageclasses",
requirePermission: {
apiGroup: "storage.k8s.io",
resource: "storageclasses",
verb: "list",
},
component: () => import("@/business/storage/storage-classes"), component: () => import("@/business/storage/storage-classes"),
name: "StorageClasses", name: "StorageClasses",
meta: { meta: {

View File

@@ -2,6 +2,7 @@ import Layout from "@/business/app-layout/horizontal-layout";
const Workloads = { const Workloads = {
path: "/workloads", path: "/workloads",
parent: true,
sort: 4, sort: 4,
component: Layout, component: Layout,
name: "Workloads", name: "Workloads",
@@ -12,6 +13,11 @@ const Workloads = {
children: [ children: [
{ {
path: "/deployments", path: "/deployments",
requirePermission: {
apiGroup: "apps",
resource: "deployments",
verb: "list",
},
component: () => import("@/business/workloads/deployments"), component: () => import("@/business/workloads/deployments"),
name: "Deployments", name: "Deployments",
meta: { meta: {
@@ -46,80 +52,13 @@ const Workloads = {
}, },
}, },
{
path: "/cronjobs",
component: () => import("@/business/workloads/cronjobs"),
name: "CronJobs",
meta: {
title: "CronJobs",
},
},
{
path: "/cronjobs/detail/:namespace/:name",
name: "CronJobDetail",
hidden: true,
component: () => import("@/business/workloads/cronjobs/detail"),
meta: {
activeMenu: "/cronjobs",
},
},
{
path: "/cronjobs/create",
name: "CronJobCreate",
hidden: true,
component: () => import("@/business/workloads/cronjobs/create"),
meta: {
activeMenu: "/cronjobs",
},
},
{
path: "/cronjobs/edit/:namespace/:name",
name: "CronJobEdit",
hidden: true,
component: () => import("@/business/workloads/cronjobs/edit"),
meta: {
activeMenu: "/cronjobs",
},
},
{
path: "/jobs",
component: () => import("@/business/workloads/jobs"),
name: "Jobs",
meta: {
title: "Jobs",
},
},
{
path: "/jobs/detail/:namespace/:name",
name: "JobDetail",
hidden: true,
component: () => import("@/business/workloads/jobs/detail"),
meta: {
activeMenu: "/jobs",
},
},
{
path: "/jobs/create",
name: "JobCreate",
hidden: true,
component: () => import("@/business/workloads/jobs/create"),
meta: {
activeMenu: "/jobs",
},
},
{
path: "/jobs/edit/:namespace/:name",
name: "JobEdit",
hidden: true,
component: () => import("@/business/workloads/jobs/edit"),
meta: {
activeMenu: "/jobs",
},
},
{ {
path: "/daemonsets", path: "/daemonsets",
requirePermission: {
apiGroup: "apps",
resource: "daemonsets",
verb: "list",
},
component: () => import("@/business/workloads/daemonsets"), component: () => import("@/business/workloads/daemonsets"),
name: "DaemonSets", name: "DaemonSets",
meta: { meta: {
@@ -156,6 +95,11 @@ const Workloads = {
{ {
path: "/statefulsets", path: "/statefulsets",
requirePermission: {
apiGroup: "apps",
resource: "statefulsets",
verb: "list",
},
component: () => import("@/business/workloads/statefulsets"), component: () => import("@/business/workloads/statefulsets"),
name: "StatefulSets", name: "StatefulSets",
meta: { meta: {
@@ -190,8 +134,96 @@ const Workloads = {
}, },
}, },
{
path: "/cronjobs",
requirePermission: {
apiGroup: "batch",
resource: "cronjobs",
verb: "list",
},
component: () => import("@/business/workloads/cronjobs"),
name: "CronJobs",
meta: {
title: "CronJobs",
},
},
{
path: "/cronjobs/detail/:namespace/:name",
name: "CronJobDetail",
hidden: true,
component: () => import("@/business/workloads/cronjobs/detail"),
meta: {
activeMenu: "/cronjobs",
},
},
{
path: "/cronjobs/create",
name: "CronJobCreate",
hidden: true,
component: () => import("@/business/workloads/cronjobs/create"),
meta: {
activeMenu: "/cronjobs",
},
},
{
path: "/cronjobs/edit/:namespace/:name",
name: "CronJobEdit",
hidden: true,
component: () => import("@/business/workloads/cronjobs/edit"),
meta: {
activeMenu: "/cronjobs",
},
},
{
path: "/jobs",
requirePermission: {
apiGroup: "batch",
resource: "jobs",
verb: "list",
},
component: () => import("@/business/workloads/jobs"),
name: "Jobs",
meta: {
title: "Jobs",
},
},
{
path: "/jobs/detail/:namespace/:name",
name: "JobDetail",
hidden: true,
component: () => import("@/business/workloads/jobs/detail"),
meta: {
activeMenu: "/jobs",
},
},
{
path: "/jobs/create",
name: "JobCreate",
hidden: true,
component: () => import("@/business/workloads/jobs/create"),
meta: {
activeMenu: "/jobs",
},
},
{
path: "/jobs/edit/:namespace/:name",
name: "JobEdit",
hidden: true,
component: () => import("@/business/workloads/jobs/edit"),
meta: {
activeMenu: "/jobs",
},
},
{ {
path: "/pods", path: "/pods",
requirePermission: {
apiGroup: "",
resource: "pods",
verb: "list",
},
component: () => import("@/business/workloads/pods"), component: () => import("@/business/workloads/pods"),
name: "Pods", name: "Pods",
meta: { meta: {

View File

@@ -2,9 +2,11 @@
const getters = { const getters = {
sidebar: state => state.app.sidebar, sidebar: state => state.app.sidebar,
name: state => state.user.name, name: state => state.user.name,
nickName: state => state.user.nickName,
// language: state => state.user.language, // language: state => state.user.language,
permission_routes: state => state.permission.routes, permission_routes: state => state.permission.routes,
menu: state => state.user.menu,
roles: state => state.user.roles, roles: state => state.user.roles,
cluster: state => state.user.cluster,
clusterRoles: state => state.user.clusterRoles,
} }
export default getters export default getters

View File

@@ -5,24 +5,37 @@ const state = {
addRoutes: [] addRoutes: []
} }
// function hasPermission(user, route) { function hasPermission(clusterRoles, route) {
// if (user && route){ if (route.requirePermission) {
// console.log("") for (const clusterRole of clusterRoles) {
// } if (clusterRole.rules.length > 0) {
// return true for (const rule of clusterRole.rules) {
// } if (rule.apiGroups.includes("*") || rule.apiGroups.includes(route.requirePermission.apiGroup)) {
if (rule.resources.includes("*") || rule.resources.includes(route.requirePermission.resource)) {
if (rule.verbs.includes("*") || rule.verbs.includes(route.requirePermission.verb)) {
return true
}
}
}
}
}
}
return false
}
return true
}
export function filterRolesRoutes (routes, user) { export function filterRolesRoutes(routes, clusterRoles) {
const res = [] const res = []
routes.forEach(route => { routes.forEach(route => {
const tmp = { ...route } const tmp = {...route}
// if (hasPermission(user, tmp)) { if (hasPermission(clusterRoles, tmp)) {
if (tmp.children) { if (tmp.children) {
tmp.children = filterRolesRoutes(tmp.children, user) tmp.children = filterRolesRoutes(tmp.children, clusterRoles)
} }
res.push(tmp) res.push(tmp)
// } }
}) })
return res return res
@@ -36,12 +49,28 @@ const mutations = {
} }
const actions = { const actions = {
generateRoutes ({ commit }, p) { generateRoutes({commit}, p) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
const user = p const clusterRoles = p
let accessedRoutes let accessedRoutes
try { try {
accessedRoutes = filterRolesRoutes(rolesRoutes, user) accessedRoutes = filterRolesRoutes(rolesRoutes, clusterRoles)
for (const route of accessedRoutes) {
if (route.parent) {
let hidden = true
if (route.children.length > 0) {
for (const childRoute of route.children) {
hidden = hidden && childRoute.hidden
}
if (hidden) {
route.hidden = true
}
} else {
route.hidden = true
}
}
}
commit("SET_ROUTES", accessedRoutes) commit("SET_ROUTES", accessedRoutes)
resolve(accessedRoutes) resolve(accessedRoutes)
} catch (error) { } catch (error) {

View File

@@ -1,14 +1,15 @@
import {login, isLogin, logout, getCurrentUser} from "@/api/auth" import {login, isLogin, logout, getCurrentClusterUser} from "@/api/auth"
import {resetRouter} from "@/router" import {resetRouter} from "@/router"
import {getLanguage, setLanguage} from "@/i18n" import {getLanguage, setLanguage} from "@/i18n"
import store from "../../store"
const state = { const state = {
login: false, login: false,
name: "", name: "",
currentProject: "",
language: getLanguage(), language: getLanguage(),
roles: [], roles: [],
cluster: "" cluster: "",
clusterRoles: [],
} }
const mutations = { const mutations = {
@@ -21,6 +22,9 @@ const mutations = {
SET_NAME: (state, name) => { SET_NAME: (state, name) => {
state.name = name state.name = name
}, },
SET_NICK_NAME: (state, nickName) => {
state.nickName = nickName
},
SET_LANGUAGE: (state, language) => { SET_LANGUAGE: (state, language) => {
state.language = language state.language = language
setLanguage(language) setLanguage(language)
@@ -28,20 +32,23 @@ const mutations = {
SET_ROLES: (state, roles) => { SET_ROLES: (state, roles) => {
state.roles = roles state.roles = roles
}, },
SET_CURRENT_MENU: (state, menu) => {
state.menu = menu
},
SET_CURRENT_CLUSTER: (state, name) => { SET_CURRENT_CLUSTER: (state, name) => {
state.cluster = name state.cluster = name
},
SET_CLUSTER_ROLES: (state, clusterRoles) => {
state.clusterRoles = clusterRoles
} }
} }
const actions = { const actions = {
login ({ commit }, userInfo) { setCurrentCluster({commit}, clusterName) {
const { username, password } = userInfo commit("SET_CURRENT_CLUSTER", clusterName)
},
login({commit}, userInfo) {
const {username, password} = userInfo
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
commit("LOGIN") commit("LOGIN")
login({ username: username.trim(), password: password }).then(response => { login({username: username.trim(), password: password}).then(response => {
commit("LOGIN") commit("LOGIN")
resolve(response) resolve(response)
}).catch(error => { }).catch(error => {
@@ -50,7 +57,7 @@ const actions = {
}) })
}, },
isLogin ({ commit }) { isLogin({commit}) {
return new Promise((resolve) => { return new Promise((resolve) => {
if (state.login) { if (state.login) {
resolve(true) resolve(true)
@@ -67,22 +74,25 @@ const actions = {
}) })
}) })
}, },
getCurrentUser ({ commit }) { getCurrentUser({commit}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
getCurrentUser().then(data => { const clusterName = store.getters.cluster
getCurrentClusterUser(clusterName).then(data => {
const user = data.data const user = data.data
user["roles"] = ["ADMIN"] user["roles"] = ["ADMIN"]
const { name, roles } = user const {name, nickName, roles, clusterRoles} = user
commit("SET_NAME", name) commit("SET_NAME", name)
commit("SET_ROLES", roles) commit("SET_ROLES", roles)
commit("SET_LANGUAGE", "zh-CN") commit("SET_LANGUAGE", "zh-CN")
commit("SET_CLUSTER_ROLES", clusterRoles)
commit("SET_NICK_NAME", nickName)
resolve(user) resolve(user)
}).catch(error => { }).catch(error => {
reject(error) reject(error)
}) })
}) })
}, },
logout ({ commit }) { logout({commit}) {
logout().then(() => { logout().then(() => {
commit("LOGOUT") commit("LOGOUT")
commit("SET_ROLES", []) commit("SET_ROLES", [])

View File

@@ -0,0 +1,19 @@
import store from '@/store'
export const checkPermissions = function (p) {
const userClusterRoles = store.getters && store.getters.clusterRoles
for (const clusterRole of userClusterRoles) {
if (clusterRole.rules.length > 0) {
for (const rule of clusterRole.rules) {
if (rule.apiGroups.includes("*") || rule.apiGroups.includes(p.apiGroup)) {
if (rule.resources.includes("*") || rule.resources.includes(p.resource)) {
if (rule.verbs.includes("*") || rule.verbs.includes(p.verb)) {
return true
}
}
}
}
}
}
return false
}

View File

@@ -39,8 +39,8 @@ export function updateClusterMember(name, memberName, member) {
} }
export function getClusterMember(name, memberName, kind) { export function getClusterMember(name, memberName) {
return get(`${baseUrl}/${name}/members/${memberName}?kind=${kind}`) return get(`${baseUrl}/${name}/members/${memberName}`)
} }
export function deleteClusterMember(name, memberName, kind) { export function deleteClusterMember(name, memberName, kind) {
@@ -70,3 +70,15 @@ export function listClusterResourceByGroupVersion(name, groupVersion) {
export function deleteClusterRole(name, clusterRoleName) { export function deleteClusterRole(name, clusterRoleName) {
return del(`${baseUrl}/${name}/clusterroles/${clusterRoleName}`) return del(`${baseUrl}/${name}/clusterroles/${clusterRoleName}`)
} }
export function listNamespaces(name) {
return get(`${baseUrl}/${name}/namespaces`)
}
export function listProjects(name) {
return get(`${baseUrl}/${name}/projects`)
}
export function createProject(name, project) {
return post(`${baseUrl}/${name}/projects`, project)
}

View File

@@ -14,14 +14,16 @@
{{ row.metadata.name }} {{ row.metadata.name }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('commons.table.description')" min-width="100" fix>
<el-table-column :label="$t('business.cluster.scope')" min-width="100" fix>
<template v-slot:default="{row}"> <template v-slot:default="{row}">
{{$t("business.cluster_role."+row.metadata.annotations["ekko-i18n"])}} {{ $t('business.cluster.' + row.metadata.labels["kubeoperator.io/role-type"]) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('commons.table.creat_by')" min-width="100" fix>
<el-table-column :label="$t('commons.table.built_in')" min-width="100" fix>
<template v-slot:default="{row}"> <template v-slot:default="{row}">
{{ row.metadata.annotations["created-by"] }} {{ $t('commons.bool.' + row.metadata.annotations["builtin"]) }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('commons.table.created_time')" min-width="100" fix> <el-table-column :label="$t('commons.table.created_time')" min-width="100" fix>
@@ -39,10 +41,12 @@
center z-index="20"> center z-index="20">
<el-form :model="ruleForm" label-position="top" label- width="60px"> <el-form :model="ruleForm" label-position="top" label- width="60px">
<el-form-item label="API Group"> <el-form-item label="API Group">
<el-select v-model="ruleForm.groupVersion" style="width:100%" @change="onAPIGroupChange"> <el-select v-model="ruleForm.apiGroup" style="width:100%" @change="onAPIGroupChange">
<el-option v-for="(item,index) in apiGroupsOptions" :key="index" <el-option v-for="(item,index) in apiGroupsOptions" :key="index"
:value="item.preferredVersion.groupVersion"> :value="item.name">
{{item.preferredVersion.groupVersion}} <span v-if="item.name">
{{ item.name }}
</span>
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -52,8 +56,8 @@
@change="onResourcesChange"> @change="onResourcesChange">
<el-option v-for="(item,index) in apiResourceOptions" <el-option v-for="(item,index) in apiResourceOptions"
:key="index" :key="index"
:value="item.name"> :value="item">
{{item.name}} {{ item }}
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -64,7 +68,7 @@
<el-option v-for="(item,index) in verbOptions" <el-option v-for="(item,index) in verbOptions"
:key="index" :key="index"
:value="item"> :value="item">
{{item}} {{ item }}
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
@@ -83,16 +87,21 @@
width="60%" width="60%"
center z-index="10"> center z-index="10">
<el-form :model="clusterRoleForm" label-position="left" label- width="60px"> <el-form :model="clusterRoleForm" label-position="left" label-width="144px">
<el-form-item label="名称"> <el-form-item :label="$t('commons.table.name')">
<el-input v-model="clusterRoleForm.name"></el-input> <el-input v-model="clusterRoleForm.name" style="width: 80%"></el-input>
</el-form-item> </el-form-item>
<el-form-item label="规则"> <el-form-item :label="$t('business.cluster.scope')">
<el-radio v-model="clusterRoleForm.type" label="cluster">{{ $t('business.cluster.cluster') }}</el-radio>
<el-radio v-model="clusterRoleForm.type" label="namespace">{{ $t('business.cluster.namespace') }}</el-radio>
</el-form-item>
<el-form-item label="规则" label-width="144px">
<el-button @click="onRuleCreate"><i class="el-icon-plus "></i></el-button> <el-button @click="onRuleCreate"><i class="el-icon-plus "></i></el-button>
<el-table <el-table
:data="clusterRoleForm.rules" :data="clusterRoleForm.rules"
border border
style="width: 100%"> style="width: 80%">
<el-table-column <el-table-column
prop="apiGroup" prop="apiGroup"
label="API Group" label="API Group"
@@ -103,7 +112,7 @@
label="API Resource" label="API Resource"
> >
<template v-slot:default="{row}"> <template v-slot:default="{row}">
<span v-for="v in row.resources" :key="v">{{v}}<br/></span> <span v-for="v in row.resources" :key="v">{{ v }}<br/></span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@@ -111,7 +120,7 @@
label="Verbs" label="Verbs"
> >
<template v-slot:default="{row}"> <template v-slot:default="{row}">
<span v-for="v in row.verbs" :key="v">{{v}}<br/></span> <span v-for="v in row.verbs" :key="v">{{ v }}<br/></span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column> <el-table-column>
@@ -159,7 +168,7 @@
label="API Resource" label="API Resource"
> >
<template v-slot:default="{row}"> <template v-slot:default="{row}">
<span v-for="v in row.resources" :key="v">{{v}}<br/></span> <span v-for="v in row.resources" :key="v">{{ v }}<br/></span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column <el-table-column
@@ -167,7 +176,7 @@
label="Verbs" label="Verbs"
> >
<template v-slot:default="{row}"> <template v-slot:default="{row}">
<span v-for="v in row.verbs" :key="v">{{v}}<br/></span> <span v-for="v in row.verbs" :key="v">{{ v }}<br/></span>
</template> </template>
</el-table-column> </el-table-column>
<el-table-column> <el-table-column>
@@ -193,19 +202,19 @@
</template> </template>
<script> <script>
import LayoutContent from "@/components/layout/LayoutContent" import LayoutContent from "@/components/layout/LayoutContent"
import ComplexTable from "@/components/complex-table" import ComplexTable from "@/components/complex-table"
import { import {
listClusterRoles, listClusterRoles,
listClusterApiGroups, listClusterApiGroups,
listClusterResourceByGroupVersion, listClusterResourceByGroupVersion,
createClusterRole, createClusterRole,
deleteClusterRole, deleteClusterRole,
updateClusterRole updateClusterRole
} from "@/api/clusters"; } from "@/api/clusters";
export default { export default {
name: "ClusterRoles", name: "ClusterRoles",
props: ["name"], props: ["name"],
components: {LayoutContent, ComplexTable}, components: {LayoutContent, ComplexTable},
@@ -221,6 +230,7 @@
memberOptions: [], memberOptions: [],
clusterRoleForm: { clusterRoleForm: {
name: "", name: "",
type: "cluster",
rules: [], rules: [],
}, },
editForm: { editForm: {
@@ -230,7 +240,7 @@
resourcesDisable: false, resourcesDisable: false,
verbsDisable: false, verbsDisable: false,
ruleForm: { ruleForm: {
groupVersion: "", apiGroup: "",
resources: [], resources: [],
verbs: [] verbs: []
}, },
@@ -240,6 +250,9 @@
icon: "el-icon-edit", icon: "el-icon-edit",
click: (row) => { click: (row) => {
this.onEdit(row) this.onEdit(row)
},
disabled: (row) => {
return row.metadata.annotations['builtin'] === 'true'
} }
}, },
{ {
@@ -247,6 +260,9 @@
icon: "el-icon-delete", icon: "el-icon-delete",
click: (row) => { click: (row) => {
this.onDelete(row) this.onDelete(row)
},
disabled: (row) => {
return row.metadata.annotations['builtin'] === 'true'
} }
}, },
], ],
@@ -266,11 +282,17 @@
this.clusterRoleForm = { this.clusterRoleForm = {
name: "", name: "",
rules: [], rules: [],
type: "cluster",
} }
this.createDialogOpened = true this.createDialogOpened = true
listClusterApiGroups(this.name).then(data => { listClusterApiGroups(this.name).then(data => {
this.apiGroupsOptions.push({preferredVersion: {groupVersion: "*"}}) this.apiGroupsOptions.push({name: "*"})
this.apiGroupsOptions = this.apiGroupsOptions.concat(data.data) this.apiGroupsOptions = this.apiGroupsOptions.concat(data.data)
for (const group of this.apiGroupsOptions) {
if (group.name === "") {
group.name = "core"
}
}
}) })
}, },
@@ -280,22 +302,35 @@
this.editForm.rules = [] this.editForm.rules = []
for (const rule of row.rules) { for (const rule of row.rules) {
if (rule.apiGroups) { if (rule.apiGroups) {
this.editForm.rules.push({ const r = {
apiGroup: rule.apiGroups[0], apiGroup: [],
resources: rule.resources, resources: rule.resources,
verbs: rule.verbs, verbs: rule.verbs,
}) }
for (const g of rule.apiGroups) {
if (g === "") {
r.apiGroup = "core"
} else {
r.apiGroup = g
}
}
this.editForm.rules.push(r)
} }
} }
this.editDialogOpened = true this.editDialogOpened = true
listClusterApiGroups(this.name).then(data => { listClusterApiGroups(this.name).then(data => {
this.apiGroupsOptions.push({preferredVersion: {groupVersion: "*"}}) this.apiGroupsOptions.push({name: "*"})
this.apiGroupsOptions = this.apiGroupsOptions.concat(data.data) this.apiGroupsOptions = this.apiGroupsOptions.concat(data.data)
for (const group of this.apiGroupsOptions) {
if (group.name === "") {
group.name = "core"
}
}
}) })
}, },
onRuleCreate() { onRuleCreate() {
this.ruleForm = { this.ruleForm = {
groupVersion: "", apiGroup: "",
resources: [], resources: [],
verbs: [], verbs: [],
} }
@@ -309,14 +344,14 @@
this.ruleForm.verbs = [] this.ruleForm.verbs = []
this.resourcesDisable = false this.resourcesDisable = false
this.verbsDisable = false this.verbsDisable = false
if (this.ruleForm.groupVersion === "*") { if (this.ruleForm.apiGroup === "*") {
this.apiResourceOptions = [{name: "*"}] this.apiResourceOptions = ["*"]
this.ruleForm.resources = ["*"] this.ruleForm.resources = ["*"]
this.resourcesDisable = true this.resourcesDisable = true
return return
} }
listClusterResourceByGroupVersion(this.name, this.ruleForm.groupVersion).then(data => { listClusterResourceByGroupVersion(this.name, this.ruleForm.apiGroup).then(data => {
this.apiResourceOptions.push({name: "*"}) this.apiResourceOptions.push("*")
this.apiResourceOptions = this.apiResourceOptions.concat(data.data); this.apiResourceOptions = this.apiResourceOptions.concat(data.data);
}) })
}, },
@@ -331,20 +366,18 @@
} }
}, },
onRuleConfirm() { onRuleConfirm() {
switch (this.operation) { const rule = {
case "create": apiGroup: this.ruleForm.apiGroup,
this.clusterRoleForm.rules.push({
apiGroup: this.ruleForm.groupVersion,
resources: this.ruleForm.resources, resources: this.ruleForm.resources,
verbs: this.ruleForm.verbs verbs: this.ruleForm.verbs
}) }
switch (this.operation) {
case "create":
this.clusterRoleForm.rules.push(rule)
break break
case "update": case "update":
this.editForm.rules.push({ this.editForm.rules.push(rule)
apiGroup: this.ruleForm.groupVersion,
resources: this.ruleForm.resources,
verbs: this.ruleForm.verbs
})
break break
} }
this.ruleDialogOpened = false this.ruleDialogOpened = false
@@ -376,6 +409,9 @@
const req = { const req = {
metadata: { metadata: {
name: this.clusterRoleForm.name, name: this.clusterRoleForm.name,
labels: {
"kubeoperator.io/role-type": this.clusterRoleForm.type
}
}, },
rules: [] rules: []
} }
@@ -405,7 +441,6 @@
verbs: rule.verbs, verbs: rule.verbs,
}) })
} }
console.log(123)
updateClusterRole(this.name, req.metadata.name, req).then(() => { updateClusterRole(this.name, req.metadata.name, req).then(() => {
this.list() this.list()
this.editDialogOpened = false this.editDialogOpened = false
@@ -415,7 +450,7 @@
created() { created() {
this.list() this.list()
} }
} }
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,8 +1,8 @@
<template> <template>
<layout-content :header="$t('business.cluster.cluster_detail')" :back-to="{ name: 'Clusters' }"> <layout-content :header="$t('business.cluster.cluster_detail')" :back-to="{ name: 'Clusters' }">
<el-menu class="menuClass" router :default-active="$route.path" mode="horizontal"> <el-menu class="menuClass" router :default-active="$route.path" mode="horizontal">
<el-menu-item :index="'/clusters/detail/'+name+'/members'">成员管理</el-menu-item> <el-menu-item :index="'/clusters/detail/'+name+'/members'">成员</el-menu-item>
<el-menu-item :index="'/clusters/detail/'+name+'/clusterroles'">角色管理</el-menu-item> <el-menu-item :index="'/clusters/detail/'+name+'/clusterroles'">角色</el-menu-item>
</el-menu> </el-menu>
<br/> <br/>
<div class="detailClass"> <div class="detailClass">
@@ -13,9 +13,9 @@
</template> </template>
<script> <script>
import LayoutContent from "@/components/layout/LayoutContent" import LayoutContent from "@/components/layout/LayoutContent"
export default { export default {
name: "ClusterEdit", name: "ClusterEdit",
props: ["name"], props: ["name"],
components: {LayoutContent}, components: {LayoutContent},
@@ -23,19 +23,19 @@
return {} return {}
}, },
methods: {}, methods: {},
} }
</script> </script>
<style scoped> <style scoped>
.menuClass { .menuClass {
position: fixed; position: fixed;
z-index: 1; z-index: 1;
width: calc(100vw - 330px); width: calc(100vw - 330px);
} }
.detailClass { .detailClass {
margin-top: 60px; margin-top: 60px;
height: calc(100% - 140px); height: calc(100% - 140px);
overflow: auto; overflow: auto;
} }
</style> </style>

View File

@@ -14,11 +14,6 @@
{{ row.name }} {{ row.name }}
</template> </template>
</el-table-column> </el-table-column>
<el-table-column :label="$t('commons.table.kind')" min-width="100" fix>
<template v-slot:default="{row}">
{{ row.kind}}
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.created_time')" min-width="100" fix> <el-table-column :label="$t('commons.table.created_time')" min-width="100" fix>
<template v-slot:default="{row}"> <template v-slot:default="{row}">
{{ row.createAt }} {{ row.createAt }}
@@ -29,38 +24,100 @@
<el-dialog <el-dialog
title="提示" title="创建命名空间角色"
:visible.sync="createDialogOpened" :visible.sync="namespaceRoleDialogOpened"
width="20%" width="40%"
center> center z-index="20">
<el-form :model="memberForm" label-position="left" label- width="60px"> <el-form label-position="left" label-width="144px" :model="namespaceRoleForm">
<el-form-item label="主体类型"> <el-form-item label="命名空间">
<el-select v-model="memberForm.subjectKind" @change="onSubjectKindChange"> <el-select v-model="namespaceRoleForm.namespace" style="width:100%">
<el-option value="User"> <el-option v-for="(item,index) in getNamespaceOptions"
用户 :key="index"
</el-option> :value="item.metadata.name">
<el-option value="Group"> {{ item.metadata.name }}
用户组
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="主体名称">
<el-select v-model="memberForm.subjectName"> <el-form-item label="角色">
<el-option v-for="(item, index) in memberOptions" :key="index" :value="item.name"> <el-select multiple v-model="namespaceRoleForm.roles" style="width:100%">
{{item.name}} <el-option v-for="(item,index) in namespaceRoleOptions"
:key="index"
:value="item.metadata.name">
{{ item.metadata.name }}
</el-option>
</el-select>
</el-form-item>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="namespaceRoleDialogOpened=false"> </el-button>
<el-button type="primary" @click="onNamespaceRoleConfirm"> </el-button>
</span>
</el-dialog>
<el-dialog
title="添加成员"
:visible.sync="createDialogOpened"
z-index="10"
width="60%"
center>
<el-form :model="memberForm" label-position="left" label-width="144px">
<el-form-item label="用户名称">
<el-select v-model="memberForm.userName" style="width: 80%">
<el-option v-for="(item, index) in userOptions" :key="index" :value="item.name">
{{ item.name }}
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="集群角色"> <el-form-item label="集群角色">
<el-select v-model="memberForm.clusterRoles" multiple placeholder="请选择"> <el-radio-group v-model="memberForm.roleType">
<el-radio label="admin">管理者</el-radio>
<el-radio label="viewer">只读者</el-radio>
<el-radio label="custom">自定义</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="memberForm.roleType==='custom'">
<el-form-item>
<el-select v-model="memberForm.customClusterRoles" multiple style="width: 80%"
placeholder="请选择">
<el-option <el-option
v-for="(item,index) in clusterRolesOptions" v-for="(item,index) in clusterRolesOptions"
:key="index" :key="index"
:value="item.metadata.name"> :value="item.metadata.name">
{{item.metadata.name}} {{ item.metadata.name }}
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="命名空间角色">
<el-button @click="onNamespaceRoleCreate"><i class="el-icon-plus "></i></el-button>
<el-table
:data="memberForm.namespaceRoles"
border
style="width: 80%">
<el-table-column
prop="namespace"
label="命名空间"
>
</el-table-column>
<el-table-column
label="角色"
>
<template v-slot:default="{row}">
<span v-for="v in row.roles" :key="v">{{ v }}<br/></span>
</template>
</el-table-column>
<el-table-column width="256x">
<template v-slot:default="{row}">
<el-button
size="mini" circle @click="onNamespaceRoleDelete(row)">
<i class="el-icon-delete"></i>
</el-button>
</template>
</el-table-column>
</el-table>
</el-form-item>
</div>
</el-form> </el-form>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="createDialogOpened = false"> </el-button> <el-button @click="createDialogOpened = false"> </el-button>
@@ -68,61 +125,57 @@
</span> </span>
</el-dialog> </el-dialog>
<el-dialog <el-dialog
title="提示" title="编辑成员"
:visible.sync="editDialogOpened" :visible.sync="editDialogOpened"
width="20%" width="60%"
center> center>
<el-form :model="editForm" label-position="left" label- width="60px"> <el-form :model="editForm" label-position="left" label-width="144px">
<el-form-item label="主体类型"> <el-form-item label="用户名称">
<el-select v-model="editForm.subjectKind" disabled @change="onSubjectKindChange"> <el-select v-model="editForm.userName" style="width: 80%" disabled="disabled">
<el-option value="User"> <el-option v-for="(item, index) in userOptions" :key="index" :value="item.name">
用户 {{ item.name }}
</el-option>
<el-option value="Group">
用户组
</el-option>
</el-select>
</el-form-item>
<el-form-item label="主体名称">
<el-select v-model="editForm.subjectName" disabled>
<el-option v-for="(item, index) in memberOptions" :key="index" :value="item.name">
{{item.name}}
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
<el-form-item label="集群角色"> <el-form-item label="集群角色">
<el-select v-model="editForm.clusterRoles" multiple placeholder="请选择"> <el-radio-group v-model="editForm.roleType">
<el-radio label="admin">管理者</el-radio>
<el-radio label="viewer">只读者</el-radio>
<el-radio label="custom">自定义</el-radio>
</el-radio-group>
</el-form-item>
<div v-if="editForm.roleType==='custom'">
<el-form-item>
<el-select v-model="editForm.customClusterRoles" multiple style="width: 80%"
placeholder="请选择">
<el-option <el-option
v-for="(item,index) in clusterRolesOptions" v-for="(item,index) in clusterRolesOptions"
:key="index" :key="index"
:value="item.metadata.name"> :value="item.metadata.name">
{{item.metadata.name}} {{ item.metadata.name }}
</el-option> </el-option>
</el-select> </el-select>
</el-form-item> </el-form-item>
</div>
</el-form> </el-form>
<span slot="footer" class="dialog-footer"> <span slot="footer" class="dialog-footer">
<el-button @click="editDialogOpened = false"> </el-button> <el-button @click="editDialogOpened = false"> </el-button>
<el-button type="primary" @click="onEditConfirm"> </el-button> <el-button type="primary" @click="onEditConfirm"> </el-button>
</span> </span>
</el-dialog> </el-dialog>
</layout-content> </layout-content>
</template> </template>
<script> <script>
import LayoutContent from "@/components/layout/LayoutContent" import LayoutContent from "@/components/layout/LayoutContent"
import ComplexTable from "@/components/complex-table" import ComplexTable from "@/components/complex-table"
import {createClusterMember, listClusterMembers} from "@/api/clusters" import {createClusterMember, listClusterMembers, listNamespaces} from "@/api/clusters"
import {listUsers} from "@/api/users" import {listUsers} from "@/api/users"
import {listGroups} from "@/api/groups" import {listClusterRoles, deleteClusterMember, getClusterMember, updateClusterMember} from "@/api/clusters";
import {listClusterRoles, deleteClusterMember, getClusterMember, updateClusterMember} from "@/api/clusters";
export default { export default {
name: "ClusterMembers", name: "ClusterMembers",
props: ["name"], props: ["name"],
components: {LayoutContent, ComplexTable}, components: {LayoutContent, ComplexTable},
@@ -130,17 +183,25 @@
return { return {
createDialogOpened: false, createDialogOpened: false,
editDialogOpened: false, editDialogOpened: false,
memberOptions: [], namespaceRoleDialogOpened: false,
userOptions: [],
clusterRolesOptions: [], clusterRolesOptions: [],
namespaceRoleOptions: [],
namespaceOptions: [],
namespaceRoleForm: {
namespace: "",
roles: [],
},
memberForm: { memberForm: {
subjectKind: "", userName: "",
subjectName: "", customClusterRoles: [],
clusterRoles: [] namespaceRoles: [],
roleType: "admin"
}, },
editForm: { editForm: {
subjectKind: "", userName: "",
subjectName: "", customClusterRoles: [],
clusterRoles: [] roleType: ""
}, },
buttons: [ buttons: [
{ {
@@ -161,6 +222,19 @@
data: [], data: [],
} }
}, },
computed: {
getNamespaceOptions() {
return this.namespaceOptions.filter((n) => {
let exists = false
for (const nrs of this.memberForm.namespaceRoles) {
if (nrs.namespace === n.metadata.name) {
exists = true
}
}
return !exists
})
}
},
methods: { methods: {
list() { list() {
this.loading = false this.loading = false
@@ -170,23 +244,42 @@
}) })
}, },
onCreate() { onCreate() {
this.memberForm.subjectKind = ""
this.memberForm.clusterRoles = []
this.createDialogOpened = true this.createDialogOpened = true
listClusterRoles(this.name).then(data => { listClusterRoles(this.name).then(data => {
console.log(data) this.clusterRolesOptions = data.data.filter((r) => {
this.clusterRolesOptions = data.data return r.metadata["labels"]["kubeoperator.io/role-type"] === "cluster"
})
this.namespaceRoleOptions = data.data.filter((r) => {
return r.metadata["labels"]["kubeoperator.io/role-type"] === "namespace"
})
})
listUsers().then(data => {
this.userOptions = data.data;
})
listNamespaces(this.name).then(data => {
this.namespaceOptions = data.data;
}) })
this.onSubjectKindChange()
}, },
onEdit(row) { onEdit(row) {
listClusterRoles(this.name).then(data => { listClusterRoles(this.name).then(data => {
this.clusterRolesOptions = data.data this.clusterRolesOptions = data.data
getClusterMember(this.name, row.name, row.kind).then(data => { getClusterMember(this.name, row.name).then(data => {
this.editForm.subjectName = data.data.name this.editForm.userName = data.data.name
this.editForm.subjectKind = data.data.kind if (data.data.clusterRoles.length === 1) {
this.editForm.clusterRoles = data.data.clusterRoles if (data.data.clusterRoles[0] === 'Admin Cluster') {
this.editForm.roleType = 'admin'
} else if (data.data.clusterRoles[0] === 'View Cluster') {
this.editForm.roleType = 'viewer'
} else {
this.editForm.roleType = 'custom'
this.editForm.customClusterRoles = data.data.clusterRoles
}
} else {
this.editForm.roleType = 'custom'
this.editForm.customClusterRoles = data.data.clusterRoles
}
}) })
}) })
this.editDialogOpened = true this.editDialogOpened = true
@@ -203,45 +296,68 @@
}) })
}); });
}, },
onSubjectKindChange() { onNamespaceRoleCreate() {
this.memberForm.subjectName = "" this.namespaceRoleForm.namespaceRoles = []
if (this.memberForm.subjectKind === 'Group') { this.namespaceRoleDialogOpened = true
listGroups().then((data) => { this.namespaceRoleForm.namespace = ""
this.memberOptions = data.data;
})
}
if (this.memberForm.subjectKind === 'User') {
listUsers().then((data) => {
this.memberOptions = data.data;
})
}
}, },
onEditConfirm() { onEditConfirm() {
updateClusterMember(this.name, this.editForm.subjectName, { let req = {
name: this.editForm.subjectName, name: this.editForm.userName,
kind: this.editForm.subjectKind, }
clusterRoles: this.editForm.clusterRoles switch (this.editForm.roleType) {
}).then(() => { case "admin":
req.clusterRoles = ["Admin Cluster"]
break
case "viewer":
req.clusterRoles = ["View Cluster"]
break
case "custom":
req.clusterRoles = this.editForm.customClusterRoles
break
}
updateClusterMember(this.name, this.editForm.userName, req).then(() => {
this.editDialogOpened = false this.editDialogOpened = false
this.list() this.list()
}) })
}, },
onNamespaceRoleConfirm() {
this.memberForm.namespaceRoles.push({
namespace: this.namespaceRoleForm.namespace,
roles: this.namespaceRoleForm.roles,
})
this.namespaceRoleDialogOpened = false
},
onNamespaceRoleDelete(row) {
this.memberForm.namespaceRoles.splice(this.memberForm.namespaceRoles.indexOf(row), 1)
},
onConfirm() { onConfirm() {
createClusterMember(this.name, { let req = {
kind: this.memberForm.subjectKind, name: this.memberForm.userName,
name: this.memberForm.subjectName, namespaceRoles: this.memberForm.namespaceRoles,
clusterRoles: this.memberForm.clusterRoles }
}).then(() => { switch (this.memberForm.roleType) {
case "admin":
req.clusterRoles = ["Admin Cluster"]
break
case "viewer":
req.clusterRoles = ["View Cluster"]
break
case "custom":
req.clusterRoles = this.memberForm.customClusterRoles
break
}
createClusterMember(this.name, req).then(() => {
this.createDialogOpened = false this.createDialogOpened = false
this.list() this.list()
}) })
} },
}, },
created() { created() {
this.list() this.list()
} }
} }
</script> </script>
<style scoped> <style scoped>

View File

@@ -1,13 +0,0 @@
<template>
<span>2</span>
</template>
<script>
export default {
name: "ClusterOverview"
}
</script>
<style scoped>
</style>

View File

@@ -86,13 +86,13 @@ const message = {
}, },
cluster: { cluster: {
cluster: "集群", cluster: "集群",
namespace: "命名空间",
scope: "作用域",
version: "版本", version: "版本",
list: "集群列表", list: "集群列表",
import: "导入集群", import: "导入集群",
edit: "编辑", edit: "编辑",
nodes: "节点", nodes: "节点",
api_server_help: "例如: https://172.16.10.100:8443",
router_help: "装有 kube-proxy 的任意节点的且可以被访问到的 IP 地址",
label: "标签", label: "标签",
description: "描述", description: "描述",
cluster_detail: "集群详情", cluster_detail: "集群详情",
@@ -106,7 +106,7 @@ const message = {
expect: "敬请期待", expect: "敬请期待",
management: "管理", management: "管理",
open_dashboard: "控制台", open_dashboard: "控制台",
cluster_version: "版本" cluster_version: "版本",
}, },
namespace: { namespace: {

View File

@@ -61,9 +61,8 @@ const Clusters = {
meta: { meta: {
activeMenu: "/clusters", activeMenu: "/clusters",
}, },
} },
] ]
} }
] ]
} }

View File

@@ -5,8 +5,6 @@ const getters = {
nickName: state => state.user.nickName, nickName: state => state.user.nickName,
// language: state => state.user.language, // language: state => state.user.language,
permission_routes: state => state.permission.routes, permission_routes: state => state.permission.routes,
menu: state => state.user.menu,
roles: state => state.user.roles,
permissions: state => state.user.permissions permissions: state => state.user.permissions
} }
export default getters export default getters

View File

@@ -5,9 +5,7 @@ import {getLanguage, setLanguage} from "@/i18n"
const state = { const state = {
login: false, login: false,
name: "", name: "",
currentProject: "",
language: getLanguage(), language: getLanguage(),
roles: [],
permissions: {} permissions: {}
} }