github.com/replicatedhq/ship@v0.55.0/pkg/lifecycle/daemon/daemon.go (about) 1 package daemon 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "io" 8 "net/http" 9 "sync" 10 "time" 11 12 "github.com/gin-contrib/cors" 13 "github.com/gin-gonic/contrib/static" 14 "github.com/gin-gonic/gin" 15 "github.com/go-kit/kit/log" 16 "github.com/go-kit/kit/log/level" 17 "github.com/pkg/errors" 18 "github.com/replicatedhq/ship/pkg/api" 19 "github.com/replicatedhq/ship/pkg/lifecycle/daemon/daemontypes" 20 "github.com/replicatedhq/ship/pkg/version" 21 "github.com/spf13/viper" 22 ) 23 24 var ( 25 errInternal = errors.New("internal_error") 26 ) 27 28 var _ daemontypes.Daemon = &ShipDaemon{} 29 30 // Daemon runs the ship api server. 31 type ShipDaemon struct { 32 Logger log.Logger 33 WebUIFactory WebUIBuilder 34 Viper *viper.Viper 35 // todo private this 36 ExitChan chan error 37 StartOnce sync.Once 38 39 *V1Routes 40 *NavcycleRoutes 41 } 42 43 func (d *ShipDaemon) AwaitShutdown() error { 44 return <-d.ExitChan 45 } 46 47 // "this is fine" 48 func (d *ShipDaemon) EnsureStarted(ctx context.Context, release *api.Release) chan error { 49 50 go d.StartOnce.Do(func() { 51 err := d.Serve(ctx, release) 52 level.Info(d.Logger).Log("event", "daemon.startonce.exit", err, "err") 53 d.ExitChan <- err 54 }) 55 56 return d.ExitChan 57 } 58 59 // Serve starts the server with the given context 60 func (d *ShipDaemon) Serve(ctx context.Context, release *api.Release) error { 61 debug := level.Debug(log.With(d.Logger, "method", "serve")) 62 config := cors.DefaultConfig() 63 config.AllowMethods = append(config.AllowMethods, "DELETE") 64 config.AllowAllOrigins = true 65 66 g := gin.New() 67 68 logWriter := loggerWriter(d.Logger) 69 g.Use(gin.LoggerWithWriter(logWriter)) 70 71 g.Use(cors.New(config)) 72 73 debug.Log("event", "routes.configure") 74 d.configureRoutes(g, release) 75 76 apiPort := viper.GetInt("api-port") 77 addr := fmt.Sprintf(":%d", apiPort) 78 server := http.Server{Addr: addr, Handler: g} 79 errChan := make(chan error) 80 81 go func() { 82 debug.Log("event", "server.listen", "server.addr", addr) 83 errChan <- server.ListenAndServe() 84 }() 85 86 openURL := fmt.Sprintf("http://localhost:%d", apiPort) 87 if !d.Viper.GetBool("no-open") { 88 go func() { 89 autoOpen := d.Viper.GetBool("headed") 90 err := d.OpenWebConsole(d.UI, openURL, autoOpen) 91 if err != nil { 92 debug.Log("event", "console.open.fail.ignore", "err", err) 93 } 94 }() 95 } else { 96 d.UI.Info(fmt.Sprintf( 97 "\nPlease visit the following URL in your browser to continue the installation\n\n %s\n\n ", 98 openURL, 99 )) 100 } 101 102 defer func() { 103 debug.Log("event", "server.shutdown") 104 shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) 105 defer cancel() 106 _ = server.Shutdown(shutdownCtx) 107 }() 108 109 select { 110 case err := <-errChan: 111 level.Error(d.Logger).Log("event", "shutdown", "reason", "ExitChan", "err", err) 112 return err 113 case <-ctx.Done(): 114 level.Error(d.Logger).Log("event", "shutdown", "reason", "context", "err", ctx.Err()) 115 return ctx.Err() 116 case _, shouldContinue := <-d.NavcycleRoutes.Shutdown: 117 debug.Log("event", "shutdown.requested", "shouldContinue", shouldContinue) 118 d.UI.Output("Shutting down...") 119 return nil 120 } 121 } 122 123 func (d *ShipDaemon) configureRoutes(g *gin.Engine, release *api.Release) { 124 125 root := g.Group("/") 126 g.Use(static.Serve("/", d.WebUIFactory("build"))) 127 g.NoRoute(func(c *gin.Context) { 128 debug := level.Debug(log.With(d.Logger, "handler", "NoRoute")) 129 debug.Log("event", "not found") 130 if c.Request.URL != nil { 131 path := c.Request.URL.Path 132 static.Serve(path, d.WebUIFactory("build"))(c) 133 134 } 135 static.Serve("/", d.WebUIFactory("build"))(c) 136 }) 137 138 root.GET("/healthz", d.Healthz) 139 root.GET("/metricz", d.Metricz) 140 141 if d.V1Routes != nil { 142 d.V1Routes.Register(root, release) 143 } 144 145 if d.NavcycleRoutes != nil { 146 d.NavcycleRoutes.Register(root, release) 147 } 148 } 149 150 // Healthz returns a 200 with the version if provided 151 func (d *ShipDaemon) Healthz(c *gin.Context) { 152 c.JSON(200, map[string]interface{}{ 153 "version": version.Version(), 154 "sha": version.GitSHA(), 155 "buildTime": version.BuildTime(), 156 }) 157 } 158 159 // Metricz returns (empty) metrics for this server 160 func (d *ShipDaemon) Metricz(c *gin.Context) { 161 type Metric struct { 162 M1 float64 `json:"m1"` 163 P95 float64 `json:"p95"` 164 } 165 c.IndentedJSON(200, map[string]Metric{}) 166 } 167 168 func loggerWriter(ginLog log.Logger) *io.PipeWriter { 169 reader, writer := io.Pipe() 170 bufReader := bufio.NewReader(reader) 171 172 go func(bufReader *bufio.Reader, ginLog log.Logger) { 173 for { 174 line, err := bufReader.ReadString('\n') 175 level.Info(ginLog).Log("event", "gin.log", "line", line) 176 if err != nil { 177 level.Error(ginLog).Log("event", "gin.log", "err", err) 178 return 179 } 180 } 181 }(bufReader, ginLog) 182 183 return writer 184 }