github.com/fanux/shipyard@v0.0.0-20161009071005-6515ce223235/controller/api/api.go (about) 1 package api 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "net/http" 7 8 log "github.com/Sirupsen/logrus" 9 "github.com/codegangsta/negroni" 10 "github.com/gorilla/context" 11 "github.com/gorilla/mux" 12 "github.com/mailgun/oxy/forward" 13 "github.com/shipyard/shipyard/auth" 14 "github.com/shipyard/shipyard/controller/manager" 15 "github.com/shipyard/shipyard/controller/middleware/access" 16 "github.com/shipyard/shipyard/controller/middleware/audit" 17 mAuth "github.com/shipyard/shipyard/controller/middleware/auth" 18 "github.com/shipyard/shipyard/tlsutils" 19 "golang.org/x/net/websocket" 20 ) 21 22 type ( 23 Api struct { 24 listenAddr string 25 manager manager.Manager 26 authWhitelistCIDRs []string 27 enableCors bool 28 serverVersion string 29 allowInsecure bool 30 tlsCACertPath string 31 tlsCertPath string 32 tlsKeyPath string 33 dUrl string 34 fwd *forward.Forwarder 35 } 36 37 ApiConfig struct { 38 ListenAddr string 39 Manager manager.Manager 40 AuthWhiteListCIDRs []string 41 EnableCORS bool 42 AllowInsecure bool 43 TLSCACertPath string 44 TLSCertPath string 45 TLSKeyPath string 46 } 47 48 Credentials struct { 49 Username string `json:"username,omitempty"` 50 Password string `json:"password,omitempty"` 51 } 52 ) 53 54 func writeCorsHeaders(w http.ResponseWriter, r *http.Request) { 55 w.Header().Add("Access-Control-Allow-Origin", "*") 56 w.Header().Add("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept") 57 w.Header().Add("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS") 58 } 59 60 func NewApi(config ApiConfig) (*Api, error) { 61 return &Api{ 62 listenAddr: config.ListenAddr, 63 manager: config.Manager, 64 authWhitelistCIDRs: config.AuthWhiteListCIDRs, 65 enableCors: config.EnableCORS, 66 allowInsecure: config.AllowInsecure, 67 tlsCertPath: config.TLSCertPath, 68 tlsKeyPath: config.TLSKeyPath, 69 tlsCACertPath: config.TLSCACertPath, 70 }, nil 71 } 72 73 func (a *Api) Run() error { 74 globalMux := http.NewServeMux() 75 controllerManager := a.manager 76 client := a.manager.DockerClient() 77 78 // forwarder for swarm 79 var err error 80 a.fwd, err = forward.New() 81 if err != nil { 82 return err 83 } 84 85 u := client.URL 86 87 // setup redirect target to swarm 88 scheme := "http://" 89 90 // check if TLS is enabled and configure if so 91 if client.TLSConfig != nil { 92 log.Debug("configuring ssl for swarm redirect") 93 scheme = "https://" 94 // setup custom roundtripper with TLS transport 95 r := forward.RoundTripper( 96 &http.Transport{ 97 TLSClientConfig: client.TLSConfig, 98 }) 99 f, err := forward.New(r) 100 if err != nil { 101 return err 102 } 103 104 a.fwd = f 105 } 106 107 a.dUrl = fmt.Sprintf("%s%s", scheme, u.Host) 108 109 log.Debugf("configured docker proxy target: %s", a.dUrl) 110 111 swarmRedirect := http.HandlerFunc(a.swarmRedirect) 112 113 swarmHijack := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { 114 a.swarmHijack(client.TLSConfig, a.dUrl, w, req) 115 }) 116 117 apiRouter := mux.NewRouter() 118 apiRouter.HandleFunc("/api/accounts", a.accounts).Methods("GET") 119 apiRouter.HandleFunc("/api/accounts", a.saveAccount).Methods("POST") 120 apiRouter.HandleFunc("/api/accounts/{username}", a.account).Methods("GET") 121 apiRouter.HandleFunc("/api/accounts/{username}", a.deleteAccount).Methods("DELETE") 122 apiRouter.HandleFunc("/api/roles", a.roles).Methods("GET") 123 apiRouter.HandleFunc("/api/roles/{name}", a.role).Methods("GET") 124 apiRouter.HandleFunc("/api/nodes", a.nodes).Methods("GET") 125 apiRouter.HandleFunc("/api/nodes/{name}", a.node).Methods("GET") 126 apiRouter.HandleFunc("/api/containers/{id}/scale", a.scaleContainer).Methods("POST") 127 apiRouter.HandleFunc("/api/events", a.events).Methods("GET") 128 apiRouter.HandleFunc("/api/events", a.purgeEvents).Methods("DELETE") 129 apiRouter.HandleFunc("/api/registries", a.registries).Methods("GET") 130 apiRouter.HandleFunc("/api/registries", a.addRegistry).Methods("POST") 131 apiRouter.HandleFunc("/api/registries/{name}", a.registry).Methods("GET") 132 apiRouter.HandleFunc("/api/registries/{name}", a.removeRegistry).Methods("DELETE") 133 apiRouter.HandleFunc("/api/registries/{name}/repositories", a.repositories).Methods("GET") 134 apiRouter.HandleFunc("/api/registries/{name}/repositories/{repo:.*}", a.repository).Methods("GET") 135 apiRouter.HandleFunc("/api/registries/{name}/repositories/{repo:.*}", a.deleteRepository).Methods("DELETE") 136 apiRouter.HandleFunc("/api/servicekeys", a.serviceKeys).Methods("GET") 137 apiRouter.HandleFunc("/api/servicekeys", a.addServiceKey).Methods("POST") 138 apiRouter.HandleFunc("/api/servicekeys", a.removeServiceKey).Methods("DELETE") 139 apiRouter.HandleFunc("/api/webhookkeys", a.webhookKeys).Methods("GET") 140 apiRouter.HandleFunc("/api/webhookkeys/{id}", a.webhookKey).Methods("GET") 141 apiRouter.HandleFunc("/api/webhookkeys", a.addWebhookKey).Methods("POST") 142 apiRouter.HandleFunc("/api/webhookkeys/{id}", a.deleteWebhookKey).Methods("DELETE") 143 apiRouter.HandleFunc("/api/consolesession/{container}", a.createConsoleSession).Methods("GET") 144 apiRouter.HandleFunc("/api/consolesession/{token}", a.consoleSession).Methods("GET") 145 apiRouter.HandleFunc("/api/consolesession/{token}", a.removeConsoleSession).Methods("DELETE") 146 147 // global handler 148 globalMux.Handle("/", http.FileServer(http.Dir("static"))) 149 150 auditExcludes := []string{ 151 "^/containers/json", 152 "^/images/json", 153 "^/api/events", 154 } 155 apiAuditor := audit.NewAuditor(controllerManager, auditExcludes) 156 157 // api router; protected by auth 158 apiAuthRouter := negroni.New() 159 apiAuthRequired := mAuth.NewAuthRequired(controllerManager, a.authWhitelistCIDRs) 160 apiAccessRequired := access.NewAccessRequired(controllerManager) 161 apiAuthRouter.Use(negroni.HandlerFunc(apiAuthRequired.HandlerFuncWithNext)) 162 apiAuthRouter.Use(negroni.HandlerFunc(apiAccessRequired.HandlerFuncWithNext)) 163 apiAuthRouter.Use(negroni.HandlerFunc(apiAuditor.HandlerFuncWithNext)) 164 apiAuthRouter.UseHandler(apiRouter) 165 globalMux.Handle("/api/", apiAuthRouter) 166 167 // account router ; protected by auth 168 accountRouter := mux.NewRouter() 169 accountRouter.HandleFunc("/account/changepassword", a.changePassword).Methods("POST") 170 accountAuthRouter := negroni.New() 171 accountAuthRequired := mAuth.NewAuthRequired(controllerManager, a.authWhitelistCIDRs) 172 accountAuthRouter.Use(negroni.HandlerFunc(accountAuthRequired.HandlerFuncWithNext)) 173 accountAuthRouter.Use(negroni.HandlerFunc(apiAuditor.HandlerFuncWithNext)) 174 accountAuthRouter.UseHandler(accountRouter) 175 globalMux.Handle("/account/", accountAuthRouter) 176 177 // login handler; public 178 loginRouter := mux.NewRouter() 179 loginRouter.HandleFunc("/auth/login", a.login).Methods("POST") 180 globalMux.Handle("/auth/", loginRouter) 181 globalMux.Handle("/exec", websocket.Handler(a.execContainer)) 182 183 // hub handler; public 184 hubRouter := mux.NewRouter() 185 hubRouter.HandleFunc("/hub/webhook/{id}", a.hubWebhook).Methods("POST") 186 globalMux.Handle("/hub/", hubRouter) 187 188 // swarm 189 swarmRouter := mux.NewRouter() 190 // these are pulled from the swarm api code to proxy and allow 191 // usage with the standard Docker cli 192 m := map[string]map[string]http.HandlerFunc{ 193 "GET": { 194 "/_ping": swarmRedirect, 195 "/events": swarmRedirect, 196 "/info": swarmRedirect, 197 "/version": swarmRedirect, 198 "/images/json": swarmRedirect, 199 "/images/viz": swarmRedirect, 200 "/images/search": swarmRedirect, 201 "/images/get": swarmRedirect, 202 "/images/{name:.*}/get": swarmRedirect, 203 "/images/{name:.*}/history": swarmRedirect, 204 "/images/{name:.*}/json": swarmRedirect, 205 "/containers/ps": swarmRedirect, 206 "/containers/json": swarmRedirect, 207 "/containers/{name:.*}/export": swarmRedirect, 208 "/containers/{name:.*}/changes": swarmRedirect, 209 "/containers/{name:.*}/json": swarmRedirect, 210 "/containers/{name:.*}/top": swarmRedirect, 211 "/containers/{name:.*}/logs": swarmRedirect, 212 "/containers/{name:.*}/stats": swarmRedirect, 213 "/containers/{name:.*}/attach/ws": swarmHijack, 214 "/exec/{execid:.*}/json": swarmRedirect, 215 }, 216 "POST": { 217 "/auth": swarmRedirect, 218 "/commit": swarmRedirect, 219 "/build": swarmRedirect, 220 "/images/create": swarmRedirect, 221 "/images/load": swarmRedirect, 222 "/images/{name:.*}/push": swarmRedirect, 223 "/images/{name:.*}/tag": swarmRedirect, 224 "/containers/create": swarmRedirect, 225 "/containers/{name:.*}/kill": swarmRedirect, 226 "/containers/{name:.*}/pause": swarmRedirect, 227 "/containers/{name:.*}/unpause": swarmRedirect, 228 "/containers/{name:.*}/rename": swarmRedirect, 229 "/containers/{name:.*}/restart": swarmRedirect, 230 "/containers/{name:.*}/start": swarmRedirect, 231 "/containers/{name:.*}/stop": swarmRedirect, 232 "/containers/{name:.*}/wait": swarmRedirect, 233 "/containers/{name:.*}/resize": swarmRedirect, 234 "/containers/{name:.*}/attach": swarmHijack, 235 "/containers/{name:.*}/copy": swarmRedirect, 236 "/containers/{name:.*}/exec": swarmRedirect, 237 "/exec/{execid:.*}/start": swarmHijack, 238 "/exec/{execid:.*}/resize": swarmRedirect, 239 }, 240 "DELETE": { 241 "/containers/{name:.*}": swarmRedirect, 242 "/images/{name:.*}": swarmRedirect, 243 }, 244 "OPTIONS": { 245 "": swarmRedirect, 246 }, 247 } 248 249 for method, routes := range m { 250 for route, fct := range routes { 251 localRoute := route 252 localFct := fct 253 wrap := func(w http.ResponseWriter, r *http.Request) { 254 if a.enableCors { 255 writeCorsHeaders(w, r) 256 } 257 localFct(w, r) 258 } 259 localMethod := method 260 261 // add the new route 262 swarmRouter.Path("/v{version:[0-9.]+}" + localRoute).Methods(localMethod).HandlerFunc(wrap) 263 swarmRouter.Path(localRoute).Methods(localMethod).HandlerFunc(wrap) 264 } 265 } 266 267 swarmAuthRouter := negroni.New() 268 swarmAuthRequired := mAuth.NewAuthRequired(controllerManager, a.authWhitelistCIDRs) 269 swarmAccessRequired := access.NewAccessRequired(controllerManager) 270 swarmAuthRouter.Use(negroni.HandlerFunc(swarmAuthRequired.HandlerFuncWithNext)) 271 swarmAuthRouter.Use(negroni.HandlerFunc(swarmAccessRequired.HandlerFuncWithNext)) 272 swarmAuthRouter.Use(negroni.HandlerFunc(apiAuditor.HandlerFuncWithNext)) 273 swarmAuthRouter.UseHandler(swarmRouter) 274 globalMux.Handle("/containers/", swarmAuthRouter) 275 globalMux.Handle("/_ping", swarmAuthRouter) 276 globalMux.Handle("/commit", swarmAuthRouter) 277 globalMux.Handle("/build", swarmAuthRouter) 278 globalMux.Handle("/events", swarmAuthRouter) 279 globalMux.Handle("/version", swarmAuthRouter) 280 globalMux.Handle("/images/", swarmAuthRouter) 281 globalMux.Handle("/exec/", swarmAuthRouter) 282 globalMux.Handle("/v1.14/", swarmAuthRouter) 283 globalMux.Handle("/v1.15/", swarmAuthRouter) 284 globalMux.Handle("/v1.16/", swarmAuthRouter) 285 globalMux.Handle("/v1.17/", swarmAuthRouter) 286 globalMux.Handle("/v1.18/", swarmAuthRouter) 287 globalMux.Handle("/v1.19/", swarmAuthRouter) 288 globalMux.Handle("/v1.20/", swarmAuthRouter) 289 290 // check for admin user 291 if _, err := controllerManager.Account("admin"); err == manager.ErrAccountDoesNotExist { 292 // create roles 293 acct := &auth.Account{ 294 Username: "admin", 295 Password: "shipyard", 296 FirstName: "Shipyard", 297 LastName: "Admin", 298 Roles: []string{"admin"}, 299 } 300 if err := controllerManager.SaveAccount(acct); err != nil { 301 log.Fatal(err) 302 } 303 log.Infof("created admin user: username: admin password: shipyard") 304 } 305 306 log.Infof("controller listening on %s", a.listenAddr) 307 308 s := &http.Server{ 309 Addr: a.listenAddr, 310 Handler: context.ClearHandler(globalMux), 311 } 312 313 var runErr error 314 315 if a.tlsCertPath != "" && a.tlsKeyPath != "" { 316 log.Infof("using TLS for communication: cert=%s key=%s", 317 a.tlsCertPath, 318 a.tlsKeyPath, 319 ) 320 321 // setup TLS config 322 var caCert []byte 323 if a.tlsCACertPath != "" { 324 ca, err := ioutil.ReadFile(a.tlsCACertPath) 325 if err != nil { 326 return err 327 } 328 329 caCert = ca 330 } 331 332 serverCert, err := ioutil.ReadFile(a.tlsCertPath) 333 if err != nil { 334 return err 335 } 336 337 serverKey, err := ioutil.ReadFile(a.tlsKeyPath) 338 if err != nil { 339 return err 340 } 341 342 tlsConfig, err := tlsutils.GetServerTLSConfig(caCert, serverCert, serverKey, a.allowInsecure) 343 if err != nil { 344 return err 345 } 346 347 s.TLSConfig = tlsConfig 348 349 runErr = s.ListenAndServeTLS(a.tlsCertPath, a.tlsKeyPath) 350 } else { 351 runErr = s.ListenAndServe() 352 } 353 354 return runErr 355 }