Merge branch '20688-wb1-to-wb2-redirects' refs #20688
[arvados.git] / services / keepstore / command.go
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 package keepstore
6
7 import (
8         "context"
9         "errors"
10         "flag"
11         "fmt"
12         "io"
13         "math/rand"
14         "net/http"
15         "os"
16         "sync"
17
18         "git.arvados.org/arvados.git/lib/cmd"
19         "git.arvados.org/arvados.git/lib/config"
20         "git.arvados.org/arvados.git/lib/service"
21         "git.arvados.org/arvados.git/sdk/go/arvados"
22         "git.arvados.org/arvados.git/sdk/go/arvadosclient"
23         "git.arvados.org/arvados.git/sdk/go/ctxlog"
24         "git.arvados.org/arvados.git/sdk/go/keepclient"
25         "github.com/prometheus/client_golang/prometheus"
26         "github.com/sirupsen/logrus"
27 )
28
29 var (
30         Command = service.Command(arvados.ServiceNameKeepstore, newHandlerOrErrorHandler)
31 )
32
33 func runCommand(prog string, args []string, stdin io.Reader, stdout, stderr io.Writer) int {
34         args, ok, code := convertKeepstoreFlagsToServiceFlags(prog, args, ctxlog.FromContext(context.Background()), stderr)
35         if !ok {
36                 return code
37         }
38         return Command.RunCommand(prog, args, stdin, stdout, stderr)
39 }
40
41 // Parse keepstore command line flags, and return equivalent
42 // service.Command flags. If the second return value ("ok") is false,
43 // the program should exit, and the third return value is a suitable
44 // exit code.
45 func convertKeepstoreFlagsToServiceFlags(prog string, args []string, lgr logrus.FieldLogger, stderr io.Writer) ([]string, bool, int) {
46         flags := flag.NewFlagSet("", flag.ContinueOnError)
47         flags.String("listen", "", "Services.Keepstore.InternalURLs")
48         flags.Int("max-buffers", 0, "API.MaxKeepBlobBuffers")
49         flags.Int("max-requests", 0, "API.MaxConcurrentRequests")
50         flags.Bool("never-delete", false, "Collections.BlobTrash")
51         flags.Bool("enforce-permissions", false, "Collections.BlobSigning")
52         flags.String("permission-key-file", "", "Collections.BlobSigningKey")
53         flags.String("blob-signing-key-file", "", "Collections.BlobSigningKey")
54         flags.String("data-manager-token-file", "", "SystemRootToken")
55         flags.Int("permission-ttl", 0, "Collections.BlobSigningTTL")
56         flags.Int("blob-signature-ttl", 0, "Collections.BlobSigningTTL")
57         flags.String("trash-lifetime", "", "Collections.BlobTrashLifetime")
58         flags.Bool("serialize", false, "Volumes.*.DriverParameters.Serialize")
59         flags.Bool("readonly", false, "Volumes.*.ReadOnly")
60         flags.String("pid", "", "-")
61         flags.String("trash-check-interval", "", "Collections.BlobTrashCheckInterval")
62
63         flags.String("azure-storage-container-volume", "", "Volumes.*.Driver")
64         flags.String("azure-storage-account-name", "", "Volumes.*.DriverParameters.StorageAccountName")
65         flags.String("azure-storage-account-key-file", "", "Volumes.*.DriverParameters.StorageAccountKey")
66         flags.String("azure-storage-replication", "", "Volumes.*.Replication")
67         flags.String("azure-max-get-bytes", "", "Volumes.*.DriverParameters.MaxDataReadSize")
68
69         flags.String("s3-bucket-volume", "", "Volumes.*.DriverParameters.Bucket")
70         flags.String("s3-region", "", "Volumes.*.DriverParameters.Region")
71         flags.String("s3-endpoint", "", "Volumes.*.DriverParameters.Endpoint")
72         flags.String("s3-access-key-file", "", "Volumes.*.DriverParameters.AccessKeyID")
73         flags.String("s3-secret-key-file", "", "Volumes.*.DriverParameters.SecretAccessKey")
74         flags.String("s3-race-window", "", "Volumes.*.DriverParameters.RaceWindow")
75         flags.String("s3-replication", "", "Volumes.*.Replication")
76         flags.String("s3-unsafe-delete", "", "Volumes.*.DriverParameters.UnsafeDelete")
77
78         flags.String("volume", "", "Volumes")
79
80         flags.Bool("version", false, "")
81         flags.String("config", "", "")
82         flags.String("legacy-keepstore-config", "", "")
83
84         if ok, code := cmd.ParseFlags(flags, prog, args, "", stderr); !ok {
85                 return nil, false, code
86         }
87
88         args = nil
89         ok := true
90         flags.Visit(func(f *flag.Flag) {
91                 if f.Name == "config" || f.Name == "legacy-keepstore-config" || f.Name == "version" {
92                         args = append(args, "-"+f.Name, f.Value.String())
93                 } else if f.Usage == "-" {
94                         ok = false
95                         lgr.Errorf("command line flag -%s is no longer supported", f.Name)
96                 } else {
97                         ok = false
98                         lgr.Errorf("command line flag -%s is no longer supported -- use Clusters.*.%s in cluster config file instead", f.Name, f.Usage)
99                 }
100         })
101         if !ok {
102                 return nil, false, 2
103         }
104
105         flags = flag.NewFlagSet("", flag.ContinueOnError)
106         loader := config.NewLoader(nil, lgr)
107         loader.SetupFlags(flags)
108         return loader.MungeLegacyConfigArgs(lgr, args, "-legacy-keepstore-config"), true, 0
109 }
110
111 type handler struct {
112         http.Handler
113         Cluster *arvados.Cluster
114         Logger  logrus.FieldLogger
115
116         pullq      *WorkQueue
117         trashq     *WorkQueue
118         volmgr     *RRVolumeManager
119         keepClient *keepclient.KeepClient
120
121         err       error
122         setupOnce sync.Once
123 }
124
125 func (h *handler) CheckHealth() error {
126         return h.err
127 }
128
129 func (h *handler) Done() <-chan struct{} {
130         return nil
131 }
132
133 func newHandlerOrErrorHandler(ctx context.Context, cluster *arvados.Cluster, token string, reg *prometheus.Registry) service.Handler {
134         var h handler
135         serviceURL, ok := service.URLFromContext(ctx)
136         if !ok {
137                 return service.ErrorHandler(ctx, cluster, errors.New("BUG: no URL from service.URLFromContext"))
138         }
139         err := h.setup(ctx, cluster, token, reg, serviceURL)
140         if err != nil {
141                 return service.ErrorHandler(ctx, cluster, err)
142         }
143         return &h
144 }
145
146 func (h *handler) setup(ctx context.Context, cluster *arvados.Cluster, token string, reg *prometheus.Registry, serviceURL arvados.URL) error {
147         h.Cluster = cluster
148         h.Logger = ctxlog.FromContext(ctx)
149         if h.Cluster.API.MaxKeepBlobBuffers <= 0 {
150                 return fmt.Errorf("API.MaxKeepBlobBuffers must be greater than zero")
151         }
152         bufs = newBufferPool(h.Logger, h.Cluster.API.MaxKeepBlobBuffers, BlockSize)
153
154         if h.Cluster.API.MaxConcurrentRequests > 0 && h.Cluster.API.MaxConcurrentRequests < h.Cluster.API.MaxKeepBlobBuffers {
155                 h.Logger.Warnf("Possible configuration mistake: not useful to set API.MaxKeepBlobBuffers (%d) higher than API.MaxConcurrentRequests (%d)", h.Cluster.API.MaxKeepBlobBuffers, h.Cluster.API.MaxConcurrentRequests)
156         }
157
158         if h.Cluster.Collections.BlobSigningKey != "" {
159         } else if h.Cluster.Collections.BlobSigning {
160                 return errors.New("cannot enable Collections.BlobSigning with no Collections.BlobSigningKey")
161         } else {
162                 h.Logger.Warn("Running without a blob signing key. Block locators returned by this server will not be signed, and will be rejected by a server that enforces permissions. To fix this, configure Collections.BlobSigning and Collections.BlobSigningKey.")
163         }
164
165         if len(h.Cluster.Volumes) == 0 {
166                 return errors.New("no volumes configured")
167         }
168
169         h.Logger.Printf("keepstore %s starting, pid %d", cmd.Version.String(), os.Getpid())
170
171         // Start a round-robin VolumeManager with the configured volumes.
172         vm, err := makeRRVolumeManager(h.Logger, h.Cluster, serviceURL, newVolumeMetricsVecs(reg))
173         if err != nil {
174                 return err
175         }
176         if len(vm.readables) == 0 {
177                 return fmt.Errorf("no volumes configured for %s", serviceURL)
178         }
179         h.volmgr = vm
180
181         // Initialize the pullq and workers
182         h.pullq = NewWorkQueue()
183         for i := 0; i < 1 || i < h.Cluster.Collections.BlobReplicateConcurrency; i++ {
184                 go h.runPullWorker(h.pullq)
185         }
186
187         // Initialize the trashq and workers
188         h.trashq = NewWorkQueue()
189         for i := 0; i < 1 || i < h.Cluster.Collections.BlobTrashConcurrency; i++ {
190                 go RunTrashWorker(h.volmgr, h.Logger, h.Cluster, h.trashq)
191         }
192
193         // Set up routes and metrics
194         h.Handler = MakeRESTRouter(ctx, cluster, reg, vm, h.pullq, h.trashq)
195
196         // Initialize keepclient for pull workers
197         c, err := arvados.NewClientFromConfig(cluster)
198         if err != nil {
199                 return err
200         }
201         ac, err := arvadosclient.New(c)
202         if err != nil {
203                 return err
204         }
205         h.keepClient = &keepclient.KeepClient{
206                 Arvados:       ac,
207                 Want_replicas: 1,
208         }
209         h.keepClient.Arvados.ApiToken = fmt.Sprintf("%x", rand.Int63())
210
211         if d := h.Cluster.Collections.BlobTrashCheckInterval.Duration(); d > 0 {
212                 go emptyTrash(h.volmgr.writables, d)
213         }
214
215         return nil
216 }