feat(cluster):导入集群操作

This commit is contained in:
chenyang
2021-06-15 18:34:55 +08:00
parent dc88e03229
commit 2b6f9bcbdb
19 changed files with 574 additions and 376 deletions

1
go.sum
View File

@@ -237,6 +237,7 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc=
github.com/imdario/mergo v0.3.5 h1:JboBksRwiiAJWvIYJVo46AfV+IAIKZpfrSzVKj42R4Q=
github.com/imdario/mergo v0.3.5/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk=
github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA=

View File

@@ -1,18 +1,16 @@
package cluster
import (
kContext "context"
"fmt"
v1 "github.com/KubeOperator/ekko/internal/model/v1"
"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/common"
pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1"
"github.com/KubeOperator/ekko/pkg/util/kubernetes"
"github.com/KubeOperator/ekko/pkg/kubernetes"
"github.com/asdine/storm/v3"
"github.com/google/uuid"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
)
type Handler struct {
@@ -25,62 +23,64 @@ func NewHandler() *Handler {
}
}
func (h *Handler) Create() iris.Handler {
func (h *Handler) CreateCluster() iris.Handler {
return func(ctx *context.Context) {
var clusterImport Import
if err := ctx.ReadJSON(&clusterImport); err != nil {
var req Cluster
if err := ctx.ReadJSON(&req); err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", err.Error())
return
}
c, err := h.clusterService.Get(clusterImport.Name)
if err != nil && err != storm.ErrNotFound {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", fmt.Sprintf("error: %s", err.Error()))
if req.CaDataStr != "" {
req.CaCertificate.CertData = []byte(req.CaDataStr)
}
if req.Spec.Authentication.Mode == "certificate" {
req.Spec.Authentication.Certificate.CertData = []byte(req.CertDataStr)
req.Spec.Authentication.Certificate.KeyData = []byte(req.KeyDataStr)
}
client := kubernetes.NewKubernetes(req.Cluster)
if err := client.Ping(); err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error())
return
}
if c != nil && c.Name != "" {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", fmt.Sprintf("cluster name %s is alerady exist", clusterImport.Name))
v, _ := client.Version()
req.Status.Version = v.GitVersion
u := ctx.Values().Get("profile")
profile := u.(session.UserProfile)
req.CreatedBy = profile.Name
if err := h.clusterService.Create(&req.Cluster, common.DBOptions{}); err != nil {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error())
return
}
client, err := kubernetes.NewKubernetesClient(&kubernetes.Config{
Host: clusterImport.ApiServer,
Token: clusterImport.Token,
})
if err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", fmt.Sprintf("import cluster filed: %s", err.Error()))
return
}
_, err = client.CoreV1().Namespaces().List(kContext.TODO(), metav1.ListOptions{})
if err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", fmt.Sprintf("import cluster failed: %s", err.Error()))
return
}
cs := v1Cluster.Cluster{
Metadata: v1.Metadata{
Name: clusterImport.Name,
UUID: uuid.New().String(),
},
Token: clusterImport.Token,
Router: clusterImport.Router,
ApiServer: clusterImport.ApiServer,
Status: "Running",
}
err = h.clusterService.Create(&cs)
if err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", fmt.Sprintf("create cluster failed: %s", err.Error()))
return
}
ctx.StatusCode(iris.StatusOK)
ctx.Values().Set("data", cs)
ctx.Values().Set("data", &req)
}
}
func (h *Handler) ListAll() iris.Handler {
func (h *Handler) SearchClusters() iris.Handler {
return func(ctx *context.Context) {
pageNum, _ := ctx.Values().GetInt(pkgV1.PageNum)
pageSize, _ := ctx.Values().GetInt(pkgV1.PageSize)
var conditions pkgV1.Conditions
if ctx.GetContentLength() > 0 {
if err := ctx.ReadJSON(&conditions); err != nil {
ctx.StatusCode(iris.StatusBadRequest)
ctx.Values().Set("message", err.Error())
return
}
}
clusters, total, err := h.clusterService.Search(pageNum, pageSize, conditions, common.DBOptions{})
if err != nil && err != storm.ErrNotFound {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error())
}
ctx.Values().Set("data", pkgV1.Page{Items: clusters, Total: total})
}
}
func (h *Handler) ListClusters() iris.Handler {
return func(ctx *context.Context) {
var clusters []v1Cluster.Cluster
clusters, err := h.clusterService.All()
@@ -94,20 +94,7 @@ func (h *Handler) ListAll() iris.Handler {
}
}
func (h *Handler) Search() iris.Handler {
return func(ctx *context.Context) {
pageNum, _ := ctx.Values().GetInt(pkgV1.PageNum)
pageSize, _ := ctx.Values().GetInt(pkgV1.PageSize)
clusters, total, err := h.clusterService.Search(pageNum, pageSize)
if err != nil && err != storm.ErrNotFound {
ctx.StatusCode(iris.StatusInternalServerError)
ctx.Values().Set("message", err.Error())
}
ctx.Values().Set("data", pkgV1.Page{Items: clusters, Total: total})
}
}
func (h *Handler) Delete() iris.Handler {
func (h *Handler) DeleteCluster() iris.Handler {
return func(ctx *context.Context) {
name := ctx.Params().GetString("name")
err := h.clusterService.Delete(name)
@@ -123,8 +110,8 @@ func (h *Handler) Delete() iris.Handler {
func Install(parent iris.Party) {
handler := NewHandler()
sp := parent.Party("/clusters")
sp.Post("", handler.Create())
sp.Get("", handler.ListAll())
sp.Delete("/{name:string}", handler.Delete())
sp.Post("/search", handler.Search())
sp.Post("", handler.CreateCluster())
sp.Get("", handler.ListClusters())
sp.Delete("/:name", handler.DeleteCluster())
sp.Post("/search", handler.SearchClusters())
}

View File

@@ -1,8 +1,10 @@
package cluster
type Import struct {
Name string `json:"name"`
ApiServer string `json:"apiServer"`
Router string `json:"router"`
Token string `json:"token"`
import v1Cluster "github.com/KubeOperator/ekko/internal/model/v1/cluster"
type Cluster struct {
v1Cluster.Cluster
KeyDataStr string `json:"keyDataStr"`
CertDataStr string `json:"certDataStr"`
CaDataStr string `json:"caDataStr"`
}

View File

@@ -245,7 +245,7 @@ func AddV1Route(app iris.Party) {
authParty.Use(resourceNameInvalidHandler())
authParty.Get("/", apiResourceHandler(authParty))
user.Install(authParty)
cluster.Install(v1Party)
cluster.Install(authParty)
group.Install(authParty)
role.Install(authParty)
}

View File

@@ -5,8 +5,43 @@ import v1 "github.com/KubeOperator/ekko/internal/model/v1"
type Cluster struct {
v1.BaseModel `storm:"inline"`
v1.Metadata `storm:"inline"`
ApiServer string `json:"apiServer"`
Router string `json:"router"`
Token string `json:"token"`
Status string `json:"status"`
CaCertificate Certificate `json:"caCertificate" storm:"inline"`
Spec Spec `json:"spec" storm:"inline"`
Status Status `json:"status" storm:"inline"`
}
type Spec struct {
Connect Connect `json:"connect" storm:"inline"`
Authentication Authentication `json:"authentication" storm:"inline"`
}
type Connect struct {
Direction string `json:"direction"`
Forward Forward `json:"forward" storm:"inline"`
}
type Forward struct {
ApiServer string `json:"apiServer"`
Proxy Proxy `json:"proxy" storm:"inline"`
}
type Proxy struct {
URL string `json:"url"`
Username string `json:"username"`
Password string `json:"password"`
}
type Authentication struct {
Mode string `json:"mode"`
BearerToken string `json:"bearerToken"`
Certificate Certificate `json:"certificate" storm:"inline"`
}
type Certificate struct {
KeyData []byte `json:"keyData"`
CertData []byte `json:"certData"`
}
type Status struct {
Version string `json:"version"`
}

View File

@@ -1,41 +1,35 @@
package proxy
import (
"crypto/tls"
"fmt"
"github.com/kataras/iris/v12"
"github.com/kataras/iris/v12/context"
"net/http"
"net/http/httputil"
"net/url"
)
func KubernetesClientProxy(ctx *context.Context) {
clusterName := ctx.Params().Get("cluster_name")
proxyPath := ctx.Params().Get("p")
cluster, err := clusterService.Get(clusterName)
if err != nil {
_, _ = ctx.JSON(iris.StatusInternalServerError)
return
}
u, err := url.Parse(cluster.ApiServer)
if err != nil {
_, _ = ctx.JSON(iris.StatusInternalServerError)
return
}
proxy := httputil.NewSingleHostReverseProxy(u)
proxy.Transport = &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
token := fmt.Sprintf("%s %s", keyPrefix, cluster.Token)
ctx.Request().Header.Add(AuthorizationHeader, token)
ctx.Request().URL.Path = proxyPath
proxy.ModifyResponse = func(response *http.Response) error {
if response.StatusCode == http.StatusUnauthorized {
response.StatusCode = http.StatusInternalServerError
}
return nil
}
proxy.ServeHTTP(ctx.ResponseWriter(), ctx.Request())
//clusterName := ctx.Params().Get("cluster_name")
//proxyPath := ctx.Params().Get("p")
//cluster, err := clusterService.Get(clusterName)
//if err != nil {
// _, _ = ctx.JSON(iris.StatusInternalServerError)
// return
//}
//u, err := url.Parse(cluster.ApiServer)
//if err != nil {
// _, _ = ctx.JSON(iris.StatusInternalServerError)
// return
//}
//proxy := httputil.NewSingleHostReverseProxy(u)
//proxy.Transport = &http.Transport{
// TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
//}
//token := fmt.Sprintf("%s %s", keyPrefix, cluster.Token)
//ctx.Request().Header.Add(AuthorizationHeader, token)
//ctx.Request().URL.Path = proxyPath
//proxy.ModifyResponse = func(response *http.Response) error {
// if response.StatusCode == http.StatusUnauthorized {
// response.StatusCode = http.StatusInternalServerError
// }
// return nil
//}
//
//proxy.ServeHTTP(ctx.ResponseWriter(), ctx.Request())
}

View File

@@ -3,14 +3,19 @@ package cluster
import (
v1Cluster "github.com/KubeOperator/ekko/internal/model/v1/cluster"
"github.com/KubeOperator/ekko/internal/server"
"github.com/KubeOperator/ekko/internal/service/v1/common"
pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1"
"github.com/google/uuid"
"time"
)
type Cluster interface {
Create(cluster *v1Cluster.Cluster) error
common.DBService
Create(cluster *v1Cluster.Cluster, options common.DBOptions) error
Get(name string) (*v1Cluster.Cluster, error)
All() ([]v1Cluster.Cluster, error)
Delete(name string) error
Search(num, size int) ([]v1Cluster.Cluster, int, error)
Search(num, size int, conditions pkgV1.Conditions, options common.DBOptions) ([]v1Cluster.Cluster, int, error)
}
func NewClusterService() Cluster {
@@ -18,10 +23,14 @@ func NewClusterService() Cluster {
}
type cluster struct {
common.DefaultDBService
}
func (c *cluster) Create(cluster *v1Cluster.Cluster) error {
db := server.DB()
func (c *cluster) Create(cluster *v1Cluster.Cluster, options common.DBOptions) error {
db := c.GetDB(options)
cluster.UUID = uuid.New().String()
cluster.CreateAt = time.Now()
cluster.UpdateAt = time.Now()
return db.Save(cluster)
}
@@ -43,8 +52,8 @@ func (c *cluster) All() ([]v1Cluster.Cluster, error) {
return clusters, nil
}
func (c *cluster) Search(num, size int) ([]v1Cluster.Cluster, int, error) {
db := server.DB()
func (c *cluster) Search(num, size int, conditions pkgV1.Conditions, options common.DBOptions) ([]v1Cluster.Cluster, int, error) {
db := c.GetDB(options)
query := db.Select().Limit(size).Skip((num - 1) * size)
count, err := query.Count(&v1Cluster.Cluster{})
if err != nil {

View File

@@ -0,0 +1,70 @@
package kubernetes
import (
"context"
"errors"
v1Cluster "github.com/KubeOperator/ekko/internal/model/v1/cluster"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/version"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"strings"
)
type Interface interface {
Ping() error
Version() (*version.Info, error)
Client() (*kubernetes.Clientset, error)
}
type Kubernetes struct {
*v1Cluster.Cluster
}
func NewKubernetes(cluster v1Cluster.Cluster) Interface {
return &Kubernetes{Cluster: &cluster}
}
func (k *Kubernetes) Client() (*kubernetes.Clientset, error) {
if k.Spec.Connect.Direction == "forward" {
kubeConf := &rest.Config{
Host: k.Spec.Connect.Forward.ApiServer,
}
if len(k.CaCertificate.CertData) > 0 {
kubeConf.CAData = k.CaCertificate.CertData
} else {
kubeConf.Insecure = true
}
switch k.Spec.Authentication.Mode {
case strings.ToLower("bearer"):
kubeConf.BearerToken = k.Spec.Authentication.BearerToken
case strings.ToLower("certificate"):
kubeConf.TLSClientConfig.CertData = k.Spec.Authentication.Certificate.CertData
kubeConf.TLSClientConfig.KeyData=k.Spec.Authentication.Certificate.KeyData
}
return kubernetes.NewForConfig(kubeConf)
}
return nil, errors.New("")
}
func (k *Kubernetes) Version() (*version.Info, error) {
client, err := k.Client()
if err != nil {
return nil, err
}
return client.ServerVersion()
}
func (k *Kubernetes) Ping() error {
client, err := k.Client()
if err != nil {
return err
}
_, err = client.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
return err
}
return nil
}

View File

@@ -0,0 +1,41 @@
package kubernetes
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/client-go/kubernetes"
"k8s.io/client-go/rest"
"testing"
)
var cert = "-----BEGIN CERTIFICATE-----\nMIIDADCCAeigAwIBAgIBAjANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p\na3ViZUNBMB4XDTIxMDMwODA4MjIyNloXDTIyMDMwOTA4MjIyNlowMTEXMBUGA1UE\nChMOc3lzdGVtOm1hc3RlcnMxFjAUBgNVBAMTDW1pbmlrdWJlLXVzZXIwggEiMA0G\nCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCmp2mjHtThDERXsKjGbTBEr/Do7xGy\nN7nxzyruZykzWERUAVSX6QZgg73ROUdsXFR9Xtg4d7UwoV2xrE0iXOPSnvKjnMtU\n5/buyqGNPXQmrUwfxVmBgABxZEIGRnWU8UXMGKPA3WbkG4P2CKEPz390fNxaD+iC\ntGvjZ9Vvtkt7Tzh/FHUt98+rLZHnD+66EX5I1ketm/QcsNMH3jIAoSIcxoFBuySS\nKWzuLMGb2cuyLJE1rxAoUI79wLnlR7AptJMjCRthZiOnpoGYfqYcIFiJ+bj69OHg\nMwvQRJW2wA9YmNjqXhMQ9u5DP6ynQaSMwc2WduHN12jCdLBiMKS//4PtAgMBAAGj\nPzA9MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUH\nAwIwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAGS6Is5lsXcRwrLEx\nNJszecVEGwWJpqf9I0ZkCGkBNyZe5vCWERAPyhWHXDCLHv2n5sUfPyaBMXoWM6LE\nfkOEj7877HgEREjf35Zqd8cYCD//17/g8VQVqEaCPOmWwJgQpTj2v+v3srQoPbFW\nMVrzZSiYvACwzkvKrUby1kltyKMD9jNA0tL3sj1Dqi04wDPS7YQZaV3zGUZCmCe5\nOu8rD2J53DLBs5rz4op/XQZz3h4xuyYEs681T9itS6ZLRgZRpmuPoS6vIxk9TqLT\nhAjA7pmPzOJaBkzQchIMmiC5MlpCrBozIGDD0arpoC02Qscvde9ZyTa9GOQioQ10\nMbRkCA==\n-----END CERTIFICATE-----"
var key = "-----BEGIN RSA PRIVATE KEY-----\nMIIEowIBAAKCAQEApqdpox7U4QxEV7Coxm0wRK/w6O8Rsje58c8q7mcpM1hEVAFU\nl+kGYIO90TlHbFxUfV7YOHe1MKFdsaxNIlzj0p7yo5zLVOf27sqhjT10Jq1MH8VZ\ngYAAcWRCBkZ1lPFFzBijwN1m5BuD9gihD89/dHzcWg/ogrRr42fVb7ZLe084fxR1\nLffPqy2R5w/uuhF+SNZHrZv0HLDTB94yAKEiHMaBQbskkils7izBm9nLsiyRNa8Q\nKFCO/cC55UewKbSTIwkbYWYjp6aBmH6mHCBYifm4+vTh4DML0ESVtsAPWJjY6l4T\nEPbuQz+sp0GkjMHNlnbhzddownSwYjCkv/+D7QIDAQABAoIBAA36ctDc0CxENCNK\nzQ0/sVHBlCpliw1wwSb4Ini2rG0DFVAagHbxc7h6tFwtOsFrCScu4mHyIH+AuXQi\nqKGeOvm6nU195Ewt3LdwxZYsNmbcGEt96SEElITuTN9r34brqdgRpQKTT4MIj8v+\nM0w5Mk6Z/n2LYgw8h+QeHHfvSWuBOdlNCMqx2XI3gGc4wm3vub3t6pGz+05QPbCD\nd/drVyMVt3yIA2HHZawKZ5WxEk2aEhAg9wqNuVcztxEmnnCTtaxtkg9S6FxeQtuK\nq2WvznCUgBCvYQpzNAOolBhjz2zlyKu7/I4F6+ZwpVPwhfvgayYYLWtkFaNPgLXe\nm9V4w90CgYEAwWcqJgI2QA4LT7XbCEXBkO0mvvWX/pO0kqvHaqVSaSNe+y/fseI0\nq3+A97sZ2MDXnpMl+f0z7q8HZecEHp1NhetbCgHTpgBD7tFaJJCEa1PnFzSGCyFL\n1r5NTHLGB01W16bH0+WMmcZM1mtsx/bctLx/K/BNKmTEpD9Gn0FP97sCgYEA3Jfl\nvbI5DiTMEtewm9C/hqOPeFf18N1Azba1YHC3P4tRVFIOHwJZp1UNsq5gSu4lyYDo\n99Odub0YIkMEZBYPHTtBKZhX90i5XvQm7jmiiKWQ8tirBWrsrljdoxopE5tudCFT\noxBJ94QMakFVcoZqJhmpTscRw3JzpN8XlOH+VHcCgYBu1eaLvbzFXMcSuU97IC7c\nFWydBzZCCPf1DkjMT045PrISFc+Gq/IvTnTkg+8+DtYC5KVg7MC0Ss5ckdYEjXV+\nB/E2fPGEMqa72HJmfgPFVmIbJFilTEGgIZM++o+OY74e/E+MmgLHpaMnRo0i09CM\nK3JeBerTHsiqsDCS1+UyPQKBgQC1t39u0/kCOLfPsdRvlvefTu9qAHO+NlUi4Sbq\nyg96jiayImI1kzcNjBgboF/8ec+w/btsI+vjTO0rlC9yz2Ul/GEChde5AjSKDvBf\nACVvEYylMG05qkpMmTIDIRLDbx//FFEUm9+CwUmE4kska6vXtP3uwjhU29x97bU8\nVSqwowKBgAPjfYKh2xawWw+tPOJ1INalYmjBY/eB0ZE/YT4hD/DAO8Y1+tpM9HX5\nJNtYwOrG/cWMahFAFW82SNZ0d9xairhZ+cOGZ4BQVUviMiY547GQAVwmj+nwqpDT\ngq+oQc1yMo69JpMcpQIKNdXB7HPFLHFdSVgY/cfEbuZhhSW2tHeZ\n-----END RSA PRIVATE KEY-----"
var ca = "-----BEGIN CERTIFICATE-----\nMIIC5zCCAc+gAwIBAgIBATANBgkqhkiG9w0BAQsFADAVMRMwEQYDVQQDEwptaW5p\na3ViZUNBMB4XDTIxMDIxODA2MjYzMFoXDTMxMDIxNzA2MjYzMFowFTETMBEGA1UE\nAxMKbWluaWt1YmVDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMXs\neb3dt6ENhYL61IeVV0vYSKobqjL+TvAPEnXPySv6BjT8ejxz8mM6qP/SVrTJnC0H\nGOtiQgRDCrRlsfzce2DsMlMR2XzgQlao0bF5h0pJdkSdvWnJT0rlnca9Fjt6jVB6\nV+WiceFM5U/LiO8fmdp9R1frefxWfYTaT/80XUo0ZVEgZUBu1QFgAfjexal0CZEi\nBFzRMddW6YEUSrHd46X79pXBz3jXZ228q8XRfkAkyuMZaCAa+poIhVbaItHRgJbd\nUtXi24G/Imt7KddDY8Q3MrlgPeFPcBRy5re4trVCJAK+U9+2Oagbe0eEH7wjB2d7\n9cBMKrUKpRpCP/lMsj8CAwEAAaNCMEAwDgYDVR0PAQH/BAQDAgKkMB0GA1UdJQQW\nMBQGCCsGAQUFBwMCBggrBgEFBQcDATAPBgNVHRMBAf8EBTADAQH/MA0GCSqGSIb3\nDQEBCwUAA4IBAQCH6yn1WbP9zMks6vlxxiqvF3Vg6xMBB+5iZ3mDrBkEYntGpgrC\nBnGnevMWDXPEZsJRUb8rEJChPpk/zB49Kc+FSWJJXTPMvtCqRr4HJoaxxyYlfjfV\nXQsScaDR2LJy9P57VbUeAOaUqN1BiJfK895C8RN1RPgR3tW92eFz5OM/oOF58Km0\nQ6enpBEeNkMpSS0gpd31fs5aL1xqn2zxIVS+cQDJK/4ct18+XSPasCXKNZmyuldJ\n/dZk2DjdR3rqwqp5QbRYBvlKlF+xdvxgM3UwWPfu/bLjo/jdv2L1ThaIPCLuUqQg\n0iBw+d0wj1Zdb6ipJ7QffjCoHtMXMI5VLDwI\n-----END CERTIFICATE-----"
func TestNewKubernetes(t *testing.T) {
//cs, err := clientcmd.BuildConfigFromFlags("", "/Users/shenchenyang/.kube/config")
//if err != nil {
// t.Error(err)
//}
cs := rest.Config{
Host: "https://127.0.0.1:51404",
TLSClientConfig: rest.TLSClientConfig{
CertData: []byte(cert),
KeyData: []byte(key),
Insecure: true,
},
}
cls, err := kubernetes.NewForConfig(&cs)
if err != nil {
t.Error(err)
}
list, err := cls.CoreV1().Namespaces().List(context.TODO(), metav1.ListOptions{})
if err != nil {
t.Error(err)
}
fmt.Println(len(list.Items))
}

View File

@@ -1,25 +0,0 @@
package kubernetes
import (
"context"
"fmt"
metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"testing"
)
func TestNewKubernetesClient(t *testing.T) {
c, err := NewKubernetesClient(&Config{
Host: "",
Token: ""
})
if err != nil {
fmt.Println(err)
}
l, err := c.CoreV1().ServiceAccounts("default").List(context.TODO(), metav1.ListOptions{})
if err != nil {
fmt.Println(err)
}
fmt.Println(l)
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 KiB

View File

@@ -1,74 +0,0 @@
<template>
<layout-content :header="$t('business.cluster.import')" :back-to="{ name: 'ClusterList' }">
<el-row>
<el-col :span="4"><br/></el-col>
<el-col :span="10">
<div class="grid-content bg-purple-light">
<el-form :model="form" label-position="left" :rules="rules" label-width="120px">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="form.name" clearable></el-input>
</el-form-item>
<el-form-item label="apiServer" prop="apiServer">
<el-input v-model="form.apiServer" :placeholder="$t('business.cluster.api_server_help')"
clearable></el-input>
</el-form-item>
<el-form-item label="router" prop="router">
<el-input v-model="form.router" :placeholder="$t('business.cluster.router_help')" clearable></el-input>
</el-form-item>
<el-form-item label="token" prop="token">
<el-input type="textarea" :autosize="{ minRows: 2}" style="width: 100%" v-model="form.token"
clearable></el-input>
</el-form-item>
<el-form-item style="float: right">
<el-button @click="onCancel()">{{ $t("commons.button.cancel") }}</el-button>
<el-button v-loading="loading" @click="onSubmit" type="primary">{{
$t("commons.button.create")
}}
</el-button>
</el-form-item>
</el-form>
</div>
</el-col>
</el-row>
</layout-content>
</template>
<script>
import LayoutContent from "@/components/layout/LayoutContent"
import {create} from "@/api/clusters"
export default {
name: "ClusterImport",
components: { LayoutContent },
data () {
return {
form: {
name: "",
apiServer: "",
router: "",
token: ""
},
rules: {},
loading: false
}
},
methods: {
onCancel () {
this.$router.push({ name: "ClusterList" })
},
onSubmit () {
create(this.form).then(() => {
this.$message({
type: "success",
message: this.$t("创建成功")
})
this.$router.push({ name: "ClusterList" })
})
}
},
}
</script>
<style scoped>
</style>

View File

@@ -1,113 +0,0 @@
<template>
<layout-content :header="$t('business.cluster.list')">
<complex-table :search-config="searchConfig" :selects.sync="selects" :data="data"
:pagination-config="paginationConfig">
<template #header>
<el-button-group>
<el-button type="primary" size="small" @click="onImport">
{{ $t("commons.button.import") }}
</el-button>
<el-button type="primary" size="small" :disabled="selects.length===0" @click="del()">
{{ $t("commons.button.delete") }}
</el-button>
</el-button-group>
</template>
<el-table-column type="selection" fix></el-table-column>
<el-table-column :label="$t('commons.table.name')" min-width="100" prop="name" fix>
<template v-slot:default="{row}">
<el-link href="/dashboard">{{ row.name }}</el-link>
</template>
</el-table-column>
<el-table-column :label="$t('commons.table.status')" min-width="100" prop="name" fix>
<template v-slot:default="{row}">
{{ row.status }}
</template>
</el-table-column>
<fu-table-operations :buttons="buttons" :label="$t('commons.table.action')"/>
</complex-table>
</layout-content>
</template>
<script>
import LayoutContent from "@/components/layout/LayoutContent"
import ComplexTable from "@/components/complex-table"
import {deleteBy, searchCluster} from "@/api/clusters"
export default {
name: "ClusterList",
components: { ComplexTable, LayoutContent },
data () {
return {
buttons: [
{
label: this.$t("commons.button.delete"),
icon: "el-icon-delete",
click: (row) => {
this.del(row.name)
}
},
],
paginationConfig: {
currentPage: 1,
pageSize: 10,
total: 0,
},
searchConfig: {
quickPlaceholder: this.$t("commons.search.quickSearch"),
components: [
{ field: "name", label: this.$t("commons.table.name"), component: "FuComplexInput", defaultOperator: "eq" },
],
},
data: [],
selects: []
}
},
methods: {
onImport () {
this.$router.push({ name: "ClusterImport" })
},
search () {
searchCluster(this.paginationConfig.currentPage, this.paginationConfig.pageSize).then(res => {
this.data = res.data.items
this.paginationConfig.total = res.data.total
})
},
del (name) {
this.$confirm(
this.$t("commons.confirm_message.delete"),
this.$t("commons.message_box.prompt"),
{
confirmButtonText: this.$t("commons.button.confirm"),
cancelButtonText: this.$t("commons.button.cancel"),
type: "warning"
}
).then(() => {
const ps = []
if (name) {
ps.push(deleteBy(name))
} else {
for (const item of this.selects) {
ps.push(deleteBy(item.name))
}
}
Promise.all(ps).then(() => {
this.search()
this.$message({
type: "success",
message: this.$t("commons.msg.delete_success")
})
}).catch(() => {
this.search()
})
})
}
},
created () {
this.search()
}
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,197 @@
<template>
<layout-content :header="$t('business.cluster.import')" :back-to="{ name: 'Clusters' }">
<el-row>
<el-col :span="4"><br/></el-col>
<el-col :span="10">
<div class="grid-content bg-purple-light">
<el-form :model="form" label-position="left" :rules="rules" label-width="120px">
<el-form-item :label="$t('commons.table.name')" prop="name">
<el-input v-model="form.name" clearable></el-input>
</el-form-item>
<el-divider content-position="center">连接设置</el-divider>
<el-form-item label="连接方式">
<el-radio v-model="form.direction" label="forward" @change="onDirectionChange">正向连接
</el-radio>
<el-radio v-model="form.direction" label="backward" @change="onDirectionChange">反向连接
</el-radio>
</el-form-item>
<div v-if="form.direction==='forward'">
<el-form-item label="API Server" prop="apiServer">
<el-input v-model="form.apiServer" clearable></el-input>
</el-form-item>
<el-form-item label="Insecure">
<el-switch v-model="form.apiServerInsecure"></el-switch>
</el-form-item>
<div v-if="!form.apiServerInsecure">
<el-form-item label="Ca Certificate" prop="caDataStr">
<el-input type="textarea" v-model="form.caDataStr" clearable></el-input>
</el-form-item>
</div>
<el-form-item label="配置代理">
<el-switch v-model="form.proxyEnable"></el-switch>
</el-form-item>
<div v-if="form.proxyEnable">
<el-form-item label="Proxy Server" prop="proxyServer">
<el-input v-model="form.proxyServer" clearable></el-input>
</el-form-item>
<el-form-item label="代理认证">
<el-switch v-model="form.proxyAuthEnable"></el-switch>
</el-form-item>
<div v-if="form.proxyAuthEnable">
<el-form-item label="代理用户名" prop="proxyUsername">
<el-input v-model="form.proxyServer" clearable></el-input>
</el-form-item>
<el-form-item label="代理密码" prop="proxyPassword">
<el-input v-model="form.proxyServer" clearable></el-input>
</el-form-item>
</div>
</div>
<el-divider content-position="center">认证设置</el-divider>
<el-form-item label="认证方式">
<el-radio v-model="form.authenticationMode" label="bearer"
@change="onAuthenticationModeChange">Bearer Token
</el-radio>
<el-radio v-model="form.authenticationMode" label="certificate"
@change="onAuthenticationModeChange">证书
</el-radio>
</el-form-item>
<div v-if="form.authenticationMode==='bearer'">
<el-form-item label="Bearer Token" prop="token">
<el-input type="textarea" v-model="form.token" clearable></el-input>
</el-form-item>
</div>
<div v-if="form.authenticationMode==='certificate'">
<el-form-item label="Certificate" prop="certDataStr">
<el-input type="textarea" v-model="form.certDataStr" clearable></el-input>
</el-form-item>
<el-form-item label="Certificate Key" prop="keyDataStr">
<el-input type="textarea" v-model="form.keyDataStr" clearable></el-input>
</el-form-item>
</div>
</div>
<el-form-item>
<div style="float: right">
<el-button @click="onCancel()">{{ $t("commons.button.cancel") }}</el-button>
<el-button type="primary" @click="onConfirm()" :disabled="isSubmitGoing">{{
$t("commons.button.confirm") }}
</el-button>
</div>
</el-form-item>
</el-form>
</div>
</el-col>
</el-row>
</layout-content>
</template>
<script>
import LayoutContent from "@/components/layout/LayoutContent"
import {createCluster} from "@/api/clusters"
export default {
name: "ClusterCreate",
components: {LayoutContent},
data() {
return {
form: {
name: "",
direction: "forward",
apiServer: "",
apiServerInsecure: true,
authenticationMode: "bearer",
token: "",
proxyEnable: false,
proxyServer: "",
proxyAuthEnable: false,
proxyUsername: "",
proxyPassword: "",
certDataStr: "",
keyDataStr: "",
caDataStr: ""
},
rules: {},
loading: false,
isSubmitGoing: false,
}
},
methods: {
onDirectionChange() {
if (this.form.direction === 'forward') {
this.form.apiServer = ""
this.form.authenticationMode = "bearer"
this.form.certificateData = ""
this.form.certificateKeyData = ""
this.form.token = ""
}
},
onAuthenticationModeChange() {
this.form.certificateData = ""
this.form.certificateKeyData = ""
this.form.token = ""
},
onCancel() {
this.$router.push({name: "Clusters"})
},
onConfirm() {
if (this.isSubmitGoing) {
return
}
this.loading = true
const req = {
apiVersion: "v1",
kind: "Cluster",
name: this.form.name,
spec: {
connect: {
direction: this.form.direction
},
authentication: {
mode: this.form.authenticationMode
}
},
keyDataStr: "",
caDataStr: this.form.caDataStr,
certDataStr: ""
}
if (this.form.direction === 'forward') {
const forwardConfig = {}
forwardConfig.apiServer = this.form.apiServer
if (this.form.proxyEnable) {
forwardConfig.proxy.username = this.form.proxyUsername
forwardConfig.proxy.password = this.form.proxyPassword
forwardConfig.proxy.url = this.form.proxyServer
}
req.spec.connect.forward = forwardConfig
}
switch (this.form.authenticationMode) {
case "bearer":
req.spec.authentication.bearerToken = this.form.token
break
case "certificate":
req.certDataStr = this.form.certDataStr
req.keyDataStr = this.form.keyDataStr
break
}
console.log(req)
createCluster(req).then(() => {
this.loading = false
this.isSubmitGoing = false
this.$message({
type: "success",
message: this.$t("commons.msg.create_success")
})
this.$router.push({"name": "Clusters"})
})
}
},
}
</script>
<style scoped>
</style>

View File

@@ -0,0 +1,75 @@
<template xmlns:el-col="http://www.w3.org/1999/html">
<layout-content>
<el-row :gutter="10">
<el-col :span="4" v-for="(item,index) in items" :key="index">
<el-card class="box-card" shadow="hover">
<div v-if="!item.add">
<div slot="header" class="clearfix">
<el-row>
<el-col :span="18" style="font-size: 18px">
{{item.name}}
</el-col>
<el-col :span="6">
<el-button icon="el-icon-user" circle></el-button>
<el-button icon="el-icon-delete" circle></el-button>
</el-col>
</el-row>
</div>
<div>
<el-row>
<el-col :span="4">
<img width="38px" height="38px" src="~@/assets/kubernetes.png" alt="kubernetes.png">
</el-col>
<el-col :span="20">
<table>
<tr>
<th style="width: 60%;text-align: left">版本</th>
<td style="padding-left: 20px">{{item.status.version}}</td>
</tr>
</table>
</el-col>
</el-row>
</div>
</div>
<div v-if="item.add" @click="onCreate">
<i class="el-icon-plus"></i>
</div>
</el-card>
</el-col>
</el-row>
</layout-content>
</template>
<script>
import LayoutContent from "@/components/layout/LayoutContent"
import {listClusters} from "@/api/clusters"
export default {
name: "ClusterList",
components: {LayoutContent},
data() {
return {
items: [],
}
},
methods: {
onCreate() {
this.$router.push({name: "ClusterCreate"})
}
},
created() {
listClusters().then(data => {
this.items = data.data;
this.items.push({
add: true,
})
})
}
}
</script>
<style scoped>
</style>

View File

@@ -19,7 +19,7 @@ const generateRoutes = async (to, from, next) => {
router.addRoute({
path: "/",
component: Layout,
redirect: "/clusterlist",
redirect: "/clusters",
})
router.addRoutes(accessRoutes)
next({...to, replace: true})

View File

@@ -1,36 +0,0 @@
import Layout from "@/business/app-layout/horizontal-layout"
const ClusterList = {
path: "/clusterlist",
sort: 1,
component: Layout,
name: "ClusterList",
meta: {
title: "business.cluster.list",
icon: "iconfont iconkubernets",
global: true
},
children: [
{
path: "/clusterlist",
component: () => import("@/business/cluster-list"),
name: "ClusterList",
meta: {
title: "business.cluster.list",
global: true
}
},
{
path: "/import",
component: () => import("@/business/cluster-list/import"),
hidden: true,
name: "ClusterImport",
meta: {
activeMenu: "/clusterlist",
global: true
}
},
]
}
export default ClusterList

View File

@@ -0,0 +1,34 @@
import Layout from "@/business/app-layout/horizontal-layout"
const Clusters = {
path: "/clusters",
sort: 1,
component: Layout,
name: "ClusterManagement",
meta: {
title: "business.cluster.list",
icon: "iconfont iconkubernets",
},
children: [
{
path: "",
component: () => import("@/business/cluster-management"),
name: "Clusters",
meta: {
title: "business.cluster.list",
activeMenu: "/clusters",
}
},
{
path: "create",
component: () => import("@/business/cluster-management/create"),
hidden: true,
name: "ClusterCreate",
meta: {
activeMenu: "/clusters",
}
},
]
}
export default Clusters