| package mirko |
| |
| import ( |
| "context" |
| "flag" |
| "fmt" |
| "net" |
| "net/http" |
| "net/http/pprof" |
| "os" |
| "os/signal" |
| "sort" |
| "strings" |
| "time" |
| |
| "github.com/golang/glog" |
| "golang.org/x/net/trace" |
| "google.golang.org/grpc" |
| "google.golang.org/grpc/reflection" |
| "k8s.io/client-go/kubernetes" |
| |
| "code.hackerspace.pl/hscloud/go/pki" |
| "code.hackerspace.pl/hscloud/go/statusz" |
| ) |
| |
| var ( |
| flagListenAddress string |
| flagDebugAddress string |
| flagDebugAllowAll bool |
| ) |
| |
| func init() { |
| flag.StringVar(&flagListenAddress, "listen_address", "127.0.0.1:4200", "gRPC listen address") |
| flag.StringVar(&flagDebugAddress, "debug_address", "127.0.0.1:4201", "HTTP debug/status listen address") |
| flag.BoolVar(&flagDebugAllowAll, "debug_allow_all", false, "HTTP debug/status available to everyone") |
| flag.Set("logtostderr", "true") |
| } |
| |
| type Mirko struct { |
| grpcListen net.Listener |
| grpcServer *grpc.Server |
| httpListen net.Listener |
| httpServer *http.Server |
| httpMux *http.ServeMux |
| |
| kubernetesCS *kubernetes.Clientset |
| |
| ctx context.Context |
| cancel context.CancelFunc |
| } |
| |
| func New() *Mirko { |
| ctx, cancel := context.WithCancel(context.Background()) |
| return &Mirko{ |
| ctx: ctx, |
| cancel: cancel, |
| } |
| } |
| |
| func authRequest(req *http.Request) (any, sensitive bool) { |
| host, _, err := net.SplitHostPort(req.RemoteAddr) |
| if err != nil { |
| host = req.RemoteAddr |
| } |
| |
| if flagDebugAllowAll { |
| return true, true |
| } |
| |
| switch host { |
| case "localhost", "127.0.0.1", "::1": |
| return true, true |
| default: |
| return false, false |
| } |
| } |
| |
| func (m *Mirko) Listen() error { |
| grpc.EnableTracing = true |
| trace.AuthRequest = authRequest |
| |
| grpcLis, err := net.Listen("tcp", flagListenAddress) |
| if err != nil { |
| return fmt.Errorf("net.Listen: %v", err) |
| } |
| m.grpcListen = grpcLis |
| m.grpcServer = grpc.NewServer(pki.WithServerHSPKI()...) |
| reflection.Register(m.grpcServer) |
| |
| httpLis, err := net.Listen("tcp", flagDebugAddress) |
| if err != nil { |
| return fmt.Errorf("net.Listen: %v", err) |
| } |
| |
| m.httpMux = http.NewServeMux() |
| // Canonical URLs |
| m.httpMux.HandleFunc("/debug/status", func(w http.ResponseWriter, r *http.Request) { |
| any, _ := authRequest(r) |
| if !any { |
| http.Error(w, "not allowed", http.StatusUnauthorized) |
| return |
| } |
| statusz.StatusHandler(w, r) |
| }) |
| m.httpMux.HandleFunc("/debug/requests", trace.Traces) |
| |
| // pprof endpoints |
| m.httpMux.HandleFunc("/debug/pprof/", pprof.Index) |
| m.httpMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) |
| m.httpMux.HandleFunc("/debug/pprof/profile", pprof.Profile) |
| m.httpMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) |
| m.httpMux.HandleFunc("/debug/pprof/trace", pprof.Trace) |
| |
| // -z legacy URLs |
| m.httpMux.HandleFunc("/statusz", func(w http.ResponseWriter, r *http.Request) { |
| http.Redirect(w, r, "/debug/status", http.StatusSeeOther) |
| }) |
| m.httpMux.HandleFunc("/rpcz", func(w http.ResponseWriter, r *http.Request) { |
| http.Redirect(w, r, "/debug/requests", http.StatusSeeOther) |
| }) |
| m.httpMux.HandleFunc("/requestz", func(w http.ResponseWriter, r *http.Request) { |
| http.Redirect(w, r, "/debug/requests", http.StatusSeeOther) |
| }) |
| m.httpMux.HandleFunc("/profilez", func(w http.ResponseWriter, r *http.Request) { |
| http.Redirect(w, r, "/debug/pprof", http.StatusSeeOther) |
| }) |
| |
| // root redirect |
| m.httpMux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { |
| http.Redirect(w, r, "/debug/status", http.StatusSeeOther) |
| }) |
| |
| m.kubernetesCS = KubernetesClient() |
| |
| debugParts := strings.Split(flagDebugAddress, ":") |
| debugPort := debugParts[len(debugParts)-1] |
| statusz.PublicAddress = fmt.Sprintf("http://%s:%s/", m.Address().String(), debugPort) |
| |
| m.httpListen = httpLis |
| m.httpServer = &http.Server{ |
| Addr: flagDebugAddress, |
| Handler: m.httpMux, |
| } |
| |
| return nil |
| } |
| |
| // Trace logs debug information to either a context trace (if present) |
| // or stderr (if not) |
| func Trace(ctx context.Context, f string, args ...interface{}) { |
| tr, ok := trace.FromContext(ctx) |
| if !ok { |
| fmtd := fmt.Sprintf(f, args...) |
| glog.Warningf("No trace in %v: %s", ctx, fmtd) |
| return |
| } |
| tr.LazyPrintf(f, args...) |
| } |
| |
| // GRPC returns the microservice's grpc.Server object |
| func (m *Mirko) GRPC() *grpc.Server { |
| if m.grpcServer == nil { |
| panic("GRPC() called before Listen()") |
| } |
| return m.grpcServer |
| } |
| |
| // HTTPMux returns the microservice's debug HTTP mux |
| func (m *Mirko) HTTPMux() *http.ServeMux { |
| if m.httpMux == nil { |
| panic("HTTPMux() called before Listen()") |
| } |
| return m.httpMux |
| } |
| |
| // Context returns a background microservice context that will be canceled |
| // when the service is shut down |
| func (m *Mirko) Context() context.Context { |
| return m.ctx |
| } |
| |
| // Done() returns a channel that will emit a value when the service is |
| // shut down. This should be used in the main() function instead of a select{} |
| // call, to allow the background context to be canceled fully. |
| func (m *Mirko) Done() <-chan struct{} { |
| return m.Context().Done() |
| } |
| |
| // Serve starts serving HTTP and gRPC requests |
| func (m *Mirko) Serve() error { |
| errs := make(chan error, 1) |
| go func() { |
| if err := m.grpcServer.Serve(m.grpcListen); err != nil { |
| errs <- err |
| } |
| }() |
| go func() { |
| if err := m.httpServer.Serve(m.httpListen); err != nil { |
| errs <- err |
| } |
| }() |
| |
| signalCh := make(chan os.Signal, 1) |
| signal.Notify(signalCh, os.Interrupt) |
| go func() { |
| select { |
| case <-signalCh: |
| m.cancel() |
| } |
| }() |
| |
| ticker := time.NewTicker(1 * time.Second) |
| select { |
| case <-ticker.C: |
| glog.Infof("gRPC listening on %s", flagListenAddress) |
| glog.Infof("HTTP listening on %s", flagDebugAddress) |
| return nil |
| case err := <-errs: |
| return err |
| } |
| } |
| |
| // Address returns a linkable address where this service is running, sans port. |
| // If running within kubernetes, this will return the pod IP. |
| // Otherwise, this will guess the main, 'external' IP address of the machine it's running on. |
| // On failures, returns loopback address. |
| func (m *Mirko) Address() net.IP { |
| // If we're not running in Kubernetes and binding to 127.0.0.1, return loopback. |
| if m.kubernetesCS == nil && strings.HasPrefix(flagListenAddress, "127.0.0.1:") { |
| return net.ParseIP("127.0.0.1") |
| } |
| |
| ifaces, err := net.Interfaces() |
| if err != nil { |
| glog.Errorf("net.Interface(): %v", err) |
| return net.ParseIP("127.0.0.1") |
| } |
| |
| addrmap := make(map[string]net.IP) |
| |
| for _, iface := range ifaces { |
| addrs, err := iface.Addrs() |
| if err != nil { |
| glog.Errorf("iface(%q).Addrs(): %v", iface.Name, err) |
| continue |
| } |
| |
| for _, addr := range addrs { |
| var ip net.IP |
| switch v := addr.(type) { |
| case *net.IPNet: |
| ip = v.IP |
| case *net.IPAddr: |
| ip = v.IP |
| default: |
| continue |
| } |
| |
| if strings.HasPrefix(ip.String(), "fe80:") { |
| continue |
| } |
| addrmap[iface.Name] = ip |
| } |
| } |
| |
| if m.kubernetesCS != nil { |
| addr, ok := addrmap["eth0"] |
| if !ok { |
| glog.Errorf("Running on Kubernetes but no eth0! Available interfaces: %v", addrmap) |
| return net.ParseIP("127.0.0.1") |
| } |
| |
| return addr |
| } |
| |
| if len(addrmap) == 0 { |
| glog.Errorf("No interfaces found!") |
| return net.ParseIP("127.0.0.1") |
| } |
| |
| // Heuristics ahoy! |
| prioritized := []*ifaceWithPriority{} |
| for iface, addr := range addrmap { |
| prio := &ifaceWithPriority{ |
| iface: iface, |
| addr: addr, |
| } |
| switch { |
| case strings.HasPrefix(iface, "lo"): |
| prio.priority = -10 |
| case strings.HasPrefix(iface, "tap"): |
| prio.priority = -5 |
| case strings.HasPrefix(iface, "tun"): |
| prio.priority = -5 |
| case strings.HasPrefix(iface, "veth"): |
| prio.priority = 5 |
| case strings.HasPrefix(iface, "wl"): |
| prio.priority = 5 |
| case strings.HasPrefix(iface, "enp"): |
| prio.priority = 10 |
| case strings.HasPrefix(iface, "eth"): |
| prio.priority = 10 |
| } |
| |
| prioritized = append(prioritized, prio) |
| } |
| |
| sort.Slice(prioritized, func(i, j int) bool { return prioritized[i].priority > prioritized[j].priority }) |
| return prioritized[0].addr |
| } |
| |
| type ifaceWithPriority struct { |
| iface string |
| addr net.IP |
| priority int |
| } |