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  }