From 5987cd46fca77339d440974785ff51ed5f6c822c Mon Sep 17 00:00:00 2001 From: Shu Muto Date: Tue, 7 May 2019 17:23:04 +0900 Subject: [PATCH] Enable to switch context on kubeconfig When user is logged in with kubeconfig, add selections for contexts included in kubeconfig into user menu and enable to switch contexts included in kubeconfig. This commit implements basic functionalities for switching contexts, so to set dashboard-metrics-scraper host for each `cluster`, you need to describe it as `sidecarHost` in `cluster` directive in kubeconfig. Note: To test this with `npm run start*`, enlarge maximum http header size with `export NODE_OPTIONS=--max-http-header-size=102400`. --- aio/develop/run-npm-on-container.sh | 4 + i18n/de/messages.de.xlf | 4 +- i18n/fr/messages.fr.xlf | 4 +- i18n/ja/messages.ja.xlf | 4 +- i18n/ko/messages.ko.xlf | 4 +- i18n/messages.xlf | 4 +- i18n/zh-Hans/messages.zh-Hans.xlf | 4 +- i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf | 4 +- i18n/zh-Hant/messages.zh-Hant.xlf | 4 +- package-lock.json | 16 +- package.json | 2 + src/app/backend/auth/api/types.go | 17 +- src/app/backend/auth/basic.go | 10 +- src/app/backend/auth/handler.go | 34 +- src/app/backend/auth/kubeconfig.go | 73 ++-- src/app/backend/auth/kubeconfig_test.go | 12 +- src/app/backend/auth/manager.go | 21 +- src/app/backend/auth/manager_test.go | 4 +- src/app/backend/auth/token.go | 8 +- src/app/backend/client/api/types.go | 2 +- src/app/backend/client/manager.go | 35 +- src/app/backend/handler/apihandler.go | 44 +-- src/app/backend/integration/metric/manager.go | 21 +- .../integration/metric/manager_test.go | 25 +- src/app/backend/plugin/config_test.go | 2 +- .../frontend/chrome/userpanel/component.ts | 16 +- .../frontend/chrome/userpanel/template.html | 7 +- .../common/services/global/authentication.ts | 63 +++- .../common/services/global/kubeconfig.ts | 337 ++++++++++++++++++ .../frontend/common/services/global/module.ts | 2 + src/app/frontend/index.config.ts | 10 + src/app/frontend/index.module.ts | 2 + src/app/frontend/login/component.spec.ts | 13 + src/app/frontend/login/component.ts | 10 +- src/app/frontend/typings/backendapi.ts | 36 +- 35 files changed, 717 insertions(+), 141 deletions(-) create mode 100644 src/app/frontend/common/services/global/kubeconfig.ts diff --git a/aio/develop/run-npm-on-container.sh b/aio/develop/run-npm-on-container.sh index ccb157c9a99..f9d88e8fd2f 100755 --- a/aio/develop/run-npm-on-container.sh +++ b/aio/develop/run-npm-on-container.sh @@ -26,6 +26,9 @@ DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" LOCAL_UID=$(id -u) LOCAL_GID=$(id -g) +# Set max http header size for NodeJS +NODE_OPTIONS=${NODE_OPTIONS:-"--max-http-header-size=102400"} + # K8S_DASHBOARD_NPM_CMD will be passed into container and will be used # by run-npm-command.sh on container. Then the shell sciprt will run `npm` # command with K8S_DASHBOAD_NPM_CMD. @@ -88,6 +91,7 @@ docker run \ -e K8S_DASHBOARD_DEBUG=${K8S_DASHBOARD_DEBUG} \ -e LOCAL_UID="${LOCAL_UID}" \ -e LOCAL_GID="${LOCAL_GID}" \ + -e NODE_OPTIONS="${NODE_OPTIONS}" \ -p ${K8S_DASHBOARD_PORT}:${K8S_DASHBOARD_PORT} \ -p ${K8S_DASHBOARD_DEBUG_PORT}:${K8S_DASHBOARD_DEBUG_PORT} \ ${DOCKER_RUN_OPTS} \ diff --git a/i18n/de/messages.de.xlf b/i18n/de/messages.de.xlf index eb9c1b175f3..1499d3d797d 100644 --- a/i18n/de/messages.de.xlf +++ b/i18n/de/messages.de.xlf @@ -3020,7 +3020,7 @@ Anmelden ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -3029,7 +3029,7 @@ Abmelden ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/fr/messages.fr.xlf b/i18n/fr/messages.fr.xlf index 906de78b663..e5913aea77b 100644 --- a/i18n/fr/messages.fr.xlf +++ b/i18n/fr/messages.fr.xlf @@ -3024,7 +3024,7 @@ Connexion ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -3033,7 +3033,7 @@ Déconnexion ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/ja/messages.ja.xlf b/i18n/ja/messages.ja.xlf index 18b2d969652..168076caf62 100644 --- a/i18n/ja/messages.ja.xlf +++ b/i18n/ja/messages.ja.xlf @@ -2750,7 +2750,7 @@ サインイン ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2759,7 +2759,7 @@ サインアウト ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/ko/messages.ko.xlf b/i18n/ko/messages.ko.xlf index 3604d7b9eaa..e5907512fce 100644 --- a/i18n/ko/messages.ko.xlf +++ b/i18n/ko/messages.ko.xlf @@ -2805,7 +2805,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2815,7 +2815,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/messages.xlf b/i18n/messages.xlf index 1c756f2669c..6bd31a7a77d 100644 --- a/i18n/messages.xlf +++ b/i18n/messages.xlf @@ -2597,7 +2597,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2605,7 +2605,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/zh-Hans/messages.zh-Hans.xlf b/i18n/zh-Hans/messages.zh-Hans.xlf index 55f1470f30a..cfb2ef35293 100644 --- a/i18n/zh-Hans/messages.zh-Hans.xlf +++ b/i18n/zh-Hans/messages.zh-Hans.xlf @@ -2805,7 +2805,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2815,7 +2815,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf b/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf index 1b7d45b3445..486122f0d39 100644 --- a/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf +++ b/i18n/zh-Hant-HK/messages.zh-Hant-HK.xlf @@ -2809,7 +2809,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2819,7 +2819,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/i18n/zh-Hant/messages.zh-Hant.xlf b/i18n/zh-Hant/messages.zh-Hant.xlf index 5a73c94b167..a821e669739 100644 --- a/i18n/zh-Hant/messages.zh-Hant.xlf +++ b/i18n/zh-Hant/messages.zh-Hant.xlf @@ -2809,7 +2809,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 37 + 43 @@ -2819,7 +2819,7 @@ ../src/app/frontend/chrome/userpanel/template.html - 42 + 48 diff --git a/package-lock.json b/package-lock.json index 99e15ade8ec..97aae4417ec 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4664,6 +4664,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -6710,7 +6711,6 @@ "version": "6.2.2", "resolved": "https://registry.npmjs.org/camelcase-keys/-/camelcase-keys-6.2.2.tgz", "integrity": "sha512-YrwaA0vEKazPBkn0ipTiMpSajYDSe+KjQfrjhcBMxJt/znbvlHd8Pw/Vamaz5EB4Wfhs3SUR3Z9mwRu/P3s3Yg==", - "dev": true, "requires": { "camelcase": "^5.3.1", "map-obj": "^4.0.0", @@ -11331,6 +11331,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -15456,8 +15457,7 @@ "map-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.1.0.tgz", - "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==", - "dev": true + "integrity": "sha512-glc9y00wgtwcDmp7GaE/0b0OnxpNJsVf3ael/An6Fe2Q51LLwN1er6sdomLRzz5h0+yMpiYLhWYF5R7HeqVd4g==" }, "map-visit": { "version": "1.0.0", @@ -16207,6 +16207,11 @@ "xmldom": "^0.1.27" } }, + "ngx-webstorage": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/ngx-webstorage/-/ngx-webstorage-5.0.0.tgz", + "integrity": "sha512-m96dBjUgLCpaknLRKfsJMEik393xrSX0EwO3paNSkS5d+xj2/cAendE3NwJeKY/W1D9EkKAhCvSUDX9/bAwCUg==" + }, "nice-try": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/nice-try/-/nice-try-1.0.5.tgz", @@ -18801,8 +18806,7 @@ "quick-lru": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-4.0.1.tgz", - "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==", - "dev": true + "integrity": "sha512-ARhCpm70fzdcvNQfPoy49IaanKkTlRWF2JMzqhcJbhSFRZv7nPTvZJdcY7301IPmvW+/p0RgIWnQDLJxifsQ7g==" }, "ramda": { "version": "0.26.1", @@ -20564,6 +20568,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, @@ -24313,6 +24318,7 @@ "dev": true, "optional": true, "requires": { + "bindings": "^1.5.0", "nan": "^2.12.1" } }, diff --git a/package.json b/package.json index 4bd4b6cc62a..d77d68bab12 100644 --- a/package.json +++ b/package.json @@ -117,6 +117,7 @@ "angular-page-visibility": "9.0.0", "ansi-to-html": "0.6.14", "c3": "0.7.18", + "camelcase-keys": "6.2.2", "core-js": "3.6.5", "d3": "5.16.0", "file-saver": "2.0.2", @@ -126,6 +127,7 @@ "ng-in-viewport": "6.1.1", "ngx-cookie-service": "3.0.4", "ngx-filter-pipe": "2.1.2", + "ngx-webstorage": "5.0.0", "normalize.css": "8.0.1", "roboto-fontface": "0.10.0", "rxjs": "6.6.0", diff --git a/src/app/backend/auth/api/types.go b/src/app/backend/auth/api/types.go index c9167a45662..7c1be04ed97 100644 --- a/src/app/backend/auth/api/types.go +++ b/src/app/backend/auth/api/types.go @@ -29,6 +29,9 @@ const ( // Expiration time (in seconds) of tokens generated by dashboard. Default: 15 min. DefaultTokenTTL = 900 + + // Default user name for AuthInfo. This should be empty string in order not to match with user specified name. + DefaultUserName = "" ) // AuthenticationModes represents auth modes supported by dashboard. @@ -111,8 +114,8 @@ type TokenManager interface { // - Kubeconfig based - Authenticates user based on kubeconfig file. Only token/basic modes are supported within // the kubeconfig file. type Authenticator interface { - // GetAuthInfo returns filled AuthInfo structure that can be used for K8S api client creation. - GetAuthInfo() (api.AuthInfo, error) + // GetAuthInfos returns filled AuthInfo structures that can be used for K8S api client creation. + GetAuthInfos() (map[string]api.AuthInfo, error) } // LoginSpec is extracted from request coming from Dashboard frontend during login request. It contains all the @@ -127,13 +130,17 @@ type LoginSpec struct { // KubeConfig is the content of users' kubeconfig file. It will be parsed and auth data will be extracted. // Kubeconfig can not contain any paths. All data has to be provided within the file. KubeConfig string `json:"kubeconfig,omitempty"` + // Server is API server endpoint + Server string `json:"server,omitempty"` + // CertificateAuthorityData is CA data for API server endpoint + CertificateAuthorityData string `json:"certificateAuthorityData,omitempty"` } // AuthResponse is returned from our backend as a response for login/refresh requests. It contains generated JWEToken // and a list of non-critical errors such as 'Failed authentication'. type AuthResponse struct { - // JWEToken is a token generated during login request that contains AuthInfo data in the payload. - JWEToken string `json:"jweToken"` + // JWETokens are tokens generated during login request that contains AuthInfo data in the payload. + JWETokens map[string]string `json:"jweTokens"` // Errors are a list of non-critical errors that happened during login request. Errors []error `json:"errors"` } @@ -141,7 +148,7 @@ type AuthResponse struct { // TokenRefreshSpec contains token that is required by token refresh operation. type TokenRefreshSpec struct { // JWEToken is a token generated during login request that contains AuthInfo data in the payload. - JWEToken string `json:"jweToken"` + JWETokens map[string]string `json:"jweTokens"` } // LoginModesResponse contains list of auth modes supported by dashboard. diff --git a/src/app/backend/auth/basic.go b/src/app/backend/auth/basic.go index 884e9abb9cd..cf6c2b94253 100644 --- a/src/app/backend/auth/basic.go +++ b/src/app/backend/auth/basic.go @@ -26,10 +26,12 @@ type basicAuthenticator struct { } // GetAuthInfo implements Authenticator interface. See Authenticator for more information. -func (self *basicAuthenticator) GetAuthInfo() (api.AuthInfo, error) { - return api.AuthInfo{ - Username: self.username, - Password: self.password, +func (self *basicAuthenticator) GetAuthInfos() (map[string]api.AuthInfo, error) { + return map[string]api.AuthInfo{ + authApi.DefaultUserName: { + Username: self.username, + Password: self.password, + }, }, nil } diff --git a/src/app/backend/auth/handler.go b/src/app/backend/auth/handler.go index ebc3cc0f680..702456c2904 100644 --- a/src/app/backend/auth/handler.go +++ b/src/app/backend/auth/handler.go @@ -16,6 +16,7 @@ package auth import ( "net/http" + "net/url" "github.com/emicklei/go-restful" @@ -64,6 +65,21 @@ func (self AuthHandler) handleLogin(request *restful.Request, response *restful. return } + cookie, err := request.Request.Cookie("server") + if err == nil && len(cookie.Value) > 0 { + server, err := url.QueryUnescape(cookie.Value) + if err == nil { + loginSpec.Server = server + } + } + cookie, err = request.Request.Cookie("certificateAuthorityData") + if err == nil && len(cookie.Value) > 0 { + ca, err := url.QueryUnescape(cookie.Value) + if err == nil { + loginSpec.CertificateAuthorityData = ca + } + } + loginResponse, err := self.manager.Login(loginSpec) if err != nil { response.AddHeader("Content-Type", "text/plain") @@ -86,16 +102,20 @@ func (self *AuthHandler) handleJWETokenRefresh(request *restful.Request, respons return } - refreshedJWEToken, err := self.manager.Refresh(tokenRefreshSpec.JWEToken) - if err != nil { - response.AddHeader("Content-Type", "text/plain") - response.WriteErrorString(errors.HandleHTTPError(err), err.Error()+"\n") - return + refreshedJWETokens := map[string]string{} + for userName, token := range tokenRefreshSpec.JWETokens { + refreshedJWEToken, err := self.manager.Refresh(token) + if err != nil { + response.AddHeader("Content-Type", "text/plain") + response.WriteErrorString(errors.HandleHTTPError(err), err.Error()+"\n") + return + } + refreshedJWETokens[userName] = refreshedJWEToken } response.WriteHeaderAndEntity(http.StatusOK, &authApi.AuthResponse{ - JWEToken: refreshedJWEToken, - Errors: make([]error, 0), + JWETokens: refreshedJWETokens, + Errors: make([]error, 0), }) } diff --git a/src/app/backend/auth/kubeconfig.go b/src/app/backend/auth/kubeconfig.go index 035d93a92d2..b90b5eabc6d 100644 --- a/src/app/backend/auth/kubeconfig.go +++ b/src/app/backend/auth/kubeconfig.go @@ -67,18 +67,18 @@ type kubeConfigAuthenticator struct { } // GetAuthInfo implements Authenticator interface. See Authenticator for more information. -func (self *kubeConfigAuthenticator) GetAuthInfo() (api.AuthInfo, error) { +func (self *kubeConfigAuthenticator) GetAuthInfos() (map[string]api.AuthInfo, error) { kubeConfig, err := self.parseKubeConfig(self.fileContent) if err != nil { - return api.AuthInfo{}, err + return map[string]api.AuthInfo{}, err } - info, err := self.getCurrentUserInfo(*kubeConfig) + infos, err := self.getUserInfos(*kubeConfig) if err != nil { - return api.AuthInfo{}, err + return map[string]api.AuthInfo{}, err } - return self.getAuthInfo(info) + return self.getAuthInfos(infos) } // Parses kubeconfig file and returns kubeConfig object. @@ -91,50 +91,63 @@ func (self *kubeConfigAuthenticator) parseKubeConfig(bytes []byte) (*kubeConfig, return kubeConfig, nil } -// Returns user info based on defined current context. In case it is not found error is returned. -func (self *kubeConfigAuthenticator) getCurrentUserInfo(config kubeConfig) (userInfo, error) { - userName := "" +// Returns user infos. User info for current context would be set at first. In case it is not found error is returned. +func (self *kubeConfigAuthenticator) getUserInfos(config kubeConfig) (map[string]userInfo, error) { + currentUserName := "" for _, context := range config.Contexts { if context.Name == config.CurrentContext { - userName = context.Context.User + currentUserName = context.Context.User } } - if len(userName) == 0 { - return userInfo{}, errors.NewInvalid("Context matching current context not found. Check if your config file is valid.") + if len(currentUserName) == 0 { + return map[string]userInfo{}, errors.NewInvalid("Context matching current context not found. Check if your config file is valid.") } + currentUser := false + userInfos := make(map[string]userInfo, len(config.Users)) for _, user := range config.Users { - if user.Name == userName { - return user.User, nil + userName := user.Name + if userName == currentUserName { + currentUser = true + userName = authApi.DefaultUserName } + userInfos[userName] = user.User } - return userInfo{}, errors.NewInvalid("User matching current context user not found. Check if your config file is valid.") + if !currentUser { + return map[string]userInfo{}, errors.NewInvalid("User matching current context user not found. Check if your config file is valid.") + } + + return userInfos, nil } // Returns auth info structure based on provided user info or error in case not enough data has been provided. -func (self *kubeConfigAuthenticator) getAuthInfo(info userInfo) (api.AuthInfo, error) { - // If "token" is empty for the current "user" entry, fallback to the value of "auth-provider.config.access-token". - if len(info.Token) == 0 { - info.Token = info.AuthProvider.Config.AccessToken - } +func (self *kubeConfigAuthenticator) getAuthInfos(infos map[string]userInfo) (map[string]api.AuthInfo, error) { + results := map[string]api.AuthInfo{} + for userName, info := range infos { + // If "token" is empty for the current "user" entry, fallback to the value of "auth-provider.config.access-token". + if len(info.Token) == 0 { + info.Token = info.AuthProvider.Config.AccessToken + } - if len(info.Token) == 0 && (len(info.Password) == 0 || len(info.Username) == 0) { - return api.AuthInfo{}, errors.NewInvalid("Not enough data to create auth info structure.") - } + if len(info.Token) == 0 && (len(info.Password) == 0 || len(info.Username) == 0) { + return map[string]api.AuthInfo{}, errors.NewInvalid("Not enough data to create auth info structure.") + } - result := api.AuthInfo{} - if self.authModes.IsEnabled(authApi.Token) { - result.Token = info.Token - } + result := api.AuthInfo{} + if self.authModes.IsEnabled(authApi.Token) { + result.Token = info.Token + } - if self.authModes.IsEnabled(authApi.Basic) { - result.Username = info.Username - result.Password = info.Password + if self.authModes.IsEnabled(authApi.Basic) { + result.Username = info.Username + result.Password = info.Password + } + results[userName] = result } - return result, nil + return results, nil } // NewBasicAuthenticator returns Authenticator based on LoginSpec. diff --git a/src/app/backend/auth/kubeconfig_test.go b/src/app/backend/auth/kubeconfig_test.go index be494361ed3..879eefe7384 100644 --- a/src/app/backend/auth/kubeconfig_test.go +++ b/src/app/backend/auth/kubeconfig_test.go @@ -75,35 +75,35 @@ func TestKubeConfigAuthenticator(t *testing.T) { info string authModes authApi.AuthenticationModes params map[string]string - expected api.AuthInfo + expected map[string]api.AuthInfo expectedErr error }{ { `If "token" is empty for the current "user" entry, the value of "auth-provider.config.access-token" is picked up.`, authModeToken, map[string]string{"accessToken": "foo", "token": ""}, - api.AuthInfo{Token: "foo"}, + map[string]api.AuthInfo{"": api.AuthInfo{Token: "foo"}}, nil, }, { `If "token" is provided for the current "user" entry, that token is picked up instead.`, authModeToken, map[string]string{"accessToken": "foo", "token": "bar"}, - api.AuthInfo{Token: "bar"}, + map[string]api.AuthInfo{"": api.AuthInfo{Token: "bar"}}, nil, }, { `If the "basic" auth mode is enabled, "username" and "password" are picked up.`, authModeBasic, map[string]string{"username": "foo", "password": "bar"}, - api.AuthInfo{Username: "foo", Password: "bar"}, + map[string]api.AuthInfo{"": api.AuthInfo{Username: "foo", Password: "bar"}}, nil, }, { `If no value for "token", "username" or "password" is provided or can be inferred, an error is returned.`, authModeBoth, map[string]string{}, - api.AuthInfo{}, + map[string]api.AuthInfo{}, errors.NewInvalid("Not enough data to create auth info structure."), }, } @@ -115,7 +115,7 @@ func TestKubeConfigAuthenticator(t *testing.T) { } kubeConfigAuthenticator := NewKubeConfigAuthenticator(&authApi.LoginSpec{KubeConfig: kb.String()}, c.authModes) - response, err := kubeConfigAuthenticator.GetAuthInfo() + response, err := kubeConfigAuthenticator.GetAuthInfos() if !areErrorsEqual(err, c.expectedErr) { t.Errorf("Test Case: %s. Expected error to be: %v, but got %v.", diff --git a/src/app/backend/auth/manager.go b/src/app/backend/auth/manager.go index 7991ca3eba7..8391a54b1e9 100644 --- a/src/app/backend/auth/manager.go +++ b/src/app/backend/auth/manager.go @@ -37,23 +37,28 @@ func (self authManager) Login(spec *authApi.LoginSpec) (*authApi.AuthResponse, e return nil, err } - authInfo, err := authenticator.GetAuthInfo() + authInfos, err := authenticator.GetAuthInfos() if err != nil { return nil, err } - err = self.healthCheck(authInfo) + // AuthInfo for current user is set in entry with "" key. + err = self.healthCheck(authInfos[authApi.DefaultUserName], spec.Server, spec.CertificateAuthorityData) nonCriticalErrors, criticalError := errors.HandleError(err) if criticalError != nil || len(nonCriticalErrors) > 0 { return &authApi.AuthResponse{Errors: nonCriticalErrors}, criticalError } - token, err := self.tokenManager.Generate(authInfo) - if err != nil { - return nil, err + tokens := map[string]string{} + for userName, authInfo := range authInfos { + token, err := self.tokenManager.Generate(authInfo) + if err != nil { + return nil, err + } + tokens[userName] = token } - return &authApi.AuthResponse{JWEToken: token, Errors: nonCriticalErrors}, nil + return &authApi.AuthResponse{JWETokens: tokens, Errors: nonCriticalErrors}, nil } // Refresh implements auth manager. See AuthManager interface for more information. @@ -89,8 +94,8 @@ func (self authManager) getAuthenticator(spec *authApi.LoginSpec) (authApi.Authe // Checks if user data extracted from provided AuthInfo structure is valid and user is correctly authenticated // by K8S apiserver. -func (self authManager) healthCheck(authInfo api.AuthInfo) error { - return self.clientManager.HasAccess(authInfo) +func (self authManager) healthCheck(authInfo api.AuthInfo, server string, caData string) error { + return self.clientManager.HasAccess(authInfo, server, caData) } // NewAuthManager creates auth manager. diff --git a/src/app/backend/auth/manager_test.go b/src/app/backend/auth/manager_test.go index 729e5bc5835..5ddbeeebdc2 100644 --- a/src/app/backend/auth/manager_test.go +++ b/src/app/backend/auth/manager_test.go @@ -82,7 +82,7 @@ func (self *fakeClientManager) CSRFKey() string { return "" } -func (self *fakeClientManager) HasAccess(authInfo api.AuthInfo) error { +func (self *fakeClientManager) HasAccess(authInfo api.AuthInfo, server string, caData string) error { return self.HasAccessError } @@ -143,7 +143,7 @@ func TestAuthManager_Login(t *testing.T) { &authApi.LoginSpec{Token: "existing-token"}, &fakeClientManager{HasAccessError: nil}, &fakeTokenManager{GeneratedToken: "generated-token"}, - &authApi.AuthResponse{JWEToken: "generated-token", Errors: make([]error, 0)}, + &authApi.AuthResponse{JWETokens: map[string]string{"": "generated-token"}, Errors: make([]error, 0)}, nil, }, { "Should propagate error on unexpected error", diff --git a/src/app/backend/auth/token.go b/src/app/backend/auth/token.go index f25bc2a632d..afc6c0b2178 100644 --- a/src/app/backend/auth/token.go +++ b/src/app/backend/auth/token.go @@ -25,9 +25,11 @@ type tokenAuthenticator struct { } // GetAuthInfo implements Authenticator interface. See Authenticator for more information. -func (self tokenAuthenticator) GetAuthInfo() (api.AuthInfo, error) { - return api.AuthInfo{ - Token: self.token, +func (self tokenAuthenticator) GetAuthInfos() (map[string]api.AuthInfo, error) { + return map[string]api.AuthInfo{ + authApi.DefaultUserName: { + Token: self.token, + }, }, nil } diff --git a/src/app/backend/client/api/types.go b/src/app/backend/client/api/types.go index 862709aa78f..31e675b1c7a 100644 --- a/src/app/backend/client/api/types.go +++ b/src/app/backend/client/api/types.go @@ -49,7 +49,7 @@ type ClientManager interface { Config(req *restful.Request) (*rest.Config, error) ClientCmdConfig(req *restful.Request) (clientcmd.ClientConfig, error) CSRFKey() string - HasAccess(authInfo api.AuthInfo) error + HasAccess(authInfo api.AuthInfo, server string, caData string) error VerberClient(req *restful.Request, config *rest.Config) (ResourceVerber, error) SetTokenManager(manager authApi.TokenManager) } diff --git a/src/app/backend/client/manager.go b/src/app/backend/client/manager.go index bbc86392327..33659f6046b 100644 --- a/src/app/backend/client/manager.go +++ b/src/app/backend/client/manager.go @@ -16,7 +16,9 @@ package client import ( "context" + "encoding/base64" "log" + "net/url" "strings" "github.com/emicklei/go-restful" @@ -208,7 +210,24 @@ func (self *clientManager) ClientCmdConfig(req *restful.Request) (clientcmd.Clie return nil, err } - cfg, err := self.buildConfigFromFlags(self.apiserverHost, self.kubeConfigPath) + server := self.apiserverHost + cookie, err := req.Request.Cookie("server") + if err == nil && len(cookie.Value) > 0 { + sv, err := url.QueryUnescape(cookie.Value) + if err == nil { + server = sv + } + } + caData := "" + cookie, err = req.Request.Cookie("certificateAuthorityData") + if err == nil && len(cookie.Value) > 0 { + ca, err := url.QueryUnescape(cookie.Value) + if err == nil { + caData = ca + } + } + + cfg, err := self.buildConfigFromFlags(server, caData, self.kubeConfigPath) if err != nil { return nil, err } @@ -223,8 +242,8 @@ func (self *clientManager) CSRFKey() string { // HasAccess configures K8S api client with provided auth info and executes a basic check against apiserver to see // if it is valid. -func (self *clientManager) HasAccess(authInfo api.AuthInfo) error { - cfg, err := self.buildConfigFromFlags(self.apiserverHost, self.kubeConfigPath) +func (self *clientManager) HasAccess(authInfo api.AuthInfo, server string, caData string) error { + cfg, err := self.buildConfigFromFlags(server, caData, self.kubeConfigPath) if err != nil { return err } @@ -295,12 +314,16 @@ func (self *clientManager) initConfig(cfg *rest.Config) { // Returns rest Config based on provided apiserverHost and kubeConfigPath flags. If both are // empty then in-cluster config will be used and if it is nil the error is returned. -func (self *clientManager) buildConfigFromFlags(apiserverHost, kubeConfigPath string) ( +func (self *clientManager) buildConfigFromFlags(apiserverHost string, caData string, kubeConfigPath string) ( *rest.Config, error) { if len(kubeConfigPath) > 0 || len(apiserverHost) > 0 { + decodedCAData := []byte{} + if len(caData) > 0 { + decodedCAData, _ = base64.StdEncoding.DecodeString(caData) + } return clientcmd.NewNonInteractiveDeferredLoadingClientConfig( &clientcmd.ClientConfigLoadingRules{ExplicitPath: kubeConfigPath}, - &clientcmd.ConfigOverrides{ClusterInfo: api.Cluster{Server: apiserverHost}}).ClientConfig() + &clientcmd.ConfigOverrides{ClusterInfo: api.Cluster{Server: apiserverHost, CertificateAuthorityData: decodedCAData}}).ClientConfig() } if self.isRunningInCluster() { @@ -525,7 +548,7 @@ func (self *clientManager) initInsecureClients() { } func (self *clientManager) initInsecureConfig() { - cfg, err := self.buildConfigFromFlags(self.apiserverHost, self.kubeConfigPath) + cfg, err := self.buildConfigFromFlags(self.apiserverHost, "", self.kubeConfigPath) if err != nil { panic(err) } diff --git a/src/app/backend/handler/apihandler.go b/src/app/backend/handler/apihandler.go index 343d4dc9c8e..06c505d1e81 100644 --- a/src/app/backend/handler/apihandler.go +++ b/src/app/backend/handler/apihandler.go @@ -809,7 +809,7 @@ func (apiHandler *APIHandler) handleGetStatefulSetList(request *restful.Request, dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics result, err := statefulset.GetStatefulSetList(k8sClient, namespace, dataSelect, - apiHandler.iManager.Metric().Client()) + apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -826,7 +826,7 @@ func (apiHandler *APIHandler) handleGetStatefulSetDetail(request *restful.Reques namespace := request.PathParameter("namespace") name := request.PathParameter("statefulset") - result, err := statefulset.GetStatefulSetDetail(k8sClient, apiHandler.iManager.Metric().Client(), namespace, name) + result, err := statefulset.GetStatefulSetDetail(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, name) if err != nil { errors.HandleInternalError(response, err) @@ -846,7 +846,7 @@ func (apiHandler *APIHandler) handleGetStatefulSetPods(request *restful.Request, name := request.PathParameter("statefulset") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := statefulset.GetStatefulSetPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, name, namespace) + result, err := statefulset.GetStatefulSetPods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, name, namespace) if err != nil { errors.HandleInternalError(response, err) return @@ -1040,7 +1040,7 @@ func (apiHandler *APIHandler) handleGetServicePods(request *restful.Request, res name := request.PathParameter("service") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := resourceService.GetServicePods(k8sClient, apiHandler.iManager.Metric().Client(), namespace, name, dataSelect) + result, err := resourceService.GetServicePods(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, name, dataSelect) if err != nil { errors.HandleInternalError(response, err) return @@ -1057,7 +1057,7 @@ func (apiHandler *APIHandler) handleGetNodeList(request *restful.Request, respon dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := node.GetNodeList(k8sClient, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := node.GetNodeList(k8sClient, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -1075,7 +1075,7 @@ func (apiHandler *APIHandler) handleGetNodeDetail(request *restful.Request, resp name := request.PathParameter("name") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := node.GetNodeDetail(k8sClient, apiHandler.iManager.Metric().Client(), name, dataSelect) + result, err := node.GetNodeDetail(k8sClient, apiHandler.iManager.Metric().Client(request), name, dataSelect) if err != nil { errors.HandleInternalError(response, err) return @@ -1111,7 +1111,7 @@ func (apiHandler *APIHandler) handleGetNodePods(request *restful.Request, respon name := request.PathParameter("name") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := node.GetNodePods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, name) + result, err := node.GetNodePods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, name) if err != nil { errors.HandleInternalError(response, err) return @@ -1266,7 +1266,7 @@ func (apiHandler *APIHandler) handleGetReplicationControllerList(request *restfu namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := replicationcontroller.GetReplicationControllerList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := replicationcontroller.GetReplicationControllerList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -1284,7 +1284,7 @@ func (apiHandler *APIHandler) handleGetReplicaSets(request *restful.Request, res namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := replicaset.GetReplicaSetList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := replicaset.GetReplicaSetList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -1301,7 +1301,7 @@ func (apiHandler *APIHandler) handleGetReplicaSetDetail(request *restful.Request namespace := request.PathParameter("namespace") replicaSet := request.PathParameter("replicaSet") - result, err := replicaset.GetReplicaSetDetail(k8sClient, apiHandler.iManager.Metric().Client(), namespace, replicaSet) + result, err := replicaset.GetReplicaSetDetail(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, replicaSet) if err != nil { errors.HandleInternalError(response, err) @@ -1322,7 +1322,7 @@ func (apiHandler *APIHandler) handleGetReplicaSetPods(request *restful.Request, replicaSet := request.PathParameter("replicaSet") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := replicaset.GetReplicaSetPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, replicaSet, namespace) + result, err := replicaset.GetReplicaSetPods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, replicaSet, namespace) if err != nil { errors.HandleInternalError(response, err) return @@ -1430,7 +1430,7 @@ func (apiHandler *APIHandler) handleGetDeployments(request *restful.Request, res namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := deployment.GetDeploymentList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := deployment.GetDeploymentList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -1522,7 +1522,7 @@ func (apiHandler *APIHandler) handleGetPods(request *restful.Request, response * namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics // download standard metrics - cpu, and memory - by default - result, err := pod.GetPodList(k8sClient, apiHandler.iManager.Metric().Client(), namespace, dataSelect) + result, err := pod.GetPodList(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, dataSelect) if err != nil { errors.HandleInternalError(response, err) return @@ -1539,7 +1539,7 @@ func (apiHandler *APIHandler) handleGetPodDetail(request *restful.Request, respo namespace := request.PathParameter("namespace") name := request.PathParameter("pod") - result, err := pod.GetPodDetail(k8sClient, apiHandler.iManager.Metric().Client(), namespace, name) + result, err := pod.GetPodDetail(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, name) if err != nil { errors.HandleInternalError(response, err) return @@ -1698,7 +1698,7 @@ func (apiHandler *APIHandler) handleGetReplicationControllerPods(request *restfu rc := request.PathParameter("replicationController") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := replicationcontroller.GetReplicationControllerPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, rc, namespace) + result, err := replicationcontroller.GetReplicationControllerPods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, rc, namespace) if err != nil { errors.HandleInternalError(response, err) return @@ -1992,7 +1992,7 @@ func (apiHandler *APIHandler) handleGetDaemonSetList(request *restful.Request, r namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := daemonset.GetDaemonSetList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := daemonset.GetDaemonSetList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -2010,7 +2010,7 @@ func (apiHandler *APIHandler) handleGetDaemonSetDetail( namespace := request.PathParameter("namespace") name := request.PathParameter("daemonSet") - result, err := daemonset.GetDaemonSetDetail(k8sClient, apiHandler.iManager.Metric().Client(), namespace, name) + result, err := daemonset.GetDaemonSetDetail(k8sClient, apiHandler.iManager.Metric().Client(request), namespace, name) if err != nil { errors.HandleInternalError(response, err) return @@ -2029,7 +2029,7 @@ func (apiHandler *APIHandler) handleGetDaemonSetPods(request *restful.Request, r name := request.PathParameter("daemonSet") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := daemonset.GetDaemonSetPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, name, namespace) + result, err := daemonset.GetDaemonSetPods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, name, namespace) if err != nil { errors.HandleInternalError(response, err) return @@ -2137,7 +2137,7 @@ func (apiHandler *APIHandler) handleGetJobList(request *restful.Request, respons namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := job.GetJobList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := job.GetJobList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -2173,7 +2173,7 @@ func (apiHandler *APIHandler) handleGetJobPods(request *restful.Request, respons name := request.PathParameter("name") dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := job.GetJobPods(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, namespace, name) + result, err := job.GetJobPods(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, namespace, name) if err != nil { errors.HandleInternalError(response, err) return @@ -2209,7 +2209,7 @@ func (apiHandler *APIHandler) handleGetCronJobList(request *restful.Request, res namespace := parseNamespacePathParameter(request) dataSelect := parser.ParseDataSelectPathParameter(request) dataSelect.MetricQuery = dataselect.StandardMetrics - result, err := cronjob.GetCronJobList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client()) + result, err := cronjob.GetCronJobList(k8sClient, namespace, dataSelect, apiHandler.iManager.Metric().Client(request)) if err != nil { errors.HandleInternalError(response, err) return @@ -2249,7 +2249,7 @@ func (apiHandler *APIHandler) handleGetCronJobJobs(request *restful.Request, res } dataSelect := parser.ParseDataSelectPathParameter(request) - result, err := cronjob.GetCronJobJobs(k8sClient, apiHandler.iManager.Metric().Client(), dataSelect, namespace, name, active) + result, err := cronjob.GetCronJobJobs(k8sClient, apiHandler.iManager.Metric().Client(request), dataSelect, namespace, name, active) if err != nil { errors.HandleInternalError(response, err) return diff --git a/src/app/backend/integration/metric/manager.go b/src/app/backend/integration/metric/manager.go index 2824d71ca6c..d110b800cad 100644 --- a/src/app/backend/integration/metric/manager.go +++ b/src/app/backend/integration/metric/manager.go @@ -17,8 +17,11 @@ package metric import ( "fmt" "log" + "net/url" "time" + "github.com/emicklei/go-restful" + clientapi "github.com/kubernetes/dashboard/src/app/backend/client/api" integrationapi "github.com/kubernetes/dashboard/src/app/backend/integration/api" metricapi "github.com/kubernetes/dashboard/src/app/backend/integration/metric/api" @@ -32,7 +35,7 @@ type MetricManager interface { // AddClient adds metric client to client list supported by this manager. AddClient(metricapi.MetricClient) MetricManager // Client returns active Metric client. - Client() metricapi.MetricClient + Client(*restful.Request) metricapi.MetricClient // Enable is responsible for switching active client if given integration application id // is found and related application is healthy (we can connect to it). Enable(integrationapi.IntegrationID) error @@ -64,7 +67,21 @@ func (self *metricManager) AddClient(client metricapi.MetricClient) MetricManage } // Client implements metric manager interface. See MetricManager for more information. -func (self *metricManager) Client() metricapi.MetricClient { +func (self *metricManager) Client(request *restful.Request) metricapi.MetricClient { + // Create client dynamically if `sidecarHost` is set in cookie. + cookie, err := request.Request.Cookie("sidecarHost") + if err == nil && len(cookie.Value) > 0 { + sidecarHost, err := url.QueryUnescape(cookie.Value) + if err == nil { + metricClient, err := sidecar.CreateSidecarClient(sidecarHost, nil) + if err == nil { + return metricClient + } + } + log.Printf("There was an error during sidecar client dynamic creation: %s", err.Error()) + return nil + } + return self.active } diff --git a/src/app/backend/integration/metric/manager_test.go b/src/app/backend/integration/metric/manager_test.go index 678d8523b0d..768e8feb8c4 100644 --- a/src/app/backend/integration/metric/manager_test.go +++ b/src/app/backend/integration/metric/manager_test.go @@ -15,9 +15,11 @@ package metric import ( + "net/http" "reflect" "testing" + restful "github.com/emicklei/go-restful" "github.com/kubernetes/dashboard/src/app/backend/client" "github.com/kubernetes/dashboard/src/app/backend/errors" integrationapi "github.com/kubernetes/dashboard/src/app/backend/integration/api" @@ -71,18 +73,35 @@ func TestNewMetricManager(t *testing.T) { func TestMetricManager_Client(t *testing.T) { cases := []struct { + request *restful.Request client api.MetricClient expected api.MetricClient }{ - {&FakeMetricClient{healthOk: false}, nil}, - {&FakeMetricClient{healthOk: true}, &FakeMetricClient{healthOk: true}}, + { + &restful.Request{ + Request: &http.Request{ + Header: http.Header(map[string][]string{}), + }, + }, + &FakeMetricClient{healthOk: false}, + nil, + }, + { + &restful.Request{ + Request: &http.Request{ + Header: http.Header(map[string][]string{}), + }, + }, + &FakeMetricClient{healthOk: true}, + &FakeMetricClient{healthOk: true}, + }, } for _, c := range cases { metricManager := NewMetricManager(nil) metricManager.AddClient(c.client) metricManager.Enable(fakeMetricClientID) - client := metricManager.Client() + client := metricManager.Client(c.request) if !reflect.DeepEqual(client, c.expected) { t.Errorf("Failed to get active metric client. Expected: %v, but got %v.", diff --git a/src/app/backend/plugin/config_test.go b/src/app/backend/plugin/config_test.go index fc0989a5554..8e42eb1a156 100644 --- a/src/app/backend/plugin/config_test.go +++ b/src/app/backend/plugin/config_test.go @@ -119,7 +119,7 @@ func (cm *fakeClientManager) CSRFKey() string { panic("implement me") } -func (cm *fakeClientManager) HasAccess(authInfo api.AuthInfo) error { +func (cm *fakeClientManager) HasAccess(authInfo api.AuthInfo, server string, caData string) error { panic("implement me") } diff --git a/src/app/frontend/chrome/userpanel/component.ts b/src/app/frontend/chrome/userpanel/component.ts index 7c8f093e978..79d8b511421 100644 --- a/src/app/frontend/chrome/userpanel/component.ts +++ b/src/app/frontend/chrome/userpanel/component.ts @@ -15,6 +15,7 @@ import {Component, OnInit} from '@angular/core'; import {LoginStatus} from '@api/backendapi'; import {AuthService} from '../../common/services/global/authentication'; +import {KubeconfigService} from '../../common/services/global/kubeconfig'; @Component({ selector: 'kd-user-panel', @@ -27,14 +28,18 @@ import {AuthService} from '../../common/services/global/authentication'; export class UserPanelComponent implements OnInit { loginStatus: LoginStatus; isLoginStatusInitialized = false; + currentContext: string; + contexts: string[]; - constructor(private readonly authService_: AuthService) {} + constructor(private readonly authService_: AuthService, private readonly kubeconfigService_: KubeconfigService) {} ngOnInit(): void { this.authService_.getLoginStatus().subscribe(status => { this.loginStatus = status; this.isLoginStatusInitialized = true; }); + this.contexts = this.kubeconfigService_.getContexts(); + this.currentContext = this.kubeconfigService_.getCurrentContext(); } isAuthSkipped(): boolean { @@ -49,6 +54,15 @@ export class UserPanelComponent implements OnInit { return this.loginStatus ? this.loginStatus.httpsMode : false; } + isCurrentContext(context: string): boolean { + this.currentContext = this.kubeconfigService_.getCurrentContext(); + return context === this.currentContext; + } + + switchContext(context: string): void { + this.kubeconfigService_.switchContext(context); + } + logout(): void { this.authService_.logout(); } diff --git a/src/app/frontend/chrome/userpanel/template.html b/src/app/frontend/chrome/userpanel/template.html index b26bbf14e47..39279a06167 100644 --- a/src/app/frontend/chrome/userpanel/template.html +++ b/src/app/frontend/chrome/userpanel/template.html @@ -30,6 +30,12 @@ + + -