diff --git a/pkg/apiserver/filters/dynamicresource.go b/pkg/apiserver/filters/dynamicresource.go index 243baef31..9bd900f19 100644 --- a/pkg/apiserver/filters/dynamicresource.go +++ b/pkg/apiserver/filters/dynamicresource.go @@ -2,12 +2,14 @@ package filters import ( "fmt" + "io" "net/http" "github.com/emicklei/go-restful/v3" "k8s.io/apimachinery/pkg/api/meta" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/apiserver/pkg/endpoints/handlers/responsewriters" + "sigs.k8s.io/controller-runtime/pkg/client" "kubesphere.io/kubesphere/pkg/api" "kubesphere.io/kubesphere/pkg/apiserver/query" @@ -15,6 +17,8 @@ import ( "kubesphere.io/kubesphere/pkg/models/resources/v1beta1" ) +var NotSupportedVerbError = fmt.Errorf("not supported verb") + type DynamicResourceHandler struct { v1beta1.ResourceManager serviceErrorHandleFallback restful.ServiceErrorHandleFunction @@ -34,12 +38,6 @@ func (d *DynamicResourceHandler) HandleServiceError(serviceError restful.Service return } - // TODO support write operation and workspace scope API - if req.Request.Method != http.MethodGet { - d.serviceErrorHandleFallback(serviceError, req, w) - return - } - reqInfo, exist := request.RequestInfoFrom(req.Request.Context()) if !exist { responsewriters.InternalError(w, req.Request, fmt.Errorf("no RequestInfo found in the context")) @@ -75,11 +73,38 @@ func (d *DynamicResourceHandler) HandleServiceError(serviceError restful.Service return } + var object client.Object + if reqInfo.Verb == request.VerbCreate || reqInfo.Verb == request.VerbUpdate || reqInfo.Verb == request.VerbPatch { + rawData, err := io.ReadAll(req.Request.Body) + if err != nil { + api.HandleError(w, req, err) + return + } + + object, err = d.CreateObjectFromRawData(gvr, rawData) + if err != nil { + api.HandleError(w, req, err) + return + } + } + var result interface{} - if reqInfo.Verb == "list" { + + switch reqInfo.Verb { + case request.VerbGet: + result, err = d.GetResource(req.Request.Context(), gvr, reqInfo.Namespace, reqInfo.Name) + case request.VerbList: result, err = d.ListResources(req.Request.Context(), gvr, reqInfo.Namespace, query.ParseQueryParameter(req)) - } else { - result, err = d.GetResource(req.Request.Context(), gvr, reqInfo.Name, reqInfo.Namespace) + case request.VerbCreate: + err = d.CreateResource(req.Request.Context(), object) + case request.VerbUpdate: + err = d.UpdateResource(req.Request.Context(), object) + case request.VerbDelete: + err = d.DeleteResource(req.Request.Context(), gvr, reqInfo.Namespace, reqInfo.Name) + case request.VerbPatch: + err = d.PatchResource(req.Request.Context(), object) + default: + err = NotSupportedVerbError } if err != nil { diff --git a/pkg/apiserver/request/requestinfo.go b/pkg/apiserver/request/requestinfo.go index f7b10c78b..f3ca0577c 100644 --- a/pkg/apiserver/request/requestinfo.go +++ b/pkg/apiserver/request/requestinfo.go @@ -39,6 +39,16 @@ import ( "kubesphere.io/kubesphere/pkg/utils/iputil" ) +const ( + VerbCreate = "create" + VerbGet = "get" + VerbList = "list" + VerbUpdate = "update" + VerbDelete = "delete" + VerbWatch = "watch" + VerbPatch = "patch" +) + type RequestInfoResolver interface { NewRequestInfo(req *http.Request) (*RequestInfo, error) } @@ -191,15 +201,15 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er } else { switch req.Method { case "POST": - requestInfo.Verb = "create" + requestInfo.Verb = VerbCreate case "GET", "HEAD": - requestInfo.Verb = "get" + requestInfo.Verb = VerbGet case "PUT": - requestInfo.Verb = "update" + requestInfo.Verb = VerbUpdate case "PATCH": - requestInfo.Verb = "patch" + requestInfo.Verb = VerbPatch case "DELETE": - requestInfo.Verb = "delete" + requestInfo.Verb = VerbDelete default: requestInfo.Verb = "" } @@ -259,7 +269,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er requestInfo.ResourceScope = r.resolveResourceScope(requestInfo) // if there's no name on the request and we thought it was a get before, then the actual verb is a list or a watch - if len(requestInfo.Name) == 0 && requestInfo.Verb == "get" { + if len(requestInfo.Name) == 0 && requestInfo.Verb == VerbGet { opts := metainternalversion.ListOptions{} if err := metainternalversionscheme.ParameterCodec.DecodeParameters(req.URL.Query(), metav1.SchemeGroupVersion, &opts); err != nil { // An error in parsing request will result in default to "list" and not setting "name" field. @@ -277,9 +287,9 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er } if opts.Watch { - requestInfo.Verb = "watch" + requestInfo.Verb = VerbWatch } else { - requestInfo.Verb = "list" + requestInfo.Verb = VerbList } if opts.FieldSelector != nil { @@ -292,7 +302,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er } // URL forms: /api/v1/watch/namespaces?labelSelector=kubesphere.io/workspace=system-workspace - if requestInfo.Verb == "watch" { + if requestInfo.Verb == VerbWatch { selector := req.URL.Query().Get("labelSelector") if strings.HasPrefix(selector, workspaceSelectorPrefix) { workspace := strings.TrimPrefix(selector, workspaceSelectorPrefix) @@ -302,7 +312,7 @@ func (r *RequestInfoFactory) NewRequestInfo(req *http.Request) (*RequestInfo, er } // if there's no name on the request and we thought it was a delete before, then the actual verb is deletecollection - if len(requestInfo.Name) == 0 && requestInfo.Verb == "delete" { + if len(requestInfo.Name) == 0 && requestInfo.Verb == VerbDelete { requestInfo.Verb = "deletecollection" } diff --git a/pkg/models/resources/v1beta1/interface.go b/pkg/models/resources/v1beta1/interface.go index 11ed88194..51024cec1 100644 --- a/pkg/models/resources/v1beta1/interface.go +++ b/pkg/models/resources/v1beta1/interface.go @@ -21,10 +21,21 @@ import ( type ResourceManager interface { IsServed(schema.GroupVersionResource) (bool, error) + CreateObjectFromRawData(gvr schema.GroupVersionResource, rawData []byte) (client.Object, error) + + CreateResource(ctx context.Context, object client.Object) error + UpdateResource(ctx context.Context, object client.Object) error + PatchResource(ctx context.Context, object client.Object) error + DeleteResource(ctx context.Context, gvr schema.GroupVersionResource, namespace string, name string) error GetResource(ctx context.Context, gvr schema.GroupVersionResource, namespace string, name string) (client.Object, error) ListResources(ctx context.Context, gvr schema.GroupVersionResource, namespace string, query *query.Query) (client.ObjectList, error) + Get(ctx context.Context, namespace, name string, object client.Object) error List(ctx context.Context, namespace string, query *query.Query, object client.ObjectList) error + Create(ctx context.Context, object client.Object) error + Delete(ctx context.Context, object client.Object) error + Update(ctx context.Context, old, new client.Object) error + Patch(ctx context.Context, old, new client.Object) error } // CompareFunc return true is left greater than right diff --git a/pkg/models/resources/v1beta1/resourcemanager.go b/pkg/models/resources/v1beta1/resourcemanager.go index 6e16b8181..fd1607da3 100644 --- a/pkg/models/resources/v1beta1/resourcemanager.go +++ b/pkg/models/resources/v1beta1/resourcemanager.go @@ -2,6 +2,7 @@ package v1beta1 import ( "context" + "encoding/json" "strings" "k8s.io/apimachinery/pkg/api/errors" @@ -19,7 +20,8 @@ import ( const labelResourceServed = "kubesphere.io/resource-served" -// TODO If delete the crd at the cluster when ks is running, the client.cache doesn`t return err but empty result +// Note that: If delete the crd at the cluster when is running, the client.cache does not return err but empty result + func New(client client.Client, cache cache.Cache) ResourceManager { return &resourceManager{ client: client, @@ -57,6 +59,39 @@ func (h *resourceManager) GetResource(ctx context.Context, gvr schema.GroupVersi return obj, nil } +func (h *resourceManager) CreateObjectFromRawData(gvr schema.GroupVersionResource, rawData []byte) (client.Object, error) { + var obj client.Object + gvk, err := h.getGVK(gvr) + if err != nil { + return nil, err + } + + if h.client.Scheme().Recognizes(gvk) { + gvkObject, err := h.client.Scheme().New(gvk) + if err != nil { + return nil, err + } + obj = gvkObject.(client.Object) + } else { + u := &unstructured.Unstructured{} + u.SetGroupVersionKind(gvk) + obj = u + } + + err = json.Unmarshal(rawData, obj) + if err != nil { + return nil, err + } + + // The object`s GroupVersionKind could be overridden if apiVersion and kind of rawData are different + // with GroupVersionKind from url, so that we should check GroupVersionKind after Unmarshal rawDate. + if obj.GetObjectKind().GroupVersionKind().String() != gvk.String() { + return nil, errors.NewBadRequest("wrong resource GroupVersionKind") + } + + return obj, nil +} + func (h *resourceManager) ListResources(ctx context.Context, gvr schema.GroupVersionResource, namespace string, query *query.Query) (client.ObjectList, error) { var obj client.ObjectList @@ -85,6 +120,38 @@ func (h *resourceManager) ListResources(ctx context.Context, gvr schema.GroupVer return obj, nil } +func (h *resourceManager) DeleteResource(ctx context.Context, gvr schema.GroupVersionResource, namespace, name string) error { + resource, err := h.GetResource(ctx, gvr, namespace, name) + if err != nil { + return err + } + return h.Delete(ctx, resource) +} + +func (h *resourceManager) UpdateResource(ctx context.Context, object client.Object) error { + old := object.DeepCopyObject().(client.Object) + err := h.Get(ctx, object.GetNamespace(), object.GetName(), old) + if err != nil { + return err + } + + return h.Update(ctx, old, object) +} + +func (h *resourceManager) PatchResource(ctx context.Context, object client.Object) error { + old := object.DeepCopyObject().(client.Object) + err := h.Get(ctx, object.GetNamespace(), object.GetName(), old) + if err != nil { + return err + } + + return h.Patch(ctx, old, object) +} + +func (h *resourceManager) CreateResource(ctx context.Context, object client.Object) error { + return h.Create(ctx, object) +} + func convertGVKToList(gvk schema.GroupVersionKind) schema.GroupVersionKind { if strings.HasSuffix(gvk.Kind, "List") { return gvk @@ -154,6 +221,23 @@ func (h *resourceManager) List(ctx context.Context, namespace string, query *que return nil } +func (h *resourceManager) Create(ctx context.Context, object client.Object) error { + return h.client.Create(ctx, object) +} + +func (h *resourceManager) Delete(ctx context.Context, object client.Object) error { + return h.client.Delete(ctx, object) +} + +func (h *resourceManager) Update(ctx context.Context, old, new client.Object) error { + new.SetResourceVersion(old.GetResourceVersion()) + return h.client.Update(ctx, new) +} + +func (h *resourceManager) Patch(ctx context.Context, old, new client.Object) error { + return h.client.Patch(ctx, new, client.MergeFrom(old)) +} + func compare(left, right runtime.Object, field query.Field) bool { l, err := meta.Accessor(left) if err != nil {