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/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"strings"
)
func (h *Handler) ListApiGroups() iris.Handler {
return func(ctx *context.Context) {
name := ctx.Params().GetString("name")
c, err := h.clusterService.Get(name,common.DBOptions{})
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()))
@@ -37,8 +39,8 @@ func (h *Handler) ListApiGroups() iris.Handler {
func (h *Handler) ListApiGroupResources() iris.Handler {
return func(ctx *context.Context) {
name := ctx.Params().GetString("name")
groupVersion := ctx.Params().GetString("group")
c, err := h.clusterService.Get(name,common.DBOptions{})
groupName := ctx.Params().GetString("group")
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()))
@@ -51,13 +53,39 @@ func (h *Handler) ListApiGroupResources() iris.Handler {
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
return
}
apiResources, err := client.ServerResourcesForGroupVersion(groupVersion)
gss, rss, err := client.ServerGroupsAndResources()
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get cluster failed: %s", err.Error()))
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/clusterbinding"
"github.com/KubeOperator/ekko/internal/service/v1/common"
"github.com/KubeOperator/ekko/internal/service/v1/project"
pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1"
"github.com/KubeOperator/ekko/pkg/certificate"
"github.com/KubeOperator/ekko/pkg/kubernetes"
@@ -26,12 +27,14 @@ import (
type Handler struct {
clusterService cluster.Service
clusterBindingService clusterbinding.Service
projectService project.Service
}
func NewHandler() *Handler {
return &Handler{
clusterService: cluster.NewService(),
clusterBindingService: clusterbinding.NewService(),
projectService: project.NewService(),
}
}
@@ -131,7 +134,6 @@ func (h *Handler) CreateCluster() iris.Handler {
txOptions := common.DBOptions{DB: tx}
req.CreatedBy = profile.Name
if err := client.CreateDefaultClusterRoles(); err != nil {
_ = tx.Rollback()
ctx.StatusCode(iris.StatusInternalServerError)
@@ -152,12 +154,9 @@ func (h *Handler) CreateCluster() iris.Handler {
CreatedBy: profile.Name,
},
Metadata: v1.Metadata{
Name: fmt.Sprintf("%s-%s-cluster-binding-", req.Name, profile.Name),
},
Subject: v1Cluster.Subject{
Name: profile.Name,
Kind: "User",
Name: fmt.Sprintf("%s-%s-cluster-binding", req.Name, profile.Name),
},
UserRef: profile.Name,
ClusterRef: req.Name,
}
@@ -186,11 +185,24 @@ func (h *Handler) CreateCluster() iris.Handler {
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{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("ekko:cluster-admin-%s", profile.Name),
Name: roleBindingName,
Annotations: map[string]string{
"created-by": profile.Name,
"builtin": "true",
},
Labels: map[string]string{
"user-name": profile.Name,
},
},
Subjects: []rbacV1.Subject{
@@ -200,7 +212,7 @@ func (h *Handler) CreateCluster() iris.Handler {
},
},
RoleRef: rbacV1.RoleRef{
Name: "cluster-admin",
Name: "Admin Cluster",
Kind: "ClusterRole",
},
}, metav1.CreateOptions{}); err != nil {
@@ -209,8 +221,8 @@ func (h *Handler) CreateCluster() iris.Handler {
ctx.Values().Set("message", err.Error())
return
}
}
_ = tx.Commit()
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})
}
}
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 {
return func(ctx *context.Context) {
@@ -266,7 +290,7 @@ func (h *Handler) ListClusters() iris.Handler {
return
}
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])
}
}
@@ -323,18 +347,19 @@ func Install(parent iris.Party) {
sp := parent.Party("/clusters")
sp.Post("", handler.CreateCluster())
sp.Get("", handler.ListClusters())
sp.Get("/:name", handler.GetCluster())
sp.Delete("/:name", handler.DeleteCluster())
sp.Post("/search", handler.SearchClusters())
sp.Get("/:name/members", handler.GetClusterMembers())
sp.Get("/:name/members", handler.ListClusterMembers())
sp.Post("/:name/members", handler.CreateClusterMember())
sp.Delete("/:name/members/:member", handler.DeleteClusterMember())
sp.Put("/:name/members/:member", handler.UpdateClusterMember())
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.Put("/:name/clusterroles/:clusterrole", handler.UpdateClusterRole())
sp.Delete("/:name/clusterroles/:clusterrole", handler.DeleteClusterRole())
sp.Get("/:name/apigroups", handler.ListApiGroups())
sp.Get("/:name/apigroups/{group:path}", handler.ListApiGroupResources())
sp.Get("/:name/namespaces", handler.ListNamespace())
}

View File

