mirror of
https://github.com/kubesphere/kubekey.git
synced 2025-12-26 01:22:51 +00:00
feat: add webUI route (#2668)
Signed-off-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
parent
6b9636d144
commit
5ff30bff45
|
|
@ -7,3 +7,9 @@ import "embed"
|
|||
//
|
||||
//go:embed swagger-ui
|
||||
var Swagger embed.FS
|
||||
|
||||
// WebUI embeds the web directory containing the static web UI assets
|
||||
// This allows serving the web UI directly from the binary without needing external files
|
||||
//
|
||||
//go:embed all:web
|
||||
var WebUI embed.FS
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hello World</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -3,10 +3,17 @@ package _const
|
|||
import "k8s.io/apimachinery/pkg/runtime"
|
||||
|
||||
const (
|
||||
// APIPath defines the base path for API endpoints in the KubeKey API server.
|
||||
// This path is used as the prefix for all API routes, including those for
|
||||
// managing inventories, playbooks, and other KubeKey resources.
|
||||
APIPath = "/kapis/"
|
||||
// CoreAPIPath defines the base path for core API endpoints in the KubeKey API server.
|
||||
// All core resource management routes (inventories, playbooks, etc.) are prefixed with this path.
|
||||
CoreAPIPath = "/kapis/"
|
||||
|
||||
// SwaggerAPIPath defines the base path for serving the Swagger UI (OpenAPI documentation).
|
||||
// This is used to provide interactive API documentation for the KubeKey API server.
|
||||
SwaggerAPIPath = "/swagger-ui/"
|
||||
|
||||
// ResourcesAPIPath defines the base path for resource-related endpoints.
|
||||
// This path is used as the prefix for routes that serve static resources, schemas, and related files.
|
||||
ResourcesAPIPath = "/resources/"
|
||||
|
||||
// KubeKeyTag is the tag used for KubeKey related resources
|
||||
// This tag is used to identify and categorize KubeKey-specific resources
|
||||
|
|
|
|||
|
|
@ -39,7 +39,8 @@ func (m webManager) Run(ctx context.Context) error {
|
|||
Add(web.NewCoreService(m.workdir, m.Client, m.Config)).
|
||||
// openapi
|
||||
Add(web.NewSwaggerUIService()).
|
||||
Add(web.NewAPIService(container.RegisteredWebServices()))
|
||||
Add(web.NewAPIService(container.RegisteredWebServices())).
|
||||
Add(web.NewUIService())
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", m.port),
|
||||
|
|
|
|||
|
|
@ -111,72 +111,79 @@ type genCertArgs struct {
|
|||
isCA *bool
|
||||
}
|
||||
|
||||
// signedCertificate generate certificate signed by root certificate
|
||||
func (gca genCertArgs) signedCertificate(cfg *cgutilcert.Config) (string, string) {
|
||||
parentKey, err := TryLoadKeyFromDisk(gca.rootKey)
|
||||
// signedCertificate generates a certificate signed by the root certificate
|
||||
func (gca genCertArgs) signedCertificate(cfg cgutilcert.Config) (string, string) {
|
||||
// Load CA private key
|
||||
caKey, err := TryLoadKeyFromDisk(gca.rootKey)
|
||||
if err != nil {
|
||||
return "", fmt.Sprintf("failed to load root key: %v", err)
|
||||
}
|
||||
parentCert, _, err := TryLoadCertChainFromDisk(gca.rootCert)
|
||||
// Load CA certificate
|
||||
caCert, _, err := TryLoadCertChainFromDisk(gca.rootCert)
|
||||
if err != nil {
|
||||
return "", fmt.Sprintf("failed to load root certificate: %v", err)
|
||||
}
|
||||
|
||||
if gca.policy == policyIfNotPresent {
|
||||
if _, err := TryLoadKeyFromDisk(gca.outKey); err != nil {
|
||||
klog.V(4).InfoS("Failed to load out key, new it")
|
||||
|
||||
goto NEW
|
||||
}
|
||||
|
||||
existCert, intermediates, err := TryLoadCertChainFromDisk(gca.outCert)
|
||||
// Function to generate and write new certificate and key
|
||||
generateAndWrite := func() (string, string) {
|
||||
newKey, err := rsa.GenerateKey(cryptorand.Reader, rsaKeySize)
|
||||
if err != nil {
|
||||
klog.V(4).InfoS("Failed to load out cert, new it")
|
||||
|
||||
goto NEW
|
||||
return "", fmt.Sprintf("generate rsa key error: %v", err)
|
||||
}
|
||||
// check if the existing key and cert match the root key and cert
|
||||
newCert, err := NewSignedCert(cfg, gca.date, newKey, caCert, caKey, ptr.Deref(gca.isCA, false))
|
||||
if err != nil {
|
||||
return "", fmt.Sprintf("failed to generate certificate: %v", err)
|
||||
}
|
||||
if err := WriteKey(gca.outKey, newKey, gca.policy); err != nil {
|
||||
return "", fmt.Sprintf("failed to write key: %v", err)
|
||||
}
|
||||
if err := WriteCert(gca.outCert, newCert, gca.policy); err != nil {
|
||||
return "", fmt.Sprintf("failed to write certificate: %v", err)
|
||||
}
|
||||
return StdoutSuccess, ""
|
||||
}
|
||||
|
||||
switch gca.policy {
|
||||
case policyIfNotPresent:
|
||||
// Check if key exists
|
||||
if _, err := TryLoadKeyFromDisk(gca.outKey); err != nil {
|
||||
klog.V(4).InfoS("Failed to load out key, create it")
|
||||
return generateAndWrite()
|
||||
}
|
||||
// Check if certificate exists
|
||||
existCert, existIntermediates, err := TryLoadCertChainFromDisk(gca.outCert)
|
||||
if err != nil {
|
||||
klog.V(4).InfoS("Failed to load out cert, create it")
|
||||
return generateAndWrite()
|
||||
}
|
||||
// Validate certificate period
|
||||
if err := ValidateCertPeriod(existCert, 0); err != nil {
|
||||
return "", fmt.Sprintf("failed to ValidateCertPeriod: %v", err)
|
||||
}
|
||||
if err := VerifyCertChain(existCert, intermediates, parentCert); err != nil {
|
||||
// Validate certificate chain
|
||||
if err := VerifyCertChain(existCert, existIntermediates, caCert); err != nil {
|
||||
return "", fmt.Sprintf("failed to VerifyCertChain: %v", err)
|
||||
}
|
||||
// Validate certificate SAN and other config
|
||||
if err := validateCertificateWithConfig(existCert, gca.outCert, cfg); err != nil {
|
||||
return "", fmt.Sprintf("failed to validateCertificateWithConfig: %v", err)
|
||||
}
|
||||
|
||||
// Existing certificate and key are valid, skip generation
|
||||
return StdoutSkip, ""
|
||||
default:
|
||||
// Otherwise, always generate new certificate and key
|
||||
return generateAndWrite()
|
||||
}
|
||||
NEW:
|
||||
newKey, err := rsa.GenerateKey(cryptorand.Reader, rsaKeySize)
|
||||
if err != nil {
|
||||
return "", fmt.Sprintf("generate rsa key error: %v", err)
|
||||
}
|
||||
newCert, err := NewSignedCert(*cfg, gca.date, newKey, parentCert, parentKey, ptr.Deref(gca.isCA, false))
|
||||
if err != nil {
|
||||
return "", fmt.Sprintf("failed to generate certificate: %v", err)
|
||||
}
|
||||
|
||||
// write key and cert to file
|
||||
if err := WriteKey(gca.outKey, newKey, gca.policy); err != nil {
|
||||
return "", fmt.Sprintf("failed to write key: %v", err)
|
||||
}
|
||||
if err := WriteCert(gca.outCert, newCert, gca.policy); err != nil {
|
||||
return "", fmt.Sprintf("failed to write certificate: %v", err)
|
||||
}
|
||||
|
||||
return StdoutSuccess, ""
|
||||
}
|
||||
|
||||
// selfSignedCertificate generate Self-signed certificate
|
||||
func (gca genCertArgs) selfSignedCertificate(cfg *cgutilcert.Config) (string, string) {
|
||||
func (gca genCertArgs) selfSignedCertificate(cfg cgutilcert.Config) (string, string) {
|
||||
newKey, err := rsa.GenerateKey(cryptorand.Reader, rsaKeySize)
|
||||
if err != nil {
|
||||
return "", fmt.Sprintf("generate rsa key error: %v", err)
|
||||
}
|
||||
|
||||
newCert, err := NewSelfSignedCACert(*cfg, gca.date, newKey)
|
||||
newCert, err := NewSelfSignedCACert(cfg, gca.date, newKey)
|
||||
if err != nil {
|
||||
return "", fmt.Sprintf("failed to generate self-signed certificate: %v", err)
|
||||
}
|
||||
|
|
@ -239,9 +246,9 @@ func ModuleGenCert(ctx context.Context, options ExecOptions) (string, string) {
|
|||
|
||||
switch {
|
||||
case gca.rootKey == "" || gca.rootCert == "":
|
||||
return gca.selfSignedCertificate(cfg)
|
||||
return gca.selfSignedCertificate(*cfg)
|
||||
default:
|
||||
return gca.signedCertificate(cfg)
|
||||
return gca.signedCertificate(*cfg)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -503,7 +510,7 @@ func VerifyCertChain(cert *x509.Certificate, intermediates []*x509.Certificate,
|
|||
|
||||
// validateCertificateWithConfig makes sure that a given certificate is valid at
|
||||
// least for the SANs defined in the configuration.
|
||||
func validateCertificateWithConfig(cert *x509.Certificate, baseName string, cfg *cgutilcert.Config) error {
|
||||
func validateCertificateWithConfig(cert *x509.Certificate, baseName string, cfg cgutilcert.Config) error {
|
||||
for _, dnsName := range cfg.AltNames.DNSNames {
|
||||
if err := cert.VerifyHostname(dnsName); err != nil {
|
||||
return errors.Wrapf(err, "certificate %s is invalid", baseName)
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ import (
|
|||
func NewCoreService(workdir string, client ctrlclient.Client, config *rest.Config) *restful.WebService {
|
||||
ws := new(restful.WebService).
|
||||
// the GroupVersion might be empty, we need to remove the final /
|
||||
Path(strings.TrimRight(_const.APIPath+kkcorev1.SchemeGroupVersion.String(), "/"))
|
||||
Path(strings.TrimRight(_const.CoreAPIPath+kkcorev1.SchemeGroupVersion.String(), "/"))
|
||||
|
||||
h := newCoreHandler(workdir, client, config)
|
||||
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ package web
|
|||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
||||
"github.com/emicklei/go-restful/v3"
|
||||
"github.com/go-openapi/spec"
|
||||
|
|
@ -14,11 +16,71 @@ import (
|
|||
"github.com/kubesphere/kubekey/v4/version"
|
||||
)
|
||||
|
||||
// NewUIService creates a new WebService that serves the static web UI and handles SPA routing.
|
||||
// - Serves "/" with index.html
|
||||
// - Serves static assets (e.g., .js, .css, .png) from the embedded web directory
|
||||
// - Forwards all other non-API paths to index.html for SPA client-side routing
|
||||
func NewUIService() *restful.WebService {
|
||||
ws := new(restful.WebService)
|
||||
ws.Path("/")
|
||||
|
||||
// Create a sub-filesystem for the embedded web UI assets
|
||||
subFS, err := fs.Sub(config.WebUI, "web")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
fileServer := http.FileServer(http.FS(subFS))
|
||||
|
||||
// Serve the root path "/" with index.html
|
||||
ws.Route(ws.GET("").To(func(req *restful.Request, resp *restful.Response) {
|
||||
data, err := fs.ReadFile(subFS, "index.html")
|
||||
if err != nil {
|
||||
_ = resp.WriteError(http.StatusNotFound, err)
|
||||
return
|
||||
}
|
||||
resp.AddHeader("Content-Type", "text/html")
|
||||
_, _ = resp.Write(data)
|
||||
}))
|
||||
|
||||
// Serve all subpaths:
|
||||
// - If the path matches an API prefix, return 404 to let other WebServices handle it
|
||||
// - If the path looks like a static asset (contains a dot), serve the file
|
||||
// - Otherwise, serve index.html for SPA routing
|
||||
ws.Route(ws.GET("/{subpath:*}").To(func(req *restful.Request, resp *restful.Response) {
|
||||
requestedPath := req.PathParameter("subpath")
|
||||
|
||||
// If the path matches any API route, return 404 so other WebServices can handle it
|
||||
if strings.HasPrefix(requestedPath, strings.TrimLeft(_const.CoreAPIPath, "/")) ||
|
||||
strings.HasPrefix(requestedPath, strings.TrimLeft(_const.SwaggerAPIPath, "/")) ||
|
||||
strings.HasPrefix(requestedPath, strings.TrimLeft(_const.ResourcesAPIPath, "/")) {
|
||||
_ = resp.WriteError(http.StatusNotFound, errors.New("not found"))
|
||||
return
|
||||
}
|
||||
|
||||
// If the path looks like a static asset (e.g., .js, .css, .ico, .png, etc.), serve it
|
||||
if strings.Contains(requestedPath, ".") {
|
||||
fileServer.ServeHTTP(resp.ResponseWriter, req.Request)
|
||||
return
|
||||
}
|
||||
|
||||
// For all other paths, serve index.html (SPA client-side routing)
|
||||
data, err := fs.ReadFile(subFS, "index.html")
|
||||
if err != nil {
|
||||
_ = resp.WriteError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
resp.AddHeader("Content-Type", "text/html")
|
||||
_, _ = resp.Write(data)
|
||||
}))
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
// NewSwaggerUIService creates a new WebService that serves the Swagger UI interface
|
||||
// It mounts the embedded swagger-ui files and handles requests to display the API documentation
|
||||
func NewSwaggerUIService() *restful.WebService {
|
||||
ws := new(restful.WebService)
|
||||
ws.Path("/swagger-ui")
|
||||
ws.Path(strings.TrimRight(_const.SwaggerAPIPath, "/"))
|
||||
|
||||
subFS, err := fs.Sub(config.Swagger, "swagger-ui")
|
||||
if err != nil {
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ import (
|
|||
// The {subpath:*} parameter allows for matching any path under /resources/schema/.
|
||||
func NewSchemaService(rootPath string, workdir string, client ctrlclient.Client) *restful.WebService {
|
||||
ws := new(restful.WebService)
|
||||
ws.Path("/resources").
|
||||
ws.Path(strings.TrimRight(_const.ResourcesAPIPath, "/")).
|
||||
Produces(restful.MIME_JSON, "text/plain")
|
||||
|
||||
h := newSchemaHandler(rootPath, workdir, client)
|
||||
|
|
|
|||
Loading…
Reference in New Issue