├── docs └── sample.config ├── podlist.go ├── main.go ├── LICENSE ├── jsonrpc.go ├── README.md ├── service_manager.go └── service_proxy.go /docs/sample.config: -------------------------------------------------------------------------------- 1 | { 2 | "id": "myapp", 3 | "selector": { 4 | "app": "MyApp" 5 | }, 6 | "containerPort": 9376, 7 | "protocol": "TCP", 8 | "port": 8765 9 | } 10 | -------------------------------------------------------------------------------- /podlist.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | type PodList struct { 4 | Items []Pod `json:"items"` 5 | } 6 | 7 | type Pod struct { 8 | ID string `json:"id"` 9 | CurrentState CurrentState `json:"currentState"` 10 | } 11 | 12 | type CurrentState struct { 13 | Status string `json:"status"` 14 | PodIP string `json:"podIP"` 15 | } 16 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "io" 5 | "log" 6 | "net" 7 | "net/http" 8 | "net/rpc" 9 | "os" 10 | ) 11 | 12 | var ( 13 | apiserver string 14 | bindIP string 15 | ) 16 | 17 | func main() { 18 | apiserver = os.Getenv("KUBERNETES_API_SERVER") 19 | if apiserver == "" { 20 | log.Fatal("KUBERNETES_API_SERVER cannot be empty") 21 | } 22 | 23 | bindIP = os.Getenv("KEP_BIND_IP") 24 | if bindIP == "" { 25 | bindIP = "0.0.0.0" 26 | } 27 | sm := newServiceManager() 28 | rpc.Register(sm) 29 | http.HandleFunc("/", func(w http.ResponseWriter, req *http.Request) { 30 | defer req.Body.Close() 31 | w.Header().Set("Content-Type", "application/json") 32 | res := NewRPCRequest(req.Body).Call() 33 | _, err := io.Copy(w, res) 34 | if err != nil { 35 | log.Println(err) 36 | } 37 | }) 38 | hostPort := net.JoinHostPort(bindIP, "8000") 39 | log.Println("starting kubernetes-external-proxy service...") 40 | log.Printf("accepting RPC request on http://%s", hostPort) 41 | log.Fatal(http.ListenAndServe(hostPort, nil)) 42 | } 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2015 Kelsey Hightower 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of 4 | this software and associated documentation files (the "Software"), to deal in 5 | the Software without restriction, including without limitation the rights to 6 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 7 | of the Software, and to permit persons to whom the Software is furnished to do 8 | so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in all 11 | copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 19 | SOFTWARE. 20 | -------------------------------------------------------------------------------- /jsonrpc.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "net/rpc/jsonrpc" 7 | ) 8 | 9 | // rpcRequest represents a RPC request. 10 | // rpcRequest implements the io.ReadWriteCloser interface. 11 | type rpcRequest struct { 12 | r io.Reader // holds the JSON formated RPC request 13 | rw io.ReadWriter // holds the JSON formated RPC response 14 | done chan bool // signals then end of the RPC request 15 | } 16 | 17 | // NewRPCRequest returns a new rpcRequest. 18 | func NewRPCRequest(r io.Reader) *rpcRequest { 19 | var buf bytes.Buffer 20 | done := make(chan bool) 21 | return &rpcRequest{r, &buf, done} 22 | } 23 | 24 | // Read implements the io.ReadWriteCloser Read method. 25 | func (r *rpcRequest) Read(p []byte) (n int, err error) { 26 | return r.r.Read(p) 27 | } 28 | 29 | // Write implements the io.ReadWriteCloser Write method. 30 | func (r *rpcRequest) Write(p []byte) (n int, err error) { 31 | n, err = r.rw.Write(p) 32 | r.done <- true 33 | return n, err 34 | } 35 | 36 | // Close implements the io.ReadWriteCloser Close method. 37 | func (r *rpcRequest) Close() error { 38 | return nil 39 | } 40 | 41 | // Call invokes the RPC request, waits for it to complete, and returns the results. 42 | func (r *rpcRequest) Call() io.Reader { 43 | go jsonrpc.ServeConn(r) 44 | <-r.done 45 | return r.rw 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Kubernetes External Proxy 2 | 3 | This will provide an external service proxy for Kubernetes Pods discovered via label queries. 4 | 5 | Currently a work in progress. 6 | 7 | ## Installation 8 | 9 | ``` 10 | go install github.com/kelseyhightower/kubernetes-external-proxy 11 | ``` 12 | 13 | ## Usage 14 | 15 | Configure the server: 16 | 17 | ``` 18 | export KUBERNETES_API_SERVER="192.168.12.20:8080" 19 | ``` 20 | 21 | Start the server: 22 | 23 | ``` 24 | kubernetes-external-proxy 25 | ``` 26 | 27 | ### Add a service 28 | 29 | Create an add service RPC request: 30 | 31 | ``` 32 | { 33 | "method": "ServiceManager.Add", 34 | "params":[{ 35 | "id": "hello", 36 | "selector": { 37 | "environment": "production" 38 | }, 39 | "containerPort": "80", 40 | "protocol": "tcp", 41 | "port": "5000" 42 | }], 43 | "id": 0 44 | } 45 | ``` 46 | 47 | ``` 48 | curl -i -d @add-hello-service.json http://127.0.0.1:8000 49 | ``` 50 | 51 | ``` 52 | {"id":0,"result":"0.0.0.0:5000","error":null} 53 | ``` 54 | 55 | ### Delete a service 56 | 57 | Create a delete service RPC request: 58 | 59 | ``` 60 | { 61 | "method": "ServiceManager.Del", 62 | "params":["hello"], 63 | "id": 0 64 | } 65 | ``` 66 | 67 | ``` 68 | curl -i -d @delete-hello-service.json http://127.0.0.1:8000 69 | ``` 70 | 71 | ``` 72 | {"id":0,"result":true,"error":null} 73 | ``` 74 | -------------------------------------------------------------------------------- /service_manager.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "errors" 5 | "log" 6 | "net" 7 | "sync" 8 | ) 9 | 10 | // A ServiceManager manages service proxies. 11 | type ServiceManager struct { 12 | mu sync.Mutex 13 | m map[string]*ServiceProxy 14 | } 15 | 16 | func newServiceManager() *ServiceManager { 17 | m := make(map[string]*ServiceProxy) 18 | return &ServiceManager{m: m} 19 | } 20 | 21 | // Add creates a new ServiceProxy based on args. 22 | func (sm *ServiceManager) Add(args *Service, reply *string) error { 23 | if err := sm.add(args); err != nil { 24 | log.Println(err) 25 | return err 26 | } 27 | *reply = net.JoinHostPort(bindIP, args.Port) 28 | log.Printf("%s service added", args.ID) 29 | return nil 30 | } 31 | 32 | // Del deletes the ServiceProxy based on the given service ID (args). 33 | // The ServiceProxy is stopped after active connections have terminated. 34 | func (sm *ServiceManager) Del(args *string, reply *bool) error { 35 | if err := sm.del(*args); err != nil { 36 | log.Println(err) 37 | return err 38 | } 39 | *reply = true 40 | log.Printf("%s service deleted", *args) 41 | return nil 42 | } 43 | 44 | func (sm *ServiceManager) add(service *Service) error { 45 | sm.mu.Lock() 46 | defer sm.mu.Unlock() 47 | if _, ok := sm.m[service.ID]; ok { 48 | err := errors.New("service already exist") 49 | log.Printf("error adding the %s service: %v", err) 50 | return err 51 | } 52 | sp := newServiceProxy(service) 53 | if err := sp.start(); err != nil { 54 | log.Printf("error adding the %s service: %v", service.ID, err) 55 | return err 56 | } 57 | sm.m[service.ID] = sp 58 | return nil 59 | } 60 | 61 | func (sm *ServiceManager) del(id string) error { 62 | sm.mu.Lock() 63 | defer sm.mu.Unlock() 64 | if v, ok := sm.m[id]; ok { 65 | if err := v.stop(); err != nil { 66 | log.Printf("error stopping the %s service: %v", id, err) 67 | return err 68 | } 69 | delete(sm.m, id) 70 | } 71 | return nil 72 | } 73 | -------------------------------------------------------------------------------- /service_proxy.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "container/ring" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io" 9 | "io/ioutil" 10 | "log" 11 | "net" 12 | "net/http" 13 | "net/url" 14 | "strings" 15 | "sync" 16 | "time" 17 | ) 18 | 19 | type Service struct { 20 | ID string `json:"id"` 21 | ContainerPort string `json:"containerPort"` 22 | Protocol string `json:"protocol"` 23 | Port string `json:"port"` 24 | Selector map[string]string `json:"selector"` 25 | } 26 | 27 | type ServiceProxy struct { 28 | service *Service 29 | shutdown chan bool 30 | done chan bool 31 | sync.Mutex 32 | pods []string 33 | r *ring.Ring 34 | } 35 | 36 | func newServiceProxy(service *Service) *ServiceProxy { 37 | shutdown := make(chan bool) 38 | done := make(chan bool) 39 | return &ServiceProxy{done: done, shutdown: shutdown, service: service} 40 | } 41 | 42 | func (sp *ServiceProxy) start() error { 43 | if err := sp.updatePods(); err != nil { 44 | return err 45 | } 46 | 47 | hostPort := net.JoinHostPort(bindIP, sp.service.Port) 48 | ln, err := net.Listen(sp.service.Protocol, hostPort) 49 | if err != nil { 50 | return err 51 | } 52 | 53 | var shutdown bool 54 | go func() { 55 | log.Printf("accepting new connections for the %s service", sp.service.ID) 56 | log.Printf("proxy established %s <-> %s", sp.service.Port, sp.service.ContainerPort) 57 | for { 58 | if shutdown { 59 | goto Shutdown 60 | } 61 | conn, err := ln.Accept() 62 | if err != nil { 63 | if shutdown { 64 | goto Shutdown 65 | } 66 | log.Printf("error accepting connections for service: %s", sp.service.ID) 67 | time.Sleep(time.Duration(5 * time.Second)) 68 | continue 69 | } 70 | go sp.handleConnection(conn) 71 | } 72 | 73 | Shutdown: 74 | log.Printf("stopping the %s service...", sp.service.ID) 75 | sp.done <- true 76 | return 77 | }() 78 | go func() { 79 | select { 80 | case <-sp.shutdown: 81 | shutdown = true 82 | ln.Close() 83 | } 84 | }() 85 | return nil 86 | } 87 | 88 | func (sp *ServiceProxy) stop() error { 89 | sp.shutdown <- true 90 | <-sp.done 91 | return nil 92 | } 93 | 94 | func (sp *ServiceProxy) updatePods() error { 95 | pods, err := podsFromLabelQuery(sp.service.Selector) 96 | if err != nil { 97 | return err 98 | } 99 | sp.Lock() 100 | sp.pods = pods 101 | r := ring.New(len(pods)) 102 | for i := 0; i < r.Len(); i++ { 103 | r.Value = pods[i] 104 | r = r.Next() 105 | } 106 | sp.r = r 107 | sp.Unlock() 108 | return nil 109 | } 110 | 111 | func (sp *ServiceProxy) nextPod() string { 112 | sp.Lock() 113 | sp.Unlock() 114 | if sp.r == nil { 115 | return "" 116 | } 117 | sp.r = sp.r.Next() 118 | return net.JoinHostPort(sp.r.Value.(string), sp.service.ContainerPort) 119 | } 120 | 121 | func (sp *ServiceProxy) handleConnection(conn net.Conn) { 122 | hostPort := sp.nextPod() 123 | if hostPort == "" { 124 | log.Printf("error cannot service request for %s: no pods available", sp.service.ID) 125 | conn.Close() 126 | return 127 | } 128 | proxyConn, err := net.Dial(sp.service.Protocol, hostPort) 129 | if err != nil { 130 | log.Println(err) 131 | return 132 | } 133 | 134 | var wg sync.WaitGroup 135 | wg.Add(2) 136 | 137 | go copyData(proxyConn, conn, &wg) 138 | go copyData(conn, proxyConn, &wg) 139 | wg.Wait() 140 | conn.Close() 141 | proxyConn.Close() 142 | } 143 | 144 | func copyData(in, out net.Conn, wg *sync.WaitGroup) { 145 | defer wg.Done() 146 | _, err := io.Copy(out, in) 147 | if err != nil { 148 | log.Println(err) 149 | } 150 | out.(*net.TCPConn).CloseWrite() 151 | in.(*net.TCPConn).CloseRead() 152 | } 153 | 154 | func podsFromLabelQuery(selector map[string]string) ([]string, error) { 155 | var pods []string 156 | 157 | labels := []string{} 158 | for k, v := range selector { 159 | labels = append(labels, fmt.Sprintf("%s=%s", k, v)) 160 | } 161 | 162 | u := &url.URL{Scheme: "http", Path: "/api/v1beta1/pods", Host: apiserver} 163 | q := u.Query() 164 | q.Set("labels", strings.Join(labels, ",")) 165 | u.RawQuery = q.Encode() 166 | 167 | req := &http.Request{Method: "GET", URL: u} 168 | client := &http.Client{} 169 | resp, err := client.Do(req) 170 | if err != nil { 171 | log.Println("error retrieving pods:", err) 172 | return nil, errors.New("error retrieving pods") 173 | } 174 | 175 | if resp.StatusCode != http.StatusOK { 176 | log.Printf("error retrieving pods from %s", u) 177 | return nil, errors.New("non 200 status code") 178 | } 179 | data, err := ioutil.ReadAll(resp.Body) 180 | if err != nil { 181 | return nil, err 182 | } 183 | defer resp.Body.Close() 184 | 185 | var ps PodList 186 | err = json.Unmarshal(data, &ps) 187 | if err != nil { 188 | return nil, err 189 | } 190 | 191 | for _, p := range ps.Items { 192 | if p.CurrentState.Status == "Running" { 193 | pods = append(pods, p.CurrentState.PodIP) 194 | } 195 | } 196 | 197 | return pods, nil 198 | } 199 | --------------------------------------------------------------------------------