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
- }