mirror of
				https://github.com/1Panel-dev/KubePi.git
				synced 2025-11-01 02:52:40 +08:00 
			
		
		
		
	feat(terminal): 支持访问pod中的terminal
This commit is contained in:
		| @@ -14,6 +14,7 @@ import ( | ||||
| 	pkgV1 "github.com/KubeOperator/ekko/pkg/api/v1" | ||||
| 	"github.com/KubeOperator/ekko/pkg/certificate" | ||||
| 	"github.com/KubeOperator/ekko/pkg/kubernetes" | ||||
| 	"github.com/KubeOperator/ekko/pkg/terminal" | ||||
| 	"github.com/asdine/storm/v3" | ||||
| 	"github.com/kataras/iris/v12" | ||||
| 	"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/{group:path}", handler.ListApiGroupResources()) | ||||
| 	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, "/") | ||||
| 			if len(ss) >= 3 { | ||||
| 				for i := range ss { | ||||
| 					if ss[i] == "proxy" { | ||||
| 					if ss[i] == "proxy" || ss[i] == "ws" { | ||||
| 						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 { | ||||
| 	Ping() error | ||||
| 	Version() (*version.Info, error) | ||||
| 	Config() *rest.Config | ||||
| 	Client() (*kubernetes.Clientset, error) | ||||
| 	HasPermission(attributes v1.ResourceAttributes) (PermissionCheckResult, error) | ||||
| 	CreateCommonUser(commonName string) ([]byte, error) | ||||
| @@ -292,8 +293,7 @@ func (k *Kubernetes) HasPermission(attributes v1.ResourceAttributes) (Permission | ||||
| 	}, nil | ||||
|  | ||||
| } | ||||
|  | ||||
| func (k *Kubernetes) Client() (*kubernetes.Clientset, error) { | ||||
| func (k *Kubernetes) Config() *rest.Config { | ||||
| 	if k.Spec.Connect.Direction == "forward" { | ||||
| 		kubeConf := &rest.Config{ | ||||
| 			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.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) { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| package shell | ||||
| package terminal | ||||
| 
 | ||||
| import ( | ||||
| 	"crypto/rand" | ||||
| @@ -29,10 +29,10 @@ type PtyHandler interface { | ||||
| 
 | ||||
| // TerminalSession implements PtyHandler (using a SockJS connection) | ||||
| type TerminalSession struct { | ||||
| 	id            string | ||||
| 	bound         chan error | ||||
| 	Id            string | ||||
| 	Bound         chan error | ||||
| 	sockJSSession sockjs.Session | ||||
| 	sizeChan      chan remotecommand.TerminalSize | ||||
| 	SizeChan      chan remotecommand.TerminalSize | ||||
| 	doneChan      chan struct{} | ||||
| } | ||||
| 
 | ||||
| @@ -54,7 +54,7 @@ type TerminalMessage struct { | ||||
| // Called in a loop from remotecommand as long as the process is running | ||||
| func (t TerminalSession) Next() *remotecommand.TerminalSize { | ||||
| 	select { | ||||
| 	case size := <-t.sizeChan: | ||||
| 	case size := <-t.SizeChan: | ||||
| 		return &size | ||||
| 	case <-t.doneChan: | ||||
| 		return nil | ||||
| @@ -79,7 +79,7 @@ func (t TerminalSession) Read(p []byte) (int, error) { | ||||
| 	case "stdin": | ||||
| 		return copy(p, msg.Data), nil | ||||
| 	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 | ||||
| 	default: | ||||
| 		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) | ||||
| } | ||||
| 
 | ||||
| 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 | ||||
| func handleTerminalSession(session sockjs.Session) { | ||||
| @@ -180,14 +180,14 @@ func handleTerminalSession(session sockjs.Session) { | ||||
| 		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) | ||||
| 		return | ||||
| 	} | ||||
| 
 | ||||
| 	terminalSession.sockJSSession = session | ||||
| 	terminalSessions.Set(msg.SessionID, terminalSession) | ||||
| 	terminalSession.bound <- nil | ||||
| 	TerminalSessions.Set(msg.SessionID, terminalSession) | ||||
| 	terminalSession.Bound <- nil | ||||
| } | ||||
| 
 | ||||
| // 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 | ||||
| // 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) { | ||||
| 	shell := "sh" | ||||
| 
 | ||||
| 	select { | ||||
| 	case <-terminalSessions.Get(sessionId).bound: | ||||
| 		close(terminalSessions.Get(sessionId).bound) | ||||
| 	case <-TerminalSessions.Get(sessionId).Bound: | ||||
| 		close(TerminalSessions.Get(sessionId).Bound) | ||||
| 
 | ||||
| 		var err error | ||||
| 		validShells := []string{"bash", "sh", "powershell", "cmd"} | ||||
| 
 | ||||
| 		if isValidShell(validShells, 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 { | ||||
| 			// 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 | ||||
| 			for _, testShell := range validShells { | ||||
| 				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 | ||||
| 				} | ||||
| 			} | ||||
| 		} | ||||
| 
 | ||||
| 		if err != nil { | ||||
| 			terminalSessions.Close(sessionId, 2, err.Error()) | ||||
| 			TerminalSessions.Close(sessionId, 2, err.Error()) | ||||
| 			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-dev-server": "^3.11.0", | ||||
|         "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": { | ||||
| @@ -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": { | ||||
|       "version": "3.5.2", | ||||
|       "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-3.5.2.tgz", | ||||
| @@ -14403,21 +14403,6 @@ | ||||
|       "integrity": "sha1-u3J3n1+kZRhrH0OPZ0+jR/2121Q=", | ||||
|       "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": { | ||||
|       "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", | ||||
|   | ||||
| @@ -54,6 +54,12 @@ | ||||
|       <div class="content unicode" style="display: block;"> | ||||
|           <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"> | ||||
|               <span class="icon iconfont"></span> | ||||
|                 <div class="name">网络</div> | ||||
| @@ -468,9 +474,9 @@ | ||||
| <pre><code class="language-css" | ||||
| >@font-face { | ||||
|   font-family: 'iconfont'; | ||||
|   src: url('iconfont.woff2?t=1621844667800') format('woff2'), | ||||
|        url('iconfont.woff?t=1621844667800') format('woff'), | ||||
|        url('iconfont.ttf?t=1621844667800') format('truetype'); | ||||
|   src: url('iconfont.woff2?t=1627546170843') format('woff2'), | ||||
|        url('iconfont.woff?t=1627546170843') format('woff'), | ||||
|        url('iconfont.ttf?t=1627546170843') format('truetype'); | ||||
| } | ||||
| </code></pre> | ||||
|           <h3 id="-iconfont-">第二步:定义使用 iconfont 的样式</h3> | ||||
| @@ -496,6 +502,15 @@ | ||||
|       <div class="content font-class"> | ||||
|         <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"> | ||||
|             <span class="icon iconfont iconnetwork"></span> | ||||
|             <div class="name"> | ||||
| @@ -1117,6 +1132,14 @@ | ||||
|       <div class="content symbol"> | ||||
|           <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"> | ||||
|                 <svg class="icon svg-icon" aria-hidden="true"> | ||||
|                   <use xlink:href="#iconnetwork"></use> | ||||
|   | ||||
| @@ -1,8 +1,8 @@ | ||||
| @font-face { | ||||
|   font-family: "iconfont"; /* Project id 2474164 */ | ||||
|   src: url('iconfont.woff2?t=1621844667800') format('woff2'), | ||||
|        url('iconfont.woff?t=1621844667800') format('woff'), | ||||
|        url('iconfont.ttf?t=1621844667800') format('truetype'); | ||||
|   src: url('iconfont.woff2?t=1627546170843') format('woff2'), | ||||
|        url('iconfont.woff?t=1627546170843') format('woff'), | ||||
|        url('iconfont.ttf?t=1627546170843') format('truetype'); | ||||
| } | ||||
|  | ||||
| .iconfont { | ||||
| @@ -13,6 +13,10 @@ | ||||
|   -moz-osx-font-smoothing: grayscale; | ||||
| } | ||||
|  | ||||
| .iconcommand-line:before { | ||||
|   content: "\e665"; | ||||
| } | ||||
|  | ||||
| .iconnetwork:before { | ||||
|   content: "\e6d9"; | ||||
| } | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -5,6 +5,13 @@ | ||||
|   "css_prefix_text": "icon", | ||||
|   "description": "KubeOperator UI", | ||||
|   "glyphs": [ | ||||
|     { | ||||
|       "icon_id": "10330790", | ||||
|       "name": "command-line", | ||||
|       "font_class": "command-line", | ||||
|       "unicode": "e665", | ||||
|       "unicode_decimal": 58981 | ||||
|     }, | ||||
|     { | ||||
|       "icon_id": "14560260", | ||||
|       "name": "网络", | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							
										
											Binary file not shown.
										
									
								
							| @@ -69,11 +69,11 @@ | ||||
|             </el-table-column> | ||||
|             <el-table-column sortable :label="$t('business.pod.ready')" prop="ready" min-width="40"> | ||||
|               <template v-slot:default="{row}"> | ||||
|                 <i class="el-icon-check" v-if="row.ready" /> | ||||
|                 <i class="el-icon-close" v-if="!row.ready" /> | ||||
|                 <i class="el-icon-check" v-if="row.ready"/> | ||||
|                 <i class="el-icon-close" v-if="!row.ready"/> | ||||
|               </template> | ||||
|             </el-table-column> | ||||
|             <el-table-column sortable :label="$t('commons.table.name')" prop="name" min-width="50" /> | ||||
|             <el-table-column sortable :label="$t('commons.table.name')" prop="name" min-width="50"/> | ||||
|             <el-table-column sortable :label="$t('business.pod.image')" min-width="170"> | ||||
|               <template v-slot:default="{row}"> | ||||
|                 <div class="myTag"> | ||||
| @@ -83,19 +83,20 @@ | ||||
|                 </div> | ||||
|               </template> | ||||
|             </el-table-column> | ||||
|             <el-table-column sortable :label="$t('business.workload.restarts')" prop="restartCount" min-width="30" /> | ||||
|             <el-table-column sortable :label="$t('business.workload.restarts')" prop="restartCount" min-width="30"/> | ||||
|             <el-table-column sortable :label="$t('commons.table.created_time')" min-width="70"> | ||||
|               <template v-slot:default="{row}"> | ||||
|                 <span v-if="row.started">{{ row.state.running.startedAt | age }}</span> | ||||
|                 <span v-if="!row.started">-</span> | ||||
|               </template> | ||||
|             </el-table-column> | ||||
|             <ko-table-operations :buttons="buttons" :label="$t('commons.table.action')"></ko-table-operations> | ||||
|           </complex-table> | ||||
|         </el-tab-pane> | ||||
|         <el-tab-pane label="Conditions" name="Conditions"> | ||||
|           <complex-table :data="form.status.conditions"> | ||||
|             <el-table-column sortable label="Condition" prop="type" /> | ||||
|             <el-table-column sortable :label="$t('commons.table.status')" prop="status" /> | ||||
|             <el-table-column sortable label="Condition" prop="type"/> | ||||
|             <el-table-column sortable :label="$t('commons.table.status')" prop="status"/> | ||||
|             <el-table-column sortable :label="$t('commons.table.lastUpdateTime')" prop="lastUpdateTime"> | ||||
|               <template v-slot:default="{row}"> | ||||
|                 {{ row.lastTransitionTime | age }} | ||||
| @@ -122,17 +123,31 @@ | ||||
|  | ||||
| <script> | ||||
| import LayoutContent from "@/components/layout/LayoutContent" | ||||
| import { getPodByName } from "@/api/pods" | ||||
| // import { listPodsWithNsSelector } from "@/api/pods" | ||||
| import {getPodByName} from "@/api/pods" | ||||
| import YamlEditor from "@/components/yaml-editor" | ||||
|  | ||||
| import ComplexTable from "@/components/complex-table" | ||||
| import KoTableOperations from "@/components/ko-table-operations"; | ||||
|  | ||||
| export default { | ||||
|   name: "PodDetail", | ||||
|   components: { LayoutContent, ComplexTable, YamlEditor }, | ||||
|   components: {LayoutContent, ComplexTable, YamlEditor, KoTableOperations}, | ||||
|   data() { | ||||
|     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: { | ||||
|         metadata: {}, | ||||
|         spec: { | ||||
|   | ||||
| @@ -29,6 +29,7 @@ const message = { | ||||
|       view_form: "查看表单", | ||||
|       view_yaml: "查看 YAML", | ||||
|       download_yaml: "下载 YAML", | ||||
|       open_shell:"打开 SHELL", | ||||
|       back_detail: "返回详情", | ||||
|       submit: "提交", | ||||
|     }, | ||||
|   | ||||
| @@ -17,15 +17,19 @@ module.exports = { | ||||
|         proxy: { | ||||
|             '/proxy': { | ||||
|                 target: 'http://0.0.0.0:2019', | ||||
|                 ws: true, | ||||
|                 secure: false, | ||||
|             }, | ||||
|             '/api': { | ||||
|                 target: 'http://0.0.0.0:2019', | ||||
|                 ws: true, | ||||
|                 secure: false, | ||||
|             }, | ||||
|             '/dashboard': { | ||||
|                 target: 'http://0.0.0.0:4400', | ||||
|             }, | ||||
|             '/terminal': { | ||||
|                 target: 'http://0.0.0.0:4200', | ||||
|             } | ||||
|  | ||||
|         } | ||||
|     }, | ||||
|     configureWebpack: { | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|   "version": "0.0.0", | ||||
|   "scripts": { | ||||
|     "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/", | ||||
|     "watch": "ng build --watch --configuration development", | ||||
|     "test": "ng test" | ||||
|   | ||||
| @@ -3,7 +3,7 @@ | ||||
|     "target": "http://localhost:2019", | ||||
|     "secure": true | ||||
|   }, | ||||
|   "/api/ws": { | ||||
|   "/api/clusters/ws": { | ||||
|     "target": "http://localhost:2019", | ||||
|     "changeOrigin": true, | ||||
|     "ws": true | ||||
|   | ||||
| @@ -16,7 +16,7 @@ import {routes} from "./app.routing"; | ||||
|     BrowserModule, | ||||
|     HttpClientModule, | ||||
|     RouterModule.forRoot(routes, { | ||||
|       useHash: true, | ||||
|       useHash: false, | ||||
|       onSameUrlNavigation: 'reload' | ||||
|     }) | ||||
|   ], | ||||
|   | ||||
| @@ -7,7 +7,8 @@ export const routes: Routes = [ | ||||
|   { | ||||
|     path: '', component: AppComponent, children: [ | ||||
|       {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; | ||||
|  | ||||
|   private readonly namespace_: string | ||||
|   clusterName: string; | ||||
|   private connecting_: boolean | ||||
|   private connectionClosed_: boolean | ||||
|   private conn_: WebSocket | ||||
| @@ -41,6 +42,7 @@ export class TerminalComponent implements AfterViewInit { | ||||
|               private _router: Router, | ||||
|               private terminalService: TerminalService | ||||
|   ) { | ||||
|     this.clusterName = this.activatedRoute_.snapshot.queryParams["cluster"] | ||||
|     this.namespace_ = this.activatedRoute_.snapshot.queryParams["namespace"] | ||||
|     this.podName = this.activatedRoute_.snapshot.queryParams["pod"] | ||||
|     this.container = this.activatedRoute_.snapshot.queryParams["container"] | ||||
| @@ -161,14 +163,18 @@ export class TerminalComponent implements AfterViewInit { | ||||
|     this.connecting_ = true; | ||||
|     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() | ||||
|       const id = data.id | ||||
|       this.conn_ = new SockJS(`/api/v1/ws/sockjs?${id}`); | ||||
|       this.conn_.onopen = this.onConnectionOpen.bind(this, id); | ||||
|       this.conn_.onmessage = this.onConnectionMessage.bind(this); | ||||
|       this.conn_.onclose = this.onConnectionClose.bind(this); | ||||
|  | ||||
|     this.conn_ = new SockJS(`/api/ws/sockjs?${id}`); | ||||
|     this.conn_.onopen = this.onConnectionOpen.bind(this, id); | ||||
|     this.conn_.onmessage = this.onConnectionMessage.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 { | ||||
|   | ||||
| @@ -11,9 +11,9 @@ export class TerminalService { | ||||
|   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 () { | ||||
|       let baseUrl = `/api/terminal/session?podName=${podName}&&containerName=${containerName}` | ||||
|       let baseUrl = `/api/v1/clusters/${clusterName}/terminal/session?podName=${podName}&&containerName=${containerName}` | ||||
|       if (namespace) { | ||||
|         baseUrl = `${baseUrl}&&namespace=${namespace}` | ||||
|       } | ||||
| @@ -21,16 +21,4 @@ export class TerminalService { | ||||
|     }() | ||||
|     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
	 chenyang
					chenyang