From 22f3b5bdd73ab2393e40b9d44dad075250ec4350 Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Mon, 17 Jan 2022 15:23:37 +0800 Subject: [PATCH 01/14] add dynamic object --- pkg/object/meshcontroller/spec/spec.go | 51 ++----------- pkg/util/dynamicobject/dynamicobject.go | 78 ++++++++++++++++++++ pkg/util/dynamicobject/dynamicobject_test.go | 64 ++++++++++++++++ 3 files changed, 147 insertions(+), 46 deletions(-) create mode 100644 pkg/util/dynamicobject/dynamicobject.go create mode 100644 pkg/util/dynamicobject/dynamicobject_test.go diff --git a/pkg/object/meshcontroller/spec/spec.go b/pkg/object/meshcontroller/spec/spec.go index 93cb05325c..6567011c50 100644 --- a/pkg/object/meshcontroller/spec/spec.go +++ b/pkg/object/meshcontroller/spec/spec.go @@ -34,6 +34,7 @@ import ( "github.com/megaease/easegress/pkg/logger" "github.com/megaease/easegress/pkg/object/httppipeline" "github.com/megaease/easegress/pkg/supervisor" + "github.com/megaease/easegress/pkg/util/dynamicobject" "github.com/megaease/easegress/pkg/util/httpfilter" "github.com/megaease/easegress/pkg/util/httpheader" "github.com/megaease/easegress/pkg/util/urlrule" @@ -360,19 +361,14 @@ type ( httppipeline.Spec `yaml:",inline"` } - // DynamicObject defines a dynamic object which is a map of string to interface{}. - // The value of this map could also be a dynamic object, but in this case, its type - // must be `map[string]interface{}`, and should not be `map[interface{}]interface{}`. - DynamicObject map[string]interface{} - // CustomResourceKind defines the spec of a custom resource kind CustomResourceKind struct { - Name string `yaml:"name" jsonschema:"required"` - JSONSchema DynamicObject `yaml:"jsonSchema" jsonschema:"omitempty"` + Name string `yaml:"name" jsonschema:"required"` + JSONSchema dynamicobject.DynamicObject `yaml:"jsonSchema" jsonschema:"omitempty"` } // CustomResource defines the spec of a custom resource - CustomResource DynamicObject + CustomResource dynamicobject.DynamicObject // HTTPMatch defines an individual route for HTTP traffic HTTPMatch struct { @@ -464,43 +460,6 @@ func (tr *TrafficRules) Clone() *TrafficRules { } } -// UnmarshalYAML implements yaml.Unmarshaler -// the type of a DynamicObject field could be `map[interface{}]interface{}` if it is -// unmarshaled from yaml, but some packages, like the standard json package could not -// handle this type, so it must be converted to `map[string]interface{}`. -func (do *DynamicObject) UnmarshalYAML(unmarshal func(interface{}) error) error { - m := map[string]interface{}{} - if err := unmarshal(&m); err != nil { - return err - } - - var convert func(interface{}) interface{} - convert = func(src interface{}) interface{} { - switch x := src.(type) { - case map[interface{}]interface{}: - x2 := map[string]interface{}{} - for k, v := range x { - x2[k.(string)] = convert(v) - } - return x2 - case []interface{}: - x2 := make([]interface{}, len(x)) - for i, v := range x { - x2[i] = convert(v) - } - return x2 - } - return src - } - - for k, v := range m { - m[k] = convert(v) - } - *do = m - - return nil -} - // Name returns the 'name' field of the custom resource func (cr CustomResource) Name() string { if v, ok := cr["name"].(string); ok { @@ -519,7 +478,7 @@ func (cr CustomResource) Kind() string { // UnmarshalYAML implements yaml.Unmarshaler func (cr *CustomResource) UnmarshalYAML(unmarshal func(interface{}) error) error { - return (*DynamicObject)(cr).UnmarshalYAML(unmarshal) + return (*dynamicobject.DynamicObject)(cr).UnmarshalYAML(unmarshal) } // Validate validates Spec. diff --git a/pkg/util/dynamicobject/dynamicobject.go b/pkg/util/dynamicobject/dynamicobject.go new file mode 100644 index 0000000000..8a737c8510 --- /dev/null +++ b/pkg/util/dynamicobject/dynamicobject.go @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2017, MegaEase + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dynamicobject + +// DynamicObject defines a dynamic object which is a map of string to interface{}. +// The value of this map could also be a dynamic object, but in this case, its type +// must be `map[string]interface{}`, and should not be `map[interface{}]interface{}`. +type DynamicObject map[string]interface{} + +// UnmarshalYAML implements yaml.Unmarshaler +// the type of a DynamicObject field could be `map[interface{}]interface{}` if it is +// unmarshaled from yaml, but some packages, like the standard json package could not +// handle this type, so it must be converted to `map[string]interface{}`. +func (do *DynamicObject) UnmarshalYAML(unmarshal func(interface{}) error) error { + m := map[string]interface{}{} + if err := unmarshal(&m); err != nil { + return err + } + + var convert func(interface{}) interface{} + convert = func(src interface{}) interface{} { + switch x := src.(type) { + case map[interface{}]interface{}: + x2 := map[string]interface{}{} + for k, v := range x { + x2[k.(string)] = convert(v) + } + return x2 + case []interface{}: + x2 := make([]interface{}, len(x)) + for i, v := range x { + x2[i] = convert(v) + } + return x2 + } + return src + } + + for k, v := range m { + m[k] = convert(v) + } + *do = m + + return nil +} + +// Get gets the value of field 'field' +func (do DynamicObject) Get(field string) interface{} { + return do[field] +} + +// GetString gets the value of field 'field' as string +func (do DynamicObject) GetString(field string) string { + if v, ok := do[field].(string); ok { + return v + } + return "" +} + +// Set set the value of field 'field' to 'value' +func (do *DynamicObject) Set(field string, value interface{}) { + (*do)[field] = value +} diff --git a/pkg/util/dynamicobject/dynamicobject_test.go b/pkg/util/dynamicobject/dynamicobject_test.go new file mode 100644 index 0000000000..ed88cacc0a --- /dev/null +++ b/pkg/util/dynamicobject/dynamicobject_test.go @@ -0,0 +1,64 @@ +/* + * Copyright (c) 2017, MegaEase + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package dynamicobject + +import ( + "testing" + + "gopkg.in/yaml.v2" +) + +func TestDynamicObject(t *testing.T) { + do := DynamicObject{} + if do.GetString("name") != "" { + t.Error("name should be empty") + } + do.Set("name", 1) + if do.GetString("name") != "" { + t.Error("name should be empty") + } + do.Set("name", "obj1") + if do.GetString("name") != "obj1" { + t.Error("name should be obj1") + } + do.Set("value", 1) + if do.Get("value") != 1 { + t.Error("value should be 1") + } + + do.Set("field1", map[string]interface{}{ + "sub1": 1, + "sub2": "value2", + }) + + do.Set("field2", []interface{}{"sub1", "sub2"}) + + data, err := yaml.Marshal(do) + if err != nil { + t.Errorf("yaml.Marshal should succeed: %v", err.Error()) + } + + err = yaml.Unmarshal(data, &do) + if err != nil { + t.Errorf("yaml.Marshal should succeed: %v", err.Error()) + } + + if _, ok := do.Get("field1").(map[string]interface{}); !ok { + t.Errorf("the type of 'field1' should be 'map[string]interface{}'") + } +} From 1fde3feb969427008aea81816048ce33a8f4af86 Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Thu, 20 Jan 2022 17:33:15 +0800 Subject: [PATCH 02/14] add custom data store --- pkg/cluster/customdatastore.go | 367 +++++++++++++++++++++++++++++++++ 1 file changed, 367 insertions(+) create mode 100644 pkg/cluster/customdatastore.go diff --git a/pkg/cluster/customdatastore.go b/pkg/cluster/customdatastore.go new file mode 100644 index 0000000000..8de20d120c --- /dev/null +++ b/pkg/cluster/customdatastore.go @@ -0,0 +1,367 @@ +/* + * Copyright (c) 2017, MegaEase + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cluster + +import ( + "context" + "fmt" + "time" + + "github.com/megaease/easegress/pkg/util/dynamicobject" + "github.com/xeipuuv/gojsonschema" + "go.etcd.io/etcd/client/v3/concurrency" + yaml "gopkg.in/yaml.v2" +) + +// CustomData represents a custom data +type CustomData struct { + dynamicobject.DynamicObject `yaml:",inline"` +} + +// CustomDataKind defines the spec of a custom data kind +type CustomDataKind struct { + // Name is the name of the CustomDataKind + Name string `yaml:"name" jsonschema:"required"` + // IDField is a field name of CustomData of this kind, this field is the ID + // of the data, that's unique among the same kind, the default value is 'name'. + IDField string `yaml:"idField" jsonschema:"omitempty"` + // JSONSchema is JSON schema to validate a CustomData of this kind + JSONSchema dynamicobject.DynamicObject `yaml:"jsonSchema" jsonschema:"omitempty"` +} + +// CustomDataStore defines the storage for custom data +type CustomDataStore struct { + cluster Cluster + KindPrefix string + DataPrefix string +} + +// NewCustomDataStore creates a new custom data store +func NewCustomDataStore(cls Cluster, kindPrefix string, dataPrefix string) *CustomDataStore { + return &CustomDataStore{ + cluster: cls, + KindPrefix: kindPrefix, + DataPrefix: dataPrefix, + } +} + +func unmarshalCustomDataKind(in []byte) (*CustomDataKind, error) { + kind := &CustomDataKind{} + err := yaml.Unmarshal(in, kind) + if err != nil { + return nil, fmt.Errorf("BUG: unmarshal %s to yaml failed: %v", string(in), err) + } + return kind, nil +} + +func (cds *CustomDataStore) kindKey(name string) string { + return cds.KindPrefix + name +} + +// GetKind gets custom data kind by its name +func (cds *CustomDataStore) GetKind(name string) (*CustomDataKind, error) { + kvs, err := cds.cluster.GetRaw(cds.kindKey(name)) + if err != nil { + return nil, err + } + + if kvs == nil { + return nil, nil + } + + return unmarshalCustomDataKind(kvs.Value) +} + +// ListKinds lists custom data kinds +func (cds *CustomDataStore) ListKinds() ([]*CustomDataKind, error) { + kvs, err := cds.cluster.GetRawPrefix(cds.KindPrefix) + if err != nil { + return nil, err + } + + kinds := make([]*CustomDataKind, 0, len(kvs)) + for _, v := range kvs { + kind, err := unmarshalCustomDataKind(v.Value) + if err != nil { + return nil, err + } + kinds = append(kinds, kind) + } + + return kinds, nil +} + +func (cds *CustomDataStore) saveKind(kind *CustomDataKind, update bool) error { + if len(kind.JSONSchema) > 0 { + sl := gojsonschema.NewGoLoader(kind.JSONSchema) + if _, err := gojsonschema.NewSchema(sl); err != nil { + return fmt.Errorf("invalid JSONSchema: %s", err.Error()) + } + } + + oldKind, err := cds.GetKind(kind.Name) + if err != nil { + return err + } + + if update && (oldKind == nil) { + return fmt.Errorf("%s not found", kind.Name) + } + if (!update) && (oldKind != nil) { + return fmt.Errorf("%s existed", kind.Name) + } + + buf, err := yaml.Marshal(kind) + if err != nil { + return fmt.Errorf("BUG: marshal %#v to yaml failed: %v", kind, err) + } + + key := cds.kindKey(kind.Name) + return cds.cluster.Put(key, string(buf)) +} + +// CreateKind creates a custom data kind +func (cds *CustomDataStore) CreateKind(kind *CustomDataKind) error { + return cds.saveKind(kind, false) +} + +// UpdateKind updates a custom data kind +func (cds *CustomDataStore) UpdateKind(kind *CustomDataKind) error { + return cds.saveKind(kind, true) +} + +// DeleteKind deletes a custom data kind +func (cds *CustomDataStore) DeleteKind(name string) error { + kind, err := cds.GetKind(name) + if err != nil { + return err + } + + if kind == nil { + return fmt.Errorf("%s not found", name) + } + + return cds.cluster.Delete(cds.kindKey(name)) + // TODO: remove custom data? +} + +func (cds *CustomDataStore) dataPrefix(kind string) string { + return cds.DataPrefix + kind + "/" +} + +func (cds *CustomDataStore) dataKey(kind string, id string) string { + return cds.dataPrefix(kind) + id +} + +func unmarshalCustomData(in []byte) (*CustomData, error) { + data := &CustomData{} + err := yaml.Unmarshal(in, data) + if err != nil { + return nil, fmt.Errorf("BUG: unmarshal %s to yaml failed: %v", string(in), err) + } + return data, nil +} + +// GetData gets custom data by its id +func (cds *CustomDataStore) GetData(kind string, id string) (*CustomData, error) { + kvs, err := cds.cluster.GetRaw(cds.dataKey(kind, id)) + if err != nil { + return nil, err + } + + if kvs == nil { + return nil, nil + } + + return unmarshalCustomData(kvs.Value) +} + +// ListData lists custom data of specified kind. +// if kind is empty, it returns custom data of all kinds. +func (cds *CustomDataStore) ListData(kind string) ([]*CustomData, error) { + key := cds.DataPrefix + if kind != "" { + key = cds.dataPrefix(kind) + } + kvs, err := cds.cluster.GetRawPrefix(key) + if err != nil { + return nil, err + } + + results := make([]*CustomData, 0, len(kvs)) + for _, v := range kvs { + data, err := unmarshalCustomData(v.Value) + if err != nil { + return nil, err + } + results = append(results, data) + } + + return results, nil +} + +func (cds *CustomDataStore) saveData(kind string, data *CustomData, update bool) error { + k, err := cds.GetKind(kind) + if err != nil { + return err + } + if k == nil { + return fmt.Errorf("kind %s not found", kind) + } + + id := data.GetString(k.IDField) + if id == "" { + return fmt.Errorf("data id is empty") + } + + if len(k.JSONSchema) > 0 { + schema := gojsonschema.NewGoLoader(k.JSONSchema) + doc := gojsonschema.NewGoLoader(data) + res, err := gojsonschema.Validate(schema, doc) + if err != nil { + return fmt.Errorf("error occurs during validation: %v", err) + } + if !res.Valid() { + return fmt.Errorf("validation failed: %v", res.Errors()) + } + } + + oldData, err := cds.GetData(kind, id) + if err != nil { + return err + } + if update && (oldData == nil) { + return fmt.Errorf("%s/%s not found", kind, id) + } + if (!update) && (oldData != nil) { + return fmt.Errorf("%s/%s existed", kind, id) + } + + buf, err := yaml.Marshal(data) + if err != nil { + return fmt.Errorf("BUG: marshal %#v to yaml failed: %v", data, err) + } + + key := cds.dataKey(kind, id) + return cds.cluster.Put(key, string(buf)) +} + +// CreateData creates a custom data +func (cds *CustomDataStore) CreateData(kind string, data *CustomData) error { + return cds.saveData(kind, data, false) +} + +// UpdateData updates a custom data +func (cds *CustomDataStore) UpdateData(kind string, data *CustomData) error { + return cds.saveData(kind, data, true) +} + +// UpdateMultipleData updates multiple custom data in a transaction +func (cds *CustomDataStore) UpdateMultipleData(kind string, data []*CustomData) error { + k, err := cds.GetKind(kind) + if err != nil { + return err + } + if k == nil { + return fmt.Errorf("kind %s not found", kind) + } + + if len(k.JSONSchema) > 0 { + schema := gojsonschema.NewGoLoader(k.JSONSchema) + for _, d := range data { + doc := gojsonschema.NewGoLoader(d) + res, err := gojsonschema.Validate(schema, doc) + if err != nil { + return fmt.Errorf("error occurs during validation: %v", err) + } + if !res.Valid() { + return fmt.Errorf("validation failed: %v", res.Errors()) + } + } + } + + for _, d := range data { + if d.GetString(k.IDField) == "" { + return fmt.Errorf("data id is empty") + } + } + + return cds.cluster.STM(func(s concurrency.STM) error { + for _, d := range data { + id := d.GetString(k.IDField) + buf, err := yaml.Marshal(data) + if err != nil { + return fmt.Errorf("BUG: marshal %#v to yaml failed: %v", data, err) + } + key := cds.dataKey(kind, id) + s.Put(key, string(buf)) + } + return nil + }) +} + +// DeleteData deletes a custom data +func (cds *CustomDataStore) DeleteData(kind string, id string) error { + data, err := cds.GetData(kind, id) + if err != nil { + return err + } + + if data == nil { + return fmt.Errorf("%s not found", id) + } + + return cds.cluster.Delete(cds.dataKey(kind, id)) +} + +// DeleteAllData deletes all custom data of kind 'kind' +func (cds *CustomDataStore) DeleteAllData(kind string) error { + prefix := cds.dataPrefix(kind) + return cds.cluster.DeletePrefix(prefix) +} + +// Watch watches the data of custom data kind 'kind' +func (cds *CustomDataStore) Watch(ctx context.Context, kind string, onChange func([]*CustomData)) error { + syncer, err := cds.cluster.Syncer(5 * time.Minute) + if err != nil { + return err + } + + prefix := cds.dataPrefix(kind) + ch, err := syncer.SyncRawPrefix(prefix) + if err != nil { + return err + } + + for { + select { + case <-ctx.Done(): + syncer.Close() + return nil + case m := <-ch: + data := make([]*CustomData, 0, len(m)) + for _, v := range m { + d, err := unmarshalCustomData(v.Value) + if err == nil { + data = append(data, d) + } + } + onChange(data) + } + } +} From bc612afcb473d87c1a5953d87021f3e9ded2d09a Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Sun, 6 Feb 2022 11:52:35 +0800 Subject: [PATCH 03/14] update custom data API --- pkg/api/customdata.go | 244 ++++++++++++++++++++++++--------- pkg/api/server.go | 9 +- pkg/cluster/customdatastore.go | 86 +++++++----- pkg/cluster/layout.go | 18 +-- 4 files changed, 245 insertions(+), 112 deletions(-) diff --git a/pkg/api/customdata.go b/pkg/api/customdata.go index b701d7dca4..df52ffd88d 100644 --- a/pkg/api/customdata.go +++ b/pkg/api/customdata.go @@ -18,134 +18,244 @@ package api import ( + "fmt" "net/http" "github.com/go-chi/chi/v5" - "go.etcd.io/etcd/client/v3/concurrency" + "github.com/megaease/easegress/pkg/cluster" "gopkg.in/yaml.v2" ) const ( - // CustomDataPrefix is the object prefix. + // CustomDataKindPrefix is the URL prefix of APIs for custom data kind + CustomDataKindPrefix = "/customdatakinds" + // CustomDataPrefix is the URL prefix of APIs for custom data CustomDataPrefix = "/customdata/{kind}" ) -type ( - // CustomData defines the custom data type - CustomData map[string]interface{} - - // ChangeRequest represents a change request to custom data - ChangeRequest struct { - Rebuild bool `yaml:"rebuild"` - Delete []string `yaml:"delete"` - List []CustomData `yaml:"list"` - } -) - -// Key returns the 'key' field of the custom data -func (cd CustomData) Key() string { - if v, ok := cd["key"].(string); ok { - return v - } - return "" +// ChangeRequest represents a change request to custom data +type ChangeRequest struct { + Rebuild bool `yaml:"rebuild"` + Delete []string `yaml:"delete"` + List []cluster.CustomData `yaml:"list"` } func (s *Server) customDataAPIEntries() []*Entry { return []*Entry{ + { + Path: CustomDataKindPrefix, + Method: http.MethodGet, + Handler: s.listCustomDataKind, + }, + { + Path: CustomDataKindPrefix + "/{name}", + Method: http.MethodGet, + Handler: s.getCustomDataKind, + }, + { + Path: CustomDataKindPrefix, + Method: http.MethodPost, + Handler: s.createCustomDataKind, + }, + { + Path: CustomDataKindPrefix, + Method: http.MethodPut, + Handler: s.updateCustomDataKind, + }, + { + Path: CustomDataKindPrefix + "/{name}", + Method: http.MethodDelete, + Handler: s.deleteCustomDataKind, + }, + { Path: CustomDataPrefix, Method: http.MethodGet, Handler: s.listCustomData, }, + { + Path: CustomDataPrefix + "/{key}", + Method: http.MethodGet, + Handler: s.getCustomData, + }, { Path: CustomDataPrefix, Method: http.MethodPost, + Handler: s.createCustomData, + }, + { + Path: CustomDataPrefix, + Method: http.MethodPut, Handler: s.updateCustomData, }, { Path: CustomDataPrefix + "/{key}", - Method: http.MethodGet, - Handler: s.getCustomData, + Method: http.MethodDelete, + Handler: s.deleteCustomData, + }, + + { + Path: CustomDataPrefix + "/items", + Method: http.MethodPut, + Handler: s.batchUpdateCustomData, }, } } -func (s *Server) listCustomData(w http.ResponseWriter, r *http.Request) { - kind := chi.URLParam(r, "kind") - prefix := s.cluster.Layout().CustomDataPrefix(kind) - kvs, err := s.cluster.GetRawPrefix(prefix) +func (s *Server) listCustomDataKind(w http.ResponseWriter, r *http.Request) { + result, err := s.cds.ListKinds() if err != nil { ClusterPanic(err) } - result := make([]CustomData, 0, len(kvs)) - for key, kv := range kvs { - key = key[len(prefix):] - cd := CustomData{} - err = yaml.Unmarshal(kv.Value, &cd) - if err != nil { - panic(err) - } - result = append(result, cd) + w.Header().Set("Content-Type", "text/vnd.yaml") + err = yaml.NewEncoder(w).Encode(result) + if err != nil { + panic(err) + } +} + +func (s *Server) getCustomDataKind(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + k, err := s.cds.GetKind(name) + if err != nil { + ClusterPanic(err) } w.Header().Set("Content-Type", "text/vnd.yaml") - err = yaml.NewEncoder(w).Encode(result) + err = yaml.NewEncoder(w).Encode(k) if err != nil { panic(err) } } -func (s *Server) updateCustomData(w http.ResponseWriter, r *http.Request) { - kind := chi.URLParam(r, "kind") +func (s *Server) createCustomDataKind(w http.ResponseWriter, r *http.Request) { + k := cluster.CustomDataKind{} + err := yaml.NewDecoder(r.Body).Decode(&k) + if err != nil { + panic(err) + } + err = s.cds.CreateKind(&k) + if err != nil { + ClusterPanic(err) + } - var cr ChangeRequest - err := yaml.NewDecoder(r.Body).Decode(&cr) + w.WriteHeader(http.StatusCreated) + location := fmt.Sprintf("%s/%s", r.URL.Path, k.Name) + w.Header().Set("Location", location) +} + +func (s *Server) updateCustomDataKind(w http.ResponseWriter, r *http.Request) { + k := cluster.CustomDataKind{} + err := yaml.NewDecoder(r.Body).Decode(&k) if err != nil { panic(err) } + s.cds.UpdateKind(&k) + if err != nil { + ClusterPanic(err) + } +} - prefix := s.cluster.Layout().CustomDataPrefix(kind) - if cr.Rebuild { - err = s.cluster.DeletePrefix(prefix) - if err != nil { - ClusterPanic(err) - } +func (s *Server) deleteCustomDataKind(w http.ResponseWriter, r *http.Request) { + name := chi.URLParam(r, "name") + err := s.cds.DeleteKind(name) + if err != nil { + ClusterPanic(err) } +} - err = s.cluster.STM(func(stm concurrency.STM) error { - if !cr.Rebuild { - for _, key := range cr.Delete { - key = s.cluster.Layout().CustomDataItem(kind, key) - stm.Del(key) - } - } - for _, v := range cr.List { - key := v.Key() - key = s.cluster.Layout().CustomDataItem(kind, key) - data, err := yaml.Marshal(v) - if err != nil { - return err - } - stm.Put(key, string(data)) - } - return nil - }) +func (s *Server) listCustomData(w http.ResponseWriter, r *http.Request) { + kind := chi.URLParam(r, "kind") + + result, err := s.cds.ListData(kind) if err != nil { ClusterPanic(err) } + + w.Header().Set("Content-Type", "text/vnd.yaml") + err = yaml.NewEncoder(w).Encode(result) + if err != nil { + panic(err) + } } func (s *Server) getCustomData(w http.ResponseWriter, r *http.Request) { kind := chi.URLParam(r, "kind") key := chi.URLParam(r, "key") - key = s.cluster.Layout().CustomDataItem(kind, key) - kv, err := s.cluster.GetRaw(key) + data, err := s.cds.GetData(kind, key) if err != nil { ClusterPanic(err) } w.Header().Set("Content-Type", "text/vnd.yaml") - w.Write(kv.Value) + err = yaml.NewEncoder(w).Encode(data) + if err != nil { + panic(err) + } +} + +func (s *Server) createCustomData(w http.ResponseWriter, r *http.Request) { + kind := chi.URLParam(r, "kind") + + data := cluster.CustomData{} + err := yaml.NewDecoder(r.Body).Decode(&data) + if err != nil { + panic(err) + } + id, err := s.cds.CreateData(kind, data) + if err != nil { + ClusterPanic(err) + } + + w.WriteHeader(http.StatusCreated) + // TODO: + location := fmt.Sprintf("%s/%s", r.URL.Path, id) + w.Header().Set("Location", location) +} + +func (s *Server) updateCustomData(w http.ResponseWriter, r *http.Request) { + kind := chi.URLParam(r, "kind") + + data := cluster.CustomData{} + err := yaml.NewDecoder(r.Body).Decode(&data) + if err != nil { + panic(err) + } + _, err = s.cds.UpdateData(kind, data) + if err != nil { + ClusterPanic(err) + } +} + +func (s *Server) deleteCustomData(w http.ResponseWriter, r *http.Request) { + kind := chi.URLParam(r, "kind") + key := chi.URLParam(r, "key") + err := s.cds.DeleteData(kind, key) + if err != nil { + ClusterPanic(err) + } +} + +func (s *Server) batchUpdateCustomData(w http.ResponseWriter, r *http.Request) { + kind := chi.URLParam(r, "kind") + + var cr ChangeRequest + err := yaml.NewDecoder(r.Body).Decode(&cr) + if err != nil { + panic(err) + } + + if cr.Rebuild { + err = s.cds.DeleteAllData(kind) + if err != nil { + ClusterPanic(err) + } + } + + err = s.cds.BatchUpdateData(kind, cr.Delete, cr.List) + if err != nil { + ClusterPanic(err) + } } diff --git a/pkg/api/server.go b/pkg/api/server.go index dbbdaca036..3f5c6a494a 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -37,6 +37,7 @@ type ( router *dynamicMux cluster cluster.Cluster super *supervisor.Supervisor + cds *cluster.CustomDataStore mutex cluster.Mutex mutexMutex sync.Mutex @@ -57,10 +58,10 @@ type ( ) // MustNewServer creates an api server. -func MustNewServer(opt *option.Options, cluster cluster.Cluster, super *supervisor.Supervisor) *Server { +func MustNewServer(opt *option.Options, cls cluster.Cluster, super *supervisor.Supervisor) *Server { s := &Server{ opt: opt, - cluster: cluster, + cluster: cls, super: super, } s.router = newDynamicMux(s) @@ -71,6 +72,10 @@ func MustNewServer(opt *option.Options, cluster cluster.Cluster, super *supervis logger.Errorf("get cluster mutex %s failed: %v", lockKey, err) } + kindPrefix := cls.Layout().CustomDataKindPrefix() + dataPrefix := cls.Layout().CustomDataPrefix() + s.cds = cluster.NewCustomDataStore(cls, kindPrefix, dataPrefix) + s.initMetadata() s.registerAPIs() diff --git a/pkg/cluster/customdatastore.go b/pkg/cluster/customdatastore.go index 8de20d120c..39ba617379 100644 --- a/pkg/cluster/customdatastore.go +++ b/pkg/cluster/customdatastore.go @@ -29,9 +29,7 @@ import ( ) // CustomData represents a custom data -type CustomData struct { - dynamicobject.DynamicObject `yaml:",inline"` -} +type CustomData dynamicobject.DynamicObject // CustomDataKind defines the spec of a custom data kind type CustomDataKind struct { @@ -44,6 +42,16 @@ type CustomDataKind struct { JSONSchema dynamicobject.DynamicObject `yaml:"jsonSchema" jsonschema:"omitempty"` } +func (cdk *CustomDataKind) dataID(data CustomData) string { + var id string + if cdk.IDField == "" { + id, _ = data["name"].(string) + } else { + id, _ = data[cdk.IDField].(string) + } + return id +} + // CustomDataStore defines the storage for custom data type CustomDataStore struct { cluster Cluster @@ -168,9 +176,9 @@ func (cds *CustomDataStore) dataKey(kind string, id string) string { return cds.dataPrefix(kind) + id } -func unmarshalCustomData(in []byte) (*CustomData, error) { - data := &CustomData{} - err := yaml.Unmarshal(in, data) +func unmarshalCustomData(in []byte) (CustomData, error) { + data := CustomData{} + err := yaml.Unmarshal(in, &data) if err != nil { return nil, fmt.Errorf("BUG: unmarshal %s to yaml failed: %v", string(in), err) } @@ -178,7 +186,7 @@ func unmarshalCustomData(in []byte) (*CustomData, error) { } // GetData gets custom data by its id -func (cds *CustomDataStore) GetData(kind string, id string) (*CustomData, error) { +func (cds *CustomDataStore) GetData(kind string, id string) (CustomData, error) { kvs, err := cds.cluster.GetRaw(cds.dataKey(kind, id)) if err != nil { return nil, err @@ -193,7 +201,7 @@ func (cds *CustomDataStore) GetData(kind string, id string) (*CustomData, error) // ListData lists custom data of specified kind. // if kind is empty, it returns custom data of all kinds. -func (cds *CustomDataStore) ListData(kind string) ([]*CustomData, error) { +func (cds *CustomDataStore) ListData(kind string) ([]CustomData, error) { key := cds.DataPrefix if kind != "" { key = cds.dataPrefix(kind) @@ -203,7 +211,7 @@ func (cds *CustomDataStore) ListData(kind string) ([]*CustomData, error) { return nil, err } - results := make([]*CustomData, 0, len(kvs)) + results := make([]CustomData, 0, len(kvs)) for _, v := range kvs { data, err := unmarshalCustomData(v.Value) if err != nil { @@ -215,18 +223,18 @@ func (cds *CustomDataStore) ListData(kind string) ([]*CustomData, error) { return results, nil } -func (cds *CustomDataStore) saveData(kind string, data *CustomData, update bool) error { +func (cds *CustomDataStore) saveData(kind string, data CustomData, update bool) (string, error) { k, err := cds.GetKind(kind) if err != nil { - return err + return "", err } if k == nil { - return fmt.Errorf("kind %s not found", kind) + return "", fmt.Errorf("kind %s not found", kind) } - id := data.GetString(k.IDField) + id := k.dataID(data) if id == "" { - return fmt.Errorf("data id is empty") + return "", fmt.Errorf("data id is empty") } if len(k.JSONSchema) > 0 { @@ -234,45 +242,50 @@ func (cds *CustomDataStore) saveData(kind string, data *CustomData, update bool) doc := gojsonschema.NewGoLoader(data) res, err := gojsonschema.Validate(schema, doc) if err != nil { - return fmt.Errorf("error occurs during validation: %v", err) + return "", fmt.Errorf("error occurs during validation: %v", err) } if !res.Valid() { - return fmt.Errorf("validation failed: %v", res.Errors()) + return "", fmt.Errorf("validation failed: %v", res.Errors()) } } oldData, err := cds.GetData(kind, id) if err != nil { - return err + return "", err } if update && (oldData == nil) { - return fmt.Errorf("%s/%s not found", kind, id) + return "", fmt.Errorf("%s/%s not found", kind, id) } if (!update) && (oldData != nil) { - return fmt.Errorf("%s/%s existed", kind, id) + return "", fmt.Errorf("%s/%s existed", kind, id) } buf, err := yaml.Marshal(data) if err != nil { - return fmt.Errorf("BUG: marshal %#v to yaml failed: %v", data, err) + return "", fmt.Errorf("BUG: marshal %#v to yaml failed: %v", data, err) } key := cds.dataKey(kind, id) - return cds.cluster.Put(key, string(buf)) + err = cds.cluster.Put(key, string(buf)) + if err != nil { + return "", err + } + + return id, nil } // CreateData creates a custom data -func (cds *CustomDataStore) CreateData(kind string, data *CustomData) error { +func (cds *CustomDataStore) CreateData(kind string, data CustomData) (string, error) { return cds.saveData(kind, data, false) } // UpdateData updates a custom data -func (cds *CustomDataStore) UpdateData(kind string, data *CustomData) error { +func (cds *CustomDataStore) UpdateData(kind string, data CustomData) (string, error) { return cds.saveData(kind, data, true) } -// UpdateMultipleData updates multiple custom data in a transaction -func (cds *CustomDataStore) UpdateMultipleData(kind string, data []*CustomData) error { +// BatchUpdateData updates multiple custom data in a transaction +func (cds *CustomDataStore) BatchUpdateData(kind string, del []string, update []CustomData) error { k, err := cds.GetKind(kind) if err != nil { return err @@ -281,10 +294,10 @@ func (cds *CustomDataStore) UpdateMultipleData(kind string, data []*CustomData) return fmt.Errorf("kind %s not found", kind) } - if len(k.JSONSchema) > 0 { + if len(update) > 0 && len(k.JSONSchema) > 0 { schema := gojsonschema.NewGoLoader(k.JSONSchema) - for _, d := range data { - doc := gojsonschema.NewGoLoader(d) + for _, data := range update { + doc := gojsonschema.NewGoLoader(data) res, err := gojsonschema.Validate(schema, doc) if err != nil { return fmt.Errorf("error occurs during validation: %v", err) @@ -295,15 +308,20 @@ func (cds *CustomDataStore) UpdateMultipleData(kind string, data []*CustomData) } } - for _, d := range data { - if d.GetString(k.IDField) == "" { + for _, data := range update { + if k.dataID(data) == "" { return fmt.Errorf("data id is empty") } } return cds.cluster.STM(func(s concurrency.STM) error { - for _, d := range data { - id := d.GetString(k.IDField) + for _, id := range del { + key := cds.dataKey(kind, id) + s.Del(key) + } + + for _, data := range update { + id := k.dataID(data) buf, err := yaml.Marshal(data) if err != nil { return fmt.Errorf("BUG: marshal %#v to yaml failed: %v", data, err) @@ -336,7 +354,7 @@ func (cds *CustomDataStore) DeleteAllData(kind string) error { } // Watch watches the data of custom data kind 'kind' -func (cds *CustomDataStore) Watch(ctx context.Context, kind string, onChange func([]*CustomData)) error { +func (cds *CustomDataStore) Watch(ctx context.Context, kind string, onChange func([]CustomData)) error { syncer, err := cds.cluster.Syncer(5 * time.Minute) if err != nil { return err @@ -354,7 +372,7 @@ func (cds *CustomDataStore) Watch(ctx context.Context, kind string, onChange fun syncer.Close() return nil case m := <-ch: - data := make([]*CustomData, 0, len(m)) + data := make([]CustomData, 0, len(m)) for _, v := range m { d, err := unmarshalCustomData(v.Value) if err == nil { diff --git a/pkg/cluster/layout.go b/pkg/cluster/layout.go index 80331c7540..daf5567f61 100644 --- a/pkg/cluster/layout.go +++ b/pkg/cluster/layout.go @@ -33,9 +33,9 @@ const ( configObjectFormat = "/config/objects/%s" // +objectName configVersion = "/config/version" wasmCodeEvent = "/wasm/code" - wasmDataPrefixFormat = "/wasm/data/%s/%s/" // + pipelineName + filterName - customDataPrefixFormat = "/custom-data/%s/" // + kind - customDataItemFormat = "/custom-data/%s/%s" // + kind + item key + wasmDataPrefixFormat = "/wasm/data/%s/%s/" // + pipelineName + filterName + customDataKindPrefix = "/custom-data-kinds/" + customDataPrefix = "/custom-data/" // the cluster name of this eg group will be registered under this path in etcd // any new member(primary or secondary ) will be rejected if it is configured a different cluster name @@ -131,12 +131,12 @@ func (l *Layout) WasmDataPrefix(pipeline string, name string) string { return fmt.Sprintf(wasmDataPrefixFormat, pipeline, name) } -// CustomDataPrefix returns the prefix of a custom data kind -func (l *Layout) CustomDataPrefix(kind string) string { - return fmt.Sprintf(customDataPrefixFormat, kind) +// CustomDataPrefix returns the prefix of all custom data +func (l *Layout) CustomDataPrefix() string { + return customDataPrefix } -// CustomDataItem returns the full key of a custom data item -func (l *Layout) CustomDataItem(kind, key string) string { - return fmt.Sprintf(customDataItemFormat, kind, key) +// CustomDataKindPrefix returns the prefix of all custom data kind +func (l *Layout) CustomDataKindPrefix() string { + return customDataKindPrefix } From a9154be3d11433826a03d2d8598744540379e418 Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Sun, 6 Feb 2022 15:19:28 +0800 Subject: [PATCH 04/14] update API for mesh --- pkg/api/customdata.go | 8 +- pkg/cluster/customdatastore.go | 28 +--- .../meshcontroller/api/api_customresource.go | 18 +-- pkg/object/meshcontroller/service/service.go | 135 ++++-------------- pkg/object/meshcontroller/spec/spec.go | 30 +--- pkg/object/meshcontroller/spec/spec_test.go | 24 ---- 6 files changed, 48 insertions(+), 195 deletions(-) diff --git a/pkg/api/customdata.go b/pkg/api/customdata.go index df52ffd88d..c2466a17d4 100644 --- a/pkg/api/customdata.go +++ b/pkg/api/customdata.go @@ -135,7 +135,7 @@ func (s *Server) createCustomDataKind(w http.ResponseWriter, r *http.Request) { if err != nil { panic(err) } - err = s.cds.CreateKind(&k) + err = s.cds.PutKind(&k, false) if err != nil { ClusterPanic(err) } @@ -151,7 +151,7 @@ func (s *Server) updateCustomDataKind(w http.ResponseWriter, r *http.Request) { if err != nil { panic(err) } - s.cds.UpdateKind(&k) + err = s.cds.PutKind(&k, true) if err != nil { ClusterPanic(err) } @@ -204,7 +204,7 @@ func (s *Server) createCustomData(w http.ResponseWriter, r *http.Request) { if err != nil { panic(err) } - id, err := s.cds.CreateData(kind, data) + id, err := s.cds.PutData(kind, data, false) if err != nil { ClusterPanic(err) } @@ -223,7 +223,7 @@ func (s *Server) updateCustomData(w http.ResponseWriter, r *http.Request) { if err != nil { panic(err) } - _, err = s.cds.UpdateData(kind, data) + _, err = s.cds.PutData(kind, data, true) if err != nil { ClusterPanic(err) } diff --git a/pkg/cluster/customdatastore.go b/pkg/cluster/customdatastore.go index 39ba617379..ab43707cff 100644 --- a/pkg/cluster/customdatastore.go +++ b/pkg/cluster/customdatastore.go @@ -29,7 +29,7 @@ import ( ) // CustomData represents a custom data -type CustomData dynamicobject.DynamicObject +type CustomData = dynamicobject.DynamicObject // CustomDataKind defines the spec of a custom data kind type CustomDataKind struct { @@ -114,7 +114,8 @@ func (cds *CustomDataStore) ListKinds() ([]*CustomDataKind, error) { return kinds, nil } -func (cds *CustomDataStore) saveKind(kind *CustomDataKind, update bool) error { +// PutKind creates or updates a custom data kind +func (cds *CustomDataStore) PutKind(kind *CustomDataKind, update bool) error { if len(kind.JSONSchema) > 0 { sl := gojsonschema.NewGoLoader(kind.JSONSchema) if _, err := gojsonschema.NewSchema(sl); err != nil { @@ -143,16 +144,6 @@ func (cds *CustomDataStore) saveKind(kind *CustomDataKind, update bool) error { return cds.cluster.Put(key, string(buf)) } -// CreateKind creates a custom data kind -func (cds *CustomDataStore) CreateKind(kind *CustomDataKind) error { - return cds.saveKind(kind, false) -} - -// UpdateKind updates a custom data kind -func (cds *CustomDataStore) UpdateKind(kind *CustomDataKind) error { - return cds.saveKind(kind, true) -} - // DeleteKind deletes a custom data kind func (cds *CustomDataStore) DeleteKind(name string) error { kind, err := cds.GetKind(name) @@ -223,7 +214,8 @@ func (cds *CustomDataStore) ListData(kind string) ([]CustomData, error) { return results, nil } -func (cds *CustomDataStore) saveData(kind string, data CustomData, update bool) (string, error) { +// PutData creates or updates a custom data item +func (cds *CustomDataStore) PutData(kind string, data CustomData, update bool) (string, error) { k, err := cds.GetKind(kind) if err != nil { return "", err @@ -274,16 +266,6 @@ func (cds *CustomDataStore) saveData(kind string, data CustomData, update bool) return id, nil } -// CreateData creates a custom data -func (cds *CustomDataStore) CreateData(kind string, data CustomData) (string, error) { - return cds.saveData(kind, data, false) -} - -// UpdateData updates a custom data -func (cds *CustomDataStore) UpdateData(kind string, data CustomData) (string, error) { - return cds.saveData(kind, data, true) -} - // BatchUpdateData updates multiple custom data in a transaction func (cds *CustomDataStore) BatchUpdateData(kind string, del []string, update []CustomData) error { k, err := cds.GetKind(kind) diff --git a/pkg/object/meshcontroller/api/api_customresource.go b/pkg/object/meshcontroller/api/api_customresource.go index c04c96d398..b209cd6cfc 100644 --- a/pkg/object/meshcontroller/api/api_customresource.go +++ b/pkg/object/meshcontroller/api/api_customresource.go @@ -124,7 +124,7 @@ func (a *API) saveCustomResourceKind(w http.ResponseWriter, r *http.Request, upd return err } - a.service.PutCustomResourceKind(kind) + a.service.PutCustomResourceKind(kind, update) return nil } @@ -163,14 +163,14 @@ func (a *API) deleteCustomResourceKind(w http.ResponseWriter, r *http.Request) { func (a *API) listAllCustomResources(w http.ResponseWriter, r *http.Request) { resources := a.service.ListCustomResources("") sort.Slice(resources, func(i, j int) bool { - k1, k2 := resources[i].Kind(), resources[j].Kind() + k1, k2 := resources[i].GetString("kind"), resources[j].GetString("kind") if k1 < k2 { return true } if k1 > k2 { return false } - return resources[i].Name() < resources[j].Name() + return resources[i].GetString("name") < resources[j].GetString("name") }) w.Header().Set("Content-Type", "application/json") @@ -194,7 +194,7 @@ func (a *API) listCustomResources(w http.ResponseWriter, r *http.Request) { resources := a.service.ListCustomResources(kind) sort.Slice(resources, func(i, j int) bool { - return resources[i].Name() < resources[j].Name() + return resources[i].GetString("name") < resources[j].GetString("name") }) w.Header().Set("Content-Type", "application/json") @@ -235,15 +235,15 @@ func (a *API) getCustomResource(w http.ResponseWriter, r *http.Request) { } func (a *API) saveCustomResource(w http.ResponseWriter, r *http.Request, update bool) error { - resource := &spec.CustomResource{} - err := json.NewDecoder(r.Body).Decode(resource) + resource := spec.CustomResource{} + err := json.NewDecoder(r.Body).Decode(&resource) if err != nil { err = fmt.Errorf("unmarshal custom resource failed: %v", err) api.HandleAPIError(w, r, http.StatusBadRequest, err) return err } - kind, name := resource.Kind(), resource.Name() + kind, name := resource.GetString("kind"), resource.GetString("name") if kind == "" { err = fmt.Errorf("kind cannot be empty") api.HandleAPIError(w, r, http.StatusBadRequest, err) @@ -292,7 +292,7 @@ func (a *API) saveCustomResource(w http.ResponseWriter, r *http.Request, update return err } - a.service.PutCustomResource(resource) + a.service.PutCustomResource(resource, update) return nil } @@ -342,7 +342,7 @@ func (a *API) watchCustomResources(w http.ResponseWriter, r *http.Request) { logger.Infof("begin watch custom resources of kind '%s'", kind) w.Header().Set("Content-type", "application/octet-stream") - a.service.WatchCustomResource(r.Context(), kind, func(resources []*spec.CustomResource) { + a.service.WatchCustomResource(r.Context(), kind, func(resources []spec.CustomResource) { err = json.NewEncoder(w).Encode(resources) if err != nil { logger.Errorf("marshal custom resource failed: %v", err) diff --git a/pkg/object/meshcontroller/service/service.go b/pkg/object/meshcontroller/service/service.go index e7bc7a4ddf..e28a6afd9c 100644 --- a/pkg/object/meshcontroller/service/service.go +++ b/pkg/object/meshcontroller/service/service.go @@ -26,6 +26,7 @@ import ( "gopkg.in/yaml.v2" "github.com/megaease/easegress/pkg/api" + "github.com/megaease/easegress/pkg/cluster" "github.com/megaease/easegress/pkg/logger" "github.com/megaease/easegress/pkg/object/meshcontroller/layout" "github.com/megaease/easegress/pkg/object/meshcontroller/spec" @@ -41,15 +42,19 @@ type ( spec *spec.Admin store storage.Storage + cds *cluster.CustomDataStore } ) // New creates a service with spec func New(superSpec *supervisor.Spec) *Service { + kindPrefix := layout.CustomResourceKindPrefix() + dataPrefix := layout.AllCustomResourcePrefix() s := &Service{ superSpec: superSpec, spec: superSpec.ObjectSpec().(*spec.Admin), store: storage.New(superSpec.Name(), superSpec.Super().Cluster()), + cds: cluster.NewCustomDataStore(superSpec.Super().Cluster(), kindPrefix, dataPrefix), } return s @@ -657,163 +662,77 @@ func (s *Service) DeleteIngressSpec(ingressName string) { // ListCustomResourceKinds lists custom resource kinds func (s *Service) ListCustomResourceKinds() []*spec.CustomResourceKind { - kvs, err := s.store.GetRawPrefix(layout.CustomResourceKindPrefix()) + kinds, err := s.cds.ListKinds() if err != nil { - api.ClusterPanic(err) - } - - kinds := []*spec.CustomResourceKind{} - for _, v := range kvs { - kind := &spec.CustomResourceKind{} - err := yaml.Unmarshal(v.Value, kind) - if err != nil { - logger.Errorf("BUG: unmarshal %s to yaml failed: %v", v, err) - continue - } - kinds = append(kinds, kind) + panic(err) } - return kinds } // DeleteCustomResourceKind deletes a custom resource kind func (s *Service) DeleteCustomResourceKind(kind string) { - err := s.store.Delete(layout.CustomResourceKindKey(kind)) + err := s.cds.DeleteKind(kind) if err != nil { - api.ClusterPanic(err) + panic(err) } } // GetCustomResourceKind gets custom resource kind with its name func (s *Service) GetCustomResourceKind(name string) *spec.CustomResourceKind { - kvs, err := s.store.GetRaw(layout.CustomResourceKindKey(name)) + kind, err := s.cds.GetKind(name) if err != nil { - api.ClusterPanic(err) - } - - if kvs == nil { - return nil + panic(err) } - - kind := &spec.CustomResourceKind{} - err = yaml.Unmarshal(kvs.Value, kind) - if err != nil { - panic(fmt.Errorf("BUG: unmarshal %s to yaml failed: %v", string(kvs.Value), err)) - } - return kind } // PutCustomResourceKind writes the custom resource kind to storage. -func (s *Service) PutCustomResourceKind(kind *spec.CustomResourceKind) { - buff, err := yaml.Marshal(kind) +func (s *Service) PutCustomResourceKind(kind *spec.CustomResourceKind, update bool) { + err := s.cds.PutKind(kind, update) if err != nil { panic(fmt.Errorf("BUG: marshal %#v to yaml failed: %v", kind, err)) } - - err = s.store.Put(layout.CustomResourceKindKey(kind.Name), string(buff)) - if err != nil { - api.ClusterPanic(err) - } } // ListCustomResources lists custom resources of specified kind. // if kind is empty, it returns custom objects of all kinds. -func (s *Service) ListCustomResources(kind string) []*spec.CustomResource { - prefix := layout.AllCustomResourcePrefix() - if kind != "" { - prefix = layout.CustomResourcePrefix(kind) - } - kvs, err := s.store.GetRawPrefix(prefix) +func (s *Service) ListCustomResources(kind string) []spec.CustomResource { + resources, err := s.cds.ListData(kind) if err != nil { - api.ClusterPanic(err) - } - - resources := []*spec.CustomResource{} - for _, v := range kvs { - resource := &spec.CustomResource{} - err := yaml.Unmarshal(v.Value, resource) - if err != nil { - logger.Errorf("BUG: unmarshal %s to yaml failed: %v", v, err) - continue - } - resources = append(resources, resource) + panic(err) } - return resources } // DeleteCustomResource deletes a custom resource func (s *Service) DeleteCustomResource(kind, name string) { - err := s.store.Delete(layout.CustomResourceKey(kind, name)) + err := s.cds.DeleteData(kind, name) if err != nil { - api.ClusterPanic(err) + panic(err) } } // GetCustomResource gets custom resource with its kind & name -func (s *Service) GetCustomResource(kind, name string) *spec.CustomResource { - kvs, err := s.store.GetRaw(layout.CustomResourceKey(kind, name)) +func (s *Service) GetCustomResource(kind, name string) spec.CustomResource { + resource, err := s.cds.GetData(kind, name) if err != nil { - api.ClusterPanic(err) + panic(err) } - - if kvs == nil { - return nil - } - - resource := &spec.CustomResource{} - err = yaml.Unmarshal(kvs.Value, resource) - if err != nil { - panic(fmt.Errorf("BUG: unmarshal %s to yaml failed: %v", string(kvs.Value), err)) - } - return resource } // PutCustomResource writes the custom resource kind to storage. -func (s *Service) PutCustomResource(obj *spec.CustomResource) { - buff, err := yaml.Marshal(obj) +func (s *Service) PutCustomResource(resource spec.CustomResource, update bool) { + kind := resource.GetString("kind") + _, err := s.cds.PutData(kind, resource, update) if err != nil { - panic(fmt.Errorf("BUG: marshal %#v to yaml failed: %v", obj, err)) - } - - err = s.store.Put(layout.CustomResourceKey(obj.Kind(), obj.Name()), string(buff)) - if err != nil { - api.ClusterPanic(err) + panic(err) } } // WatchCustomResource watches custom resources of the specified kind -func (s *Service) WatchCustomResource(ctx context.Context, kind string, onChange func([]*spec.CustomResource)) error { - syncer, err := s.store.Syncer() - if err != nil { - return err - } - - prefix := layout.CustomResourcePrefix(kind) - ch, err := syncer.SyncRawPrefix(prefix) - if err != nil { - return err - } - - for { - select { - case <-ctx.Done(): - syncer.Close() - return nil - case m := <-ch: - resources := make([]*spec.CustomResource, 0, len(m)) - for _, v := range m { - resource := &spec.CustomResource{} - err = yaml.Unmarshal(v.Value, resource) - if err == nil { - resources = append(resources, resource) - } - } - onChange(resources) - } - } +func (s *Service) WatchCustomResource(ctx context.Context, kind string, onChange func([]spec.CustomResource)) error { + return s.cds.Watch(ctx, kind, onChange) } // ListHTTPRouteGroups lists HTTP route groups diff --git a/pkg/object/meshcontroller/spec/spec.go b/pkg/object/meshcontroller/spec/spec.go index 6567011c50..8b1f31e7cc 100644 --- a/pkg/object/meshcontroller/spec/spec.go +++ b/pkg/object/meshcontroller/spec/spec.go @@ -24,6 +24,7 @@ import ( "gopkg.in/yaml.v2" + "github.com/megaease/easegress/pkg/cluster" "github.com/megaease/easegress/pkg/filter/circuitbreaker" "github.com/megaease/easegress/pkg/filter/meshadaptor" "github.com/megaease/easegress/pkg/filter/mock" @@ -34,7 +35,6 @@ import ( "github.com/megaease/easegress/pkg/logger" "github.com/megaease/easegress/pkg/object/httppipeline" "github.com/megaease/easegress/pkg/supervisor" - "github.com/megaease/easegress/pkg/util/dynamicobject" "github.com/megaease/easegress/pkg/util/httpfilter" "github.com/megaease/easegress/pkg/util/httpheader" "github.com/megaease/easegress/pkg/util/urlrule" @@ -362,13 +362,10 @@ type ( } // CustomResourceKind defines the spec of a custom resource kind - CustomResourceKind struct { - Name string `yaml:"name" jsonschema:"required"` - JSONSchema dynamicobject.DynamicObject `yaml:"jsonSchema" jsonschema:"omitempty"` - } + CustomResourceKind = cluster.CustomDataKind // CustomResource defines the spec of a custom resource - CustomResource dynamicobject.DynamicObject + CustomResource = cluster.CustomData // HTTPMatch defines an individual route for HTTP traffic HTTPMatch struct { @@ -460,27 +457,6 @@ func (tr *TrafficRules) Clone() *TrafficRules { } } -// Name returns the 'name' field of the custom resource -func (cr CustomResource) Name() string { - if v, ok := cr["name"].(string); ok { - return v - } - return "" -} - -// Kind returns the 'kind' field of the custom resource -func (cr CustomResource) Kind() string { - if v, ok := cr["kind"].(string); ok { - return v - } - return "" -} - -// UnmarshalYAML implements yaml.Unmarshaler -func (cr *CustomResource) UnmarshalYAML(unmarshal func(interface{}) error) error { - return (*dynamicobject.DynamicObject)(cr).UnmarshalYAML(unmarshal) -} - // Validate validates Spec. func (a Admin) Validate() error { switch a.RegistryType { diff --git a/pkg/object/meshcontroller/spec/spec_test.go b/pkg/object/meshcontroller/spec/spec_test.go index 306c2438aa..f55bf2cc29 100644 --- a/pkg/object/meshcontroller/spec/spec_test.go +++ b/pkg/object/meshcontroller/spec/spec_test.go @@ -1205,30 +1205,6 @@ func TestEgressName(t *testing.T) { func TestCustomResource(t *testing.T) { r := CustomResource{} - if r.Name() != "" { - t.Error("name should be empty") - } - r["name"] = 1 - if r.Name() != "" { - t.Error("name should be empty") - } - r["name"] = "obj1" - if r.Name() != "obj1" { - t.Error("name should be obj1") - } - - if r.Kind() != "" { - t.Error("kind should be empty") - } - r["kind"] = 1 - if r.Kind() != "" { - t.Error("kind should be empty") - } - r["kind"] = "kind1" - if r.Kind() != "kind1" { - t.Error("kind should be kind1") - } - r["field1"] = map[string]interface{}{ "sub1": 1, "sub2": "value2", From 7f7f38d06acc53262a30fc2d55153317755984cf Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Mon, 7 Feb 2022 14:55:06 +0800 Subject: [PATCH 05/14] add client command for custom data --- cmd/client/command/common.go | 6 +- cmd/client/command/customdata.go | 198 ++++++++++++++++++++++++++++++- cmd/client/main.go | 1 + pkg/api/customdata.go | 15 ++- 4 files changed, 205 insertions(+), 15 deletions(-) diff --git a/cmd/client/command/common.go b/cmd/client/command/common.go index 37aacd5417..261cb1a8be 100644 --- a/cmd/client/command/common.go +++ b/cmd/client/command/common.go @@ -63,8 +63,10 @@ const ( wasmCodeURL = apiURL + "/wasm/code" wasmDataURL = apiURL + "/wasm/data/%s/%s" - customDataKindURL = apiURL + "/customdata/%s" - customDataURL = apiURL + "/customdata/%s/%s" + customDataKindURL = apiURL + "/customdatakinds" + customDataKindItemURL = apiURL + "/customdatakinds/%s" + customDataURL = apiURL + "/customdata/%s" + customDataItemURL = apiURL + "/customdata/%s/%s" // MeshTenantsURL is the mesh tenant prefix. MeshTenantsURL = apiURL + "/mesh/tenants" diff --git a/cmd/client/command/customdata.go b/cmd/client/command/customdata.go index 4f7e07dea3..ac9c82f73d 100644 --- a/cmd/client/command/customdata.go +++ b/cmd/client/command/customdata.go @@ -24,6 +24,118 @@ import ( "github.com/spf13/cobra" ) +// CustomDataKindCmd defines custom data kind command. +func CustomDataKindCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "custom-data-kind", + Short: "View and change custom data kind", + } + + cmd.AddCommand(listCustomDataKindCmd()) + cmd.AddCommand(getCustomDataKindCmd()) + cmd.AddCommand(createCustomDataKindCmd()) + cmd.AddCommand(updateCustomDataKindCmd()) + cmd.AddCommand(deleteCustomDataKindCmd()) + + return cmd +} + +func listCustomDataKindCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "list", + Short: "List all custom data kinds", + Example: "egctl custom-data-kind list", + + Run: func(cmd *cobra.Command, args []string) { + handleRequest(http.MethodGet, makeURL(customDataKindURL), nil, cmd) + }, + } + + return cmd +} + +func getCustomDataKindCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "get", + Short: "Get a custom data kind", + Example: "egctl custom-data-kind get ", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("requires custom data kind to be retrieved") + } + return nil + }, + + Run: func(cmd *cobra.Command, args []string) { + handleRequest(http.MethodGet, makeURL(customDataKindItemURL, args[0]), nil, cmd) + }, + } + + return cmd +} + +func createCustomDataKindCmd() *cobra.Command { + var specFile string + cmd := &cobra.Command{ + Use: "create", + Short: "Create a custom data kind from a yaml file or stdin", + Example: "egctl custom-data-kind create -f ", + Run: func(cmd *cobra.Command, args []string) { + visitor := buildYAMLVisitor(specFile, cmd) + visitor.Visit(func(yamlDoc []byte) error { + handleRequest(http.MethodPost, makeURL(customDataKindURL), yamlDoc, cmd) + return nil + }) + visitor.Close() + }, + } + + cmd.Flags().StringVarP(&specFile, "file", "f", "", "A yaml file specifying the change request.") + + return cmd +} + +func updateCustomDataKindCmd() *cobra.Command { + var specFile string + cmd := &cobra.Command{ + Use: "update", + Short: "Update a custom data from a yaml file or stdin", + Example: "egctl custom-data-kind update -f ", + Run: func(cmd *cobra.Command, args []string) { + visitor := buildYAMLVisitor(specFile, cmd) + visitor.Visit(func(yamlDoc []byte) error { + handleRequest(http.MethodPut, makeURL(customDataKindURL), yamlDoc, cmd) + return nil + }) + visitor.Close() + }, + } + + cmd.Flags().StringVarP(&specFile, "file", "f", "", "A yaml file specifying the change request.") + + return cmd +} + +func deleteCustomDataKindCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a custom data kind", + Example: "egctl custom-data-kind delete ", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("requires custom data kind to be retrieved") + } + return nil + }, + + Run: func(cmd *cobra.Command, args []string) { + handleRequest(http.MethodDelete, makeURL(customDataKindItemURL, args[0]), nil, cmd) + }, + } + + return cmd +} + // CustomDataCmd defines custom data command. func CustomDataCmd() *cobra.Command { cmd := &cobra.Command{ @@ -33,7 +145,10 @@ func CustomDataCmd() *cobra.Command { cmd.AddCommand(listCustomDataCmd()) cmd.AddCommand(getCustomDataCmd()) + cmd.AddCommand(createCustomDataCmd()) cmd.AddCommand(updateCustomDataCmd()) + cmd.AddCommand(batchUpdateCustomDataCmd()) + cmd.AddCommand(deleteCustomDataCmd()) return cmd } @@ -41,7 +156,7 @@ func CustomDataCmd() *cobra.Command { func getCustomDataCmd() *cobra.Command { cmd := &cobra.Command{ Use: "get", - Short: "Get an custom data", + Short: "Get a custom data", Example: "egctl custom-data get ", Args: func(cmd *cobra.Command, args []string) error { if len(args) != 2 { @@ -51,7 +166,7 @@ func getCustomDataCmd() *cobra.Command { }, Run: func(cmd *cobra.Command, args []string) { - handleRequest(http.MethodGet, makeURL(customDataURL, args[0], args[1]), nil, cmd) + handleRequest(http.MethodGet, makeURL(customDataItemURL, args[0], args[1]), nil, cmd) }, } @@ -70,19 +185,73 @@ func listCustomDataCmd() *cobra.Command { return nil }, Run: func(cmd *cobra.Command, args []string) { - handleRequest(http.MethodGet, makeURL(customDataKindURL, args[0]), nil, cmd) + handleRequest(http.MethodGet, makeURL(customDataURL, args[0]), nil, cmd) }, } return cmd } +func createCustomDataCmd() *cobra.Command { + var specFile string + cmd := &cobra.Command{ + Use: "create", + Short: "Create a custom data from a yaml file or stdin", + Example: "egctl custom-data create -f ", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("requires custom data kind to be retrieved") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + visitor := buildYAMLVisitor(specFile, cmd) + visitor.Visit(func(yamlDoc []byte) error { + handleRequest(http.MethodPost, makeURL(customDataURL, args[0]), yamlDoc, cmd) + return nil + }) + visitor.Close() + }, + } + + cmd.Flags().StringVarP(&specFile, "file", "f", "", "A yaml file specifying the change request.") + + return cmd +} + func updateCustomDataCmd() *cobra.Command { var specFile string cmd := &cobra.Command{ Use: "update", + Short: "Update a custom data from a yaml file or stdin", + Example: "egctl custom-data update -f ", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 1 { + return errors.New("requires custom data kind to be retrieved") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + visitor := buildYAMLVisitor(specFile, cmd) + visitor.Visit(func(yamlDoc []byte) error { + handleRequest(http.MethodPut, makeURL(customDataURL, args[0]), yamlDoc, cmd) + return nil + }) + visitor.Close() + }, + } + + cmd.Flags().StringVarP(&specFile, "file", "f", "", "A yaml file specifying the change request.") + + return cmd +} + +func batchUpdateCustomDataCmd() *cobra.Command { + var specFile string + cmd := &cobra.Command{ + Use: "batch-update", Short: "Batch update custom data from a yaml file or stdin", - Example: "egctl custom-data update -f ", + Example: "egctl custom-data batch-update -f ", Args: func(cmd *cobra.Command, args []string) error { if len(args) != 1 { return errors.New("requires custom data kind to be retrieved") @@ -92,7 +261,7 @@ func updateCustomDataCmd() *cobra.Command { Run: func(cmd *cobra.Command, args []string) { visitor := buildYAMLVisitor(specFile, cmd) visitor.Visit(func(yamlDoc []byte) error { - handleRequest(http.MethodPost, makeURL(customDataKindURL, args[0]), yamlDoc, cmd) + handleRequest(http.MethodPost, makeURL(customDataItemURL, args[0], "items"), yamlDoc, cmd) return nil }) visitor.Close() @@ -103,3 +272,22 @@ func updateCustomDataCmd() *cobra.Command { return cmd } + +func deleteCustomDataCmd() *cobra.Command { + cmd := &cobra.Command{ + Use: "delete", + Short: "Delete a custom data item", + Example: "egctl custom-data delete ", + Args: func(cmd *cobra.Command, args []string) error { + if len(args) != 2 { + return errors.New("requires custom data kind and id to be retrieved") + } + return nil + }, + Run: func(cmd *cobra.Command, args []string) { + handleRequest(http.MethodDelete, makeURL(customDataItemURL, args[0], args[1]), nil, cmd) + }, + } + + return cmd +} diff --git a/cmd/client/main.go b/cmd/client/main.go index d200591c76..1bdd1e5bd9 100644 --- a/cmd/client/main.go +++ b/cmd/client/main.go @@ -110,6 +110,7 @@ func main() { command.ObjectCmd(), command.MemberCmd(), command.WasmCmd(), + command.CustomDataKindCmd(), command.CustomDataCmd(), completionCmd, ) diff --git a/pkg/api/customdata.go b/pkg/api/customdata.go index c2466a17d4..ae3f4238ef 100644 --- a/pkg/api/customdata.go +++ b/pkg/api/customdata.go @@ -74,7 +74,7 @@ func (s *Server) customDataAPIEntries() []*Entry { Handler: s.listCustomData, }, { - Path: CustomDataPrefix + "/{key}", + Path: CustomDataPrefix + "/{id}", Method: http.MethodGet, Handler: s.getCustomData, }, @@ -89,14 +89,14 @@ func (s *Server) customDataAPIEntries() []*Entry { Handler: s.updateCustomData, }, { - Path: CustomDataPrefix + "/{key}", + Path: CustomDataPrefix + "/{id}", Method: http.MethodDelete, Handler: s.deleteCustomData, }, { Path: CustomDataPrefix + "/items", - Method: http.MethodPut, + Method: http.MethodPost, Handler: s.batchUpdateCustomData, }, } @@ -182,9 +182,9 @@ func (s *Server) listCustomData(w http.ResponseWriter, r *http.Request) { func (s *Server) getCustomData(w http.ResponseWriter, r *http.Request) { kind := chi.URLParam(r, "kind") - key := chi.URLParam(r, "key") + id := chi.URLParam(r, "id") - data, err := s.cds.GetData(kind, key) + data, err := s.cds.GetData(kind, id) if err != nil { ClusterPanic(err) } @@ -210,7 +210,6 @@ func (s *Server) createCustomData(w http.ResponseWriter, r *http.Request) { } w.WriteHeader(http.StatusCreated) - // TODO: location := fmt.Sprintf("%s/%s", r.URL.Path, id) w.Header().Set("Location", location) } @@ -231,8 +230,8 @@ func (s *Server) updateCustomData(w http.ResponseWriter, r *http.Request) { func (s *Server) deleteCustomData(w http.ResponseWriter, r *http.Request) { kind := chi.URLParam(r, "kind") - key := chi.URLParam(r, "key") - err := s.cds.DeleteData(kind, key) + id := chi.URLParam(r, "id") + err := s.cds.DeleteData(kind, id) if err != nil { ClusterPanic(err) } From 117b7566a8d25f54cafac70a4848e2de5b778944 Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Mon, 7 Feb 2022 19:00:13 +0800 Subject: [PATCH 06/14] fix mesh layout typo --- pkg/object/meshcontroller/layout/layout.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/object/meshcontroller/layout/layout.go b/pkg/object/meshcontroller/layout/layout.go index 05965fadd7..c26310c8a9 100644 --- a/pkg/object/meshcontroller/layout/layout.go +++ b/pkg/object/meshcontroller/layout/layout.go @@ -53,10 +53,10 @@ const ( trafficTargetPrefix = "/mesh/traffic-targets/" customResourceKindPrefix = "/mesh/custom-resource-kinds/" - customResourceKind = "/mesh/custom-resource-kinds/%s/" // +kind + customResourceKind = "/mesh/custom-resource-kinds/%s" // +kind allCustomResourcePrefix = "/mesh/custom-resources/" - customResourcePrefix = "/mesh/custom-resources/%s/" // +kind - customResource = "/mesh/custom-resources/%s/%s/" // +kind +name + customResourcePrefix = "/mesh/custom-resources/%s/" // +kind + customResource = "/mesh/custom-resources/%s/%s" // +kind +name globalCanaryHeaders = "/mesh/canary-headers" From 206ddded455a43aa92989e363e2cb4205ddd1f8c Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Wed, 9 Feb 2022 10:36:39 +0800 Subject: [PATCH 07/14] rename for adding test cases --- pkg/api/customdata.go | 16 +- pkg/api/server.go | 5 +- .../customdata.go} | 147 +++++++++--------- pkg/object/meshcontroller/service/service.go | 6 +- pkg/object/meshcontroller/spec/spec.go | 6 +- 5 files changed, 91 insertions(+), 89 deletions(-) rename pkg/cluster/{customdatastore.go => customdata/customdata.go} (61%) diff --git a/pkg/api/customdata.go b/pkg/api/customdata.go index ae3f4238ef..21af6d350b 100644 --- a/pkg/api/customdata.go +++ b/pkg/api/customdata.go @@ -22,7 +22,7 @@ import ( "net/http" "github.com/go-chi/chi/v5" - "github.com/megaease/easegress/pkg/cluster" + "github.com/megaease/easegress/pkg/cluster/customdata" "gopkg.in/yaml.v2" ) @@ -35,9 +35,9 @@ const ( // ChangeRequest represents a change request to custom data type ChangeRequest struct { - Rebuild bool `yaml:"rebuild"` - Delete []string `yaml:"delete"` - List []cluster.CustomData `yaml:"list"` + Rebuild bool `yaml:"rebuild"` + Delete []string `yaml:"delete"` + List []customdata.Data `yaml:"list"` } func (s *Server) customDataAPIEntries() []*Entry { @@ -130,7 +130,7 @@ func (s *Server) getCustomDataKind(w http.ResponseWriter, r *http.Request) { } func (s *Server) createCustomDataKind(w http.ResponseWriter, r *http.Request) { - k := cluster.CustomDataKind{} + k := customdata.Kind{} err := yaml.NewDecoder(r.Body).Decode(&k) if err != nil { panic(err) @@ -146,7 +146,7 @@ func (s *Server) createCustomDataKind(w http.ResponseWriter, r *http.Request) { } func (s *Server) updateCustomDataKind(w http.ResponseWriter, r *http.Request) { - k := cluster.CustomDataKind{} + k := customdata.Kind{} err := yaml.NewDecoder(r.Body).Decode(&k) if err != nil { panic(err) @@ -199,7 +199,7 @@ func (s *Server) getCustomData(w http.ResponseWriter, r *http.Request) { func (s *Server) createCustomData(w http.ResponseWriter, r *http.Request) { kind := chi.URLParam(r, "kind") - data := cluster.CustomData{} + data := customdata.Data{} err := yaml.NewDecoder(r.Body).Decode(&data) if err != nil { panic(err) @@ -217,7 +217,7 @@ func (s *Server) createCustomData(w http.ResponseWriter, r *http.Request) { func (s *Server) updateCustomData(w http.ResponseWriter, r *http.Request) { kind := chi.URLParam(r, "kind") - data := cluster.CustomData{} + data := customdata.Data{} err := yaml.NewDecoder(r.Body).Decode(&data) if err != nil { panic(err) diff --git a/pkg/api/server.go b/pkg/api/server.go index 3f5c6a494a..acb7f57f74 100644 --- a/pkg/api/server.go +++ b/pkg/api/server.go @@ -24,6 +24,7 @@ import ( "time" "github.com/megaease/easegress/pkg/cluster" + "github.com/megaease/easegress/pkg/cluster/customdata" "github.com/megaease/easegress/pkg/logger" "github.com/megaease/easegress/pkg/option" "github.com/megaease/easegress/pkg/supervisor" @@ -37,7 +38,7 @@ type ( router *dynamicMux cluster cluster.Cluster super *supervisor.Supervisor - cds *cluster.CustomDataStore + cds *customdata.Store mutex cluster.Mutex mutexMutex sync.Mutex @@ -74,7 +75,7 @@ func MustNewServer(opt *option.Options, cls cluster.Cluster, super *supervisor.S kindPrefix := cls.Layout().CustomDataKindPrefix() dataPrefix := cls.Layout().CustomDataPrefix() - s.cds = cluster.NewCustomDataStore(cls, kindPrefix, dataPrefix) + s.cds = customdata.NewStore(cls, kindPrefix, dataPrefix) s.initMetadata() s.registerAPIs() diff --git a/pkg/cluster/customdatastore.go b/pkg/cluster/customdata/customdata.go similarity index 61% rename from pkg/cluster/customdatastore.go rename to pkg/cluster/customdata/customdata.go index ab43707cff..ed1ebf88ed 100644 --- a/pkg/cluster/customdatastore.go +++ b/pkg/cluster/customdata/customdata.go @@ -15,61 +15,62 @@ * limitations under the License. */ -package cluster +package customdata import ( "context" "fmt" "time" + "github.com/megaease/easegress/pkg/cluster" "github.com/megaease/easegress/pkg/util/dynamicobject" "github.com/xeipuuv/gojsonschema" "go.etcd.io/etcd/client/v3/concurrency" yaml "gopkg.in/yaml.v2" ) -// CustomData represents a custom data -type CustomData = dynamicobject.DynamicObject +// Data represents a custom data +type Data = dynamicobject.DynamicObject -// CustomDataKind defines the spec of a custom data kind -type CustomDataKind struct { - // Name is the name of the CustomDataKind +// Kind defines the spec of a custom data kind +type Kind struct { + // Name is the name of the Kind Name string `yaml:"name" jsonschema:"required"` - // IDField is a field name of CustomData of this kind, this field is the ID + // IDField is a field name of custom data of this kind, this field is the ID // of the data, that's unique among the same kind, the default value is 'name'. IDField string `yaml:"idField" jsonschema:"omitempty"` - // JSONSchema is JSON schema to validate a CustomData of this kind + // JSONSchema is JSON schema to validate a custom data of this kind JSONSchema dynamicobject.DynamicObject `yaml:"jsonSchema" jsonschema:"omitempty"` } -func (cdk *CustomDataKind) dataID(data CustomData) string { +func (k *Kind) dataID(data Data) string { var id string - if cdk.IDField == "" { + if k.IDField == "" { id, _ = data["name"].(string) } else { - id, _ = data[cdk.IDField].(string) + id, _ = data[k.IDField].(string) } return id } -// CustomDataStore defines the storage for custom data -type CustomDataStore struct { - cluster Cluster +// Store defines the storage for custom data +type Store struct { + cluster cluster.Cluster KindPrefix string DataPrefix string } -// NewCustomDataStore creates a new custom data store -func NewCustomDataStore(cls Cluster, kindPrefix string, dataPrefix string) *CustomDataStore { - return &CustomDataStore{ +// NewStore creates a new custom data store +func NewStore(cls cluster.Cluster, kindPrefix string, dataPrefix string) *Store { + return &Store{ cluster: cls, KindPrefix: kindPrefix, DataPrefix: dataPrefix, } } -func unmarshalCustomDataKind(in []byte) (*CustomDataKind, error) { - kind := &CustomDataKind{} +func unmarshalKind(in []byte) (*Kind, error) { + kind := &Kind{} err := yaml.Unmarshal(in, kind) if err != nil { return nil, fmt.Errorf("BUG: unmarshal %s to yaml failed: %v", string(in), err) @@ -77,13 +78,13 @@ func unmarshalCustomDataKind(in []byte) (*CustomDataKind, error) { return kind, nil } -func (cds *CustomDataStore) kindKey(name string) string { - return cds.KindPrefix + name +func (s *Store) kindKey(name string) string { + return s.KindPrefix + name } // GetKind gets custom data kind by its name -func (cds *CustomDataStore) GetKind(name string) (*CustomDataKind, error) { - kvs, err := cds.cluster.GetRaw(cds.kindKey(name)) +func (s *Store) GetKind(name string) (*Kind, error) { + kvs, err := s.cluster.GetRaw(s.kindKey(name)) if err != nil { return nil, err } @@ -92,19 +93,19 @@ func (cds *CustomDataStore) GetKind(name string) (*CustomDataKind, error) { return nil, nil } - return unmarshalCustomDataKind(kvs.Value) + return unmarshalKind(kvs.Value) } // ListKinds lists custom data kinds -func (cds *CustomDataStore) ListKinds() ([]*CustomDataKind, error) { - kvs, err := cds.cluster.GetRawPrefix(cds.KindPrefix) +func (s *Store) ListKinds() ([]*Kind, error) { + kvs, err := s.cluster.GetRawPrefix(s.KindPrefix) if err != nil { return nil, err } - kinds := make([]*CustomDataKind, 0, len(kvs)) + kinds := make([]*Kind, 0, len(kvs)) for _, v := range kvs { - kind, err := unmarshalCustomDataKind(v.Value) + kind, err := unmarshalKind(v.Value) if err != nil { return nil, err } @@ -115,7 +116,7 @@ func (cds *CustomDataStore) ListKinds() ([]*CustomDataKind, error) { } // PutKind creates or updates a custom data kind -func (cds *CustomDataStore) PutKind(kind *CustomDataKind, update bool) error { +func (s *Store) PutKind(kind *Kind, update bool) error { if len(kind.JSONSchema) > 0 { sl := gojsonschema.NewGoLoader(kind.JSONSchema) if _, err := gojsonschema.NewSchema(sl); err != nil { @@ -123,7 +124,7 @@ func (cds *CustomDataStore) PutKind(kind *CustomDataKind, update bool) error { } } - oldKind, err := cds.GetKind(kind.Name) + oldKind, err := s.GetKind(kind.Name) if err != nil { return err } @@ -140,13 +141,13 @@ func (cds *CustomDataStore) PutKind(kind *CustomDataKind, update bool) error { return fmt.Errorf("BUG: marshal %#v to yaml failed: %v", kind, err) } - key := cds.kindKey(kind.Name) - return cds.cluster.Put(key, string(buf)) + key := s.kindKey(kind.Name) + return s.cluster.Put(key, string(buf)) } // DeleteKind deletes a custom data kind -func (cds *CustomDataStore) DeleteKind(name string) error { - kind, err := cds.GetKind(name) +func (s *Store) DeleteKind(name string) error { + kind, err := s.GetKind(name) if err != nil { return err } @@ -155,20 +156,20 @@ func (cds *CustomDataStore) DeleteKind(name string) error { return fmt.Errorf("%s not found", name) } - return cds.cluster.Delete(cds.kindKey(name)) + return s.cluster.Delete(s.kindKey(name)) // TODO: remove custom data? } -func (cds *CustomDataStore) dataPrefix(kind string) string { - return cds.DataPrefix + kind + "/" +func (s *Store) dataPrefix(kind string) string { + return s.DataPrefix + kind + "/" } -func (cds *CustomDataStore) dataKey(kind string, id string) string { - return cds.dataPrefix(kind) + id +func (s *Store) dataKey(kind string, id string) string { + return s.dataPrefix(kind) + id } -func unmarshalCustomData(in []byte) (CustomData, error) { - data := CustomData{} +func unmarshalData(in []byte) (Data, error) { + data := Data{} err := yaml.Unmarshal(in, &data) if err != nil { return nil, fmt.Errorf("BUG: unmarshal %s to yaml failed: %v", string(in), err) @@ -177,8 +178,8 @@ func unmarshalCustomData(in []byte) (CustomData, error) { } // GetData gets custom data by its id -func (cds *CustomDataStore) GetData(kind string, id string) (CustomData, error) { - kvs, err := cds.cluster.GetRaw(cds.dataKey(kind, id)) +func (s *Store) GetData(kind string, id string) (Data, error) { + kvs, err := s.cluster.GetRaw(s.dataKey(kind, id)) if err != nil { return nil, err } @@ -187,24 +188,24 @@ func (cds *CustomDataStore) GetData(kind string, id string) (CustomData, error) return nil, nil } - return unmarshalCustomData(kvs.Value) + return unmarshalData(kvs.Value) } // ListData lists custom data of specified kind. // if kind is empty, it returns custom data of all kinds. -func (cds *CustomDataStore) ListData(kind string) ([]CustomData, error) { - key := cds.DataPrefix +func (s *Store) ListData(kind string) ([]Data, error) { + key := s.DataPrefix if kind != "" { - key = cds.dataPrefix(kind) + key = s.dataPrefix(kind) } - kvs, err := cds.cluster.GetRawPrefix(key) + kvs, err := s.cluster.GetRawPrefix(key) if err != nil { return nil, err } - results := make([]CustomData, 0, len(kvs)) + results := make([]Data, 0, len(kvs)) for _, v := range kvs { - data, err := unmarshalCustomData(v.Value) + data, err := unmarshalData(v.Value) if err != nil { return nil, err } @@ -215,8 +216,8 @@ func (cds *CustomDataStore) ListData(kind string) ([]CustomData, error) { } // PutData creates or updates a custom data item -func (cds *CustomDataStore) PutData(kind string, data CustomData, update bool) (string, error) { - k, err := cds.GetKind(kind) +func (s *Store) PutData(kind string, data Data, update bool) (string, error) { + k, err := s.GetKind(kind) if err != nil { return "", err } @@ -241,7 +242,7 @@ func (cds *CustomDataStore) PutData(kind string, data CustomData, update bool) ( } } - oldData, err := cds.GetData(kind, id) + oldData, err := s.GetData(kind, id) if err != nil { return "", err } @@ -257,8 +258,8 @@ func (cds *CustomDataStore) PutData(kind string, data CustomData, update bool) ( return "", fmt.Errorf("BUG: marshal %#v to yaml failed: %v", data, err) } - key := cds.dataKey(kind, id) - err = cds.cluster.Put(key, string(buf)) + key := s.dataKey(kind, id) + err = s.cluster.Put(key, string(buf)) if err != nil { return "", err } @@ -267,8 +268,8 @@ func (cds *CustomDataStore) PutData(kind string, data CustomData, update bool) ( } // BatchUpdateData updates multiple custom data in a transaction -func (cds *CustomDataStore) BatchUpdateData(kind string, del []string, update []CustomData) error { - k, err := cds.GetKind(kind) +func (s *Store) BatchUpdateData(kind string, del []string, update []Data) error { + k, err := s.GetKind(kind) if err != nil { return err } @@ -296,10 +297,10 @@ func (cds *CustomDataStore) BatchUpdateData(kind string, del []string, update [] } } - return cds.cluster.STM(func(s concurrency.STM) error { + return s.cluster.STM(func(stm concurrency.STM) error { for _, id := range del { - key := cds.dataKey(kind, id) - s.Del(key) + key := s.dataKey(kind, id) + stm.Del(key) } for _, data := range update { @@ -308,16 +309,16 @@ func (cds *CustomDataStore) BatchUpdateData(kind string, del []string, update [] if err != nil { return fmt.Errorf("BUG: marshal %#v to yaml failed: %v", data, err) } - key := cds.dataKey(kind, id) - s.Put(key, string(buf)) + key := s.dataKey(kind, id) + stm.Put(key, string(buf)) } return nil }) } // DeleteData deletes a custom data -func (cds *CustomDataStore) DeleteData(kind string, id string) error { - data, err := cds.GetData(kind, id) +func (s *Store) DeleteData(kind string, id string) error { + data, err := s.GetData(kind, id) if err != nil { return err } @@ -326,23 +327,23 @@ func (cds *CustomDataStore) DeleteData(kind string, id string) error { return fmt.Errorf("%s not found", id) } - return cds.cluster.Delete(cds.dataKey(kind, id)) + return s.cluster.Delete(s.dataKey(kind, id)) } // DeleteAllData deletes all custom data of kind 'kind' -func (cds *CustomDataStore) DeleteAllData(kind string) error { - prefix := cds.dataPrefix(kind) - return cds.cluster.DeletePrefix(prefix) +func (s *Store) DeleteAllData(kind string) error { + prefix := s.dataPrefix(kind) + return s.cluster.DeletePrefix(prefix) } // Watch watches the data of custom data kind 'kind' -func (cds *CustomDataStore) Watch(ctx context.Context, kind string, onChange func([]CustomData)) error { - syncer, err := cds.cluster.Syncer(5 * time.Minute) +func (s *Store) Watch(ctx context.Context, kind string, onChange func([]Data)) error { + syncer, err := s.cluster.Syncer(5 * time.Minute) if err != nil { return err } - prefix := cds.dataPrefix(kind) + prefix := s.dataPrefix(kind) ch, err := syncer.SyncRawPrefix(prefix) if err != nil { return err @@ -354,9 +355,9 @@ func (cds *CustomDataStore) Watch(ctx context.Context, kind string, onChange fun syncer.Close() return nil case m := <-ch: - data := make([]CustomData, 0, len(m)) + data := make([]Data, 0, len(m)) for _, v := range m { - d, err := unmarshalCustomData(v.Value) + d, err := unmarshalData(v.Value) if err == nil { data = append(data, d) } diff --git a/pkg/object/meshcontroller/service/service.go b/pkg/object/meshcontroller/service/service.go index e28a6afd9c..ea052e13a7 100644 --- a/pkg/object/meshcontroller/service/service.go +++ b/pkg/object/meshcontroller/service/service.go @@ -26,7 +26,7 @@ import ( "gopkg.in/yaml.v2" "github.com/megaease/easegress/pkg/api" - "github.com/megaease/easegress/pkg/cluster" + "github.com/megaease/easegress/pkg/cluster/customdata" "github.com/megaease/easegress/pkg/logger" "github.com/megaease/easegress/pkg/object/meshcontroller/layout" "github.com/megaease/easegress/pkg/object/meshcontroller/spec" @@ -42,7 +42,7 @@ type ( spec *spec.Admin store storage.Storage - cds *cluster.CustomDataStore + cds *customdata.Store } ) @@ -54,7 +54,7 @@ func New(superSpec *supervisor.Spec) *Service { superSpec: superSpec, spec: superSpec.ObjectSpec().(*spec.Admin), store: storage.New(superSpec.Name(), superSpec.Super().Cluster()), - cds: cluster.NewCustomDataStore(superSpec.Super().Cluster(), kindPrefix, dataPrefix), + cds: customdata.NewStore(superSpec.Super().Cluster(), kindPrefix, dataPrefix), } return s diff --git a/pkg/object/meshcontroller/spec/spec.go b/pkg/object/meshcontroller/spec/spec.go index 8b1f31e7cc..acc6d3147a 100644 --- a/pkg/object/meshcontroller/spec/spec.go +++ b/pkg/object/meshcontroller/spec/spec.go @@ -24,7 +24,7 @@ import ( "gopkg.in/yaml.v2" - "github.com/megaease/easegress/pkg/cluster" + "github.com/megaease/easegress/pkg/cluster/customdata" "github.com/megaease/easegress/pkg/filter/circuitbreaker" "github.com/megaease/easegress/pkg/filter/meshadaptor" "github.com/megaease/easegress/pkg/filter/mock" @@ -362,10 +362,10 @@ type ( } // CustomResourceKind defines the spec of a custom resource kind - CustomResourceKind = cluster.CustomDataKind + CustomResourceKind = customdata.Kind // CustomResource defines the spec of a custom resource - CustomResource = cluster.CustomData + CustomResource = customdata.Data // HTTPMatch defines an individual route for HTTP traffic HTTPMatch struct { From cfb8a470d7be576802c65764473c8f88df266542 Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Wed, 9 Feb 2022 14:16:17 +0800 Subject: [PATCH 08/14] add unit tests --- pkg/cluster/clustertest/cluster.go | 206 +++++++++ pkg/cluster/customdata/customdata_test.go | 538 ++++++++++++++++++++++ 2 files changed, 744 insertions(+) create mode 100644 pkg/cluster/clustertest/cluster.go create mode 100644 pkg/cluster/customdata/customdata_test.go diff --git a/pkg/cluster/clustertest/cluster.go b/pkg/cluster/clustertest/cluster.go new file mode 100644 index 0000000000..f11fc99480 --- /dev/null +++ b/pkg/cluster/clustertest/cluster.go @@ -0,0 +1,206 @@ +package clustertest + +import ( + "sync" + "time" + + "github.com/megaease/easegress/pkg/cluster" + "go.etcd.io/etcd/api/v3/mvccpb" + "go.etcd.io/etcd/client/v3/concurrency" +) + +// MockedCluster defines a mocked cluster +type MockedCluster struct { + MockedIsLeader func() bool + MockedLayout func() *cluster.Layout + MockedGet func(key string) (*string, error) + MockedGetPrefix func(prefix string) (map[string]string, error) + MockedGetRaw func(key string) (*mvccpb.KeyValue, error) + MockedGetRawPrefix func(prefix string) (map[string]*mvccpb.KeyValue, error) + MockedGetWithOp func(key string, ops ...cluster.ClientOp) (map[string]string, error) + MockedPut func(key, value string) error + MockedPutUnderLease func(key, value string) error + MockedPutAndDelete func(map[string]*string) error + MockedPutAndDeleteUnderLease func(map[string]*string) error + MockedDelete func(key string) error + MockedDeletePrefix func(prefix string) error + MockedSTM func(apply func(concurrency.STM) error) error + MockedWatcher func() (cluster.Watcher, error) + MockedSyncer func(pullInterval time.Duration) (*cluster.Syncer, error) + MockedMutex func(name string) (cluster.Mutex, error) + MockedCloseServer func(wg *sync.WaitGroup) + MockedStartServer func() (chan struct{}, chan struct{}, error) + MockedClose func(wg *sync.WaitGroup) + MockedPurgeMember func(member string) error +} + +// NewMockedCluster creates a new mocked cluster +func NewMockedCluster() *MockedCluster { + return &MockedCluster{} +} + +// IsLeader implements interface function IsLeader +func (mc *MockedCluster) IsLeader() bool { + if mc.MockedIsLeader != nil { + return mc.MockedIsLeader() + } + return true +} + +// Layout implements interface function Layout +func (mc *MockedCluster) Layout() *cluster.Layout { + if mc.MockedLayout != nil { + return mc.MockedLayout() + } + return nil +} + +// Get implements interface function Get +func (mc *MockedCluster) Get(key string) (*string, error) { + if mc.MockedGet != nil { + return mc.MockedGet(key) + } + return nil, nil +} + +// GetPrefix implements interface function GetPrefix +func (mc *MockedCluster) GetPrefix(prefix string) (map[string]string, error) { + if mc.MockedGetPrefix != nil { + return mc.MockedGetPrefix(prefix) + } + return nil, nil +} + +// GetRaw implements interface function GetRaw +func (mc *MockedCluster) GetRaw(key string) (*mvccpb.KeyValue, error) { + if mc.MockedGetRaw != nil { + return mc.MockedGetRaw(key) + } + return nil, nil +} + +// GetRawPrefix implements interface function GetRawPrefix +func (mc *MockedCluster) GetRawPrefix(prefix string) (map[string]*mvccpb.KeyValue, error) { + if mc.MockedGetRawPrefix != nil { + return mc.MockedGetRawPrefix(prefix) + } + return nil, nil +} + +// GetWithOp implements interface function GetWithOp +func (mc *MockedCluster) GetWithOp(key string, ops ...cluster.ClientOp) (map[string]string, error) { + if mc.MockedGetWithOp != nil { + return mc.MockedGetWithOp(key, ops...) + } + return nil, nil +} + +// Put implements interface function Put +func (mc *MockedCluster) Put(key, value string) error { + if mc.MockedPut != nil { + return mc.MockedPut(key, value) + } + return nil +} + +// PutUnderLease implements interface function PutUnderLease +func (mc *MockedCluster) PutUnderLease(key, value string) error { + if mc.MockedPutUnderLease != nil { + return mc.MockedPutUnderLease(key, value) + } + return nil +} + +// PutAndDelete implements interface function PutAndDelete +func (mc *MockedCluster) PutAndDelete(m map[string]*string) error { + if mc.MockedPutAndDelete != nil { + return mc.MockedPutAndDelete(m) + } + return nil +} + +// PutAndDeleteUnderLease implements interface function PutAndDeleteUnderLease +func (mc *MockedCluster) PutAndDeleteUnderLease(m map[string]*string) error { + if mc.MockedPutAndDeleteUnderLease != nil { + return mc.MockedPutAndDeleteUnderLease(m) + } + return nil +} + +// Delete implements interface function Delete +func (mc *MockedCluster) Delete(key string) error { + if mc.MockedDelete != nil { + return mc.MockedDelete(key) + } + return nil +} + +// DeletePrefix implements interface function DeletePrefix +func (mc *MockedCluster) DeletePrefix(prefix string) error { + if mc.MockedDeletePrefix != nil { + return mc.MockedDeletePrefix(prefix) + } + return nil +} + +// STM implements interface function STM +func (mc *MockedCluster) STM(apply func(concurrency.STM) error) error { + if mc.MockedSTM != nil { + return mc.MockedSTM(apply) + } + return nil +} + +// Watcher implements interface function Watcher +func (mc *MockedCluster) Watcher() (cluster.Watcher, error) { + if mc.MockedWatcher != nil { + return mc.MockedWatcher() + } + return nil, nil +} + +// Syncer implements interface function Syncer +func (mc *MockedCluster) Syncer(pullInterval time.Duration) (*cluster.Syncer, error) { + if mc.MockedSyncer != nil { + return mc.MockedSyncer(pullInterval) + } + return nil, nil +} + +// Mutex implements interface function Mutex +func (mc *MockedCluster) Mutex(name string) (cluster.Mutex, error) { + if mc.MockedMutex != nil { + return mc.MockedMutex(name) + } + return nil, nil +} + +// CloseServer implements interface function CloseServer +func (mc *MockedCluster) CloseServer(wg *sync.WaitGroup) { + if mc.MockedCloseServer != nil { + mc.MockedCloseServer(wg) + } +} + +// StartServer implements interface function StartServer +func (mc *MockedCluster) StartServer() (chan struct{}, chan struct{}, error) { + if mc.MockedStartServer != nil { + return mc.MockedStartServer() + } + return nil, nil, nil +} + +// Close implements interface function Close +func (mc *MockedCluster) Close(wg *sync.WaitGroup) { + if mc.MockedClose != nil { + mc.MockedClose(wg) + } +} + +// PurgeMember implements interface function PurgeMember +func (mc *MockedCluster) PurgeMember(member string) error { + if mc.MockedPurgeMember != nil { + return mc.MockedPurgeMember(member) + } + return nil +} diff --git a/pkg/cluster/customdata/customdata_test.go b/pkg/cluster/customdata/customdata_test.go new file mode 100644 index 0000000000..17cfa4b25a --- /dev/null +++ b/pkg/cluster/customdata/customdata_test.go @@ -0,0 +1,538 @@ +package customdata + +import ( + "context" + "fmt" + "strings" + "testing" + "time" + + "github.com/megaease/easegress/pkg/cluster" + "github.com/megaease/easegress/pkg/cluster/clustertest" + "github.com/megaease/easegress/pkg/util/dynamicobject" + + "go.etcd.io/etcd/api/v3/mvccpb" +) + +func TestDataID(t *testing.T) { + k := Kind{} + data := Data{} + data["name"] = "abc" + data["key"] = "key" + + if id := k.dataID(data); id != "abc" { + t.Errorf("data ID should be 'abc` instead of %q", id) + } + + k.IDField = "key" + if id := k.dataID(data); id != "key" { + t.Errorf("data ID should be 'key` instead of %q", id) + } +} + +func TestUnmarshal(t *testing.T) { + data := []byte(":%344") + + _, err := unmarshalKind(data) + if err == nil { + t.Errorf("unmarshalKind should fail") + } + + _, err = unmarshalData(data) + if err == nil { + t.Errorf("unmarshal should fail") + } + + data = []byte(`name: kind1 +idField: name +jsonSchema: + type: object`) + + k, err := unmarshalKind(data) + if err != nil { + t.Errorf("unmarshalKind should succeeded") + } + if k.Name != "kind1" { + t.Errorf("kind name should be 'kind1' instead of %q", k.Name) + } + if k.IDField != "name" { + t.Errorf("ID field should be 'name' instead of %q", k.IDField) + } + + data = []byte(`name: data1 +field1: 123 +`) + d, err := unmarshalData(data) + if err != nil { + t.Errorf("unmarshalData should succeeded") + } + + if name := d.GetString("name"); name != "data1" { + t.Errorf("name should be 'data1' instead of %q", name) + } + + if v := d.Get("field1"); v != 123 { + t.Errorf("field1 should be 123 instead of %v", v) + } +} + +func TestNewStore(t *testing.T) { + s := NewStore(nil, "/kind/", "/data/") + if s.cluster != nil { + t.Error("cluster should be nil") + } + if s.KindPrefix != "/kind/" { + t.Error("KindPrefix should be '/kind'") + } + if s.DataPrefix != "/data/" { + t.Error("DataPrefix should be '/data'") + } + + if key := s.kindKey("test"); key != "/kind/test" { + t.Errorf("kind key should be '/kind/test` instead of %q", key) + } + + if prefix := s.dataPrefix("test"); prefix != "/data/test/" { + t.Errorf("data prefix should be '/data/test/` instead of %q", prefix) + } + + if key := s.dataKey("foo", "bar"); key != "/data/foo/bar" { + t.Errorf("data key should be '/data/foo/bar` instead of %q", key) + } +} + +func TestGetKind(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, fmt.Errorf("mocked error") + } + _, err := s.GetKind("kind1") + if err == nil { + t.Errorf("GetKind should fail") + } + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, nil + } + k, err := s.GetKind("kind1") + if err != nil { + t.Errorf("GetKind should succeed") + } + if k != nil { + t.Errorf("GetKind should return nil") + } + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return &mvccpb.KeyValue{ + Value: []byte(`name: kind1`), + }, nil + } + k, err = s.GetKind("kind1") + if err != nil { + t.Errorf("GetKind should succeed") + } + if k == nil { + t.Errorf("GetKind should return non-nil value") + } +} + +func TestListKinds(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + + cls.MockedGetRawPrefix = func(prefix string) (map[string]*mvccpb.KeyValue, error) { + return nil, fmt.Errorf("mocked error") + } + _, err := s.ListKinds() + if err == nil { + t.Errorf("ListKinds should fail") + } + + cls.MockedGetRawPrefix = func(prefix string) (map[string]*mvccpb.KeyValue, error) { + return map[string]*mvccpb.KeyValue{ + "kind1": {Value: []byte(`name: kind1`)}, + "kind2": {Value: []byte(`@3sj3`)}, + }, nil + } + _, err = s.ListKinds() + if err == nil { + t.Errorf("ListKinds should fail") + } + + cls.MockedGetRawPrefix = func(prefix string) (map[string]*mvccpb.KeyValue, error) { + return map[string]*mvccpb.KeyValue{ + "kind1": {Value: []byte(`name: kind1`)}, + "kind2": {Value: []byte(`name: kind2 +idField: key`), + }, + }, nil + } + kinds, err := s.ListKinds() + if err != nil { + t.Errorf("ListKinds shoud succeed") + } + + if len(kinds) != 2 { + t.Errorf("ListKinds should return 2 kinds") + } +} + +func TestPutKind(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + + k := &Kind{JSONSchema: dynamicobject.DynamicObject{"@#": "2938"}} + err := s.PutKind(k, true) + if err == nil { + t.Errorf("PutKind should fail") + } + + k.JSONSchema = nil + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, fmt.Errorf("mocked error") + } + err = s.PutKind(k, true) + if err == nil { + t.Errorf("PutKind should fail") + } + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, nil + } + err = s.PutKind(k, true) + if err == nil { + t.Errorf("PutKind should fail") + } + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return &mvccpb.KeyValue{ + Value: []byte(`name: kind1`), + }, nil + } + err = s.PutKind(k, false) + if err == nil { + t.Errorf("PutKind should fail") + } + + err = s.PutKind(k, true) + if err != nil { + t.Errorf("PutKind should succeed") + } +} + +func TestDeleteKind(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, fmt.Errorf("mocked error") + } + err := s.DeleteKind("kind1") + if err == nil { + t.Errorf("DeleteKind should fail") + } + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, nil + } + err = s.DeleteKind("kind1") + if err == nil { + t.Errorf("DeleteKind should fail") + } + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return &mvccpb.KeyValue{ + Value: []byte(`name: kind1`), + }, nil + } + err = s.DeleteKind("kind1") + if err != nil { + t.Errorf("DeleteKind should succeed") + } +} + +func TestGetData(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, fmt.Errorf("mocked error") + } + _, err := s.GetData("kind1", "data1") + if err == nil { + t.Errorf("GetData should fail") + } + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, nil + } + d, err := s.GetData("kind1", "data1") + if err != nil { + t.Errorf("GetData should succeed") + } + if d != nil { + t.Errorf("GetData should return nil") + } + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return &mvccpb.KeyValue{ + Value: []byte(`name: data1`), + }, nil + } + d, err = s.GetData("kind1", "data1") + if err != nil { + t.Errorf("GetData should succeed") + } + if d == nil { + t.Errorf("GetData should return non-nil value") + } +} + +func TestListData(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + + cls.MockedGetRawPrefix = func(prefix string) (map[string]*mvccpb.KeyValue, error) { + return nil, fmt.Errorf("mocked error") + } + _, err := s.ListData("") + if err == nil { + t.Errorf("ListData should fail") + } + _, err = s.ListData("kind1") + if err == nil { + t.Errorf("ListData should fail") + } + + cls.MockedGetRawPrefix = func(prefix string) (map[string]*mvccpb.KeyValue, error) { + return map[string]*mvccpb.KeyValue{ + "data1": {Value: []byte(`name: data1`)}, + "data2": {Value: []byte(`@3sj3`)}, + }, nil + } + _, err = s.ListData("kind1") + if err == nil { + t.Errorf("ListData should fail") + } + + cls.MockedGetRawPrefix = func(prefix string) (map[string]*mvccpb.KeyValue, error) { + return map[string]*mvccpb.KeyValue{ + "data1": {Value: []byte(`name: data1`)}, + "data2": {Value: []byte(`name: data2 +field1: 123`), + }, + }, nil + } + data, err := s.ListData("kind1") + if err != nil { + t.Errorf("ListData shoud succeed") + } + + if len(data) != 2 { + t.Errorf("ListData should return 2 data items") + } +} + +func TestPutData(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + + // failed to get kind + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, fmt.Errorf("mocked error") + } + _, err := s.PutData("kind1", nil, true) + if err == nil { + t.Errorf("PutData should fail") + } + + // kind not found + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, nil + } + _, err = s.PutData("kind1", nil, true) + if err == nil { + t.Errorf("PutData should fail") + } + + // empty data id + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return &mvccpb.KeyValue{ + Value: []byte(`name: kind1 +jsonSchema: + type: object + properties: + field1: + type: string + required: true +`)}, nil + } + _, err = s.PutData("kind1", nil, true) + if err == nil { + t.Errorf("PutData should fail") + } + + // validation failure + d := Data{"name": "data1"} + _, err = s.PutData("kind1", d, true) + if err == nil { + t.Errorf("PutData should fail") + } + + // make validation success + d["field1"] = "abc" + + // get data fail + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + if strings.HasSuffix(key, "data1") { + return nil, fmt.Errorf("mocked error") + } + return &mvccpb.KeyValue{ + Value: []byte(`name: kind1 +jsonSchema: + type: object + properties: + field1: + type: string + required: true +`)}, nil + } + _, err = s.PutData("kind1", d, true) + if err == nil { + t.Errorf("PutData should fail") + } + + // get nil data + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + if strings.HasSuffix(key, "data1") { + return nil, nil + } + return &mvccpb.KeyValue{ + Value: []byte(`name: kind1 +jsonSchema: + type: object + properties: + field1: + type: string + required: true +`)}, nil + } + _, err = s.PutData("kind1", d, true) + if err == nil { + t.Errorf("PutData should fail") + } + + // get data success + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + if strings.HasSuffix(key, "data1") { + return &mvccpb.KeyValue{ + Value: []byte(`name: data1`), + }, nil + } + return &mvccpb.KeyValue{ + Value: []byte(`name: kind1`)}, nil + } + _, err = s.PutData("kind1", d, false) + if err == nil { + t.Errorf("PutData should fail") + } + + // update success + id, err := s.PutData("kind1", d, true) + if err != nil { + t.Errorf("PutData should succeed") + } + if id != "data1" { + t.Errorf("data ID should be 'data1' instead of %q", id) + } + + // put data failure + cls.MockedPut = func(key, value string) error { + return fmt.Errorf("mocked error") + } + _, err = s.PutData("kind1", d, true) + if err == nil { + t.Errorf("PutData should fail") + } +} + +func TestBatchUpdateData(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + + // failed to get kind + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, fmt.Errorf("mocked error") + } + err := s.BatchUpdateData("kind1", nil, nil) + if err == nil { + t.Errorf("BatchUpdateData should fail") + } + + // kind not found + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, nil + } + err = s.BatchUpdateData("kind1", nil, nil) + if err == nil { + t.Errorf("BatchUpdateData should fail") + } + + data := []Data{ + {"name": "data1", "field1": "foo"}, + {"field2": "field2"}, + } + // validation failure + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return &mvccpb.KeyValue{ + Value: []byte(`name: kind1 +jsonSchema: + type: object + properties: + field1: + type: string + required: true +`)}, nil + } + err = s.BatchUpdateData("kind1", nil, data) + if err == nil { + t.Errorf("BatchUpdateData should fail") + } + + // empty data id + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return &mvccpb.KeyValue{ + Value: []byte(`name: kind1`), + }, nil + } + err = s.BatchUpdateData("kind1", nil, data) + if err == nil { + t.Errorf("BatchUpdateData should fail") + } + + // make validation success + data[1]["name"] = "data2" + err = s.BatchUpdateData("kind1", []string{"data1", "data3"}, data) + if err != nil { + t.Errorf("BatchUpdateData should succeed") + } +} + +func TestWatch(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + + cls.MockedSyncer = func(pullInterval time.Duration) (*cluster.Syncer, error) { + return nil, fmt.Errorf("mocked error") + } + + ctx, cancel := context.WithCancel(context.Background()) + err := s.Watch(ctx, "kind1", func(d []Data) {}) + if err == nil { + t.Errorf("Watch should fail") + } + + cancel() +} From 9c7a69a021d2d58c12559c2bf53f79e26670763d Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Wed, 9 Feb 2022 14:48:37 +0800 Subject: [PATCH 09/14] add more testcases --- pkg/cluster/clustertest/cluster.go | 17 +++++++ pkg/cluster/customdata/customdata_test.go | 61 ++++++++++++++++++++++- 2 files changed, 77 insertions(+), 1 deletion(-) diff --git a/pkg/cluster/clustertest/cluster.go b/pkg/cluster/clustertest/cluster.go index f11fc99480..7e79b66eee 100644 --- a/pkg/cluster/clustertest/cluster.go +++ b/pkg/cluster/clustertest/cluster.go @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2017, MegaEase + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package clustertest import ( diff --git a/pkg/cluster/customdata/customdata_test.go b/pkg/cluster/customdata/customdata_test.go index 17cfa4b25a..fd872c1a75 100644 --- a/pkg/cluster/customdata/customdata_test.go +++ b/pkg/cluster/customdata/customdata_test.go @@ -1,3 +1,20 @@ +/* + * Copyright (c) 2017, MegaEase + * All rights reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + package customdata import ( @@ -369,13 +386,14 @@ jsonSchema: required: true `)}, nil } + d := Data{} _, err = s.PutData("kind1", nil, true) if err == nil { t.Errorf("PutData should fail") } // validation failure - d := Data{"name": "data1"} + d["name"] = "data1" _, err = s.PutData("kind1", d, true) if err == nil { t.Errorf("PutData should fail") @@ -520,6 +538,47 @@ jsonSchema: } } +func TestDeleteData(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + return nil, fmt.Errorf("mocked error") + } + err := s.DeleteData("kind1", "data1") + if err == nil { + t.Errorf("DeleteData should fail") + } + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + if strings.HasSuffix(key, "data1") { + return nil, nil + } + return &mvccpb.KeyValue{Value: []byte(`name: kind1`)}, nil + } + err = s.DeleteData("kind1", "data1") + if err == nil { + t.Errorf("DeleteData should fail") + } + + cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { + if strings.HasSuffix(key, "data1") { + return &mvccpb.KeyValue{Value: []byte(`name: data1`)}, nil + } + return &mvccpb.KeyValue{Value: []byte(`name: kind1`)}, nil + } + err = s.DeleteData("kind1", "data1") + if err != nil { + t.Errorf("DeleteData should succeed") + } +} + +func testDeleteAllData(t *testing.T) { + cls := clustertest.NewMockedCluster() + s := NewStore(cls, "/kind/", "/data/") + s.DeleteAllData("kind1") +} + func TestWatch(t *testing.T) { cls := clustertest.NewMockedCluster() s := NewStore(cls, "/kind/", "/data/") From c8f1e77868071eeb4dc9bb46b16e9b6bf958a693 Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Wed, 9 Feb 2022 15:28:58 +0800 Subject: [PATCH 10/14] improve coverage --- pkg/cluster/clustertest/cluster.go | 41 ++++++++++++++++++++ pkg/cluster/customdata/customdata_test.go | 47 ++++++++--------------- 2 files changed, 56 insertions(+), 32 deletions(-) diff --git a/pkg/cluster/clustertest/cluster.go b/pkg/cluster/clustertest/cluster.go index 7e79b66eee..abb742aae6 100644 --- a/pkg/cluster/clustertest/cluster.go +++ b/pkg/cluster/clustertest/cluster.go @@ -23,6 +23,7 @@ import ( "github.com/megaease/easegress/pkg/cluster" "go.etcd.io/etcd/api/v3/mvccpb" + clientv3 "go.etcd.io/etcd/client/v3" "go.etcd.io/etcd/client/v3/concurrency" ) @@ -221,3 +222,43 @@ func (mc *MockedCluster) PurgeMember(member string) error { } return nil } + +// MockedSTM is a mocked cocurrency.STM +type MockedSTM struct { + // embed concurrency.STM for commit & reset + concurrency.STM + MockedGet func(key ...string) string + MockedPut func(key, val string, opts ...clientv3.OpOption) + MockedRev func(key string) int64 + MockedDel func(key string) +} + +// Get implements STM.Get +func (stm *MockedSTM) Get(key ...string) string { + if stm.MockedGet != nil { + return stm.MockedGet(key...) + } + return "" +} + +// Put implements STM.Put +func (stm *MockedSTM) Put(key, val string, opts ...clientv3.OpOption) { + if stm.MockedPut != nil { + stm.MockedPut(key, val, opts...) + } +} + +// Rev implements STM.Rev +func (stm *MockedSTM) Rev(key string) int64 { + if stm.MockedRev != nil { + return stm.MockedRev(key) + } + return 0 +} + +// Del implements STM.Del +func (stm *MockedSTM) Del(key string) { + if stm.MockedRev != nil { + stm.MockedDel(key) + } +} diff --git a/pkg/cluster/customdata/customdata_test.go b/pkg/cluster/customdata/customdata_test.go index fd872c1a75..e5e9695fae 100644 --- a/pkg/cluster/customdata/customdata_test.go +++ b/pkg/cluster/customdata/customdata_test.go @@ -29,6 +29,7 @@ import ( "github.com/megaease/easegress/pkg/util/dynamicobject" "go.etcd.io/etcd/api/v3/mvccpb" + "go.etcd.io/etcd/client/v3/concurrency" ) func TestDataID(t *testing.T) { @@ -382,12 +383,12 @@ jsonSchema: type: object properties: field1: - type: string - required: true + type: string + required: true `)}, nil } d := Data{} - _, err = s.PutData("kind1", nil, true) + _, err = s.PutData("kind1", d, true) if err == nil { t.Errorf("PutData should fail") } @@ -404,18 +405,7 @@ jsonSchema: // get data fail cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { - if strings.HasSuffix(key, "data1") { - return nil, fmt.Errorf("mocked error") - } - return &mvccpb.KeyValue{ - Value: []byte(`name: kind1 -jsonSchema: - type: object - properties: - field1: - type: string - required: true -`)}, nil + return nil, fmt.Errorf("mocked error") } _, err = s.PutData("kind1", d, true) if err == nil { @@ -427,15 +417,7 @@ jsonSchema: if strings.HasSuffix(key, "data1") { return nil, nil } - return &mvccpb.KeyValue{ - Value: []byte(`name: kind1 -jsonSchema: - type: object - properties: - field1: - type: string - required: true -`)}, nil + return &mvccpb.KeyValue{Value: []byte(`name: kind1`)}, nil } _, err = s.PutData("kind1", d, true) if err == nil { @@ -445,12 +427,9 @@ jsonSchema: // get data success cls.MockedGetRaw = func(key string) (*mvccpb.KeyValue, error) { if strings.HasSuffix(key, "data1") { - return &mvccpb.KeyValue{ - Value: []byte(`name: data1`), - }, nil + return &mvccpb.KeyValue{Value: []byte(`name: data1`)}, nil } - return &mvccpb.KeyValue{ - Value: []byte(`name: kind1`)}, nil + return &mvccpb.KeyValue{Value: []byte(`name: kind1`)}, nil } _, err = s.PutData("kind1", d, false) if err == nil { @@ -510,8 +489,8 @@ jsonSchema: type: object properties: field1: - type: string - required: true + type: string + required: true `)}, nil } err = s.BatchUpdateData("kind1", nil, data) @@ -531,6 +510,10 @@ jsonSchema: } // make validation success + cls.MockedSTM = func(apply func(concurrency.STM) error) error { + stm := &clustertest.MockedSTM{} + return apply(stm) + } data[1]["name"] = "data2" err = s.BatchUpdateData("kind1", []string{"data1", "data3"}, data) if err != nil { @@ -573,7 +556,7 @@ func TestDeleteData(t *testing.T) { } } -func testDeleteAllData(t *testing.T) { +func TestDeleteAllData(t *testing.T) { cls := clustertest.NewMockedCluster() s := NewStore(cls, "/kind/", "/data/") s.DeleteAllData("kind1") From 2c0c6f13e78130b337549aa83f554d4210bb083f Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Thu, 10 Feb 2022 10:46:25 +0800 Subject: [PATCH 11/14] add documentation for custom data --- doc/README.md | 3 ++ doc/reference/customdata.md | 105 ++++++++++++++++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 doc/reference/customdata.md diff --git a/doc/README.md b/doc/README.md index 13c55f5af9..b500037a66 100644 --- a/doc/README.md +++ b/doc/README.md @@ -9,6 +9,7 @@ - [4.1.1 System Controllers](#411-system-controllers) - [4.1.2 Business Controllers](#412-business-controllers) - [4.2 Filters](#42-filters) + - [4.3 Custom Data](#43-custom-data) ## 1. Cookbook / How-To Guide @@ -91,4 +92,6 @@ It could be created, updated, deleted by admin operation. They control various r - [Validator](./reference/filters.md#Validator) - The Validator filter validates requests, forwards valid ones, and rejects invalid ones. - [WasmHost](./reference/filters.md#WasmHost) - The WasmHost filter implements a host environment for user-developed WebAssembly code. +### 4.3 Custom Data +- [Custom Data Management](./reference/customdata.md) - Create/Read/Update/Delete custom data kinds and custom data items. diff --git a/doc/reference/customdata.md b/doc/reference/customdata.md new file mode 100644 index 0000000000..28db27529c --- /dev/null +++ b/doc/reference/customdata.md @@ -0,0 +1,105 @@ +# Custom Data Management + +The `Custom Data` feature implements a storage for 'any' data, which can be used by other components for data persistence. + +Because the schema of the data being persisted varies from components, a `CustomDataKind` must be defined to distinguish the data. + +## CustomDataKind + +The YAML example below defines a CustomDataKind: + +```yaml +name: kind1 +idField: name +jsonSchema: + type: object + properties: + name: + type: string + required: true +``` + +The `name` field is required, it is the kind name of custom data items of this kind. +The `idField` is optional, and its default value is `name`, the value of this field of a data item is used as its identifier. +The `jsonSchema` is optional, if provided, data items of this kind will be validated against this [JSON Schema](http://json-schema.org/). + +## CustomData + +CustomData is a map, the keys of this map must be strings while the values can be any valid JSON values, but when value is also a map, its keys must be strings too. + +A CustomData item must contain the `idField` defined by its corresponding CustomDataKind, for example, the data items of the kind defined in the above example must contain the `name` field as their identifiers. + +Below is an example of a CustomData: + +```yaml +name: data1 +field1: 12 +field2: abc +field3: [1, 2, 3, 4] +``` + +## API + +* **Create a CustomDataKind** + * **URL**: http://{ip}:{port}/apis/v1/customdatakinds + * **Method**: POST + * **Body**: CustomDataKind definition is YAML. + +* **Update a CustomDataKind** + * **URL**: http://{ip}:{port}/apis/v1/customdatakinds + * **Method**: PUT + * **Body**: CustomDataKind definition is YAML. + +* **Query the definition of a CustomDataKind** + * **URL**: http://{ip}:{port}/apis/v1/customdatakinds/{kind name} + * **Method**: GET + +* **List the definition of all CustomDataKind** + * **URL**: http://{ip}:{port}/apis/v1/customdatakinds + * **Method**: GET + +* **Delete a CustomDataKind** + * **URL**: http://{ip}:{port}/apis/v1/customdatakinds/{kind name} + * **Method**: DELETE + +* **Create a CustomData** + * **URL**: http://{ip}:{port}/apis/v1/customdata/{kind name} + * **Method**: POST + * **Body**: CustomData definition is YAML. + +* **Update a CustomData** + * **URL**: http://{ip}:{port}/apis/v1/customdata/{kind name} + * **Method**: PUT + * **Body**: CustomData definition is YAML. + +* **Query the definition of a CustomData** + * **URL**: http://{ip}:{port}/apis/v1/customdata/{kind name}/{data id} + * **Method**: GET + +* **List the definition of all CustomData of a kind** + * **URL**: http://{ip}:{port}/apis/v1/customdata/{kind name} + * **Method**: GET + +* **Delete a CustomData** + * **URL**: http://{ip}:{port}/apis/v1/customdata/{kind name}/{data id} + * **Method**: DELETE + +* **Delete all CustomData of a kind** + * **URL**: http://{ip}:{port}/apis/v1/customdata/{kind name} + * **Method**: DELETE + +* **Bulk update** + * **URL**: http://{ip}:{port}/apis/v1/customdata/{kind name}/items + * **Method**: POST + * **Body**: A change request in YAML, as defined below. + + ```yaml + rebuild: false + delete: [data1, data2] + list: + - name: data3 + field1: 12 + - name: data4 + field1: foo + ``` + When `rebuild` is true (default is false), all existing data items are deleted before processing the data items in `list`. `delete` is an array of data identifiers to be deleted, this array is ignored when `rebuild` is true. `list` is an array of data items to be created or updated. From 11bb7618cde5dc37b9d283174da20280fefdf6f9 Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Thu, 10 Feb 2022 11:40:26 +0800 Subject: [PATCH 12/14] Apply suggestions from code review Co-authored-by: Samu Tamminen --- doc/reference/customdata.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/reference/customdata.md b/doc/reference/customdata.md index 28db27529c..a86eb2a718 100644 --- a/doc/reference/customdata.md +++ b/doc/reference/customdata.md @@ -15,8 +15,8 @@ jsonSchema: type: object properties: name: - type: string - required: true + type: string + required: true ``` The `name` field is required, it is the kind name of custom data items of this kind. From 3ebe8933eda3a539f14c67b99ed4bb7cc0c5d440 Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Thu, 10 Feb 2022 11:43:57 +0800 Subject: [PATCH 13/14] Apply suggestions from code review Co-authored-by: Samu Tamminen --- doc/reference/customdata.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/reference/customdata.md b/doc/reference/customdata.md index a86eb2a718..25727ddfac 100644 --- a/doc/reference/customdata.md +++ b/doc/reference/customdata.md @@ -93,13 +93,13 @@ field3: [1, 2, 3, 4] * **Method**: POST * **Body**: A change request in YAML, as defined below. - ```yaml - rebuild: false - delete: [data1, data2] - list: - - name: data3 - field1: 12 - - name: data4 - field1: foo +```yaml +rebuild: false +delete: [data1, data2] +list: +- name: data3 + field1: 12 +- name: data4 + field1: foo ``` When `rebuild` is true (default is false), all existing data items are deleted before processing the data items in `list`. `delete` is an array of data identifiers to be deleted, this array is ignored when `rebuild` is true. `list` is an array of data items to be created or updated. From 8a86e089bef1cdfe8edb16d508de48aadf72620a Mon Sep 17 00:00:00 2001 From: Bomin Zhang Date: Thu, 10 Feb 2022 13:49:33 +0800 Subject: [PATCH 14/14] update according to comment --- doc/reference/customdata.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/reference/customdata.md b/doc/reference/customdata.md index 25727ddfac..9467bd761f 100644 --- a/doc/reference/customdata.md +++ b/doc/reference/customdata.md @@ -25,7 +25,7 @@ The `jsonSchema` is optional, if provided, data items of this kind will be valid ## CustomData -CustomData is a map, the keys of this map must be strings while the values can be any valid JSON values, but when value is also a map, its keys must be strings too. +CustomData is a map, the keys of this map must be strings while the values can be any valid JSON values, but the keys of a nested map must be strings too. A CustomData item must contain the `idField` defined by its corresponding CustomDataKind, for example, the data items of the kind defined in the above example must contain the `name` field as their identifiers. @@ -101,5 +101,5 @@ list: field1: 12 - name: data4 field1: foo - ``` - When `rebuild` is true (default is false), all existing data items are deleted before processing the data items in `list`. `delete` is an array of data identifiers to be deleted, this array is ignored when `rebuild` is true. `list` is an array of data items to be created or updated. +``` +When `rebuild` is true (default is false), all existing data items are deleted before processing the data items in `list`. `delete` is an array of data identifiers to be deleted, this array is ignored when `rebuild` is true. `list` is an array of data items to be created or updated.