From a89aa40dda83b8294288ebf0b2258d1093671cd9 Mon Sep 17 00:00:00 2001 From: Evan Hazlett Date: Sat, 29 Jul 2017 22:54:43 -0400 Subject: [PATCH] proxy: start on api and caddy based proxy Signed-off-by: Evan Hazlett --- .gitignore | 2 + cmd/element/.gitignore | 2 +- config.toml | 1 - config/config.go | 4 +- config/utils.go | 16 ++++++++ proxy/config.go | 48 +++++++++++++++++++++++ proxy/frontend.go | 49 +++++++++++++++++++++++ proxy/proxy.go | 38 ++++++++++++++++++ proxy/reload.go | 15 +++++++ proxy/start.go | 17 ++++++++ proxy/stop.go | 9 +++++ proxy/template.go | 11 ++++++ proxy/update.go | 10 +++++ proxy/wait.go | 7 ++++ server/add.go | 24 ++++++++++++ server/config.go | 30 ++++++++++++++ server/handler.go | 22 +++++++++-- server/reload.go | 13 +++++++ server/remove.go | 19 +++++++++ server/router.go | 16 ++++++++ server/routes.go | 12 ------ server/server.go | 88 ++++++++++++++++++++++++++++++++---------- server/update.go | 24 ++++++++++++ vendor.conf | 32 +++++++++++---- 24 files changed, 461 insertions(+), 48 deletions(-) create mode 100644 .gitignore delete mode 100644 config.toml create mode 100644 proxy/config.go create mode 100644 proxy/frontend.go create mode 100644 proxy/proxy.go create mode 100644 proxy/reload.go create mode 100644 proxy/start.go create mode 100644 proxy/stop.go create mode 100644 proxy/template.go create mode 100644 proxy/update.go create mode 100644 proxy/wait.go create mode 100644 server/add.go create mode 100644 server/config.go create mode 100644 server/reload.go create mode 100644 server/remove.go create mode 100644 server/router.go delete mode 100644 server/routes.go create mode 100644 server/update.go diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..9afc9d2 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +*.swp +config.toml diff --git a/cmd/element/.gitignore b/cmd/element/.gitignore index 3f20684..eab5335 100644 --- a/cmd/element/.gitignore +++ b/cmd/element/.gitignore @@ -1 +1 @@ -coupler +element diff --git a/config.toml b/config.toml deleted file mode 100644 index f7e25bd..0000000 --- a/config.toml +++ /dev/null @@ -1 +0,0 @@ -ListenAddr = ":8080" diff --git a/config/config.go b/config/config.go index b15f075..c0ca6be 100644 --- a/config/config.go +++ b/config/config.go @@ -2,6 +2,6 @@ package config // Config is the top level configuration type Config struct { - ListenAddr string - EnableMetrics bool + ListenAddr string + SocketPath string } diff --git a/config/utils.go b/config/utils.go index c582e00..949557b 100644 --- a/config/utils.go +++ b/config/utils.go @@ -2,6 +2,12 @@ package config import ( "github.com/BurntSushi/toml" + "github.com/sirupsen/logrus" +) + +const ( + defaultListenAddr = ":8080" + defaultSocketPath = "/var/run/element.sock" ) // ParseConfig returns a Config object from a raw string config TOML @@ -11,5 +17,15 @@ func ParseConfig(data string) (*Config, error) { return nil, err } + if cfg.ListenAddr == "" { + logrus.Warnf("using default listen addr: %s", defaultListenAddr) + cfg.ListenAddr = defaultListenAddr + } + + if cfg.SocketPath == "" { + logrus.Warnf("using default socket path: %s", defaultSocketPath) + cfg.SocketPath = defaultSocketPath + } + return &cfg, nil } diff --git a/proxy/config.go b/proxy/config.go new file mode 100644 index 0000000..04a0b5c --- /dev/null +++ b/proxy/config.go @@ -0,0 +1,48 @@ +package proxy + +import ( + "bytes" + "text/template" + + "github.com/sirupsen/logrus" +) + +type Config struct { + Frontends map[string]*Frontend `json:"frontends,omitempty"` +} + +type Frontend struct { + Name string `json:"name"` + Hosts []string `json:"hosts,omitempty"` + Backend *Backend `json:"backend,omitempty"` +} + +type Backend struct { + Path string `json:"path"` + Upstreams []string `json:"upstreams,omitempty"` +} + +func (c *Config) Body() []byte { + t := template.New("proxy") + tmpl, err := t.Parse(configTemplate) + if err != nil { + logrus.Errorf("error parsing proxy template: %s", err) + return nil + } + + var b bytes.Buffer + if err := tmpl.Execute(&b, c); err != nil { + logrus.Errorf("error executing proxy template: %s", err) + return nil + } + + return b.Bytes() +} + +func (c *Config) Path() string { + return "" +} + +func (c *Config) ServerType() string { + return "http" +} diff --git a/proxy/frontend.go b/proxy/frontend.go new file mode 100644 index 0000000..3f590ca --- /dev/null +++ b/proxy/frontend.go @@ -0,0 +1,49 @@ +package proxy + +import "github.com/sirupsen/logrus" + +func (p *Proxy) AddFrontend(f *Frontend) error { + p.m.Lock() + defer p.m.Unlock() + + if _, ok := p.config.Frontends[f.Name]; ok { + return ErrFrontendExists + } + + p.config.Frontends[f.Name] = f + logrus.WithFields(logrus.Fields{ + "name": f.Name, + "hosts": f.Hosts, + }).Debug("frontend added") + return nil +} + +func (p *Proxy) RemoveFrontend(name string) error { + p.m.Lock() + defer p.m.Unlock() + + if _, ok := p.config.Frontends[name]; ok { + delete(p.config.Frontends, name) + logrus.WithFields(logrus.Fields{ + "name": name, + }).Debug("frontend removed") + } + + return nil +} + +func (p *Proxy) UpdateFrontend(f *Frontend) error { + p.m.Lock() + defer p.m.Unlock() + + if _, ok := p.config.Frontends[f.Name]; !ok { + return ErrFrontendDoesNotExist + } + + p.config.Frontends[f.Name] = f + logrus.WithFields(logrus.Fields{ + "name": f.Name, + }).Debug("frontend updated") + + return nil +} diff --git a/proxy/proxy.go b/proxy/proxy.go new file mode 100644 index 0000000..9b4fef7 --- /dev/null +++ b/proxy/proxy.go @@ -0,0 +1,38 @@ +package proxy + +import ( + "errors" + "sync" + + "github.com/ehazlett/element/version" + "github.com/mholt/caddy" + _ "github.com/mholt/caddy/caddyhttp" +) + +var ( + ErrFrontendExists = errors.New("frontend exists") + ErrFrontendDoesNotExist = errors.New("frontend does not exist") +) + +type Proxy struct { + config *Config + instance *caddy.Instance + m sync.Mutex +} + +func NewProxy(config *Config) (*Proxy, error) { + if config.Frontends == nil { + config.Frontends = map[string]*Frontend{} + } + caddy.AppName = "element" + caddy.AppVersion = version.Version + version.Build + + return &Proxy{ + config: config, + m: sync.Mutex{}, + }, nil +} + +func (p *Proxy) Config() (*Config, error) { + return p.config, nil +} diff --git a/proxy/reload.go b/proxy/reload.go new file mode 100644 index 0000000..1517714 --- /dev/null +++ b/proxy/reload.go @@ -0,0 +1,15 @@ +package proxy + +func (p *Proxy) Reload() error { + p.m.Lock() + defer p.m.Unlock() + + i, err := p.instance.Restart(p.config) + if err != nil { + return err + } + + p.instance = i + + return nil +} diff --git a/proxy/start.go b/proxy/start.go new file mode 100644 index 0000000..baab8d7 --- /dev/null +++ b/proxy/start.go @@ -0,0 +1,17 @@ +package proxy + +import "github.com/mholt/caddy" + +func (p *Proxy) Start() error { + p.m.Lock() + defer p.m.Unlock() + + i, err := caddy.Start(p.config) + if err != nil { + return err + } + + p.instance = i + + return nil +} diff --git a/proxy/stop.go b/proxy/stop.go new file mode 100644 index 0000000..b53a02b --- /dev/null +++ b/proxy/stop.go @@ -0,0 +1,9 @@ +package proxy + +func (p *Proxy) Stop() error { + if p.instance != nil { + return p.instance.Stop() + } + + return nil +} diff --git a/proxy/template.go b/proxy/template.go new file mode 100644 index 0000000..76763af --- /dev/null +++ b/proxy/template.go @@ -0,0 +1,11 @@ +package proxy + +const configTemplate = ` # element router configuration +{{ range $frontend := .Frontends }} +# {{ $frontend.Name }} +{{ range $host := $frontend.Hosts }}{{ $host }} { + proxy {{ $frontend.Backend.Path }}{{ range $upstream := $frontend.Backend.Upstreams }} {{ $upstream }} {{ end }} { + transparent + } +} {{ end }} {{ end }} +` diff --git a/proxy/update.go b/proxy/update.go new file mode 100644 index 0000000..596c988 --- /dev/null +++ b/proxy/update.go @@ -0,0 +1,10 @@ +package proxy + +func (p *Proxy) Update(config *Config) error { + p.m.Lock() + defer p.m.Unlock() + + p.config = config + + return nil +} diff --git a/proxy/wait.go b/proxy/wait.go new file mode 100644 index 0000000..983543a --- /dev/null +++ b/proxy/wait.go @@ -0,0 +1,7 @@ +package proxy + +func (p *Proxy) Wait() { + if p.instance != nil { + p.instance.Wait() + } +} diff --git a/server/add.go b/server/add.go new file mode 100644 index 0000000..2c649dd --- /dev/null +++ b/server/add.go @@ -0,0 +1,24 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/ehazlett/element/proxy" +) + +func (s *Server) addFrontend(w http.ResponseWriter, r *http.Request) { + var frontend *proxy.Frontend + if err := json.NewDecoder(r.Body).Decode(&frontend); err != nil { + http.Error(w, fmt.Sprintf("invalid fronend: %s", err), http.StatusBadRequest) + return + } + + if err := s.proxy.AddFrontend(frontend); err != nil { + http.Error(w, fmt.Sprintf("error adding frontend: %s", err), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/server/config.go b/server/config.go new file mode 100644 index 0000000..62a7ed9 --- /dev/null +++ b/server/config.go @@ -0,0 +1,30 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" +) + +func (s *Server) getConfig(w http.ResponseWriter, r *http.Request) { + cfg, err := s.proxy.Config() + if err != nil { + http.Error(w, fmt.Sprintf("error getting config: %s", err), http.StatusInternalServerError) + return + } + + if err := json.NewEncoder(w).Encode(cfg); err != nil { + http.Error(w, fmt.Sprintf("error serializing config: %s", err), http.StatusInternalServerError) + return + } +} + +func (s *Server) getConfigRaw(w http.ResponseWriter, r *http.Request) { + cfg, err := s.proxy.Config() + if err != nil { + http.Error(w, fmt.Sprintf("error getting config: %s", err), http.StatusInternalServerError) + return + } + + w.Write(cfg.Body()) +} diff --git a/server/handler.go b/server/handler.go index 67f55e6..23511bc 100644 --- a/server/handler.go +++ b/server/handler.go @@ -2,11 +2,25 @@ package server import ( "net/http" + "time" - "github.com/ehazlett/element/version" + "github.com/sirupsen/logrus" ) -func (s *Server) getRequestHandler(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-Content-Server", "element "+version.FullVersion()) - w.WriteHeader(http.StatusOK) +func (s *Server) genericHandler(w http.ResponseWriter, r *http.Request) { + logrus.WithFields(logrus.Fields{ + "host": r.Host, + "uri": r.RequestURI, + }).Debug("new domain request") + + // TODO: check and / or configure backend container + time.Sleep(time.Millisecond * 1000) + + // TODO: update proxy config with new backend + time.Sleep(time.Millisecond * 1000) + + // TODO: issue redirect to host to have client re-send and connect to backend + + w.Header().Set("Location", r.RequestURI) + w.WriteHeader(http.StatusFound) } diff --git a/server/reload.go b/server/reload.go new file mode 100644 index 0000000..04ac53e --- /dev/null +++ b/server/reload.go @@ -0,0 +1,13 @@ +package server + +import ( + "fmt" + "net/http" +) + +func (s *Server) reload(w http.ResponseWriter, r *http.Request) { + if err := s.proxy.Reload(); err != nil { + http.Error(w, fmt.Sprintf("error reloading: %s", err), http.StatusInternalServerError) + return + } +} diff --git a/server/remove.go b/server/remove.go new file mode 100644 index 0000000..fb3d964 --- /dev/null +++ b/server/remove.go @@ -0,0 +1,19 @@ +package server + +import ( + "fmt" + "net/http" + + "github.com/gorilla/mux" +) + +func (s *Server) removeFrontend(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + name := vars["name"] + if err := s.proxy.RemoveFrontend(name); err != nil { + http.Error(w, fmt.Sprintf("error removing frontend: %s", err), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/server/router.go b/server/router.go new file mode 100644 index 0000000..6dccef3 --- /dev/null +++ b/server/router.go @@ -0,0 +1,16 @@ +package server + +import "github.com/gorilla/mux" + +func (s *Server) router() *mux.Router { + r := mux.NewRouter() + r.HandleFunc("/", s.genericHandler) + r.HandleFunc("/config", s.getConfig).Methods("GET") + r.HandleFunc("/config/raw", s.getConfigRaw).Methods("GET") + r.HandleFunc("/frontends", s.addFrontend).Methods("POST") + r.HandleFunc("/frontends", s.updateFrontend).Methods("PUT") + r.HandleFunc("/frontends/{name}", s.removeFrontend).Methods("DELETE") + r.HandleFunc("/reload", s.reload).Methods("POST") + + return r +} diff --git a/server/routes.go b/server/routes.go deleted file mode 100644 index 028d3a6..0000000 --- a/server/routes.go +++ /dev/null @@ -1,12 +0,0 @@ -package server - -import ( - "github.com/gorilla/mux" -) - -func (s *Server) router() (*mux.Router, error) { - r := mux.NewRouter() - r.HandleFunc("/", s.getRequestHandler).Methods("GET") - - return r, nil -} diff --git a/server/server.go b/server/server.go index b77ed9c..381c773 100644 --- a/server/server.go +++ b/server/server.go @@ -1,44 +1,90 @@ package server import ( + "net" "net/http" + "os" + "os/signal" + "syscall" "github.com/ehazlett/element/config" - "github.com/prometheus/client_golang/prometheus" + "github.com/ehazlett/element/proxy" "github.com/sirupsen/logrus" ) type Server struct { - cfg *config.Config + cfg *config.Config + proxy *proxy.Proxy } func NewServer(cfg *config.Config) (*Server, error) { + p, err := proxy.NewProxy(&proxy.Config{}) + if err != nil { + return nil, err + } return &Server{ - cfg: cfg, + cfg: cfg, + proxy: p, }, nil } func (s *Server) Run() error { - if s.cfg.EnableMetrics { - // start prometheus listener - http.Handle("/metrics", prometheus.Handler()) - go func() { - if err := http.ListenAndServe(s.cfg.ListenAddr, nil); err != nil { - logrus.Error("unable to start metric listener: %s", err) + r := s.router() + + srv := &http.Server{ + Handler: r, + } + + go func() { + // check for existing socket + if _, err := os.Stat(s.cfg.SocketPath); err == nil { + os.Remove(s.cfg.SocketPath) + } + l, err := net.Listen("unix", s.cfg.SocketPath) + if err != nil { + logrus.Errorf("unable to start element server: %s", err) + return + } + + srv.Serve(l) + }() + + cfg := &proxy.Config{ + Frontends: map[string]*proxy.Frontend{ + "element": &proxy.Frontend{ + Name: "element", + Hosts: []string{s.cfg.ListenAddr}, + Backend: &proxy.Backend{ + Path: "/", + Upstreams: []string{"unix:" + s.cfg.SocketPath}, + }, + }, + }, + } + + if err := s.proxy.Update(cfg); err != nil { + return err + } + + if err := s.proxy.Start(); err != nil { + return err + } + + c := make(chan os.Signal, 1) + signal.Notify(c, syscall.SIGHUP) + go func() { + for range c { + if err := s.proxy.Reload(); err != nil { + logrus.Errorf("error reloading proxy: %s", err) } - }() - } + } + }() - r, err := s.router() - if err != nil { - return err - } - - http.Handle("/", r) - - if err := http.ListenAndServe(s.cfg.ListenAddr, nil); err != nil { - return err - } + s.proxy.Wait() return nil } + +func (s *Server) Stop() error { + return s.proxy.Stop() +} diff --git a/server/update.go b/server/update.go new file mode 100644 index 0000000..64f3e74 --- /dev/null +++ b/server/update.go @@ -0,0 +1,24 @@ +package server + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/ehazlett/element/proxy" +) + +func (s *Server) updateFrontend(w http.ResponseWriter, r *http.Request) { + var frontend *proxy.Frontend + if err := json.NewDecoder(r.Body).Decode(&frontend); err != nil { + http.Error(w, fmt.Sprintf("invalid fronend: %s", err), http.StatusBadRequest) + return + } + + if err := s.proxy.UpdateFrontend(frontend); err != nil { + http.Error(w, fmt.Sprintf("error adding frontend: %s", err), http.StatusInternalServerError) + return + } + + w.WriteHeader(http.StatusOK) +} diff --git a/vendor.conf b/vendor.conf index f1b9586..e14bae8 100644 --- a/vendor.conf +++ b/vendor.conf @@ -1,12 +1,30 @@ -github.com/prometheus/client_golang 94ff84a9a6ebb5e6eb9172897c221a64df3443bc +github.com/gorilla/mux ac112f7d75a0714af1bd86ab17749b31f7809640 +github.com/gorilla/context 08b5f424b9271eedf6f9f0ce86cb9396ed337a42 +github.com/mholt/caddy e7f08bff38988c3049b7fda301c52a681af63cd8 github.com/sirupsen/logrus 181d419aa9e2223811b824e8f0b4af96f9ba9302 golang.org/x/crypto 558b6879de74bc843225cde5686419267ff707ca golang.org/x/sys 0f826bdd13b500be0f1d4004938ad978fcc6031e -github.com/beorn7/perks 4c0e84591b9aa9e6dcfdf3e020114cd81f89d5f9 -github.com/golang/protobuf 748d386b5c1ea99658fd69fe9f03991ce86a90c1 -github.com/prometheus/client_model 6f3806018612930941127f2a7c6c453ba2c527d2 -github.com/prometheus/common 3e6a7635bac6573d43f49f97b47eb9bda195dba8 -github.com/prometheus/procfs e645f4e5aaa8506fc71d6edbc5c4ff02c04c46f2 -github.com/matttproud/golang_protobuf_extensions c12348ce28de40eed0136aa2b644d0ee0650e56c +github.com/gorilla/websocket a69d9f6de432e2c6b296a947d8a5ee88f68522cf +github.com/nu7hatch/gouuid 179d4d0c4d8d407a32af483c2354df1d2c91e6c3 +golang.org/x/net f5079bd7f6f74e23c4d65efa0f4ce14cbd6a3c0f +golang.org/x/text 836efe42bb4aa16aaa17b9c155d8813d336ed720 +github.com/russross/blackfriday 4048872b16cc0fc2c5fd9eacf0ed2c2fedaa0c8c +github.com/naoina/toml e6f5723bf2a66af014955e0888881314cf294129 +gopkg.in/yaml.v2 25c4ec802a7d637f88d584ab26798e94ad14c13b +github.com/naoina/go-stringutil 6b638e95a32d0c1131db0e7fe83775cbea4a0d0b +github.com/hashicorp/go-syslog b609c7d9de4658cded34a7336b90886c56f9dbdb +github.com/lucas-clemente/quic-go 811315e31a0c190e7a9e86c84102e86c9ed2a072 +gopkg.in/natefinch/lumberjack.v2 a96e63847dc3c67d17befa69c303767e2f84e54f +github.com/codahale/aesnicheck 349fcc471aaccc29cd074e1275f1a494323826cd +github.com/xenolf/lego 4dde48a9b9916926a8dd4f69639c8dba40930355 +github.com/miekg/dns 0f3adef2e2201d72e50309a36fc99d8a9d1a4960 +gopkg.in/square/go-jose.v1 aa2e30fdd1fe9dd3394119af66451ae790d50e0d +github.com/hashicorp/golang-lru 0a025b7e63adc15a622f29b0b2c4c3848243bbf6 +github.com/lucas-clemente/aes12 25700e67be5c860bcc999137275b9ef8b65932bd +github.com/lucas-clemente/fnv128a 393af48d391698c6ae4219566bfbdfef67269997 +github.com/lucas-clemente/quic-go-certificates d2f86524cced5186554df90d92529757d22c1cb6 +github.com/dustin/go-humanize 259d2a102b871d17f30e3cd9881a642961a1e486 +github.com/jimstudt/http-authentication 3eca13d6893afd7ecabe15f4445f5d2872a1b012 +github.com/flynn/go-shlex 3f9db97f856818214da2e1057f8ad84803971cff github.com/BurntSushi/toml a368813c5e648fee92e5f6c30e3944ff9d5e8895 github.com/codegangsta/cli 4b90d79a682b4bf685762c7452db20f2a676ecb2