diff --git a/package-lock.json b/package-lock.json index 0d335d24c..665ea1ec9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "kore-board", - "version": "0.3.3", + "version": "0.4.0", "lockfileVersion": 1, "requires": true, "packages": { @@ -25538,6 +25538,15 @@ "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", "dev": true }, + "ansi-to-html": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/ansi-to-html/-/ansi-to-html-0.7.2.tgz", + "integrity": "sha512-v6MqmEpNlxF+POuyhKkidusCHWWkaLcGRURzivcU3I9tv7k4JVhFcnukrM5Rlk2rUywdZuzYAZ+kbZqWCnfN3g==", + "dev": true, + "requires": { + "entities": "^2.2.0" + } + }, "ansi-wrap": { "version": "0.1.0", "resolved": "https://registry.npmjs.org/ansi-wrap/-/ansi-wrap-0.1.0.tgz", @@ -27520,17 +27529,6 @@ "integrity": "sha512-FxqpkPPwu1HjuN93Omfm4h8uIanXofW0RxVEW3k5RKx+mJJYSthzNhp32Kzxxy3YAEZ/Dc/EWN1vZRY0+kOhbw==", "dev": true }, - "clipboard": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/clipboard/-/clipboard-2.0.8.tgz", - "integrity": "sha512-Y6WO0unAIQp5bLmk1zdThRhgJt/x3ks6f30s3oE3H1mgIEU33XyQjEf8gsf6DxC7NPX8Y1SsNWjUjL/ywLnnbQ==", - "dev": true, - "requires": { - "good-listener": "^1.2.2", - "select": "^1.1.2", - "tiny-emitter": "^2.0.0" - } - }, "cliui": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/cliui/-/cliui-5.0.0.tgz", @@ -28812,12 +28810,6 @@ "integrity": "sha512-8UWj5lNv7HD+kB0e9w77Z7TdQlbUYDVWqITLHNqFIn6khrNHv5WQo38Dcm1f6HeNyZf0U7UbPf6WeZDSdCzGDQ==", "dev": true }, - "delegate": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/delegate/-/delegate-3.2.0.tgz", - "integrity": "sha512-IofjkYBZaZivn0V8nnsMJGBr4jVLxHDheKSW88PyxS5QC4Vo9ZbZVvhzlSxY87fVq3STR6r+4cGepyHkcWOQSw==", - "dev": true - }, "depd": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", @@ -31274,15 +31266,6 @@ "sparkles": "^1.0.0" } }, - "good-listener": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/good-listener/-/good-listener-1.2.2.tgz", - "integrity": "sha1-1TswzfkxPf+33JoNR3CWqm0UXFA=", - "dev": true, - "requires": { - "delegate": "^3.1.2" - } - }, "graceful-fs": { "version": "4.2.6", "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.6.tgz", @@ -37267,12 +37250,6 @@ "integrity": "sha512-1j2RlmUNADEprCkzDaeo8w2tdum/mvQWAKdRaS2raud7IOnPaDbLSFKhcY5xXPbAFYWk4ZQ0BUnfmg0ZUcI+Pg==", "dev": true }, - "select": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", - "integrity": "sha1-DnNQrN7ICxEIUoeG7B1EGNEbOW0=", - "dev": true - }, "select2": { "version": "4.0.13", "resolved": "https://registry.npmjs.org/select2/-/select2-4.0.13.tgz", @@ -37780,6 +37757,12 @@ "extend-shallow": "^3.0.0" } }, + "splitpanes": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/splitpanes/-/splitpanes-2.3.8.tgz", + "integrity": "sha512-eM/qZ1v7U5BMV8FQR7oeqVlllz3sTGTm0//g/eJMa0hZ4s+A1VK68j26FWzcaVlw2P5+dCXk7/X6ZRjjwcbrgw==", + "dev": true + }, "sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -38547,12 +38530,6 @@ "integrity": "sha1-QFQRqOfmM5/mTbmiNN4R3DHgK9Q=", "dev": true }, - "tiny-emitter": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", - "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", - "dev": true - }, "tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", diff --git a/package.json b/package.json index afa0fddd6..58f0f5d1b 100644 --- a/package.json +++ b/package.json @@ -56,6 +56,8 @@ "xterm": "^4.12.0", "xterm-addon-fit": "^0.5.0", "xterm-addon-web-links": "^0.4.0", + "splitpanes": "^2.3.8", + "ansi-to-html": "^0.7.2", "jsonpath": "^1.1.1" }, "engines": { @@ -63,4 +65,4 @@ "npm": ">=6.0.0" }, "dependencies": {} -} \ No newline at end of file +} diff --git a/src/app/backend/model/node.go b/src/app/backend/model/node.go index b5719a23e..c9779b23f 100644 --- a/src/app/backend/model/node.go +++ b/src/app/backend/model/node.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "strings" + "time" "github.com/kore3lab/dashboard/pkg/config" "github.com/kore3lab/dashboard/pkg/lang" @@ -76,14 +77,17 @@ func GetNodeListWithUsage(cluster string) (interface{}, error) { return nil, err } + //timeout 5s + timeout := int64(5) + // node-list - nodeList, err := apiClient.CoreV1().Nodes().List(context.TODO(), metaV1.ListOptions{}) + nodeList, err := apiClient.CoreV1().Nodes().List(context.TODO(), metaV1.ListOptions{TimeoutSeconds: &timeout}) if err != nil { return nil, err } // self.Workloads.Pods (노드별 파드 수 & running 파드 수) - podList, err := apiClient.CoreV1().Pods("").List(context.TODO(), metaV1.ListOptions{}) + podList, err := apiClient.CoreV1().Pods("").List(context.TODO(), metaV1.ListOptions{TimeoutSeconds: &timeout}) if err != nil { return nil, err } @@ -96,12 +100,13 @@ func GetNodeListWithUsage(cluster string) (interface{}, error) { nodes := map[string]*NodeWithMetrics{} summary := NodeMetricsUsage{} + d := time.Duration(timeout) * time.Second + nodeSummary := ProxyNodeSummary{} for _, m := range nodeList.Items { // node summary for storage used percentage (/api/v1/nodes//proxy/stats/summary) - nodeSummary := ProxyNodeSummary{} - request := apiClient.CoreV1().RESTClient().Get().Resource("nodes").Name(m.Name).SubResource("proxy").Suffix("stats/summary") + request := apiClient.CoreV1().RESTClient().Get().Resource("nodes").Name(m.Name).SubResource("proxy").Suffix("stats/summary").Timeout(d) responseRawArrayOfBytes, err := request.DoRaw(context.Background()) if err != nil { log.Warnf("Unable to get %s/proxy/stats/summary (cause=%v)", m, err) diff --git a/src/app/backend/pkg/app/ack.go b/src/app/backend/pkg/app/ack.go index ae9da1082..a168170ca 100644 --- a/src/app/backend/pkg/app/ack.go +++ b/src/app/backend/pkg/app/ack.go @@ -6,6 +6,7 @@ import ( "fmt" log "github.com/sirupsen/logrus" "net/http" + "net/url" "github.com/astaxie/beego/validation" "github.com/gin-gonic/gin" @@ -82,3 +83,17 @@ func (g *Gin) ValidateUrl(params []string) error { return nil } + +// parse querystrings +func (g *Gin) ParseQuery() (url.Values, error) { + + u, err := url.Parse(g.C.Request.RequestURI) + if err != nil { + return nil, err + } + query, err := url.ParseQuery(u.RawQuery) + if err != nil { + return nil, err + } + return query, nil +} diff --git a/src/app/backend/router/apis/raw.go b/src/app/backend/router/apis/raw.go index 8d539410f..16a88973d 100644 --- a/src/app/backend/router/apis/raw.go +++ b/src/app/backend/router/apis/raw.go @@ -7,13 +7,18 @@ package apis */ import ( + "context" + "io" "net/http" - "net/url" + "strconv" "strings" + "time" "github.com/gin-gonic/gin" "github.com/kore3lab/dashboard/pkg/app" "github.com/kore3lab/dashboard/pkg/config" + log "github.com/sirupsen/logrus" + coreV1 "k8s.io/api/core/v1" "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/apimachinery/pkg/types" ) @@ -113,8 +118,7 @@ func GetRaw(c *gin.Context) { var err error ListOptions := v1.ListOptions{} - u, _ := url.Parse(c.Request.RequestURI) - query, _ := url.ParseQuery(u.RawQuery) + query, _ := g.ParseQuery() err = v1.Convert_url_Values_To_v1_ListOptions(&query, &ListOptions, nil) if err != nil { @@ -202,3 +206,113 @@ func PatchRaw(c *gin.Context) { g.Send(http.StatusOK, r) } + +// Get Pod logs +func GetPodLogs(c *gin.Context) { + g := app.Gin{C: c} + + var err error + + // url parameter validation + v := []string{"NAMESPACE", "NAME"} + if err := g.ValidateUrl(v); err != nil { + g.SendMessage(http.StatusBadRequest, err.Error(), err) + return + } + + // instancing dynamic client + client, err := config.Cluster.Client(g.C.Param("CLUSTER")) + if err != nil { + g.SendMessage(http.StatusBadRequest, err.Error(), err) + return + } + + apiClient, err := client.NewKubernetesClient() + if err != nil { + g.SendMessage(http.StatusBadRequest, err.Error(), err) + return + } + + // log options (with querystring) + options := coreV1.PodLogOptions{} + var limitLines = int64(300) + query, err := g.ParseQuery() + if err == nil { + if len(query) > 0 { + if query["tailLines"] != nil { + var num1, err1 = strconv.Atoi(query["tailLines"][0]) + if err1 != nil { + g.SendMessage(http.StatusBadRequest, err.Error(), err) + return + } + limitLines = int64(num1) + } + options.TailLines = &limitLines + + if query["sinceTime"] != nil { + var timestamp v1.Time + timestamp.UnmarshalQueryParameter(query["sinceTime"][0]) + options.SinceTime = ×tamp + } + + if query["container"] != nil { + options.Container = query["container"][0] + } + if query["follow"] != nil { + options.Follow, _ = strconv.ParseBool(query["follow"][0]) + } + if query["previous"] != nil { + options.Previous, _ = strconv.ParseBool(query["previous"][0]) + } + if query["timestamps"] != nil { + options.Timestamps, _ = strconv.ParseBool(query["timestamps"][0]) + } + } + } + + // get a log stream + req := apiClient.CoreV1().Pods(g.C.Param("NAMESPACE")).GetLogs(g.C.Param("NAME"), &options) + stream, err := req.Stream(context.TODO()) + if err != nil { + g.SendMessage(http.StatusBadRequest, err.Error(), err) + return + } + defer stream.Close() + + // read a stream go-routine + chanStream := make(chan []byte, 10) + go func() { + defer close(chanStream) + + for { + buf := make([]byte, 4096) + numBytes, err := stream.Read(buf) + + if err != nil { + if err != io.EOF { + log.Infof("finished log streaming (cause=%s)", err.Error()) + return + } else { + if options.Follow == false { + log.Debug("log stream is EOF") + break + } else { + time.Sleep(time.Second * 1) + } + } + } else { + chanStream <- buf[:numBytes] + } + } + }() + + // write stream to client + g.C.Stream(func(w io.Writer) bool { + if data, ok := <-chanStream; ok { + w.Write(data) + return true + } + return false + }) + +} diff --git a/src/app/backend/router/router.go b/src/app/backend/router/router.go index ba67d2030..43382a25d 100644 --- a/src/app/backend/router/router.go +++ b/src/app/backend/router/router.go @@ -98,6 +98,7 @@ func CreateUrlMappings() { rawAPI.GET("/:A/:B/:RESOURCE/:NAME", apis.GetRaw) // "/namespaces/:NAMESPACE/:RESOURCE/:NAME" > namespaced core apiGroup - get rawAPI.DELETE("/:A/:B/:RESOURCE/:NAME", apis.DeleteRaw) // "/namespaces/:NAMESPACE/:RESOURCE/:NAME" > namespaced core apiGroup - delete rawAPI.PATCH("/:A/:B/:RESOURCE/:NAME", apis.PatchRaw) // "/namespaces/:NAMESPACE/:RESOURCE/:NAME" > namespaced core apiGroup - patch + rawAPI.GET("/:A/:B/:RESOURCE/:NAME/log", apis.GetPodLogs) // "/namespaces/:NAMESPACE/pods/:NAME/log" > get a pod logs } // RAW-API Grouped diff --git a/src/app/frontend/assets/css/app.css b/src/app/frontend/assets/css/app.css index dc0ef4160..0fe652a46 100644 --- a/src/app/frontend/assets/css/app.css +++ b/src/app/frontend/assets/css/app.css @@ -1,68 +1,76 @@ -/* layout */ -.main-sidebar, .main-sidebar::before { width: 17rem;} -.sidebar-collapse .main-sidebar, .sidebar-collapse .main-sidebar::before { margin-left: -17rem; } -.sidebar {margin-left: 3.7rem; padding: 0; padding-left: .1rem;} -.sidebar::-webkit-scrollbar{ display:none;} -.brand-link { margin-left: 3.7rem;} -.layout-fixed .brand-link { width: 13.2rem;} -.main-footer {padding: .5rem 1rem; height: 2.5rem} -aside a.brand-link img {width: 1.7em; height:1.7em;} -#aside-contexts {width:3.7rem; position:absolute;min-height:100vh; padding: 1.5rem .3rem; } -#aside-contexts > div { margin: .5rem .2rem;} -#aside-contexts button.active { border-color: #fd7e14; background-color: rgb(255, 193, 7)} -#aside-contexts p { font-size:x-small; margin: 0;} -body:not(.layout-top-nav) .content-wrapper {margin-left:0} -.wrapper .content-wrapper { min-height: calc(100vh - calc(3.5rem + 1px) - calc(2.5rem + 1px));} -.content-header { padding: .5rem .5rem .2rem .5rem;} -label:not(.form-check-label):not(.custom-file-label) {font-weight: 400;} -.brand-link .brand-image {margin-left: .5rem} - -@media (max-width: 992px) { - body:not(.sidebar-open) .main-sidebar, .main-sidebar::before { margin-left: -17rem;} -} -@media (min-width: 1024px) { - body:not(.sidebar-mini-md) .content-wrapper, body:not(.sidebar-mini-md) .main-footer, body:not(.sidebar-mini-md) .main-header { - margin-left: 17rem; - } - .sidebar-mini.sidebar-collapse .content-wrapper, .sidebar-mini.sidebar-collapse .main-footer, .sidebar-mini.sidebar-collapse .main-header { - margin-left: 7.4rem !important; - } - .sidebar-mini.sidebar-collapse .main-sidebar, .sidebar-mini.sidebar-collapse .main-sidebar::before { - width: 7.4rem; - } +/* layout - default */ +:root { + --layout-header-height: 3.5rem; + --layout-footer-height: 2.5rem; + --layout-aside-width: 17rem; + --layout-aside-width-md: 7.4rem; + --bg-color: #f4f6f9; + --border-color: #e9ecef; } - -/* custom */ -.lh-vh-50 {line-height: 50vh;} .lh-vh-100 {line-height: 100vh;} -.h-chart { height: 14em;} -.cursor {cursor:pointer} -.label { - display: inline-grid; - border:1px #73b5fc solid; - background-color: #eff6fd; - padding: 1px 4px; - line-height: 1.3rem; - border-radius: .25em!important; - margin: .1em .2em .1em 0em; - color: #0363ca; +/* layout - default */ +body.sidebar-mini {overflow:hidden;} +body.sidebar-mini .main-sidebar { width: var(--layout-aside-width);} +body.sidebar-mini .main-header { position: fixed; top:0; width:100%; } +body.sidebar-mini .main-footer { margin:0; padding: .5rem 1rem .5rem calc(var(--layout-aside-width) + 1rem); min-height: var(--layout-footer-height); position: fixed; bottom:0; width:100%; z-index: 10} +body.sidebar-mini .main-body { padding: var(--layout-header-height) 0rem var(--layout-footer-height) var(--layout-aside-width) } +/* layout - sidebar > brand */ +body.sidebar-mini .main-sidebar .brand-link { margin-left: 3.7rem; width: 13.2rem;} +body.sidebar-mini .main-sidebar .brand-link img.brand-image {width: 1.7em; height:1.7em;margin-left: .5rem} +/* layout - sidebar > contexts */ +body.sidebar-mini .main-sidebar .aside-contexts {width:3.7rem; position:absolute;min-height:100vh; padding: 1.5rem .3rem; } +body.sidebar-mini .main-sidebar .aside-contexts > div { margin: .5rem .2rem;} +body.sidebar-mini .main-sidebar .aside-contexts button.current { border-color: #fd7e14; background-color: rgb(255, 193, 7)} +body.sidebar-mini .main-sidebar .aside-contexts p { font-size:x-small; margin: 0;} +/* layout - sidebar > menu */ +body.sidebar-mini .main-sidebar .aside-menus {margin-left: 3.7rem; padding: 0; padding-left: .1rem;} +body.sidebar-mini .main-sidebar .aside-menus::-webkit-scrollbar{ display:none;} +/* layout - body > splitpanes */ +body.sidebar-mini .main-body .splitpanes__pane.body-pane { overflow-y: auto; background-color: var(--bg-color); } +body.sidebar-mini .main-body:not(.terminal-closed) .splitpanes__splitter { background-color:transparent; height:.3rem; border-top: .1rem solid #343a40; background-color: var(--bg-color)} +body.sidebar-mini .main-body.terminal-closed .splitpanes__splitter { display: none; } +body.sidebar-mini .main-body .splitpanes__pane.terminal-pane > i { float:right; line-height:1.6rem; padding: .2rem .5rem .2rem .5rem; border-left: 1px dashed #ccc; background-color:#fff; } +/* layout - body > list-page > header */ +body.sidebar-mini .main-body .content-header { padding: .5rem .5rem .2rem .5rem;} +body.sidebar-mini .main-body .content-wrapper { margin-left: 0!important; min-height: 100% !important;} +/* layout - sidebar @media style + min-width: 992px (992px 이상) + default sidebar : visible 상태, class = body:not(.sidebar-collapse)) + 햄버거 메뉴 click : sidebar 축소, class = body.sidebar-collapse) + sidebar width : 17rem + max-width: 992px (992px 미만) + default sidebar : hidden 상태, class = body.sidebar-closed + 햄버거 메뉴 click : sidebar 오픈, class = body.sidebar-open +*/ +@media (min-width: 992px) { + body.sidebar-mini:not(.sidebar-collapse) .main-header {padding-left: var(--layout-aside-width); margin-left: 0 ;} + body.sidebar-mini:not(.sidebar-collapse) .main-body { padding-left: var(--layout-aside-width); } + body.sidebar-mini.sidebar-collapse .main-header { padding-left: var(--layout-aside-width-md); margin-left: 0rem !important; } + body.sidebar-mini.sidebar-collapse .main-sidebar {width: var(--layout-aside-width-md) } + body.sidebar-mini.sidebar-collapse .main-body { padding-left: var(--layout-aside-width-md); } + body.sidebar-mini.sidebar-collapse .main-footer { margin-left: 0!important; padding-left: calc(var(--layout-aside-width-md) + 1rem); } } -.border-box { - display: inline-grid; - border: 1px #6c757d dotted; - line-height: 1.3rem; - padding: 0 .4em; - border-radius: .25em!important; - margin: .1em .2em .1em 0em; - color: #6c757d; -} -.border-box.background { - background-color: #f3f4f5; +@media (max-width: 992px) { + body.sidebar-mini .main-body { padding-left: 0 } + body.sidebar-mini:not(.sidebar-open) .main-sidebar { margin-left: calc(var(--layout-aside-width) * -1) } + body.sidebar-mini .main-footer { padding-left: 1rem; } } - -/* table of viewpage*/ +/* override */ +body.sidebar-mini .main-body label:not(.form-check-label):not(.custom-file-label) {font-weight: 400;} +/* override > vue-boostrap */ +table.b-table > tbody > .table-active, .table.b-table > tbody > .table-active > th, .table.b-table > tbody > .table-active > td {background-color: rgba(0, 0, 0, 0.03);} +table.b-table tr:focus { outline: none; } +table.b-table.table-sm {margin: 0;} +table.b-table.table-sm tbody > tr {line-height: 1rem; margin-bottom: 0;} table.b-table.subset th {font-weight:400; padding: .3rem} table.b-table.subset td {font-weight:350; padding: .3rem} - +/* custom */ +.lh-vh-50 {line-height: 50vh;} +.lh-vh-100 {line-height: 100vh;} +.h-chart { height: 14em;} +.cursor {cursor:pointer} +.label {display: inline-grid;border:1px #73b5fc solid;background-color: #eff6fd;padding: 1px 4px;line-height: 1.3rem;border-radius: .25em!important;margin: .1em .2em .1em 0em;color: #0363ca;} +.border-box {display: inline-grid;border: 1px #6c757d dotted;line-height: 1.3rem;padding: 0 .4em;border-radius: .25em!important;margin: .1em .2em .1em 0em;color: #6c757d;} +.border-box.background {background-color: #f3f4f5;} /* custom > view */ section.content-view {font-size: .95rem;} section.content-view .card-primary > .card-header {font-size: 1.1rem; line-height: 1.1rem;} @@ -77,10 +85,3 @@ section.content-view .card-body.group > ul { padding: 0; margin:0; list-style: n section.content-view .card-body.group > ul > li { margin-top: .5em } section.content-view .card-body.group > ul > li .title { margin: .3rem .3rem; font-weight:400; font-size:1rem } section.content-view .card-body.group > ul > li > dl { border: 1px solid #dee2e6!important; margin: 0 0;} - - -/* custom -vue-boostrap */ -table.b-table > tbody > .table-active, .table.b-table > tbody > .table-active > th, .table.b-table > tbody > .table-active > td {background-color: rgba(0, 0, 0, 0.03);} -table.b-table tr:focus { outline: none; } -table.b-table.table-sm {margin: 0;} -table.b-table.table-sm tbody > tr {line-height: 1rem; margin-bottom: 0;} diff --git a/src/app/frontend/components/terminal/terminal-logs.vue b/src/app/frontend/components/terminal/terminal-logs.vue new file mode 100644 index 000000000..08c2bff3d --- /dev/null +++ b/src/app/frontend/components/terminal/terminal-logs.vue @@ -0,0 +1,160 @@ + + + \ No newline at end of file diff --git a/src/app/frontend/layouts/components/aside.vue b/src/app/frontend/layouts/components/aside.vue index 3c774a91f..888cd4408 100644 --- a/src/app/frontend/layouts/components/aside.vue +++ b/src/app/frontend/layouts/components/aside.vue @@ -1,7 +1,7 @@