13647: Count "failed to start services" as failure, not success.
[arvados.git] / lib / service / cmd.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: Apache-2.0
4
5 // package service provides a cmd.Handler that brings up a system service.
6 package service
7
8 import (
9         "context"
10         "flag"
11         "fmt"
12         "io"
13         "net"
14         "net/http"
15         "net/url"
16         "os"
17         "strings"
18
19         "git.curoverse.com/arvados.git/lib/cmd"
20         "git.curoverse.com/arvados.git/lib/config"
21         "git.curoverse.com/arvados.git/sdk/go/arvados"
22         "git.curoverse.com/arvados.git/sdk/go/ctxlog"
23         "git.curoverse.com/arvados.git/sdk/go/httpserver"
24         "github.com/coreos/go-systemd/daemon"
25         "github.com/prometheus/client_golang/prometheus"
26         "github.com/sirupsen/logrus"
27 )
28
29 type Handler interface {
30         http.Handler
31         CheckHealth() error
32 }
33
34 type NewHandlerFunc func(_ context.Context, _ *arvados.Cluster, token string, registry *prometheus.Registry) Handler
35
36 type command struct {
37         newHandler NewHandlerFunc
38         svcName    arvados.ServiceName
39         ctx        context.Context // enables tests to shutdown service; no public API yet
40 }
41
42 // Command returns a cmd.Handler that loads site config, calls
43 // newHandler with the current cluster and node configs, and brings up
44 // an http server with the returned handler.
45 //
46 // The handler is wrapped with server middleware (adding X-Request-ID
47 // headers, logging requests/responses, etc).
48 func Command(svcName arvados.ServiceName, newHandler NewHandlerFunc) cmd.Handler {
49         return &command{
50                 newHandler: newHandler,
51                 svcName:    svcName,
52                 ctx:        context.Background(),
53         }
54 }
55
56 func (c *command) RunCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
57         log := ctxlog.New(stderr, "json", "info")
58
59         var err error
60         defer func() {
61                 if err != nil {
62                         log.WithError(err).Info("exiting")
63                 }
64         }()
65
66         flags := flag.NewFlagSet("", flag.ContinueOnError)
67         flags.SetOutput(stderr)
68
69         loader := config.NewLoader(stdin, log)
70         loader.SetupFlags(flags)
71         versionFlag := flags.Bool("version", false, "Write version information to stdout and exit 0")
72         err = flags.Parse(args)
73         if err == flag.ErrHelp {
74                 err = nil
75                 return 0
76         } else if err != nil {
77                 return 2
78         } else if *versionFlag {
79                 return cmd.Version.RunCommand(prog, args, stdin, stdout, stderr)
80         }
81
82         cfg, err := loader.Load()
83         if err != nil {
84                 return 1
85         }
86         cluster, err := cfg.GetCluster("")
87         if err != nil {
88                 return 1
89         }
90
91         // Now that we've read the config, replace the bootstrap
92         // logger with a new one according to the logging config.
93         log = ctxlog.New(stderr, cluster.SystemLogs.Format, cluster.SystemLogs.LogLevel)
94         logger := log.WithFields(logrus.Fields{
95                 "PID": os.Getpid(),
96         })
97         ctx := ctxlog.Context(c.ctx, logger)
98
99         listenURL, err := getListenAddr(cluster.Services, c.svcName)
100         if err != nil {
101                 return 1
102         }
103         ctx = context.WithValue(ctx, contextKeyURL{}, listenURL)
104
105         if cluster.SystemRootToken == "" {
106                 logger.Warn("SystemRootToken missing from cluster config, falling back to ARVADOS_API_TOKEN environment variable")
107                 cluster.SystemRootToken = os.Getenv("ARVADOS_API_TOKEN")
108         }
109         if cluster.Services.Controller.ExternalURL.Host == "" {
110                 logger.Warn("Services.Controller.ExternalURL missing from cluster config, falling back to ARVADOS_API_HOST(_INSECURE) environment variables")
111                 u, err := url.Parse("https://" + os.Getenv("ARVADOS_API_HOST"))
112                 if err != nil {
113                         err = fmt.Errorf("ARVADOS_API_HOST: %s", err)
114                         return 1
115                 }
116                 cluster.Services.Controller.ExternalURL = arvados.URL(*u)
117                 if i := os.Getenv("ARVADOS_API_HOST_INSECURE"); i != "" && i != "0" {
118                         cluster.TLS.Insecure = true
119                 }
120         }
121
122         reg := prometheus.NewRegistry()
123         handler := c.newHandler(ctx, cluster, cluster.SystemRootToken, reg)
124         if err = handler.CheckHealth(); err != nil {
125                 return 1
126         }
127
128         instrumented := httpserver.Instrument(reg, log,
129                 httpserver.HandlerWithContext(ctx,
130                         httpserver.AddRequestIDs(
131                                 httpserver.LogRequests(
132                                         httpserver.NewRequestLimiter(cluster.API.MaxConcurrentRequests, handler, reg)))))
133         srv := &httpserver.Server{
134                 Server: http.Server{
135                         Handler: instrumented.ServeAPI(cluster.ManagementToken, instrumented),
136                 },
137                 Addr: listenURL.Host,
138         }
139         if listenURL.Scheme == "https" {
140                 tlsconfig, err := tlsConfigWithCertUpdater(cluster, logger)
141                 if err != nil {
142                         logger.WithError(err).Errorf("cannot start %s service on %s", c.svcName, listenURL.String())
143                         return 1
144                 }
145                 srv.TLSConfig = tlsconfig
146         }
147         err = srv.Start()
148         if err != nil {
149                 return 1
150         }
151         logger.WithFields(logrus.Fields{
152                 "URL":     listenURL,
153                 "Listen":  srv.Addr,
154                 "Service": c.svcName,
155         }).Info("listening")
156         if _, err := daemon.SdNotify(false, "READY=1"); err != nil {
157                 logger.WithError(err).Errorf("error notifying init daemon")
158         }
159         go func() {
160                 <-ctx.Done()
161                 srv.Close()
162         }()
163         err = srv.Wait()
164         if err != nil {
165                 return 1
166         }
167         return 0
168 }
169
170 const rfc3339NanoFixed = "2006-01-02T15:04:05.000000000Z07:00"
171
172 func getListenAddr(svcs arvados.Services, prog arvados.ServiceName) (arvados.URL, error) {
173         svc, ok := svcs.Map()[prog]
174         if !ok {
175                 return arvados.URL{}, fmt.Errorf("unknown service name %q", prog)
176         }
177         for url := range svc.InternalURLs {
178                 if strings.HasPrefix(url.Host, "localhost:") {
179                         return url, nil
180                 }
181                 listener, err := net.Listen("tcp", url.Host)
182                 if err == nil {
183                         listener.Close()
184                         return url, nil
185                 }
186         }
187         return arvados.URL{}, fmt.Errorf("configuration does not enable the %s service on this host", prog)
188 }
189
190 type contextKeyURL struct{}
191
192 func URLFromContext(ctx context.Context) (arvados.URL, bool) {
193         u, ok := ctx.Value(contextKeyURL{}).(arvados.URL)
194         return u, ok
195 }