refactor: No need to customize steve for explorer

This commit is contained in:
Yuxing Deng 2024-07-16 16:02:03 +08:00
parent cd955243b6
commit faa83722a0
23 changed files with 2989 additions and 46 deletions

2
.gitignore vendored
View File

@ -21,3 +21,5 @@
*.swp
/.vscode
/vendor
/internal/ui/ui/

68
.golangci.json Normal file
View File

@ -0,0 +1,68 @@
{
"linters": {
"disable-all": true,
"enable": [
"govet",
"revive",
"goimports",
"misspell",
"ineffassign",
"gofmt"
]
},
"linters-settings": {
"govet": {
"check-shadowing": false
},
"gofmt": {
"simplify": false
}
},
"run": {
"skip-dirs": [
"vendor",
"tests",
"pkg/client",
"pkg/generated",
"scripts"
],
"tests": false,
"timeout": "10m"
},
"issues": {
"exclude-rules": [
{
"linters": "govet",
"text": "^(nilness|structtag)"
},
{
"path":"pkg/apis/management.cattle.io/v3/globaldns_types.go",
"text":".*lobalDns.*"
},
{
"path": "pkg/apis/management.cattle.io/v3/zz_generated_register.go",
"text":".*lobalDns.*"
},
{
"path":"pkg/apis/management.cattle.io/v3/zz_generated_list_types.go",
"text":".*lobalDns.*"
},
{
"linters": "revive",
"text": "should have comment"
},
{
"linters": "revive",
"text": "should be of the form"
},
{
"linters": "revive",
"text": "by other packages, and that stutters"
},
{
"linters": "typecheck",
"text": "imported but not used as apierrors"
}
]
}
}

View File

@ -1,4 +1,4 @@
FROM registry.suse.com/bci/golang:1.21
FROM registry.suse.com/bci/golang:1.22
ARG PROXY
ARG GOPROXY
ARG DAPPER_HOST_ARCH
@ -17,13 +17,10 @@ RUN if [ "${ARCH}" == "amd64" ]; then \
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s v1.54.2; \
fi
ENV GIT_COMMIT="dafd3fc3b06454bb1b6a2cea68c561bb57482deb" \
GIT_BRANCH="ke/v0.4" \
GIT_SOURCE=${GOPATH}/src/github.com/rancher/steve \
CATTLE_DASHBOARD_UI_VERSION="v2.8.0-kube-explorer-ui-rc3"
ENV CATTLE_DASHBOARD_UI_VERSION="v2.8.0-kube-explorer-ui-rc3"
ENV DAPPER_ENV REPO TAG DRONE_TAG CROSS GOPROXY
ENV DAPPER_SOURCE /opt/kube-explorer
ENV DAPPER_SOURCE /go/src/github.com/cnrancher/kube-explorer
ENV DAPPER_OUTPUT ./bin ./dist
ENV DAPPER_DOCKER_SOCKET true
ENV DAPPER_RUN_ARGS "-v ke-pkg:/go/pkg -v ke-cache:/root/.cache/go-build --privileged"

122
go.mod Normal file
View File