@@ -3,7 +3,6 @@ package cluster
import (
goContext "context"
"fmt"
"github.com/KubeOperator/ekko/internal/api/v1/session"
"github.com/KubeOperator/ekko/internal/service/v1/common"
"github.com/KubeOperator/ekko/pkg/kubernetes"
"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()))
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{})
if err != nil {
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()))
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{})
if err != nil {
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()))
return
}
u := ctx.Values().Get("profile")
profile := u.(session.UserProfile)
req.Annotations = map[string]string{
"ekko-i18n": "none",
"created-by": profile.Name,
"builtin": "false",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
}
req.Labels = map[string]string{
"manage": "ekko",
}
req.Labels[kubernetes.LabelManageKey] = "ekko"
resp, err := client.RbacV1().ClusterRoles().Create(goContext.TODO(), &req, metav1.CreateOptions{})
if err != nil {
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) {
name := ctx.Params().GetString("name")
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()))
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 {
ctx.StatusCode(iris.StatusInternalServerError)
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"
rbacV1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"strings"
"time"
)
@@ -46,10 +45,7 @@ func (h *Handler) UpdateClusterMember() iris.Handler {
return
}
binding, err := h.clusterBindingService.GetBindingByClusterNameAndSubject(name, v1Cluster.Subject{
Kind: req.Kind,
Name: req.Name,
}, common.DBOptions{})
binding, err := h.clusterBindingService.GetBindingByClusterNameAndUserName(name, req.Name, common.DBOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
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{
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 {
ctx.StatusCode(iris.StatusInternalServerError)
@@ -96,25 +92,22 @@ func (h *Handler) UpdateClusterMember() iris.Handler {
return
}
}
u := ctx.Values().Get("profile")
profile := u.(session.UserProfile)
for i := range forCreate {
b := rbacV1.ClusterRoleBinding{
ObjectMeta: metav1.ObjectMeta{
Name: fmt.Sprintf("%s-%s-%s", name, req.Name, forCreate[i]),
Labels: map[string]string{
"manage": "ekko",
"subject-name": req.Name,
"subject-kind": req.Kind,
"kubeoperator.io/manage": "ekko",
"user-name": req.Name,
},
Annotations: map[string]string{
"created-by": profile.Name,
"builtin": "false",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
},
Subjects: []rbacV1.Subject{
{
Kind: req.Kind,
Kind: "User",
Name: req.Name,
},
},
@@ -140,27 +133,6 @@ func (h *Handler) GetClusterMember() iris.Handler {
name := ctx.Params().GetString("name")
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{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
@@ -175,7 +147,7 @@ func (h *Handler) GetClusterMember() iris.Handler {
return
}
binding, err := h.clusterBindingService.GetBindingByClusterNameAndSubject(name, subject, common.DBOptions{})
binding, err := h.clusterBindingService.GetBindingByClusterNameAndUserName(name, memberName, common.DBOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
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{
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 {
ctx.StatusCode(iris.StatusInternalServerError)
@@ -193,8 +165,7 @@ func (h *Handler) GetClusterMember() iris.Handler {
var member Member
member.ClusterRoles = make([]string, 0)
member.Kind = binding.Subject.Kind
member.Name = binding.Subject.Name
member.Name = binding.UserRef
set := collectons.NewStringSet()
for i := range clusterRoleBindings.Items {
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) {
name := ctx.Params().GetString("name")
bindings, err := h.clusterBindingService.GetClusterBindingByClusterName(name, common.DBOptions{})
@@ -214,17 +185,12 @@ func (h *Handler) GetClusterMembers() iris.Handler {
ctx.Values().Set("message", err.Error())
return
}
subjectMap := map[v1Cluster.Subject]v1Cluster.Binding{}
var members []Member
for i := range bindings {
subjectMap[bindings[i].Subject] = bindings[i]
}
members := make([]Member, 0)
for key := range subjectMap {
members = append(members, Member{
Name: key.Name,
Kind: key.Kind,
BindingName: subjectMap[key].Name,
CreateAt: subjectMap[key].CreateAt,
Name: bindings[i].UserRef,
BindingName: bindings[i].Name,
CreateAt: bindings[i].CreateAt,
})
}
ctx.Values().Set("data", members)
@@ -238,7 +204,7 @@ func (h *Handler) CreateClusterMember() iris.Handler {
err := ctx.ReadJSON(&req)
if err != nil {
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
}
u := ctx.Values().Get("profile")
@@ -249,12 +215,9 @@ func (h *Handler) CreateClusterMember() iris.Handler {
CreatedBy: profile.Name,
},
Metadata: v1.Metadata{
Name: fmt.Sprintf("%s-%s-cluster-binding-", name, req.Name),
},
Subject: v1Cluster.Subject{
Name: req.Name,
Kind: req.Kind,
Name: fmt.Sprintf("%s-%s-cluster-binding", name, req.Name),
},
UserRef: req.Name,
ClusterRef: name,
}
c, err := h.clusterService.Get(name, common.DBOptions{})
@@ -264,7 +227,6 @@ func (h *Handler) CreateClusterMember() iris.Handler {
return
}
k := kubernetes.NewKubernetes(*c)
if req.Kind == "User" {
cert, err := k.CreateCommonUser(req.Name)
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
@@ -272,7 +234,6 @@ func (h *Handler) CreateClusterMember() iris.Handler {
return
}
binding.Certificate = cert
}
if err := h.clusterBindingService.CreateClusterBinding(&binding, common.DBOptions{}); err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error())
@@ -284,25 +245,23 @@ func (h *Handler) CreateClusterMember() iris.Handler {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", fmt.Sprintf("get k8s client failed: %s", err.Error()))
return
}
for i := range req.ClusterRoles {
binding := rbacV1.ClusterRoleBinding{
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{
"manage": "ekko",
"subject-name": req.Name,
"subject-kind": req.Kind,
"kubeoperator.io/manage": "ekko",
"user-name": req.Name,
},
Annotations: map[string]string{
"created-by": profile.Name,
"builtin": "false",
"created-at": time.Now().Format("2006-01-02 15:04:05"),
},
},
Subjects: []rbacV1.Subject{
{
Kind: req.Kind,
Kind: "User",
Name: req.Name,
},
},
@@ -318,6 +277,42 @@ func (h *Handler) CreateClusterMember() iris.Handler {
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)
}
}
@@ -325,11 +320,7 @@ func (h *Handler) CreateClusterMember() iris.Handler {
func (h *Handler) DeleteClusterMember() iris.Handler {
return func(ctx *context.Context) {
name := ctx.Params().GetString("name")
member := ctx.Params().GetString("member")
var kind string
if ctx.URLParamExists("kind") {
kind = ctx.URLParam("kind")
}
memberName := ctx.Params().GetString("member")
u := ctx.Values().Get("profile")
profile := u.(session.UserProfile)
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()))
return
}
if c.CreatedBy == profile.Name {
if c.CreatedBy == memberName {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", fmt.Sprintf("can not delete cluster importer %s", profile.Name))
return
}
subject := v1Cluster.Subject{
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{})
binding, err := h.clusterBindingService.GetBindingByClusterNameAndUserName(c.Name, memberName, common.DBOptions{})
if err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
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(),
metav1.DeleteOptions{},
metav1.ListOptions{
LabelSelector: fmt.Sprintf("subject-name=%s,subject-kind=%s", subject.Name, subject.Kind),
LabelSelector: fmt.Sprintf("user-name=%s", memberName),
}); err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
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"`
}
type NamespaceRoles struct {
Namespace string `json:"namespace"`
Roles []string `json:"roles"`
}
type Member struct {
Name string `json:"name"`
Kind string `json:"kind"`
ClusterRoles []string `json:"clusterRoles"`
CreateAt time.Time `json:"createAt"`
BindingName string `json:"bindingName"`
CreateAt time.Time `json:"createAt"`
NamespaceRoles []NamespaceRoles `json:"namespaceRoles"`
}

View File

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

View File

@@ -1,30 +1,37 @@
package session
import (
goContext "context"
"errors"
"fmt"
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/role"
"github.com/KubeOperator/ekko/internal/service/v1/rolebinding"
"github.com/KubeOperator/ekko/internal/service/v1/user"
pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1"
"github.com/KubeOperator/ekko/pkg/collectons"
"github.com/KubeOperator/ekko/pkg/kubernetes"
"github.com/asdine/storm/v3"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
"github.com/kataras/iris/v12/sessions"
"golang.org/x/crypto/bcrypt"
v1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Handler struct {
userService user.Service
roleService role.Service
clusterService cluster.Service
rolebindingService rolebinding.Service
}
func NewHandler() *Handler {
return &Handler{
clusterService: cluster.NewService(),
userService: user.NewService(),
roleService: role.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) {
handler := NewHandler()
sp := parent.Party("/sessions")
sp.Post("", handler.Login())
sp.Delete("", handler.Logout())
sp.Get("", handler.GetProfile())
sp.Get("/:cluster_name", handler.GetClusterProfile())
sp.Get("/status", handler.IsLogin())
sp.Get("/:cluster_name/namespaces", handler.ListUserNamespace())
}

View File

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

View File

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

View File

@@ -2,15 +2,10 @@ package cluster
import v1 "github.com/KubeOperator/ekko/internal/model/v1"
type Subject struct {
Kind string `json:"kind"`
Name string `json:"name"`
}
type Binding struct {
v1.BaseModel `storm:"inline"`
v1.Metadata `storm:"inline"`
Subject Subject `json:"subject" storm:"inline"`
UserRef string `json:"UserRef" storm:"inline"`
ClusterRef string `json:"clusterRef" storm:"index"`
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)
CreateClusterBinding(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
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 {
@@ -45,9 +44,9 @@ func (s *service) UpdateClusterBinding(name string, binding *v1Cluster.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)
query := db.Select(q.Eq("Subject", subject))
query := db.Select(q.Eq("UserRef", userName))
var rbs []v1Cluster.Binding
if err := query.Find(&rbs); err != nil {
return rbs, err
@@ -55,9 +54,9 @@ func (s *service) GetBindingsBySubject(subject v1Cluster.Subject, options common
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)
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
if err := query.First(&rb); err != nil {
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"
v1 "k8s.io/api/authorization/v1"
certv1 "k8s.io/api/certificates/v1"
rbacV1 "k8s.io/api/rbac/v1"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/kubernetes"
@@ -40,69 +39,20 @@ func NewKubernetes(cluster v1Cluster.Cluster) Interface {
}
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()
if err != nil {
return err
}
for i := range defaultRoles {
instance, err := client.RbacV1().ClusterRoles().Get(context.TODO(), defaultRoles[i].Name, metav1.GetOptions{})
for i := range initClusterRoles {
instance, err := client.RbacV1().ClusterRoles().Get(context.TODO(), initClusterRoles[i].Name, metav1.GetOptions{})
if err != nil {
if !strings.Contains(strings.ToLower(err.Error()), "not found") {
return err
}
}
if instance == nil {
_, err = client.RbacV1().ClusterRoles().Create(context.TODO(), &defaultRoles[i], metav1.CreateOptions{})
if instance == nil || instance.Name == "" {
_, err = client.RbacV1().ClusterRoles().Create(context.TODO(), &initClusterRoles[i], metav1.CreateOptions{})
if err != nil {
return err
}

View File

@@ -14,6 +14,15 @@ export function getCurrentUser() {
return get(authUrl)
}
export function getCurrentClusterUser(clusterName) {
return get(`${authUrl}/${clusterName}`)
}
export function isLogin() {
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"
export function create(data) {
return post(authUrl, data)
}
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}`)
export function getCluster(name) {
return get(`${authUrl}/${name}`)
}

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">
<span class="el-dropdown-link">
<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>
</span>
<el-dropdown-menu slot="dropdown">
@@ -22,7 +22,7 @@
name: "UserSetting",
computed: {
...mapGetters([
"name"
"nickName"
])
},
methods: {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -48,7 +48,6 @@
},
},
created() {
console.log(this.permission_routes)
}
}
</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 filters from "./filters";
import JsonViewer from 'vue-json-viewer'
import directives from "./directive";
Vue.config.productionTip = false
@@ -34,6 +36,7 @@ library.add(fas, far, fab)
Vue.use(icons);
Vue.use(filters);
Vue.use(directives)
Vue.use(JsonViewer)
new Vue({

View File

@@ -2,21 +2,31 @@ import router from "./router"
import NProgress from "nprogress"
import "nprogress/nprogress.css"
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 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) {
next()
} else {
try {
const user = await store.dispatch("user/getCurrentUser")
const accessRoutes = await store.dispatch("permission/generateRoutes", user)
const {clusterRoles} = await store.dispatch("user/getCurrentUser")
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)
next({ ...to, replace: true })
next({...to, replace: true})
} catch (error) {
NProgress.done()
}
@@ -26,22 +36,24 @@ const generateRoutes = async (to, from, next) => {
//路由前置钩子,根据实际需求修改
router.beforeEach(async (to, from, next) => {
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")
if (isLogin) {
if (to.path === "/login") {
next({ path: "/" })
next({path: "/"})
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)
} else {
/* has not login*/

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -2,6 +2,7 @@ import Layout from "@/business/app-layout/horizontal-layout";
const Workloads = {
path: "/workloads",
parent: true,
sort: 4,
component: Layout,
name: "Workloads",
@@ -12,6 +13,11 @@ const Workloads = {
children: [
{
path: "/deployments",
requirePermission: {
apiGroup: "apps",
resource: "deployments",
verb: "list",
},
component: () => import("@/business/workloads/deployments"),
name: "Deployments",
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",
requirePermission: {
apiGroup: "apps",
resource: "daemonsets",
verb: "list",
},
component: () => import("@/business/workloads/daemonsets"),
name: "DaemonSets",
meta: {
@@ -156,6 +95,11 @@ const Workloads = {
{
path: "/statefulsets",
requirePermission: {
apiGroup: "apps",
resource: "statefulsets",
verb: "list",
},
component: () => import("@/business/workloads/statefulsets"),
name: "StatefulSets",
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",
requirePermission: {
apiGroup: "",
resource: "pods",
verb: "list",
},
component: () => import("@/business/workloads/pods"),
name: "Pods",
meta: {

View File

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

View File

@@ -5,24 +5,37 @@ const state = {
addRoutes: []
}
// function hasPermission(user, route) {
// if (user && route){
// console.log("")
// }
// return true
// }
function hasPermission(clusterRoles, route) {
if (route.requirePermission) {
for (const clusterRole of clusterRoles) {
if (clusterRole.rules.length > 0) {
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 = []
routes.forEach(route => {
const tmp = { ...route }
// if (hasPermission(user, tmp)) {
const tmp = {...route}
if (hasPermission(clusterRoles, tmp)) {
if (tmp.children) {
tmp.children = filterRolesRoutes(tmp.children, user)
tmp.children = filterRolesRoutes(tmp.children, clusterRoles)
}
res.push(tmp)
// }
}
})
return res
@@ -36,12 +49,28 @@ const mutations = {
}
const actions = {
generateRoutes ({ commit }, p) {
generateRoutes({commit}, p) {
return new Promise((resolve, reject) => {
const user = p
const clusterRoles = p
let accessedRoutes
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)
resolve(accessedRoutes)
} 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 {getLanguage, setLanguage} from "@/i18n"
import store from "../../store"
const state = {
login: false,
name: "",
currentProject: "",
language: getLanguage(),
roles: [],
cluster: ""
cluster: "",
clusterRoles: [],
}
const mutations = {
@@ -21,6 +22,9 @@ const mutations = {
SET_NAME: (state, name) => {
state.name = name
},
SET_NICK_NAME: (state, nickName) => {
state.nickName = nickName
},
SET_LANGUAGE: (state, language) => {
state.language = language
setLanguage(language)
@@ -28,20 +32,23 @@ const mutations = {
SET_ROLES: (state, roles) => {
state.roles = roles
},
SET_CURRENT_MENU: (state, menu) => {
state.menu = menu
},
SET_CURRENT_CLUSTER: (state, name) => {
state.cluster = name
},
SET_CLUSTER_ROLES: (state, clusterRoles) => {
state.clusterRoles = clusterRoles
}
}
const actions = {
login ({ commit }, userInfo) {
const { username, password } = userInfo
setCurrentCluster({commit}, clusterName) {
commit("SET_CURRENT_CLUSTER", clusterName)
},
login({commit}, userInfo) {
const {username, password} = userInfo
return new Promise((resolve, reject) => {
commit("LOGIN")
login({ username: username.trim(), password: password }).then(response => {
login({username: username.trim(), password: password}).then(response => {
commit("LOGIN")
resolve(response)
}).catch(error => {
@@ -50,7 +57,7 @@ const actions = {
})
},
isLogin ({ commit }) {
isLogin({commit}) {
return new Promise((resolve) => {
if (state.login) {
resolve(true)
@@ -67,22 +74,25 @@ const actions = {
})
})
},
getCurrentUser ({ commit }) {
getCurrentUser({commit}) {
return new Promise((resolve, reject) => {
getCurrentUser().then(data => {
const clusterName = store.getters.cluster
getCurrentClusterUser(clusterName).then(data => {
const user = data.data
user["roles"] = ["ADMIN"]
const { name, roles } = user
const {name, nickName, roles, clusterRoles} = user
commit("SET_NAME", name)
commit("SET_ROLES", roles)
commit("SET_LANGUAGE", "zh-CN")
commit("SET_CLUSTER_ROLES", clusterRoles)
commit("SET_NICK_NAME", nickName)
resolve(user)
}).catch(error => {
reject(error)
})
})
},
logout ({ commit }) {
logout({commit}) {
logout().then(() => {
commit("LOGOUT")
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) {
return get(`${baseUrl}/${name}/members/${memberName}?kind=${kind}`)
export function getClusterMember(name, memberName) {
return get(`${baseUrl}/${name}/members/${memberName}`)
}
export function deleteClusterMember(name, memberName, kind) {
@@ -70,3 +70,15 @@ export function listClusterResourceByGroupVersion(name, groupVersion) {
export function deleteClusterRole(name, 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 }}
</template>
</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}">
{{$t("business.cluster_role."+row.metadata.annotations["ekko-i18n"])}}
{{ $t('business.cluster.' + row.metadata.labels["kubeoperator.io/role-type"]) }}
</template>
</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}">
{{ row.metadata.annotations["created-by"] }}
{{ $t('commons.bool.' + row.metadata.annotations["builtin"]) }}
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.created_time')" min-width="100" fix>
@@ -39,10 +41,12 @@
center z-index="20">
<el-form :model="ruleForm" label-position="top" label- width="60px">
<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"
:value="item.preferredVersion.groupVersion">
{{item.preferredVersion.groupVersion}}
:value="item.name">
<span v-if="item.name">
{{ item.name }}
</span>
</el-option>
</el-select>
</el-form-item>
@@ -52,8 +56,8 @@
@change="onResourcesChange">
<el-option v-for="(item,index) in apiResourceOptions"
:key="index"
:value="item.name">
{{item.name}}
:value="item">
{{ item }}
</el-option>
</el-select>
</el-form-item>
@@ -64,7 +68,7 @@
<el-option v-for="(item,index) in verbOptions"
:key="index"
:value="item">
{{item}}
{{ item }}
</el-option>
</el-select>
</el-form-item>
@@ -83,16 +87,21 @@
width="60%"
center z-index="10">
<el-form :model="clusterRoleForm" label-position="left" label- width="60px">
<el-form-item label="名称">
<el-input v-model="clusterRoleForm.name"></el-input>
<el-form :model="clusterRoleForm" label-position="left" label-width="144px">
<el-form-item :label="$t('commons.table.name')">
<el-input v-model="clusterRoleForm.name" style="width: 80%"></el-input>
</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-table
:data="clusterRoleForm.rules"
border
style="width: 100%">
style="width: 80%">
<el-table-column
prop="apiGroup"
label="API Group"
@@ -103,7 +112,7 @@
label="API Resource"
>
<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>
</el-table-column>
<el-table-column
@@ -111,7 +120,7 @@
label="Verbs"
>
<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>
</el-table-column>
<el-table-column>
@@ -159,7 +168,7 @@
label="API Resource"
>
<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>
</el-table-column>
<el-table-column
@@ -167,7 +176,7 @@
label="Verbs"
>
<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>
</el-table-column>
<el-table-column>
@@ -193,19 +202,19 @@
</template>
<script>
import LayoutContent from "@/components/layout/LayoutContent"
import ComplexTable from "@/components/complex-table"
import {
import LayoutContent from "@/components/layout/LayoutContent"
import ComplexTable from "@/components/complex-table"
import {
listClusterRoles,
listClusterApiGroups,
listClusterResourceByGroupVersion,
createClusterRole,
deleteClusterRole,
updateClusterRole
} from "@/api/clusters";
} from "@/api/clusters";
export default {
export default {
name: "ClusterRoles",
props: ["name"],
components: {LayoutContent, ComplexTable},
@@ -221,6 +230,7 @@
memberOptions: [],
clusterRoleForm: {
name: "",
type: "cluster",
rules: [],
},
editForm: {
@@ -230,7 +240,7 @@
resourcesDisable: false,
verbsDisable: false,
ruleForm: {
groupVersion: "",
apiGroup: "",
resources: [],
verbs: []
},
@@ -240,6 +250,9 @@
icon: "el-icon-edit",
click: (row) => {
this.onEdit(row)
},
disabled: (row) => {
return row.metadata.annotations['builtin'] === 'true'
}
},
{
@@ -247,6 +260,9 @@
icon: "el-icon-delete",
click: (row) => {
this.onDelete(row)
},
disabled: (row) => {
return row.metadata.annotations['builtin'] === 'true'
}
},
],
@@ -266,11 +282,17 @@
this.clusterRoleForm = {
name: "",
rules: [],
type: "cluster",
}
this.createDialogOpened = true
listClusterApiGroups(this.name).then(data => {
this.apiGroupsOptions.push({preferredVersion: {groupVersion: "*"}})
this.apiGroupsOptions.push({name: "*"})
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 = []
for (const rule of row.rules) {
if (rule.apiGroups) {
this.editForm.rules.push({
apiGroup: rule.apiGroups[0],
const r = {
apiGroup: [],
resources: rule.resources,
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
listClusterApiGroups(this.name).then(data => {
this.apiGroupsOptions.push({preferredVersion: {groupVersion: "*"}})
this.apiGroupsOptions.push({name: "*"})
this.apiGroupsOptions = this.apiGroupsOptions.concat(data.data)
for (const group of this.apiGroupsOptions) {
if (group.name === "") {
group.name = "core"
}
}
})
},
onRuleCreate() {
this.ruleForm = {
groupVersion: "",
apiGroup: "",
resources: [],
verbs: [],
}
@@ -309,14 +344,14 @@
this.ruleForm.verbs = []
this.resourcesDisable = false
this.verbsDisable = false
if (this.ruleForm.groupVersion === "*") {
this.apiResourceOptions = [{name: "*"}]
if (this.ruleForm.apiGroup === "*") {
this.apiResourceOptions = ["*"]
this.ruleForm.resources = ["*"]
this.resourcesDisable = true
return
}
listClusterResourceByGroupVersion(this.name, this.ruleForm.groupVersion).then(data => {
this.apiResourceOptions.push({name: "*"})
listClusterResourceByGroupVersion(this.name, this.ruleForm.apiGroup).then(data => {
this.apiResourceOptions.push("*")
this.apiResourceOptions = this.apiResourceOptions.concat(data.data);
})
},
@@ -331,20 +366,18 @@
}
},
onRuleConfirm() {
switch (this.operation) {
case "create":
this.clusterRoleForm.rules.push({
apiGroup: this.ruleForm.groupVersion,
const rule = {
apiGroup: this.ruleForm.apiGroup,
resources: this.ruleForm.resources,
verbs: this.ruleForm.verbs
})
}
switch (this.operation) {
case "create":
this.clusterRoleForm.rules.push(rule)
break
case "update":
this.editForm.rules.push({
apiGroup: this.ruleForm.groupVersion,
resources: this.ruleForm.resources,
verbs: this.ruleForm.verbs
})
this.editForm.rules.push(rule)
break
}
this.ruleDialogOpened = false
@@ -376,6 +409,9 @@
const req = {
metadata: {
name: this.clusterRoleForm.name,
labels: {
"kubeoperator.io/role-type": this.clusterRoleForm.type
}
},
rules: []
}
@@ -405,7 +441,6 @@
verbs: rule.verbs,
})
}
console.log(123)
updateClusterRole(this.name, req.metadata.name, req).then(() => {
this.list()
this.editDialogOpened = false
@@ -415,7 +450,7 @@
created() {
this.list()
}
}
}
</script>
<style scoped>

View File

@@ -1,8 +1,8 @@
<template>
<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-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+'/members'">成员</el-menu-item>
<el-menu-item :index="'/clusters/detail/'+name+'/clusterroles'">角色</el-menu-item>
</el-menu>
<br/>
<div class="detailClass">
@@ -13,9 +13,9 @@
</template>
<script>
import LayoutContent from "@/components/layout/LayoutContent"
import LayoutContent from "@/components/layout/LayoutContent"
export default {
export default {
name: "ClusterEdit",
props: ["name"],
components: {LayoutContent},
@@ -23,19 +23,19 @@
return {}
},
methods: {},
}
}
</script>
<style scoped>
.menuClass {
.menuClass {
position: fixed;
z-index: 1;
width: calc(100vw - 330px);
}
}
.detailClass {
.detailClass {
margin-top: 60px;
height: calc(100% - 140px);
overflow: auto;
}
}
</style>

View File

@@ -14,11 +14,6 @@
{{ row.name }}
</template>
</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>
<template v-slot:default="{row}">
{{ row.createAt }}
@@ -29,38 +24,100 @@
<el-dialog
title="提示"
:visible.sync="createDialogOpened"
width="20%"
center>
<el-form :model="memberForm" label-position="left" label- width="60px">
<el-form-item label="主体类型">
<el-select v-model="memberForm.subjectKind" @change="onSubjectKindChange">
<el-option value="User">
用户
</el-option>
<el-option value="Group">
用户组
title="创建命名空间角色"
:visible.sync="namespaceRoleDialogOpened"
width="40%"
center z-index="20">
<el-form label-position="left" label-width="144px" :model="namespaceRoleForm">
<el-form-item label="命名空间">
<el-select v-model="namespaceRoleForm.namespace" style="width:100%">
<el-option v-for="(item,index) in getNamespaceOptions"
:key="index"
:value="item.metadata.name">
{{ item.metadata.name }}
</el-option>
</el-select>
</el-form-item>
<el-form-item label="主体名称">
<el-select v-model="memberForm.subjectName">
<el-option v-for="(item, index) in memberOptions" :key="index" :value="item.name">
{{item.name}}
<el-form-item label="角色">
<el-select multiple v-model="namespaceRoleForm.roles" style="width:100%">
<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-select>
</el-form-item>
<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
v-for="(item,index) in clusterRolesOptions"
:key="index"
:value="item.metadata.name">
{{item.metadata.name}}
{{ item.metadata.name }}
</el-option>
</el-select>
</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>
<span slot="footer" class="dialog-footer">
<el-button @click="createDialogOpened = false"> </el-button>
@@ -68,61 +125,57 @@
</span>
</el-dialog>
<el-dialog
title="提示"
title="编辑成员"
:visible.sync="editDialogOpened"
width="20%"
width="60%"
center>
<el-form :model="editForm" label-position="left" label- width="60px">
<el-form-item label="主体类型">
<el-select v-model="editForm.subjectKind" disabled @change="onSubjectKindChange">
<el-option value="User">
用户
</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-form :model="editForm" label-position="left" label-width="144px">
<el-form-item label="用户名称">
<el-select v-model="editForm.userName" style="width: 80%" disabled="disabled">
<el-option v-for="(item, index) in userOptions" :key="index" :value="item.name">
{{ item.name }}
</el-option>
</el-select>
</el-form-item>
<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
v-for="(item,index) in clusterRolesOptions"
:key="index"
:value="item.metadata.name">
{{item.metadata.name}}
{{ item.metadata.name }}
</el-option>
</el-select>
</el-form-item>
</div>
</el-form>
<span slot="footer" class="dialog-footer">
<el-button @click="editDialogOpened = false"> </el-button>
<el-button type="primary" @click="onEditConfirm"> </el-button>
</span>
</el-dialog>
</layout-content>
</template>
<script>
import LayoutContent from "@/components/layout/LayoutContent"
import ComplexTable from "@/components/complex-table"
import {createClusterMember, listClusterMembers} from "@/api/clusters"
import {listUsers} from "@/api/users"
import {listGroups} from "@/api/groups"
import {listClusterRoles, deleteClusterMember, getClusterMember, updateClusterMember} from "@/api/clusters";
import LayoutContent from "@/components/layout/LayoutContent"
import ComplexTable from "@/components/complex-table"
import {createClusterMember, listClusterMembers, listNamespaces} from "@/api/clusters"
import {listUsers} from "@/api/users"
import {listClusterRoles, deleteClusterMember, getClusterMember, updateClusterMember} from "@/api/clusters";
export default {
export default {
name: "ClusterMembers",
props: ["name"],
components: {LayoutContent, ComplexTable},
@@ -130,17 +183,25 @@
return {
createDialogOpened: false,
editDialogOpened: false,
memberOptions: [],
namespaceRoleDialogOpened: false,
userOptions: [],
clusterRolesOptions: [],
namespaceRoleOptions: [],
namespaceOptions: [],
namespaceRoleForm: {
namespace: "",
roles: [],
},
memberForm: {
subjectKind: "",
subjectName: "",
clusterRoles: []
userName: "",
customClusterRoles: [],
namespaceRoles: [],
roleType: "admin"
},
editForm: {
subjectKind: "",
subjectName: "",
clusterRoles: []
userName: "",
customClusterRoles: [],
roleType: ""
},
buttons: [
{
@@ -161,6 +222,19 @@
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: {
list() {
this.loading = false
@@ -170,23 +244,42 @@
})
},
onCreate() {
this.memberForm.subjectKind = ""
this.memberForm.clusterRoles = []
this.createDialogOpened = true
listClusterRoles(this.name).then(data => {
console.log(data)
this.clusterRolesOptions = data.data
this.clusterRolesOptions = data.data.filter((r) => {
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) {
listClusterRoles(this.name).then(data => {
this.clusterRolesOptions = data.data
getClusterMember(this.name, row.name, row.kind).then(data => {
this.editForm.subjectName = data.data.name
this.editForm.subjectKind = data.data.kind
this.editForm.clusterRoles = data.data.clusterRoles
getClusterMember(this.name, row.name).then(data => {
this.editForm.userName = data.data.name
if (data.data.clusterRoles.length === 1) {
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
@@ -203,45 +296,68 @@
})
});
},
onSubjectKindChange() {
this.memberForm.subjectName = ""
if (this.memberForm.subjectKind === 'Group') {
listGroups().then((data) => {
this.memberOptions = data.data;
})
}
if (this.memberForm.subjectKind === 'User') {
listUsers().then((data) => {
this.memberOptions = data.data;
})
}
onNamespaceRoleCreate() {
this.namespaceRoleForm.namespaceRoles = []
this.namespaceRoleDialogOpened = true
this.namespaceRoleForm.namespace = ""
},
onEditConfirm() {
updateClusterMember(this.name, this.editForm.subjectName, {
name: this.editForm.subjectName,
kind: this.editForm.subjectKind,
clusterRoles: this.editForm.clusterRoles
}).then(() => {
let req = {
name: this.editForm.userName,
}
switch (this.editForm.roleType) {
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.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() {
createClusterMember(this.name, {
kind: this.memberForm.subjectKind,
name: this.memberForm.subjectName,
clusterRoles: this.memberForm.clusterRoles
}).then(() => {
let req = {
name: this.memberForm.userName,
namespaceRoles: this.memberForm.namespaceRoles,
}
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.list()
})
}
},
},
created() {
this.list()
}
}
}
</script>
<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: "集群",
namespace: "命名空间",
scope: "作用域",
version: "版本",
list: "集群列表",
import: "导入集群",
edit: "编辑",
nodes: "节点",
api_server_help: "例如: https://172.16.10.100:8443",
router_help: "装有 kube-proxy 的任意节点的且可以被访问到的 IP 地址",
label: "标签",
description: "描述",
cluster_detail: "集群详情",
@@ -106,7 +106,7 @@ const message = {
expect: "敬请期待",
management: "管理",
open_dashboard: "控制台",
cluster_version: "版本"
cluster_version: "版本",
},
namespace: {

View File

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

View File

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

View File

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