-
Notifications
You must be signed in to change notification settings - Fork 2
/
bootstrap.go
276 lines (237 loc) · 7.51 KB
/
bootstrap.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
// Copyright 2021 Outreach Corporation. All Rights Reserved.
// Description: This file contains cli functions used in bootstrap
// and eventually in stencil.
package cli
import (
"context"
"fmt"
"io"
"os"
"os/signal"
"os/user"
"runtime"
"runtime/debug"
"strings"
"syscall"
"github.com/getoutreach/gobox/pkg/app"
"github.com/getoutreach/gobox/pkg/cfg"
"github.com/getoutreach/gobox/pkg/env"
"github.com/getoutreach/gobox/pkg/exec"
"github.com/getoutreach/gobox/pkg/log"
"github.com/getoutreach/gobox/pkg/secrets"
"github.com/getoutreach/gobox/pkg/trace"
"github.com/getoutreach/gobox/pkg/updater"
"github.com/sirupsen/logrus"
"github.com/urfave/cli/v2"
"gopkg.in/yaml.v2"
)
// UpdateExitCode is the exit code returned when an update ocurred
const UpdateExitCode = 5
// overrideConfigLoaders fakes certain parts of the config that usually get pulled
// in via mechanisms that don't make sense to use in CLIs.
func overrideConfigLoaders(honeycombAPIKey, dataset string, tracingDebug bool) {
// override the secret loader so that we can read specific keys from variables
// otherwise fallback to the original secret loader, if it was set.
var fallbackSecretLookup func(context.Context, string) ([]byte, error)
fallbackSecretLookup = secrets.SetDevLookup(func(ctx context.Context, path string) ([]byte, error) {
// use the embedded in value
if path == "APIKey" {
return []byte(honeycombAPIKey), nil
}
// if no fallback, return an error, failed to find :(
// note: as of this time the secrets logic looks for
// the path before falling back to the devlookup so this
// is safe to assume all attempts have failed
if fallbackSecretLookup == nil {
return nil, fmt.Errorf("failed to find secret at path '%s', or compiled into binary", path)
}
return fallbackSecretLookup(ctx, path)
})
fallbackConfigReader := cfg.DefaultReader()
cfg.SetDefaultReader(func(fileName string) ([]byte, error) {
if fileName == "trace.yaml" {
traceConfig := &trace.Config{
Honeycomb: trace.Honeycomb{
Enabled: true,
APIHost: "https://api.honeycomb.io",
APIKey: cfg.Secret{
Path: "APIKey",
},
Debug: tracingDebug,
Dataset: dataset,
SamplePercent: 100,
},
}
b, err := yaml.Marshal(&traceConfig)
if err != nil {
panic(err)
}
return b, nil
}
return fallbackConfigReader(fileName)
})
}
// intPtr turns an int into a *int
func intPtr(i int) *int {
return &i
}
// funcPtr turns a func into a *func
func funcPtr(fn func()) *func() {
return &fn
}
// urfaveRegisterShutdownHandler registers a signal notifier that translates various term
// signals into context cancel
func urfaveRegisterShutdownHandler(cancel context.CancelFunc) {
// handle ^C gracefully
c := make(chan os.Signal, 1)
signal.Notify(c, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP)
go func() {
<-c
signal.Reset()
cancel()
}()
}
// setupTracer sets up a root trace for the CLI and initializes the tracer
func setupTracer(ctx context.Context, name string) context.Context {
if err := trace.InitTracer(ctx, name); err != nil {
fmt.Println(err)
return ctx
}
return trace.StartTrace(ctx, name)
}
// setupPanicHandler sets up a panic handler for CLIs
func setupPanicHandler(exitCode *int) {
if r := recover(); r != nil {
fmt.Printf("stacktrace from panic: %s\n%s\n", r, string(debug.Stack()))
// Go sets panic exit codes to 2
(*exitCode) = 2
}
}
// setupExitHandler sets up an exit handler
func setupExitHandler(ctx context.Context) (exitCode *int, exit func(), cleanup *func()) {
exitCode = intPtr(0)
cleanup = funcPtr(func() {})
exit = func() {
trace.End(ctx)
trace.CloseTracer(ctx)
if cleanup != nil {
(*cleanup)()
}
os.Exit(*exitCode)
}
return
}
// HookInUrfaveCLI sets up an app.Before that automatically traces command runs
// and automatically updates itself.
//nolint:funlen // Why: Also not worth doing at the moment, we split a lot of this out already.
func HookInUrfaveCLI(ctx context.Context, cancel context.CancelFunc, a *cli.App,
logger logrus.FieldLogger, honeycombAPIKey, dataset string) {
env.ApplyOverrides()
app.SetName(a.Name)
// Ensure that we don't use the standard outreach logger
log.SetOutput(io.Discard)
// IDEA: Can we ever hook up --debug to this?
overrideConfigLoaders(honeycombAPIKey, dataset, false)
urfaveRegisterShutdownHandler(cancel)
ctx = setupTracer(ctx, a.Name)
exitCode, exit, cleanup := setupExitHandler(ctx)
defer exit()
cli.OsExiter = func(code int) { (*exitCode) = code }
// Print a stack trace when a panic occurs and set the exit code
defer setupPanicHandler(exitCode)
ctx = trace.StartCall(ctx, "main")
defer trace.EndCall(ctx)
oldBefore := (*a).Before //nolint:gocritic // Why: we're saving the previous value
a.Before = func(c *cli.Context) error {
if oldBefore != nil {
if err := oldBefore(c); err != nil {
return err
}
}
return urfaveBefore(a, logger, exit, cleanup, exitCode)(c)
}
// append the standard flags
a.Flags = append(a.Flags, []cli.Flag{
&cli.BoolFlag{
Name: "skip-update",
Usage: "skips the updater check",
},
&cli.BoolFlag{
Name: "debug",
Usage: "enables debug logging for all components (i.e updater)",
},
&cli.BoolFlag{
Name: "enable-prereleases",
Usage: "Enable considering pre-releases when checking for updates",
},
&cli.BoolFlag{
Name: "force-update-check",
Usage: "Force checking for an update",
},
}...)
if err := a.RunContext(ctx, os.Args); err != nil {
logger.Errorf("failed to run: %v", err)
//nolint:errcheck // Why: We're attaching the error to the trace.
trace.SetCallStatus(ctx, err)
(*exitCode) = 1
return
}
}
// urfaveBefore is a cli.BeforeFunc that implements tracing and automatic updating
//nolint:funlen // Why: Not worth splitting out yet. May want to do so w/ more CLI support.
func urfaveBefore(a *cli.App, logger logrus.FieldLogger, exit func(), cleanup *func(),
exitCode *int) func(c *cli.Context) error {
return func(c *cli.Context) error {
cargs := c.Args().Slice()
command := ""
args := make([]string, 0)
if len(cargs) > 0 {
command = cargs[0]
}
if len(cargs) > 1 {
args = cargs[1:]
}
userName := "unknown"
if u, err := user.Current(); err == nil {
userName = u.Username
}
trace.AddInfo(c.Context, log.F{
a.Name + ".subcommand": command,
a.Name + ".args": strings.Join(args, " "),
"os.user": userName,
"os.name": runtime.GOOS,
})
// restart when updated
traceCtx := trace.StartCall(c.Context, "updater.NeedsUpdate")
defer trace.EndCall(traceCtx)
// restart when updated
if updater.NeedsUpdate(traceCtx, logger, "", app.Version,
c.Bool("skip-update"), c.Bool("debug"), c.Bool("enable-prereleases"),
c.Bool("force-update-check")) {
switch runtime.GOOS {
case "linux", "darwin":
(*cleanup) = func() {
binPath, err := exec.ResolveExecuable(os.Args[0])
if err != nil {
logger.WithError(err).Warn("Failed to find binary location, please re-run your command manually")
return
}
logger.Infof("%s has been updated, re-running automatically", a.Name)
//nolint:gosec // Why: We're passing in os.Args
if err := syscall.Exec(binPath, os.Args, os.Environ()); err != nil {
logger.WithError(err).Warn("failed to re-run binary, please re-run your command manually")
return
}
}
default:
logger.Infof("%s has been updated, please re-run your command", a.Name)
}
trace.EndCall(traceCtx)
(*exitCode) = UpdateExitCode
trace.EndCall(traceCtx)
exit()
return nil
}
return nil
}
}