forked from hasura/graphql-engine
-
Notifications
You must be signed in to change notification settings - Fork 0
/
cli.go
387 lines (331 loc) · 11.2 KB
/
cli.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
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
// Package cli and it's sub packages implements the command line tool for Hasura
// GraphQL Engine. The CLI operates on a directory, denoted by
// "ExecutionDirectory" in the "ExecutionContext" struct.
//
// The ExecutionContext is passed to all the subcommands so that a singleton
// context is available for the execution. Logger and Spinner comes from the same
// context.
package cli
import (
"encoding/json"
"fmt"
"net/url"
"os"
"path/filepath"
"time"
"github.com/hasura/graphql-engine/cli/telemetry"
"github.com/hasura/graphql-engine/cli/util"
"github.com/briandowns/spinner"
"github.com/gofrs/uuid"
"github.com/hasura/graphql-engine/cli/version"
colorable "github.com/mattn/go-colorable"
"github.com/pkg/errors"
"github.com/sirupsen/logrus"
"github.com/spf13/viper"
)
// Other constants used in the package
const (
// Name of the global configuration directory
GlobalConfigDirName = ".hasura"
// Name of the global configuration file
GlobalConfigFileName = "config.json"
// Name of the file to store last update check time
LastUpdateCheckFileName = "last_update_check_at"
)
// String constants
const (
StrTelemetryNotice = `Help us improve Hasura! The cli collects anonymized usage stats which
allow us to keep improving Hasura at warp speed. To opt-out or read more,
visit https://docs.hasura.io/1.0/graphql/manual/guides/telemetry.html
`
)
// ServerConfig has the config values required to contact the server.
type ServerConfig struct {
// Endpoint for the GraphQL Engine
Endpoint string
// AdminSecret (optional) required to query the endpoint
AdminSecret string
ParsedEndpoint *url.URL
}
type rawServerConfig struct {
// Endpoint for the GraphQL Engine
Endpoint string `json:"endpoint"`
// AccessKey (deprecated) (optional) Admin secret key required to query the endpoint
AccessKey string `json:"access_key,omitempty"`
// AdminSecret (optional) Admin secret required to query the endpoint
AdminSecret string `json:"admin_secret,omitempty"`
ParsedEndpoint *url.URL `json:"-"`
}
func (r rawServerConfig) toServerConfig() ServerConfig {
s := r.AdminSecret
if s == "" {
s = r.AccessKey
}
return ServerConfig{
Endpoint: r.Endpoint,
AdminSecret: s,
ParsedEndpoint: r.ParsedEndpoint,
}
}
func (s ServerConfig) toRawServerConfig() rawServerConfig {
return rawServerConfig{
Endpoint: s.Endpoint,
AccessKey: "",
AdminSecret: s.AdminSecret,
ParsedEndpoint: s.ParsedEndpoint,
}
}
// MarshalJSON converts s to JSON
func (s ServerConfig) MarshalJSON() ([]byte, error) {
return json.Marshal(s.toRawServerConfig())
}
// UnmarshalJSON converts b to struct s
func (s ServerConfig) UnmarshalJSON(b []byte) error {
var r rawServerConfig
err := json.Unmarshal(b, &r)
if err != nil {
return errors.Wrap(err, "unmarshal error")
}
sc := r.toServerConfig()
s.Endpoint = sc.Endpoint
s.AdminSecret = sc.AdminSecret
s.ParsedEndpoint = sc.ParsedEndpoint
return nil
}
// ParseEndpoint ensures the endpoint is valid.
func (s *ServerConfig) ParseEndpoint() error {
nurl, err := url.Parse(s.Endpoint)
if err != nil {
return err
}
s.ParsedEndpoint = nurl
return nil
}
// ExecutionContext contains various contextual information required by the cli
// at various points of it's execution. Values are filled in by the
// initializers and passed on to each command. Commands can also fill in values
// to be used further down the line.
type ExecutionContext struct {
// CMDName is the name of CMD (os.Args[0]). To be filled in later to
// correctly render example strings etc.
CMDName string
// ID is a unique ID for this Execution
ID string
// ServerUUID is the unique ID for the server this execution is contacting.
ServerUUID string
// Spinner is the global spinner object used to show progress across the cli.
Spinner *spinner.Spinner
// Logger is the global logger object to print logs.
Logger *logrus.Logger
// ExecutionDirectory is the directory in which command is being executed.
ExecutionDirectory string
// MigrationDir is the name of directory where migrations are stored.
MigrationDir string
// ConfigFile is the file where endpoint etc. are stored.
ConfigFile string
// MetadataFile (optional) is a yaml|json file where Hasura metadata is stored.
MetadataFile []string
// ServerConfig is the configuration object storing the endpoint and admin secret
// information after reading from config file or env var.
ServerConfig *ServerConfig
// GlobalConfigDir is the ~/.hasura-graphql directory to store configuration
// globally.
GlobalConfigDir string
// GlobalConfigFile is the file inside GlobalConfigDir where values are
// stored.
GlobalConfigFile string
// GlobalConfig holds all the configuration options.
GlobalConfig *GlobalConfig
// IsStableRelease indicates if the CLI release is stable or not.
IsStableRelease bool
// Version indicates the version object
Version *version.Version
// Viper indicates the viper object for the execution
Viper *viper.Viper
// LogLevel indicates the logrus default logging level
LogLevel string
// Telemetry collects the telemetry data throughout the execution
Telemetry *telemetry.Data
// LastUpdateCheckFile is the file where the timestamp of last update check is stored
LastUpdateCheckFile string
// SkipUpdateCheck will skip the auto update check if set to true
SkipUpdateCheck bool
}
// NewExecutionContext returns a new instance of execution context
func NewExecutionContext() *ExecutionContext {
ec := &ExecutionContext{}
ec.Telemetry = telemetry.BuildEvent()
ec.Telemetry.Version = version.BuildVersion
return ec
}
// Prepare as the name suggests, prepares the ExecutionContext ec by
// initializing most of the variables to sensible defaults, if it is not already
// set.
func (ec *ExecutionContext) Prepare() error {
// set the command name
cmdName := os.Args[0]
if len(cmdName) == 0 {
cmdName = "hasura"
}
ec.CMDName = cmdName
// set spinner
ec.setupSpinner()
// set logger
ec.setupLogger()
// populate version
ec.setVersion()
// setup global config
err := ec.setupGlobalConfig()
if err != nil {
return errors.Wrap(err, "setting up global config failed")
}
ec.LastUpdateCheckFile = filepath.Join(ec.GlobalConfigDir, LastUpdateCheckFileName)
// initialize a blank server config
if ec.ServerConfig == nil {
ec.ServerConfig = &ServerConfig{}
}
// generate an execution id
if ec.ID == "" {
id := "00000000-0000-0000-0000-000000000000"
u, err := uuid.NewV4()
if err == nil {
id = u.String()
} else {
ec.Logger.Debugf("generating uuid for execution ID failed, %v", err)
}
ec.ID = id
ec.Logger.Debugf("execution id: %v", ec.ID)
}
ec.Telemetry.ExecutionID = ec.ID
return nil
}
// Validate prepares the ExecutionContext ec and then validates the
// ExecutionDirectory to see if all the required files and directories are in
// place.
func (ec *ExecutionContext) Validate() error {
// validate execution directory
err := ec.validateDirectory()
if err != nil {
return errors.Wrap(err, "validating current directory failed")
}
// set names of files and directories
ec.MigrationDir = filepath.Join(ec.ExecutionDirectory, "migrations")
ec.ConfigFile = filepath.Join(ec.ExecutionDirectory, "config.yaml")
ec.MetadataFile = append(ec.MetadataFile, filepath.Join(ec.MigrationDir, "metadata.yaml"))
ec.MetadataFile = append(ec.MetadataFile, filepath.Join(ec.MigrationDir, "metadata.json"))
// read config and parse the values into Config
err = ec.readConfig()
if err != nil {
return errors.Wrap(err, "cannot read config")
}
ec.Logger.Debug("graphql engine endpoint: ", ec.ServerConfig.Endpoint)
ec.Logger.Debug("graphql engine admin_secret: ", ec.ServerConfig.AdminSecret)
// get version from the server and match with the cli version
err = ec.checkServerVersion()
if err != nil {
return errors.Wrap(err, "version check")
}
state := util.GetServerState(ec.ServerConfig.Endpoint, ec.ServerConfig.AdminSecret, ec.Version.ServerSemver, ec.Logger)
ec.ServerUUID = state.UUID
ec.Telemetry.ServerUUID = ec.ServerUUID
ec.Logger.Debugf("server: uuid: %s", ec.ServerUUID)
return nil
}
func (ec *ExecutionContext) checkServerVersion() error {
v, err := version.FetchServerVersion(ec.ServerConfig.Endpoint)
if err != nil {
return errors.Wrap(err, "failed to get version from server")
}
ec.Version.SetServerVersion(v)
ec.Telemetry.ServerVersion = ec.Version.GetServerVersion()
isCompatible, reason := ec.Version.CheckCLIServerCompatibility()
ec.Logger.Debugf("versions: cli: [%s] server: [%s]", ec.Version.GetCLIVersion(), ec.Version.GetServerVersion())
ec.Logger.Debugf("compatibility check: [%v] %v", isCompatible, reason)
if !isCompatible {
return errors.Errorf("[cli: %s] [server: %s] versions incompatible: %s", ec.Version.GetCLIVersion(), ec.Version.GetServerVersion(), reason)
}
return nil
}
// readConfig reads the configuration from config file, flags and env vars,
// through viper.
func (ec *ExecutionContext) readConfig() error {
// need to get existing viper because https://github.com/spf13/viper/issues/233
v := ec.Viper
v.SetEnvPrefix("HASURA_GRAPHQL")
v.AutomaticEnv()
v.SetConfigName("config")
v.SetDefault("endpoint", "http://localhost:8080")
v.SetDefault("admin_secret", "")
v.SetDefault("access_key", "")
v.AddConfigPath(ec.ExecutionDirectory)
err := v.ReadInConfig()
if err != nil {
return errors.Wrap(err, "cannot read config from file/env")
}
adminSecret := v.GetString("admin_secret")
if adminSecret == "" {
adminSecret = v.GetString("access_key")
}
ec.ServerConfig = &ServerConfig{
Endpoint: v.GetString("endpoint"),
AdminSecret: adminSecret,
}
return ec.ServerConfig.ParseEndpoint()
}
// setupSpinner creates a default spinner if the context does not already have
// one.
func (ec *ExecutionContext) setupSpinner() {
if ec.Spinner == nil {
spnr := spinner.New(spinner.CharSets[7], 100*time.Millisecond)
spnr.Writer = os.Stderr
ec.Spinner = spnr
}
}
// Spin stops any existing spinner and starts a new one with the given message.
func (ec *ExecutionContext) Spin(message string) {
ec.Spinner.Stop()
ec.Spinner.Prefix = message
ec.Spinner.Start()
}
// setupLogger creates a default logger if context does not have one set.
func (ec *ExecutionContext) setupLogger() {
if ec.Logger == nil {
logger := logrus.New()
logger.Formatter = &logrus.TextFormatter{
ForceColors: true,
DisableTimestamp: true,
}
logger.Out = colorable.NewColorableStdout()
ec.Logger = logger
}
if ec.LogLevel != "" {
level, err := logrus.ParseLevel(ec.LogLevel)
if err != nil {
ec.Logger.WithError(err).Error("error parsing log-level flag")
return
}
ec.Logger.SetLevel(level)
}
// set the logger for telemetry
if ec.Telemetry.Logger == nil {
ec.Telemetry.Logger = ec.Logger
}
}
// SetVersion sets the version inside context, according to the variable
// 'version' set during build context.
func (ec *ExecutionContext) setVersion() {
if ec.Version == nil {
ec.Version = version.New()
}
}
// GetMetadataPath returns the file path based on the format.
func (ec *ExecutionContext) GetMetadataFilePath(format string) (string, error) {
ext := fmt.Sprintf(".%s", format)
for _, filePath := range ec.MetadataFile {
switch p := filepath.Ext(filePath); p {
case ext:
return filePath, nil
}
}
return "", errors.New("unsupported file type")
}