blob: 78dbda1a01110e7831542f1131b47bb550135146 [file] [log] [blame]
Serge Bazanski3fd70d82018-10-14 08:12:46 -07001package mirko
2
3import (
Serge Bazanskiaa81aa22018-10-14 08:36:05 -07004 "context"
Serge Bazanski3fd70d82018-10-14 08:12:46 -07005 "flag"
6 "fmt"
7 "net"
8 "net/http"
Serge Bazanski4f7ee512018-10-14 18:20:59 +01009 "os"
10 "os/signal"
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020011 "sort"
12 "strings"
Serge Bazanski3fd70d82018-10-14 08:12:46 -070013 "time"
14
Serge Bazanski3fd70d82018-10-14 08:12:46 -070015 "github.com/golang/glog"
Serge Bazanski3fd70d82018-10-14 08:12:46 -070016 "golang.org/x/net/trace"
17 "google.golang.org/grpc"
18 "google.golang.org/grpc/reflection"
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020019 "k8s.io/client-go/kubernetes"
20
21 "code.hackerspace.pl/hscloud/go/pki"
22 "code.hackerspace.pl/hscloud/go/statusz"
Serge Bazanski3fd70d82018-10-14 08:12:46 -070023)
24
25var (
26 flagListenAddress string
27 flagDebugAddress string
Serge Bazanski446c9e12018-10-14 17:06:09 +010028 flagDebugAllowAll bool
Serge Bazanski3fd70d82018-10-14 08:12:46 -070029)
30
31func init() {
Serge Bazanski69de9cb2018-10-14 08:49:04 -070032 flag.StringVar(&flagListenAddress, "listen_address", "127.0.0.1:4200", "gRPC listen address")
33 flag.StringVar(&flagDebugAddress, "debug_address", "127.0.0.1:4201", "HTTP debug/status listen address")
Serge Bazanskif77e7d32018-10-14 17:11:08 +010034 flag.BoolVar(&flagDebugAllowAll, "debug_allow_all", false, "HTTP debug/status available to everyone")
Serge Bazanskiaa81aa22018-10-14 08:36:05 -070035 flag.Set("logtostderr", "true")
Serge Bazanski3fd70d82018-10-14 08:12:46 -070036}
37
38type Mirko struct {
39 grpcListen net.Listener
40 grpcServer *grpc.Server
41 httpListen net.Listener
42 httpServer *http.Server
43 httpMux *http.ServeMux
Serge Bazanski4f7ee512018-10-14 18:20:59 +010044
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +020045 kubernetesCS *kubernetes.Clientset
46
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +020047 ctx context.Context
48 cancel context.CancelFunc
Serge Bazanski3fd70d82018-10-14 08:12:46 -070049}
50
51func New() *Mirko {
Serge Bazanski4f7ee512018-10-14 18:20:59 +010052 ctx, cancel := context.WithCancel(context.Background())
53 return &Mirko{
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +020054 ctx: ctx,
55 cancel: cancel,
Serge Bazanski4f7ee512018-10-14 18:20:59 +010056 }
Serge Bazanski3fd70d82018-10-14 08:12:46 -070057}
58
Serge Bazanski446c9e12018-10-14 17:06:09 +010059func authRequest(req *http.Request) (any, sensitive bool) {
60 host, _, err := net.SplitHostPort(req.RemoteAddr)
61 if err != nil {
62 host = req.RemoteAddr
63 }
64
65 if flagDebugAllowAll {
66 return true, true
67 }
68
69 switch host {
70 case "localhost", "127.0.0.1", "::1":
71 return true, true
72 default:
73 return false, false
74 }
75}
76
Serge Bazanski3fd70d82018-10-14 08:12:46 -070077func (m *Mirko) Listen() error {
78 grpc.EnableTracing = true
Serge Bazanski446c9e12018-10-14 17:06:09 +010079 trace.AuthRequest = authRequest
80
Serge Bazanski3fd70d82018-10-14 08:12:46 -070081 grpcLis, err := net.Listen("tcp", flagListenAddress)
82 if err != nil {
83 return fmt.Errorf("net.Listen: %v", err)
84 }
85 m.grpcListen = grpcLis
Serge Bazanskidd3d40f2018-10-25 12:14:18 +010086 m.grpcServer = grpc.NewServer(pki.WithServerHSPKI()...)
Serge Bazanski3fd70d82018-10-14 08:12:46 -070087 reflection.Register(m.grpcServer)
88
89 httpLis, err := net.Listen("tcp", flagDebugAddress)
90 if err != nil {
91 return fmt.Errorf("net.Listen: %v", err)
92 }
93
94 m.httpMux = http.NewServeMux()
95 // Canonical URLs
Serge Bazanski446c9e12018-10-14 17:06:09 +010096 m.httpMux.HandleFunc("/debug/status", func(w http.ResponseWriter, r *http.Request) {
Serge Bazanskibbb5b5f2018-10-14 17:13:16 +010097 any, _ := authRequest(r)
Serge Bazanski446c9e12018-10-14 17:06:09 +010098 if !any {
99 http.Error(w, "not allowed", http.StatusUnauthorized)
100 return
101 }
102 statusz.StatusHandler(w, r)
103 })
Serge Bazanski3fd70d82018-10-14 08:12:46 -0700104 m.httpMux.HandleFunc("/debug/requests", trace.Traces)
105
106 // -z legacy URLs
107 m.httpMux.HandleFunc("/statusz", func(w http.ResponseWriter, r *http.Request) {
108 http.Redirect(w, r, "/debug/status", http.StatusSeeOther)
109 })
110 m.httpMux.HandleFunc("/rpcz", func(w http.ResponseWriter, r *http.Request) {
111 http.Redirect(w, r, "/debug/requests", http.StatusSeeOther)
112 })
113 m.httpMux.HandleFunc("/requestz", func(w http.ResponseWriter, r *http.Request) {
114 http.Redirect(w, r, "/debug/requests", http.StatusSeeOther)
115 })
116
117 // root redirect
118 m.httpMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
119 http.Redirect(w, r, "/debug/status", http.StatusSeeOther)
120 })
121
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200122 m.kubernetesConnect()
123
124 debugParts := strings.Split(flagDebugAddress, ":")
125 debugPort := debugParts[len(debugParts)-1]
126 statusz.PublicAddress = fmt.Sprintf("http://%s:%s/", m.Address().String(), debugPort)
127
Serge Bazanski3fd70d82018-10-14 08:12:46 -0700128 m.httpListen = httpLis
129 m.httpServer = &http.Server{
130 Addr: flagDebugAddress,
131 Handler: m.httpMux,
132 }
133
134 return nil
135}
136
Serge Bazanski4f7ee512018-10-14 18:20:59 +0100137// Trace logs debug information to either a context trace (if present)
138// or stderr (if not)
Serge Bazanskic99e0b92018-10-14 18:03:48 +0100139func Trace(ctx context.Context, f string, args ...interface{}) {
Serge Bazanskiaa81aa22018-10-14 08:36:05 -0700140 tr, ok := trace.FromContext(ctx)
141 if !ok {
142 fmtd := fmt.Sprintf(f, args...)
143 glog.Warningf("No trace in %v: %s", ctx, fmtd)
144 return
145 }
146 tr.LazyPrintf(f, args...)
147}
148
Serge Bazanski4f7ee512018-10-14 18:20:59 +0100149// GRPC returns the microservice's grpc.Server object
Serge Bazanski3fd70d82018-10-14 08:12:46 -0700150func (m *Mirko) GRPC() *grpc.Server {
151 if m.grpcServer == nil {
152 panic("GRPC() called before Listen()")
153 }
154 return m.grpcServer
155}
156
Serge Bazanski4f7ee512018-10-14 18:20:59 +0100157// HTTPMux returns the microservice's debug HTTP mux
Serge Bazanski3fd70d82018-10-14 08:12:46 -0700158func (m *Mirko) HTTPMux() *http.ServeMux {
159 if m.httpMux == nil {
160 panic("HTTPMux() called before Listen()")
161 }
162 return m.httpMux
163}
164
Serge Bazanski4f7ee512018-10-14 18:20:59 +0100165// Context returns a background microservice context that will be canceled
166// when the service is shut down
167func (m *Mirko) Context() context.Context {
168 return m.ctx
169}
170
171// Done() returns a channel that will emit a value when the service is
172// shut down. This should be used in the main() function instead of a select{}
173// call, to allow the background context to be canceled fully.
Sergiusz Bazanski9dc4b682019-04-05 23:51:49 +0200174func (m *Mirko) Done() <-chan struct{} {
175 return m.Context().Done()
Serge Bazanski4f7ee512018-10-14 18:20:59 +0100176}
177
178// Serve starts serving HTTP and gRPC requests
Serge Bazanski3fd70d82018-10-14 08:12:46 -0700179func (m *Mirko) Serve() error {
180 errs := make(chan error, 1)
181 go func() {
182 if err := m.grpcServer.Serve(m.grpcListen); err != nil {
183 errs <- err
184 }
185 }()
186 go func() {
187 if err := m.httpServer.Serve(m.httpListen); err != nil {
188 errs <- err
189 }
190 }()
191
Serge Bazanski4f7ee512018-10-14 18:20:59 +0100192 signalCh := make(chan os.Signal, 1)
193 signal.Notify(signalCh, os.Interrupt)
194 go func() {
195 select {
196 case <-signalCh:
197 m.cancel()
Serge Bazanski4f7ee512018-10-14 18:20:59 +0100198 }
199 }()
200
Serge Bazanski3fd70d82018-10-14 08:12:46 -0700201 ticker := time.NewTicker(1 * time.Second)
202 select {
203 case <-ticker.C:
204 glog.Infof("gRPC listening on %s", flagListenAddress)
205 glog.Infof("HTTP listening on %s", flagDebugAddress)
206 return nil
207 case err := <-errs:
208 return err
209 }
210}
Sergiusz Bazanski8fe651b2019-07-21 23:50:05 +0200211
212// Address returns a linkable address where this service is running, sans port.
213// If running within kubernetes, this will return the pod IP.
214// Otherwise, this will guess the main, 'external' IP address of the machine it's running on.
215// On failures, returns loopback address.
216func (m *Mirko) Address() net.IP {
217 // If we're not running in Kubernetes and binding to 127.0.0.1, return loopback.
218 if m.kubernetesCS == nil && strings.HasPrefix(flagListenAddress, "127.0.0.1:") {
219 return net.ParseIP("127.0.0.1")
220 }
221
222 ifaces, err := net.Interfaces()
223 if err != nil {
224 glog.Errorf("net.Interface(): %v", err)
225 return net.ParseIP("127.0.0.1")
226 }
227
228 addrmap := make(map[string]net.IP)
229
230 for _, iface := range ifaces {
231 addrs, err := iface.Addrs()
232 if err != nil {
233 glog.Errorf("iface(%q).Addrs(): %v", iface.Name, err)
234 continue
235 }
236
237 for _, addr := range addrs {
238 var ip net.IP
239 switch v := addr.(type) {
240 case *net.IPNet:
241 ip = v.IP
242 case *net.IPAddr:
243 ip = v.IP
244 default:
245 continue
246 }
247
248 if strings.HasPrefix(ip.String(), "fe80:") {
249 continue
250 }
251 addrmap[iface.Name] = ip
252 }
253 }
254
255 if m.kubernetesCS != nil {
256 addr, ok := addrmap["eth0"]
257 if !ok {
258 glog.Errorf("Running on Kubernetes but no eth0! Available interfaces: %v", addrmap)
259 return net.ParseIP("127.0.0.1")
260 }
261
262 return addr
263 }
264
265 if len(addrmap) == 0 {
266 glog.Errorf("No interfaces found!")
267 return net.ParseIP("127.0.0.1")
268 }
269
270 // Heuristics ahoy!
271 prioritized := []*ifaceWithPriority{}
272 for iface, addr := range addrmap {
273 prio := &ifaceWithPriority{
274 iface: iface,
275 addr: addr,
276 }
277 switch {
278 case strings.HasPrefix(iface, "lo"):
279 prio.priority = -10
280 case strings.HasPrefix(iface, "tap"):
281 prio.priority = -5
282 case strings.HasPrefix(iface, "tun"):
283 prio.priority = -5
284 case strings.HasPrefix(iface, "veth"):
285 prio.priority = 5
286 case strings.HasPrefix(iface, "wl"):
287 prio.priority = 5
288 case strings.HasPrefix(iface, "enp"):
289 prio.priority = 10
290 case strings.HasPrefix(iface, "eth"):
291 prio.priority = 10
292 }
293
294 prioritized = append(prioritized, prio)
295 }
296
297 sort.Slice(prioritized, func(i, j int) bool { return prioritized[i].priority > prioritized[j].priority })
298 return prioritized[0].addr
299}
300
301type ifaceWithPriority struct {
302 iface string
303 addr net.IP
304 priority int
305}