1
- //go:build !slim
2
-
3
1
package cli
4
2
5
- import (
6
- "context"
7
- "errors"
8
- "fmt"
9
- "net/http"
10
- "os"
11
- "regexp"
12
- "time"
13
-
14
- "github.com/google/uuid"
15
- "github.com/prometheus/client_golang/prometheus"
16
- "github.com/prometheus/client_golang/prometheus/collectors"
17
- "github.com/prometheus/client_golang/prometheus/promhttp"
18
- "golang.org/x/xerrors"
19
-
20
- "cdr.dev/slog"
21
- "cdr.dev/slog/sloggers/sloghuman"
22
- agpl "github.com/coder/coder/v2/cli"
23
- "github.com/coder/coder/v2/cli/clilog"
24
- "github.com/coder/coder/v2/cli/cliui"
25
- "github.com/coder/coder/v2/cli/cliutil"
26
- "github.com/coder/coder/v2/coderd/database"
27
- "github.com/coder/coder/v2/codersdk"
28
- "github.com/coder/coder/v2/codersdk/drpc"
29
- "github.com/coder/coder/v2/provisioner/terraform"
30
- "github.com/coder/coder/v2/provisionerd"
31
- provisionerdproto "github.com/coder/coder/v2/provisionerd/proto"
32
- "github.com/coder/coder/v2/provisionersdk"
33
- "github.com/coder/coder/v2/provisionersdk/proto"
34
- "github.com/coder/serpent"
35
- )
3
+ import "github.com/coder/serpent"
36
4
37
5
func (r * RootCmd ) provisionerDaemons () * serpent.Command {
38
6
cmd := & serpent.Command {
@@ -50,337 +18,3 @@ func (r *RootCmd) provisionerDaemons() *serpent.Command {
50
18
51
19
return cmd
52
20
}
53
-
54
- func validateProvisionerDaemonName (name string ) error {
55
- if len (name ) > 64 {
56
- return xerrors .Errorf ("name cannot be greater than 64 characters in length" )
57
- }
58
- if ok , err := regexp .MatchString (`^[a-zA-Z0-9][a-zA-Z0-9-]{0,61}[a-zA-Z0-9]$` , name ); err != nil || ! ok {
59
- return xerrors .Errorf ("name %q is not a valid hostname" , name )
60
- }
61
- return nil
62
- }
63
-
64
- func (r * RootCmd ) provisionerDaemonStart () * serpent.Command {
65
- var (
66
- cacheDir string
67
- logHuman string
68
- logJSON string
69
- logStackdriver string
70
- logFilter []string
71
- name string
72
- rawTags []string
73
- pollInterval time.Duration
74
- pollJitter time.Duration
75
- preSharedKey string
76
- verbose bool
77
-
78
- prometheusEnable bool
79
- prometheusAddress string
80
- )
81
- orgContext := agpl .NewOrganizationContext ()
82
- client := new (codersdk.Client )
83
- cmd := & serpent.Command {
84
- Use : "start" ,
85
- Short : "Run a provisioner daemon" ,
86
- Middleware : serpent .Chain (
87
- // disable checks and warnings because this command starts a daemon; it is
88
- // not meant for humans typing commands. Furthermore, the checks are
89
- // incompatible with PSK auth that this command uses
90
- r .InitClient (client ),
91
- ),
92
- Handler : func (inv * serpent.Invocation ) error {
93
- ctx , cancel := context .WithCancel (inv .Context ())
94
- defer cancel ()
95
-
96
- stopCtx , stopCancel := inv .SignalNotifyContext (ctx , agpl .StopSignalsNoInterrupt ... )
97
- defer stopCancel ()
98
- interruptCtx , interruptCancel := inv .SignalNotifyContext (ctx , agpl .InterruptSignals ... )
99
- defer interruptCancel ()
100
-
101
- // This can fail to get the current organization
102
- // if the client is not authenticated as a user,
103
- // like when only PSK is provided.
104
- // This will be cleaner once PSK is replaced
105
- // with org scoped authentication tokens.
106
- org , err := orgContext .Selected (inv , client )
107
- if err != nil {
108
- var cErr * codersdk.Error
109
- if ! errors .As (err , & cErr ) || cErr .StatusCode () != http .StatusUnauthorized {
110
- return xerrors .Errorf ("current organization: %w" , err )
111
- }
112
-
113
- if preSharedKey == "" {
114
- return xerrors .New ("must provide a pre-shared key when not authenticated as a user" )
115
- }
116
-
117
- org = codersdk.Organization {MinimalOrganization : codersdk.MinimalOrganization {ID : uuid .Nil }}
118
- if orgContext .FlagSelect != "" {
119
- // If we are using PSK, we can't fetch the organization
120
- // to validate org name so we need the user to provide
121
- // a valid organization ID.
122
- orgID , err := uuid .Parse (orgContext .FlagSelect )
123
- if err != nil {
124
- return xerrors .New ("must provide an org ID when not authenticated as a user and organization is specified" )
125
- }
126
- org = codersdk.Organization {MinimalOrganization : codersdk.MinimalOrganization {ID : orgID }}
127
- }
128
- }
129
-
130
- tags , err := agpl .ParseProvisionerTags (rawTags )
131
- if err != nil {
132
- return err
133
- }
134
-
135
- if name == "" {
136
- name = cliutil .Hostname ()
137
- }
138
-
139
- if err := validateProvisionerDaemonName (name ); err != nil {
140
- return err
141
- }
142
-
143
- logOpts := []clilog.Option {
144
- clilog .WithFilter (logFilter ... ),
145
- clilog .WithHuman (logHuman ),
146
- clilog .WithJSON (logJSON ),
147
- clilog .WithStackdriver (logStackdriver ),
148
- }
149
- if verbose {
150
- logOpts = append (logOpts , clilog .WithVerbose ())
151
- }
152
-
153
- logger , closeLogger , err := clilog .New (logOpts ... ).Build (inv )
154
- if err != nil {
155
- // Fall back to a basic logger
156
- logger = slog .Make (sloghuman .Sink (inv .Stderr ))
157
- logger .Error (ctx , "failed to initialize logger" , slog .Error (err ))
158
- } else {
159
- defer closeLogger ()
160
- }
161
-
162
- if len (tags ) == 0 {
163
- logger .Info (ctx , "note: untagged provisioners can only pick up jobs from untagged templates" )
164
- }
165
-
166
- // When authorizing with a PSK, we automatically scope the provisionerd
167
- // to organization. Scoping to user with PSK auth is not a valid configuration.
168
- if preSharedKey != "" {
169
- logger .Info (ctx , "psk auth automatically sets tag " + provisionersdk .TagScope + "=" + provisionersdk .ScopeOrganization )
170
- tags [provisionersdk .TagScope ] = provisionersdk .ScopeOrganization
171
- }
172
-
173
- err = os .MkdirAll (cacheDir , 0o700 )
174
- if err != nil {
175
- return xerrors .Errorf ("mkdir %q: %w" , cacheDir , err )
176
- }
177
-
178
- tempDir , err := os .MkdirTemp ("" , "provisionerd" )
179
- if err != nil {
180
- return err
181
- }
182
-
183
- terraformClient , terraformServer := drpc .MemTransportPipe ()
184
- go func () {
185
- <- ctx .Done ()
186
- _ = terraformClient .Close ()
187
- _ = terraformServer .Close ()
188
- }()
189
-
190
- errCh := make (chan error , 1 )
191
- go func () {
192
- defer cancel ()
193
-
194
- err := terraform .Serve (ctx , & terraform.ServeOptions {
195
- ServeOptions : & provisionersdk.ServeOptions {
196
- Listener : terraformServer ,
197
- Logger : logger .Named ("terraform" ),
198
- WorkDirectory : tempDir ,
199
- },
200
- CachePath : cacheDir ,
201
- })
202
- if err != nil && ! xerrors .Is (err , context .Canceled ) {
203
- select {
204
- case errCh <- err :
205
- default :
206
- }
207
- }
208
- }()
209
-
210
- var metrics * provisionerd.Metrics
211
- if prometheusEnable {
212
- logger .Info (ctx , "starting Prometheus endpoint" , slog .F ("address" , prometheusAddress ))
213
-
214
- prometheusRegistry := prometheus .NewRegistry ()
215
- prometheusRegistry .MustRegister (collectors .NewGoCollector ())
216
- prometheusRegistry .MustRegister (collectors .NewProcessCollector (collectors.ProcessCollectorOpts {}))
217
-
218
- m := provisionerd .NewMetrics (prometheusRegistry )
219
- m .Runner .NumDaemons .Set (float64 (1 )) // Set numDaemons to 1 as this is standalone mode.
220
- metrics = & m
221
-
222
- closeFunc := agpl .ServeHandler (ctx , logger , promhttp .InstrumentMetricHandler (
223
- prometheusRegistry , promhttp .HandlerFor (prometheusRegistry , promhttp.HandlerOpts {}),
224
- ), prometheusAddress , "prometheus" )
225
- defer closeFunc ()
226
- }
227
-
228
- logger .Info (ctx , "starting provisioner daemon" , slog .F ("tags" , tags ), slog .F ("name" , name ))
229
-
230
- connector := provisionerd.LocalProvisioners {
231
- string (database .ProvisionerTypeTerraform ): proto .NewDRPCProvisionerClient (terraformClient ),
232
- }
233
- srv := provisionerd .New (func (ctx context.Context ) (provisionerdproto.DRPCProvisionerDaemonClient , error ) {
234
- return client .ServeProvisionerDaemon (ctx , codersdk.ServeProvisionerDaemonRequest {
235
- ID : uuid .New (),
236
- Name : name ,
237
- Provisioners : []codersdk.ProvisionerType {
238
- codersdk .ProvisionerTypeTerraform ,
239
- },
240
- Tags : tags ,
241
- PreSharedKey : preSharedKey ,
242
- Organization : org .ID ,
243
- })
244
- }, & provisionerd.Options {
245
- Logger : logger ,
246
- UpdateInterval : 500 * time .Millisecond ,
247
- Connector : connector ,
248
- Metrics : metrics ,
249
- })
250
-
251
- waitForProvisionerJobs := false
252
- var exitErr error
253
- select {
254
- case <- stopCtx .Done ():
255
- exitErr = stopCtx .Err ()
256
- _ , _ = fmt .Fprintln (inv .Stdout , cliui .Bold (
257
- "Stop caught, waiting for provisioner jobs to complete and gracefully exiting. Use ctrl+\\ to force quit" ,
258
- ))
259
- waitForProvisionerJobs = true
260
- case <- interruptCtx .Done ():
261
- exitErr = interruptCtx .Err ()
262
- _ , _ = fmt .Fprintln (inv .Stdout , cliui .Bold (
263
- "Interrupt caught, gracefully exiting. Use ctrl+\\ to force quit" ,
264
- ))
265
- case exitErr = <- errCh :
266
- }
267
- if exitErr != nil && ! xerrors .Is (exitErr , context .Canceled ) {
268
- cliui .Errorf (inv .Stderr , "Unexpected error, shutting down server: %s\n " , exitErr )
269
- }
270
-
271
- err = srv .Shutdown (ctx , waitForProvisionerJobs )
272
- if err != nil {
273
- return xerrors .Errorf ("shutdown: %w" , err )
274
- }
275
-
276
- // Shutdown does not call close. Must call it manually.
277
- err = srv .Close ()
278
- if err != nil {
279
- return xerrors .Errorf ("close server: %w" , err )
280
- }
281
-
282
- cancel ()
283
- if xerrors .Is (exitErr , context .Canceled ) {
284
- return nil
285
- }
286
- return exitErr
287
- },
288
- }
289
-
290
- cmd .Options = serpent.OptionSet {
291
- {
292
- Flag : "cache-dir" ,
293
- FlagShorthand : "c" ,
294
- Env : "CODER_CACHE_DIRECTORY" ,
295
- Description : "Directory to store cached data." ,
296
- Default : codersdk .DefaultCacheDir (),
297
- Value : serpent .StringOf (& cacheDir ),
298
- },
299
- {
300
- Flag : "tag" ,
301
- FlagShorthand : "t" ,
302
- Env : "CODER_PROVISIONERD_TAGS" ,
303
- Description : "Tags to filter provisioner jobs by." ,
304
- Value : serpent .StringArrayOf (& rawTags ),
305
- },
306
- {
307
- Flag : "poll-interval" ,
308
- Env : "CODER_PROVISIONERD_POLL_INTERVAL" ,
309
- Default : time .Second .String (),
310
- Description : "Deprecated and ignored." ,
311
- Value : serpent .DurationOf (& pollInterval ),
312
- },
313
- {
314
- Flag : "poll-jitter" ,
315
- Env : "CODER_PROVISIONERD_POLL_JITTER" ,
316
- Description : "Deprecated and ignored." ,
317
- Default : (100 * time .Millisecond ).String (),
318
- Value : serpent .DurationOf (& pollJitter ),
319
- },
320
- {
321
- Flag : "psk" ,
322
- Env : "CODER_PROVISIONER_DAEMON_PSK" ,
323
- Description : "Pre-shared key to authenticate with Coder server." ,
324
- Value : serpent .StringOf (& preSharedKey ),
325
- },
326
- {
327
- Flag : "name" ,
328
- Env : "CODER_PROVISIONER_DAEMON_NAME" ,
329
- Description : "Name of this provisioner daemon. Defaults to the current hostname without FQDN." ,
330
- Value : serpent .StringOf (& name ),
331
- Default : "" ,
332
- },
333
- {
334
- Flag : "verbose" ,
335
- Env : "CODER_PROVISIONER_DAEMON_VERBOSE" ,
336
- Description : "Output debug-level logs." ,
337
- Value : serpent .BoolOf (& verbose ),
338
- Default : "false" ,
339
- },
340
- {
341
- Flag : "log-human" ,
342
- Env : "CODER_PROVISIONER_DAEMON_LOGGING_HUMAN" ,
343
- Description : "Output human-readable logs to a given file." ,
344
- Value : serpent .StringOf (& logHuman ),
345
- Default : "/dev/stderr" ,
346
- },
347
- {
348
- Flag : "log-json" ,
349
- Env : "CODER_PROVISIONER_DAEMON_LOGGING_JSON" ,
350
- Description : "Output JSON logs to a given file." ,
351
- Value : serpent .StringOf (& logJSON ),
352
- Default : "" ,
353
- },
354
- {
355
- Flag : "log-stackdriver" ,
356
- Env : "CODER_PROVISIONER_DAEMON_LOGGING_STACKDRIVER" ,
357
- Description : "Output Stackdriver compatible logs to a given file." ,
358
- Value : serpent .StringOf (& logStackdriver ),
359
- Default : "" ,
360
- },
361
- {
362
- Flag : "log-filter" ,
363
- Env : "CODER_PROVISIONER_DAEMON_LOG_FILTER" ,
364
- Description : "Filter debug logs by matching against a given regex. Use .* to match all debug logs." ,
365
- Value : serpent .StringArrayOf (& logFilter ),
366
- Default : "" ,
367
- },
368
- {
369
- Flag : "prometheus-enable" ,
370
- Env : "CODER_PROMETHEUS_ENABLE" ,
371
- Description : "Serve prometheus metrics on the address defined by prometheus address." ,
372
- Value : serpent .BoolOf (& prometheusEnable ),
373
- Default : "false" ,
374
- },
375
- {
376
- Flag : "prometheus-address" ,
377
- Env : "CODER_PROMETHEUS_ADDRESS" ,
378
- Description : "The bind address to serve prometheus metrics." ,
379
- Value : serpent .StringOf (& prometheusAddress ),
380
- Default : "127.0.0.1:2112" ,
381
- },
382
- }
383
- orgContext .AttachOptions (cmd )
384
-
385
- return cmd
386
- }
0 commit comments