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  }