feat: kubesphere 4.0 (#6115)
* feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> * feat: kubesphere 4.0 Signed-off-by: ci-bot <ci-bot@kubesphere.io> --------- Signed-off-by: ci-bot <ci-bot@kubesphere.io> Co-authored-by: ks-ci-bot <ks-ci-bot@example.com> Co-authored-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
committed by
GitHub
parent
b5015ec7b9
commit
447a51f08b
@@ -1,35 +1,35 @@
|
||||
/*
|
||||
Copyright 2020 KubeSphere Authors
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"github.com/gorilla/websocket"
|
||||
corev1 "k8s.io/api/core/v1"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/client-go/tools/remotecommand"
|
||||
"k8s.io/klog/v2"
|
||||
"k8s.io/kubectl/pkg/scheme"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
requestctx "kubesphere.io/kubesphere/pkg/apiserver/request"
|
||||
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"github.com/gorilla/websocket"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"kubesphere.io/kubesphere/pkg/models/terminal"
|
||||
)
|
||||
|
||||
@@ -40,19 +40,15 @@ var upgrader = websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
}
|
||||
|
||||
type terminalHandler struct {
|
||||
terminaler terminal.Interface
|
||||
authorizer authorizer.Authorizer
|
||||
type handler struct {
|
||||
client kubernetes.Interface
|
||||
config *rest.Config
|
||||
terminaler terminal.Interface
|
||||
authorizer authorizer.Authorizer
|
||||
uploadFileLimit int64
|
||||
}
|
||||
|
||||
func newTerminalHandler(client kubernetes.Interface, authorizer authorizer.Authorizer, config *rest.Config, options *terminal.Options) *terminalHandler {
|
||||
return &terminalHandler{
|
||||
authorizer: authorizer,
|
||||
terminaler: terminal.NewTerminaler(client, config, options),
|
||||
}
|
||||
}
|
||||
|
||||
func (t *terminalHandler) handleTerminalSession(request *restful.Request, response *restful.Response) {
|
||||
func (h *handler) HandleTerminalSession(request *restful.Request, response *restful.Response) {
|
||||
namespace := request.PathParameter("namespace")
|
||||
podName := request.PathParameter("pod")
|
||||
containerName := request.QueryParameter("container")
|
||||
@@ -60,17 +56,18 @@ func (t *terminalHandler) handleTerminalSession(request *restful.Request, respon
|
||||
|
||||
user, _ := requestctx.UserFrom(request.Request.Context())
|
||||
|
||||
createPodsExec := authorizer.AttributesRecord{
|
||||
createPodExec := authorizer.AttributesRecord{
|
||||
User: user,
|
||||
Verb: "create",
|
||||
Resource: "pods",
|
||||
Subresource: "exec",
|
||||
Name: podName,
|
||||
Namespace: namespace,
|
||||
ResourceRequest: true,
|
||||
ResourceScope: requestctx.NamespaceScope,
|
||||
}
|
||||
|
||||
decision, reason, err := t.authorizer.Authorize(createPodsExec)
|
||||
decision, reason, err := h.authorizer.Authorize(createPodExec)
|
||||
if err != nil {
|
||||
api.HandleInternalError(response, request, err)
|
||||
return
|
||||
@@ -87,10 +84,43 @@ func (t *terminalHandler) handleTerminalSession(request *restful.Request, respon
|
||||
return
|
||||
}
|
||||
|
||||
t.terminaler.HandleSession(shell, namespace, podName, containerName, conn)
|
||||
h.terminaler.HandleSession(request.Request.Context(), shell, namespace, podName, containerName, conn)
|
||||
}
|
||||
|
||||
func (t *terminalHandler) handleShellAccessToNode(request *restful.Request, response *restful.Response) {
|
||||
func (h *handler) HandleUserKubectlSession(request *restful.Request, response *restful.Response) {
|
||||
user, _ := requestctx.UserFrom(request.Request.Context())
|
||||
|
||||
createPodExec := authorizer.AttributesRecord{
|
||||
User: user,
|
||||
Verb: "create",
|
||||
Resource: "pods",
|
||||
Subresource: "exec",
|
||||
Namespace: constants.KubeSphereNamespace,
|
||||
ResourceRequest: true,
|
||||
Name: fmt.Sprintf("%s-%s", constants.KubectlPodNamePrefix, user.GetName()),
|
||||
ResourceScope: requestctx.NamespaceScope,
|
||||
}
|
||||
|
||||
decision, reason, err := h.authorizer.Authorize(createPodExec)
|
||||
if err != nil {
|
||||
api.HandleInternalError(response, request, err)
|
||||
return
|
||||
}
|
||||
|
||||
if decision != authorizer.DecisionAllow {
|
||||
api.HandleForbidden(response, request, errors.New(reason))
|
||||
return
|
||||
}
|
||||
|
||||
conn, err := upgrader.Upgrade(response.ResponseWriter, request.Request, nil)
|
||||
if err != nil {
|
||||
klog.Warning(err)
|
||||
return
|
||||
}
|
||||
h.terminaler.HandleUserKubectlSession(request.Request.Context(), user.GetName(), conn)
|
||||
}
|
||||
|
||||
func (h *handler) HandleShellAccessToNode(request *restful.Request, response *restful.Response) {
|
||||
nodename := request.PathParameter("nodename")
|
||||
|
||||
user, _ := requestctx.UserFrom(request.Request.Context())
|
||||
@@ -104,7 +134,7 @@ func (t *terminalHandler) handleShellAccessToNode(request *restful.Request, resp
|
||||
ResourceScope: requestctx.ClusterScope,
|
||||
}
|
||||
|
||||
decision, reason, err := t.authorizer.Authorize(createNodesExec)
|
||||
decision, reason, err := h.authorizer.Authorize(createNodesExec)
|
||||
if err != nil {
|
||||
api.HandleInternalError(response, request, err)
|
||||
return
|
||||
@@ -121,5 +151,248 @@ func (t *terminalHandler) handleShellAccessToNode(request *restful.Request, resp
|
||||
return
|
||||
}
|
||||
|
||||
t.terminaler.HandleShellAccessToNode(nodename, conn)
|
||||
h.terminaler.HandleShellAccessToNode(request.Request.Context(), nodename, conn)
|
||||
}
|
||||
|
||||
type fileWithHeader struct {
|
||||
file multipart.File
|
||||
header *multipart.FileHeader
|
||||
}
|
||||
|
||||
func (h *handler) UploadFile(request *restful.Request, response *restful.Response) {
|
||||
if err := request.Request.ParseMultipartForm(h.uploadFileLimit); err != nil {
|
||||
api.HandleInternalError(response, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
files := make([]fileWithHeader, 0)
|
||||
for name := range request.Request.MultipartForm.File {
|
||||
file, header, err := request.Request.FormFile(name)
|
||||
if err != nil {
|
||||
api.HandleBadRequest(response, nil, err)
|
||||
return
|
||||
}
|
||||
files = append(files, fileWithHeader{
|
||||
file: file,
|
||||
header: header,
|
||||
})
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
|
||||
tarWriter := tar.NewWriter(writer)
|
||||
defer tarWriter.Close()
|
||||
|
||||
for _, f := range files {
|
||||
func(f fileWithHeader) {
|
||||
defer f.file.Close()
|
||||
|
||||
// Write the tar header to the tar file
|
||||
if err := tarWriter.WriteHeader(&tar.Header{
|
||||
Name: f.header.Filename,
|
||||
Mode: 0600,
|
||||
Size: f.header.Size,
|
||||
}); err != nil {
|
||||
api.HandleInternalError(response, nil, err)
|
||||
return
|
||||
}
|
||||
// Copy the file content to the tar file
|
||||
if _, err := io.Copy(tarWriter, f.file); err != nil {
|
||||
api.HandleInternalError(response, nil, err)
|
||||
return
|
||||
}
|
||||
}(f)
|
||||
}
|
||||
}()
|
||||
|
||||
targetDir := request.QueryParameter("path")
|
||||
if targetDir == "" {
|
||||
targetDir = "/"
|
||||
}
|
||||
|
||||
namespace := request.PathParameter("namespace")
|
||||
podName := request.PathParameter("pod")
|
||||
containerName := request.QueryParameter("container")
|
||||
|
||||
req := h.client.CoreV1().RESTClient().Post().
|
||||
Resource("pods").
|
||||
Name(podName).
|
||||
Namespace(namespace).
|
||||
SubResource("exec").
|
||||
VersionedParams(&corev1.PodExecOptions{
|
||||
Container: containerName,
|
||||
Command: []string{"tar", "-xmf", "-", "-C", targetDir},
|
||||
Stdin: true,
|
||||
Stdout: false,
|
||||
Stderr: true,
|
||||
TTY: false,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(h.config, "POST", req.URL())
|
||||
if err != nil {
|
||||
api.HandleInternalError(response, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
if err = exec.StreamWithContext(request.Request.Context(), remotecommand.StreamOptions{
|
||||
Stdin: reader,
|
||||
Stdout: nil,
|
||||
Stderr: response,
|
||||
TerminalSizeQueue: nil,
|
||||
}); err != nil {
|
||||
api.HandleInternalError(response, nil, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
type tarPipe struct {
|
||||
config *rest.Config
|
||||
client rest.Interface
|
||||
|
||||
reader *io.PipeReader
|
||||
outStream *io.PipeWriter
|
||||
bytesRead uint64
|
||||
size uint64
|
||||
ctx context.Context
|
||||
|
||||
namespace, name, container, filePath string
|
||||
}
|
||||
|
||||
func newTarPipe(ctx context.Context, config *rest.Config, client rest.Interface, namespace, name, container, filePath string) (*tarPipe, error) {
|
||||
t := &tarPipe{
|
||||
config: config,
|
||||
client: client,
|
||||
namespace: namespace,
|
||||
name: name,
|
||||
container: container,
|
||||
filePath: filePath,
|
||||
ctx: ctx,
|
||||
}
|
||||
|
||||
if err := t.getFileSize(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := t.initReadFrom(0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
func (t *tarPipe) getFileSize() error {
|
||||
req := t.client.Post().
|
||||
Resource("pods").
|
||||
Name(t.name).
|
||||
Namespace(t.namespace).
|
||||
SubResource("exec").
|
||||
VersionedParams(&corev1.PodExecOptions{
|
||||
Container: t.container,
|
||||
Command: []string{"sh", "-c", fmt.Sprintf("tar cf - %s | wc -c", t.filePath)},
|
||||
Stdin: false,
|
||||
Stdout: true,
|
||||
Stderr: false,
|
||||
TTY: false,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(t.config, "POST", req.URL())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
reader, writer := io.Pipe()
|
||||
go func() {
|
||||
defer writer.Close()
|
||||
|
||||
if err = exec.StreamWithContext(t.ctx, remotecommand.StreamOptions{
|
||||
Stdin: nil,
|
||||
Stdout: writer,
|
||||
Stderr: nil,
|
||||
TerminalSizeQueue: nil,
|
||||
}); err != nil {
|
||||
klog.Error(err)
|
||||
}
|
||||
}()
|
||||
|
||||
result, err := io.ReadAll(reader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
num, err := strconv.ParseUint(strings.TrimSpace(string(result)), 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
t.size = num
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tarPipe) initReadFrom(n uint64) error {
|
||||
t.reader, t.outStream = io.Pipe()
|
||||
|
||||
req := t.client.Post().
|
||||
Resource("pods").
|
||||
Name(t.name).
|
||||
Namespace(t.namespace).
|
||||
SubResource("exec").
|
||||
VersionedParams(&corev1.PodExecOptions{
|
||||
Container: t.container,
|
||||
Command: []string{"sh", "-c", fmt.Sprintf("tar cf - %s | tail -c+%d", t.filePath, n)},
|
||||
Stdin: false,
|
||||
Stdout: true,
|
||||
Stderr: false,
|
||||
TTY: false,
|
||||
}, scheme.ParameterCodec)
|
||||
|
||||
exec, err := remotecommand.NewSPDYExecutor(t.config, "POST", req.URL())
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go func() {
|
||||
defer t.outStream.Close()
|
||||
|
||||
if err = exec.StreamWithContext(t.ctx, remotecommand.StreamOptions{
|
||||
Stdin: nil,
|
||||
Stdout: t.outStream,
|
||||
Stderr: nil,
|
||||
TerminalSizeQueue: nil,
|
||||
}); err != nil {
|
||||
klog.Error(err)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t *tarPipe) Read(p []byte) (int, error) {
|
||||
n, err := t.reader.Read(p)
|
||||
if err != nil {
|
||||
if t.bytesRead == t.size {
|
||||
return n, io.EOF
|
||||
}
|
||||
return n, t.initReadFrom(t.bytesRead + 1)
|
||||
}
|
||||
t.bytesRead += uint64(n)
|
||||
return n, nil
|
||||
}
|
||||
|
||||
func (h *handler) DownloadFile(request *restful.Request, response *restful.Response) {
|
||||
filePath := request.QueryParameter("path")
|
||||
fileName := filepath.Base(filePath)
|
||||
|
||||
response.AddHeader("Content-Disposition", fmt.Sprintf("attachment; filename=%s.tar", fileName))
|
||||
|
||||
namespace := request.PathParameter("namespace")
|
||||
podName := request.PathParameter("pod")
|
||||
containerName := request.QueryParameter("container")
|
||||
|
||||
reader, err := newTarPipe(request.Request.Context(), h.config, h.client.CoreV1().RESTClient(), namespace, podName, containerName, filePath)
|
||||
if err != nil {
|
||||
api.HandleInternalError(response, nil, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = io.Copy(response.ResponseWriter, reader); err != nil {
|
||||
api.HandleInternalError(response, nil, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,33 +1,25 @@
|
||||
/*
|
||||
Copyright 2019 The KubeSphere Authors.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
*/
|
||||
* Please refer to the LICENSE file in the root directory of the project.
|
||||
* https://github.com/kubesphere/kubesphere/blob/master/LICENSE
|
||||
*/
|
||||
|
||||
package v1alpha2
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"k8s.io/apimachinery/pkg/api/resource"
|
||||
"k8s.io/apimachinery/pkg/runtime/schema"
|
||||
"k8s.io/client-go/kubernetes"
|
||||
"k8s.io/client-go/rest"
|
||||
"k8s.io/klog/v2"
|
||||
|
||||
"kubesphere.io/kubesphere/pkg/api"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/authorization/authorizer"
|
||||
|
||||
restapi "kubesphere.io/kubesphere/pkg/apiserver/rest"
|
||||
"kubesphere.io/kubesphere/pkg/apiserver/runtime"
|
||||
"kubesphere.io/kubesphere/pkg/constants"
|
||||
"kubesphere.io/kubesphere/pkg/models"
|
||||
"kubesphere.io/kubesphere/pkg/models/terminal"
|
||||
)
|
||||
|
||||
@@ -37,29 +29,80 @@ const (
|
||||
|
||||
var GroupVersion = schema.GroupVersion{Group: GroupName, Version: "v1alpha2"}
|
||||
|
||||
func AddToContainer(c *restful.Container, client kubernetes.Interface, authorizer authorizer.Authorizer, config *rest.Config, options *terminal.Options) error {
|
||||
func NewHandler(client kubernetes.Interface, authorizer authorizer.Authorizer, config *rest.Config, options *terminal.Options) restapi.Handler {
|
||||
var uploadFileLimit int64 = 100 << 20 // 100 MB
|
||||
q, err := resource.ParseQuantity(options.UploadFileLimit)
|
||||
if err != nil {
|
||||
klog.Warningf("parse UploadFileLimit failed: %s, using default value 100Mi", err.Error())
|
||||
} else {
|
||||
uploadFileLimit = q.Value()
|
||||
}
|
||||
|
||||
webservice := runtime.NewWebService(GroupVersion)
|
||||
return &handler{
|
||||
client: client,
|
||||
config: config,
|
||||
authorizer: authorizer,
|
||||
terminaler: terminal.NewTerminaler(client, config, options),
|
||||
uploadFileLimit: uploadFileLimit,
|
||||
}
|
||||
}
|
||||
|
||||
handler := newTerminalHandler(client, authorizer, config, options)
|
||||
func NewFakeHandler() restapi.Handler {
|
||||
return &handler{}
|
||||
}
|
||||
|
||||
webservice.Route(webservice.GET("/namespaces/{namespace}/pods/{pod}/exec").
|
||||
To(handler.handleTerminalSession).
|
||||
Param(webservice.PathParameter("namespace", "namespace of which the pod located in")).
|
||||
Param(webservice.PathParameter("pod", "name of the pod")).
|
||||
Doc("create terminal session").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TerminalTag}).
|
||||
Writes(models.PodInfo{}))
|
||||
func (h *handler) AddToContainer(c *restful.Container) error {
|
||||
ws := runtime.NewWebService(GroupVersion)
|
||||
|
||||
//Add new Route to support shell access to the node
|
||||
webservice.Route(webservice.GET("/nodes/{nodename}/exec").
|
||||
To(handler.handleShellAccessToNode).
|
||||
Param(webservice.PathParameter("nodename", "name of cluster node")).
|
||||
Doc("create shell access to node session").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{constants.TerminalTag}).
|
||||
Writes(models.PodInfo{}))
|
||||
ws.Route(ws.GET("/namespaces/{namespace}/pods/{pod}/exec").
|
||||
To(h.HandleTerminalSession).
|
||||
Doc("Create pod terminal session").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagTerminal}).
|
||||
Operation("create-pod-exec").
|
||||
Param(ws.PathParameter("namespace", "The specified namespace.")).
|
||||
Param(ws.PathParameter("pod", "pod name")))
|
||||
|
||||
c.Add(webservice)
|
||||
ws.Route(ws.POST("/namespaces/{namespace}/pods/{pod}/file").
|
||||
To(h.UploadFile).
|
||||
Doc("Upload files to pod").
|
||||
Consumes(runtime.MimeMultipartFormData).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagTerminal}).
|
||||
Operation("upload-file-to-pod").
|
||||
Param(ws.PathParameter("namespace", "The specified namespace.")).
|
||||
Param(ws.PathParameter("pod", "pod name")).
|
||||
Param(ws.QueryParameter("container", "container name")).
|
||||
Param(ws.QueryParameter("path", "dest dir path")).
|
||||
Returns(http.StatusOK, api.StatusOK, nil))
|
||||
|
||||
ws.Route(ws.GET("/namespaces/{namespace}/pods/{pod}/file").
|
||||
To(h.DownloadFile).
|
||||
Doc("Download file from pod").
|
||||
Consumes(runtime.MimeMultipartFormData).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagTerminal}).
|
||||
Operation("download-file-from-pod").
|
||||
Param(ws.PathParameter("namespace", "The specified namespace.")).
|
||||
Param(ws.PathParameter("pod", "pod name")).
|
||||
Param(ws.QueryParameter("container", "container name")).
|
||||
Param(ws.QueryParameter("path", "file path")).
|
||||
Returns(http.StatusOK, api.StatusOK, nil))
|
||||
|
||||
ws.Route(ws.GET("/users/{user}/kubectl").
|
||||
To(h.HandleUserKubectlSession).
|
||||
Param(ws.PathParameter("user", "username")).
|
||||
Doc("Create kubectl pod terminal session for current user").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagTerminal}).
|
||||
Operation("create-user-kubectl-pod-exec"))
|
||||
|
||||
// Add new Route to support shell access to the node
|
||||
ws.Route(ws.GET("/nodes/{nodename}/exec").
|
||||
To(h.HandleShellAccessToNode).
|
||||
Doc("Create node terminal session").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagTerminal}).
|
||||
Operation("create-node-exec").
|
||||
Param(ws.PathParameter("nodename", "node name")).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{api.TagTerminal}))
|
||||
|
||||
c.Add(ws)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user