diff --git a/pkg/schemaserver/types/server_types.go b/pkg/schemaserver/types/server_types.go index 975177c..a985e7f 100644 --- a/pkg/schemaserver/types/server_types.go +++ b/pkg/schemaserver/types/server_types.go @@ -50,7 +50,7 @@ func (r *RawResource) MarshalJSON() ([]byte, error) { return nil, err } - if len(data) < 2 || data[0] != '{' || data[len(data)-1] != '}' { + if len(data) < 3 || data[0] != '{' || data[len(data)-1] != '}' { return outer, nil } @@ -288,3 +288,8 @@ func FormatterChain(formatter Formatter, next Formatter) Formatter { next(request, resource) } } + +func (r *APIRequest) Clone() *APIRequest { + clone := *r + return &clone +} diff --git a/pkg/server/resources/schema.go b/pkg/server/resources/schema.go index e00ae7a..326ca2a 100644 --- a/pkg/server/resources/schema.go +++ b/pkg/server/resources/schema.go @@ -11,13 +11,16 @@ import ( "github.com/rancher/steve/pkg/server/resources/apigroups" "github.com/rancher/steve/pkg/server/resources/common" "github.com/rancher/steve/pkg/server/resources/counts" + "github.com/rancher/steve/pkg/server/resources/userpreferences" + "github.com/rancher/steve/pkg/server/store/proxy" "k8s.io/client-go/discovery" ) -func DefaultSchemas(baseSchema *types.APISchemas, ccache clustercache.ClusterCache) *types.APISchemas { +func DefaultSchemas(baseSchema *types.APISchemas, ccache clustercache.ClusterCache, cg proxy.ClientGetter) *types.APISchemas { counts.Register(baseSchema, ccache) subscribe.Register(baseSchema) apiroot.Register(baseSchema, []string{"v1"}, []string{"proxy:/apis"}) + userpreferences.Register(baseSchema, cg) return baseSchema } diff --git a/pkg/server/resources/userpreferences/configmap.go b/pkg/server/resources/userpreferences/configmap.go new file mode 100644 index 0000000..eab5ee9 --- /dev/null +++ b/pkg/server/resources/userpreferences/configmap.go @@ -0,0 +1,103 @@ +package userpreferences + +import ( + "github.com/rancher/steve/pkg/schemaserver/store/empty" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/server/store/proxy" + "github.com/rancher/wrangler/pkg/data" + "github.com/rancher/wrangler/pkg/data/convert" + "github.com/rancher/wrangler/pkg/schemas/validation" + apierrors "k8s.io/apimachinery/pkg/api/errors" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" +) + +type configMapStore struct { + empty.Store + cg proxy.ClientGetter +} + +func (e *configMapStore) getClient(apiOp *types.APIRequest) (dynamic.ResourceInterface, error) { + cmSchema := apiOp.Schemas.LookupSchema("configmap") + if cmSchema == nil { + return nil, validation.NotFound + } + + return e.cg.AdminClient(apiOp, cmSchema, "kube-system") +} + +func (e *configMapStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + u := getUser(apiOp) + client, err := e.getClient(apiOp) + if err != nil { + return types.APIObject{}, err + } + + pref := &UserPreference{ + Data: map[string]string{}, + } + result := types.APIObject{ + Type: "userpreference", + ID: u.GetName(), + Object: pref, + } + + obj, err := client.Get(prefName(u), metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + return result, nil + } + + d := data.Object(obj.Object).Map("data") + return result, convert.ToObj(d, &pref.Data) +} + +func (e *configMapStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + obj, err := e.ByID(apiOp, schema, "") + if err != nil { + return types.APIObjectList{}, err + } + return types.APIObjectList{ + Objects: []types.APIObject{ + obj, + }, + }, nil +} + +func (e *configMapStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) { + u := getUser(apiOp) + client, err := e.getClient(apiOp) + if err != nil { + return types.APIObject{}, err + } + + obj, err := client.Get(prefName(u), metav1.GetOptions{}) + if apierrors.IsNotFound(err) { + _, err = client.Create(&unstructured.Unstructured{ + Object: map[string]interface{}{ + "metadata": map[string]interface{}{ + "name": prefName(u), + }, + "data": data.Data().Map("data"), + }, + }, metav1.CreateOptions{}) + } else if err == nil { + obj.Object["data"] = data.Data().Map("data") + _, err = client.Update(obj, metav1.UpdateOptions{}) + } + if err != nil { + return types.APIObject{}, err + } + + return e.ByID(apiOp, schema, "") +} + +func (e *configMapStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + u := getUser(apiOp) + client, err := e.getClient(apiOp) + if err != nil { + return types.APIObject{}, err + } + + return types.APIObject{}, client.Delete(prefName(u), nil) +} diff --git a/pkg/server/resources/userpreferences/rancherpref.go b/pkg/server/resources/userpreferences/rancherpref.go new file mode 100644 index 0000000..9d53e4e --- /dev/null +++ b/pkg/server/resources/userpreferences/rancherpref.go @@ -0,0 +1,136 @@ +package userpreferences + +import ( + "github.com/rancher/steve/pkg/attributes" + "github.com/rancher/steve/pkg/schemaserver/store/empty" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/server/store/proxy" + "github.com/rancher/wrangler/pkg/data/convert" + "github.com/rancher/wrangler/pkg/schemas/validation" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" + "k8s.io/client-go/dynamic" +) + +var ( + rancherSchema = "management.cattle.io.preference" +) + +type rancherPrefStore struct { + empty.Store + cg proxy.ClientGetter +} + +func (e *rancherPrefStore) getClient(apiOp *types.APIRequest) (dynamic.ResourceInterface, error) { + u := getUser(apiOp).GetName() + cmSchema := apiOp.Schemas.LookupSchema(rancherSchema) + if cmSchema == nil { + return nil, validation.NotFound + } + + return e.cg.AdminClient(apiOp, cmSchema, u) +} + +func (e *rancherPrefStore) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + u := getUser(apiOp) + client, err := e.getClient(apiOp) + if err != nil { + return types.APIObject{}, err + } + + pref := &UserPreference{ + Data: map[string]string{}, + } + result := types.APIObject{ + Type: "userpreference", + ID: u.GetName(), + Object: pref, + } + + objs, err := client.List(metav1.ListOptions{}) + if err != nil { + return result, err + } + + for _, obj := range objs.Items { + pref.Data[obj.GetName()] = convert.ToString(obj.Object["value"]) + } + + return result, nil +} + +func (e *rancherPrefStore) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + obj, err := e.ByID(apiOp, schema, "") + if err != nil { + return types.APIObjectList{}, err + } + return types.APIObjectList{ + Objects: []types.APIObject{ + obj, + }, + }, nil +} + +func (e *rancherPrefStore) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) { + client, err := e.getClient(apiOp) + if err != nil { + return types.APIObject{}, err + } + + gvk := attributes.GVK(apiOp.Schemas.LookupSchema(rancherSchema)) + + newValues := map[string]string{} + for k, v := range data.Data().Map("data") { + newValues[k] = convert.ToString(v) + } + + prefs, err := client.List(metav1.ListOptions{}) + if err != nil { + return types.APIObject{}, err + } + + for _, pref := range prefs.Items { + key := pref.GetName() + newValue, ok := newValues[key] + delete(newValues, key) + if ok && newValue != pref.Object["value"] { + pref.Object["value"] = newValue + _, err := client.Update(&pref, metav1.UpdateOptions{}) + if err != nil { + return types.APIObject{}, err + } + } else if !ok { + err := client.Delete(key, nil) + if err != nil { + return types.APIObject{}, err + } + } + } + + for k, v := range newValues { + _, err = client.Create(&unstructured.Unstructured{ + Object: map[string]interface{}{ + "apiVersion": gvk.GroupVersion().String(), + "kind": gvk.Kind, + "metadata": map[string]interface{}{ + "name": k, + }, + "value": v, + }, + }, metav1.CreateOptions{}) + if err != nil { + return types.APIObject{}, err + } + } + + return e.ByID(apiOp, schema, "") +} + +func (e *rancherPrefStore) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + client, err := e.getClient(apiOp) + if err != nil { + return types.APIObject{}, err + } + + return types.APIObject{}, client.DeleteCollection(nil, metav1.ListOptions{}) +} diff --git a/pkg/server/resources/userpreferences/userpreferences.go b/pkg/server/resources/userpreferences/userpreferences.go new file mode 100644 index 0000000..4f5b75f --- /dev/null +++ b/pkg/server/resources/userpreferences/userpreferences.go @@ -0,0 +1,89 @@ +package userpreferences + +import ( + "net/http" + + "github.com/rancher/steve/pkg/schemaserver/store/empty" + "github.com/rancher/steve/pkg/schemaserver/types" + "github.com/rancher/steve/pkg/server/store/proxy" + "github.com/rancher/wrangler/pkg/name" + "k8s.io/apiserver/pkg/authentication/user" + "k8s.io/apiserver/pkg/endpoints/request" +) + +type UserPreference struct { + Data map[string]string `json:"data"` +} + +func Register(schemas *types.APISchemas, cg proxy.ClientGetter) { + schemas.InternalSchemas.TypeName("userpreference", UserPreference{}) + schemas.MustImportAndCustomize(UserPreference{}, func(schema *types.APISchema) { + schema.CollectionMethods = []string{http.MethodGet} + schema.ResourceMethods = []string{http.MethodGet} + schema.ResourceMethods = []string{http.MethodGet, http.MethodPut, http.MethodDelete} + schema.Store = New(cg) + }) +} + +func New(cg proxy.ClientGetter) types.Store { + return &Store{ + rancher: &rancherPrefStore{ + cg: cg, + }, + configMapStore: &configMapStore{ + cg: cg, + }, + } +} + +type Store struct { + empty.Store + rancher *rancherPrefStore + configMapStore *configMapStore +} + +func isRancher(apiOp *types.APIRequest) bool { + return apiOp.Schemas.LookupSchema(rancherSchema) != nil +} + +func (e *Store) ByID(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + if isRancher(apiOp) { + return e.rancher.ByID(apiOp, schema, id) + } + return e.configMapStore.ByID(apiOp, schema, id) +} + +func (e *Store) List(apiOp *types.APIRequest, schema *types.APISchema) (types.APIObjectList, error) { + if isRancher(apiOp) { + return e.rancher.List(apiOp, schema) + } + return e.configMapStore.List(apiOp, schema) +} + +func (e *Store) Update(apiOp *types.APIRequest, schema *types.APISchema, data types.APIObject, id string) (types.APIObject, error) { + if isRancher(apiOp) { + return e.rancher.Update(apiOp, schema, data, id) + } + return e.configMapStore.Update(apiOp, schema, data, id) +} + +func (e *Store) Delete(apiOp *types.APIRequest, schema *types.APISchema, id string) (types.APIObject, error) { + if isRancher(apiOp) { + return e.rancher.Delete(apiOp, schema, id) + } + return e.configMapStore.Delete(apiOp, schema, id) +} + +func prefName(u user.Info) string { + return name.SafeConcatName("pref", u.GetName()) +} + +func getUser(apiOp *types.APIRequest) user.Info { + u, ok := request.UserFrom(apiOp.Context()) + if !ok { + u = &user.DefaultInfo{ + Name: "dashboard-user", + } + } + return u +} diff --git a/pkg/server/server.go b/pkg/server/server.go index 92d42d7..88449de 100644 --- a/pkg/server/server.go +++ b/pkg/server/server.go @@ -68,7 +68,7 @@ func setup(ctx context.Context, server *Server) (http.Handler, *schema.Collectio ccache := clustercache.NewClusterCache(ctx, cf.MetadataClient()) - server.BaseSchemas = resources.DefaultSchemas(server.BaseSchemas, ccache) + server.BaseSchemas = resources.DefaultSchemas(server.BaseSchemas, ccache, cf) server.SchemaTemplates = append(server.SchemaTemplates, resources.DefaultSchemaTemplates(cf, asl, server.K8s.Discovery())...) cols, err := common.NewDynamicColumns(server.RestConfig)