@ -0,0 +1,122 @@
module github.com/cnrancher/kube-explorer
go 1.22.0
replace (
github.com/knative/pkg => github.com/rancher/pkg v0.0.0-20190514055449-b30ab9de040e
github.com/matryer/moq => github.com/rancher/moq v0.0.0-20200712062324-13d1f37d2d77
github.com/rancher/steve => github.com/rancher/steve v0.0.0-20240709130809-47871606146c
k8s.io/client-go => k8s.io/client-go v0.30.1
)
require (
github.com/gorilla/mux v1.8.1
github.com/rancher/apiserver v0.0.0-20240708202538-39a6f2535146
github.com/rancher/steve v0.0.0-20240709130809-47871606146c
github.com/rancher/wrangler/v3 v3.0.0
github.com/sirupsen/logrus v1.9.3
github.com/urfave/cli v1.22.15
golang.org/x/text v0.14.0
k8s.io/api v0.30.1
k8s.io/apimachinery v0.30.1
k8s.io/apiserver v0.30.1
k8s.io/client-go v12.0.0+incompatible
)
require (
github.com/adrg/xdg v0.4.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.2.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dustin/go-humanize v1.0.1 // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
github.com/evanphx/json-patch v5.6.0+incompatible // indirect
github.com/felixge/httpsnoop v1.0.3 // indirect
github.com/ghodss/yaml v1.0.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-openapi/jsonpointer v0.19.6 // indirect
github.com/go-openapi/jsonreference v0.20.2 // indirect
github.com/go-openapi/swag v0.22.3 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/gnostic-models v0.6.8 // indirect
github.com/google/go-cmp v0.6.0 // indirect
github.com/google/gofuzz v1.2.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/gorilla/websocket v1.5.1 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.16.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/josharian/intern v1.0.0 // indirect
github.com/json-iterator/go v1.1.12 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/mxk/go-flowrate v0.0.0-20140419014527-cca7078d478f // indirect
github.com/ncruces/go-strftime v0.1.9 // indirect
github.com/pborman/uuid v1.2.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/prometheus/client_golang v1.16.0 // indirect
github.com/prometheus/client_model v0.4.0 // indirect
github.com/prometheus/common v0.44.0 // indirect
github.com/prometheus/procfs v0.10.1 // indirect
github.com/rancher/dynamiclistener v0.6.0-rc2 // indirect
github.com/rancher/kubernetes-provider-detector v0.1.5 // indirect
github.com/rancher/lasso v0.0.0-20240705194423-b2a060d103c1 // indirect
github.com/rancher/norman v0.0.0-20240708202514-a0127673d1b9 // indirect
github.com/rancher/remotedialer v0.3.2 // indirect
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/spf13/pflag v1.0.5 // indirect
github.com/urfave/cli/v2 v2.27.1 // indirect
github.com/xrash/smetrics v0.0.0-20201216005158-039620a65673 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.44.0 // indirect
go.opentelemetry.io/otel v1.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.19.0 // indirect
go.opentelemetry.io/otel/metric v1.19.0 // indirect
go.opentelemetry.io/otel/sdk v1.19.0 // indirect
go.opentelemetry.io/otel/trace v1.19.0 // indirect
go.opentelemetry.io/proto/otlp v1.0.0 // indirect
golang.org/x/crypto v0.22.0 // indirect
golang.org/x/net v0.24.0 // indirect
golang.org/x/oauth2 v0.16.0 // indirect
golang.org/x/sync v0.7.0 // indirect
golang.org/x/sys v0.19.0 // indirect
golang.org/x/term v0.19.0 // indirect
golang.org/x/time v0.3.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto/googleapis/api v0.0.0-20230726155614-23370e0ffb3e // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20230822172742-b8732ec3820d // indirect
google.golang.org/grpc v1.58.3 // indirect
google.golang.org/protobuf v1.33.0 // indirect
gopkg.in/inf.v0 v0.9.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
k8s.io/apiextensions-apiserver v0.30.1 // indirect
k8s.io/component-base v0.30.1 // indirect
k8s.io/klog v1.0.0 // indirect
k8s.io/klog/v2 v2.120.1 // indirect
k8s.io/kube-aggregator v0.30.1 // indirect
k8s.io/kube-openapi v0.0.0-20240411171206-dc4e619f62f3 // indirect
k8s.io/utils v0.0.0-20230726121419-3b25d923346b // indirect
modernc.org/gc/v3 v3.0.0-20240107210532-573471604cb6 // indirect
modernc.org/libc v1.49.3 // indirect
modernc.org/mathutil v1.6.0 // indirect
modernc.org/memory v1.8.0 // indirect
modernc.org/sqlite v1.29.10 // indirect
modernc.org/strutil v1.2.0 // indirect
modernc.org/token v1.1.0 // indirect
sigs.k8s.io/apiserver-network-proxy/konnectivity-client v0.29.0 // indirect
sigs.k8s.io/cli-utils v0.35.0 // indirect
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
sigs.k8s.io/structured-merge-diff/v4 v4.4.1 // indirect
sigs.k8s.io/yaml v1.3.0 // indirect
)

