Add write operation for dynamic resource (#5601)
add write operation for dynamic resource Signed-off-by: wenhaozhou <wenhaozhou@yunify.com>
This commit is contained in:
@@ -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 {
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user