mirror of
https://github.com/1Panel-dev/KubePi.git
synced 2025-10-31 02:26:57 +08:00
feat(terminal): 支持访问pod中的terminal
This commit is contained in:
@@ -14,6 +14,7 @@ import (
|
|||||||
pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1"
|
pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1"
|
||||||
"github.com/KubeOperator/ekko/pkg/certificate"
|
"github.com/KubeOperator/ekko/pkg/certificate"
|
||||||
"github.com/KubeOperator/ekko/pkg/kubernetes"
|
"github.com/KubeOperator/ekko/pkg/kubernetes"
|
||||||
|
"github.com/KubeOperator/ekko/pkg/terminal"
|
||||||
"github.com/asdine/storm/v3"
|
"github.com/asdine/storm/v3"
|
||||||
"github.com/kataras/iris/v12"
|
"github.com/kataras/iris/v12"
|
||||||
"github.com/kataras/iris/v12/context"
|
"github.com/kataras/iris/v12/context"
|
||||||
@@ -375,4 +376,11 @@ func Install(parent iris.Party) {
|
|||||||
sp.Get("/:name/apigroups", handler.ListApiGroups())
|
sp.Get("/:name/apigroups", handler.ListApiGroups())
|
||||||
sp.Get("/:name/apigroups/{group:path}", handler.ListApiGroupResources())
|
sp.Get("/:name/apigroups/{group:path}", handler.ListApiGroupResources())
|
||||||
sp.Get("/:name/namespaces", handler.ListNamespace())
|
sp.Get("/:name/namespaces", handler.ListNamespace())
|
||||||
|
sp.Get("/:name/terminal/session", handler.TerminalSessionHandler())
|
||||||
|
|
||||||
|
wsParty := parent.Party("/ws")
|
||||||
|
h := terminal.CreateAttachHandler("/ws/sockjs")
|
||||||
|
wsParty.Any("/sockjs/{p:path}", func(ctx *context.Context) {
|
||||||
|
h.ServeHTTP(ctx.ResponseWriter(), ctx.Request())
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
54
internal/api/v1/cluster/terminal.go
Normal file
54
internal/api/v1/cluster/terminal.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package cluster
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/KubeOperator/ekko/internal/service/v1/common"
|
||||||
|
"github.com/KubeOperator/ekko/pkg/kubernetes"
|
||||||
|
"github.com/KubeOperator/ekko/pkg/terminal"
|
||||||
|
"github.com/kataras/iris/v12"
|
||||||
|
"github.com/kataras/iris/v12/context"
|
||||||
|
"k8s.io/client-go/tools/remotecommand"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TerminalResponse struct {
|
||||||
|
ID string `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) TerminalSessionHandler() iris.Handler {
|
||||||
|
return func(ctx *context.Context) {
|
||||||
|
namespace := ctx.URLParam("namespace")
|
||||||
|
podName := ctx.URLParam("podName")
|
||||||
|
containerName := ctx.URLParam("containerName")
|
||||||
|
|
||||||
|
sessionID, err := terminal.GenTerminalSessionId()
|
||||||
|
if err != nil {
|
||||||
|
ctx.StatusCode(iris.StatusInternalServerError)
|
||||||
|
ctx.Values().Set("message", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clusterName := ctx.Params().GetString("name")
|
||||||
|
c, err := h.clusterService.Get(clusterName, common.DBOptions{})
|
||||||
|
if err != nil {
|
||||||
|
ctx.StatusCode(iris.StatusInternalServerError)
|
||||||
|
ctx.Values().Set("message", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
k := kubernetes.NewKubernetes(*c)
|
||||||
|
conf := k.Config()
|
||||||
|
client, err := k.Client()
|
||||||
|
if err != nil {
|
||||||
|
ctx.StatusCode(iris.StatusInternalServerError)
|
||||||
|
ctx.Values().Set("message", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
terminal.TerminalSessions.Set(sessionID, terminal.TerminalSession{
|
||||||
|
Id: sessionID,
|
||||||
|
Bound: make(chan error),
|
||||||
|
SizeChan: make(chan remotecommand.TerminalSize),
|
||||||
|
})
|
||||||
|
go terminal.WaitForTerminal(client, conf, namespace, podName, containerName, sessionID)
|
||||||
|
ctx.StatusCode(http.StatusOK)
|
||||||
|
resp := TerminalResponse{ID: sessionID}
|
||||||
|
ctx.Values().Set("data", resp)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -82,7 +82,7 @@ func (e *EkkoSerer) setResultHandler() {
|
|||||||
ss := strings.Split(p, "/")
|
ss := strings.Split(p, "/")
|
||||||
if len(ss) >= 3 {
|
if len(ss) >= 3 {
|
||||||
for i := range ss {
|
for i := range ss {
|
||||||
if ss[i] == "proxy" {
|
if ss[i] == "proxy" || ss[i] == "ws" {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,43 +0,0 @@
|
|||||||
package shell
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/kataras/iris/v12/context"
|
|
||||||
)
|
|
||||||
|
|
||||||
type TerminalResponse struct {
|
|
||||||
ID string `json:"id"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func ExecShellHandler(ctx *context.Context) {
|
|
||||||
//namespace := ctx.URLParam("namespace")
|
|
||||||
//podName := ctx.URLParam("podName")
|
|
||||||
//containerName := ctx.URLParam("containerName")
|
|
||||||
//
|
|
||||||
//sessionID, err := GenTerminalSessionId()
|
|
||||||
//if err != nil {
|
|
||||||
// ctx.StatusCode(http.StatusInternalServerError)
|
|
||||||
// _, _ = ctx.JSON(map[string]interface{}{
|
|
||||||
// "msg": err.Error(),
|
|
||||||
// })
|
|
||||||
//}
|
|
||||||
//ekkoConfig := config.GetConfig()
|
|
||||||
//var cfg *rest.Config
|
|
||||||
//if ekkoConfig.KubeConfig != "" {
|
|
||||||
// cs, err := clientcmd.BuildConfigFromFlags("", ekkoConfig.KubeConfig)
|
|
||||||
// if err != nil {
|
|
||||||
// ctx.StatusCode(http.StatusInternalServerError)
|
|
||||||
// _, _ = ctx.JSON(err.Error())
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
// cfg = cs
|
|
||||||
//}
|
|
||||||
//client, err := kubernetes.NewForConfig(cfg)
|
|
||||||
//terminalSessions.Set(sessionID, TerminalSession{
|
|
||||||
// id: sessionID,
|
|
||||||
// bound: make(chan error),
|
|
||||||
// sizeChan: make(chan remotecommand.TerminalSize),
|
|
||||||
//})
|
|
||||||
//go WaitForTerminal(client, cfg, namespace, podName, containerName, sessionID)
|
|
||||||
//ctx.StatusCode(http.StatusOK)
|
|
||||||
//_, _ = ctx.JSON(TerminalResponse{ID: sessionID})
|
|
||||||
}
|
|
||||||
@@ -20,6 +20,7 @@ import (
|
|||||||
type Interface interface {
|
type Interface interface {
|
||||||
Ping() error
|
Ping() error
|
||||||
Version() (*version.Info, error)
|
Version() (*version.Info, error)
|
||||||
|
Config() *rest.Config
|
||||||
Client() (*kubernetes.Clientset, error)
|
Client() (*kubernetes.Clientset, error)
|
||||||
HasPermission(attributes v1.ResourceAttributes) (PermissionCheckResult, error)
|
HasPermission(attributes v1.ResourceAttributes) (PermissionCheckResult, error)
|
||||||
CreateCommonUser(commonName string) ([]byte, error)
|
CreateCommonUser(commonName string) ([]byte, error)
|
||||||
@@ -292,8 +293,7 @@ func (k *Kubernetes) HasPermission(attributes v1.ResourceAttributes) (Permission
|
|||||||
}, nil
|
}, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
func (k *Kubernetes) Config() *rest.Config {
|
||||||
func (k *Kubernetes) Client() (*kubernetes.Clientset, error) {
|
|
||||||
if k.Spec.Connect.Direction == "forward" {
|
if k.Spec.Connect.Direction == "forward" {
|
||||||
kubeConf := &rest.Config{
|
kubeConf := &rest.Config{
|
||||||
Host: k.Spec.Connect.Forward.ApiServer,
|
Host: k.Spec.Connect.Forward.ApiServer,
|
||||||
@@ -310,10 +310,13 @@ func (k *Kubernetes) Client() (*kubernetes.Clientset, error) {
|
|||||||
kubeConf.TLSClientConfig.CertData = k.Spec.Authentication.Certificate.CertData
|
kubeConf.TLSClientConfig.CertData = k.Spec.Authentication.Certificate.CertData
|
||||||
kubeConf.TLSClientConfig.KeyData = k.Spec.Authentication.Certificate.KeyData
|
kubeConf.TLSClientConfig.KeyData = k.Spec.Authentication.Certificate.KeyData
|
||||||
}
|
}
|
||||||
return kubernetes.NewForConfig(kubeConf)
|
return kubeConf
|
||||||
|
|
||||||
}
|
}
|
||||||
return nil, errors.New("")
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (k *Kubernetes) Client() (*kubernetes.Clientset, error) {
|
||||||
|
return kubernetes.NewForConfig(k.Config())
|
||||||
}
|
}
|
||||||
|
|
||||||
func (k *Kubernetes) Version() (*version.Info, error) {
|
func (k *Kubernetes) Version() (*version.Info, error) {
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
package shell
|
package terminal
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/rand"
|
"crypto/rand"
|
||||||
@@ -29,10 +29,10 @@ type PtyHandler interface {
|
|||||||
|
|
||||||
// TerminalSession implements PtyHandler (using a SockJS connection)
|
// TerminalSession implements PtyHandler (using a SockJS connection)
|
||||||
type TerminalSession struct {
|
type TerminalSession struct {
|
||||||
id string
|
Id string
|
||||||
bound chan error
|
Bound chan error
|
||||||
sockJSSession sockjs.Session
|
sockJSSession sockjs.Session
|
||||||
sizeChan chan remotecommand.TerminalSize
|
SizeChan chan remotecommand.TerminalSize
|
||||||
doneChan chan struct{}
|
doneChan chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,7 +54,7 @@ type TerminalMessage struct {
|
|||||||
// Called in a loop from remotecommand as long as the process is running
|
// Called in a loop from remotecommand as long as the process is running
|
||||||
func (t TerminalSession) Next() *remotecommand.TerminalSize {
|
func (t TerminalSession) Next() *remotecommand.TerminalSize {
|
||||||
select {
|
select {
|
||||||
case size := <-t.sizeChan:
|
case size := <-t.SizeChan:
|
||||||
return &size
|
return &size
|
||||||
case <-t.doneChan:
|
case <-t.doneChan:
|
||||||
return nil
|
return nil
|
||||||
@@ -79,7 +79,7 @@ func (t TerminalSession) Read(p []byte) (int, error) {
|
|||||||
case "stdin":
|
case "stdin":
|
||||||
return copy(p, msg.Data), nil
|
return copy(p, msg.Data), nil
|
||||||
case "resize":
|
case "resize":
|
||||||
t.sizeChan <- remotecommand.TerminalSize{Width: msg.Cols, Height: msg.Rows}
|
t.SizeChan <- remotecommand.TerminalSize{Width: msg.Cols, Height: msg.Rows}
|
||||||
return 0, nil
|
return 0, nil
|
||||||
default:
|
default:
|
||||||
return copy(p, END_OF_TRANSMISSION), fmt.Errorf("unknown message type '%s'", msg.Op)
|
return copy(p, END_OF_TRANSMISSION), fmt.Errorf("unknown message type '%s'", msg.Op)
|
||||||
@@ -154,7 +154,7 @@ func (sm *SessionMap) Close(sessionId string, status uint32, reason string) {
|
|||||||
delete(sm.Sessions, sessionId)
|
delete(sm.Sessions, sessionId)
|
||||||
}
|
}
|
||||||
|
|
||||||
var terminalSessions = SessionMap{Sessions: make(map[string]TerminalSession)}
|
var TerminalSessions = SessionMap{Sessions: make(map[string]TerminalSession)}
|
||||||
|
|
||||||
// handleTerminalSession is Called by net/http for any new /api/sockjs connections
|
// handleTerminalSession is Called by net/http for any new /api/sockjs connections
|
||||||
func handleTerminalSession(session sockjs.Session) {
|
func handleTerminalSession(session sockjs.Session) {
|
||||||
@@ -180,14 +180,14 @@ func handleTerminalSession(session sockjs.Session) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if terminalSession = terminalSessions.Get(msg.SessionID); terminalSession.id == "" {
|
if terminalSession = TerminalSessions.Get(msg.SessionID); terminalSession.Id == "" {
|
||||||
log.Printf("handleTerminalSession: can't find session '%s'", msg.SessionID)
|
log.Printf("handleTerminalSession: can't find session '%s'", msg.SessionID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
terminalSession.sockJSSession = session
|
terminalSession.sockJSSession = session
|
||||||
terminalSessions.Set(msg.SessionID, terminalSession)
|
TerminalSessions.Set(msg.SessionID, terminalSession)
|
||||||
terminalSession.bound <- nil
|
terminalSession.Bound <- nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CreateAttachHandler is called from main for /api/sockjs
|
// CreateAttachHandler is called from main for /api/sockjs
|
||||||
@@ -252,36 +252,36 @@ func isValidShell(validShells []string, shell string) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// WaitForTerminal is called from apihandler.handleAttach as a goroutine
|
// WaitForTerminal is called from apihandler.handleAttach as a goroutine
|
||||||
// Waits for the SockJS connection to be opened by the client the session to be bound in handleTerminalSession
|
// Waits for the SockJS connection to be opened by the client the session to be Bound in handleTerminalSession
|
||||||
func WaitForTerminal(k8sClient kubernetes.Interface, cfg *rest.Config, namespace string, podName string, containerName string, sessionId string) {
|
func WaitForTerminal(k8sClient kubernetes.Interface, cfg *rest.Config, namespace string, podName string, containerName string, sessionId string) {
|
||||||
shell := "sh"
|
shell := "sh"
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-terminalSessions.Get(sessionId).bound:
|
case <-TerminalSessions.Get(sessionId).Bound:
|
||||||
close(terminalSessions.Get(sessionId).bound)
|
close(TerminalSessions.Get(sessionId).Bound)
|
||||||
|
|
||||||
var err error
|
var err error
|
||||||
validShells := []string{"bash", "sh", "powershell", "cmd"}
|
validShells := []string{"bash", "sh", "powershell", "cmd"}
|
||||||
|
|
||||||
if isValidShell(validShells, shell) {
|
if isValidShell(validShells, shell) {
|
||||||
cmd := []string{shell}
|
cmd := []string{shell}
|
||||||
err = startProcess(k8sClient, cfg, cmd, namespace, podName, containerName, terminalSessions.Get(sessionId))
|
err = startProcess(k8sClient, cfg, cmd, namespace, podName, containerName, TerminalSessions.Get(sessionId))
|
||||||
} else {
|
} else {
|
||||||
// No shell given or it was not valid: try some shells until one succeeds or all fail
|
// No shell given or it was not valid: try some shells until one succeeds or all fail
|
||||||
// FIXME: if the first shell fails then the first keyboard event is lost
|
// FIXME: if the first shell fails then the first keyboard event is lost
|
||||||
for _, testShell := range validShells {
|
for _, testShell := range validShells {
|
||||||
cmd := []string{testShell}
|
cmd := []string{testShell}
|
||||||
if err = startProcess(k8sClient, cfg, cmd, namespace, podName, containerName, terminalSessions.Get(sessionId)); err == nil {
|
if err = startProcess(k8sClient, cfg, cmd, namespace, podName, containerName, TerminalSessions.Get(sessionId)); err == nil {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
terminalSessions.Close(sessionId, 2, err.Error())
|
TerminalSessions.Close(sessionId, 2, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
terminalSessions.Close(sessionId, 1, "Process exited")
|
TerminalSessions.Close(sessionId, 1, "Process exited")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
163
web/dashboard/package-lock.json
generated
163
web/dashboard/package-lock.json
generated
@@ -1974,6 +1974,80 @@
|
|||||||
"webpack-chain": "^6.4.0",
|
"webpack-chain": "^6.4.0",
|
||||||
"webpack-dev-server": "^3.11.0",
|
"webpack-dev-server": "^3.11.0",
|
||||||
"webpack-merge": "^4.2.2"
|
"webpack-merge": "^4.2.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"ansi-styles": {
|
||||||
|
"version": "4.3.0",
|
||||||
|
"resolved": "https://registry.nlark.com/ansi-styles/download/ansi-styles-4.3.0.tgz",
|
||||||
|
"integrity": "sha1-7dgDYornHATIWuegkG7a00tkiTc=",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"color-convert": "^2.0.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"chalk": {
|
||||||
|
"version": "4.1.1",
|
||||||
|
"resolved": "https://registry.nlark.com/chalk/download/chalk-4.1.1.tgz",
|
||||||
|
"integrity": "sha1-yAs/qyi/Y3HmhjMl7uZ+YYt35q0=",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"ansi-styles": "^4.1.0",
|
||||||
|
"supports-color": "^7.1.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"color-convert": {
|
||||||
|
"version": "2.0.1",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/color-convert/download/color-convert-2.0.1.tgz",
|
||||||
|
"integrity": "sha1-ctOmjVmMm9s68q0ehPIdiWq9TeM=",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"color-name": "~1.1.4"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"has-flag": {
|
||||||
|
"version": "4.0.0",
|
||||||
|
"resolved": "https://registry.nlark.com/has-flag/download/has-flag-4.0.0.tgz?cache=0&sync_timestamp=1626715907927&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fhas-flag%2Fdownload%2Fhas-flag-4.0.0.tgz",
|
||||||
|
"integrity": "sha1-lEdx/ZyByBJlxNaUGGDaBrtZR5s=",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true
|
||||||
|
},
|
||||||
|
"loader-utils": {
|
||||||
|
"version": "2.0.0",
|
||||||
|
"resolved": "https://registry.npm.taobao.org/loader-utils/download/loader-utils-2.0.0.tgz",
|
||||||
|
"integrity": "sha1-5MrOW4FtQloWa18JfhDNErNgZLA=",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"big.js": "^5.2.2",
|
||||||
|
"emojis-list": "^3.0.0",
|
||||||
|
"json5": "^2.1.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"supports-color": {
|
||||||
|
"version": "7.2.0",
|
||||||
|
"resolved": "https://registry.nlark.com/supports-color/download/supports-color-7.2.0.tgz?cache=0&sync_timestamp=1626703400240&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fsupports-color%2Fdownload%2Fsupports-color-7.2.0.tgz",
|
||||||
|
"integrity": "sha1-G33NyzK4E4gBs+R4umpRyqiWSNo=",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"has-flag": "^4.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"vue-loader-v16": {
|
||||||
|
"version": "npm:vue-loader@16.3.3",
|
||||||
|
"resolved": "https://registry.nlark.com/vue-loader/download/vue-loader-16.3.3.tgz?cache=0&sync_timestamp=1626830452707&other_urls=https%3A%2F%2Fregistry.nlark.com%2Fvue-loader%2Fdownload%2Fvue-loader-16.3.3.tgz",
|
||||||
|
"integrity": "sha1-5EDk6xJ4bhYTi12YthIgjynd9TI=",
|
||||||
|
"dev": true,
|
||||||
|
"optional": true,
|
||||||
|
"requires": {
|
||||||
|
"chalk": "^4.1.0",
|
||||||
|
"hash-sum": "^2.0.0",
|
||||||
|
"loader-utils": "^2.0.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"@vue/cli-shared-utils": {
|
"@vue/cli-shared-utils": {
|
||||||
@@ -13554,80 +13628,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"vue-loader-v16": {
|
|
||||||
"version": "npm:vue-loader@16.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/vue-loader/-/vue-loader-16.3.0.tgz",
|
|
||||||
"integrity": "sha512-UDgni/tUVSdwHuQo+vuBmEgamWx88SuSlEb5fgdvHrlJSPB9qMBRF6W7bfPWSqDns425Gt1wxAUif+f+h/rWjg==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"chalk": "^4.1.0",
|
|
||||||
"hash-sum": "^2.0.0",
|
|
||||||
"loader-utils": "^2.0.0"
|
|
||||||
},
|
|
||||||
"dependencies": {
|
|
||||||
"ansi-styles": {
|
|
||||||
"version": "4.3.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
|
|
||||||
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"color-convert": "^2.0.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"chalk": {
|
|
||||||
"version": "4.1.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.1.tgz",
|
|
||||||
"integrity": "sha512-diHzdDKxcU+bAsUboHLPEDQiw0qEe0qd7SYUn3HgcFlWgbDcfLGswOHYeGrHKzG9z6UYf01d9VFMfZxPM1xZSg==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"ansi-styles": "^4.1.0",
|
|
||||||
"supports-color": "^7.1.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"color-convert": {
|
|
||||||
"version": "2.0.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
|
|
||||||
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"color-name": "~1.1.4"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"has-flag": {
|
|
||||||
"version": "4.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
|
|
||||||
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true
|
|
||||||
},
|
|
||||||
"loader-utils": {
|
|
||||||
"version": "2.0.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.0.tgz",
|
|
||||||
"integrity": "sha512-rP4F0h2RaWSvPEkD7BLDFQnvSf+nK+wr3ESUjNTyAGobqrijmW92zc+SO6d4p4B1wh7+B/Jg1mkQe5NYUEHtHQ==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"big.js": "^5.2.2",
|
|
||||||
"emojis-list": "^3.0.0",
|
|
||||||
"json5": "^2.1.2"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"supports-color": {
|
|
||||||
"version": "7.2.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
|
||||||
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
|
|
||||||
"dev": true,
|
|
||||||
"optional": true,
|
|
||||||
"requires": {
|
|
||||||
"has-flag": "^4.0.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"vue-router": {
|
"vue-router": {
|
||||||
"version": "3.5.2",
|
"version": "3.5.2",
|
||||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.2.tgz",
|
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.2.tgz",
|
||||||
@@ -14403,21 +14403,6 @@
|
|||||||
"integrity": "sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q=",
|
"integrity": "sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q=",
|
||||||
"dev": true
|
"dev": true
|
||||||
},
|
},
|
||||||
"xterm": {
|
|
||||||
"version": "4.13.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/xterm/-/xterm-4.13.0.tgz",
|
|
||||||
"integrity": "sha512-HVW1gdoLOTnkMaqQCr2r3mQy4fX9iSa5gWxKZ2UTYdLa4iqavv7QxJ8n1Ypse32shPVkhTYPLS6vHEFZp5ghzw=="
|
|
||||||
},
|
|
||||||
"xterm-addon-attach": {
|
|
||||||
"version": "0.6.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/xterm-addon-attach/-/xterm-addon-attach-0.6.0.tgz",
|
|
||||||
"integrity": "sha512-Mo8r3HTjI/EZfczVCwRU6jh438B4WLXxdFO86OB7bx0jGhwh2GdF4ifx/rP+OB+Cb2vmLhhVIZ00/7x3YSP3dg=="
|
|
||||||
},
|
|
||||||
"xterm-addon-fit": {
|
|
||||||
"version": "0.5.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/xterm-addon-fit/-/xterm-addon-fit-0.5.0.tgz",
|
|
||||||
"integrity": "sha512-DsS9fqhXHacEmsPxBJZvfj2la30Iz9xk+UKjhQgnYNkrUIN5CYLbw7WEfz117c7+S86S/tpHPfvNxJsF5/G8wQ=="
|
|
||||||
},
|
|
||||||
"y18n": {
|
"y18n": {
|
||||||
"version": "4.0.3",
|
"version": "4.0.3",
|
||||||
"resolved": "https://registry.npm.taobao.org/y18n/download/y18n-4.0.3.tgz?cache=0&sync_timestamp=1617822642544&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fy18n%2Fdownload%2Fy18n-4.0.3.tgz",
|
"resolved": "https://registry.npm.taobao.org/y18n/download/y18n-4.0.3.tgz?cache=0&sync_timestamp=1617822642544&other_urls=https%3A%2F%2Fregistry.npm.taobao.org%2Fy18n%2Fdownload%2Fy18n-4.0.3.tgz",
|
||||||
|
|||||||
@@ -54,6 +54,12 @@
|
|||||||
<div class="content unicode" style="display: block;">
|
<div class="content unicode" style="display: block;">
|
||||||
<ul class="icon_lists dib-box">
|
<ul class="icon_lists dib-box">
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont"></span>
|
||||||
|
<div class="name">command-line</div>
|
||||||
|
<div class="code-name">&#xe665;</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dib">
|
<li class="dib">
|
||||||
<span class="icon iconfont"></span>
|
<span class="icon iconfont"></span>
|
||||||
<div class="name">网络</div>
|
<div class="name">网络</div>
|
||||||
@@ -468,9 +474,9 @@
|
|||||||
<pre><code class="language-css"
|
<pre><code class="language-css"
|
||||||
>@font-face {
|
>@font-face {
|
||||||
font-family: 'iconfont';
|
font-family: 'iconfont';
|
||||||
src: url('iconfont.woff2?t=1621844667800') format('woff2'),
|
src: url('iconfont.woff2?t=1627546170843') format('woff2'),
|
||||||
url('iconfont.woff?t=1621844667800') format('woff'),
|
url('iconfont.woff?t=1627546170843') format('woff'),
|
||||||
url('iconfont.ttf?t=1621844667800') format('truetype');
|
url('iconfont.ttf?t=1627546170843') format('truetype');
|
||||||
}
|
}
|
||||||
</code></pre>
|
</code></pre>
|
||||||
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
<h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3>
|
||||||
@@ -496,6 +502,15 @@
|
|||||||
<div class="content font-class">
|
<div class="content font-class">
|
||||||
<ul class="icon_lists dib-box">
|
<ul class="icon_lists dib-box">
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<span class="icon iconfont iconcommand-line"></span>
|
||||||
|
<div class="name">
|
||||||
|
command-line
|
||||||
|
</div>
|
||||||
|
<div class="code-name">.iconcommand-line
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dib">
|
<li class="dib">
|
||||||
<span class="icon iconfont iconnetwork"></span>
|
<span class="icon iconfont iconnetwork"></span>
|
||||||
<div class="name">
|
<div class="name">
|
||||||
@@ -1117,6 +1132,14 @@
|
|||||||
<div class="content symbol">
|
<div class="content symbol">
|
||||||
<ul class="icon_lists dib-box">
|
<ul class="icon_lists dib-box">
|
||||||
|
|
||||||
|
<li class="dib">
|
||||||
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
|
<use xlink:href="#iconcommand-line"></use>
|
||||||
|
</svg>
|
||||||
|
<div class="name">command-line</div>
|
||||||
|
<div class="code-name">#iconcommand-line</div>
|
||||||
|
</li>
|
||||||
|
|
||||||
<li class="dib">
|
<li class="dib">
|
||||||
<svg class="icon svg-icon" aria-hidden="true">
|
<svg class="icon svg-icon" aria-hidden="true">
|
||||||
<use xlink:href="#iconnetwork"></use>
|
<use xlink:href="#iconnetwork"></use>
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
@font-face {
|
@font-face {
|
||||||
font-family: "iconfont"; /* Project id 2474164 */
|
font-family: "iconfont"; /* Project id 2474164 */
|
||||||
src: url('iconfont.woff2?t=1621844667800') format('woff2'),
|
src: url('iconfont.woff2?t=1627546170843') format('woff2'),
|
||||||
url('iconfont.woff?t=1621844667800') format('woff'),
|
url('iconfont.woff?t=1627546170843') format('woff'),
|
||||||
url('iconfont.ttf?t=1621844667800') format('truetype');
|
url('iconfont.ttf?t=1627546170843') format('truetype');
|
||||||
}
|
}
|
||||||
|
|
||||||
.iconfont {
|
.iconfont {
|
||||||
@@ -13,6 +13,10 @@
|
|||||||
-moz-osx-font-smoothing: grayscale;
|
-moz-osx-font-smoothing: grayscale;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.iconcommand-line:before {
|
||||||
|
content: "\e665";
|
||||||
|
}
|
||||||
|
|
||||||
.iconnetwork:before {
|
.iconnetwork:before {
|
||||||
content: "\e6d9";
|
content: "\e6d9";
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -5,6 +5,13 @@
|
|||||||
"css_prefix_text": "icon",
|
"css_prefix_text": "icon",
|
||||||
"description": "KubeOperator UI",
|
"description": "KubeOperator UI",
|
||||||
"glyphs": [
|
"glyphs": [
|
||||||
|
{
|
||||||
|
"icon_id": "10330790",
|
||||||
|
"name": "command-line",
|
||||||
|
"font_class": "command-line",
|
||||||
|
"unicode": "e665",
|
||||||
|
"unicode_decimal": 58981
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"icon_id": "14560260",
|
"icon_id": "14560260",
|
||||||
"name": "网络",
|
"name": "网络",
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -90,6 +90,7 @@
|
|||||||
<span v-if="!row.started">-</span>
|
<span v-if="!row.started">-</span>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<ko-table-operations :buttons="buttons" :label="$t('commons.table.action')"></ko-table-operations>
|
||||||
</complex-table>
|
</complex-table>
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
<el-tab-pane label="Conditions" name="Conditions">
|
<el-tab-pane label="Conditions" name="Conditions">
|
||||||
@@ -123,16 +124,30 @@
|
|||||||
<script>
|
<script>
|
||||||
import LayoutContent from "@/components/layout/LayoutContent"
|
import LayoutContent from "@/components/layout/LayoutContent"
|
||||||
import {getPodByName} from "@/api/pods"
|
import {getPodByName} from "@/api/pods"
|
||||||
// import { listPodsWithNsSelector } from "@/api/pods"
|
|
||||||
import YamlEditor from "@/components/yaml-editor"
|
import YamlEditor from "@/components/yaml-editor"
|
||||||
|
|
||||||
import ComplexTable from "@/components/complex-table"
|
import ComplexTable from "@/components/complex-table"
|
||||||
|
import KoTableOperations from "@/components/ko-table-operations";
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: "PodDetail",
|
name: "PodDetail",
|
||||||
components: { LayoutContent, ComplexTable, YamlEditor },
|
components: {LayoutContent, ComplexTable, YamlEditor, KoTableOperations},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
buttons: [
|
||||||
|
{
|
||||||
|
label: this.$t("commons.button.open_shell"),
|
||||||
|
icon: "iconfont iconcommand-line",
|
||||||
|
click: (row) => {
|
||||||
|
const namespace = this.form.metadata.namespace
|
||||||
|
const podName = this.form.metadata.name
|
||||||
|
const containerName = row.name
|
||||||
|
const clusterName = this.$route.query["cluster"]
|
||||||
|
const terminalUrl = `/terminal/app?cluster=${clusterName}&pod=${podName}&namespace=${namespace}&container=${containerName}`
|
||||||
|
window.open(terminalUrl,"_blank")
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
form: {
|
form: {
|
||||||
metadata: {},
|
metadata: {},
|
||||||
spec: {
|
spec: {
|
||||||
|
|||||||
@@ -29,6 +29,7 @@ const message = {
|
|||||||
view_form: "查看表单",
|
view_form: "查看表单",
|
||||||
view_yaml: "查看 YAML",
|
view_yaml: "查看 YAML",
|
||||||
download_yaml: "下载 YAML",
|
download_yaml: "下载 YAML",
|
||||||
|
open_shell:"打开 SHELL",
|
||||||
back_detail: "返回详情",
|
back_detail: "返回详情",
|
||||||
submit: "提交",
|
submit: "提交",
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -17,15 +17,19 @@ module.exports = {
|
|||||||
proxy: {
|
proxy: {
|
||||||
'/proxy': {
|
'/proxy': {
|
||||||
target: 'http://0.0.0.0:2019',
|
target: 'http://0.0.0.0:2019',
|
||||||
ws: true,
|
|
||||||
secure: false,
|
|
||||||
},
|
},
|
||||||
'/api': {
|
'/api': {
|
||||||
target: 'http://0.0.0.0:2019',
|
target: 'http://0.0.0.0:2019',
|
||||||
|
ws: true,
|
||||||
|
secure: false,
|
||||||
},
|
},
|
||||||
'/dashboard': {
|
'/dashboard': {
|
||||||
target: 'http://0.0.0.0:4400',
|
target: 'http://0.0.0.0:4400',
|
||||||
|
},
|
||||||
|
'/terminal': {
|
||||||
|
target: 'http://0.0.0.0:4200',
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
configureWebpack: {
|
configureWebpack: {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"ng": "ng",
|
"ng": "ng",
|
||||||
"start": "ng serve --proxy-config proxy.config.json",
|
"start": "ng serve --proxy-config proxy.config.json --base-href=/terminal/",
|
||||||
"build": "ng build --prod --aot --base-href=/terminal/",
|
"build": "ng build --prod --aot --base-href=/terminal/",
|
||||||
"watch": "ng build --watch --configuration development",
|
"watch": "ng build --watch --configuration development",
|
||||||
"test": "ng test"
|
"test": "ng test"
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
"target": "http://localhost:2019",
|
"target": "http://localhost:2019",
|
||||||
"secure": true
|
"secure": true
|
||||||
},
|
},
|
||||||
"/api/ws": {
|
"/api/clusters/ws": {
|
||||||
"target": "http://localhost:2019",
|
"target": "http://localhost:2019",
|
||||||
"changeOrigin": true,
|
"changeOrigin": true,
|
||||||
"ws": true
|
"ws": true
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {routes} from "./app.routing";
|
|||||||
BrowserModule,
|
BrowserModule,
|
||||||
HttpClientModule,
|
HttpClientModule,
|
||||||
RouterModule.forRoot(routes, {
|
RouterModule.forRoot(routes, {
|
||||||
useHash: true,
|
useHash: false,
|
||||||
onSameUrlNavigation: 'reload'
|
onSameUrlNavigation: 'reload'
|
||||||
})
|
})
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ export const routes: Routes = [
|
|||||||
{
|
{
|
||||||
path: '', component: AppComponent, children: [
|
path: '', component: AppComponent, children: [
|
||||||
{path: '', redirectTo: 'app', pathMatch: 'full'},
|
{path: '', redirectTo: 'app', pathMatch: 'full'},
|
||||||
{path: 'app', component: TerminalComponent}
|
{path: 'app', component: TerminalComponent},
|
||||||
|
{path: '*', redirectTo: '', pathMatch: 'full'},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export class TerminalComponent implements AfterViewInit {
|
|||||||
container: string;
|
container: string;
|
||||||
|
|
||||||
private readonly namespace_: string
|
private readonly namespace_: string
|
||||||
|
clusterName: string;
|
||||||
private connecting_: boolean
|
private connecting_: boolean
|
||||||
private connectionClosed_: boolean
|
private connectionClosed_: boolean
|
||||||
private conn_: WebSocket
|
private conn_: WebSocket
|
||||||
@@ -41,6 +42,7 @@ export class TerminalComponent implements AfterViewInit {
|
|||||||
private _router: Router,
|
private _router: Router,
|
||||||
private terminalService: TerminalService
|
private terminalService: TerminalService
|
||||||
) {
|
) {
|
||||||
|
this.clusterName = this.activatedRoute_.snapshot.queryParams["cluster"]
|
||||||
this.namespace_ = this.activatedRoute_.snapshot.queryParams["namespace"]
|
this.namespace_ = this.activatedRoute_.snapshot.queryParams["namespace"]
|
||||||
this.podName = this.activatedRoute_.snapshot.queryParams["pod"]
|
this.podName = this.activatedRoute_.snapshot.queryParams["pod"]
|
||||||
this.container = this.activatedRoute_.snapshot.queryParams["container"]
|
this.container = this.activatedRoute_.snapshot.queryParams["container"]
|
||||||
@@ -161,14 +163,18 @@ export class TerminalComponent implements AfterViewInit {
|
|||||||
this.connecting_ = true;
|
this.connecting_ = true;
|
||||||
this.connectionClosed_ = false;
|
this.connectionClosed_ = false;
|
||||||
|
|
||||||
const {id} = await this.terminalService.createTerminalSession(this.namespace_, this.podName, this.container).toPromise()
|
try {
|
||||||
|
const {data} = await this.terminalService.createTerminalSession(this.clusterName, this.namespace_, this.podName, this.container).toPromise()
|
||||||
this.conn_ = new SockJS(`/api/ws/sockjs?${id}`);
|
const id = data.id
|
||||||
|
this.conn_ = new SockJS(`/api/v1/ws/sockjs?${id}`);
|
||||||
this.conn_.onopen = this.onConnectionOpen.bind(this, id);
|
this.conn_.onopen = this.onConnectionOpen.bind(this, id);
|
||||||
this.conn_.onmessage = this.onConnectionMessage.bind(this);
|
this.conn_.onmessage = this.onConnectionMessage.bind(this);
|
||||||
this.conn_.onclose = this.onConnectionClose.bind(this);
|
this.conn_.onclose = this.onConnectionClose.bind(this);
|
||||||
|
|
||||||
this.cdr_.markForCheck();
|
this.cdr_.markForCheck();
|
||||||
|
} catch (e) {
|
||||||
|
alert(e.error.message)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private handleConnectionMessage(frame: ShellFrame): void {
|
private handleConnectionMessage(frame: ShellFrame): void {
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ export class TerminalService {
|
|||||||
constructor(private http: HttpClient) {
|
constructor(private http: HttpClient) {
|
||||||
}
|
}
|
||||||
|
|
||||||
createTerminalSession(namespace: string, podName: string, containerName: string): Observable<any> {
|
createTerminalSession(clusterName: string, namespace: string, podName: string, containerName: string): Observable<any> {
|
||||||
const url = function () {
|
const url = function () {
|
||||||
let baseUrl = `/api/terminal/session?podName=${podName}&&containerName=${containerName}`
|
let baseUrl = `/api/v1/clusters/${clusterName}/terminal/session?podName=${podName}&&containerName=${containerName}`
|
||||||
if (namespace) {
|
if (namespace) {
|
||||||
baseUrl = `${baseUrl}&&namespace=${namespace}`
|
baseUrl = `${baseUrl}&&namespace=${namespace}`
|
||||||
}
|
}
|
||||||
@@ -21,16 +21,4 @@ export class TerminalService {
|
|||||||
}()
|
}()
|
||||||
return this.http.get<any>(url)
|
return this.http.get<any>(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
readPod(podName: string, namespace?: string): Observable<Pod> {
|
|
||||||
const url = function () {
|
|
||||||
let baseUrl = '/api/proxy/api/v1'
|
|
||||||
if (namespace) {
|
|
||||||
baseUrl = `${baseUrl}/namespace/${namespace}`
|
|
||||||
}
|
|
||||||
baseUrl = `${baseUrl}/pods/${podName}`
|
|
||||||
return baseUrl
|
|
||||||
}()
|
|
||||||
return this.http.get<Pod>(url)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,11 +0,0 @@
|
|||||||
<html>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
</body>
|
|
||||||
<script>
|
|
||||||
const ws = new WebSocket("ws://localhost:8081/echo")
|
|
||||||
ws.onopen = (res) => {
|
|
||||||
ws.send(res)
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
</html>
|
|
||||||
Reference in New Issue
Block a user