2003
go.sum Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,80 @@
package cluster
import (
"context"
"errors"
"fmt"
"net/http"
"time"
"github.com/rancher/apiserver/pkg/types"
"github.com/rancher/steve/pkg/podimpersonation"
"github.com/rancher/steve/pkg/resources/cluster"
"github.com/rancher/steve/pkg/server"
"golang.org/x/text/cases"
"golang.org/x/text/language"
)
func Register(_ context.Context, server *server.Server, displayName string) error {
cg := server.ClientFactory
shell := &shell{
cg: cg,
namespace: shellPodNS,
impersonator: podimpersonation.New("shell", cg, time.Hour, func() string { return shellPodImage }),
}
clusterSchema := server.BaseSchemas.LookupSchema("management.cattle.io.cluster")
if clusterSchema == nil {
return errors.New("failed to find management.cattle.io.cluster in base schema")
}
if clusterSchema.LinkHandlers == nil {
clusterSchema.LinkHandlers = make(map[string]http.Handler)
}
clusterSchema.LinkHandlers["shell"] = shell
clusterSchema.Store = func() types.Store {
return &displaynameWrapper{Store: clusterSchema.Store, displayName: displayName}
}()
return nil
}
type displaynameWrapper struct {
types.Store
displayName string
}
func (s *displaynameWrapper) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
obj, err := s.Store.ByID(apiOp, schema, id)
if err != nil {
return obj, err
}
if obj.ID != "local" {
return obj, nil
}
if c, ok := obj.Object.(*cluster.Cluster); ok {
c.Spec.DisplayName = getDisplayNameWithContext(s.displayName)
}
return obj, nil
}
func (s *displaynameWrapper) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) {
rtn, err := s.Store.List(apiOp, schema)
if err != nil {
return rtn, err
}
for _, obj := range rtn.Objects {
if obj.ID != "local" {
continue
}
if c, ok := obj.Object.(*cluster.Cluster); ok {
c.Spec.DisplayName = getDisplayNameWithContext(s.displayName)
}
}
return rtn, nil
}
func getDisplayNameWithContext(CurrentKubeContext string) string {
if CurrentKubeContext != "" {
return fmt.Sprintf("%s Cluster", cases.Title(language.English).String(CurrentKubeContext))
}
return "Local Cluster"
}

View File

@ -0,0 +1,154 @@
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/v3/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"
)
const (
shellPodImage = "rancher/shell:v0.1.20"
shellPodNS = "kube-system"
)
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.impersonator.DeleteRole(ctx, *pod)
}()
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,
},
},
},
}
}

92
internal/server/config.go Normal file
View File

@ -0,0 +1,92 @@
package server
import (
"context"
"net/http"
"strings"
"github.com/rancher/apiserver/pkg/types"
steveauth "github.com/rancher/steve/pkg/auth"
"github.com/rancher/steve/pkg/schema"
"github.com/rancher/steve/pkg/server"
"github.com/rancher/steve/pkg/server/cli"
"github.com/rancher/steve/pkg/server/router"
"github.com/rancher/wrangler/v3/pkg/kubeconfig"
"github.com/rancher/wrangler/v3/pkg/ratelimit"
"github.com/cnrancher/kube-explorer/internal/resources/cluster"
"github.com/cnrancher/kube-explorer/internal/ui"
)
func ToServer(ctx context.Context, c *cli.Config, sqlCache bool) (*server.Server, error) {
var (
auth steveauth.Middleware
)
restConfig, err := kubeconfig.GetNonInteractiveClientConfigWithContext(c.KubeConfig, c.Context).ClientConfig()
if err != nil {
return nil, err
}
restConfig.RateLimiter = ratelimit.None
restConfig.Insecure = insecureSkipTLSVerify
if restConfig.Insecure {
restConfig.CAData = nil
restConfig.CAFile = ""
}
if c.WebhookConfig.WebhookAuthentication {
auth, err = c.WebhookConfig.WebhookMiddleware()
if err != nil {
return nil, err
}
}
controllers, err := server.NewController(restConfig, nil)
if err != nil {
return nil, err
}
steveServer, err := server.New(ctx, restConfig, &server.Options{
AuthMiddleware: auth,
Controllers: controllers,
Next: ui.New(c.UIPath),
SQLCache: sqlCache,
// router needs to hack here
Router: func(h router.Handlers) http.Handler {
return rewriteLocalCluster(router.Routes(h))
},
})
if err != nil {
return nil, err
}
// registrer local cluster
if err := cluster.Register(ctx, steveServer, c.Context); err != nil {
return steveServer, err
}
// wrap default store
steveServer.SchemaFactory.AddTemplate(schema.Template{
Customize: func(a *types.APISchema) {
if a.Store == nil {
return
}
a.Store = &deleteOptionStore{
Store: a.Store,
}
},
})
return steveServer, controllers.Start(ctx)
}
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)
})
}

