mirror of
https://github.com/cnrancher/kube-explorer.git
synced 2025-08-18 22:47:02 +00:00
refactor: UI resource logic
- Support embed api-ui resources - The ui-path arg will be applied if provided. Also applied to api-ui resource files
This commit is contained in:
parent
004e4751c8
commit
eacc47482e
@ -20,6 +20,7 @@ RUN if [ "${ARCH}" == "amd64" ]; then \
|
|||||||
fi
|
fi
|
||||||
COPY --from=tools /app/release-notary /usr/local/bin/
|
COPY --from=tools /app/release-notary /usr/local/bin/
|
||||||
ENV CATTLE_DASHBOARD_UI_VERSION="v2.8.0-kube-explorer-ui-rc3"
|
ENV CATTLE_DASHBOARD_UI_VERSION="v2.8.0-kube-explorer-ui-rc3"
|
||||||
|
ENV CATTLE_API_UI_VERSION="1.1.11"
|
||||||
|
|
||||||
ENV DAPPER_ENV REPO TAG DRONE_TAG CROSS GOPROXY SKIP_COMPRESS GITHUB_REPOSITORY GITHUB_TOKEN
|
ENV DAPPER_ENV REPO TAG DRONE_TAG CROSS GOPROXY SKIP_COMPRESS GITHUB_REPOSITORY GITHUB_TOKEN
|
||||||
ENV DAPPER_SOURCE /go/src/github.com/cnrancher/kube-explorer
|
ENV DAPPER_SOURCE /go/src/github.com/cnrancher/kube-explorer
|
||||||
|
@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
var InsecureSkipTLSVerify bool
|
var InsecureSkipTLSVerify bool
|
||||||
var SystemDefaultRegistry string
|
var SystemDefaultRegistry string
|
||||||
|
var APIUIVersion = "1.1.11"
|
||||||
|
|
||||||
var ShellPodImage string
|
var ShellPodImage string
|
||||||
|
|
||||||
@ -24,5 +25,11 @@ func Flags() []cli.Flag {
|
|||||||
Destination: &ShellPodImage,
|
Destination: &ShellPodImage,
|
||||||
Value: "rancher/shell:v0.2.1-rc.7",
|
Value: "rancher/shell:v0.2.1-rc.7",
|
||||||
},
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "apiui-version",
|
||||||
|
Hidden: true,
|
||||||
|
Destination: &APIUIVersion,
|
||||||
|
Value: APIUIVersion,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
11
internal/config/steve.go
Normal file
11
internal/config/steve.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/rancher/steve/pkg/debug"
|
||||||
|
stevecli "github.com/rancher/steve/pkg/server/cli"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
Steve stevecli.Config
|
||||||
|
Debug debug.Config
|
||||||
|
)
|
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/cnrancher/kube-explorer/internal/config"
|
"github.com/cnrancher/kube-explorer/internal/config"
|
||||||
"github.com/cnrancher/kube-explorer/internal/resources/cluster"
|
"github.com/cnrancher/kube-explorer/internal/resources/cluster"
|
||||||
"github.com/cnrancher/kube-explorer/internal/ui"
|
"github.com/cnrancher/kube-explorer/internal/ui"
|
||||||
|
"github.com/cnrancher/kube-explorer/internal/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
func ToServer(ctx context.Context, c *cli.Config, sqlCache bool) (*server.Server, error) {
|
func ToServer(ctx context.Context, c *cli.Config, sqlCache bool) (*server.Server, error) {
|
||||||
@ -48,10 +49,15 @@ func ToServer(ctx context.Context, c *cli.Config, sqlCache bool) (*server.Server
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ui, apiui := ui.New(&ui.Options{
|
||||||
|
ReleaseSetting: version.IsRelease,
|
||||||
|
Path: func() string { return c.UIPath },
|
||||||
|
})
|
||||||
|
|
||||||
steveServer, err := server.New(ctx, restConfig, &server.Options{
|
steveServer, err := server.New(ctx, restConfig, &server.Options{
|
||||||
AuthMiddleware: auth,
|
AuthMiddleware: auth,
|
||||||
Controllers: controllers,
|
Controllers: controllers,
|
||||||
Next: ui.New(c.UIPath),
|
Next: ui,
|
||||||
SQLCache: sqlCache,
|
SQLCache: sqlCache,
|
||||||
// router needs to hack here
|
// router needs to hack here
|
||||||
Router: func(h router.Handlers) http.Handler {
|
Router: func(h router.Handlers) http.Handler {
|
||||||
@ -62,6 +68,8 @@ func ToServer(ctx context.Context, c *cli.Config, sqlCache bool) (*server.Server
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
steveServer.APIServer.CustomAPIUIResponseWriter(apiui.CSS(), apiui.JS(), func() string { return config.APIUIVersion })
|
||||||
|
|
||||||
// registrer local cluster
|
// registrer local cluster
|
||||||
if err := cluster.Register(ctx, steveServer, c.Context); err != nil {
|
if err := cluster.Register(ctx, steveServer, c.Context); err != nil {
|
||||||
return steveServer, err
|
return steveServer, err
|
||||||
|
55
internal/ui/apiui.go
Normal file
55
internal/ui/apiui.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import "github.com/rancher/apiserver/pkg/writer"
|
||||||
|
|
||||||
|
type APIUI struct {
|
||||||
|
offline StringSetting
|
||||||
|
release BoolSetting
|
||||||
|
embed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func apiUI(opt *Options) APIUI {
|
||||||
|
var rtn = APIUI{
|
||||||
|
offline: opt.Offline,
|
||||||
|
release: opt.ReleaseSetting,
|
||||||
|
embed: true,
|
||||||
|
}
|
||||||
|
if rtn.offline == nil {
|
||||||
|
rtn.offline = StaticSetting("dynamic")
|
||||||
|
}
|
||||||
|
if rtn.release == nil {
|
||||||
|
rtn.release = StaticSetting(false)
|
||||||
|
}
|
||||||
|
for _, file := range []string{
|
||||||
|
"ui/api-ui/ui.min.css",
|
||||||
|
"ui/api-ui/ui.min.js",
|
||||||
|
} {
|
||||||
|
if _, err := staticContent.Open(file); err != nil {
|
||||||
|
rtn.embed = false
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a APIUI) content(name string) writer.StringGetter {
|
||||||
|
return func() (rtn string) {
|
||||||
|
switch a.offline() {
|
||||||
|
case "dynamic":
|
||||||
|
if !a.release() && !a.embed {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
case "false":
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a APIUI) CSS() writer.StringGetter {
|
||||||
|
return a.content("/api-ui/ui.min.css")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a APIUI) JS() writer.StringGetter {
|
||||||
|
return a.content("/api-ui/ui.min.js")
|
||||||
|
}
|
24
internal/ui/content/content.go
Normal file
24
internal/ui/content/content.go
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
package content
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type fsFunc func(name string) (fs.File, error)
|
||||||
|
|
||||||
|
func (f fsFunc) Open(name string) (fs.File, error) {
|
||||||
|
return f(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
type fsContent interface {
|
||||||
|
ToFileServer(basePaths ...string) http.Handler
|
||||||
|
Open(name string) (fs.File, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Handler interface {
|
||||||
|
ServeAssets(middleware func(http.Handler) http.Handler, hext http.Handler) http.Handler
|
||||||
|
ServeFaviconDashboard() http.Handler
|
||||||
|
GetIndex() ([]byte, error)
|
||||||
|
Refresh()
|
||||||
|
}
|
97
internal/ui/content/external.go
Normal file
97
internal/ui/content/external.go
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
package content
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/tls"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultIndex = "https://releases.rancher.com/dashboard/latest/index.html"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewExternal(getIndex func() string) Handler {
|
||||||
|
return &externalIndexHandler{
|
||||||
|
getIndexFunc: getIndex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
insecureClient = &http.Client{
|
||||||
|
Transport: &http.Transport{
|
||||||
|
Proxy: http.ProxyFromEnvironment,
|
||||||
|
TLSClientConfig: &tls.Config{
|
||||||
|
InsecureSkipVerify: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
_ Handler = &externalIndexHandler{}
|
||||||
|
)
|
||||||
|
|
||||||
|
type externalIndexHandler struct {
|
||||||
|
sync.RWMutex
|
||||||
|
getIndexFunc func() string
|
||||||
|
current string
|
||||||
|
downloadSuccess *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *externalIndexHandler) ServeAssets(_ func(http.Handler) http.Handler, next http.Handler) http.Handler {
|
||||||
|
return next
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *externalIndexHandler) ServeFaviconDashboard() http.Handler {
|
||||||
|
return http.NotFoundHandler()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *externalIndexHandler) GetIndex() ([]byte, error) {
|
||||||
|
if u.canDownload() {
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
if err := serveIndex(&buffer, u.current); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
return nil, errors.New("external index is not available")
|
||||||
|
}
|
||||||
|
|
||||||
|
func serveIndex(resp io.Writer, url string) error {
|
||||||
|
r, err := insecureClient.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer r.Body.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(resp, r.Body)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *externalIndexHandler) canDownload() bool {
|
||||||
|
u.RLock()
|
||||||
|
rtn := u.downloadSuccess
|
||||||
|
u.RUnlock()
|
||||||
|
if rtn != nil {
|
||||||
|
return *rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
return u.refresh()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *externalIndexHandler) refresh() bool {
|
||||||
|
u.Lock()
|
||||||
|
defer u.RUnlock()
|
||||||
|
|
||||||
|
u.current = u.getIndexFunc()
|
||||||
|
if u.current == "" {
|
||||||
|
u.current = defaultIndex
|
||||||
|
}
|
||||||
|
t := serveIndex(io.Discard, u.current) == nil
|
||||||
|
u.downloadSuccess = &t
|
||||||
|
return t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (u *externalIndexHandler) Refresh() {
|
||||||
|
_ = u.refresh()
|
||||||
|
}
|
71
internal/ui/content/fs.go
Normal file
71
internal/ui/content/fs.go
Normal file
@ -0,0 +1,71 @@
|
|||||||
|
package content
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ Handler = &handler{}
|
||||||
|
|
||||||
|
func newFS(content fsContent) Handler {
|
||||||
|
return &handler{
|
||||||
|
content: content,
|
||||||
|
cacheFS: &sync.Map{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type handler struct {
|
||||||
|
content fsContent
|
||||||
|
cacheFS *sync.Map
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) pathExist(path string) bool {
|
||||||
|
_, err := h.content.Open(path)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) serveContent(basePaths ...string) http.Handler {
|
||||||
|
key := filepath.Join(basePaths...)
|
||||||
|
if rtn, ok := h.cacheFS.Load(key); ok {
|
||||||
|
return rtn.(http.Handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
rtn := h.content.ToFileServer(basePaths...)
|
||||||
|
h.cacheFS.Store(key, rtn)
|
||||||
|
return rtn
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) Refresh() {
|
||||||
|
h.cacheFS.Range(func(key, _ any) bool {
|
||||||
|
h.cacheFS.Delete(key)
|
||||||
|
return true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) ServeAssets(middleware func(http.Handler) http.Handler, next http.Handler) http.Handler {
|
||||||
|
assets := middleware(h.serveContent())
|
||||||
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if h.pathExist(r.URL.Path) {
|
||||||
|
assets.ServeHTTP(w, r)
|
||||||
|
} else {
|
||||||
|
next.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) ServeFaviconDashboard() http.Handler {
|
||||||
|
return h.serveContent("dashboard")
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *handler) GetIndex() ([]byte, error) {
|
||||||
|
path := filepath.Join("dashboard", "index.html")
|
||||||
|
f, err := h.content.Open(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer f.Close()
|
||||||
|
return io.ReadAll(f)
|
||||||
|
}
|
43
internal/ui/content/fs_embed.go
Normal file
43
internal/ui/content/fs_embed.go
Normal file
@ -0,0 +1,43 @@
|
|||||||
|
package content
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewEmbedded(staticContent embed.FS, prefix string) Handler {
|
||||||
|
return newFS(&embedFS{
|
||||||
|
pathPrefix: prefix,
|
||||||
|
staticContent: staticContent,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fsContent = &embedFS{}
|
||||||
|
|
||||||
|
type embedFS struct {
|
||||||
|
pathPrefix string
|
||||||
|
staticContent embed.FS
|
||||||
|
}
|
||||||
|
|
||||||
|
// Open implements fsContent.
|
||||||
|
func (e *embedFS) Open(name string) (fs.File, error) {
|
||||||
|
return e.staticContent.Open(joinEmbedFilepath(e.pathPrefix, name))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ToFileServer implements fsContent.
|
||||||
|
func (e *embedFS) ToFileServer(basePaths ...string) http.Handler {
|
||||||
|
handler := fsFunc(func(name string) (fs.File, error) {
|
||||||
|
assetPath := joinEmbedFilepath(joinEmbedFilepath(basePaths...), name)
|
||||||
|
return e.Open(assetPath)
|
||||||
|
})
|
||||||
|
|
||||||
|
return http.FileServer(http.FS(handler))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *embedFS) Refresh() error { return nil }
|
||||||
|
|
||||||
|
func joinEmbedFilepath(paths ...string) string {
|
||||||
|
return filepath.ToSlash(filepath.Join(paths...))
|
||||||
|
}
|
41
internal/ui/content/fs_filepath.go
Normal file
41
internal/ui/content/fs_filepath.go
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
package content
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
"path/filepath"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewFilepath(getPath func() string) Handler {
|
||||||
|
return newFS(&filepathFS{
|
||||||
|
getPath: getPath,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ fsContent = &filepathFS{}
|
||||||
|
|
||||||
|
type filepathFS struct {
|
||||||
|
getPath func() string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *filepathFS) ToFileServer(basePaths ...string) http.Handler {
|
||||||
|
root := f.getPath()
|
||||||
|
if root == "" {
|
||||||
|
return http.NotFoundHandler()
|
||||||
|
}
|
||||||
|
path := filepath.Join(append([]string{string(root)}, basePaths...)...)
|
||||||
|
return http.FileServer(http.Dir(path))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *filepathFS) Open(name string) (fs.File, error) {
|
||||||
|
root := f.getPath()
|
||||||
|
if root == "" {
|
||||||
|
return nil, errors.New("filepath fs is not ready")
|
||||||
|
}
|
||||||
|
return http.Dir(root).Open(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *filepathFS) Refresh() error {
|
||||||
|
return nil
|
||||||
|
}
|
7
internal/ui/dev.go
Normal file
7
internal/ui/dev.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
//go:build !embed
|
||||||
|
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
var staticContent embed.FS
|
@ -4,88 +4,9 @@ package ui
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"io"
|
|
||||||
"io/fs"
|
|
||||||
"net/http"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
"github.com/sirupsen/logrus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// content holds our static web server content.
|
// content holds our static web server content.
|
||||||
//
|
//
|
||||||
//go:embed all:ui/*
|
//go:embed all:ui/*
|
||||||
var staticContent embed.FS
|
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 {
|
|
||||||
_, err := staticContent.Open(path)
|
|
||||||
return err == nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func openFile(path string) (fs.File, error) {
|
|
||||||
file, err := staticContent.Open(path)
|
|
||||||
if err != nil {
|
|
||||||
logrus.Errorf("openEmbedFile %s err: %v", path, err)
|
|
||||||
}
|
|
||||||
return file, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func serveEmbed(basePaths ...string) http.Handler {
|
|
||||||
handler := fsFunc(func(name string) (fs.File, error) {
|
|
||||||
logrus.Debugf("serveEmbed name: %s", name)
|
|
||||||
assetPath := joinEmbedFilepath(append(basePaths, 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 := joinEmbedFilepath(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(u.pathSetting(), "dashboard").ServeHTTP(rw, req)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *Handler) IndexFileOnNotFound() http.Handler {
|
|
||||||
return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
|
||||||
path := joinEmbedFilepath(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)
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
func joinEmbedFilepath(paths ...string) string {
|
|
||||||
return filepath.ToSlash(filepath.Join(paths...))
|
|
||||||
}
|
|
||||||
|
@ -1,42 +0,0 @@
|
|||||||
//go: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"))
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
@ -1,43 +1,30 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"crypto/tls"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"sync"
|
|
||||||
|
|
||||||
|
"github.com/cnrancher/kube-explorer/internal/ui/content"
|
||||||
"github.com/rancher/apiserver/pkg/middleware"
|
"github.com/rancher/apiserver/pkg/middleware"
|
||||||
"github.com/sirupsen/logrus"
|
"github.com/sirupsen/logrus"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
|
||||||
defaultPath = "./ui"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
insecureClient = &http.Client{
|
|
||||||
Transport: &http.Transport{
|
|
||||||
Proxy: http.ProxyFromEnvironment,
|
|
||||||
TLSClientConfig: &tls.Config{
|
|
||||||
InsecureSkipVerify: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type StringSetting func() string
|
type StringSetting func() string
|
||||||
type BoolSetting func() bool
|
type BoolSetting func() bool
|
||||||
|
|
||||||
|
func StaticSetting[T any](input T) func() T {
|
||||||
|
return func() T {
|
||||||
|
return input
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
type Handler struct {
|
type Handler struct {
|
||||||
|
contentHandlers map[string]content.Handler
|
||||||
pathSetting func() string
|
pathSetting func() string
|
||||||
indexSetting func() string
|
indexSetting func() string
|
||||||
releaseSetting func() bool
|
releaseSetting func() bool
|
||||||
offlineSetting func() string
|
offlineSetting func() string
|
||||||
middleware func(http.Handler) http.Handler
|
middleware func(http.Handler) http.Handler
|
||||||
indexMiddleware func(http.Handler) http.Handler
|
indexMiddleware func(http.Handler) http.Handler
|
||||||
|
|
||||||
downloadOnce sync.Once
|
|
||||||
downloadSuccess bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Options struct {
|
type Options struct {
|
||||||
@ -45,7 +32,7 @@ type Options struct {
|
|||||||
Path StringSetting
|
Path StringSetting
|
||||||
// The HTTP URL of the index file to download
|
// The HTTP URL of the index file to download
|
||||||
Index StringSetting
|
Index StringSetting
|
||||||
// Whether or not to run the UI offline, should return true/false/dynamic
|
// Whether or not to run the UI offline, should return true/false/dynamic/embed
|
||||||
Offline StringSetting
|
Offline StringSetting
|
||||||
// Whether or not is it release, if true UI will run offline if set to dynamic
|
// Whether or not is it release, if true UI will run offline if set to dynamic
|
||||||
ReleaseSetting BoolSetting
|
ReleaseSetting BoolSetting
|
||||||
@ -57,6 +44,7 @@ func NewUIHandler(opts *Options) *Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
h := &Handler{
|
h := &Handler{
|
||||||
|
contentHandlers: make(map[string]content.Handler),
|
||||||
indexSetting: opts.Index,
|
indexSetting: opts.Index,
|
||||||
offlineSetting: opts.Offline,
|
offlineSetting: opts.Offline,
|
||||||
pathSetting: opts.Path,
|
pathSetting: opts.Path,
|
||||||
@ -75,67 +63,76 @@ func NewUIHandler(opts *Options) *Handler {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if h.indexSetting == nil {
|
if h.indexSetting == nil {
|
||||||
h.indexSetting = func() string {
|
h.indexSetting = StaticSetting("")
|
||||||
return "https://releases.rancher.com/dashboard/latest/index.html"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.offlineSetting == nil {
|
if h.offlineSetting == nil {
|
||||||
h.offlineSetting = func() string {
|
h.offlineSetting = StaticSetting("dynamic")
|
||||||
return "dynamic"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.pathSetting == nil {
|
if h.pathSetting == nil {
|
||||||
h.pathSetting = func() string {
|
h.pathSetting = StaticSetting("")
|
||||||
return defaultPath
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if h.releaseSetting == nil {
|
if h.releaseSetting == nil {
|
||||||
h.releaseSetting = func() bool {
|
h.releaseSetting = StaticSetting(false)
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
h.contentHandlers["embed"] = content.NewEmbedded(staticContent, "ui")
|
||||||
|
h.contentHandlers["false"] = content.NewExternal(h.indexSetting)
|
||||||
|
h.contentHandlers["true"] = content.NewFilepath(h.pathSetting)
|
||||||
|
|
||||||
return h
|
return h
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Handler) path() (path string, isURL bool) {
|
func (h *Handler) content() content.Handler {
|
||||||
switch u.offlineSetting() {
|
offline := h.offlineSetting()
|
||||||
case "dynamic":
|
if handler, ok := h.contentHandlers[offline]; ok {
|
||||||
if u.releaseSetting() {
|
return handler
|
||||||
return u.pathSetting(), false
|
|
||||||
}
|
}
|
||||||
if u.canDownload(u.indexSetting()) {
|
embedHandler := h.contentHandlers["embed"]
|
||||||
return u.indexSetting(), true
|
filepathHandler := h.contentHandlers["true"]
|
||||||
|
externalHandler := h.contentHandlers["false"]
|
||||||
|
// default to dynamic
|
||||||
|
switch {
|
||||||
|
case h.pathSetting() != "":
|
||||||
|
if _, err := filepathHandler.GetIndex(); err == nil {
|
||||||
|
return filepathHandler
|
||||||
}
|
}
|
||||||
return u.pathSetting(), false
|
fallthrough
|
||||||
case "true":
|
case h.releaseSetting():
|
||||||
return u.pathSetting(), false
|
// release must use embed first
|
||||||
|
return embedHandler
|
||||||
default:
|
default:
|
||||||
return u.indexSetting(), true
|
// try embed
|
||||||
|
if _, err := embedHandler.GetIndex(); err == nil {
|
||||||
|
return embedHandler
|
||||||
|
}
|
||||||
|
return externalHandler
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (u *Handler) canDownload(url string) bool {
|
func (h *Handler) ServeAssets(next http.Handler) http.Handler {
|
||||||
u.downloadOnce.Do(func() {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := serveIndex(io.Discard, url); err == nil {
|
h.content().ServeAssets(h.middleware, next).ServeHTTP(w, r)
|
||||||
u.downloadSuccess = true
|
|
||||||
} else {
|
|
||||||
logrus.Errorf("Failed to download %s, falling back to packaged UI", url)
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
return u.downloadSuccess
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func serveIndex(resp io.Writer, url string) error {
|
func (h *Handler) ServeFaviconDashboard() http.Handler {
|
||||||
r, err := insecureClient.Get(url)
|
return h.middleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
h.content().ServeFaviconDashboard().ServeHTTP(w, r)
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *Handler) IndexFile() http.Handler {
|
||||||
|
return h.indexMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
rtn, err := h.content().GetIndex()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
logrus.Warnf("failed to serve index with error %v", err)
|
||||||
|
http.NotFoundHandler().ServeHTTP(w, r)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
defer r.Body.Close()
|
w.WriteHeader(http.StatusOK)
|
||||||
|
_, _ = w.Write(rtn)
|
||||||
_, err = io.Copy(resp, r.Body)
|
}))
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
@ -4,29 +4,11 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/cnrancher/kube-explorer/internal/version"
|
|
||||||
"github.com/gorilla/mux"
|
"github.com/gorilla/mux"
|
||||||
)
|
)
|
||||||
|
|
||||||
func New(path string) http.Handler {
|
func New(opt *Options) (http.Handler, APIUI) {
|
||||||
vue := NewUIHandler(&Options{
|
vue := NewUIHandler(opt)
|
||||||
Path: func() string {
|
|
||||||
if path == "" {
|
|
||||||
return defaultPath
|
|
||||||
}
|
|
||||||
return path
|
|
||||||
},
|
|
||||||
Offline: func() string {
|
|
||||||
if path != "" {
|
|
||||||
return "true"
|
|
||||||
}
|
|
||||||
return "dynamic"
|
|
||||||
},
|
|
||||||
ReleaseSetting: func() bool {
|
|
||||||
return version.IsRelease()
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
router := mux.NewRouter()
|
router := mux.NewRouter()
|
||||||
router.UseEncodedPath()
|
router.UseEncodedPath()
|
||||||
|
|
||||||
@ -35,7 +17,8 @@ func New(path string) http.Handler {
|
|||||||
router.Handle("/dashboard/", vue.IndexFile())
|
router.Handle("/dashboard/", vue.IndexFile())
|
||||||
router.Handle("/favicon.png", vue.ServeFaviconDashboard())
|
router.Handle("/favicon.png", vue.ServeFaviconDashboard())
|
||||||
router.Handle("/favicon.ico", vue.ServeFaviconDashboard())
|
router.Handle("/favicon.ico", vue.ServeFaviconDashboard())
|
||||||
router.PathPrefix("/dashboard/").Handler(vue.IndexFileOnNotFound())
|
router.PathPrefix("/dashboard/").Handler(vue.ServeAssets(vue.IndexFile()))
|
||||||
|
router.PathPrefix("/api-ui/").Handler(vue.ServeAssets(http.NotFoundHandler()))
|
||||||
router.PathPrefix("/k8s/clusters/local").HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
router.PathPrefix("/k8s/clusters/local").HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
|
||||||
url := strings.TrimPrefix(req.URL.Path, "/k8s/clusters/local")
|
url := strings.TrimPrefix(req.URL.Path, "/k8s/clusters/local")
|
||||||
if url == "" {
|
if url == "" {
|
||||||
@ -44,5 +27,5 @@ func New(path string) http.Handler {
|
|||||||
http.Redirect(rw, req, url, http.StatusFound)
|
http.Redirect(rw, req, url, http.StatusFound)
|
||||||
})
|
})
|
||||||
|
|
||||||
return router
|
return router, apiUI(opt)
|
||||||
}
|
}
|
||||||
|
15
main.go
15
main.go
@ -14,19 +14,14 @@ import (
|
|||||||
"github.com/cnrancher/kube-explorer/internal/server"
|
"github.com/cnrancher/kube-explorer/internal/server"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
config stevecli.Config
|
|
||||||
debugconfig debug.Config
|
|
||||||
)
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := cli.NewApp()
|
app := cli.NewApp()
|
||||||
app.Name = "kube-explorer"
|
app.Name = "kube-explorer"
|
||||||
app.Version = version.FriendlyVersion()
|
app.Version = version.FriendlyVersion()
|
||||||
app.Usage = ""
|
app.Usage = ""
|
||||||
app.Flags = joinFlags(
|
app.Flags = joinFlags(
|
||||||
stevecli.Flags(&config),
|
stevecli.Flags(&keconfig.Steve),
|
||||||
debug.Flags(&debugconfig),
|
debug.Flags(&keconfig.Debug),
|
||||||
keconfig.Flags(),
|
keconfig.Flags(),
|
||||||
)
|
)
|
||||||
app.Action = run
|
app.Action = run
|
||||||
@ -38,12 +33,12 @@ func main() {
|
|||||||
|
|
||||||
func run(_ *cli.Context) error {
|
func run(_ *cli.Context) error {
|
||||||
ctx := signals.SetupSignalContext()
|
ctx := signals.SetupSignalContext()
|
||||||
debugconfig.MustSetupDebug()
|
keconfig.Debug.MustSetupDebug()
|
||||||
s, err := server.ToServer(ctx, &config, false)
|
s, err := server.ToServer(ctx, &keconfig.Steve, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return s.ListenAndServe(ctx, config.HTTPSListenPort, config.HTTPListenPort, nil)
|
return s.ListenAndServe(ctx, keconfig.Steve.HTTPSListenPort, keconfig.Steve.HTTPListenPort, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func joinFlags(flags ...[]cli.Flag) []cli.Flag {
|
func joinFlags(flags ...[]cli.Flag) []cli.Flag {
|
||||||
|
@ -11,7 +11,8 @@ OS_ARCH_ARG_DARWIN="amd64 arm64"
|
|||||||
OS_ARCH_ARG_WINDOWS="amd64"
|
OS_ARCH_ARG_WINDOWS="amd64"
|
||||||
|
|
||||||
LD_INJECT_VALUES="-X github.com/cnrancher/kube-explorer/internal/version.Version=$VERSION
|
LD_INJECT_VALUES="-X github.com/cnrancher/kube-explorer/internal/version.Version=$VERSION
|
||||||
-X github.com/cnrancher/kube-explorer/internal/version.GitCommit=$COMMIT"
|
-X github.com/cnrancher/kube-explorer/internal/version.GitCommit=$COMMIT
|
||||||
|
-X github.com/cnrancher/kube-explorer/internal/config.APIUIVersion=$CATTLE_API_UI_VERSION"
|
||||||
|
|
||||||
[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
|
[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
|
||||||
|
|
||||||
|
@ -10,9 +10,13 @@ else
|
|||||||
TAR_CMD="tar"
|
TAR_CMD="tar"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
rm -rf internal/ui/ui/*
|
||||||
|
|
||||||
mkdir -p internal/ui/ui/dashboard
|
mkdir -p internal/ui/ui/dashboard
|
||||||
cd internal/ui/ui/dashboard || exit 1;
|
cd internal/ui/ui/dashboard || exit 1;
|
||||||
curl -sL https://pandaria-dashboard-ui.s3.ap-southeast-2.amazonaws.com/release-2.8-cn/kube-explorer-ui/${CATTLE_DASHBOARD_UI_VERSION}.tar.gz | $TAR_CMD xvzf - --strip-components=2
|
curl -sL https://pandaria-dashboard-ui.s3.ap-southeast-2.amazonaws.com/release-2.8-cn/kube-explorer-ui/${CATTLE_DASHBOARD_UI_VERSION}.tar.gz | $TAR_CMD xvzf - --strip-components=2
|
||||||
cp index.html ../index.html
|
cp index.html ../index.html
|
||||||
|
|
||||||
|
mkdir ../api-ui
|
||||||
|
cd ../api-ui || exit 1;
|
||||||
|
curl -sL https://releases.rancher.com/api-ui/${CATTLE_API_UI_VERSION}.tar.gz | $TAR_CMD xvzf - --strip-components=1
|
||||||
|
Loading…
Reference in New Issue
Block a user