mirror of
https://github.com/kubesphere/kubekey.git
synced 2025-12-25 17:12:50 +00:00
fix: add ui-path for kk web (#2670)
Signed-off-by: joyceliu <joyceliu@yunify.com>
This commit is contained in:
parent
5ff30bff45
commit
7b61dafb95
|
|
@ -20,6 +20,7 @@ type KubeKeyWebOptions struct {
|
|||
Workdir string // Workdir specifies the base directory for KubeKey
|
||||
|
||||
SchemaPath string
|
||||
UIPath string
|
||||
}
|
||||
|
||||
// NewKubeKeyWebOptions creates and returns a new KubeKeyWebOptions instance with default values
|
||||
|
|
@ -46,6 +47,7 @@ func (o *KubeKeyWebOptions) Flags() cliflag.NamedFlagSets {
|
|||
wfs.IntVar(&o.Port, "port", o.Port, fmt.Sprintf("the server port of kubekey web default is: %d", o.Port))
|
||||
wfs.StringVar(&o.Workdir, "workdir", o.Workdir, "the base Dir for kubekey. Default current dir. ")
|
||||
wfs.StringVar(&o.SchemaPath, "schema-path", o.SchemaPath, "the json schema dir path to render web ui.")
|
||||
wfs.StringVar(&o.UIPath, "ui-path", o.SchemaPath, "the web ui package path.")
|
||||
|
||||
return fss
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,6 +42,7 @@ func newWebCommand() *cobra.Command {
|
|||
Workdir: o.Workdir,
|
||||
Port: o.Port,
|
||||
SchemaPath: o.SchemaPath,
|
||||
UIPath: o.UIPath,
|
||||
Client: client,
|
||||
Config: restconfig,
|
||||
}).Run(cmd.Context())
|
||||
|
|
|
|||
|
|
@ -7,9 +7,3 @@ 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
|
||||
|
|
|
|||
|
|
@ -1,10 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>Hello World</title>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hello World</h1>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -64,6 +64,7 @@ type WebManagerOptions struct {
|
|||
Workdir string
|
||||
Port int
|
||||
SchemaPath string
|
||||
UIPath string
|
||||
ctrlclient.Client
|
||||
*rest.Config
|
||||
}
|
||||
|
|
@ -74,6 +75,7 @@ func NewWebManager(o WebManagerOptions) Manager {
|
|||
workdir: o.Workdir,
|
||||
port: o.Port,
|
||||
schemaPath: o.SchemaPath,
|
||||
uiPath: o.UIPath,
|
||||
Client: o.Client,
|
||||
Config: o.Config,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,6 +23,7 @@ type webManager struct {
|
|||
workdir string
|
||||
|
||||
schemaPath string
|
||||
uiPath string
|
||||
|
||||
ctrlclient.Client
|
||||
*rest.Config
|
||||
|
|
@ -40,7 +41,7 @@ func (m webManager) Run(ctx context.Context) error {
|
|||
// openapi
|
||||
Add(web.NewSwaggerUIService()).
|
||||
Add(web.NewAPIService(container.RegisteredWebServices())).
|
||||
Add(web.NewUIService())
|
||||
Add(web.NewUIService(m.uiPath))
|
||||
|
||||
server := &http.Server{
|
||||
Addr: fmt.Sprintf(":%d", m.port),
|
||||
|
|
|
|||
|
|
@ -19,6 +19,11 @@ package api
|
|||
const (
|
||||
// SchemaLabelSubfix is the label key used to indicate which schema a playbook belongs to.
|
||||
SchemaLabelSubfix = "kubekey.kubesphere.io/schema"
|
||||
|
||||
// SchemaProductFile is the predefined file name for storing product information.
|
||||
SchemaProductFile = "product.json"
|
||||
// SchemaConfigFile is the predefined file name for caching configuration.
|
||||
SchemaConfigFile = "config.json"
|
||||
)
|
||||
|
||||
const (
|
||||
|
|
@ -70,24 +75,26 @@ type InventoryHostGroups struct {
|
|||
Index int `json:"index"` // the index of groups which hosts belong to
|
||||
}
|
||||
|
||||
// SchemaTable represents schema metadata for a resource.
|
||||
// It includes fields such as name, type, title, description, version, namespace, logo, priority, and associated playbooks.
|
||||
// The Playbook field is a slice of SchemaTablePlaybook, each representing a playbook reference.
|
||||
type SchemaTable struct {
|
||||
Name string `json:"name"` // Name of schema, defined by filename
|
||||
SchemaType string `json:"schemaType"` // Type of the schema (e.g., CRD, built-in)
|
||||
Title string `json:"title"` // Title of the schema
|
||||
Description string `json:"description"` // Description of the schema
|
||||
Version string `json:"version"` // Version of the schema
|
||||
Namespace string `json:"namespace"` // Namespace of the schema
|
||||
Logo string `json:"logo"` // Logo URL or identifier
|
||||
Priority int `json:"priority"` // Priority for display or ordering
|
||||
Playbook SchemaTablePlaybook `json:"playbook"` // List of reference playbooks
|
||||
PlaybookPath map[string]string `json:"playbookPath,omitempty"` // PlaybookPath for current schema
|
||||
// SchemaFileDataSchema represents the metadata section of a schema file as used in the API layer.
|
||||
// It contains the main data schema metadata such as title, description, version, namespace, logo, and priority.
|
||||
type SchemaFileDataSchema struct {
|
||||
Title string `json:"title"` // Title of the schema
|
||||
Description string `json:"description"` // Description of the schema
|
||||
Version string `json:"version"` // Version of the schema
|
||||
Namespace string `json:"namespace"` // Namespace of the schema
|
||||
Logo string `json:"logo"` // Logo URL or identifier
|
||||
Priority int `json:"priority"` // Priority for display or ordering
|
||||
}
|
||||
|
||||
// SchemaTablePlaybook represents a reference to a playbook associated with a schema.
|
||||
// It includes the playbook's name, namespace, and phase.
|
||||
// SchemaFile represents the structure of a schema file as used in the API layer.
|
||||
// It contains the main data schema metadata and a mapping of playbook labels to their paths.
|
||||
type SchemaFile struct {
|
||||
DataSchema SchemaFileDataSchema `json:"dataSchema"` // Metadata of the schema file
|
||||
PlaybookPath map[string]string `json:"playbookPath"` // Mapping of playbook labels to their file paths
|
||||
}
|
||||
|
||||
// SchemaTablePlaybook represents the details of a playbook associated with a schema in the response table.
|
||||
// It includes the path, name, namespace, phase, and result of the playbook.
|
||||
type SchemaTablePlaybook struct {
|
||||
Path string `json:"path"` // Path of playbook template.
|
||||
Name string `json:"name"` // Name of the playbook
|
||||
|
|
@ -96,6 +103,20 @@ type SchemaTablePlaybook struct {
|
|||
Result any `json:"result"` // Result of the playbook
|
||||
}
|
||||
|
||||
// SchemaTable represents the response table constructed from a schema file.
|
||||
// It includes metadata fields such as name, title, description, version, namespace, logo, and priority.
|
||||
// The Playbook field is a map of playbook labels to SchemaTablePlaybook, each representing a playbook reference.
|
||||
type SchemaTable struct {
|
||||
Name string `json:"name"` // Name of schema, defined by filename
|
||||
Title string `json:"title"` // Title of the schema
|
||||
Description string `json:"description"` // Description of the schema
|
||||
Version string `json:"version"` // Version of the schema
|
||||
Namespace string `json:"namespace"` // Namespace of the schema
|
||||
Logo string `json:"logo"` // Logo URL or identifier
|
||||
Priority int `json:"priority"` // Priority for display or ordering
|
||||
Playbook map[string]SchemaTablePlaybook `json:"playbook"` // Map of playbook labels to playbook details
|
||||
}
|
||||
|
||||
// IPTable represents an IP address entry and its SSH status information.
|
||||
// It indicates whether the IP is a localhost, if SSH is reachable, and if SSH authorization is present.
|
||||
type IPTable struct {
|
||||
|
|
@ -105,3 +126,27 @@ type IPTable struct {
|
|||
SSHReachable bool `json:"sshReachable"` // Whether SSH port is reachable on this IP
|
||||
SSHAuthorized bool `json:"sshAuthorized"` // Whether SSH is authorized for this IP
|
||||
}
|
||||
|
||||
// SchemaFile2Table converts a SchemaFile and its filename into a SchemaTable structure.
|
||||
// It initializes the SchemaTable fields from the SchemaFile's DataSchema and sets up the Playbook map
|
||||
// with playbook labels and their corresponding paths. Other playbook fields are left empty for later population.
|
||||
func SchemaFile2Table(schemaFile SchemaFile, filename string) SchemaTable {
|
||||
table := SchemaTable{
|
||||
Name: filename,
|
||||
Title: schemaFile.DataSchema.Title,
|
||||
Description: schemaFile.DataSchema.Description,
|
||||
Version: schemaFile.DataSchema.Version,
|
||||
Namespace: schemaFile.DataSchema.Namespace,
|
||||
Logo: schemaFile.DataSchema.Logo,
|
||||
Priority: schemaFile.DataSchema.Priority,
|
||||
Playbook: make(map[string]SchemaTablePlaybook),
|
||||
}
|
||||
// Populate the Playbook map with playbook labels and their paths from the schema file.
|
||||
for k, v := range schemaFile.PlaybookPath {
|
||||
table.Playbook[k] = SchemaTablePlaybook{
|
||||
Path: v,
|
||||
}
|
||||
}
|
||||
|
||||
return table
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package web
|
|||
import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/cockroachdb/errors"
|
||||
|
|
@ -20,20 +21,16 @@ import (
|
|||
// - 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 {
|
||||
func NewUIService(path string) *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))
|
||||
fileServer := http.FileServer(http.FS(os.DirFS(path)))
|
||||
|
||||
// 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")
|
||||
data, err := fs.ReadFile(os.DirFS(path), "index.html")
|
||||
if err != nil {
|
||||
_ = resp.WriteError(http.StatusNotFound, err)
|
||||
return
|
||||
|
|
@ -64,7 +61,7 @@ func NewUIService() *restful.WebService {
|
|||
}
|
||||
|
||||
// For all other paths, serve index.html (SPA client-side routing)
|
||||
data, err := fs.ReadFile(subFS, "index.html")
|
||||
data, err := fs.ReadFile(os.DirFS(path), "index.html")
|
||||
if err != nil {
|
||||
_ = resp.WriteError(http.StatusInternalServerError, err)
|
||||
return
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@ package web
|
|||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/netip"
|
||||
|
|
@ -58,15 +59,22 @@ func NewSchemaService(rootPath string, workdir string, client ctrlclient.Client)
|
|||
ws.Route(ws.GET("/schema").To(h.allSchema).
|
||||
Doc("list all schema as table").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.ResourceTag}).
|
||||
Param(ws.QueryParameter("schemaType", "the type of schema json").Required(false)).
|
||||
Param(ws.QueryParameter("playbookLabel", "the reference playbook of schema. eg: \"install.kubekey.kubesphere.io/schema\", \"check.kubekey.kubesphere.io/schema\" "+
|
||||
"if empty will not return any reference playbook").Required(false)).
|
||||
Param(ws.PathParameter("namespace", "the namespace of the playbook").Required(false).DefaultValue("default")).
|
||||
Param(ws.QueryParameter(query.ParameterPage, "page").Required(false).DataFormat("page=%d")).
|
||||
Param(ws.QueryParameter(query.ParameterLimit, "limit").Required(false)).
|
||||
Param(ws.QueryParameter(query.ParameterAscending, "sort parameters, e.g. reverse=true").Required(false).DefaultValue("false")).
|
||||
Param(ws.QueryParameter(query.ParameterOrderBy, "sort parameters, e.g. orderBy=priority")).
|
||||
Returns(http.StatusOK, _const.StatusOK, api.ListResult[api.SchemaTable]{}))
|
||||
|
||||
ws.Route(ws.POST("/schema/config").To(h.storeConfig).
|
||||
Doc("storing user-defined configuration information").
|
||||
Reads(struct{}{}).
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.ResourceTag}))
|
||||
|
||||
ws.Route(ws.GET("/schema/config").To(h.configInfo).
|
||||
Doc("get user-defined configuration information").
|
||||
Metadata(restfulspec.KeyOpenAPITags, []string{_const.ResourceTag}))
|
||||
|
||||
return ws
|
||||
}
|
||||
|
||||
|
|
@ -82,6 +90,35 @@ type schemaHandler struct {
|
|||
client ctrlclient.Client
|
||||
}
|
||||
|
||||
func (h schemaHandler) configInfo(request *restful.Request, response *restful.Response) {
|
||||
file, err := os.Open(filepath.Join(h.rootPath, api.SchemaConfigFile))
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
_ = response.WriteError(http.StatusNotFound, err)
|
||||
} else {
|
||||
_ = response.WriteError(http.StatusInternalServerError, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
_, _ = io.Copy(response.ResponseWriter, file)
|
||||
}
|
||||
|
||||
func (h schemaHandler) storeConfig(request *restful.Request, response *restful.Response) {
|
||||
file, err := os.OpenFile(filepath.Join(h.rootPath, api.SchemaConfigFile), os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
_ = response.WriteError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = io.Copy(file, request.Request.Body)
|
||||
if err != nil {
|
||||
_ = response.WriteError(http.StatusInternalServerError, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func (h schemaHandler) listIP(request *restful.Request, response *restful.Response) {
|
||||
queryParam := query.ParseQueryParameter(request)
|
||||
cidr, ok := queryParam.Filters["cidr"]
|
||||
|
|
@ -193,8 +230,7 @@ func (h schemaHandler) schemaInfo(request *restful.Request, response *restful.Re
|
|||
// It supports filtering, sorting, and pagination via query parameters.
|
||||
func (h schemaHandler) allSchema(request *restful.Request, response *restful.Response) {
|
||||
queryParam := query.ParseQueryParameter(request)
|
||||
playbookLabel := string(queryParam.Filters["playbookLabel"])
|
||||
// Get all entries in the rootPath directory.
|
||||
// Read all entries in the rootPath directory.
|
||||
entries, err := os.ReadDir(h.rootPath)
|
||||
if err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
|
|
@ -202,9 +238,10 @@ func (h schemaHandler) allSchema(request *restful.Request, response *restful.Res
|
|||
}
|
||||
schemaTable := make([]api.SchemaTable, 0)
|
||||
for _, entry := range entries {
|
||||
if entry.IsDir() || // Skip directories.
|
||||
!strings.HasSuffix(entry.Name(), ".json") || // Only process files with .json suffix.
|
||||
entry.Name() == "product.json" { // "product.json" is agreed file name
|
||||
// Skip directories, non-JSON files, and special schema files.
|
||||
if entry.IsDir() ||
|
||||
!strings.HasSuffix(entry.Name(), ".json") ||
|
||||
entry.Name() == api.SchemaProductFile || entry.Name() == api.SchemaConfigFile {
|
||||
continue
|
||||
}
|
||||
// Read the JSON file.
|
||||
|
|
@ -213,50 +250,49 @@ func (h schemaHandler) allSchema(request *restful.Request, response *restful.Res
|
|||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
schema := api.SchemaTable{Name: entry.Name()}
|
||||
var schemaFile api.SchemaFile
|
||||
// Unmarshal the JSON data into a SchemaTable struct.
|
||||
if err := json.Unmarshal(data, &schema); err != nil {
|
||||
if err := json.Unmarshal(data, &schemaFile); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
// get reference playbook
|
||||
if playbookLabel != "" {
|
||||
playbookList := &kkcorev1.PlaybookList{}
|
||||
if err := h.client.List(request.Request.Context(), playbookList, ctrlclient.MatchingLabels{
|
||||
playbookLabel: entry.Name(),
|
||||
}); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
switch len(playbookList.Items) {
|
||||
case 0: // skip
|
||||
case 1:
|
||||
item := &playbookList.Items[0]
|
||||
var result any
|
||||
if len(item.Status.Result.Raw) != 0 {
|
||||
if err := json.Unmarshal(item.Status.Result.Raw, &result); err != nil {
|
||||
api.HandleBadRequest(response, request, errors.Errorf("failed to unmarshal result from playbook of schema %q", schema.Name))
|
||||
// Get all playbooks in the given namespace.
|
||||
playbookList := &kkcorev1.PlaybookList{}
|
||||
if err := h.client.List(request.Request.Context(), playbookList, ctrlclient.InNamespace(request.PathParameter("namespace"))); err != nil {
|
||||
api.HandleBadRequest(response, request, err)
|
||||
return
|
||||
}
|
||||
schema := api.SchemaFile2Table(schemaFile, entry.Name())
|
||||
// For each playbook, if it matches a label in schema.Playbook and the label value equals schema.Name, add its info.
|
||||
for _, playbook := range playbookList.Items {
|
||||
for label, schemaName := range playbook.Labels {
|
||||
// Only process playbooks whose label is defined in schema.Playbook and value matches schema.Name.
|
||||
if _, ok := schema.Playbook[label]; ok && schemaName == schema.Name {
|
||||
// If a playbook for this label already exists, return an error.
|
||||
if schema.Playbook[label].Name != "" {
|
||||
api.HandleBadRequest(response, request, errors.Errorf("schema %q has multiple playbooks of label %q", entry.Name(), label))
|
||||
return
|
||||
}
|
||||
var result any
|
||||
// If the playbook has a result, unmarshal it.
|
||||
if len(playbook.Status.Result.Raw) != 0 {
|
||||
if err := json.Unmarshal(playbook.Status.Result.Raw, &result); err != nil {
|
||||
api.HandleBadRequest(response, request, errors.Errorf("failed to unmarshal result from playbook of schema %q", schema.Name))
|
||||
return
|
||||
}
|
||||
}
|
||||
// Fill in playbook info for this label.
|
||||
schema.Playbook[label] = api.SchemaTablePlaybook{
|
||||
Path: schema.Playbook[label].Path,
|
||||
Name: playbook.Name,
|
||||
Namespace: playbook.Namespace,
|
||||
Phase: string(playbook.Status.Phase),
|
||||
Result: result,
|
||||
}
|
||||
}
|
||||
schema.Playbook = api.SchemaTablePlaybook{
|
||||
Path: schema.PlaybookPath[playbookLabel],
|
||||
Name: item.Name,
|
||||
Namespace: item.Namespace,
|
||||
Phase: string(item.Status.Phase),
|
||||
Result: result,
|
||||
}
|
||||
default:
|
||||
playbookNames := make([]string, 0, len(playbookList.Items))
|
||||
for _, playbook := range playbookList.Items {
|
||||
playbookNames = append(playbookNames, playbook.Name)
|
||||
}
|
||||
api.HandleBadRequest(response, request, errors.Errorf("schema %q has multiple playbooks: %q", entry.Name(), playbookNames))
|
||||
return
|
||||
}
|
||||
}
|
||||
// clear PlaybookPath
|
||||
schema.PlaybookPath = nil
|
||||
// Add the processed schema to the schemaTable slice.
|
||||
schemaTable = append(schemaTable, schema)
|
||||
}
|
||||
// less is a comparison function for sorting SchemaTable items by a given field.
|
||||
|
|
@ -269,6 +305,7 @@ func (h schemaHandler) allSchema(request *restful.Request, response *restful.Res
|
|||
case reflect.Int, reflect.Int64:
|
||||
return leftVal.Int() > rightVal.Int()
|
||||
default:
|
||||
// If the field is not a string or int, sort by Priority as a fallback.
|
||||
return left.Priority > right.Priority
|
||||
}
|
||||
}
|
||||
|
|
@ -290,6 +327,7 @@ func (h schemaHandler) allSchema(request *restful.Request, response *restful.Res
|
|||
}
|
||||
|
||||
// Use the DefaultList function to apply filtering, sorting, and pagination.
|
||||
// The results variable contains the filtered, sorted, and paginated schemaTable.
|
||||
results := query.DefaultList(schemaTable, queryParam, less, filter)
|
||||
_ = response.WriteEntity(results)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue