├── .gitattributes ├── .github └── workflows │ └── release.yml ├── .gitignore ├── Makefile ├── README.md ├── config ├── local.toml └── ut.toml ├── go.mod ├── go.sum ├── internal ├── apiv1 │ ├── configmap.go │ ├── debug.go │ ├── file.go │ ├── kube.go │ ├── pprof.go │ ├── proxy.go │ ├── tcpdump.go │ └── terminal.go ├── invoker │ └── invoker.go ├── model │ ├── dao │ │ └── kubecluster.go │ └── dto │ │ ├── common.go │ │ ├── configmap.go │ │ ├── debug.go │ │ ├── file.go │ │ ├── kube.go │ │ ├── pprof.go │ │ ├── proxy.go │ │ ├── tcpdump.go │ │ └── terminal.go ├── router │ └── router.go ├── service │ ├── init.go │ ├── kube │ │ ├── api │ │ │ └── types.go │ │ ├── cache.go │ │ ├── client.go │ │ ├── client_test.go │ │ ├── factory.go │ │ ├── handler.go │ │ ├── patcher │ │ │ └── patcher.go │ │ ├── resource.go │ │ └── unstructured.go │ ├── pod_file.go │ ├── pod_file_test.go │ ├── pprof.go │ ├── proxy.go │ ├── proxy_test.go │ ├── service_test.go │ ├── tcpdump.go │ ├── tcpdump_test.go │ └── terminal │ │ ├── terminal.go │ │ └── transport │ │ ├── debug_transport.go │ │ ├── simplify.go │ │ ├── transport.go │ │ └── ws_transport.go ├── ui │ ├── dist │ │ └── README.md │ └── ui.go └── util │ ├── env.go │ └── error.go ├── main.go ├── pkg ├── component │ └── core │ │ ├── core.go │ │ ├── core_test.go │ │ ├── user.go │ │ └── validator.go └── storage │ ├── filesystem │ └── client.go │ └── storage.go ├── scripts ├── build │ ├── gobuild.sh │ └── report_build_info.sh └── flamegraph.pl └── ui ├── .editorconfig ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky ├── .gitignore ├── commit-msg └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── config ├── config.ts ├── defaultSettings.ts ├── proxy.ts └── routes.ts ├── jest.config.js ├── jsconfig.json ├── mock └── api.ts ├── package.json ├── playwright.config.ts ├── public ├── favicon.ico ├── icon.png ├── icons │ ├── 1024x1024.png │ ├── 128x128.png │ ├── 16x16.png │ ├── 24x24.png │ ├── 256x256.png │ ├── 32x32.png │ ├── 48x48.png │ ├── 512x512.png │ ├── 64x64.png │ ├── icon.icns │ └── icon.ico └── logo.svg ├── src ├── app.tsx ├── assets │ ├── icon │ │ ├── text-not-wrap.svg │ │ └── text-wrap.svg │ └── toolsIcon │ │ ├── config.png │ │ ├── debug.png │ │ ├── files.png │ │ ├── pod-proxy.png │ │ ├── profiling.png │ │ ├── tcpdump.png │ │ └── terminal.png ├── components │ ├── BackToTopButton │ │ └── index.tsx │ ├── ClusterSettingButton │ │ ├── index.tsx │ │ └── styles │ │ │ └── index.styled.ts │ ├── ContainerSelectCard │ │ └── index.tsx │ ├── ContentCard │ │ ├── index.tsx │ │ └── styles │ │ │ └── index.styled.ts │ ├── CustomPageWrapper │ │ └── index.tsx │ ├── Footer │ │ └── index.tsx │ ├── IconFont │ │ └── index.tsx │ ├── Loading │ │ ├── index.tsx │ │ └── styles │ │ │ └── index.styled.ts │ ├── PageHeader │ │ ├── index.tsx │ │ └── styles │ │ │ └── index.styled.ts │ ├── PodSelectCard │ │ ├── index.tsx │ │ └── styles │ │ │ └── index.styled.ts │ ├── ToolCardEmpty │ │ ├── index.tsx │ │ └── styles │ │ │ └── index.styled.ts │ ├── ToolsMenuButton │ │ ├── index.tsx │ │ └── styles │ │ │ └── index.styled.ts │ └── index.md ├── configs │ └── default.ts ├── enums │ └── pretty.ts ├── global.less ├── hooks │ ├── useContainer.ts │ ├── usePodSelect.ts │ ├── useTerminal.ts │ └── useTools.ts ├── layouts │ ├── index.tsx │ └── styles │ │ └── index.styled.ts ├── main │ ├── createProtocol.ts │ ├── enums │ │ └── index.ts │ ├── index.ts │ ├── init │ │ ├── init.html │ │ └── init.ts │ ├── server.ts │ └── utils │ │ ├── downloadFileUtil.ts │ │ ├── fixPathUtil.ts │ │ ├── loggerUtil.ts │ │ └── menuUtil.ts ├── models │ └── pod.ts ├── pages │ ├── 404.tsx │ ├── AppInit │ │ ├── index.tsx │ │ └── styles │ │ │ └── index.less │ ├── ClustersManage │ │ ├── ClusterOptions.tsx │ │ ├── components │ │ │ ├── ClusterForm │ │ │ │ └── index.tsx │ │ │ ├── CreateClusterModal │ │ │ │ └── index.tsx │ │ │ └── UpdateClusterModal │ │ │ │ └── index.tsx │ │ ├── hooks │ │ │ └── useClusters.ts │ │ ├── index.tsx │ │ └── styles │ │ │ ├── form.less │ │ │ └── index.less │ ├── Tools │ │ ├── ConfigMap │ │ │ ├── components │ │ │ │ ├── ConfigMapEditor │ │ │ │ │ └── index.tsx │ │ │ │ ├── ConfigmapFiles │ │ │ │ │ ├── ConfigMapFileItem.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── CreateFileModal │ │ │ │ │ └── index.tsx │ │ │ │ └── DiffContextModal │ │ │ │ │ └── index.tsx │ │ │ ├── hooks │ │ │ │ └── useConfigMap.ts │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ ├── editor.styled.ts │ │ │ │ ├── files.styled.ts │ │ │ │ └── index.styled.ts │ │ ├── Debug │ │ │ ├── components │ │ │ │ └── ConfigForm │ │ │ │ │ └── index.tsx │ │ │ ├── hooks │ │ │ │ └── useDebug.ts │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ ├── config.styled.ts │ │ │ │ └── index.styled.ts │ │ ├── Files │ │ │ ├── components │ │ │ │ ├── FileList │ │ │ │ │ └── index.tsx │ │ │ │ ├── FilesContent │ │ │ │ │ └── index.tsx │ │ │ │ └── FilesHeader │ │ │ │ │ ├── FilesPathItem.tsx │ │ │ │ │ └── index.tsx │ │ │ ├── hooks │ │ │ │ └── useFiles.ts │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ ├── header.less │ │ │ │ ├── index.styled.ts │ │ │ │ ├── list.less │ │ │ │ └── list.styled.ts │ │ ├── Nodes │ │ │ ├── components │ │ │ │ └── FilterForm │ │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ └── form.styled.ts │ │ ├── PodProxy │ │ │ ├── components │ │ │ │ ├── ProxyRequest │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProxyRequestInfo │ │ │ │ │ ├── RequestBody.tsx │ │ │ │ │ ├── RequestHeaders.tsx │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProxyResponse │ │ │ │ │ └── index.tsx │ │ │ │ ├── RequestType │ │ │ │ │ └── index.tsx │ │ │ │ ├── ResponseInfo │ │ │ │ │ ├── ResponseBody.tsx │ │ │ │ │ ├── ResponseBodyOptions.tsx │ │ │ │ │ ├── ResponseCookiesProps.tsx │ │ │ │ │ ├── ResponseHeaders.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── ResponseType │ │ │ │ │ └── index.tsx │ │ │ ├── hooks │ │ │ │ ├── useFormOptions.ts │ │ │ │ ├── useResponseBodyOptions.ts │ │ │ │ └── useResponseType.ts │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ ├── request.styled.ts │ │ │ │ ├── response.less │ │ │ │ └── response.styled.ts │ │ ├── Profiling │ │ │ ├── components │ │ │ │ ├── CreateProfile │ │ │ │ │ └── index.tsx │ │ │ │ ├── ProfileHistoryList │ │ │ │ │ ├── DiffProfileModal.tsx │ │ │ │ │ ├── ProfileHistoryOption.tsx │ │ │ │ │ └── index.tsx │ │ │ │ └── ProfileViewSvg │ │ │ │ │ └── index.tsx │ │ │ ├── hooks │ │ │ │ ├── useCheckProfilingDependencies.ts │ │ │ │ ├── useDiffProfile.ts │ │ │ │ └── useProfile.ts │ │ │ ├── index.tsx │ │ │ ├── styles │ │ │ │ ├── history.styled.ts │ │ │ │ ├── index.styled.ts │ │ │ │ └── profileView.styled.ts │ │ │ └── utils │ │ │ │ ├── dependencyErrorsUtil.tsx │ │ │ │ └── downloadProfileUtil.ts │ │ ├── Tcpdump │ │ │ ├── components │ │ │ │ ├── TcpdumpCard │ │ │ │ │ └── index.tsx │ │ │ │ ├── TcpdumpConfigCard │ │ │ │ │ └── index.tsx │ │ │ │ └── TcpdumpForm │ │ │ │ │ └── index.tsx │ │ │ ├── hooks │ │ │ │ ├── useCheckWireshark.tsx │ │ │ │ ├── useSocketTcpdump.ts │ │ │ │ └── useTcpdump.ts │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ ├── config.styled.ts │ │ │ │ └── index.styled.ts │ │ ├── Terminal │ │ │ ├── components │ │ │ │ └── Term │ │ │ │ │ └── index.tsx │ │ │ ├── index.tsx │ │ │ └── styles │ │ │ │ ├── body.styled.ts │ │ │ │ └── term.styled.ts │ │ └── index.tsx │ ├── ToolsMap │ │ ├── components │ │ │ └── ToolItem │ │ │ │ └── ToolItem.tsx │ │ ├── configs │ │ │ └── configs.ts │ │ ├── hooks │ │ │ └── useResizeWindow.ts │ │ ├── index.tsx │ │ └── styles │ │ │ ├── index.styled.ts │ │ │ └── item.styled.ts │ ├── document.ejs │ └── index.tsx ├── preload │ └── index.ts ├── services │ ├── cluster.ts │ ├── configmap.ts │ ├── dependency.ts │ ├── namespace.ts │ ├── podContainers.ts │ ├── podProxy.ts │ ├── pods.ts │ ├── profiling.ts │ ├── tcpdump.ts │ ├── type.d.ts │ └── workload.ts ├── types │ └── index.d.ts ├── typings.d.ts └── utils │ ├── checkJsonUtils.ts │ ├── common.ts │ ├── documentScrollUtil.ts │ ├── electronRenderUtil.ts │ ├── storageUtil.ts │ └── timeUtils.ts ├── tests ├── run-tests.js └── setupTests.js ├── tsconfig.json └── yarn.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | *.ts linguist-detectable=false 2 | *.tsx linguist-detectable=false -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | bin 2 | logs 3 | tmp 4 | .idea 5 | config/*.json 6 | dist 7 | database.db 8 | .DS_Store 9 | static-tcpdump 10 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | APP_NAME:=k8z 2 | APP_PATH:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) 3 | SCRIPT_PATH:=$(APP_PATH)/scripts 4 | COMPILE_OUT:=$(APP_PATH)/bin/$(APP_NAME) 5 | 6 | TCPDUMP_VERSION=4.9.2 7 | STATIC_TCPDUMP_NAME=static-tcpdump 8 | 9 | run:export EGO_DEBUG=true 10 | run: 11 | @cd $(APP_PATH) && egoctl run 12 | install:export EGO_DEBUG=true 13 | install: 14 | @cd $(APP_PATH) && go run main.go --config=config/local.toml --job=install 15 | 16 | build.server: 17 | @echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>making build app<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 18 | @make ./bin/static-tcpdump 19 | @chmod +x $(SCRIPT_PATH)/build/*.sh 20 | @cd $(APP_PATH) && GOOS=$(GOOS) $(SCRIPT_PATH)/build/gobuild.sh $(APP_NAME) $(COMPILE_OUT) 21 | @cd $(APP_PATH) && mkdir -p ./ui/server && cp ./bin/* ./ui/server/ && cp -r config ./ui/server/ && chmod +x ./bin/* 22 | 23 | build.api: 24 | @echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>making build app<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 25 | @chmod +x $(SCRIPT_PATH)/build/*.sh 26 | @cd $(APP_PATH) && $(SCRIPT_PATH)/build/gobuild.sh $(APP_NAME) $(COMPILE_OUT) 27 | 28 | build.ui: 29 | @echo ">>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>making $@<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<" 30 | @git submodule update --init --recursive --remote 31 | @cd $(APP_PATH)/ui && yarn install --frozen-lockfile && yarn run build 32 | @cp -r $(APP_PATH)/ui/dist $(APP_PATH)/internal/ui 33 | @echo -e "\n" 34 | 35 | 36 | ./bin/static-tcpdump: 37 | # wget http://www.tcpdump.org/release/tcpdump-${TCPDUMP_VERSION}.tar.gz 38 | # tar -xvf tcpdump-${TCPDUMP_VERSION}.tar.gz 39 | # cd tcpdump-${TCPDUMP_VERSION} && CFLAGS=-static ./configure --without-crypto && make 40 | # mv tcpdump-${TCPDUMP_VERSION}/tcpdump ./${STATIC_TCPDUMP_NAME} 41 | # rm -rf tcpdump-${TCPDUMP_VERSION} tcpdump-${TCPDUMP_VERSION}.tar.gz 42 | wget https://github.com/eldadru/ksniff/releases/download/v1.6.2/ksniff.zip 43 | unzip -d ksniff ksniff.zip 44 | cp ksniff/static-tcpdump artifacts/ 45 | rm -rf ksniff ksniff.zip 46 | 47 | build.electron.darwin: 48 | make build.server GOOS=darwin 49 | cd ui && yarn electron:build:pro --mac 50 | 51 | build.electron.windows: 52 | make build.server GOOS=windows COMPILE_OUT=$(APP_PATH)/bin/$(APP_NAME).exe 53 | cd ui && yarn electron:build:pro --win 54 | 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # k8z: kubernetes business layer 2 | 3 | K8Z 意在 k8s 业务层面,提供一个方便好用的 k8s 集群可视化工具集 4 | 5 | # Features 6 | - 终端:连接到集群任意 POD 容器上,方便调试 7 | - Tcpdump:对集群内容器进行 tcpdump 抓包,可直接展示抓包信息,也可拉起 wireshark 实时分析 8 | - Files:可将本机文件上传至集群 POD 里或从集群 POD 上下载文件 9 | - Profiling: 对开启了 pprof 的 go 服务进行 profile,请求 profile 并绘制火焰图方便分析 10 | - POD HTTP proxy: 代理 http 请求到集群内 POD 上,方便一些本地网络和集群 POD 网络不通的场景调试接口使用 11 | - Debug:复制一个 POD 并新建一个终端连接上去,方便针对 crash 的 POD 手动调试故障 12 | - ConfigMap:提供方便的编辑器来管理集群内的configMap 13 | - 更多工具开发中 ... 14 | 15 | # Demo 16 | ![k8z-demo](https://user-images.githubusercontent.com/9847143/212584308-777becf0-5283-4a67-bb1f-b854080078d6.gif) 17 | -------------------------------------------------------------------------------- /config/local.toml: -------------------------------------------------------------------------------- 1 | token = "123456" 2 | 3 | [app] 4 | # hashStatecode 5 | rootURL = "http://localhost:9001" 6 | 7 | [server.http] 8 | port=9001 9 | embedPath = "dist" 10 | enableWebsocketCheckOrigin = true 11 | 12 | 13 | [storage] 14 | [storage.filesystem] 15 | basePath = "./tmp/goprobe/pprof" 16 | tcpdumpPath = "./tmp/tcpdump" 17 | -------------------------------------------------------------------------------- /config/ut.toml: -------------------------------------------------------------------------------- 1 | token = "123456" 2 | 3 | [app] 4 | # hashStatecode 5 | rootURL = "http://localhost:9001" 6 | 7 | [server.http] 8 | port=9001 9 | embedPath = "dist" 10 | 11 | 12 | [storage] 13 | [storage.filesystem] 14 | basePath = "../../tmp/goprobe/pprof" 15 | tcpdumpPath = "../../tmp/tcpdump" 16 | -------------------------------------------------------------------------------- /internal/apiv1/file.go: -------------------------------------------------------------------------------- 1 | package apiv1 2 | 3 | import ( 4 | "io" 5 | "io/ioutil" 6 | "net/http" 7 | "path" 8 | "path/filepath" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/gotomicro/ego/core/elog" 12 | "go.uber.org/zap" 13 | 14 | "k8z/internal/model/dto" 15 | "k8z/internal/service" 16 | "k8z/pkg/component/core" 17 | ) 18 | 19 | func ListPodFile(c *core.Context) { 20 | var params dto.ListPodFileReq 21 | err := c.Bind(¶ms) 22 | if err != nil { 23 | c.JSONE(core.CodeErr, "参数无效: "+err.Error(), nil) 24 | return 25 | } 26 | data, err := service.ListPodFile(c.Request.Context(), ¶ms) 27 | if err != nil { 28 | c.JSONE(core.CodeErr, "error: "+err.Error(), nil) 29 | return 30 | } 31 | c.JSONOK(gin.H{"current": params.Path, "files": data}) 32 | } 33 | 34 | func DownloadFileFromPod(c *core.Context) { 35 | var params dto.DownloadFileFromPod 36 | err := c.Bind(¶ms) 37 | if err != nil { 38 | c.JSONE(core.CodeErr, "参数无效: "+err.Error(), nil) 39 | return 40 | } 41 | data, err := service.DownloadFileFromPod(c.Request.Context(), ¶ms) 42 | if err != nil { 43 | c.JSONE(core.CodeErr, "error: "+err.Error(), nil) 44 | return 45 | } 46 | c.Writer.WriteHeader(http.StatusOK) 47 | var outputName string 48 | if len(params.Paths) > 1 { 49 | outputName = path.Base(path.Clean(params.Paths[0])) + "+" 50 | } else { 51 | outputName = path.Base(path.Clean(params.Paths[0])) 52 | } 53 | c.Header("Content-Disposition", "attachment; filename="+outputName+".tar") 54 | c.Header("Content-Type", "application/octet-stream") 55 | //c.Header("Content-Length", fmt.Sprintf("%d", len(data))) 56 | _, err = io.Copy(c.Writer, data) 57 | if err != nil { 58 | elog.Error("download file from pod error", zap.Error(err)) 59 | return 60 | } 61 | } 62 | 63 | func UploadFileToPod(c *core.Context) { 64 | var params dto.UploadFileToPod 65 | err := c.Bind(¶ms) 66 | if err != nil { 67 | c.JSONE(core.CodeErr, "参数无效: "+err.Error(), nil) 68 | return 69 | } 70 | if params.FilePath != "" { 71 | params.Filename = filepath.Base(params.FilePath) 72 | } else { 73 | fh, err := c.FormFile("file") 74 | if err != nil { 75 | c.JSONE(core.CodeErr, "获取文件错误: "+err.Error(), nil) 76 | return 77 | } 78 | params.Filename = fh.Filename 79 | f, err := fh.Open() 80 | if err != nil { 81 | c.JSONE(core.CodeErr, "open文件错误: "+err.Error(), nil) 82 | return 83 | } 84 | params.SrcContent, _ = ioutil.ReadAll(f) 85 | } 86 | err = service.UploadFileToPod(c.Request.Context(), ¶ms) 87 | if err != nil { 88 | c.JSONE(core.CodeErr, "error: "+err.Error(), nil) 89 | return 90 | } 91 | c.JSONOK() 92 | } 93 | -------------------------------------------------------------------------------- /internal/apiv1/proxy.go: -------------------------------------------------------------------------------- 1 | package apiv1 2 | 3 | import ( 4 | "k8z/internal/model/dto" 5 | "k8z/internal/service" 6 | "k8z/pkg/component/core" 7 | ) 8 | 9 | func PODProxyHTTP(c *core.Context) { 10 | var params dto.PODProxyHTTPReq 11 | err := c.Bind(¶ms) 12 | if err != nil { 13 | c.JSONE(core.CodeErr, "参数无效: "+err.Error(), nil) 14 | return 15 | } 16 | data, err := service.PODProxyHTTP(c.Request.Context(), ¶ms) 17 | if err != nil { 18 | c.JSONE(core.CodeErr, "代理请求出错: "+err.Error(), nil) 19 | return 20 | } 21 | c.JSONOK(data) 22 | } 23 | -------------------------------------------------------------------------------- /internal/apiv1/terminal.go: -------------------------------------------------------------------------------- 1 | package apiv1 2 | 3 | import ( 4 | "github.com/gotomicro/ego/server/egin" 5 | v1 "k8s.io/apimachinery/pkg/apis/meta/v1" 6 | "k8s.io/client-go/kubernetes/scheme" 7 | 8 | "k8z/internal/invoker" 9 | "k8z/internal/model/dto" 10 | "k8z/internal/service/kube" 11 | "k8z/internal/service/terminal" 12 | "k8z/internal/service/terminal/transport" 13 | "k8z/pkg/component/core" 14 | ) 15 | 16 | func Terminal(c *core.Context) { 17 | var params dto.ReqTerminal 18 | err := c.Bind(¶ms) 19 | if err != nil { 20 | c.JSONE(core.CodeErr, "参数无效: "+err.Error(), nil) 21 | return 22 | } 23 | cm, err := kube.GetClusterManager(params.ClusterName) 24 | if err != nil { 25 | c.JSONE(core.CodeErr, "cluster not found", nil) 26 | return 27 | } 28 | invoker.Gin.BuildWebsocket().Upgrade(c.Writer, c.Request, c.Context, func(conn *egin.WebSocketConn, err error) { 29 | wsTransport := transport.NewWSTransport(conn.Conn) 30 | restConfig := cm.Config 31 | restConfig.ContentConfig.GroupVersion = &v1.Unversioned 32 | restConfig.ContentConfig.NegotiatedSerializer = scheme.Codecs 33 | 34 | tml, err := terminal.NewWebTerminal(cm.Config, params.Namespace, params.PodName, params.ContainerName, wsTransport) 35 | if err != nil { 36 | _, _ = wsTransport.Write([]byte("连接终端失败,该Pod可能已经下线\n" + err.Error())) 37 | return 38 | } 39 | err = tml.Run() 40 | if err != nil { 41 | _, _ = wsTransport.Write([]byte("连接终端失败:" + err.Error())) 42 | return 43 | } 44 | }) 45 | } 46 | -------------------------------------------------------------------------------- /internal/invoker/invoker.go: -------------------------------------------------------------------------------- 1 | package invoker 2 | 3 | import ( 4 | "os" 5 | "path/filepath" 6 | 7 | "github.com/glebarez/sqlite" 8 | "github.com/gotomicro/ego/core/eflag" 9 | "github.com/gotomicro/ego/server/egin" 10 | "github.com/spf13/cast" 11 | "gorm.io/gorm" 12 | 13 | "k8z/internal/ui" 14 | ) 15 | 16 | var ( 17 | Gin *egin.Component 18 | DB *gorm.DB 19 | ) 20 | 21 | func Init() error { 22 | Gin = egin.Load("server.http").Build(egin.WithEmbedFs(ui.WebUI), egin.WithPort(cast.ToInt(eflag.String("port")))) 23 | homeDir, _ := os.UserHomeDir() 24 | profileDir := filepath.Join(homeDir, ".k8z") 25 | err := os.MkdirAll(profileDir, os.ModePerm) 26 | if err != nil { 27 | return err 28 | } 29 | db, err := gorm.Open(sqlite.Open(filepath.Join(profileDir, "database.db")), &gorm.Config{}) 30 | if err != nil { 31 | return err 32 | } 33 | DB = db 34 | return nil 35 | } 36 | -------------------------------------------------------------------------------- /internal/model/dao/kubecluster.go: -------------------------------------------------------------------------------- 1 | package dao 2 | 3 | import ( 4 | "github.com/gotomicro/ego/core/elog" 5 | "go.uber.org/zap" 6 | "gorm.io/gorm" 7 | ) 8 | 9 | type KubeCluster struct { 10 | Name string `gorm:"type:varchar(60);index:name,unique" json:"name"` 11 | ApiServer string `gorm:"type:varchar(60)" json:"apiServer"` 12 | KubeConfig string `gorm:"type:text" json:"kubeConfig"` 13 | } 14 | 15 | func Migrate(db *gorm.DB) error { 16 | return db.Debug().Migrator().AutoMigrate(&KubeCluster{}) 17 | } 18 | 19 | func ClusterList(db *gorm.DB) ([]*KubeCluster, error) { 20 | var ret []*KubeCluster 21 | err := db.Table("kube_clusters").Find(&ret).Error 22 | if err != nil { 23 | elog.Error("get cluster list error", zap.Error(err)) 24 | return nil, err 25 | } 26 | return ret, err 27 | } 28 | 29 | func GetClusterByName(db *gorm.DB, name string) (*KubeCluster, error) { 30 | var ret KubeCluster 31 | err := db.Table("kube_clusters").Where("name=?", name).First(&ret).Error 32 | if err != nil { 33 | elog.Error("get cluster by name error", zap.Error(err), zap.String("name", name)) 34 | return nil, err 35 | } 36 | return &ret, err 37 | } 38 | 39 | func CreateCluster(db *gorm.DB, bean *KubeCluster) error { 40 | err := db.Table("kube_clusters").Create(bean).Error 41 | if err != nil { 42 | elog.Error("create cluster error", zap.Error(err), zap.Any("bean", bean)) 43 | return err 44 | } 45 | return nil 46 | } 47 | 48 | func UpdateCluster(db *gorm.DB, bean *KubeCluster) error { 49 | err := db.Table("kube_clusters").Where("name=?", bean.Name).Save(bean).Error 50 | if err != nil { 51 | elog.Error("update cluster error", zap.Error(err), zap.Any("bean", bean)) 52 | return err 53 | } 54 | return nil 55 | } 56 | 57 | func DeleteCluster(db *gorm.DB, name string) error { 58 | err := db.Table("kube_clusters").Where("name=?", name).Delete(&KubeCluster{}).Error 59 | if err != nil { 60 | elog.Error("delete cluster error", zap.Error(err), zap.String("name", name)) 61 | return err 62 | } 63 | return nil 64 | } 65 | -------------------------------------------------------------------------------- /internal/model/dto/common.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | import ( 4 | "k8z/internal/util" 5 | ) 6 | 7 | type CheckDependenciesResponse struct { 8 | Success bool `json:"success"` 9 | DependencyErrors util.DepErrors `json:"dependencyErrors"` 10 | } 11 | -------------------------------------------------------------------------------- /internal/model/dto/configmap.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ConfigMapConfiguration struct { 4 | // 配置项名称 可能含有后缀 .json|.toml|.yml 等 5 | ConfigMapConfiguration string `json:"configMapConfiguration"` 6 | Cluster string `json:"cluster"` 7 | Namespace string `json:"namespace"` 8 | ConfigMap string `json:"configMap"` 9 | } 10 | 11 | type ConfigMapConfigurationData struct { 12 | Data string `json:"data"` 13 | ConfigMapConfiguration string `json:"configMapConfiguration"` 14 | Cluster string `json:"cluster"` 15 | Namespace string `json:"namespace"` 16 | ConfigMap string `json:"configMap"` 17 | } 18 | 19 | type SaveConfigMapConfigurationReq struct { 20 | Cluster string `json:"cluster"` 21 | Namespace string `json:"namespace"` 22 | ConfigMap string `json:"configMap"` 23 | ConfigMapConfiguration string `json:"configMapConfiguration"` 24 | ConfigMapConfigurationData string `json:"configMapConfigurationData"` 25 | } 26 | -------------------------------------------------------------------------------- /internal/model/dto/debug.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ReqRunDebug struct { 4 | ClusterName string `form:"clusterName" json:"clusterName"` 5 | PodName string `form:"podName" json:"podName"` 6 | Namespace string `form:"namespace" json:"namespace"` 7 | ContainerName string `form:"containerName" json:"containerName"` 8 | } 9 | -------------------------------------------------------------------------------- /internal/model/dto/file.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ListPodFileReq struct { 4 | ClusterName string `form:"clusterName" json:"clusterName"` 5 | PodName string `form:"podName" json:"podName"` 6 | Namespace string `form:"namespace" json:"namespace"` 7 | Path string `form:"path" json:"path"` 8 | Name string `form:"name" json:"name"` 9 | ContainerName string `form:"containerName" json:"containerName"` 10 | } 11 | 12 | type DownloadFileFromPod struct { 13 | ClusterName string `form:"clusterName" json:"clusterName"` 14 | PodName string `form:"podName" json:"podName"` 15 | Namespace string `form:"namespace" json:"namespace"` 16 | ContainerName string `form:"containerName" json:"containerName"` 17 | Paths []string `form:"paths" json:"paths"` 18 | } 19 | 20 | type UploadFileToPod struct { 21 | ClusterName string `form:"clusterName" json:"clusterName"` 22 | PodName string `form:"podName" json:"podName"` 23 | Namespace string `form:"namespace" json:"namespace"` 24 | ContainerName string `form:"containerName" json:"containerName"` 25 | DstPath string `form:"dstPath" json:"dstPath"` 26 | FilePath string `form:"filePath" json:"filePath"` // 本地上传方式 27 | Filename string `json:"-" form:"-"` 28 | SrcContent []byte `json:"-" form:"-"` 29 | } 30 | -------------------------------------------------------------------------------- /internal/model/dto/kube.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ReqKubeClusterAdd struct { 4 | Name string `json:"name" form:"name" binding:"required"` 5 | ApiServer string `json:"apiServer" form:"apiServer" binding:"required"` 6 | KubeConfig string `json:"kubeConfig" form:"kubeConfig" binding:"required"` 7 | } 8 | 9 | type KubePod struct { 10 | Cluster string `json:"cluster"` 11 | Namespace string `json:"namespace"` 12 | Workload string `json:"workload"` 13 | WorkloadKind string `json:"workloadKind"` 14 | Name string `json:"name"` 15 | Ready bool `json:"ready"` 16 | Reason string `json:"reason"` 17 | Containers []string `json:"containers"` 18 | } 19 | 20 | type KubeWorkload struct { 21 | Cluster string `json:"cluster"` 22 | Namespace string `json:"namespace"` 23 | Kind string `json:"kind"` 24 | Name string `json:"name"` 25 | Pods []*KubePod `json:"pods"` 26 | } 27 | 28 | type KubeNamespace struct { 29 | Cluster string `json:"cluster"` 30 | Name string `json:"name"` 31 | Workloads []*KubeWorkload `json:"workloads"` 32 | } 33 | 34 | type KubeCluster struct { 35 | Name string `json:"name"` 36 | IsStaticConfig bool `json:"isStaticConfig"` 37 | Namespaces []*KubeNamespace `json:"namespaces"` 38 | } 39 | 40 | type KubeConfigMap struct { 41 | Name string `json:"name"` 42 | Cluster string `json:"cluster"` 43 | Namespace string `json:"namespace"` 44 | } 45 | -------------------------------------------------------------------------------- /internal/model/dto/pprof.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ( 4 | // ReqRunProfile .. 5 | ReqRunProfile struct { 6 | Mode string `form:"mode" json:"mode" binding:"required"` // Pod, Ip 7 | Cluster string `form:"cluster" json:"cluster"` 8 | PodName string `form:"podName" json:"podName"` 9 | Port int `form:"port" json:"port"` 10 | Namespace string `form:"namespace" json:"namespace"` 11 | Addr string `form:"addr" json:"addr"` 12 | Seconds int `form:"seconds" json:"seconds"` 13 | Type int `form:"type" json:"type"` 14 | WorkloadKind string `form:"workloadKind" json:"workloadKind"` 15 | Workload string `form:"workload" json:"workload"` 16 | 17 | UniqueKey string `form:"-" json:"-"` 18 | } 19 | 20 | ReqPprofGraph struct { 21 | SvgType string `form:"svgType" binding:"required"` // flame | profile 22 | GoType string `form:"goType" binding:"required"` // block | goroutine | heap | profile 23 | Url string `form:"url" binding:"required"` 24 | } 25 | 26 | ReqPprofDownload struct { 27 | Url string `form:"url" binding:"required"` 28 | } 29 | 30 | ReqGetPprofList struct { 31 | Cluster string `form:"cluster" json:"cluster" binding:"required"` 32 | Namespace string `form:"namespace" json:"namespace" binding:"required"` 33 | WorkloadKind string `form:"workloadKind" json:"workloadKind" binding:"required"` 34 | Workload string `form:"workload" json:"workload" binding:"required"` 35 | Pod string `form:"pod" json:"pod"` 36 | } 37 | 38 | RespGetPprofListItem struct { 39 | Url string `json:"url"` 40 | PodName string `json:"podName"` 41 | Ctime int64 `json:"ctime"` 42 | } 43 | 44 | ReqPProfDiffGraph struct { 45 | SvgType string `form:"svgType" binding:"required"` // flame | profile 46 | GoType string `form:"goType" binding:"required"` // block | goroutine | heap | profile 47 | BaseUrl string `form:"baseUrl" binding:"required"` 48 | TargetUrl string `form:"targetUrl" binding:"required"` 49 | } 50 | 51 | ReqPProfRunDiff struct { 52 | BaseUrl string `form:"baseUrl" json:"baseUrl" binding:"required"` 53 | TargetUrl string `form:"targetUrl" json:"targetUrl" binding:"required"` 54 | } 55 | ) 56 | -------------------------------------------------------------------------------- /internal/model/dto/proxy.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type PODProxyHTTPReq struct { 4 | ClusterName string `form:"clusterName" json:"clusterName"` 5 | PodName string `form:"podName" json:"podName"` 6 | Namespace string `form:"namespace" json:"namespace"` 7 | Method string `form:"method" json:"method"` 8 | URL string `form:"url" json:"url"` 9 | Payload string `form:"payload" json:"payload"` 10 | Headers string `form:"headers" json:"headers"` 11 | } 12 | 13 | type PODProxyHTTPResp struct { 14 | Body string `json:"body"` 15 | Headers map[string]string `json:"headers"` 16 | Status string `json:"status"` 17 | StatusCode int `json:"statusCode"` 18 | Duration int64 `json:"duration"` 19 | ContentLength int `json:"contentLength"` 20 | } 21 | -------------------------------------------------------------------------------- /internal/model/dto/tcpdump.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ( 4 | // ReqRunTCPDump .. 5 | ReqRunTCPDump struct { 6 | ClusterName string `form:"clusterName" json:"clusterName"` 7 | PodName string `form:"podName" json:"podName"` 8 | Namespace string `form:"namespace" json:"namespace"` 9 | ContainerName string `form:"containerName" json:"containerName"` 10 | Interface string `form:"interface" json:"interface"` 11 | Filter string `form:"filter" json:"filter"` 12 | Mode string `form:"mode" json:"mode"` 13 | 14 | CreatedAt int64 `form:"-" json:"createdAt"` 15 | } 16 | ReqTCPDumpStop struct { 17 | TaskId string `form:"taskId" json:"taskId"` 18 | } 19 | ReqTCPDumpDownload struct { 20 | TaskId string `form:"taskId" json:"taskId"` 21 | } 22 | // ReqTCPDumpList .. 23 | ReqTCPDumpList struct { 24 | ClusterName string `form:"clusterName" json:"clusterName"` 25 | PodName string `form:"podName" json:"podName"` 26 | Namespace string `form:"namespace" json:"namespace"` 27 | ContainerName string `form:"containerName" json:"containerName"` 28 | } 29 | // RespTCPDumpListItem .. 30 | RespTCPDumpListItem struct { 31 | ClusterName string `json:"clusterName"` 32 | PodName string `json:"podName"` 33 | Namespace string `json:"namespace"` 34 | ContainerName string `json:"containerName"` 35 | Interface string `json:"interface"` 36 | Filter string ` json:"filter"` 37 | CreatedAt int64 `json:"createdAt"` 38 | Status int `json:"status"` 39 | Key string `json:"key"` 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /internal/model/dto/terminal.go: -------------------------------------------------------------------------------- 1 | package dto 2 | 3 | type ReqTerminal struct { 4 | ClusterName string `form:"clusterName" json:"clusterName"` 5 | PodName string `form:"podName" json:"podName"` 6 | Namespace string `form:"namespace" json:"namespace"` 7 | ContainerName string `form:"containerName" json:"containerName"` 8 | } 9 | -------------------------------------------------------------------------------- /internal/service/init.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "k8z/internal/invoker" 5 | "k8z/internal/model/dao" 6 | "k8z/internal/service/kube" 7 | ) 8 | 9 | func Init() error { 10 | err := dao.Migrate(invoker.DB) 11 | if err != nil { 12 | return err 13 | } 14 | err = kube.InitKube() 15 | if err != nil { 16 | return err 17 | } 18 | err = initPProf() 19 | if err != nil { 20 | return err 21 | } 22 | err = initTCPDump() 23 | if err != nil { 24 | return err 25 | } 26 | return nil 27 | } 28 | -------------------------------------------------------------------------------- /internal/service/kube/cache.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "time" 5 | 6 | "k8s.io/client-go/informers" 7 | "k8s.io/client-go/kubernetes" 8 | 9 | appsv1 "k8s.io/client-go/listers/apps/v1" 10 | corev1 "k8s.io/client-go/listers/core/v1" 11 | 12 | "k8z/internal/service/kube/api" 13 | ) 14 | 15 | type CacheFactory struct { 16 | stopChan chan struct{} 17 | sharedInformerFactory informers.SharedInformerFactory 18 | } 19 | 20 | func buildCacheController(client *kubernetes.Clientset) (*CacheFactory, error) { 21 | stop := make(chan struct{}) 22 | sharedInformerFactory := informers.NewSharedInformerFactory(client, defaultResyncPeriod) 23 | 24 | // Start all Resources defined in KindToResourceMap 25 | for _, value := range api.KindToResourceMap { 26 | _, err := sharedInformerFactory.ForResource(value.GroupVersionResourceKind.GroupVersionResource) 27 | if err != nil { 28 | return nil, err 29 | } 30 | } 31 | 32 | sharedInformerFactory.Start(stop) 33 | 34 | return &CacheFactory{ 35 | stopChan: stop, 36 | sharedInformerFactory: sharedInformerFactory, 37 | }, nil 38 | } 39 | 40 | func (c *CacheFactory) PodLister() corev1.PodLister { 41 | return c.sharedInformerFactory.Core().V1().Pods().Lister() 42 | } 43 | 44 | func (c *CacheFactory) DeploymentLister() appsv1.DeploymentLister { 45 | return c.sharedInformerFactory.Apps().V1().Deployments().Lister() 46 | } 47 | 48 | func (c *CacheFactory) Stop() { 49 | close(c.stopChan) 50 | } 51 | 52 | func (c *CacheFactory) WaitForCacheSync(timeout time.Duration) { 53 | stop := make(chan struct{}) 54 | time.AfterFunc(timeout, func() { 55 | close(stop) 56 | }) 57 | c.sharedInformerFactory.WaitForCacheSync(stop) 58 | } 59 | -------------------------------------------------------------------------------- /internal/service/kube/client_test.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "fmt" 5 | "strings" 6 | "testing" 7 | 8 | "github.com/BurntSushi/toml" 9 | "github.com/gotomicro/ego/core/econf" 10 | ) 11 | 12 | func TestGetAllClusters(t *testing.T) { 13 | econf.LoadFromReader(strings.NewReader(` 14 | [[cluster]] 15 | apiServer="127.0.0.1" 16 | `), toml.Unmarshal) 17 | gotResult, err := GetAllClusters() 18 | fmt.Printf("err--------------->"+"%+v\n", err) 19 | for _, value := range gotResult { 20 | fmt.Printf("gotResult--------------->"+"%+v\n", value) 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /internal/service/kube/factory.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | appsv1 "k8s.io/api/apps/v1" 5 | autoscalingv1 "k8s.io/api/autoscaling/v1" 6 | batchv1 "k8s.io/api/batch/v1" 7 | corev1 "k8s.io/api/core/v1" 8 | extensionsv1beta1 "k8s.io/api/extensions/v1beta1" 9 | rbacv1 "k8s.io/api/rbac/v1" 10 | storagev1 "k8s.io/api/storage/v1" 11 | "k8s.io/apimachinery/pkg/runtime/schema" 12 | "k8s.io/client-go/rest" 13 | ) 14 | 15 | func (h *resourceHandler) getClientByGroupVersion(groupVersion schema.GroupVersionResource) rest.Interface { 16 | switch groupVersion.Group { 17 | case corev1.GroupName: 18 | return h.client.CoreV1().RESTClient() 19 | case appsv1.GroupName: 20 | return h.client.AppsV1().RESTClient() 21 | case autoscalingv1.GroupName: 22 | return h.client.AutoscalingV1().RESTClient() 23 | case batchv1.GroupName: 24 | if groupVersion.Version == "v1beta1" { 25 | return h.client.BatchV1beta1().RESTClient() 26 | } 27 | return h.client.BatchV1().RESTClient() 28 | case extensionsv1beta1.GroupName: 29 | return h.client.ExtensionsV1beta1().RESTClient() 30 | case storagev1.GroupName: 31 | return h.client.StorageV1().RESTClient() 32 | case rbacv1.GroupName: 33 | return h.client.RbacV1().RESTClient() 34 | default: 35 | return h.client.CoreV1().RESTClient() 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /internal/service/kube/unstructured.go: -------------------------------------------------------------------------------- 1 | package kube 2 | 3 | import ( 4 | "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" 5 | "k8s.io/cli-runtime/pkg/resource" 6 | ) 7 | 8 | func SetDefaultNamespaceIfScopedAndNoneSet(u *unstructured.Unstructured, helper *resource.Helper) { 9 | namespace := u.GetNamespace() 10 | if helper.NamespaceScoped && namespace == "" { 11 | namespace = "default" 12 | u.SetNamespace(namespace) 13 | } 14 | } 15 | 16 | func SetNamespaceIfScoped(namespace string, u *unstructured.Unstructured, helper *resource.Helper) { 17 | if helper.NamespaceScoped { 18 | u.SetNamespace(namespace) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /internal/service/pod_file_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "os/exec" 6 | "testing" 7 | 8 | "k8z/internal/model/dto" 9 | ) 10 | 11 | func TestListPodContainerFile(t *testing.T) { 12 | //ListPodFile(context.Background()) 13 | } 14 | 15 | func TestUploadFileToPod(t *testing.T) { 16 | //UploadFileToPod(context.Background()) 17 | } 18 | 19 | func TestDownloadFileFromPod(t *testing.T) { 20 | DownloadFileFromPod(context.Background(), &dto.DownloadFileFromPod{ 21 | ClusterName: "c1", 22 | PodName: "pod-xxxx", 23 | Namespace: "default", 24 | ContainerName: "", 25 | Paths: []string{"/data/config", "/data/1.txt"}, 26 | }) 27 | } 28 | 29 | func Test_parseLSOutput(t *testing.T) { 30 | out, _ := exec.Command("ls", "-al", "/").Output() 31 | parseLSOutput(string(out)) 32 | } 33 | -------------------------------------------------------------------------------- /internal/service/proxy_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "context" 5 | "testing" 6 | 7 | "github.com/stretchr/testify/assert" 8 | "github.com/stretchr/testify/require" 9 | 10 | "k8z/internal/model/dto" 11 | ) 12 | 13 | func TestPODProxyHTTP(t *testing.T) { 14 | resp, err := PODProxyHTTP(context.Background(), &dto.PODProxyHTTPReq{ 15 | ClusterName: "c1", 16 | PodName: "pod-xxxx", 17 | Namespace: "default", 18 | Method: "GET", 19 | URL: "http://pod-xxxx:9003/metrics", 20 | Payload: "", 21 | Headers: "", 22 | }) 23 | require.NoError(t, err) 24 | assert.Equal(t, 200, resp.StatusCode) 25 | } 26 | -------------------------------------------------------------------------------- /internal/service/service_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "flag" 5 | "testing" 6 | 7 | "github.com/gotomicro/ego" 8 | 9 | "k8z/internal/invoker" 10 | ) 11 | 12 | func TestMain(m *testing.M) { 13 | flag.String("config", "../../config/ut.toml", "") 14 | _ = ego.New(ego.WithDisableFlagConfig(true), ego.WithHang(false)). 15 | Invoker(invoker.Init). 16 | Invoker(Init). 17 | Invoker(func() error { 18 | m.Run() 19 | return nil 20 | }).Run() 21 | } 22 | -------------------------------------------------------------------------------- /internal/service/tcpdump_test.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | "testing" 7 | "time" 8 | 9 | "github.com/davecgh/go-spew/spew" 10 | "github.com/stretchr/testify/require" 11 | 12 | "k8z/internal/model/dto" 13 | ) 14 | 15 | func Test_tcpDump_Run(t1 *testing.T) { 16 | key, err := TCPDump.Run(context.Background(), &dto.ReqRunTCPDump{ 17 | ClusterName: "c1", 18 | PodName: "pod-xxxx", 19 | Namespace: "default", 20 | ContainerName: "", 21 | Interface: "any", 22 | Filter: "", 23 | }) 24 | require.NoError(t1, err) 25 | time.Sleep(time.Second * 10) 26 | TCPDump.Stop(context.Background(), key) 27 | time.Sleep(time.Second * 3) 28 | } 29 | 30 | func Test_tcpDump_Run1(t1 *testing.T) { 31 | buf := bytes.NewBuffer(nil) 32 | var data = make([]byte, 10) 33 | buf.WriteByte(0x03) 34 | n, err := buf.Read(data) 35 | spew.Dump(n, err) 36 | 37 | } 38 | 39 | func Test_tcpDump_List(t1 *testing.T) { 40 | list, err := TCPDump.List(context.Background(), &dto.ReqTCPDumpList{ 41 | ClusterName: "c1", 42 | PodName: "pod-xxxx", 43 | Namespace: "default", 44 | ContainerName: "", 45 | }) 46 | require.NoError(t1, err) 47 | spew.Dump(list) 48 | } 49 | -------------------------------------------------------------------------------- /internal/service/terminal/terminal.go: -------------------------------------------------------------------------------- 1 | package terminal 2 | 3 | import ( 4 | v1 "k8s.io/api/core/v1" 5 | "k8s.io/client-go/kubernetes" 6 | "k8s.io/client-go/kubernetes/scheme" 7 | "k8s.io/client-go/rest" 8 | "k8s.io/client-go/tools/remotecommand" 9 | 10 | "k8z/internal/service/terminal/transport" 11 | ) 12 | 13 | type WebTerminal struct { 14 | executor remotecommand.Executor 15 | transport transport.Transport 16 | restConfig *rest.Config 17 | namespace string 18 | podName string 19 | containerName string 20 | } 21 | 22 | func NewWebTerminal(config *rest.Config, namespace, podName, containerName string, transport transport.Transport) (webTerminal *WebTerminal, err error) { 23 | restClient, err := kubernetes.NewForConfig(config) 24 | if err != nil { 25 | return 26 | } 27 | execReq := restClient.CoreV1().RESTClient().Post(). 28 | Resource("pods"). 29 | Name(podName). 30 | Namespace(namespace). 31 | SubResource("exec"). 32 | VersionedParams(&v1.PodExecOptions{ 33 | Stdin: true, 34 | Stdout: true, 35 | Stderr: true, 36 | TTY: true, 37 | Container: containerName, 38 | Command: []string{"/bin/sh", "-c", "TERM=xterm-256color; export TERM; [ -x /bin/bash ] && ([ -x /usr/bin/script ] && /usr/bin/script -q -c \"/bin/bash\" /dev/null || exec /bin/bash) || exec /bin/sh"}, 39 | }, scheme.ParameterCodec) 40 | executor, err := remotecommand.NewSPDYExecutor(config, "POST", execReq.URL()) 41 | if err != nil { 42 | return 43 | } 44 | webTerminal = &WebTerminal{ 45 | executor: executor, 46 | transport: transport, 47 | restConfig: config, 48 | namespace: namespace, 49 | podName: podName, 50 | containerName: containerName, 51 | } 52 | return 53 | } 54 | 55 | func (w *WebTerminal) Run() (err error) { 56 | return w.executor.Stream(remotecommand.StreamOptions{ 57 | Stdin: w.transport, 58 | Stdout: w.transport, 59 | Stderr: w.transport, 60 | TerminalSizeQueue: w.transport, 61 | Tty: true, 62 | }) 63 | } 64 | -------------------------------------------------------------------------------- /internal/service/terminal/transport/debug_transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "k8s.io/client-go/tools/remotecommand" 5 | ) 6 | 7 | type DebugTransport struct { 8 | ResizeChan chan remotecommand.TerminalSize 9 | Commands []string 10 | } 11 | 12 | func NewDebugTransport() (d *DebugTransport) { 13 | return &DebugTransport{ 14 | ResizeChan: make(chan remotecommand.TerminalSize), 15 | } 16 | } 17 | 18 | func (d *DebugTransport) Read(p []byte) (n int, err error) { 19 | if len(d.Commands) <= 0 { 20 | return 0, nil 21 | } 22 | cmd := d.Commands[0] 23 | copy(p, cmd) 24 | cmdList := make([]string, 0) 25 | for i := 1; i < len(d.Commands); i++ { 26 | cmdList = append(cmdList, d.Commands[i]) 27 | } 28 | d.Commands = cmdList 29 | return len(cmd), nil 30 | } 31 | 32 | func (d *DebugTransport) Write(p []byte) (n int, err error) { 33 | return len(p), nil 34 | } 35 | 36 | func (d *DebugTransport) Next() *remotecommand.TerminalSize { 37 | ret := <-d.ResizeChan 38 | return &ret 39 | } 40 | 41 | func (d DebugTransport) Close() { 42 | return 43 | } 44 | -------------------------------------------------------------------------------- /internal/service/terminal/transport/simplify.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "sync" 7 | 8 | "github.com/gorilla/websocket" 9 | "github.com/gotomicro/ego/core/elog" 10 | ) 11 | 12 | type SimplifyTransport struct { 13 | conn *websocket.Conn 14 | stopChan chan struct{} 15 | closed bool 16 | l sync.RWMutex 17 | } 18 | 19 | func NewSimplifyTransport(conn *websocket.Conn) *SimplifyTransport { 20 | return &SimplifyTransport{ 21 | conn: conn, 22 | stopChan: make(chan struct{}), 23 | closed: false, 24 | l: sync.RWMutex{}, 25 | } 26 | } 27 | 28 | func (w *SimplifyTransport) Read() (err error) { 29 | if w.closed { 30 | return 31 | } 32 | if w.conn == nil { 33 | w.closed = true 34 | return 35 | } 36 | msgType, msgBytes, err := w.conn.ReadMessage() 37 | if err != nil { 38 | return 39 | } 40 | elog.Info("websocket", elog.String("msgBytes", string(msgBytes))) 41 | if msgType != websocket.TextMessage { 42 | return 43 | } 44 | var msg message 45 | if err = json.Unmarshal(msgBytes, &msg); err != nil { 46 | w.l.Lock() 47 | _ = w.conn.WriteMessage(websocket.TextMessage, []byte(fmt.Sprintf("conn read failed. err=%s", err.Error()))) 48 | w.l.Unlock() 49 | return 50 | } 51 | elog.Info("websocket", elog.Any("msg", msg)) 52 | switch msg.Type { 53 | case MsgClose: 54 | _ = w.conn.Close() 55 | return 56 | case MsgStdin: 57 | return 58 | case MsgResize: 59 | return 60 | case MsgPing: 61 | w.l.Lock() 62 | _ = w.conn.WriteMessage(websocket.TextMessage, []byte(MsgPong)) 63 | w.l.Unlock() 64 | return 65 | default: 66 | } 67 | return nil 68 | } 69 | 70 | func (w *SimplifyTransport) WriteStdoutData(data interface{}) (n int, err error) { 71 | msg, err := json.Marshal(WsMessage{ 72 | Type: MsgStdout, 73 | Data: data, 74 | }) 75 | if err != nil { 76 | return 0, nil 77 | } 78 | w.l.Lock() 79 | err = w.conn.WriteMessage(websocket.TextMessage, msg) 80 | w.l.Unlock() 81 | if err != nil { 82 | return 0, err 83 | } 84 | return len(msg), nil 85 | } 86 | 87 | func (w *SimplifyTransport) Write(p []byte) (n int, err error) { 88 | msg, err := json.Marshal(message{ 89 | Type: MsgStdout, 90 | Data: string(p), 91 | }) 92 | if err != nil { 93 | return 0, nil 94 | } 95 | w.l.Lock() 96 | err = w.conn.WriteMessage(websocket.TextMessage, msg) 97 | w.l.Unlock() 98 | if err != nil { 99 | return 0, err 100 | } 101 | return len(p), nil 102 | } 103 | 104 | func (w *SimplifyTransport) Close() { 105 | w.stopChan <- struct{}{} 106 | } 107 | 108 | func (w *SimplifyTransport) IsClose() bool { 109 | return w.closed 110 | } 111 | -------------------------------------------------------------------------------- /internal/service/terminal/transport/transport.go: -------------------------------------------------------------------------------- 1 | package transport 2 | 3 | import ( 4 | "io" 5 | 6 | "k8s.io/client-go/tools/remotecommand" 7 | ) 8 | 9 | type Transport interface { 10 | io.Reader 11 | io.Writer 12 | remotecommand.TerminalSizeQueue 13 | Close() 14 | } 15 | 16 | const EndTransmission = "exit\r\n" 17 | -------------------------------------------------------------------------------- /internal/ui/dist/README.md: -------------------------------------------------------------------------------- 1 | embed dir 2 | -------------------------------------------------------------------------------- /internal/ui/ui.go: -------------------------------------------------------------------------------- 1 | package ui 2 | 3 | import ( 4 | "embed" 5 | ) 6 | 7 | //go:embed dist 8 | var WebUI embed.FS 9 | -------------------------------------------------------------------------------- /internal/util/env.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "github.com/gotomicro/ego/core/eflag" 5 | ) 6 | 7 | func IsRunModeElectron() bool { 8 | return eflag.String("mode") == "electron" 9 | } 10 | -------------------------------------------------------------------------------- /internal/util/error.go: -------------------------------------------------------------------------------- 1 | package util 2 | 3 | import ( 4 | "fmt" 5 | ) 6 | 7 | type DepError struct { 8 | Dependency string `json:"dependency"` 9 | Refer string `json:"refer"` 10 | } 11 | 12 | type DepErrors []DepError 13 | 14 | func (de DepError) Error() string { 15 | if de.Refer == "" { 16 | return fmt.Sprintf("dependency [%s] missing", de.Dependency) 17 | } 18 | return fmt.Sprintf("dependency [%s] missing, please refer to %s to solve this issue", de.Dependency, de.Refer) 19 | } 20 | 21 | func (des DepErrors) Error() string { 22 | var str string 23 | for _, de := range des { 24 | str += de.Error() 25 | str += "\n" 26 | } 27 | return str[:len(str)-1] 28 | } 29 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/gotomicro/ego" 5 | "github.com/gotomicro/ego/core/eflag" 6 | "github.com/gotomicro/ego/core/elog" 7 | 8 | "k8z/internal/invoker" 9 | "k8z/internal/router" 10 | "k8z/internal/service" 11 | ) 12 | 13 | func main() { 14 | eflag.Register(&eflag.IntFlag{ 15 | Name: "port", 16 | Usage: "--port", 17 | EnvVar: "K8Z_PORT", 18 | Default: 9001, 19 | Action: func(string, *eflag.FlagSet) {}, 20 | }) 21 | eflag.Register(&eflag.StringFlag{ 22 | Name: "mode", 23 | Usage: "--mode", 24 | EnvVar: "K8Z_MODE", 25 | Default: "electron", 26 | Action: func(string, *eflag.FlagSet) {}, 27 | }) 28 | err := ego.New(). 29 | Invoker( 30 | invoker.Init, 31 | service.Init, 32 | ). 33 | Serve(router.Server()). 34 | Run() 35 | if err != nil { 36 | elog.Panic("start up error: " + err.Error()) 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /pkg/component/core/core_test.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "bytes" 5 | "net/http" 6 | "net/http/httptest" 7 | "testing" 8 | 9 | "github.com/gin-gonic/gin" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | func TestBind(t *testing.T) { 14 | w := httptest.NewRecorder() 15 | c, _ := gin.CreateTestContext(w) 16 | c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`{"title":"t", "email":"@email.com"}`)) 17 | c.Request.Header.Add("Content-Type", gin.MIMEJSON) 18 | gc := Context{Context: c} 19 | var obj struct { 20 | Title string `json:"title" binding:"required,max=32,min=4" label:"标题"` 21 | Email *string `json:"email" binding:"required,email" label:"邮箱"` 22 | } 23 | assert.Equal(t, gc.Bind(&obj).Error(), "标题长度必须至少为4个字符|邮箱必须是一个有效的邮箱") 24 | assert.Equal(t, w.Code, 400) 25 | t.Log("Code:", w.Code, "Body:", w.Body.String()) 26 | assert.Empty(t, c.Errors) 27 | } 28 | 29 | func TestShouldBind(t *testing.T) { 30 | w := httptest.NewRecorder() 31 | c, _ := gin.CreateTestContext(w) 32 | c.Request, _ = http.NewRequest("POST", "/", bytes.NewBufferString(`{"title":"t", "email":"@email.com"}`)) 33 | c.Request.Header.Add("Content-Type", gin.MIMEJSON) 34 | gc := Context{Context: c} 35 | var obj struct { 36 | Title string `json:"title" binding:"required,max=32,min=4" label:"标题"` 37 | Email *string `json:"email" binding:"required,email" label:"邮箱"` 38 | } 39 | assert.Equal(t, gc.ShouldBind(&obj).Error(), "标题长度必须至少为4个字符|邮箱必须是一个有效的邮箱") 40 | assert.Equal(t, w.Code, 200) 41 | t.Log("Code:", w.Code, "Body:", w.Body.String()) 42 | assert.Empty(t, c.Errors) 43 | } 44 | -------------------------------------------------------------------------------- /pkg/component/core/user.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "github.com/gin-gonic/gin" 5 | ) 6 | 7 | const ( 8 | UserContextKey = "moauth/context/user" 9 | ) 10 | 11 | type User struct { 12 | Uid int64 `protobuf:"varint,1,opt,name=uid,proto3" json:"uid,omitempty"` 13 | Nickname string `protobuf:"bytes,2,opt,name=nickname,proto3" json:"nickname,omitempty"` 14 | Username string `protobuf:"bytes,3,opt,name=username,proto3" json:"username,omitempty"` 15 | Avatar string `protobuf:"bytes,4,opt,name=avatar,proto3" json:"avatar,omitempty"` 16 | Email string `protobuf:"bytes,5,opt,name=email,proto3" json:"email,omitempty"` 17 | } 18 | 19 | // Uid gets uid from context 20 | func (c *Context) Uid() int { 21 | return Uid(c.Context) 22 | } 23 | 24 | func (c *Context) User() *User { 25 | return ContextUser(c.Context) 26 | } 27 | 28 | // Uid get uid from gin.Context 29 | func Uid(c *gin.Context) int { 30 | return int(ContextUser(c).Uid) 31 | } 32 | 33 | // ContextUser get user from gin.Context 34 | func ContextUser(c *gin.Context) *User { 35 | resp := &User{} 36 | respI, flag := c.Get(UserContextKey) 37 | if flag { 38 | resp = respI.(*User) 39 | } 40 | return resp 41 | } 42 | -------------------------------------------------------------------------------- /pkg/component/core/validator.go: -------------------------------------------------------------------------------- 1 | package core 2 | 3 | import ( 4 | "errors" 5 | "reflect" 6 | "strings" 7 | "sync" 8 | 9 | "github.com/gin-gonic/gin/binding" 10 | "github.com/go-playground/locales/zh" 11 | ut "github.com/go-playground/universal-translator" 12 | "github.com/go-playground/validator/v10" 13 | tzh "github.com/go-playground/validator/v10/translations/zh" 14 | "github.com/gotomicro/ego/core/elog" 15 | ) 16 | 17 | func init() { 18 | binding.Validator = &defaultValidator{} 19 | } 20 | 21 | type defaultValidator struct { 22 | once sync.Once 23 | validate *validator.Validate 24 | } 25 | 26 | var _ binding.StructValidator = &defaultValidator{} 27 | 28 | func (v *defaultValidator) ValidateStruct(obj interface{}) error { 29 | value := reflect.ValueOf(obj) 30 | valueType := value.Kind() 31 | if valueType == reflect.Ptr { 32 | valueType = value.Elem().Kind() 33 | } 34 | if valueType == reflect.Struct { 35 | v.lazyinit() 36 | if err := v.validate.Struct(obj); err != nil { 37 | return err 38 | } 39 | } 40 | return nil 41 | } 42 | 43 | func (v *defaultValidator) Engine() interface{} { 44 | v.lazyinit() 45 | return v.validate 46 | } 47 | 48 | func newValidator() *validator.Validate { 49 | // 注册translator 50 | zhTranslator := zh.New() 51 | uni := ut.New(zhTranslator, zhTranslator) 52 | trans, _ = uni.GetTranslator("zh") 53 | validate := validator.New() 54 | validate.RegisterTagNameFunc(func(field reflect.StructField) string { 55 | label := field.Tag.Get("label") 56 | if label == "" { 57 | return field.Name 58 | } 59 | return label 60 | }) 61 | if err := tzh.RegisterDefaultTranslations(validate, trans); err != nil { 62 | elog.Fatal("Gin fail to registered Translation") 63 | } 64 | return validate 65 | } 66 | 67 | func (v *defaultValidator) lazyinit() { 68 | v.once.Do(func() { 69 | v.validate = newValidator() 70 | v.validate.SetTagName("binding") 71 | }) 72 | } 73 | 74 | var trans ut.Translator 75 | 76 | func validate(errs error) error { 77 | if validationErrors, ok := errs.(validator.ValidationErrors); ok { 78 | var errList []string 79 | for _, e := range validationErrors { 80 | errList = append(errList, e.Translate(trans)) 81 | } 82 | return errors.New(strings.Join(errList, "|")) 83 | } 84 | return errs 85 | } 86 | -------------------------------------------------------------------------------- /pkg/storage/filesystem/client.go: -------------------------------------------------------------------------------- 1 | package filesystem 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "k8z/pkg/storage" 12 | ) 13 | 14 | type Client struct { 15 | basePath string 16 | } 17 | 18 | func NewFilesystemClient(basePath string) *Client { 19 | return &Client{basePath: basePath} 20 | } 21 | 22 | var _ storage.Client = &Client{} 23 | 24 | func (c Client) GetBytes(ctx context.Context, key string) ([]byte, error) { 25 | f, err := os.Open(filepath.Join(c.basePath, key)) 26 | if err != nil { 27 | if os.IsNotExist(err) { 28 | return nil, storage.ErrNotFound 29 | } 30 | return nil, fmt.Errorf("open file error: %w", err) 31 | } 32 | defer f.Close() 33 | return ioutil.ReadAll(f) 34 | } 35 | 36 | func (c Client) PutBytes(ctx context.Context, key string, data []byte) error { 37 | path := key[:strings.LastIndex(key, string(filepath.Separator))] 38 | err := os.MkdirAll(filepath.Join(c.basePath, path), 0755) 39 | if err != nil { 40 | return fmt.Errorf("mkdir error: %w", err) 41 | } 42 | f, err := os.OpenFile(filepath.Join(c.basePath, key), os.O_WRONLY|os.O_CREATE, 0755) 43 | if err != nil { 44 | return fmt.Errorf("open file error: %w", err) 45 | } 46 | defer f.Close() 47 | _, err = f.Write(data) 48 | if err != nil { 49 | return err 50 | } 51 | return nil 52 | } 53 | 54 | func (c Client) List(ctx context.Context, key string) ([]string, error) { 55 | ps, err := os.ReadDir(filepath.Join(c.basePath, key)) 56 | if err != nil { 57 | if os.IsNotExist(err) { 58 | return nil, storage.ErrNotFound 59 | } 60 | return nil, err 61 | } 62 | var out []string 63 | for _, p := range ps { 64 | out = append(out, p.Name()) 65 | } 66 | return out, nil 67 | } 68 | 69 | func (c Client) Delete(ctx context.Context, key string) error { 70 | return os.Remove(filepath.Join(c.basePath, key)) 71 | } 72 | -------------------------------------------------------------------------------- /pkg/storage/storage.go: -------------------------------------------------------------------------------- 1 | package storage 2 | 3 | import ( 4 | "context" 5 | "errors" 6 | ) 7 | 8 | var ( 9 | ErrNotFound = errors.New("not found") 10 | ) 11 | 12 | type Client interface { 13 | GetBytes(ctx context.Context, key string) ([]byte, error) 14 | PutBytes(ctx context.Context, key string, data []byte) error 15 | Delete(ctx context.Context, key string) error 16 | List(ctx context.Context, key string) ([]string, error) 17 | } 18 | -------------------------------------------------------------------------------- /scripts/build/report_build_info.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # WARNING: DO NOT EDIT, THIS FILE IS PROBABLY A COPY 4 | # 5 | # The original version of this file is located in the https://github.com/istio/common-files repo. 6 | # If you're looking at this file in a different repo and want to make a change, please go to the 7 | # common-files repo, make the change there and check it in. Then come back to this repo and run 8 | # "make update-common". 9 | 10 | # Copyright Istio Authors 11 | # 12 | # Licensed under the Apache License, Version 2.0 (the "License"); 13 | # you may not use this file except in compliance with the License. 14 | # You may obtain a copy of the License at 15 | # 16 | # http://www.apache.org/licenses/LICENSE-2.0 17 | # 18 | # Unless required by applicable law or agreed to in writing, software 19 | # distributed under the License is distributed on an "AS IS" BASIS, 20 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 21 | # See the License for the specific language governing permissions and 22 | # limitations under the License. 23 | APP_NAME=${1:?"app name"} 24 | 25 | if BUILD_GIT_REVISION=$(git rev-parse HEAD 2> /dev/null); then 26 | if [[ -n "$(git status --porcelain 2>/dev/null)" ]]; then 27 | BUILD_GIT_REVISION=${BUILD_GIT_REVISION}"-dirty" 28 | fi 29 | else 30 | BUILD_GIT_REVISION=unknown 31 | fi 32 | 33 | # Check for local changes 34 | if git diff-index --quiet HEAD --; then 35 | tree_status="Clean" 36 | else 37 | tree_status="Modified" 38 | fi 39 | 40 | # XXX This needs to be updated to accomodate tags added after building, rather than prior to builds 41 | RELEASE_TAG=$(git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --exact-match 2> /dev/null || echo "") 42 | 43 | # security wanted VERSION='unknown' 44 | VERSION="${BUILD_GIT_REVISION}" 45 | if [[ -n "${RELEASE_TAG}" ]]; then 46 | VERSION="${RELEASE_TAG}" 47 | fi 48 | 49 | GIT_DESCRIBE_TAG=$(git describe --match '[0-9]*\.[0-9]*\.[0-9]*' --exact-match 2> /dev/null || echo "") 50 | 51 | # used by common/scripts/gobuild.sh 52 | # used by scripts/build/gobuild.sh 53 | EGO_APP_PKG="github.com/gotomicro/ego/core/eapp" 54 | echo "${EGO_APP_PKG}.appName=${APP_NAME}" 55 | echo "${EGO_APP_PKG}.buildVersion=${VERSION}" 56 | echo "${EGO_APP_PKG}.buildAppVersion=${BUILD_GIT_REVISION}" 57 | echo "${EGO_APP_PKG}.buildStatus=${tree_status}" 58 | echo "${EGO_APP_PKG}.buildTag=${GIT_DESCRIBE_TAG}" 59 | echo "${EGO_APP_PKG}.buildUser=$(whoami)" 60 | echo "${EGO_APP_PKG}.buildHost=$(hostname -f)" 61 | echo "${EGO_APP_PKG}.buildTime=$(date '+%Y-%m-%d--%T')" 62 | 63 | 64 | -------------------------------------------------------------------------------- /ui/.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | charset = utf-8 9 | trim_trailing_whitespace = true 10 | insert_final_newline = true 11 | 12 | [*.md] 13 | trim_trailing_whitespace = false 14 | 15 | [Makefile] 16 | indent_style = tab 17 | -------------------------------------------------------------------------------- /ui/.eslintignore: -------------------------------------------------------------------------------- 1 | /lambda/ 2 | /scripts 3 | .history 4 | /config 5 | public 6 | dist 7 | .umi 8 | .umi-production 9 | .umi-test 10 | mock 11 | .eslintrc.js 12 | typings.d.ts 13 | -------------------------------------------------------------------------------- /ui/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: [require.resolve('@umijs/fabric/dist/eslint')], 3 | globals: { 4 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: true, 5 | page: true, 6 | REACT_APP_ENV: true, 7 | }, 8 | rules: { 9 | 'react-hooks/rules-of-hooks': 'error', // 检查 Hook 的规则 10 | 'react-hooks/exhaustive-deps': 'warn', // 检查 effect 的依赖 11 | 'no-empty': 'off', // catch{} 允许为空 12 | '@typescript-eslint/no-shadow': ['off'], // 当前作用域变量名不能与父级作用域变量同名 13 | }, 14 | }; 15 | -------------------------------------------------------------------------------- /ui/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | **/node_modules 5 | # roadhog-api-doc ignore 6 | /src/utils/request-temp.js 7 | _roadhog-api-doc 8 | 9 | # production 10 | /dist 11 | /dist_electron 12 | /package 13 | 14 | # misc 15 | .DS_Store 16 | npm-debug.log* 17 | yarn-error.log 18 | 19 | /coverage 20 | .idea 21 | package-lock.json 22 | pnpm-lock.yaml 23 | *bak 24 | 25 | 26 | # visual studio code 27 | .history 28 | *.log 29 | functions/* 30 | .temp/** 31 | 32 | # umi 33 | .umi 34 | .umi-production 35 | 36 | # screenshot 37 | screenshot 38 | .firebase 39 | .eslintcache 40 | 41 | build 42 | 43 | server 44 | -------------------------------------------------------------------------------- /ui/.husky/.gitignore: -------------------------------------------------------------------------------- 1 | _ 2 | -------------------------------------------------------------------------------- /ui/.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo 'commit-msg hook disabled' 3 | #. "$(dirname "$0")/_/husky.sh" 4 | # 5 | ## Export Git hook params 6 | #export GIT_PARAMS=$* 7 | # 8 | #npx --no-install fabric verify-commit 9 | -------------------------------------------------------------------------------- /ui/.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | echo 'pre-commit disabled' 3 | #. "$(dirname "$0")/_/husky.sh" 4 | # 5 | #npx --no-install lint-staged 6 | -------------------------------------------------------------------------------- /ui/.npmrc: -------------------------------------------------------------------------------- 1 | electron_mirror=https://npm.taobao.org/mirrors/electron/ 2 | phantomjs_cdnurl=http://cdn.npm.taobao.org/dist/phantomjs 3 | -------------------------------------------------------------------------------- /ui/.prettierignore: -------------------------------------------------------------------------------- 1 | **/*.svg 2 | package.json 3 | .umi 4 | .umi-production 5 | /dist 6 | .dockerignore 7 | .DS_Store 8 | .eslintignore 9 | *.png 10 | *.toml 11 | docker 12 | .editorconfig 13 | Dockerfile* 14 | .gitignore 15 | .prettierignore 16 | LICENSE 17 | .eslintcache 18 | *.lock 19 | yarn-error.log 20 | .history 21 | CNAME 22 | /build 23 | /public -------------------------------------------------------------------------------- /ui/.prettierrc.js: -------------------------------------------------------------------------------- 1 | const fabric = require('@umijs/fabric'); 2 | 3 | module.exports = { 4 | ...fabric.prettier, 5 | }; 6 | -------------------------------------------------------------------------------- /ui/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["esbenp.prettier-vscode", "dbaeumer.vscode-eslint", "wangzy.sneak-mark"] 3 | } 4 | -------------------------------------------------------------------------------- /ui/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.formatOnSave": true, 3 | "prettier.requireConfig": true, 4 | "editor.defaultFormatter": "esbenp.prettier-vscode" 5 | } 6 | -------------------------------------------------------------------------------- /ui/README.md: -------------------------------------------------------------------------------- 1 | # K8Z 2 | 3 | ## Install node_modules 4 | 5 | Install `node_modules`: 6 | 7 | ```bash 8 | yarn 9 | ``` 10 | 11 | ### Start project 12 | 13 | ```bash 14 | local: yarn start # 本地启动,用于开发使用 15 | dev: yarn start:dev || yarn dev # dev 环境启动 16 | pro: yarn start:pro || yarn pro # pro 环境启动 17 | ``` 18 | 19 | ### Start project by electron 20 | 21 | ```bash 22 | local: yarn estart # 本地启动,用于开发使用 23 | dev: yarn estart:dev # dev 环境启动 24 | pro: yarn estart:pro # pro 环境启动 25 | ``` 26 | 27 | ### Build project 28 | 29 | ```bash 30 | yarn build # dev 环境 31 | yarn build:pro # pro 环境打包 32 | ``` 33 | 34 | ### Build electron project 35 | 36 | ```bash 37 | # dev 环境 38 | yarn build:win 39 | yarn build:mac 40 | yarn run build:linux 41 | 42 | # pro 环境 43 | yarn build:win:pro 44 | yarn build:mac:pro 45 | yarn build:linux:pro 46 | ``` 47 | 48 | ### Check code style 49 | 50 | ```bash 51 | yarn run lint 52 | ``` 53 | 54 | ```bash 55 | yarn run lint:fix 56 | ``` 57 | 58 | ### Test code 59 | 60 | ```bash 61 | yarn test 62 | ``` 63 | -------------------------------------------------------------------------------- /ui/config/defaultSettings.ts: -------------------------------------------------------------------------------- 1 | import { Settings as LayoutSettings } from '@ant-design/pro-components'; 2 | 3 | const Settings: LayoutSettings & { 4 | pwa?: boolean; 5 | logo?: string; 6 | } = { 7 | navTheme: 'light', 8 | layout: 'top', 9 | contentWidth: 'Fixed', 10 | fixedHeader: true, 11 | fixSiderbar: true, 12 | footerRender: false, 13 | headerRender: false, 14 | menuRender: false, 15 | menuHeaderRender: false, 16 | menu: { 17 | locale: false, 18 | }, 19 | pwa: false, 20 | logo: '/logo.svg', 21 | splitMenus: false, 22 | title: 'k8z', 23 | }; 24 | 25 | export default Settings; 26 | -------------------------------------------------------------------------------- /ui/config/proxy.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 在生产环境 代理是无法生效的,所以这里没有生产环境的配置 3 | * ------------------------------- 4 | * The agent cannot take effect in the production environment 5 | * so there is no configuration of the production environment 6 | * For details, please see 7 | * https://pro.ant.design/docs/deploy 8 | */ 9 | export default { 10 | dev: { 11 | '/api/v1': { 12 | // 要代理的地址 13 | target: 'http://127.0.0.1:9001', 14 | // 配置了这个可以从 http 代理到 https 15 | // 依赖 origin 的功能可能需要这个,比如 cookie 16 | changeOrigin: true, 17 | }, 18 | }, 19 | test: { 20 | '/api/v1': { 21 | target: 'http://127.0.0.1:9001', 22 | changeOrigin: true, 23 | pathRewrite: { '^': '' }, 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /ui/config/routes.ts: -------------------------------------------------------------------------------- 1 | export default [ 2 | { 3 | path: '/', 4 | layout: false, 5 | component: '@/layouts/index', 6 | routes: [ 7 | { 8 | path: '/', 9 | name: '首页', 10 | component: '@/pages/index', 11 | }, 12 | { 13 | path: '/:tools', 14 | name: '工具', 15 | component: '@/pages/Tools', 16 | }, 17 | { path: '/manage/cluster', component: '@/pages/ClustersManage' }, 18 | ], 19 | }, 20 | { path: '/init', layout: false, component: '@/pages/AppInit' }, 21 | { 22 | path: '*', 23 | layout: false, 24 | component: '@/pages/404', 25 | }, 26 | ]; 27 | -------------------------------------------------------------------------------- /ui/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | testURL: 'http://localhost:8000', 3 | verbose: false, 4 | extraSetupFiles: ['./tests/setupTests.js'], 5 | globals: { 6 | ANT_DESIGN_PRO_ONLY_DO_NOT_USE_IN_YOUR_PRODUCTION: false, 7 | localStorage: null, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /ui/jsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "jsx": "react-jsx", 4 | "emitDecoratorMetadata": true, 5 | "experimentalDecorators": true, 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["./src/*"] 9 | } 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /ui/mock/api.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | 'GET /api/v1/k8s/clusters': { 3 | code: 0, 4 | msg: '', 5 | data: [ 6 | { 7 | name: 'k8s1', 8 | }, 9 | ], 10 | }, 11 | '/api/v1/k8s/:cluster/namespaces': { 12 | code: 0, 13 | msg: '', 14 | data: [ 15 | { 16 | name: 'default', 17 | }, 18 | { 19 | name: 'staging', 20 | }, 21 | ], 22 | }, 23 | 24 | 'GET /api/v1/k8s/:cluster/:namespace/workloads': { 25 | code: 0, 26 | msg: '', 27 | data: [ 28 | { 29 | name: 'ab-test', 30 | kind: 'deployment', 31 | }, 32 | ], 33 | }, 34 | 35 | 'GET /api/v1/k8s/:cluster/:namespace/:workloadKind/:workload/pods': { 36 | code: 0, 37 | msg: '', 38 | data: [ 39 | { 40 | name: 'test-xxx-111', 41 | }, 42 | ], 43 | }, 44 | 'GET /api/v1/pprof/profile-list': { 45 | code: 0, 46 | msg: '成功', 47 | data: [ 48 | { 49 | url: 'mdp-69949797f6-lww25_1662371785086', 50 | podName: 'mdp-69949797f6-lww25', 51 | ctime: 1662371785, 52 | }, 53 | { 54 | url: 'mdp-69949797f6-lww25_1662372117500', 55 | podName: 'mdp-69949797f6-lww25', 56 | ctime: 1662372117, 57 | }, 58 | ], 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /ui/playwright.config.ts: -------------------------------------------------------------------------------- 1 | // playwright.config.ts 2 | import type { PlaywrightTestConfig } from '@playwright/test'; 3 | import { devices } from '@playwright/test'; 4 | 5 | const config: PlaywrightTestConfig = { 6 | forbidOnly: !!process.env.CI, 7 | retries: process.env.CI ? 2 : 0, 8 | use: { 9 | trace: 'on-first-retry', 10 | }, 11 | projects: [ 12 | { 13 | name: 'chromium', 14 | use: { ...devices['Desktop Chrome'] }, 15 | }, 16 | { 17 | name: 'firefox', 18 | use: { ...devices['Desktop Firefox'] }, 19 | }, 20 | ], 21 | }; 22 | export default config; 23 | -------------------------------------------------------------------------------- /ui/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/favicon.ico -------------------------------------------------------------------------------- /ui/public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icon.png -------------------------------------------------------------------------------- /ui/public/icons/1024x1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/1024x1024.png -------------------------------------------------------------------------------- /ui/public/icons/128x128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/128x128.png -------------------------------------------------------------------------------- /ui/public/icons/16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/16x16.png -------------------------------------------------------------------------------- /ui/public/icons/24x24.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/24x24.png -------------------------------------------------------------------------------- /ui/public/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/256x256.png -------------------------------------------------------------------------------- /ui/public/icons/32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/32x32.png -------------------------------------------------------------------------------- /ui/public/icons/48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/48x48.png -------------------------------------------------------------------------------- /ui/public/icons/512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/512x512.png -------------------------------------------------------------------------------- /ui/public/icons/64x64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/64x64.png -------------------------------------------------------------------------------- /ui/public/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/icon.icns -------------------------------------------------------------------------------- /ui/public/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/gotomicro/k8z/b97c5a690fbd332b066210a875c1d00361542a7f/ui/public/icons/icon.ico -------------------------------------------------------------------------------- /ui/src/app.tsx: -------------------------------------------------------------------------------- 1 | import Footer from '@/components/Footer'; 2 | import { handleElectronMessage, RequestBaseUrl } from '@/utils/electronRenderUtil'; 3 | import type { Settings as LayoutSettings } from '@ant-design/pro-components'; 4 | import { loader } from '@monaco-editor/react'; 5 | import type { RunTimeLayoutConfig } from '@umijs/max'; 6 | import { history } from '@umijs/max'; 7 | import { notification } from 'antd'; 8 | import type { RequestConfig } from 'umi'; 9 | import defaultSettings from '../config/defaultSettings'; 10 | 11 | // 初始化 monaco 12 | loader 13 | .init() 14 | .then(() => { 15 | console.log('monaco init success'); 16 | }) 17 | .catch((e) => { 18 | console.error('error: ', e); 19 | notification.error({ 20 | message: '依赖加载错误', 21 | description: 'Monaco-editor 依赖加载错误,请检查网络后重启项目,否则将影响编辑器的使用。', 22 | duration: null, 23 | key: 'error-init-monaco-editor', 24 | }); 25 | }); 26 | 27 | const loginPath = '/user/login'; 28 | 29 | // 处理 electron 传递过来的消息 30 | window.electron?.getMessage?.(handleElectronMessage); 31 | 32 | /** 33 | * @see https://v3.umijs.org/zh-CN/plugins/plugin-request 34 | */ 35 | export const request: RequestConfig = { 36 | // @ts-ignore 37 | baseURL: RequestBaseUrl, 38 | }; 39 | 40 | /** 41 | * @see https://umijs.org/zh-CN/plugins/plugin-initial-state 42 | * */ 43 | export async function getInitialState(): Promise<{ 44 | settings?: Partial; 45 | loading?: boolean; 46 | // 判断运行环境是否是在 Electron 47 | isElectron?: boolean; 48 | }> { 49 | const userAgent = navigator.userAgent.toLowerCase(); 50 | // 如果不是登录页面,执行 51 | if (history.location.pathname !== loginPath) { 52 | return { 53 | settings: defaultSettings, 54 | isElectron: userAgent.indexOf(' electron/') > -1, 55 | }; 56 | } 57 | return { 58 | settings: defaultSettings, 59 | isElectron: userAgent.indexOf(' electron/') > -1, 60 | }; 61 | } 62 | 63 | // ProLayout 支持的api https://procomponents.ant.design/components/layout 64 | export const layout: RunTimeLayoutConfig = ({ initialState }) => { 65 | return { 66 | disableContentMargin: false, 67 | footerRender: () =>