├── .gitignore ├── LICENSE ├── httpaction.go ├── README.md ├── models.go ├── httpreq.go └── eurekaclient.go /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled Object files, Static and Dynamic libs (Shared Objects) 2 | *.o 3 | *.a 4 | *.so 5 | 6 | # Folders 7 | _obj 8 | _test 9 | 10 | # Architecture specific extensions/prefixes 11 | *.[568vq] 12 | [568vq].out 13 | 14 | *.cgo1.go 15 | *.cgo2.c 16 | _cgo_defun.c 17 | _cgo_gotypes.go 18 | _cgo_export.* 19 | 20 | _testmain.go 21 | 22 | *.exe 23 | *.test 24 | *.prof 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 ErikL 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /httpaction.go: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 ErikL 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | package eeureka 25 | 26 | type HttpAction struct { 27 | Method string `yaml:"method"` 28 | Url string `yaml:"url"` 29 | Body string `yaml:"body"` 30 | Template string `yaml:"template"` 31 | Accept string `yaml:"accept"` 32 | ContentType string `yaml:"contentType"` 33 | Title string `yaml:"title"` 34 | StoreCookie string `yaml:"storeCookie"` 35 | } 36 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # eeureka 2 | Simplistic Eureka client for Go Microservices 3 | 4 | ## What is is? 5 | I use this in my personal Microservice projects where I use Go-based microservices in a Spring Cloud context deployed on docker containers. It is not production tested, use at your own peril... 6 | 7 | Internally, it's basically wraps a little HTTP client for the following operations of the [Netflix Eureka REST API](https://github.com/Netflix/eureka/wiki/Eureka-REST-operations) : 8 | 9 | - register appId 10 | - heartbeat appId 11 | - deregister appId 12 | - instances of appId 13 | 14 | The top three is enough to handle the lifecycle of a Microservice while the "instances of appId" can be used to get hostname/port for all running instances of a particular appId. Useful for client-side load-balancing. 15 | 16 | ## Usage 17 | 18 | Import 19 | 20 | import "github.com/eriklupander/eeureka" 21 | 22 | In your code, call the Register method: 23 | 24 | eeureka.Register("myMicroservice", "8080", "8443") 25 | 26 | or 27 | 28 | eeureka.RegisterAt("http://192.168.123.123:8761","myMicroservice", "8080", "8443") 29 | 30 | The register method will try to contact the Eureka server indefinitely. When registration succeeds (HTTP 204), heartbeats (PUTs) will be issued every 30 seconds. When the microservice exits by a Sigterm or OS interrupt signal, the microservice will deregister itself with Eureka before shutting down. 31 | 32 | ### Public functions 33 | - RegisterAt - registers your application at a specific Eureka service URL. 34 | - Register - registers your application at the default Eureka service URL http://192.168.99.100:8761 (e.g. typical local Docker installation) 35 | - GetServiceInstances - Returns all running instances of a given appName 36 | 37 | The register methods automatically handles retries, heartbeats and deregistration. 38 | 39 | ### Configuration 40 | 41 | By default, the Eureka server is assumed to be at http://192.168.99.100:8761 42 | 43 | Otherwise, use the _RegisterAt_ function. 44 | 45 | ### Spring Cloud vs Netflix Eureka 46 | 47 | This lib has only been tested with the Spring Cloud flavour of Eureka. Internally, this lib uses the REST endpoints of Eureka which exists in two versions. With "/v2" or without. This lib is only tested with the non-v2 version. 48 | 49 | # License 50 | MIT License. See LICENSE.md -------------------------------------------------------------------------------- /models.go: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 ErikL 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | package eeureka 25 | 26 | /** 27 | Defines a graph of structs that conforms to a part of the return type of the Eureka "get instances for appId", e.g: 28 | 29 | GET /eureka/v2/apps/appID 30 | 31 | The root is the EurekaServiceResponse which contains a single EurekaApplication, which in its turn contains an array 32 | of EurekaInstance instances. 33 | */ 34 | 35 | // Response for /eureka/apps/{appName} 36 | type EurekaServiceResponse struct { 37 | Application EurekaApplication `json:"application"` 38 | } 39 | 40 | // Response for /eureka/apps 41 | type EurekaApplicationsRootResponse struct { 42 | Resp EurekaApplicationsResponse `json:"applications"` 43 | } 44 | 45 | type EurekaApplicationsResponse struct { 46 | Version string `json:"versions__delta"` 47 | AppsHashcode string `json:"versions__delta"` 48 | Applications []EurekaApplication `json:"application"` 49 | } 50 | 51 | type EurekaApplication struct { 52 | Name string `json:"name"` 53 | Instance []EurekaInstance `json:"instance"` 54 | } 55 | 56 | type EurekaInstance struct { 57 | HostName string `json:"hostName"` 58 | Port EurekaPort `json:"port"` 59 | } 60 | 61 | type EurekaPort struct { 62 | Port int `json:"$"` 63 | } 64 | -------------------------------------------------------------------------------- /httpreq.go: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 ErikL 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | package eeureka 25 | 26 | import ( 27 | "crypto/tls" 28 | "io/ioutil" 29 | "log" 30 | "net/http" 31 | "strings" 32 | ) 33 | 34 | func executeQuery(httpAction HttpAction) ([]byte, error) { 35 | req := buildHttpRequest(httpAction) 36 | 37 | var DefaultTransport http.RoundTripper = &http.Transport{ 38 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 39 | } 40 | 41 | resp, err := DefaultTransport.RoundTrip(req) 42 | if err != nil { 43 | return []byte(nil), err 44 | } else { 45 | defer resp.Body.Close() 46 | responseBody, err := ioutil.ReadAll(resp.Body) 47 | if err != nil { 48 | return []byte(nil), err 49 | } 50 | return responseBody, nil 51 | } 52 | } 53 | 54 | func doHttpRequest(httpAction HttpAction) bool { 55 | req := buildHttpRequest(httpAction) 56 | 57 | var DefaultTransport http.RoundTripper = &http.Transport{ 58 | TLSClientConfig: &tls.Config{InsecureSkipVerify: true}, 59 | } 60 | 61 | resp, err := DefaultTransport.RoundTrip(req) 62 | if resp != nil && resp.StatusCode > 299 { 63 | defer resp.Body.Close() 64 | log.Printf("HTTP request failed with status: %d", resp.StatusCode) 65 | return false 66 | } else if err != nil { 67 | log.Printf("HTTP request failed: %s", err.Error()) 68 | return false 69 | } else { 70 | return true 71 | defer resp.Body.Close() 72 | } 73 | return false 74 | } 75 | 76 | func buildHttpRequest(httpAction HttpAction) *http.Request { 77 | var req *http.Request 78 | var err error 79 | if httpAction.Body != "" { 80 | reader := strings.NewReader(httpAction.Body) 81 | req, err = http.NewRequest(httpAction.Method, httpAction.Url, reader) 82 | } else if httpAction.Template != "" { 83 | reader := strings.NewReader(httpAction.Template) 84 | req, err = http.NewRequest(httpAction.Method, httpAction.Url, reader) 85 | } else { 86 | req, err = http.NewRequest(httpAction.Method, httpAction.Url, nil) 87 | } 88 | if err != nil { 89 | log.Fatal(err) 90 | } 91 | 92 | // Add headers 93 | req.Header = map[string][]string{ 94 | "Accept": {httpAction.Accept}, 95 | "Content-Type": {httpAction.ContentType}, 96 | } 97 | return req 98 | } 99 | 100 | /** 101 | * Trims leading and trailing byte r from string s 102 | */ 103 | func trimChar(s string, r byte) string { 104 | sz := len(s) 105 | 106 | if sz > 0 && s[sz-1] == r { 107 | s = s[:sz-1] 108 | } 109 | sz = len(s) 110 | if sz > 0 && s[0] == r { 111 | s = s[1:sz] 112 | } 113 | return s 114 | } 115 | -------------------------------------------------------------------------------- /eurekaclient.go: -------------------------------------------------------------------------------- 1 | /** 2 | The MIT License (MIT) 3 | 4 | Copyright (c) 2016 ErikL 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. 23 | */ 24 | package eeureka 25 | 26 | import ( 27 | "encoding/json" 28 | "fmt" 29 | "github.com/twinj/uuid" 30 | "log" 31 | "net" 32 | "os" 33 | "os/signal" 34 | "strings" 35 | "syscall" 36 | "time" 37 | ) 38 | 39 | var instanceId string 40 | var discoveryServerUrl = "http://192.168.99.100:8761" 41 | 42 | var regTpl = `{ 43 | "instance": { 44 | "hostName":"${ipAddress}", 45 | "app":"${appName}", 46 | "ipAddr":"${ipAddress}", 47 | "vipAddress":"${appName}", 48 | "status":"UP", 49 | "port": { 50 | "$":${port}, 51 | "@enabled": true 52 | }, 53 | "securePort": { 54 | "$":${securePort}, 55 | "@enabled": true 56 | }, 57 | "homePageUrl" : "http://${ipAddress}:${port}/", 58 | "statusPageUrl": "http://${ipAddress}:${port}/info", 59 | "healthCheckUrl": "http://${ipAddress}:${port}/health", 60 | "dataCenterInfo" : { 61 | "@class":"com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo", 62 | "name": "MyOwn" 63 | }, 64 | "metadata": { 65 | "instanceId" : "${appName}:${instanceId}" 66 | } 67 | } 68 | }` 69 | 70 | /** 71 | * Registers this application at the Eureka server at @eurekaUrl as @appName running on port(s) @port and/or @securePort. 72 | */ 73 | func RegisterAt(eurekaUrl string, appName string, port string, securePort string) { 74 | discoveryServerUrl = eurekaUrl 75 | Register(appName, port, securePort) 76 | } 77 | 78 | /** 79 | Register the application at the default eurekaUrl. 80 | */ 81 | func Register(appName string, port string, securePort string) { 82 | instanceId = getUUID() 83 | 84 | tpl := string(regTpl) 85 | tpl = strings.Replace(tpl, "${ipAddress}", getLocalIP(), -1) 86 | tpl = strings.Replace(tpl, "${port}", port, -1) 87 | tpl = strings.Replace(tpl, "${securePort}", securePort, -1) 88 | tpl = strings.Replace(tpl, "${instanceId}", instanceId, -1) 89 | tpl = strings.Replace(tpl, "${appName}", appName, -1) 90 | 91 | // Register. 92 | registerAction := HttpAction{ 93 | Url: discoveryServerUrl + "/eureka/apps/" + appName, 94 | Method: "POST", 95 | ContentType: "application/json;charset=UTF-8", 96 | Body: tpl, 97 | } 98 | var result bool 99 | for { 100 | result = doHttpRequest(registerAction) 101 | if result { 102 | fmt.Println("Registration OK") 103 | handleSigterm(appName) 104 | go startHeartbeat(appName) 105 | break 106 | } else { 107 | fmt.Println("Registration attempt of " + appName + " failed...") 108 | time.Sleep(time.Second * 5) 109 | } 110 | } 111 | 112 | } 113 | 114 | /** 115 | * Given the supplied appName, this func queries the Eureka API for instances of the appName and returns 116 | * them as a EurekaApplication struct. 117 | */ 118 | func GetServiceInstances(appName string) ([]EurekaInstance, error) { 119 | var m EurekaServiceResponse 120 | fmt.Println("Querying eureka for instances of " + appName + " at: " + discoveryServerUrl + "/eureka/apps/" + appName) 121 | queryAction := HttpAction{ 122 | Url: discoveryServerUrl + "/eureka/apps/" + appName, 123 | Method: "GET", 124 | Accept: "application/json;charset=UTF-8", 125 | ContentType: "application/json;charset=UTF-8", 126 | } 127 | log.Println("Doing queryAction using URL: " + queryAction.Url) 128 | bytes, err := executeQuery(queryAction) 129 | if err != nil { 130 | return nil, err 131 | } else { 132 | fmt.Println("Got instances response from Eureka:\n" + string(bytes)) 133 | err := json.Unmarshal(bytes, &m) 134 | if err != nil { 135 | fmt.Println("Problem parsing JSON response from Eureka: " + err.Error()) 136 | return nil, err 137 | } 138 | return m.Application.Instance, nil 139 | } 140 | } 141 | 142 | // Experimental, untested. 143 | func GetServices() ([]EurekaApplication, error) { 144 | var m EurekaApplicationsRootResponse 145 | fmt.Println("Querying eureka for services at: " + discoveryServerUrl + "/eureka/apps") 146 | queryAction := HttpAction{ 147 | Url: discoveryServerUrl + "/eureka/apps", 148 | Method: "GET", 149 | Accept: "application/json;charset=UTF-8", 150 | ContentType: "application/json;charset=UTF-8", 151 | } 152 | log.Println("Doing queryAction using URL: " + queryAction.Url) 153 | bytes, err := executeQuery(queryAction) 154 | if err != nil { 155 | return nil, err 156 | } else { 157 | fmt.Println("Got services response from Eureka:\n" + string(bytes)) 158 | err := json.Unmarshal(bytes, &m) 159 | if err != nil { 160 | fmt.Println("Problem parsing JSON response from Eureka: " + err.Error()) 161 | return nil, err 162 | } 163 | return m.Resp.Applications, nil 164 | } 165 | } 166 | 167 | // Start as goroutine, will loop indefinitely until application exits. 168 | func startHeartbeat(appName string) { 169 | for { 170 | time.Sleep(time.Second * 30) 171 | heartbeat(appName) 172 | } 173 | } 174 | 175 | func heartbeat(appName string) { 176 | heartbeatAction := HttpAction{ 177 | Url: discoveryServerUrl + "/eureka/apps/" + appName + "/" + getLocalIP(), 178 | Method: "PUT", 179 | ContentType: "application/json;charset=UTF-8", 180 | } 181 | fmt.Println("Issuing heartbeat to " + heartbeatAction.Url) 182 | doHttpRequest(heartbeatAction) 183 | } 184 | 185 | func deregister(appName string) { 186 | fmt.Println("Trying to deregister application " + appName + "...") 187 | // Deregister 188 | deregisterAction := HttpAction{ 189 | Url: discoveryServerUrl + "/eureka/apps/" + appName + "/" + getLocalIP(), 190 | ContentType: "application/json;charset=UTF-8", 191 | Method: "DELETE", 192 | } 193 | doHttpRequest(deregisterAction) 194 | fmt.Println("Deregistered application " + appName + ", exiting. Check Eureka...") 195 | } 196 | 197 | func getLocalIP() string { 198 | addrs, err := net.InterfaceAddrs() 199 | if err != nil { 200 | return "" 201 | } 202 | for _, address := range addrs { 203 | // check the address type and if it is not a loopback the display it 204 | if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() { 205 | if ipnet.IP.To4() != nil { 206 | return ipnet.IP.String() 207 | } 208 | } 209 | } 210 | panic("Unable to determine local IP address (non loopback). Exiting.") 211 | } 212 | 213 | func getUUID() string { 214 | return uuid.NewV4().String() 215 | } 216 | 217 | func handleSigterm(appName string) { 218 | c := make(chan os.Signal, 1) 219 | signal.Notify(c, os.Interrupt) 220 | signal.Notify(c, syscall.SIGTERM) 221 | go func() { 222 | <-c 223 | deregister(appName) 224 | os.Exit(1) 225 | }() 226 | } 227 | --------------------------------------------------------------------------------