diff --git a/cmd/kk/app/options/web.go b/cmd/kk/app/options/web.go
index 463fb00f..a3d5b14b 100644
--- a/cmd/kk/app/options/web.go
+++ b/cmd/kk/app/options/web.go
@@ -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
}
diff --git a/cmd/kk/app/web.go b/cmd/kk/app/web.go
index 91f1113f..a7d2b4cc 100644
--- a/cmd/kk/app/web.go
+++ b/cmd/kk/app/web.go
@@ -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())
diff --git a/config/file.go b/config/file.go
index 4bc4de1b..4b000a0c 100644
--- a/config/file.go
+++ b/config/file.go
@@ -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
diff --git a/config/web/index.html b/config/web/index.html
deleted file mode 100644
index 50ed8ab6..00000000
--- a/config/web/index.html
+++ /dev/null
@@ -1,10 +0,0 @@
-
-
-
-
- Hello World
-
-
- Hello World
-
-
diff --git a/pkg/manager/manager.go b/pkg/manager/manager.go
index e2ddce57..4ba3a286 100644
--- a/pkg/manager/manager.go
+++ b/pkg/manager/manager.go
@@ -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,
}
diff --git a/pkg/manager/web_manager.go b/pkg/manager/web_manager.go
index a2216876..b116565d 100644
--- a/pkg/manager/web_manager.go
+++ b/pkg/manager/web_manager.go
@@ -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),
diff --git a/pkg/web/api/result.go b/pkg/web/api/result.go
index 3a7380e0..7092c60e 100644
--- a/pkg/web/api/result.go
+++ b/pkg/web/api/result.go
@@ -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
+}
diff --git a/pkg/web/openapi.go b/pkg/web/openapi.go
index 6b7a7ab6..d778c8ad 100644
--- a/pkg/web/openapi.go
+++ b/pkg/web/openapi.go
@@ -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
diff --git a/pkg/web/resources.go b/pkg/web/resources.go
index d8894c52..27adda1d 100644
--- a/pkg/web/resources.go
+++ b/pkg/web/resources.go
@@ -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)
}