diff --git a/pkg/diff/diff.go b/pkg/diff/diff.go index a3e4722..32179fa 100644 --- a/pkg/diff/diff.go +++ b/pkg/diff/diff.go @@ -258,6 +258,8 @@ func (sc *Syncer) init() error { types.ServicePackage, types.ServiceVersion, types.Document, types.FilterChain, + + types.DegraphqlRoute, } sc.entityDiffers = map[types.EntityType]types.Differ{} diff --git a/pkg/diff/order.go b/pkg/diff/order.go index bf1ae3b..558a08a 100644 --- a/pkg/diff/order.go +++ b/pkg/diff/order.go @@ -24,6 +24,7 @@ L3 +---------------------------> Service <---+ +-> Route | | | | v | L4 +----------> Document <---------+ +-> Plugins / <---------+ FilterChains + PluginEntities - DegraphqlRoute */ // dependencyOrder defines the order in which entities will be synced by decK. @@ -64,6 +65,7 @@ var dependencyOrder = [][]types.EntityType{ types.Plugin, types.FilterChain, types.Document, + types.DegraphqlRoute, }, } diff --git a/pkg/file/builder.go b/pkg/file/builder.go index 012e74f..ebe2711 100644 --- a/pkg/file/builder.go +++ b/pkg/file/builder.go @@ -108,6 +108,7 @@ func (b *stateBuilder) build() (*utils.KongRawState, *utils.KonnectRawState, err b.consumers() b.plugins() b.filterChains() + b.pluginEntities() b.enterprise() // konnect @@ -1551,6 +1552,108 @@ func filterChainRelations(filterChain *kong.FilterChain) (rID, sID string) { return } +func (b *stateBuilder) pluginEntities() { + if b.err != nil { + return + } + + supportedPluginEntities := map[string]bool{ + degraphqlRoutesType: true, + } + + var pluginEntities []FPluginEntity + for _, e := range b.targetContent.PluginEntities { + if !supportedPluginEntities[*e.Type] { + b.err = fmt.Errorf("plugin entity %v is not supported", *e.Type) + return + } + + pluginEntities = append(pluginEntities, e) + } + + b.ingestPluginEntities(pluginEntities) +} + +func (b *stateBuilder) ingestPluginEntities(pluginEntities []FPluginEntity) { + for _, e := range pluginEntities { + switch *e.Type { + case degraphqlRoutesType: + b.ingestDeGraphqlRoute(e) + } + } +} + +func (b *stateBuilder) ingestDeGraphqlRoute(degraphqlRouteEntity FPluginEntity) { + degraphqlRoute, err := copyToDegraphqlRoute(degraphqlRouteEntity) + if err != nil { + b.err = err + return + } + + if utils.Empty(degraphqlRoute.ID) { + d, err := b.currentState.DegraphqlRoutes.GetByUriQuery(*degraphqlRoute.URI, *degraphqlRoute.Query) + if errors.Is(err, state.ErrNotFound) { + degraphqlRoute.ID = uuid() + } else if err != nil { + b.err = err + return + } else { + degraphqlRoute.ID = kong.String(*d.ID) + } + } else { + degraphqlRoute.ID = kong.String(*degraphqlRoute.ID) + } + + b.rawState.DegraphqlRoutes = append(b.rawState.DegraphqlRoutes, °raphqlRoute.DegraphqlRoute) +} + +func copyToDegraphqlRoute(fpEntity FPluginEntity) (DegraphqlRoute, error) { + degraphqlRoute := DegraphqlRoute{} + if fpEntity.ID != nil { + degraphqlRoute.ID = fpEntity.ID + } + + if fpEntity.Fields == nil { + return DegraphqlRoute{}, fmt.Errorf("fields are required for degraphql_routes") + } + + if fpEntity.Fields["service"] != nil { + if service, ok := fpEntity.Fields["service"].(*string); ok { + degraphqlRoute.Service = &kong.Service{ + ID: service, + } + } + } + + if fpEntity.Fields["uri"] != nil { + if uri, ok := fpEntity.Fields["uri"].(*string); ok { + degraphqlRoute.URI = uri + } + } + + if fpEntity.Fields["query"] != nil { + if query, ok := fpEntity.Fields["query"].(*string); ok { + degraphqlRoute.Query = query + } + } + + if fpEntity.Fields["methods"] != nil { + if methods, ok := fpEntity.Fields["methods"].([]*string); ok { + methodsString := make([]string, len(methods)) + for i, method := range methods { + methodsString[i] = *method + } + degraphqlRoute.Methods = kong.StringSlice(methodsString...) + } + } + + if degraphqlRoute.Methods == nil { + degraphqlRoute.Methods = kong.StringSlice("GET") + } + + return degraphqlRoute, nil +} + func defaulter( ctx context.Context, client *kong.Client, fileContent *Content, disableDynamicDefaults, isKonnect bool, ) (*utils.Defaulter, error) { diff --git a/pkg/file/codegen/main.go b/pkg/file/codegen/main.go index 85f80ba..af6486c 100644 --- a/pkg/file/codegen/main.go +++ b/pkg/file/codegen/main.go @@ -129,6 +129,9 @@ func main() { } schema.Definitions["MTLSAuth"].Required = []string{"id", "subject_name"} + // plugin entities + schema.Definitions["FPluginEntity"].Required = []string{"type", "plugin"} + // RBAC resources schema.Definitions["FRBACRole"].Required = []string{"name"} schema.Definitions["FRBACEndpointPermission"].Required = []string{"workspace", "endpoint"} diff --git a/pkg/file/kong_json_schema.json b/pkg/file/kong_json_schema.json index 28f4ef6..adb34c4 100644 --- a/pkg/file/kong_json_schema.json +++ b/pkg/file/kong_json_schema.json @@ -68,6 +68,13 @@ }, "type": "array" }, + "plugin_entities": { + "items": { + "$schema": "http://json-schema.org/draft-04/schema#", + "$ref": "#/definitions/FPluginEntity" + }, + "type": "array" + }, "plugins": { "items": { "$ref": "#/definitions/FPlugin" @@ -745,6 +752,29 @@ "additionalProperties": false, "type": "object" }, + "FPluginEntity": { + "required": [ + "type" + ], + "properties": { + "fields": { + "patternProperties": { + ".*": { + "additionalProperties": true + } + }, + "type": "object" + }, + "id": { + "type": "string" + }, + "type": { + "type": "string" + } + }, + "additionalProperties": false, + "type": "object" + }, "FRBACEndpointPermission": { "required": [ "workspace", @@ -1472,25 +1502,25 @@ }, "LookUpSelectorTags": { "properties": { - "consumers": { + "consumer_groups": { "items": { "type": "string" }, "type": "array" }, - "routes": { + "consumers": { "items": { "type": "string" }, "type": "array" }, - "services": { + "routes": { "items": { "type": "string" }, "type": "array" }, - "consumer_groups": { + "services": { "items": { "type": "string" }, diff --git a/pkg/file/types.go b/pkg/file/types.go index 443dbeb..486675c 100644 --- a/pkg/file/types.go +++ b/pkg/file/types.go @@ -30,6 +30,10 @@ const ( httpsPort = 443 ) +const ( + degraphqlRoutesType = "degraphql_routes" +) + // FFilterChain represents a Kong FilterChain. // +k8s:deepcopy-gen=true type FFilterChain struct { @@ -894,6 +898,163 @@ func (c FLicense) sortKey() string { return "" } +// This struct could be used for any custom entity for plugins +// Based on "Type", the entity can be serialized into its +// apt struct. +// +k8s:deepcopy-gen=true +type FPluginEntity struct { + ID *string `json:"id,omitempty" yaml:"id,omitempty"` + Type *string `json:"type,omitempty" yaml:"type,omitempty"` + Fields PluginEntityConfiguration `json:"fields,omitempty" yaml:"fields,omitempty"` +} + +// Configuration represents a config of a plugin-entity in Kong. +type PluginEntityConfiguration map[string]interface{} + +// DeepCopyInto copies the receiver, writing into out. in must be non-nil. +func (in PluginEntityConfiguration) DeepCopyInto(out *PluginEntityConfiguration) { + // Resorting to JSON since interface{} cannot be DeepCopied easily. + // This could be replaced using reflection-fu. + // XXX Ignoring errors + b, _ := json.Marshal(&in) + _ = json.Unmarshal(b, out) +} + +// DeepCopy copies the receiver, creating a new Configuration. +func (in PluginEntityConfiguration) DeepCopy() PluginEntityConfiguration { + if in == nil { + return nil + } + out := new(PluginEntityConfiguration) + in.DeepCopyInto(out) + return *out +} + +func (f *FPluginEntity) UnmarshalJSON(b []byte) error { + var temp map[string]interface{} + if err := json.Unmarshal(b, &temp); err != nil { + return err + } + + if temp["type"] == nil { + return fmt.Errorf("type field is required") + } + + switch temp["type"] { + case degraphqlRoutesType: + var entity map[string]interface{} + err := json.Unmarshal(b, &entity) + if err != nil { + return err + } + return copyToFPluginEntity(entity, f) + default: + return fmt.Errorf("unknown entity type: %s", *f.Type) + } +} + +// UnmarshalYAML is a custom marshal method to handle +// foreign references. +func (f *FPluginEntity) UnmarshalYAML(unmarshal func(interface{}) error) error { + switch *f.Type { + case degraphqlRoutesType: + var entity DegraphqlRoute + if err := unmarshal(&entity); err != nil { + return err + } + return copyFromDegraphqlRoute(entity, f) + default: + return fmt.Errorf("unknown entity type: %s", *f.Type) + } + +} + +// sortKey is used for sorting. +func (f FPluginEntity) sortKey() string { + if f.ID != nil { + return *f.ID + } + return "" +} + +// +k8s:deepcopy-gen=true +type DegraphqlRoute struct { + kong.DegraphqlRoute +} + +func copyFromDegraphqlRoute(dRoute DegraphqlRoute, fpEntity *FPluginEntity) error { + fpEntity.Type = kong.String(degraphqlRoutesType) + + if dRoute.ID != nil { + fpEntity.ID = dRoute.ID + } + + fpEntity.Fields = make(map[string]interface{}) + + if dRoute.Service != nil && dRoute.Service.ID != nil { + fpEntity.Fields["service"] = *dRoute.Service.ID + } + + if dRoute.URI != nil { + fpEntity.Fields["uri"] = *dRoute.URI + } + + if dRoute.Query != nil { + fpEntity.Fields["query"] = *dRoute.Query + } + + if dRoute.Methods != nil { + methods := make([]interface{}, len(dRoute.Methods)) + for i, method := range dRoute.Methods { + methods[i] = method + } + fpEntity.Fields["methods"] = methods + } + + return nil +} + +func copyToFPluginEntity(dRoute map[string]interface{}, fpEntity *FPluginEntity) error { + fpEntity.Type = kong.String(degraphqlRoutesType) + + if dRoute["id"] != nil { + fpEntity.ID = kong.String(dRoute["id"].(string)) + } + + fpEntity.Fields = make(map[string]interface{}) + dRouteFields := dRoute["fields"].(map[string]interface{}) + + if dRouteFields["service"] != nil { + fpEntity.Fields["service"] = kong.String(dRouteFields["service"].(string)) + } + + if dRouteFields["uri"] != nil { + fpEntity.Fields["uri"] = kong.String(dRouteFields["uri"].(string)) + } + + if dRouteFields["query"] != nil { + fpEntity.Fields["query"] = kong.String(dRouteFields["query"].(string)) + } + + if dRouteFields["methods"] != nil { + methods := make([]string, len(dRouteFields["methods"].([]interface{}))) + for i, method := range dRouteFields["methods"].([]interface{}) { + methods[i] = method.(string) + } + fpEntity.Fields["methods"] = kong.StringSlice(methods...) + } + + return nil +} + +// sortKey is used for sorting. +func (d DegraphqlRoute) sortKey() string { + if d.ID != nil { + return *d.ID + } + return "" +} + //go:generate go run ./codegen/main.go // Content represents a serialized Kong state. @@ -924,4 +1085,6 @@ type Content struct { Vaults []FVault `json:"vaults,omitempty" yaml:"vaults,omitempty"` Licenses []FLicense `json:"licenses,omitempty" yaml:"licenses,omitempty"` + + PluginEntities []FPluginEntity `json:"plugin_entities,omitempty" yaml:"plugin_entities,omitempty"` } diff --git a/pkg/file/writer.go b/pkg/file/writer.go index f413116..7e2d6f3 100644 --- a/pkg/file/writer.go +++ b/pkg/file/writer.go @@ -123,6 +123,11 @@ func KongStateToContent(kongState *state.KongState, config WriteConfig) (*Conten return nil, err } + err = populateDegraphqlRoutes(kongState, file) + if err != nil { + return nil, err + } + return file, nil } @@ -878,6 +883,32 @@ func populateLicenses(kongState *state.KongState, file *Content, return nil } +func populateDegraphqlRoutes(kongState *state.KongState, file *Content) error { + degraphqlRoutes, err := kongState.DegraphqlRoutes.GetAll() + if err != nil { + return err + } + + for _, d := range degraphqlRoutes { + f := FPluginEntity{} + + err := copyFromDegraphqlRoute(DegraphqlRoute{ + DegraphqlRoute: d.DegraphqlRoute, + }, &f) + if err != nil { + return err + } + utils.ZeroOutTimestamps(&f) + + file.PluginEntities = append(file.PluginEntities, f) + } + sort.SliceStable(file.PluginEntities, func(i, j int) bool { + return compareOrder(file.PluginEntities[i], file.PluginEntities[j]) + }) + + return nil +} + func WriteContentToFile(content *Content, filename string, format Format) error { var c []byte var err error diff --git a/pkg/file/zz_generated.deepcopy.go b/pkg/file/zz_generated.deepcopy.go index 436a529..287f370 100644 --- a/pkg/file/zz_generated.deepcopy.go +++ b/pkg/file/zz_generated.deepcopy.go @@ -141,6 +141,13 @@ func (in *Content) DeepCopyInto(out *Content) { (*in)[i].DeepCopyInto(&(*out)[i]) } } + if in.PluginEntities != nil { + in, out := &in.PluginEntities, &out.PluginEntities + *out = make([]FPluginEntity, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } return } @@ -154,6 +161,23 @@ func (in *Content) DeepCopy() *Content { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *DegraphqlRoute) DeepCopyInto(out *DegraphqlRoute) { + *out = *in + in.DegraphqlRoute.DeepCopyInto(&out.DegraphqlRoute) + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new DegraphqlRoute. +func (in *DegraphqlRoute) DeepCopy() *DegraphqlRoute { + if in == nil { + return nil + } + out := new(DegraphqlRoute) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FCACertificate) DeepCopyInto(out *FCACertificate) { *out = *in @@ -472,6 +496,33 @@ func (in *FPlugin) DeepCopy() *FPlugin { return out } +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *FPluginEntity) DeepCopyInto(out *FPluginEntity) { + *out = *in + if in.ID != nil { + in, out := &in.ID, &out.ID + *out = new(string) + **out = **in + } + if in.Type != nil { + in, out := &in.Type, &out.Type + *out = new(string) + **out = **in + } + out.Fields = in.Fields.DeepCopy() + return +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new FPluginEntity. +func (in *FPluginEntity) DeepCopy() *FPluginEntity { + if in == nil { + return nil + } + out := new(FPluginEntity) + in.DeepCopyInto(out) + return out +} + // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *FRBACEndpointPermission) DeepCopyInto(out *FRBACEndpointPermission) { *out = *in diff --git a/pkg/state/builder.go b/pkg/state/builder.go index f9c3f33..de47dd9 100644 --- a/pkg/state/builder.go +++ b/pkg/state/builder.go @@ -360,6 +360,39 @@ func buildKong(kongState *KongState, raw *utils.KongRawState) error { return fmt.Errorf("inserting license into state: %w", err) } } + + for _, d := range raw.DegraphqlRoutes { + if d.Service != nil && !utils.Empty(d.Service.ID) { + ok, s, err := ensureService(kongState, *d.Service.ID) + if err != nil { + return err + } + if ok { + d.Service = s + } + } + err := kongState.DegraphqlRoutes.Add(DegraphqlRoute{DegraphqlRoute: *d}) + if err != nil { + return fmt.Errorf("inserting degraphql route into state: %w", err) + } + } + + for _, c := range raw.CustomEntities { + if c.Type() == "degraphql_routes" { + entity := c.Object() + + degraphqlRoute, err := buildDegraphqlRouteFromCustomEntity(kongState, entity) + if err != nil { + return fmt.Errorf("building degraphql route from custom entity: %w", err) + } + + err = kongState.DegraphqlRoutes.Add(degraphqlRoute) + if err != nil { + return fmt.Errorf("inserting degraphql route into state: %w", err) + } + } + } + return nil } @@ -416,3 +449,40 @@ func GetKonnectState(rawKong *utils.KongRawState, } return kongState, nil } + +func buildDegraphqlRouteFromCustomEntity(kongState *KongState, entity map[string]interface{}) (DegraphqlRoute, error) { + var degraphqlRoute DegraphqlRoute + + if entity["id"] != nil { + degraphqlRoute.ID = kong.String(entity["id"].(string)) + } + + if entity["service"] != nil { + serviceId := entity["service"].(map[string]interface{})["id"].(string) + ok, s, err := ensureService(kongState, serviceId) + if err != nil { + return DegraphqlRoute{}, err + } + if ok { + degraphqlRoute.Service = s + } + } + + if entity["uri"] != nil { + degraphqlRoute.URI = kong.String(entity["uri"].(string)) + } + + if entity["query"] != nil { + degraphqlRoute.Query = kong.String(entity["query"].(string)) + } + + if entity["methods"] != nil { + methods := make([]string, len(entity["methods"].([]interface{}))) + for i, v := range entity["methods"].([]interface{}) { + methods[i] = fmt.Sprint(v) + } + degraphqlRoute.Methods = kong.StringSlice(methods...) + } + + return degraphqlRoute, nil +} diff --git a/pkg/state/degraphql_route.go b/pkg/state/degraphql_route.go new file mode 100644 index 0000000..00acccf --- /dev/null +++ b/pkg/state/degraphql_route.go @@ -0,0 +1,113 @@ +package state + +import ( + "fmt" + + memdb "github.com/hashicorp/go-memdb" + "github.com/kong/go-database-reconciler/pkg/state/indexers" +) + +// DegraphqlRoutesCollection stores and indexes jwt-auth credentials. +type DegraphqlRoutesCollection struct { + pluginEntitiesCollection +} + +const pluginEntityType = "degraphql_routes" + +func newDegraphqlRoutesCollection(common collection) *DegraphqlRoutesCollection { + return &DegraphqlRoutesCollection{ + pluginEntitiesCollection: pluginEntitiesCollection{ + collection: common, + PluginEntityType: pluginEntityType, + customIndexes: map[string]*memdb.IndexSchema{ + "uriQuery": { + Name: "uriQuery", + Unique: true, + Indexer: &indexers.MD5FieldsIndexer{ + Fields: []string{"URI", "Query"}, + }, + }, + }, + }, + } +} + +func getDegraphqlRouteByUriQuery(txn *memdb.Txn, uri, query string) (*DegraphqlRoute, error) { + res, err := txn.First(pluginEntityType, "uriQuery", uri, query) + if err != nil { + return nil, err + } + if res == nil { + return nil, ErrNotFound + } + + d, ok := res.(*DegraphqlRoute) + if !ok { + panic(unexpectedType) + } + return &DegraphqlRoute{DegraphqlRoute: *d.DeepCopy()}, nil +} + +// GetByUriQuery gets a degraphql route with +// the same uri and query from the collection. +func (k *DegraphqlRoutesCollection) GetByUriQuery(uri, + query string, +) (*DegraphqlRoute, error) { + if uri == "" || query == "" { + return nil, fmt.Errorf("uri/query cannot be empty string") + } + + txn := k.db.Txn(false) + defer txn.Abort() + + return getDegraphqlRouteByUriQuery(txn, uri, query) +} + +// Add adds a degraphql route credential to DegraphqlRoutesCollection +func (k *DegraphqlRoutesCollection) Add(degraphqlRoute DegraphqlRoute) error { + e := (pluginEntity)(°raphqlRoute) + return k.pluginEntitiesCollection.Add(e) +} + +// Get gets a degraphql route ID. +func (k *DegraphqlRoutesCollection) Get(id string) (*DegraphqlRoute, error) { + e, err := k.pluginEntitiesCollection.Get(id) + if err != nil { + return nil, err + } + + degraphqlRoute, ok := e.(*DegraphqlRoute) + if !ok { + panic(unexpectedType) + } + return &DegraphqlRoute{DegraphqlRoute: *degraphqlRoute.DeepCopy()}, nil +} + +// Update updates an existing degraphql route +func (k *DegraphqlRoutesCollection) Update(degraphqlRoute DegraphqlRoute) error { + e := (pluginEntity)(°raphqlRoute) + return k.pluginEntitiesCollection.Update(e) +} + +// Delete deletes a degraphql route by ID. +func (k *DegraphqlRoutesCollection) Delete(id string) error { + return k.pluginEntitiesCollection.Delete(id) +} + +// GetAll gets all degraphql routes +func (k *DegraphqlRoutesCollection) GetAll() ([]*DegraphqlRoute, error) { + pluginEntities, err := k.pluginEntitiesCollection.GetAll() + if err != nil { + return nil, err + } + + var res []*DegraphqlRoute + for _, e := range pluginEntities { + r, ok := e.(*DegraphqlRoute) + if !ok { + panic(unexpectedType) + } + res = append(res, &DegraphqlRoute{DegraphqlRoute: *r.DeepCopy()}) + } + return res, nil +} diff --git a/pkg/state/plugin_entities.go b/pkg/state/plugin_entities.go new file mode 100644 index 0000000..de6ed2c --- /dev/null +++ b/pkg/state/plugin_entities.go @@ -0,0 +1,169 @@ +package state + +import ( + "errors" + "fmt" + + memdb "github.com/hashicorp/go-memdb" +) + +// pluginEntitiesCollection stores and indexes key-auth credentials. +type pluginEntitiesCollection struct { + collection + PluginEntityType string + customIndexes map[string]*memdb.IndexSchema +} + +func (k *pluginEntitiesCollection) TableName() string { + return k.PluginEntityType +} + +func (k *pluginEntitiesCollection) Schema() *memdb.TableSchema { + completeIndex := map[string]*memdb.IndexSchema{ + "id": { + Name: "id", + Unique: true, + Indexer: &memdb.StringFieldIndex{Field: "ID"}, + }, + all: allIndex, + } + + if k.customIndexes != nil { + for key, index := range k.customIndexes { + completeIndex[key] = index + } + } + + return &memdb.TableSchema{ + Name: k.PluginEntityType, + Indexes: completeIndex, + } +} + +func (k *pluginEntitiesCollection) getByPluginEntityId(txn *memdb.Txn, id string) (pluginEntity, error) { + if id == "" { + return nil, errIDRequired + } + + res, err := txn.First(k.PluginEntityType, "id", id) + if err != nil { + return nil, err + } + if res == nil { + return nil, ErrNotFound + } + + pluginEntity, ok := res.(pluginEntity) + if !ok { + panic(unexpectedType) + } + return pluginEntity, nil +} + +// Add adds a pluginEntity to pluginEntitiesCollection. +func (k *pluginEntitiesCollection) Add(e pluginEntity) error { + if e.GetPluginEntityID() == "" { + return errIDRequired + } + txn := k.db.Txn(true) + defer txn.Abort() + + _, err := k.getByPluginEntityId(txn, e.GetPluginEntityID()) + if err == nil { + return fmt.Errorf("inserting plugin-entity %v - %v : %w", k.PluginEntityType, e.GetPluginEntityID(), ErrAlreadyExists) + } else if !errors.Is(err, ErrNotFound) { + return err + } + + err = txn.Insert(k.PluginEntityType, e) + if err != nil { + return err + } + txn.Commit() + return nil +} + +// Get gets a pluginEntity by ID +func (k *pluginEntitiesCollection) Get(id string) (pluginEntity, error) { + if id == "" { + return nil, errIDRequired + } + + txn := k.db.Txn(false) + defer txn.Abort() + return k.getByPluginEntityId(txn, id) +} + +// Update updates an existing pluginEntity +func (k *pluginEntitiesCollection) Update(e pluginEntity) error { + if e.GetPluginEntityID() == "" { + return errIDRequired + } + + txn := k.db.Txn(true) + defer txn.Abort() + + err := k.deleteCred(txn, e.GetPluginEntityID()) + if err != nil { + return err + } + err = txn.Insert(k.PluginEntityType, e) + if err != nil { + return err + } + + txn.Commit() + return nil +} + +func (k *pluginEntitiesCollection) deleteCred(txn *memdb.Txn, nameOrID string) error { + e, err := k.getByPluginEntityId(txn, nameOrID) + if err != nil { + return err + } + + err = txn.Delete(k.PluginEntityType, e) + if err != nil { + return err + } + return nil +} + +// Delete deletes pluginEntity by ID +func (k *pluginEntitiesCollection) Delete(id string) error { + if id == "" { + return errIDRequired + } + + txn := k.db.Txn(true) + defer txn.Abort() + + err := k.deleteCred(txn, id) + if err != nil { + return err + } + + txn.Commit() + return nil +} + +// GetAll gets all pluginEntities +func (k *pluginEntitiesCollection) GetAll() ([]pluginEntity, error) { + txn := k.db.Txn(false) + defer txn.Abort() + + iter, err := txn.Get(k.PluginEntityType, all, true) + if err != nil { + return nil, err + } + + var res []pluginEntity + for el := iter.Next(); el != nil; el = iter.Next() { + r, ok := el.(pluginEntity) + if !ok { + panic(unexpectedType) + } + res = append(res, r) + } + return res, nil +} diff --git a/pkg/state/state.go b/pkg/state/state.go index d19d06c..cb903e5 100644 --- a/pkg/state/state.go +++ b/pkg/state/state.go @@ -37,6 +37,7 @@ type KongState struct { ACLGroups *ACLGroupsCollection Oauth2Creds *Oauth2CredsCollection MTLSAuths *MTLSAuthsCollection + DegraphqlRoutes *DegraphqlRoutesCollection RBACRoles *RBACRolesCollection RBACEndpointPermissions *RBACEndpointPermissionsCollection @@ -55,6 +56,7 @@ func NewKongState() (*KongState, error) { jwtAuthTemp := newJWTAuthsCollection(collection{}) oauth2CredsTemp := newOauth2CredsCollection(collection{}) mtlsAuthTemp := newMTLSAuthsCollection(collection{}) + degraphqlRouteTemp := newDegraphqlRoutesCollection(collection{}) schema := &memdb.DBSchema{ Tables: map[string]*memdb.TableSchema{ @@ -76,6 +78,8 @@ func NewKongState() (*KongState, error) { vaultTableName: vaultTableSchema, licenseTableName: licenseTableSchema, + degraphqlRouteTemp.TableName(): degraphqlRouteTemp.Schema(), + keyAuthTemp.TableName(): keyAuthTemp.Schema(), hmacAuthTemp.TableName(): hmacAuthTemp.Schema(), basicAuthTemp.TableName(): basicAuthTemp.Schema(), @@ -119,6 +123,8 @@ func NewKongState() (*KongState, error) { state.Vaults = (*VaultsCollection)(&state.common) state.Licenses = (*LicensesCollection)(&state.common) + state.DegraphqlRoutes = newDegraphqlRoutesCollection(state.common) + state.KeyAuths = newKeyAuthsCollection(state.common) state.HMACAuths = newHMACAuthsCollection(state.common) state.BasicAuths = newBasicAuthsCollection(state.common) diff --git a/pkg/state/types.go b/pkg/state/types.go index ca83fd7..1c30695 100644 --- a/pkg/state/types.go +++ b/pkg/state/types.go @@ -1791,3 +1791,50 @@ func (l *License) EqualWithOpts(l2 *License, ignoreID, ignoreTS bool) bool { } return reflect.DeepEqual(l1Copy, l2Copy) } + +type pluginEntity interface { + // ID of the plugin entity. + GetPluginEntityID() string + // Type of the plugin entity + GetPluginEntityType() string +} + +type DegraphqlRoute struct { + kong.DegraphqlRoute `yaml:",inline"` + Meta +} + +// Identifier returns the ID of the DegraphqlRoute. +func (d *DegraphqlRoute) GetPluginEntityID() string { + return *d.ID +} + +// Identifier returns the ID of the DegraphqlRoute. +func (d *DegraphqlRoute) GetPluginEntityType() string { + return "degraphql_routes" +} + +// Console returns the string to identify the DegraphqlRoute. +// Since DegraphqlRoute do not have an alternative field to identify them, it also returns the ID. +func (d *DegraphqlRoute) Console() string { + return *d.ID +} + +// Equal returns true if degraphql route d and d2 are equal. +func (d *DegraphqlRoute) Equal(d2 *DegraphqlRoute) bool { + return d.EqualWithOpts(d2, false) +} + +// EqualWithOpts returns true if degraphql route d and d2 are equal. +// If ignoreID is set to true, IDs will be ignored while comparison. +func (d *DegraphqlRoute) EqualWithOpts(d2 *DegraphqlRoute, ignoreID bool) bool { + d1Copy := d.DegraphqlRoute.DeepCopy() + d2Copy := d2.DegraphqlRoute.DeepCopy() + + if ignoreID { + d1Copy.ID = nil + d2Copy.ID = nil + } + + return reflect.DeepEqual(d1Copy, d2Copy) +} diff --git a/pkg/types/core.go b/pkg/types/core.go index 912c970..7b48c14 100644 --- a/pkg/types/core.go +++ b/pkg/types/core.go @@ -127,6 +127,8 @@ const ( // FilterChain identifies a FilterChain in Kong. FilterChain EntityType = "filter-chain" + + DegraphqlRoute EntityType = "degraphql_routes" ) // AllTypes represents all types defined in the @@ -151,6 +153,8 @@ var AllTypes = []EntityType{ Vault, License, FilterChain, + + DegraphqlRoute, } func entityTypeToKind(t EntityType) crud.Kind { @@ -571,6 +575,22 @@ func NewEntity(t EntityType, opts EntityOpts) (Entity, error) { targetState: opts.TargetState, }, }, nil + case DegraphqlRoute: + return entityImpl{ + typ: DegraphqlRoute, + crudActions: °raphqlRouteCRUD{ + client: opts.KongClient, + }, + postProcessActions: °raphqlRoutePostAction{ + currentState: opts.CurrentState, + }, + differ: °raphqlRouteDiffer{ + kind: entityTypeToKind(DegraphqlRoute), + currentState: opts.CurrentState, + targetState: opts.TargetState, + }, + }, nil + default: return nil, fmt.Errorf("unknown type: %q", t) } diff --git a/pkg/types/degraphql_route.go b/pkg/types/degraphql_route.go new file mode 100644 index 0000000..0a3342d --- /dev/null +++ b/pkg/types/degraphql_route.go @@ -0,0 +1,162 @@ +package types + +import ( + "context" + "errors" + "fmt" + + "github.com/kong/go-database-reconciler/pkg/crud" + "github.com/kong/go-database-reconciler/pkg/state" + "github.com/kong/go-kong/kong" +) + +// degraphqlRouteCRUD implements crud.Actions interface. +type degraphqlRouteCRUD struct { + client *kong.Client +} + +func degraphqlRouteFromStruct(arg crud.Event) *state.DegraphqlRoute { + degraphqlRoute, ok := arg.Obj.(*state.DegraphqlRoute) + if !ok { + panic("unexpected type, expected *state.DegraphqlRoute") + } + + return degraphqlRoute +} + +// Create creates a DegraphqlRoute in Kong. +// The arg should be of type crud.Event, containing the degraphql route to be created, +// else the function will panic. +// It returns a the created *state.DegraphqlRoute. +func (s *degraphqlRouteCRUD) Create(ctx context.Context, arg ...crud.Arg) (crud.Arg, error) { + event := crud.EventFromArg(arg[0]) + degraphqlRoute := degraphqlRouteFromStruct(event) + + createdDegraphqlRoute, err := s.client.DegraphqlRoutes.Create(ctx, °raphqlRoute.DegraphqlRoute) + if err != nil { + return nil, err + } + return &state.DegraphqlRoute{DegraphqlRoute: *createdDegraphqlRoute}, nil +} + +// Delete deletes a DegraphqlRoute in Kong. +// The arg should be of type crud.Event, containing the degraphql route to be deleted, +// else the function will panic. +// It returns a the deleted *state.DegraphqlRoute. +func (s *degraphqlRouteCRUD) Delete(ctx context.Context, arg ...crud.Arg) (crud.Arg, error) { + event := crud.EventFromArg(arg[0]) + degraphqlRoute := degraphqlRouteFromStruct(event) + err := s.client.DegraphqlRoutes.Delete(ctx, degraphqlRoute.Service.ID, degraphqlRoute.ID) + if err != nil { + return nil, err + } + return degraphqlRoute, nil +} + +// Update updates a DegraphqlRoute in Kong. +// The arg should be of type crud.Event, containing the degraphql route to be updated, +// else the function will panic. +// It returns a the updated *state.DegraphqlRoute. +func (s *degraphqlRouteCRUD) Update(ctx context.Context, arg ...crud.Arg) (crud.Arg, error) { + event := crud.EventFromArg(arg[0]) + degraphqlRoute := degraphqlRouteFromStruct(event) + + updatedDegraphqlRoute, err := s.client.DegraphqlRoutes.Update(ctx, °raphqlRoute.DegraphqlRoute) + if err != nil { + return nil, err + } + return &state.DegraphqlRoute{DegraphqlRoute: *updatedDegraphqlRoute}, nil +} + +type degraphqlRouteDiffer struct { + kind crud.Kind + + currentState, targetState *state.KongState +} + +func (d *degraphqlRouteDiffer) Deletes(handler func(crud.Event) error) error { + currentDegraphqlRoutes, err := d.currentState.DegraphqlRoutes.GetAll() + if err != nil { + return fmt.Errorf("error fetching degraphql routes from state: %w", err) + } + + for _, degraphqlRoute := range currentDegraphqlRoutes { + n, err := d.deleteDegraphqlRoute(degraphqlRoute) + if err != nil { + return err + } + if n != nil { + err = handler(*n) + if err != nil { + return err + } + } + } + return nil +} + +func (d *degraphqlRouteDiffer) deleteDegraphqlRoute(degraphqlRoute *state.DegraphqlRoute) (*crud.Event, error) { + _, err := d.targetState.DegraphqlRoutes.Get(*degraphqlRoute.ID) + if errors.Is(err, state.ErrNotFound) { + return &crud.Event{ + Op: crud.Delete, + Kind: d.kind, + Obj: degraphqlRoute, + }, nil + } + if err != nil { + return nil, fmt.Errorf("looking up degraphql route %q: %w", *degraphqlRoute.ID, err) + } + return nil, nil +} + +func (d *degraphqlRouteDiffer) CreateAndUpdates(handler func(crud.Event) error) error { + targetDegraphqlRoutes, err := d.targetState.DegraphqlRoutes.GetAll() + if err != nil { + return fmt.Errorf("error fetching degraphql routes from state: %w", err) + } + + for _, degraphqlRoute := range targetDegraphqlRoutes { + n, err := d.createUpdateDegraphqlRoute(degraphqlRoute) + if err != nil { + return err + } + if n != nil { + err = handler(*n) + if err != nil { + return err + } + } + } + return nil +} + +func (d *degraphqlRouteDiffer) createUpdateDegraphqlRoute(degraphqlRoute *state.DegraphqlRoute) (*crud.Event, error) { + degraphqlRoute = &state.DegraphqlRoute{DegraphqlRoute: *degraphqlRoute.DeepCopy()} + + currentDegraphqlRoute, err := d.currentState.DegraphqlRoutes.Get(*degraphqlRoute.ID) + + if errors.Is(err, state.ErrNotFound) { + // degraphql route not present, create it + return &crud.Event{ + Op: crud.Create, + Kind: d.kind, + Obj: degraphqlRoute, + }, nil + } + if err != nil { + return nil, fmt.Errorf("error looking up degraphql route %q: %w", + *degraphqlRoute.ID, err) + } + + // found, check if update needed + if !currentDegraphqlRoute.EqualWithOpts(degraphqlRoute, false) { + return &crud.Event{ + Op: crud.Update, + Kind: d.kind, + Obj: degraphqlRoute, + OldObj: currentDegraphqlRoute, + }, nil + } + return nil, nil +} diff --git a/pkg/types/postProcess.go b/pkg/types/postProcess.go index 9df0ddf..2126c2d 100644 --- a/pkg/types/postProcess.go +++ b/pkg/types/postProcess.go @@ -512,3 +512,19 @@ func (crud *filterChainPostAction) Delete(_ context.Context, args ...crud.Arg) ( func (crud *filterChainPostAction) Update(_ context.Context, args ...crud.Arg) (crud.Arg, error) { return nil, crud.currentState.FilterChains.Update(*args[0].(*state.FilterChain)) } + +type degraphqlRoutePostAction struct { + currentState *state.KongState +} + +func (crud *degraphqlRoutePostAction) Create(_ context.Context, args ...crud.Arg) (crud.Arg, error) { + return nil, crud.currentState.DegraphqlRoutes.Add(*args[0].(*state.DegraphqlRoute)) +} + +func (crud *degraphqlRoutePostAction) Delete(_ context.Context, args ...crud.Arg) (crud.Arg, error) { + return nil, crud.currentState.DegraphqlRoutes.Delete(*((args[0].(*state.DegraphqlRoute)).ID)) +} + +func (crud *degraphqlRoutePostAction) Update(_ context.Context, args ...crud.Arg) (crud.Arg, error) { + return nil, crud.currentState.DegraphqlRoutes.Update(*args[0].(*state.DegraphqlRoute)) +} diff --git a/pkg/utils/types.go b/pkg/utils/types.go index defa942..64382b7 100644 --- a/pkg/utils/types.go +++ b/pkg/utils/types.go @@ -54,6 +54,8 @@ type KongRawState struct { Oauth2Creds []*kong.Oauth2Credential MTLSAuths []*kong.MTLSAuth + DegraphqlRoutes []*kong.DegraphqlRoute + RBACRoles []*kong.RBACRole RBACEndpointPermissions []*kong.RBACEndpointPermission }