├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE.txt ├── Makefile ├── README.md ├── event.go ├── go.mod ├── go.sum ├── instance.go ├── main.go ├── pluginserver.go ├── steptypes.go └── test.sh /.gitignore: -------------------------------------------------------------------------------- 1 | go-pluginserver 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | dist: xenial 2 | 3 | language: go 4 | 5 | go: 6 | - 1.13.x 7 | 8 | addons: 9 | apt: 10 | packages: 11 | - nmap # for ncat 12 | 13 | git: 14 | depth: 1 15 | 16 | before_script: 17 | - curl -Lo rq.tar.gz https://github.com/dflemstr/rq/releases/download/v1.0.2/rq-v1.0.2-x86_64-unknown-linux-gnu.tar.gz 18 | - tar zxvpf rq.tar.gz 19 | - go get -v -u golang.org/x/lint/golint 20 | - ( cd ~/gopath/src; mkdir -p github.com/Kong; cd github.com/Kong; git clone https://github.com/kong/go-pdk && cd go-pdk && go install ) 21 | - ( cd ~/gopath/src/github.com/Kong; git clone https://github.com/kong/go-plugins && cd go-plugins && git checkout v0.5.0 && make && cp *.so ../go-pluginserver ) 22 | 23 | script: 24 | - golint ./... 25 | - go get 26 | - go build 27 | - PATH=.:$PATH ./test.sh 28 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Table of Contents 2 | 3 | - [v0.6.0](#v060) 4 | - [v0.5.0](#v050) 5 | - [v0.4.0](#v040) 6 | - [v0.3.3](#v033) 7 | - [v0.3.2](#v032) 8 | 9 | ## [v0.6.0] 10 | 11 | ### Additions 12 | 13 | - New CLI flag `-dump-all-plugins` to dump info from all managed 14 | plugins. 15 | [#38](https://github.com/Kong/go-pluginserver/pull/38) 16 | 17 | ## [v0.5.0] 18 | 19 | > Released 2020/06/22 20 | 21 | ### Additions 22 | 23 | - Add support for a `Response` phase, allowing manipulation of 24 | response data. 25 | [#32](https://github.com/Kong/go-pluginserver/pull/32) 26 | 27 | ### Fixes 28 | 29 | - Fix map representation in plugin configuration. 30 | [#19](https://github.com/Kong/go-pluginserver/pull/19) 31 | Thanks, [melchor629](https://github.com/melchor629) for the patch! 32 | - Fix array representation in plugin configuration. 33 | [#20](https://github.com/Kong/go-pluginserver/pull/20) 34 | Thanks, [longquan0104](https://github.com/longquan0104) for the patch! 35 | 36 | ## [v0.4.0] 37 | 38 | > Released 2020/05/27 39 | 40 | - Bump go-pdk to v0.5.0 41 | 42 | ## [v0.3.3] 43 | 44 | > Released 2020/05/25 45 | 46 | - Bump go-pdk to v0.4.0 47 | 48 | ## [v0.3.2] 49 | 50 | > Released 2020/05/11 51 | 52 | ### Fixes 53 | 54 | - Properly handle array fields. 55 | [#18](https://github.com/Kong/go-pluginserver/pull/18). 56 | Thanks, [jumal](https://github.com/jumal). 57 | 58 | [Back to TOC](#table-of-contents) 59 | 60 | [v0.6.0]: https://github.com/Kong/go-pluginserver/compare/v0.5.0..v0.6.0 61 | [v0.5.0]: https://github.com/Kong/go-pluginserver/compare/v0.4.0..v0.5.0 62 | [v0.4.0]: https://github.com/Kong/go-pluginserver/compare/v0.3.3..v0.4.0 63 | [v0.3.3]: https://github.com/Kong/go-pluginserver/compare/v0.3.2..v0.3.3 64 | [v0.3.2]: https://github.com/Kong/go-pluginserver/compare/v0.3.1..v0.3.2 65 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | BIN_NAME=go-pluginserver 2 | VERSION?=development 3 | 4 | all: build 5 | 6 | build: 7 | go build -ldflags="-s -w -X main.version=$(VERSION)" -o $(BIN_NAME) -v 8 | 9 | clean: 10 | rm -rf $(BIN_NAME) 11 | 12 | .PHONY: all build clean 13 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | go-pluginserver 2 | === 3 | 4 | DEPRECATED 5 | --- 6 | 7 | **NOTE**: the external go-pluginserver is deprecated, and no longer maintained. 8 | For Kong 2.8.0, while existing usage of the old style will still be supported, 9 | users are encouraged to upgrade to the new embedded server style. 10 | And it will be no longer supported since Kong 3.0.0. Check out [the Docs](https://docs.konghq.com/gateway/latest/reference/external-plugins/#updating-from-legacy-to-embedded-server-style) for upgrade steps. 11 | 12 | Pluginserver 13 | -- 14 | 15 | Runs Kong plugins written in Go. Implemented as a standalone MessagePack-RPC server. 16 | 17 | There's no explicit state associated with the client connections, so the same plugins, 18 | instances and even events could be shared with several clients, or a single client can 19 | use more than one connection from a pool. For the same reason, plugin instances and 20 | events could survive client disconnections, if the client reconnects when necessary. 21 | 22 | Holds the running server status. Starts and stops with the server process. 23 | 24 | RPC Methods: 25 | 26 | - `plugin.SetPluginDir()` 27 | - `plugin.GetPluginInfo()` 28 | 29 | Plugin Instance 30 | -- 31 | 32 | Holds a plugin config object, directly related to each configuration instance. The config object 33 | is defined by the plugin code, and exported fields (name starts with a caplital letter) are filled 34 | with configuration data and described in the schema. Any private field is ignored but preserved 35 | between events. 36 | 37 | RPC Methods: 38 | 39 | - `plugin.StartInstance()` 40 | - `plugin.InstanceStatus()` 41 | - `plugin.CloseInstance()` 42 | 43 | The `StartInstance()` method receives configuration data in a serialized format (currently JSON) 44 | in a binary string. If the configuration is modified externally, a new instance should be started 45 | and the old one closed. 46 | 47 | Event 48 | -- 49 | 50 | Handles a Kong event. The event instance lives during the whole callback/response cyle. 51 | Several events can share a single plugin instance concurrently, exercise care if mutating 52 | shared data. 53 | 54 | RPC Methods: 55 | 56 | - `plugin.HandleEvent()` 57 | - `plugin.Step()` 58 | - `plugin.StepError()` 59 | - `plugin.StepCredential()` 60 | - `plugin.StepRoute()` 61 | - `plugin.StepService()` 62 | - `plugin.StepConsumer()` 63 | - `plugin.StepMemoryStats()` 64 | 65 | To start an event, call `plugin.HandleEvent()` with an instance id and event name. The return data 66 | will include the event ID and either a `"ret"` string or callback request and parameters. If the 67 | callback response is a primitive type (number, string, simple dictionary) return it via the 68 | `plugin.Step()` method, including the event ID. To return an error, use `plugin.StepError()`. 69 | For other specific complex types, use the corresponding `plugin.StepXXX()` method. 70 | -------------------------------------------------------------------------------- /event.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "github.com/Kong/go-pdk" 6 | "time" 7 | ) 8 | 9 | // Incoming data for a new event. 10 | // TODO: add some relevant data to reduce number of callbacks. 11 | type StartEventData struct { 12 | InstanceId int // Instance ID to start the event 13 | EventName string // event name (not handler method name) 14 | // .... 15 | } 16 | 17 | type eventData struct { 18 | id int // event id 19 | instance *instanceData // plugin instance 20 | ipc chan interface{} // communication channel (TODO: use decoded structs) 21 | pdk *pdk.PDK // go-pdk instance 22 | } 23 | 24 | // HandleEvent starts the call/{callback/response}*/finish cycle. 25 | // More than one event can be run concurrenty for a single plugin instance, 26 | // they all receive the same object instance, so should be careful if it's 27 | // mutated or holds references to mutable data. 28 | // 29 | // RPC exported method 30 | func (s *PluginServer) HandleEvent(in StartEventData, out *StepData) error { 31 | s.lock.RLock() 32 | instance, ok := s.instances[in.InstanceId] 33 | s.lock.RUnlock() 34 | if !ok { 35 | return fmt.Errorf("No plugin instance %d", in.InstanceId) 36 | } 37 | 38 | h, ok := instance.handlers[in.EventName] 39 | if !ok { 40 | return fmt.Errorf("undefined method %s on plugin %s", 41 | in.EventName, instance.plugin.name) 42 | } 43 | 44 | ipc := make(chan interface{}) 45 | 46 | event := eventData{ 47 | instance: instance, 48 | ipc: ipc, 49 | pdk: pdk.Init(ipc), 50 | } 51 | 52 | s.lock.Lock() 53 | event.id = s.nextEventId 54 | s.nextEventId++ 55 | s.events[event.id] = &event 56 | s.lock.Unlock() 57 | 58 | //log.Printf("Will launch goroutine for key %d / operation %s\n", key, op) 59 | go func() { 60 | _ = <-ipc 61 | h(event.pdk) 62 | 63 | func() { 64 | defer func() { recover() }() 65 | ipc <- "ret" 66 | }() 67 | 68 | s.lock.Lock() 69 | defer s.lock.Unlock() 70 | event.instance.lastEvent = time.Now() 71 | delete(s.events, event.id) 72 | }() 73 | 74 | ipc <- "run" // kickstart the handler 75 | 76 | *out = StepData{EventId: event.id, Data: <-ipc} 77 | return nil 78 | } 79 | 80 | // A callback's response/request. 81 | type StepData struct { 82 | EventId int // event cycle to which this belongs 83 | Data interface{} // carried data 84 | } 85 | 86 | // Step carries a callback's anser back from Kong to the plugin, 87 | // the return value is either a new callback request or a finish signal. 88 | // 89 | // RPC exported method 90 | func (s *PluginServer) Step(in StepData, out *StepData) error { 91 | s.lock.RLock() 92 | event, ok := s.events[in.EventId] 93 | s.lock.RUnlock() 94 | if !ok { 95 | return fmt.Errorf("No running event %d", in.EventId) 96 | } 97 | 98 | event.ipc <- in.Data 99 | outStr := <-event.ipc 100 | *out = StepData{EventId: in.EventId, Data: outStr} 101 | 102 | return nil 103 | } 104 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/Kong/go-pluginserver 2 | 3 | go 1.13 4 | 5 | require ( 6 | github.com/Kong/go-pdk v0.6.1 7 | github.com/ugorji/go/codec v1.2.1 8 | ) 9 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/Kong/go-pdk v0.6.0 h1:KZZKGYK5NbTIMQa5P1IRldMvA5/LJEoHhrsOQlVCPHo= 2 | github.com/Kong/go-pdk v0.6.0/go.mod h1:SrAne3YN4a7pNrlJ6Fb6xwa6kOhV2vDY/mqpb3X0fs4= 3 | github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= 4 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 5 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 6 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 7 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 8 | github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= 9 | github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= 10 | github.com/ugorji/go v1.2.1 h1:dz+JxTe7GZQdErTo7SREc1jQj/hFP1k7jyIAwODoW+k= 11 | github.com/ugorji/go v1.2.1/go.mod h1:cSVypSfTLm2o9fKxXvQgn3rMmkPXovcWor6Qn5tbFmI= 12 | github.com/ugorji/go/codec v1.2.1 h1:/TRfW3XKkvWvmAYyCUaQlhoCDGjcvNR8xVVA/l5p/jQ= 13 | github.com/ugorji/go/codec v1.2.1/go.mod h1:s/WxCRi46t8rA+fowL40EnmD7ec0XhR7ZypxeBNdzsM= 14 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= 15 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 16 | gopkg.in/yaml.v2 v2.2.2 h1:ZCJp+EgiOT7lHqUV2J862kp8Qj64Jo6az82+3Td9dZw= 17 | gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= 18 | -------------------------------------------------------------------------------- /instance.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "github.com/Kong/go-pdk" 7 | "log" 8 | "time" 9 | ) 10 | 11 | // --- instanceData --- // 12 | type instanceData struct { 13 | id int 14 | plugin *pluginData 15 | startTime time.Time 16 | initialized bool 17 | config interface{} 18 | handlers map[string]func(kong *pdk.PDK) 19 | lastEvent time.Time 20 | } 21 | 22 | type ( 23 | certificater interface{ Certificate(*pdk.PDK) } 24 | rewriter interface{ Rewrite(*pdk.PDK) } 25 | accesser interface{ Access(*pdk.PDK) } 26 | responser interface{ Response(*pdk.PDK) } 27 | prereader interface{ Preread(*pdk.PDK) } 28 | logger interface{ Log(*pdk.PDK) } 29 | ) 30 | 31 | func getHandlers(config interface{}) map[string]func(kong *pdk.PDK) { 32 | handlers := map[string]func(kong *pdk.PDK){} 33 | 34 | if h, ok := config.(certificater); ok { handlers["certificate"] = h.Certificate } 35 | if h, ok := config.(rewriter) ; ok { handlers["rewrite"] = h.Rewrite } 36 | if h, ok := config.(accesser) ; ok { handlers["access"] = h.Access } 37 | if h, ok := config.(responser) ; ok { handlers["response"] = h.Response } 38 | if h, ok := config.(prereader) ; ok { handlers["preread"] = h.Preread } 39 | if h, ok := config.(logger) ; ok { handlers["log"] = h.Log } 40 | 41 | return handlers 42 | } 43 | 44 | func (s *PluginServer) expireInstances() error { 45 | const instanceTimeout = 60 46 | expirationCutoff := time.Now().Add(time.Second * -instanceTimeout) 47 | 48 | oldinstances := map[int]bool{} 49 | for id, inst := range s.instances { 50 | if inst.startTime.Before(expirationCutoff) && inst.lastEvent.Before(expirationCutoff) { 51 | oldinstances[id] = true 52 | } 53 | } 54 | 55 | for _, evt := range s.events { 56 | instId := evt.instance.id 57 | if _, ok := oldinstances[instId]; ok { 58 | delete(oldinstances, instId) 59 | } 60 | } 61 | 62 | for id := range oldinstances { 63 | inst := s.instances[id] 64 | log.Printf("closing instance %#v:%v", inst.plugin.name, inst.id) 65 | delete(s.instances, id) 66 | } 67 | 68 | return nil 69 | } 70 | 71 | // Configuration data for a new plugin instance. 72 | type PluginConfig struct { 73 | Name string // plugin name 74 | Config []byte // configuration data, as a JSON string 75 | } 76 | 77 | // Current state of a plugin instance. TODO: add some statistics 78 | type InstanceStatus struct { 79 | Name string // plugin name 80 | Id int // instance id 81 | Config interface{} // configuration data, decoded 82 | StartTime int64 83 | } 84 | 85 | // StartInstance starts a plugin instance, as requred by configuration data. More than 86 | // one instance can be started for a single plugin. If the configuration changes, 87 | // a new instance should be started and the old one closed. 88 | // 89 | // RPC exported method 90 | func (s *PluginServer) StartInstance(config PluginConfig, status *InstanceStatus) error { 91 | plug, err := s.loadPlugin(config.Name) 92 | if err != nil { 93 | return err 94 | } 95 | 96 | plug.lock.Lock() 97 | defer plug.lock.Unlock() 98 | 99 | instanceConfig := plug.constructor() 100 | 101 | if err := json.Unmarshal(config.Config, instanceConfig); err != nil { 102 | return fmt.Errorf("Decoding config: %w", err) 103 | } 104 | 105 | instance := instanceData{ 106 | plugin: plug, 107 | startTime: time.Now(), 108 | config: instanceConfig, 109 | handlers: getHandlers(instanceConfig), 110 | } 111 | 112 | s.lock.Lock() 113 | instance.id = s.nextInstanceId 114 | s.nextInstanceId++ 115 | s.instances[instance.id] = &instance 116 | 117 | plug.lastStartInstance = instance.startTime 118 | 119 | s.lock.Unlock() 120 | 121 | *status = InstanceStatus{ 122 | Name: config.Name, 123 | Id: instance.id, 124 | Config: instance.config, 125 | StartTime: instance.startTime.Unix(), 126 | } 127 | 128 | log.Printf("Started instance %#v:%v", config.Name, instance.id) 129 | 130 | return nil 131 | } 132 | 133 | // InstanceStatus returns a given resource's status (the same given when started) 134 | // 135 | // RPC exported method 136 | func (s *PluginServer) InstanceStatus(id int, status *InstanceStatus) error { 137 | s.lock.RLock() 138 | instance, ok := s.instances[id] 139 | s.lock.RUnlock() 140 | if !ok { 141 | return fmt.Errorf("No plugin instance %d", id) 142 | } 143 | 144 | *status = InstanceStatus{ 145 | Name: instance.plugin.name, 146 | Id: instance.id, 147 | Config: instance.config, 148 | } 149 | 150 | return nil 151 | } 152 | 153 | // CloseInstance is used when an instance shouldn't be used anymore. 154 | // Doesn't kill any running event but the instance is no longer accesible, 155 | // so it's not possible to start a new event with it and will be garbage 156 | // collected after the last reference event finishes. 157 | // Returns the status just before closing. 158 | // 159 | // RPC exported method 160 | func (s *PluginServer) CloseInstance(id int, status *InstanceStatus) error { 161 | s.lock.RLock() 162 | instance, ok := s.instances[id] 163 | s.lock.RUnlock() 164 | if !ok { 165 | return fmt.Errorf("No plugin instance %d", id) 166 | } 167 | 168 | *status = InstanceStatus{ 169 | Name: instance.plugin.name, 170 | Id: instance.id, 171 | Config: instance.config, 172 | } 173 | 174 | // kill? 175 | 176 | log.Printf("closed instance %#v:%v", instance.plugin.name, instance.id) 177 | 178 | s.lock.Lock() 179 | instance.plugin.lastCloseInstance = time.Now() 180 | delete(s.instances, id) 181 | s.expireInstances() 182 | s.lock.Unlock() 183 | 184 | return nil 185 | } 186 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | // go-pluginserver is a standalone RPC server that runs 2 | // Go plugins for Kong. 3 | package main 4 | 5 | import ( 6 | "flag" 7 | "fmt" 8 | "github.com/ugorji/go/codec" 9 | "log" 10 | "net" 11 | "net/rpc" 12 | "os" 13 | "path" 14 | "path/filepath" 15 | "reflect" 16 | "runtime" 17 | "strings" 18 | "time" 19 | ) 20 | 21 | var version = "development" 22 | 23 | /* flags */ 24 | var ( 25 | kongPrefix = flag.String("kong-prefix", "/usr/local/kong", "Kong prefix path (specified by the -p argument commonly used in the kong cli)") 26 | dump = flag.String("dump-plugin-info", "", "Dump info about `plugin` as a MessagePack object") 27 | dumpAllPlugins = flag.Bool("dump-all-plugins", false, "Dump info about all available plugins") 28 | pluginsDir = flag.String("plugins-directory", "", "Set directory `path` where to search plugins") 29 | showVersion = flag.Bool("version", false, "Print binary and runtime version") 30 | ) 31 | 32 | var socket string 33 | 34 | func init() { 35 | flag.Parse() 36 | socket = *kongPrefix + "/" + "go_pluginserver.sock" 37 | 38 | if *kongPrefix == "" && *dump == "" { 39 | flag.Usage() 40 | os.Exit(2) 41 | } 42 | } 43 | 44 | func printVersion() { 45 | fmt.Printf("Version: %s\nRuntime Version: %s\n", version, runtime.Version()) 46 | } 47 | 48 | func dumpInfo() { 49 | s := newServer() 50 | 51 | info := PluginInfo{} 52 | err := s.GetPluginInfo(*dump, &info) 53 | if err != nil { 54 | log.Printf("%s", err) 55 | } 56 | 57 | var handle codec.MsgpackHandle 58 | handle.ReaderBufferSize = 4096 59 | handle.WriterBufferSize = 4096 60 | handle.RawToString = true 61 | handle.MapType = reflect.TypeOf(map[string]interface{}(nil)) 62 | 63 | enc := codec.NewEncoder(os.Stdout, &handle) 64 | _ = enc.Encode(info) 65 | } 66 | 67 | func dumpAll() { 68 | s := newServer() 69 | 70 | pluginPaths, err := filepath.Glob(path.Join(s.pluginsDir, "/*.so")) 71 | if err != nil { 72 | log.Printf("can't get plugin names from %s: %s", s.pluginsDir, err) 73 | return 74 | } 75 | 76 | infos := make([]PluginInfo, len(pluginPaths)) 77 | 78 | for i, pluginPath := range pluginPaths { 79 | pluginName := strings.TrimSuffix(path.Base(pluginPath), ".so") 80 | 81 | err = s.GetPluginInfo(pluginName, &infos[i]) 82 | if err != nil { 83 | log.Printf("can't load Plugin %s: %s", pluginName, err) 84 | continue 85 | } 86 | } 87 | 88 | var handle codec.JsonHandle 89 | enc := codec.NewEncoder(os.Stdout, &handle) 90 | _ = enc.Encode(infos) 91 | } 92 | 93 | func runServer(listener net.Listener) { 94 | var handle codec.MsgpackHandle 95 | handle.ReaderBufferSize = 4096 96 | handle.WriterBufferSize = 4096 97 | handle.RawToString = true 98 | handle.MapType = reflect.TypeOf(map[string]interface{}(nil)) 99 | 100 | for { 101 | conn, err := listener.Accept() 102 | if err != nil { 103 | return 104 | } 105 | 106 | enc := codec.NewEncoder(conn, &handle) 107 | _ = enc.Encode([]interface{}{2, "serverPid", os.Getpid()}) 108 | 109 | rpcCodec := codec.MsgpackSpecRpc.ServerCodec(conn, &handle) 110 | go rpc.ServeCodec(rpcCodec) 111 | } 112 | } 113 | 114 | func startServer() { 115 | err := os.Remove(socket) 116 | if err != nil && !os.IsNotExist(err) { 117 | log.Printf(`removing "%s": %s`, kongPrefix, err) 118 | return 119 | } 120 | 121 | listener, err := net.Listen("unix", socket) 122 | if err != nil { 123 | log.Printf(`listen("%s"): %s`, socket, err) 124 | return 125 | } 126 | 127 | rpc.RegisterName("plugin", newServer()) 128 | runServer(listener) 129 | } 130 | 131 | func isParentAlive() bool { 132 | return os.Getppid() != 1 // assume ppid 1 means process was adopted by init 133 | } 134 | 135 | func main() { 136 | if *showVersion == true { 137 | printVersion() 138 | os.Exit(0) 139 | } 140 | 141 | if *dump != "" { 142 | dumpInfo() 143 | os.Exit(0) 144 | } 145 | 146 | if *dumpAllPlugins { 147 | dumpAll() 148 | os.Exit(0) 149 | } 150 | 151 | if socket != "" { 152 | go func() { 153 | for { 154 | if !isParentAlive() { 155 | log.Printf("Kong exited; shutting down...") 156 | os.Exit(0) 157 | } 158 | 159 | time.Sleep(1 * time.Second) 160 | } 161 | }() 162 | 163 | startServer() 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /pluginserver.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "path" 7 | "plugin" 8 | "reflect" 9 | "strings" 10 | "sync" 11 | "time" 12 | ) 13 | 14 | // --- PluginServer --- // 15 | 16 | // Holds the execution status of the plugin server. 17 | type PluginServer struct { 18 | lock sync.RWMutex 19 | pluginsDir string 20 | plugins map[string]*pluginData 21 | instances map[int]*instanceData 22 | events map[int]*eventData 23 | nextInstanceId int 24 | nextEventId int 25 | } 26 | 27 | // Create a new server context. 28 | func newServer() *PluginServer { 29 | s := PluginServer{ 30 | plugins: map[string]*pluginData{}, 31 | instances: map[int]*instanceData{}, 32 | events: map[int]*eventData{}, 33 | } 34 | 35 | if *pluginsDir == "" { 36 | if dir, err := os.Getwd(); err == nil { 37 | s.pluginsDir = dir 38 | } 39 | } else { 40 | s.pluginsDir = *pluginsDir 41 | } 42 | 43 | return &s 44 | } 45 | 46 | // SetPluginDir tells the server where to find the plugins. 47 | // 48 | // RPC exported method 49 | func (s *PluginServer) SetPluginDir(dir string, reply *string) error { 50 | s.lock.Lock() 51 | s.pluginsDir = dir 52 | s.lock.Unlock() 53 | *reply = "ok" 54 | return nil 55 | } 56 | 57 | // --- status --- // 58 | 59 | type ServerStatusData struct { 60 | Pid int 61 | Plugins map[string]PluginStatusData 62 | } 63 | 64 | func (s *PluginServer) GetStatus(n int, reply *ServerStatusData) error { 65 | s.lock.Lock() 66 | defer s.lock.Unlock() 67 | 68 | *reply = ServerStatusData{ 69 | Pid: os.Getpid(), 70 | Plugins: make(map[string]PluginStatusData), 71 | } 72 | 73 | var err error 74 | for pluginname := range s.plugins { 75 | reply.Plugins[pluginname], err = s.getPluginStatus(pluginname) 76 | if err != nil { 77 | return err 78 | } 79 | } 80 | 81 | return nil 82 | } 83 | 84 | // --- pluginData --- // 85 | 86 | type pluginData struct { 87 | lock sync.Mutex 88 | name string 89 | code *plugin.Plugin 90 | modtime time.Time 91 | loadtime time.Time 92 | constructor func() interface{} 93 | config interface{} 94 | lastStartInstance time.Time 95 | lastCloseInstance time.Time 96 | } 97 | 98 | func getModTime(fname string) (modtime time.Time, err error) { 99 | finfo, err := os.Stat(fname) 100 | if err != nil { 101 | return 102 | } 103 | 104 | modtime = finfo.ModTime() 105 | return 106 | } 107 | 108 | func (s *PluginServer) loadPlugin(name string) (plug *pluginData, err error) { 109 | s.lock.Lock() 110 | defer s.lock.Unlock() 111 | 112 | plug, ok := s.plugins[name] 113 | if ok { 114 | return 115 | } 116 | 117 | plugFName := path.Join(s.pluginsDir, name+".so") 118 | plugModTime, err := getModTime(plugFName) 119 | if err != nil { 120 | return 121 | } 122 | 123 | code, err := plugin.Open(plugFName) 124 | if err != nil { 125 | err = fmt.Errorf("failed to open plugin %s: %w", name, err) 126 | return 127 | } 128 | 129 | constructorSymbol, err := code.Lookup("New") 130 | if err != nil { 131 | err = fmt.Errorf("No constructor function on plugin %s: %w", name, err) 132 | return 133 | } 134 | 135 | constructor, ok := constructorSymbol.(func() interface{}) 136 | if !ok { 137 | err = fmt.Errorf("Wrong constructor signature on plugin %s: %w", name, err) 138 | return 139 | } 140 | 141 | plug = &pluginData{ 142 | name: name, 143 | code: code, 144 | modtime: plugModTime, 145 | loadtime: time.Now(), 146 | constructor: constructor, 147 | config: constructor(), 148 | } 149 | 150 | s.plugins[name] = plug 151 | 152 | return 153 | } 154 | 155 | type schemaDict map[string]interface{} 156 | 157 | func getSchemaDict(t reflect.Type) schemaDict { 158 | switch t.Kind() { 159 | case reflect.String: 160 | return schemaDict{"type": "string"} 161 | 162 | case reflect.Bool: 163 | return schemaDict{"type": "boolean"} 164 | 165 | case reflect.Int, reflect.Int32: 166 | return schemaDict{"type": "integer"} 167 | 168 | case reflect.Uint, reflect.Uint32: 169 | return schemaDict{ 170 | "type": "integer", 171 | "between": []int{0, 2147483648}, 172 | } 173 | 174 | case reflect.Float32, reflect.Float64: 175 | return schemaDict{"type": "number"} 176 | 177 | case reflect.Slice: 178 | elemType := getSchemaDict(t.Elem()) 179 | if elemType == nil { 180 | break 181 | } 182 | return schemaDict{ 183 | "type": "array", 184 | "elements": elemType, 185 | } 186 | 187 | case reflect.Map: 188 | kType := getSchemaDict(t.Key()) 189 | vType := getSchemaDict(t.Elem()) 190 | if kType == nil || vType == nil { 191 | break 192 | } 193 | return schemaDict{ 194 | "type": "map", 195 | "keys": kType, 196 | "values": vType, 197 | } 198 | 199 | case reflect.Struct: 200 | fieldsArray := []schemaDict{} 201 | for i := 0; i < t.NumField(); i++ { 202 | field := t.Field(i) 203 | typeDecl := getSchemaDict(field.Type) 204 | if typeDecl == nil { 205 | // ignore unrepresentable types 206 | continue 207 | } 208 | name := field.Tag.Get("json") 209 | if name == "" { 210 | name = strings.ToLower(field.Name) 211 | } 212 | fieldsArray = append(fieldsArray, schemaDict{name: typeDecl}) 213 | } 214 | return schemaDict{ 215 | "type": "record", 216 | "fields": fieldsArray, 217 | } 218 | } 219 | 220 | return nil 221 | } 222 | 223 | // Information obtained from a plugin's compiled code. 224 | type PluginInfo struct { 225 | Name string // plugin name 226 | ModTime time.Time `codec:",omitempty"` // plugin file modification time 227 | LoadTime time.Time `codec:",omitempty"` // plugin load time 228 | Phases []string // events it can handle 229 | Version string // version number 230 | Priority int // priority info 231 | Schema schemaDict // JSON representation of the config schema 232 | } 233 | 234 | // GetPluginInfo loads and retrieves information from the compiled plugin. 235 | // TODO: reload if the plugin code has been updated. 236 | // 237 | // RPC exported method 238 | func (s *PluginServer) GetPluginInfo(name string, info *PluginInfo) error { 239 | plug, err := s.loadPlugin(name) 240 | if err != nil { 241 | return err 242 | } 243 | 244 | *info = PluginInfo{Name: name} 245 | 246 | plug.lock.Lock() 247 | defer plug.lock.Unlock() 248 | handlers := getHandlers(plug.config) 249 | 250 | info.Phases = make([]string, len(handlers)) 251 | var i = 0 252 | for name := range handlers { 253 | info.Phases[i] = name 254 | i++ 255 | } 256 | 257 | v, _ := plug.code.Lookup("Version") 258 | if v != nil { 259 | info.Version = *v.(*string) 260 | } 261 | 262 | prio, _ := plug.code.Lookup("Priority") 263 | if prio != nil { 264 | info.Priority = *prio.(*int) 265 | } 266 | 267 | // st, _ := getSchemaDict(reflect.TypeOf(plug.config).Elem()) 268 | info.Schema = schemaDict{ 269 | "name": name, 270 | "fields": []schemaDict{ 271 | schemaDict{"config": getSchemaDict(reflect.TypeOf(plug.config).Elem())}, 272 | }, 273 | } 274 | 275 | return nil 276 | } 277 | 278 | type PluginStatusData struct { 279 | Name string 280 | Modtime int64 281 | LoadTime int64 282 | Instances []InstanceStatus 283 | LastStartInstance int64 284 | LastCloseInstance int64 285 | } 286 | 287 | func (s *PluginServer) getPluginStatus(name string) (status PluginStatusData, err error) { 288 | plug, ok := s.plugins[name] 289 | if !ok { 290 | err = fmt.Errorf("plugin %#v not loaded", name) 291 | return 292 | } 293 | 294 | instances := []InstanceStatus{} 295 | for _, instance := range s.instances { 296 | if instance.plugin == plug { 297 | instances = append(instances, InstanceStatus{ 298 | Name: name, 299 | Id: instance.id, 300 | Config: instance.config, 301 | StartTime: instance.startTime.Unix(), 302 | }) 303 | } 304 | } 305 | 306 | status = PluginStatusData{ 307 | Name: name, 308 | Modtime: plug.modtime.Unix(), 309 | LoadTime: plug.loadtime.Unix(), 310 | Instances: instances, 311 | LastStartInstance: plug.lastStartInstance.Unix(), 312 | LastCloseInstance: plug.lastCloseInstance.Unix(), 313 | } 314 | return 315 | } 316 | -------------------------------------------------------------------------------- /steptypes.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/Kong/go-pdk/client" 5 | "github.com/Kong/go-pdk/entities" 6 | "github.com/Kong/go-pdk/node" 7 | ) 8 | 9 | type Error string 10 | 11 | func (e Error) Error() string { 12 | return string(e) 13 | } 14 | 15 | type StepErrorData struct { 16 | EventId int 17 | Data Error 18 | } 19 | 20 | func (s *PluginServer) StepError(in StepErrorData, out *StepData) error { 21 | return s.Step(StepData{ 22 | EventId: in.EventId, 23 | Data: in.Data, 24 | }, out) 25 | } 26 | 27 | type StepMultiMapData struct { 28 | EventId int 29 | Data map[string][]string 30 | } 31 | 32 | func (s *PluginServer) StepMultiMap(in StepMultiMapData, out *StepData) error { 33 | return s.Step(StepData{ 34 | EventId: in.EventId, 35 | Data: in.Data, 36 | }, out) 37 | } 38 | 39 | type StepCredentialData struct { 40 | EventId int 41 | Data client.AuthenticatedCredential 42 | } 43 | 44 | func (s *PluginServer) StepCredential(in StepCredentialData, out *StepData) error { 45 | return s.Step(StepData{ 46 | EventId: in.EventId, 47 | Data: in.Data, 48 | }, out) 49 | } 50 | 51 | type StepRouteData struct { 52 | EventId int 53 | Data entities.Route 54 | } 55 | 56 | func (s *PluginServer) StepRoute(in StepRouteData, out *StepData) error { 57 | return s.Step(StepData{ 58 | EventId: in.EventId, 59 | Data: in.Data, 60 | }, out) 61 | } 62 | 63 | type StepServiceData struct { 64 | EventId int 65 | Data entities.Service 66 | } 67 | 68 | func (s *PluginServer) StepService(in StepServiceData, out *StepData) error { 69 | return s.Step(StepData{ 70 | EventId: in.EventId, 71 | Data: in.Data, 72 | }, out) 73 | } 74 | 75 | type StepConsumerData struct { 76 | EventId int 77 | Data entities.Consumer 78 | } 79 | 80 | func (s *PluginServer) StepConsumer(in StepConsumerData, out *StepData) error { 81 | return s.Step(StepData{ 82 | EventId: in.EventId, 83 | Data: in.Data, 84 | }, out) 85 | } 86 | 87 | type StepMemoryStatsData struct { 88 | EventId int 89 | Data node.MemoryStats 90 | } 91 | 92 | func (s *PluginServer) StepMemoryStats(in StepMemoryStatsData, out *StepData) error { 93 | return s.Step(StepData{ 94 | EventId: in.EventId, 95 | Data: in.Data, 96 | }, out) 97 | } 98 | -------------------------------------------------------------------------------- /test.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | if [ "$1" == "-v" ]; then 4 | VERBOSE='true' 5 | fi 6 | if [ "$1" == "-vv" ]; then 7 | VERBOSE='very' 8 | fi 9 | 10 | pkill go-pluginserver 11 | rq --help >/dev/null 12 | 13 | echo "pwd: $PWD" 14 | 15 | SOCKET='go_pluginserver.sock' 16 | 17 | if pgrep go-pluginserver -l; then 18 | PREVIOUS_SERVER="yes" 19 | else 20 | echo "starting server..." 21 | [ -S "$SOCKET" ] && rm "$SOCKET" 22 | ./go-pluginserver -kong-prefix . & 23 | pgrep go-pluginserver -l 24 | sleep 0.1s 25 | fi 26 | 27 | msg() { 28 | query="$1" 29 | [ -v VERBOSE ] && rq <<< "$query" 30 | [ "$VERBOSE" == "very" ] && rq <<< "$query" -M | hd 31 | METHOD="$(rq <<< "$query" -- 'at([2])')" 32 | response="$(rq <<< "$query" -M | ncat -U "$SOCKET" | rq -m | jq 'select(.[0]==1)' )" 33 | [ -v VERBOSE ] && rq <<< "$response" 34 | 35 | ERROR="$(jq <<< "$response" '.[2]')" 36 | RESULT="$(jq <<< "$response" '.[3]')" 37 | # echo $RESULT 38 | } 39 | 40 | assert_noerr() { 41 | if [ "$ERROR" != "null" ]; then 42 | echo "query: $query" 43 | echo "response: $response" 44 | echo "$METHOD : $ERROR" > /dev/stderr 45 | exit 1 46 | fi 47 | echo "$METHOD: ok" 48 | } 49 | 50 | assert_fld_match() { 51 | fld="$1" 52 | pattern="$2" 53 | 54 | fld_v="$(query_result '.'$fld'')" 55 | if [[ "$fld_v" =~ "$pattern" ]]; then 56 | echo "==> $fld_v : ok" 57 | else 58 | echo "==> $fld_v : no match '$pattern'" 59 | exit 1 60 | fi 61 | } 62 | 63 | query_result() { 64 | jq <<< "$RESULT" "$1" 65 | } 66 | 67 | msg '[0, 19, "plugin.SetPluginDir", ["'$PWD'"]]' 68 | assert_noerr 69 | 70 | msg '[0, 19, "plugin.GetStatus", []]' 71 | assert_noerr 72 | assert_fld_match 'Plugins' '{}' 73 | 74 | msg '[0, 19, "plugin.GetPluginInfo", ["go-hello-lm"]]' 75 | assert_noerr 76 | 77 | msg '[0, 19, "plugin.StartInstance", [{"Name":"go-hello-lm", "Config":"{\"message\":\"howdy\"}"}]]' 78 | assert_noerr 79 | helloId=$(query_result '."Id"') 80 | echo "helloId: $helloId" 81 | 82 | msg '[0, 19, "plugin.StartInstance", [{"Name":"go-log-lm", "Config":"{\"reopen\":false, \"path\":\"/some/where/else/\"}"}]]' 83 | assert_noerr 84 | logId=$(query_result '."Id"') 85 | echo "logId: $logId" 86 | 87 | msg '[0, 19, "plugin.HandleEvent", [{"InstanceId": '$helloId', "EventName": "access", "Params": [45, 23]}]]' 88 | assert_noerr 89 | helloEventId=$(query_result '."EventId"') 90 | 91 | assert_fld_match 'Data.Method' 'kong.request.get_header' 92 | assert_fld_match 'Data.Args' '"host"' 93 | 94 | msg '[0, 20, "plugin.HandleEvent", [{"InstanceId": '$logId', "EventName": "log", "Params": [45, 23]}]]' 95 | assert_noerr 96 | logEventId=$(query_result '."EventId"') 97 | 98 | assert_noerr 99 | assert_fld_match 'Data.Method' 'kong.log.serialize' 100 | 101 | # msg '[0, 19, "plugin.StepError", [{"EventId": '$helloEventId', "Data": "not in the mood for routes"}]]' 102 | #msg "$(cat <<-EOF 103 | #[ 104 | # 0, 19, "plugin.StepRoute", 105 | # [{ 106 | # "Data": { 107 | # "created_at": 1574445198, 108 | # "https_redirect_status_code": 426, 109 | # "id": "c0ba987b-99e0-4342-a255-61ff47d54fe6", 110 | # "paths": [ "/" ], 111 | # "preserve_host": false, 112 | # "protocols": [ "http", "https" ], 113 | # "regex_priority": 0, 114 | # "service": { 115 | # "id": "a1a72823-4c75-42b3-92e6-79c865175287" 116 | # }, 117 | # "strip_path": true, 118 | # "updated_at": 1574445198 119 | # }, 120 | # "EventId": 0 121 | # }] 122 | #] 123 | #EOF 124 | #)" 125 | #assert_noerr 126 | # 127 | #callBack=$(query_result '."Data"') 128 | # 129 | #assert_fld_match 'Data' '"ret"' 130 | 131 | 132 | msg '[0, 19, "plugin.InstanceStatus", ['$helloId']]' 133 | assert_noerr 134 | 135 | msg "[0, 19, \"plugin.CloseInstance\", [$helloId]]" 136 | assert_noerr 137 | 138 | msg '[0, 19, "plugin.InstanceStatus", ['$logId']]' 139 | assert_noerr 140 | 141 | msg "[0, 19, \"plugin.CloseInstance\", [$logId]]" 142 | assert_noerr 143 | 144 | msg '[0, 19, "plugin.GetStatus", []]' 145 | assert_noerr 146 | assert_fld_match 'Plugins["go-hello-lm"]' '"Name": "go-hello-lm"' 147 | assert_fld_match 'Plugins["go-log-lm"]' '"Name": "go-log-lm"' 148 | 149 | 150 | if [ ! -v PREVIOUS_SERVER ]; then 151 | pkill go-pluginserver 152 | rm "$SOCKET" 153 | fi 154 | 155 | --------------------------------------------------------------------------------