mirror of
https://github.com/kubesphere/kubekey.git
synced 2025-12-25 17:12:50 +00:00
* feat: enhance precheck tasks for image registry and network validation - Added a task to ensure successful authentication to the image registry. - Updated existing tasks to provide clearer failure messages for required configurations. - Improved validation for network interfaces and CIDR configurations, ensuring dual-stack support. - Enhanced error handling in the resource handler for playbook creation. Signed-off-by: joyceliu <joyceliu@yunify.com> * feat: enhance configuration and query handling - Added `-trimpath` flag to Go build configuration for improved binary paths. - Updated REST configuration to set QPS and Burst limits for better performance. - Refactored query handling to use string types for field and value, improving type consistency. - Enhanced error handling in resource configuration updates and improved parsing of request bodies. Signed-off-by: joyceliu <joyceliu@yunify.com> * feat: check inventory when it's changed Signed-off-by: joyceliu <joyceliu@yunify.com> * feat: enhance playbook execution and query handling - Added a new optional query parameter `promise` to the playbook and inventory endpoints, allowing for asynchronous execution control. - Introduced a new result state `ResultPending` to indicate ongoing operations. - Refactored the executor function to handle the `promise` parameter, enabling conditional execution of playbooks. - Improved error handling and logging during playbook execution. Signed-off-by: joyceliu <joyceliu@yunify.com> --------- Signed-off-by: joyceliu <joyceliu@yunify.com>
312 lines
16 KiB
Go
312 lines
16 KiB
Go
package web
|
|
|
|
import (
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/cockroachdb/errors"
|
|
restfulspec "github.com/emicklei/go-restful-openapi/v2"
|
|
"github.com/emicklei/go-restful/v3"
|
|
"github.com/go-openapi/spec"
|
|
kkcorev1 "github.com/kubesphere/kubekey/api/core/v1"
|
|
"k8s.io/apimachinery/pkg/types"
|
|
"k8s.io/client-go/rest"
|
|
"k8s.io/klog/v2"
|
|
ctrlclient "sigs.k8s.io/controller-runtime/pkg/client"
|
|
|
|
"github.com/kubesphere/kubekey/v4/config"
|
|
_const "github.com/kubesphere/kubekey/v4/pkg/const"
|
|
"github.com/kubesphere/kubekey/v4/pkg/web/api"
|
|
"github.com/kubesphere/kubekey/v4/pkg/web/handler"
|
|
"github.com/kubesphere/kubekey/v4/pkg/web/query"
|
|
"github.com/kubesphere/kubekey/v4/version"
|
|
)
|
|
|
|
// NewCoreService creates and configures a new RESTful web service for managing inventories and playbooks.
|
|
// It sets up routes for CRUD operations on inventories and playbooks, including pagination, sorting, and filtering.
|
|
func NewCoreService(workdir string, client ctrlclient.Client, restconfig *rest.Config) *restful.WebService {
|
|
ws := new(restful.WebService).
|
|
// the GroupVersion might be empty, we need to remove the final /
|
|
Path(strings.TrimRight(_const.CoreAPIPath+kkcorev1.SchemeGroupVersion.String(), "/"))
|
|
|
|
inventoryHandler := handler.NewInventoryHandler(workdir, restconfig, client)
|
|
// Inventory management routes
|
|
ws.Route(ws.POST("/inventories").To(inventoryHandler.Post).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("create a inventory.").Operation("createInventory").
|
|
Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON).
|
|
Reads(kkcorev1.Inventory{}).
|
|
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Inventory{}))
|
|
|
|
ws.Route(ws.PATCH("/namespaces/{namespace}/inventories/{inventory}").To(inventoryHandler.Patch).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("patch a inventory.").Operation("patchInventory").
|
|
Consumes(string(types.JSONPatchType), string(types.MergePatchType), string(types.ApplyPatchType)).Produces(restful.MIME_JSON).
|
|
Reads(kkcorev1.Inventory{}).
|
|
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
|
|
Param(ws.PathParameter("inventory", "the name of the inventory")).
|
|
Param(ws.QueryParameter("promise", "promise to execute playbook").Required(false).DefaultValue("true")).
|
|
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Inventory{}))
|
|
|
|
ws.Route(ws.GET("/inventories").To(inventoryHandler.List).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("list all inventories.").Operation("listInventory").
|
|
Produces(restful.MIME_JSON).
|
|
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=createTime")).
|
|
Returns(http.StatusOK, _const.StatusOK, api.ListResult[kkcorev1.Inventory]{}))
|
|
|
|
ws.Route(ws.GET("/namespaces/{namespace}/inventories").To(inventoryHandler.List).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("list all inventories in a namespace.").
|
|
Produces(restful.MIME_JSON).Operation("listInventoryInNamespace").
|
|
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
|
|
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=createTime")).
|
|
Returns(http.StatusOK, _const.StatusOK, api.ListResult[kkcorev1.Inventory]{}))
|
|
|
|
ws.Route(ws.GET("/namespaces/{namespace}/inventories/{inventory}").To(inventoryHandler.Info).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("get a inventory in a namespace.").Operation("getInventory").
|
|
Produces(restful.MIME_JSON).
|
|
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
|
|
Param(ws.PathParameter("inventory", "the name of the inventory")).
|
|
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Inventory{}))
|
|
|
|
ws.Route(ws.GET("/namespaces/{namespace}/inventories/{inventory}/hosts").To(inventoryHandler.ListHosts).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("list all hosts in a inventory.").Operation("listInventoryHosts").
|
|
Produces(restful.MIME_JSON).
|
|
Param(ws.PathParameter("namespace", "the namespace of the inventory")).
|
|
Param(ws.PathParameter("inventory", "the name of the inventory")).
|
|
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=createTime")).
|
|
Returns(http.StatusOK, _const.StatusOK, api.ListResult[api.InventoryHostTable]{}))
|
|
|
|
playbookHandler := handler.NewPlaybookHandler(workdir, restconfig, client)
|
|
// Playbook management routes
|
|
ws.Route(ws.POST("/playbooks").To(playbookHandler.Post).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("create a playbook.").Operation("createPlaybook").
|
|
Param(ws.QueryParameter("promise", "promise to execute playbook").Required(false).DefaultValue("true")).
|
|
Consumes(restful.MIME_JSON).Produces(restful.MIME_JSON).
|
|
Reads(kkcorev1.Playbook{}).
|
|
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Playbook{}))
|
|
|
|
ws.Route(ws.GET("/playbooks").To(playbookHandler.List).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("list all playbooks.").Operation("listPlaybook").
|
|
Produces(restful.MIME_JSON).
|
|
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=createTime")).
|
|
Returns(http.StatusOK, _const.StatusOK, api.ListResult[kkcorev1.Playbook]{}))
|
|
|
|
ws.Route(ws.GET("/namespaces/{namespace}/playbooks").To(playbookHandler.List).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("list all playbooks in a namespace.").Operation("listPlaybookInNamespace").
|
|
Produces(restful.MIME_JSON).
|
|
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
|
|
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=createTime")).
|
|
Returns(http.StatusOK, _const.StatusOK, api.ListResult[kkcorev1.Playbook]{}))
|
|
|
|
ws.Route(ws.GET("/namespaces/{namespace}/playbooks/{playbook}").To(playbookHandler.Info).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("get or watch a playbook in a namespace.").Operation("getPlaybook").
|
|
Produces(restful.MIME_JSON).
|
|
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
|
|
Param(ws.PathParameter("playbook", "the name of the playbook")).
|
|
Param(ws.QueryParameter("watch", "set to true to watch this playbook")).
|
|
Returns(http.StatusOK, _const.StatusOK, kkcorev1.Playbook{}))
|
|
|
|
ws.Route(ws.GET("/namespaces/{namespace}/playbooks/{playbook}/log").To(playbookHandler.Log).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("get a playbook execute log.").Operation("getPlaybookLog").
|
|
Produces("text/plain").
|
|
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
|
|
Param(ws.PathParameter("playbook", "the name of the playbook")).
|
|
Returns(http.StatusOK, _const.StatusOK, ""))
|
|
|
|
ws.Route(ws.DELETE("/namespaces/{namespace}/playbooks/{playbook}").To(playbookHandler.Delete).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.KubeKeyTag}).
|
|
Doc("delete a playbook.").Operation("deletePlaybook").
|
|
Produces(restful.MIME_JSON).
|
|
Param(ws.PathParameter("namespace", "the namespace of the playbook")).
|
|
Param(ws.PathParameter("playbook", "the name of the playbook")).
|
|
Returns(http.StatusOK, _const.StatusOK, api.Result{}))
|
|
|
|
return ws
|
|
}
|
|
|
|
// NewSchemaService creates a new WebService that serves schema files from the specified root path.
|
|
// It sets up a route that handles GET requests to /resources/schema/{subpath} and serves files from the rootPath directory.
|
|
// 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(strings.TrimRight(_const.ResourcesAPIPath, "/")).
|
|
Produces(restful.MIME_JSON, "text/plain")
|
|
|
|
resourceHandler := handler.NewResourceHandler(rootPath, workdir, client)
|
|
ws.Route(ws.GET("/ip").To(resourceHandler.ListIP).
|
|
Doc("list available ip from ip cidr").
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.ResourceTag}).
|
|
Param(ws.QueryParameter("cidr", "the cidr for ip").Required(true)).
|
|
Param(ws.QueryParameter("sshPort", "the ssh port for ip").Required(false)).
|
|
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=ip").Required(false).DefaultValue("ip")).
|
|
Returns(http.StatusOK, _const.StatusOK, api.ListResult[api.IPTable]{}))
|
|
|
|
ws.Route(ws.GET("/schema/{subpath:*}").To(resourceHandler.SchemaInfo).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.ResourceTag}))
|
|
|
|
ws.Route(ws.GET("/schema").To(resourceHandler.ListSchema).
|
|
Doc("list all schema as table").
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.ResourceTag}).
|
|
Param(ws.QueryParameter("cluster", "The namespace where the cluster resides").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(resourceHandler.PostConfig).
|
|
Doc("storing user-defined configuration information").
|
|
Reads(struct{}{}).
|
|
Param(ws.QueryParameter("cluster", "The namespace where the cluster resides").Required(false).DefaultValue("default")).
|
|
Param(ws.QueryParameter("inventory", "the inventory of the playbook").Required(false).DefaultValue("default")).
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.ResourceTag}))
|
|
|
|
ws.Route(ws.GET("/schema/config").To(resourceHandler.ConfigInfo).
|
|
Doc("get user-defined configuration information").
|
|
Metadata(restfulspec.KeyOpenAPITags, []string{_const.ResourceTag}))
|
|
|
|
return ws
|
|
}
|
|
|
|
// 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(path string) *restful.WebService {
|
|
ws := new(restful.WebService)
|
|
ws.Path("/")
|
|
|
|
// Create a sub-filesystem for the embedded web UI assets
|
|
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(os.DirFS(path), "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(os.DirFS(path), "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(strings.TrimRight(_const.SwaggerAPIPath, "/"))
|
|
|
|
subFS, err := fs.Sub(config.Swagger, "swagger-ui")
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
fileServer := http.StripPrefix("/swagger-ui/", http.FileServer(http.FS(subFS)))
|
|
|
|
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)
|
|
}).Metadata(restfulspec.KeyOpenAPITags, []string{_const.OpenAPITag}))
|
|
ws.Route(ws.GET("/{subpath:*}").To(func(req *restful.Request, resp *restful.Response) {
|
|
fileServer.ServeHTTP(resp.ResponseWriter, req.Request)
|
|
}).Metadata(restfulspec.KeyOpenAPITags, []string{_const.OpenAPITag}))
|
|
|
|
return ws
|
|
}
|
|
|
|
// NewAPIService creates a new WebService that serves the OpenAPI/Swagger specification
|
|
// It takes a list of WebServices and generates the API documentation
|
|
func NewAPIService(webservice []*restful.WebService) *restful.WebService {
|
|
restconfig := restfulspec.Config{
|
|
WebServices: webservice, // you control what services are visible
|
|
APIPath: "/apidocs.json",
|
|
PostBuildSwaggerObjectHandler: func(swo *spec.Swagger) {
|
|
swo.Info = &spec.Info{
|
|
InfoProps: spec.InfoProps{
|
|
Title: "KubeKey Web API",
|
|
Description: "KubeKey Web OpenAPI",
|
|
Version: version.Get().String(),
|
|
Contact: &spec.ContactInfo{
|
|
ContactInfoProps: spec.ContactInfoProps{
|
|
Name: "KubeKey",
|
|
URL: "https://github.com/kubesphere/kubekey",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
}}
|
|
for _, ws := range webservice {
|
|
klog.V(2).Infof("%s", ws.RootPath())
|
|
}
|
|
|
|
return restfulspec.NewOpenAPIService(restconfig)
|
|
}
|