View File

@ -0,0 +1,16 @@
package server
import (
"github.com/rancher/apiserver/pkg/types"
)
type deleteOptionStore struct {
types.Store
}
func (s *deleteOptionStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) {
query := apiOp.Request.URL.Query()
query.Add("propagationPolicy", "Background")
apiOp.Request.URL.RawQuery = query.Encode()
return s.Store.Delete(apiOp, schema, id)
}

16
internal/server/flags.go Normal file
View File

@ -0,0 +1,16 @@
package server
import (
"github.com/urfave/cli"
)
var insecureSkipTLSVerify bool
func Flags() []cli.Flag {
return []cli.Flag{
cli.BoolFlag{
Name: "insecure-skip-tls-verify",
Destination: &insecureSkipTLSVerify,
},
}
}

91
internal/ui/embed.go Normal file
View File

@ -0,0 +1,91 @@
//go:build embed
package ui
import (
"embed"
"io"
"io/fs"
"net/http"
"path/filepath"
"github.com/sirupsen/logrus"
)
// content holds our static web server content.
//
//go:embed all: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 {
_, 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...))
}

42
internal/ui/external.go Normal file
View File

@ -0,0 +1,42 @@
//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"))
}
}))
}

141
internal/ui/handler.go Normal file
View File

@ -0,0 +1,141 @@
package ui
import (
"crypto/tls"
"io"
"net/http"
"sync"
"github.com/rancher/apiserver/pkg/middleware"
"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 BoolSetting func() bool
type Handler struct {
pathSetting func() string
indexSetting func() string
releaseSetting func() bool
offlineSetting func() string
middleware func(http.Handler) http.Handler
indexMiddleware func(http.Handler) http.Handler
downloadOnce sync.Once
downloadSuccess bool
}
type Options struct {
// The location on disk of the UI files
Path StringSetting
// The HTTP URL of the index file to download
Index StringSetting
// Whether or not to run the UI offline, should return true/false/dynamic
Offline StringSetting
// Whether or not is it release, if true UI will run offline if set to dynamic
ReleaseSetting BoolSetting
}
func NewUIHandler(opts *Options) *Handler {
if opts == nil {
opts = &Options{}
}
h := &Handler{
indexSetting: opts.Index,
offlineSetting: opts.Offline,
pathSetting: opts.Path,
releaseSetting: opts.ReleaseSetting,
middleware: middleware.Chain{
middleware.Gzip,
middleware.FrameOptions,
middleware.CacheMiddleware("json", "js", "css"),
}.Handler,
indexMiddleware: middleware.Chain{
middleware.Gzip,
middleware.NoCache,
middleware.FrameOptions,
middleware.ContentType,
}.Handler,
}
if h.indexSetting == nil {
h.indexSetting = func() string {
return "https://releases.rancher.com/dashboard/latest/index.html"
}
}
if h.offlineSetting == nil {
h.offlineSetting = func() string {
return "dynamic"
}
}
if h.pathSetting == nil {
h.pathSetting = func() string {
return defaultPath
}
}
if h.releaseSetting == nil {
h.releaseSetting = func() bool {
return false
}
}
return h
}
func (u *Handler) path() (path string, isURL bool) {
switch u.offlineSetting() {
case "dynamic":
if u.releaseSetting() {
return u.pathSetting(), false
}
if u.canDownload(u.indexSetting()) {
return u.indexSetting(), true
}
return u.pathSetting(), false
case "true":
return u.pathSetting(), false
default:
return u.indexSetting(), true
}
}
func (u *Handler) canDownload(url string) bool {
u.downloadOnce.Do(func() {
if err := serveIndex(io.Discard, url); err == nil {
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 {
r, err := insecureClient.Get(url)
if err != nil {
return err
}
defer r.Body.Close()
_, err = io.Copy(resp, r.Body)
return err
}

48
internal/ui/routers.go Normal file
View File

@ -0,0 +1,48 @@
package ui
import (
"net/http"
"strings"
"github.com/cnrancher/kube-explorer/internal/version"
"github.com/gorilla/mux"
)
func New(path string) http.Handler {
vue := NewUIHandler(&Options{
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.UseEncodedPath()
router.Handle("/", http.RedirectHandler("/dashboard/", http.StatusFound))
router.Handle("/dashboard", http.RedirectHandler("/dashboard/", http.StatusFound))
router.Handle("/dashboard/", vue.IndexFile())
router.Handle("/favicon.png", vue.ServeFaviconDashboard())
router.Handle("/favicon.ico", vue.ServeFaviconDashboard())
router.PathPrefix("/dashboard/").Handler(vue.IndexFileOnNotFound())
router.PathPrefix("/k8s/clusters/local").HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
url := strings.TrimPrefix(req.URL.Path, "/k8s/clusters/local")
if url == "" {
url = "/"
}
http.Redirect(rw, req, url, http.StatusFound)
})
return router
}

View File

@ -0,0 +1,23 @@
package version
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)
}

54
main.go Normal file
View File

@ -0,0 +1,54 @@
package main
import (
"os"
"github.com/rancher/steve/pkg/debug"
stevecli "github.com/rancher/steve/pkg/server/cli"
"github.com/rancher/steve/pkg/version"
"github.com/rancher/wrangler/v3/pkg/signals"
"github.com/sirupsen/logrus"
"github.com/urfave/cli"
"github.com/cnrancher/kube-explorer/internal/server"
)
var (
config stevecli.Config
debugconfig debug.Config
)
func main() {
app := cli.NewApp()
app.Name = "kube-explorer"
app.Version = version.FriendlyVersion()
app.Usage = ""
app.Flags = joinFlags(
stevecli.Flags(&config),
debug.Flags(&debugconfig),
server.Flags(),
)
app.Action = run
if err := app.Run(os.Args); err != nil {
logrus.Fatal(err)
}
}
func run(_ *cli.Context) error {
ctx := signals.SetupSignalContext()
debugconfig.MustSetupDebug()
s, err := server.ToServer(ctx, &config, false)
if err != nil {
return err
}
return s.ListenAndServe(ctx, config.HTTPSListenPort, config.HTTPListenPort, nil)
}
func joinFlags(flags ...[]cli.Flag) []cli.Flag {
var rtn []cli.Flag
for _, flag := range flags {
rtn = append(rtn, flag...)
}
return rtn
}

View File

@ -1,7 +1,7 @@
FROM registry.suse.com/bci/bci-minimal:15.5
FROM registry.suse.com/bci/bci-minimal:15.6
ARG TARGETARCH
ARG TARGETOS
ENV ARCH=${TARGETARCH:-"amd64"} OS=${TARGETOS:-"linux"}
COPY package/entrypoint.sh /usr/bin/
COPY bin/kube-explorer-${OS}-${ARCH} /usr/bin/kube-explorer
COPY dist/kube-explorer-${OS}-${ARCH} /usr/bin/kube-explorer
ENTRYPOINT ["entrypoint.sh"]

View File

@ -1,19 +1,18 @@
#!/bin/bash
set -e
source $(dirname $0)/version
source "$(dirname $0)/version"
cd "$(dirname $0)/.."
OS_ARCH_ARG_LINUX="amd64 arm arm64"
OS_ARCH_ARG_DARWIN="amd64 arm64"
OS_ARCH_ARG_WINDOWS="amd64"
LD_INJECT_VALUES="-X github.com/rancher/steve/pkg/version.Version=$VERSION
-X github.com/rancher/steve/pkg/version.GitCommit=$COMMIT"
LD_INJECT_VALUES="-X github.com/cnrancher/kube-explorer/internal/version.Version=$VERSION
-X github.com/cnrancher/kube-explorer/internal/version.GitCommit=$COMMIT"
[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
pushd $GIT_SOURCE
case "$CROSS" in
"push")
for ARCH in ${OS_ARCH_ARG_LINUX}; do
@ -62,18 +61,19 @@ case "$CROSS" in
;;
esac
mkdir -p $DAPPER_SOURCE/bin
mkdir -p "$DAPPER_SOURCE/bin"
mkdir -p "$DAPPER_SOURCE/dist"
for f in $(ls ./bin/); do
if [[ $f != *darwin* ]]; then
upx -o $DAPPER_SOURCE/bin/$f bin/$f || true
for f in ./bin/*; do
filename=$(basename "$f")
if [[ $filename != *darwin* ]]; then
upx -o "$DAPPER_SOURCE/dist/$filename" "bin/$filename" || true
fi
if [ -f $DAPPER_SOURCE/bin/$f ]; then
if [ -f "$DAPPER_SOURCE/dist/$filename" ]; then
echo "UPX done!"
else
echo "Copy origin file as UPX failed!!!"
cp bin/$f $DAPPER_SOURCE/bin/$f
cp "bin/$filename" "$DAPPER_SOURCE/dist/$filename"
fi
done
popd

View File

@ -6,13 +6,8 @@ cd $(dirname $0)
[ "$(uname)" != "Darwin" ] && LINKFLAGS="-extldflags -static -s"
pushd $GIT_SOURCE
CGO_ENABLED=0 go build \
-ldflags \
"$LINKFLAGS" \
-o bin/kube-explorer
mv bin/kube-explorer $DAPPER_SOURCE/bin/
popd

View File

@ -1,17 +1,18 @@
#!/bin/bash
mkdir -p $(dirname $GIT_SOURCE)
source $(dirname $0)/version
pushd $(dirname $GIT_SOURCE)
cd "$(dirname $0)/.." || exit 1;
git clone --depth=1 --branch ${GIT_BRANCH} https://github.com/orangedeng/ke-steve.git steve
cd steve
git reset --hard ${GIT_COMMIT}
if [[ "$(uname)" == "Darwin" ]]; then
TAR_CMD="gtar"
else
TAR_CMD="tar"
fi
mkdir -p pkg/ui/ui/dashboard
cd pkg/ui/ui/dashboard
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 xvzf - --strip-components=2
mkdir -p internal/ui/ui/dashboard
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
cp index.html ../index.html
popd

View File

@ -2,10 +2,11 @@
set -e
mkdir -p bin dist
if [ -e ./scripts/$1 ]; then
if [ -e "./scripts/$1" ]; then
./scripts/"$@"
else
exec "$@"
fi
chown -R $DAPPER_UID:$DAPPER_GID .
chown -R "$DAPPER_UID:$DAPPER_GID" .

View File

@ -2,9 +2,7 @@
set -e
source $(dirname $0)/version
cd "$(dirname $0)/.."
pushd $DAPPER_SOURCE
docker build -f package/Dockerfile -t "cnrancher/kube-explorer:$VERSION" .
docker build -f package/Dockerfile -t cnrancher/kube-explorer:$VERSION .
popd

View File

@ -1,7 +1,8 @@
#!/bin/bash
set -e
source $(dirname $0)/version
pushd $GIT_SOURCE
cd "$(dirname $0)/.."
if ! command -v golangci-lint; then
echo Running: go fmt
@ -9,13 +10,11 @@ if ! command -v golangci-lint; then
exit
fi
#echo Running: golangci-lint
#golangci-lint run
echo Running: golangci-lint
golangci-lint run
echo Tidying up modules
go mod tidy
echo Verifying modules
go mod verify
popd