├── Dockerfile ├── README.md ├── motorboat-rc.yaml ├── LICENSE └── main.go /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM scratch 2 | MAINTAINER Kelsey Hightower 3 | ADD motorboat motorboat 4 | ENTRYPOINT ["/motorboat"] 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # motorboat 2 | 3 | Dynamically sync Nginx Plus backends from Kubernetes service endpoints. 4 | 5 | ## Usage 6 | 7 | ``` 8 | motorboat -h 9 | ``` 10 | 11 | ``` 12 | Usage of motorboat: 13 | -api-server string 14 | Kubernetes API server for watching endpoints. (ip:port) (default "127.0.0.1:8080") 15 | -nginx-server string 16 | Nginx server for managing backends. (ip:port) 17 | ``` 18 | -------------------------------------------------------------------------------- /motorboat-rc.yaml: -------------------------------------------------------------------------------- 1 | apiVersion: v1 2 | kind: ReplicationController 3 | metadata: 4 | name: motorboat 5 | spec: 6 | replicas: 1 7 | selector: 8 | name: motorboat 9 | template: 10 | metadata: 11 | labels: 12 | name: motorboat 13 | spec: 14 | containers: 15 | - name: "motorboat" 16 | image: "b.gcr.io/kuar/motorboat:0.0.1" 17 | args: 18 | - "--api-server=10.240.178.167:8080" 19 | - "--nginx-server=10.240.26.48:8080" 20 | - "--nginx-status-server=10.240.26.48:9090" 21 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "encoding/json" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "io/ioutil" 9 | "log" 10 | "net" 11 | "net/http" 12 | "time" 13 | 14 | "golang.org/x/net/websocket" 15 | ) 16 | 17 | var ( 18 | apiServer string 19 | nginxServer string 20 | nginxStatusServer string 21 | ) 22 | 23 | type Object struct { 24 | Object Endpoints `json:"object"` 25 | } 26 | 27 | type Endpoints struct { 28 | Kind string `json:"kind"` 29 | ApiVersion string `json:"apiVersion"` 30 | Metadata Metadata `json:"metadata"` 31 | Subsets []Subset `json:"subsets"` 32 | } 33 | 34 | type Metadata struct { 35 | Name string `json:"name"` 36 | } 37 | 38 | type Subset struct { 39 | Addresses []Address `json:"addresses"` 40 | Ports []Port `json:"ports"` 41 | } 42 | 43 | type Address struct { 44 | IP string `json:"ip"` 45 | } 46 | 47 | type Port struct { 48 | Name string `json:"name"` 49 | Port int `json:"port"` 50 | } 51 | 52 | type NginxResponse struct { 53 | Upstreams map[string][]Backend `json:"upstreams"` 54 | } 55 | 56 | type Backend struct { 57 | ID int `json:"id"` 58 | Server string `json:"server"` 59 | } 60 | 61 | func NginxStatus() (*NginxResponse, error) { 62 | resp, err := http.Get(fmt.Sprintf("http://%s/status", nginxStatusServer)) 63 | if err != nil { 64 | return nil, err 65 | } 66 | if resp.StatusCode != http.StatusOK { 67 | return nil, errors.New("Non 200 OK") 68 | } 69 | defer resp.Body.Close() 70 | 71 | data, err := ioutil.ReadAll(resp.Body) 72 | if err != nil { 73 | return nil, err 74 | } 75 | 76 | var er NginxResponse 77 | if err := json.Unmarshal(data, &er); err != nil { 78 | return nil, err 79 | } 80 | return &er, nil 81 | } 82 | 83 | func init() { 84 | flag.StringVar(&apiServer, "api-server", "127.0.0.1:8080", "Kubernetes API server for watching endpoints. (ip:port)") 85 | flag.StringVar(&nginxServer, "nginx-server", "", "Nginx server for managing backends. (ip:port)") 86 | flag.StringVar(&nginxStatusServer, "nginx-status-server", "", "Nginx server for status. (ip:port)") 87 | } 88 | 89 | func main() { 90 | 91 | flag.Parse() 92 | 93 | origin := "http://localhost" 94 | url := fmt.Sprintf("ws://%s/api/v1/watch/endpoints", apiServer) 95 | ws, err := websocket.Dial(url, "", origin) 96 | if err != nil { 97 | log.Fatal(err) 98 | } 99 | 100 | for { 101 | var ep Object 102 | if err := websocket.JSON.Receive(ws, &ep); err != nil { 103 | log.Println(err) 104 | time.Sleep(time.Duration(2 * time.Second)) 105 | } 106 | 107 | nr, err := NginxStatus() 108 | if err != nil { 109 | log.Println(err) 110 | time.Sleep(time.Duration(2 * time.Second)) 111 | } 112 | 113 | upstreamName := ep.Object.Metadata.Name 114 | upstreams, ok := nr.Upstreams[upstreamName] 115 | if !ok { 116 | log.Printf("no matching upstream for service %s skipping...", upstreamName) 117 | continue 118 | } 119 | 120 | for _, subset := range ep.Object.Subsets { 121 | RowLoop: 122 | for _, address := range subset.Addresses { 123 | server := net.JoinHostPort(address.IP, "80") 124 | 125 | for _, backend := range upstreams { 126 | if server == backend.Server { 127 | continue RowLoop 128 | } 129 | } 130 | 131 | log.Printf("registering backend %s with %s ...", server, upstreamName) 132 | url := fmt.Sprintf("http://%s/upstream_conf?add=&upstream=%s&server=%s", nginxServer, upstreamName, server) 133 | resp, err := http.Get(url) 134 | if err != nil { 135 | log.Fatal(err) 136 | } 137 | resp.Body.Close() 138 | if resp.StatusCode != http.StatusOK { 139 | log.Fatal(errors.New("Non 200 OK")) 140 | } 141 | } 142 | } 143 | 144 | // Remove old backends 145 | for _, backend := range upstreams { 146 | for _, subset := range ep.Object.Subsets { 147 | 148 | var server string 149 | hasBackend := false 150 | for _, address := range subset.Addresses { 151 | server = net.JoinHostPort(address.IP, "80") 152 | if server == backend.Server { 153 | hasBackend = true 154 | break 155 | } 156 | } 157 | 158 | if !hasBackend { 159 | log.Printf("removing backend %s [#%d] from %s ...", backend.Server, backend.ID, upstreamName) 160 | url := fmt.Sprintf("http://%s/upstream_conf?remove=&upstream=%s&id=%d", nginxServer, upstreamName, backend.ID) 161 | resp, err := http.Get(url) 162 | if err != nil { 163 | log.Println(err) 164 | } 165 | data, err := ioutil.ReadAll(resp.Body) 166 | if err != nil { 167 | log.Println(err) 168 | } 169 | if resp.StatusCode != http.StatusOK { 170 | log.Println(string(data)) 171 | log.Println(errors.New("Non 200 OK"), resp.StatusCode) 172 | } 173 | } 174 | } 175 | } 176 | } 177 | } 178 | --------------------------------------------------------------------------------