From 5049a746da959ae521526d9b672a162c2fef3959 Mon Sep 17 00:00:00 2001 From: niusmallnan Date: Tue, 13 Apr 2021 15:18:39 +0800 Subject: [PATCH 1/4] K-EXPLORER: support UIOffline for value injection --- pkg/ui/routes.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/pkg/ui/routes.go b/pkg/ui/routes.go index 5fc0cd1..afa7a4f 100644 --- a/pkg/ui/routes.go +++ b/pkg/ui/routes.go @@ -7,6 +7,10 @@ import ( "github.com/gorilla/mux" ) +var ( + UIOffline = "dynamic" +) + func New(path string) http.Handler { vue := NewUIHandler(&Options{ Path: func() string { @@ -15,6 +19,9 @@ func New(path string) http.Handler { } return path }, + Offline: func() string { + return UIOffline + }, }) router := mux.NewRouter() From 255623bd7c0277f8b8c61e66fdf873d5a43778a3 Mon Sep 17 00:00:00 2001 From: niusmallnan Date: Wed, 14 Apr 2021 13:34:14 +0800 Subject: [PATCH 2/4] K-EXPLORER: fix the k8s/local ws issue --- pkg/server/handler/apiserver.go | 4 ++-- pkg/server/handler/handlers.go | 15 +++++++++++++++ 2 files changed, 17 insertions(+), 2 deletions(-) diff --git a/pkg/server/handler/apiserver.go b/pkg/server/handler/apiserver.go index aaf1a65..ca830da 100644 --- a/pkg/server/handler/apiserver.go +++ b/pkg/server/handler/apiserver.go @@ -48,9 +48,9 @@ func New(cfg *rest.Config, sf schema.Factory, authMiddleware auth.Middleware, ne APIRoot: w(a.apiHandler(apiRoot)), } if routerFunc == nil { - return a.server, router.Routes(handlers), nil + return a.server, rewriteLocalCluster(router.Routes(handlers)), nil } - return a.server, routerFunc(handlers), nil + return a.server, rewriteLocalCluster(routerFunc(handlers)), nil } type apiServer struct { diff --git a/pkg/server/handler/handlers.go b/pkg/server/handler/handlers.go index 0b4fa3e..cc46b02 100644 --- a/pkg/server/handler/handlers.go +++ b/pkg/server/handler/handlers.go @@ -1,6 +1,9 @@ package handler import ( + "net/http" + "strings" + "github.com/gorilla/mux" "github.com/rancher/apiserver/pkg/types" "github.com/rancher/steve/pkg/attributes" @@ -35,3 +38,15 @@ func k8sAPI(sf schema.Factory, apiOp *types.APIRequest) { func apiRoot(sf schema.Factory, apiOp *types.APIRequest) { apiOp.Type = "apiRoot" } + +func rewriteLocalCluster(next http.Handler) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if strings.HasPrefix(req.URL.Path, "/k8s/clusters/local") { + req.URL.Path = strings.TrimPrefix(req.URL.Path, "/k8s/clusters/local") + if req.URL.Path == "" { + req.URL.Path = "/" + } + } + next.ServeHTTP(rw, req) + }) +} From 8c327e08adf2905981d492c48a0618e6740d25d1 Mon Sep 17 00:00:00 2001 From: niusmallnan Date: Thu, 15 Apr 2021 10:46:06 +0800 Subject: [PATCH 3/4] K-EXPLORER: add shell link for local cluster --- pkg/resources/cluster/cluster.go | 18 ++++ pkg/resources/cluster/shell.go | 148 +++++++++++++++++++++++++++++++ 2 files changed, 166 insertions(+) create mode 100644 pkg/resources/cluster/shell.go diff --git a/pkg/resources/cluster/cluster.go b/pkg/resources/cluster/cluster.go index bf62240..a4b70af 100644 --- a/pkg/resources/cluster/cluster.go +++ b/pkg/resources/cluster/cluster.go @@ -3,12 +3,14 @@ package cluster import ( "context" "net/http" + "time" "github.com/rancher/apiserver/pkg/store/empty" "github.com/rancher/apiserver/pkg/types" detector "github.com/rancher/kubernetes-provider-detector" "github.com/rancher/steve/pkg/accesscontrol" "github.com/rancher/steve/pkg/attributes" + "github.com/rancher/steve/pkg/podimpersonation" steveschema "github.com/rancher/steve/pkg/schema" "github.com/rancher/steve/pkg/stores/proxy" "github.com/rancher/wrangler/pkg/genericcondition" @@ -19,7 +21,19 @@ import ( "k8s.io/client-go/discovery" ) +const ( + shellPodImage = "rancher/shell:v0.1.6" + shellPodNS = "kube-system" +) + func Register(ctx context.Context, apiSchemas *types.APISchemas, cg proxy.ClientGetter, schemaFactory steveschema.Factory) { + // K-EXPLORER + shell := &shell{ + cg: cg, + namespace: shellPodNS, + impersonator: podimpersonation.New("shell", cg, time.Hour, func() string { return shellPodImage }), + } + apiSchemas.InternalSchemas.TypeName("management.cattle.io.cluster", Cluster{}) apiSchemas.MustImportAndCustomize(&ApplyInput{}, nil) @@ -57,6 +71,10 @@ func Register(ctx context.Context, apiSchemas *types.APISchemas, cg proxy.Client Output: "applyOutput", }, } + // K-EXPLORER + schema.LinkHandlers = map[string]http.Handler{ + "shell": shell, + } }) } diff --git a/pkg/resources/cluster/shell.go b/pkg/resources/cluster/shell.go new file mode 100644 index 0000000..0f27865 --- /dev/null +++ b/pkg/resources/cluster/shell.go @@ -0,0 +1,148 @@ +package cluster + +import ( + "context" + "net/http" + "net/http/httputil" + "time" + + "github.com/rancher/steve/pkg/podimpersonation" + "github.com/rancher/steve/pkg/stores/proxy" + "github.com/rancher/wrangler/pkg/schemas/validation" + v1 "k8s.io/api/core/v1" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" + "k8s.io/client-go/kubernetes" + "k8s.io/client-go/kubernetes/scheme" + "k8s.io/client-go/rest" +) + +type shell struct { + namespace string + impersonator *podimpersonation.PodImpersonation + cg proxy.ClientGetter +} + +func (s *shell) ServeHTTP(rw http.ResponseWriter, req *http.Request) { + ctx, user, client, err := s.contextAndClient(req) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + pod, err := s.impersonator.CreatePod(ctx, user, s.createPod(), &podimpersonation.PodOptions{ + Wait: true, + }) + if err != nil { + http.Error(rw, err.Error(), http.StatusInternalServerError) + return + } + + defer func() { + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + _ = client.CoreV1().Pods(pod.Namespace).Delete(ctx, pod.Name, metav1.DeleteOptions{}) + }() + s.proxyRequest(rw, req, pod, client) +} + +func (s *shell) proxyRequest(rw http.ResponseWriter, req *http.Request, pod *v1.Pod, client kubernetes.Interface) { + attachURL := client.CoreV1().RESTClient(). + Get(). + Namespace(pod.Namespace). + Resource("pods"). + Name(pod.Name). + SubResource("exec"). + VersionedParams(&v1.PodExecOptions{ + Stdin: true, + Stdout: true, + Stderr: true, + TTY: true, + Container: "shell", + Command: []string{"welcome"}, + }, scheme.ParameterCodec).URL() + + httpClient := client.CoreV1().RESTClient().(*rest.RESTClient).Client + p := httputil.ReverseProxy{ + Director: func(req *http.Request) { + req.URL = attachURL + req.Host = attachURL.Host + delete(req.Header, "Impersonate-Group") + delete(req.Header, "Impersonate-User") + delete(req.Header, "Authorization") + delete(req.Header, "Cookie") + }, + Transport: httpClient.Transport, + FlushInterval: time.Millisecond * 100, + } + + p.ServeHTTP(rw, req) +} + +func (s *shell) contextAndClient(req *http.Request) (context.Context, user.Info, kubernetes.Interface, error) { + ctx := req.Context() + client, err := s.cg.AdminK8sInterface() + if err != nil { + return ctx, nil, nil, err + } + + user, ok := request.UserFrom(ctx) + if !ok { + return ctx, nil, nil, validation.Unauthorized + } + + return ctx, user, client, nil +} + +func (s *shell) createPod() *v1.Pod { + return &v1.Pod{ + ObjectMeta: metav1.ObjectMeta{ + GenerateName: "dashboard-shell-", + Namespace: s.namespace, + }, + Spec: v1.PodSpec{ + TerminationGracePeriodSeconds: new(int64), + RestartPolicy: v1.RestartPolicyNever, + NodeSelector: map[string]string{ + "kubernetes.io/os": "linux", + }, + Tolerations: []v1.Toleration{ + { + Key: "cattle.io/os", + Operator: "Equal", + Value: "linux", + Effect: "NoSchedule", + }, + { + Key: "node-role.kubernetes.io/controlplane", + Operator: "Equal", + Value: "true", + Effect: "NoSchedule", + }, + { + Key: "node-role.kubernetes.io/etcd", + Operator: "Equal", + Value: "true", + Effect: "NoExecute", + }, + }, + Containers: []v1.Container{ + { + Name: "shell", + TTY: true, + Stdin: true, + StdinOnce: true, + Env: []v1.EnvVar{ + { + Name: "KUBECONFIG", + Value: "/home/shell/.kube/config", + }, + }, + Image: shellPodImage, + ImagePullPolicy: v1.PullIfNotPresent, + }, + }, + }, + } +} From 327be56d3a6a2b85cf4751148f6834402e8211d5 Mon Sep 17 00:00:00 2001 From: niusmallnan Date: Fri, 16 Apr 2021 14:17:08 +0800 Subject: [PATCH 4/4] K-EXPLORER: add embed ui mode --- go.mod | 2 +- pkg/ui/embed.go | 97 ++++++++++++++++++++++++++++++++++++++++++ pkg/ui/external.go | 42 ++++++++++++++++++ pkg/ui/handler.go | 55 +++++------------------- pkg/ui/routes.go | 13 +++--- pkg/version/version.go | 13 +++++- 6 files changed, 170 insertions(+), 52 deletions(-) create mode 100644 pkg/ui/embed.go create mode 100644 pkg/ui/external.go diff --git a/go.mod b/go.mod index a17aa85..cc990f9 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/rancher/steve -go 1.13 +go 1.16 replace ( github.com/crewjam/saml => github.com/rancher/saml v0.0.0-20180713225824-ce1532152fde diff --git a/pkg/ui/embed.go b/pkg/ui/embed.go new file mode 100644 index 0000000..19f3197 --- /dev/null +++ b/pkg/ui/embed.go @@ -0,0 +1,97 @@ +// +build embed + +package ui + +import ( + "embed" + "io" + "io/fs" + "net/http" + "path/filepath" + "strings" + + "github.com/sirupsen/logrus" +) + +// content holds our static web server content. +//go:embed ui/* +var staticContent embed.FS + +type fsFunc func(name string) (fs.File, error) + +func (f fsFunc) Open(name string) (fs.File, error) { + return f(name) +} + +func pathExist(path string) bool { + path = formatPath(path) + _, err := staticContent.Open(path) + return err == nil +} + +func openFile(path string) (fs.File, error) { + path = formatPath(path) + file, err := staticContent.Open(path) + if err != nil { + logrus.Errorf("openEmbedFile %s err: %v", path, err) + } + return file, err +} + +func formatPath(path string) string { + // To replace _nuxt/_cluster/... + // For embed, If a pattern names a directory, + // all files in the subtree rooted at that directory are embedded (recursively), + // except that files with names beginning with ‘.’ or ‘_’ are excluded. + return strings.ReplaceAll(path, "_", "") +} + +func serveEmbed(basePath string) http.Handler { + handler := fsFunc(func(name string) (fs.File, error) { + logrus.Debugf("serveEmbed name: %s", name) + assetPath := filepath.Join(basePath, name) + logrus.Debugf("serveEmbed final path: %s", assetPath) + return openFile(assetPath) + }) + + return http.FileServer(http.FS(handler)) +} + +func serveEmbedIndex(basePath string) http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + path := filepath.Join(basePath, "dashboard", "index.html") + logrus.Debugf("serveEmbedIndex : %s", path) + f, _ := staticContent.Open(path) + io.Copy(rw, f) + f.Close() + }) +} + +func (u *Handler) ServeAsset() http.Handler { + return u.middleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + serveEmbed(u.pathSetting()).ServeHTTP(rw, req) + })) +} + +func (u *Handler) ServeFaviconDashboard() http.Handler { + return u.middleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + serveEmbed(filepath.Join(u.pathSetting(), "dashboard")).ServeHTTP(rw, req) + })) +} + +func (u *Handler) IndexFileOnNotFound() http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + path := filepath.Join(u.pathSetting(), req.URL.Path) + if pathExist(path) { + u.ServeAsset().ServeHTTP(rw, req) + } else { + u.IndexFile().ServeHTTP(rw, req) + } + }) +} + +func (u *Handler) IndexFile() http.Handler { + return u.indexMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + serveEmbedIndex(u.pathSetting()).ServeHTTP(rw, req) + })) +} diff --git a/pkg/ui/external.go b/pkg/ui/external.go new file mode 100644 index 0000000..1243a9b --- /dev/null +++ b/pkg/ui/external.go @@ -0,0 +1,42 @@ +// +build !embed + +package ui + +import ( + "net/http" + "os" + "path/filepath" +) + +func (u *Handler) ServeAsset() http.Handler { + return u.middleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + http.FileServer(http.Dir(u.pathSetting())).ServeHTTP(rw, req) + })) +} + +func (u *Handler) ServeFaviconDashboard() http.Handler { + return u.middleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + http.FileServer(http.Dir(filepath.Join(u.pathSetting(), "dashboard"))).ServeHTTP(rw, req) + })) +} + +func (u *Handler) IndexFileOnNotFound() http.Handler { + return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + // we ignore directories here because we want those to come from the CDN when running in that mode + if stat, err := os.Stat(filepath.Join(u.pathSetting(), req.URL.Path)); err == nil && !stat.IsDir() { + u.ServeAsset().ServeHTTP(rw, req) + } else { + u.IndexFile().ServeHTTP(rw, req) + } + }) +} + +func (u *Handler) IndexFile() http.Handler { + return u.indexMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { + if path, isURL := u.path(); isURL { + _ = serveIndex(rw, path) + } else { + http.ServeFile(rw, req, filepath.Join(path, "index.html")) + } + })) +} diff --git a/pkg/ui/handler.go b/pkg/ui/handler.go index f630487..fc35f91 100644 --- a/pkg/ui/handler.go +++ b/pkg/ui/handler.go @@ -5,14 +5,16 @@ import ( "io" "io/ioutil" "net/http" - "os" - "path/filepath" "sync" "github.com/rancher/apiserver/pkg/middleware" "github.com/sirupsen/logrus" ) +const ( + defaultPath = "./ui" +) + var ( insecureClient = &http.Client{ Transport: &http.Transport{ @@ -24,10 +26,6 @@ var ( } ) -const ( - defaultPath = "./ui" -) - type StringSetting func() string type BoolSetting func() bool @@ -104,17 +102,6 @@ func NewUIHandler(opts *Options) *Handler { return h } -func (u *Handler) canDownload(url string) bool { - u.downloadOnce.Do(func() { - if err := serveIndex(ioutil.Discard, url); err == nil { - u.downloadSuccess = true - } else { - logrus.Errorf("Failed to download %s, falling back to packaged UI", url) - } - }) - return u.downloadSuccess -} - func (u *Handler) path() (path string, isURL bool) { switch u.offlineSetting() { case "dynamic": @@ -132,37 +119,15 @@ func (u *Handler) path() (path string, isURL bool) { } } -func (u *Handler) ServeAsset() http.Handler { - return u.middleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - http.FileServer(http.Dir(u.pathSetting())).ServeHTTP(rw, req) - })) -} - -func (u *Handler) ServeFaviconDashboard() http.Handler { - return u.middleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - http.FileServer(http.Dir(filepath.Join(u.pathSetting(), "dashboard"))).ServeHTTP(rw, req) - })) -} - -func (u *Handler) IndexFileOnNotFound() http.Handler { - return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - // we ignore directories here because we want those to come from the CDN when running in that mode - if stat, err := os.Stat(filepath.Join(u.pathSetting(), req.URL.Path)); err == nil && !stat.IsDir() { - u.ServeAsset().ServeHTTP(rw, req) +func (u *Handler) canDownload(url string) bool { + u.downloadOnce.Do(func() { + if err := serveIndex(ioutil.Discard, url); err == nil { + u.downloadSuccess = true } else { - u.IndexFile().ServeHTTP(rw, req) + logrus.Errorf("Failed to download %s, falling back to packaged UI", url) } }) -} - -func (u *Handler) IndexFile() http.Handler { - return u.indexMiddleware(http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) { - if path, isURL := u.path(); isURL { - _ = serveIndex(rw, path) - } else { - http.ServeFile(rw, req, filepath.Join(path, "index.html")) - } - })) + return u.downloadSuccess } func serveIndex(resp io.Writer, url string) error { diff --git a/pkg/ui/routes.go b/pkg/ui/routes.go index afa7a4f..b8ea5db 100644 --- a/pkg/ui/routes.go +++ b/pkg/ui/routes.go @@ -5,10 +5,7 @@ import ( "strings" "github.com/gorilla/mux" -) - -var ( - UIOffline = "dynamic" + "github.com/rancher/steve/pkg/version" ) func New(path string) http.Handler { @@ -20,7 +17,13 @@ func New(path string) http.Handler { return path }, Offline: func() string { - return UIOffline + if path != "" { + return "true" + } + return "dynamic" + }, + ReleaseSetting: func() bool { + return version.IsRelease() }, }) diff --git a/pkg/version/version.go b/pkg/version/version.go index bfbfa54..f18e206 100644 --- a/pkg/version/version.go +++ b/pkg/version/version.go @@ -1,12 +1,23 @@ package version -import "fmt" +import ( + "fmt" + "regexp" + "strings" +) var ( Version = "dev" GitCommit = "HEAD" + + // K-EXPLORER + releasePattern = regexp.MustCompile("^v[0-9]") ) func FriendlyVersion() string { return fmt.Sprintf("%s (%s)", Version, GitCommit) } + +func IsRelease() bool { + return !strings.Contains(Version, "dev") && releasePattern.MatchString(Version) +}