2018-06-24 16:47:01 +03:00
|
|
|
// 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
|
2018-06-24 16:40:48 +03:00
|
|
|
|
|
|
|
import (
|
2019-01-28 16:55:28 +03:00
|
|
|
"encoding/json"
|
|
|
|
"io/ioutil"
|
2018-07-09 16:47:38 +03:00
|
|
|
"net/url"
|
2018-06-24 16:40:48 +03:00
|
|
|
"os"
|
|
|
|
"path/filepath"
|
|
|
|
"regexp"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
"github.com/hasura/graphql-engine/cli/telemetry"
|
|
|
|
"github.com/hasura/graphql-engine/cli/util"
|
|
|
|
|
2018-06-24 16:40:48 +03:00
|
|
|
"github.com/briandowns/spinner"
|
2019-01-28 16:55:28 +03:00
|
|
|
"github.com/gofrs/uuid"
|
2018-07-04 15:43:52 +03:00
|
|
|
"github.com/hasura/graphql-engine/cli/version"
|
2018-06-24 16:40:48 +03:00
|
|
|
colorable "github.com/mattn/go-colorable"
|
|
|
|
homedir "github.com/mitchellh/go-homedir"
|
|
|
|
"github.com/pkg/errors"
|
|
|
|
"github.com/sirupsen/logrus"
|
2018-06-24 16:47:01 +03:00
|
|
|
"github.com/spf13/viper"
|
2018-06-24 16:40:48 +03:00
|
|
|
)
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// Environment variable names recognised by the CLI.
|
2018-06-24 16:40:48 +03:00
|
|
|
const (
|
2018-06-24 16:47:01 +03:00
|
|
|
// ENV_ENDPOINT is the name of env var which indicates the Hasura GraphQL
|
|
|
|
// Engine endpoint URL.
|
|
|
|
ENV_ENDPOINT = "HASURA_GRAPHQL_ENDPOINT"
|
|
|
|
// ENV_ACCESS_KEY is the name of env var that has the access key for GraphQL
|
|
|
|
// Engine endpoint.
|
2018-06-24 16:40:48 +03:00
|
|
|
ENV_ACCESS_KEY = "HASURA_GRAPHQL_ACCESS_KEY"
|
|
|
|
)
|
|
|
|
|
2018-06-27 15:04:09 +03:00
|
|
|
// Other constants used in the package
|
|
|
|
const (
|
|
|
|
// Name of the global configuration directory
|
2019-01-28 16:55:28 +03:00
|
|
|
GLOBAL_CONFIG_DIR_NAME = ".hasura"
|
2018-06-27 15:04:09 +03:00
|
|
|
// Name of the global configuration file
|
|
|
|
GLOBAL_CONFIG_FILE_NAME = "config.json"
|
|
|
|
)
|
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
// 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
|
|
|
|
`
|
|
|
|
)
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// HasuraGraphQLConfig has the config values required to contact the server.
|
2018-06-24 16:40:48 +03:00
|
|
|
type HasuraGraphQLConfig struct {
|
2018-06-24 16:47:01 +03:00
|
|
|
// Endpoint for the GraphQL Engine
|
|
|
|
Endpoint string `json:"endpoint"`
|
|
|
|
// AccessKey (optional) required to query the endpoint
|
|
|
|
AccessKey string `json:"access_key,omitempty"`
|
2018-07-09 16:47:38 +03:00
|
|
|
|
2018-07-11 12:22:36 +03:00
|
|
|
ParsedEndpoint *url.URL `json:"-"`
|
2018-07-09 16:47:38 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
// ParseEndpoint ensures the endpoint is valid.
|
|
|
|
func (hgc *HasuraGraphQLConfig) ParseEndpoint() error {
|
|
|
|
nurl, err := url.Parse(hgc.Endpoint)
|
|
|
|
if err != nil {
|
|
|
|
return err
|
|
|
|
}
|
|
|
|
hgc.ParsedEndpoint = nurl
|
|
|
|
return nil
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
// GlobalConfig is the configuration object stored in the GlobalConfigFile.
|
|
|
|
type GlobalConfig struct {
|
|
|
|
// UUID used for telemetry, generated on first run.
|
|
|
|
UUID string `json:"uuid"`
|
|
|
|
|
|
|
|
// Indicate if telemetry is enabled or not
|
|
|
|
EnableTelemetry bool `json:"enable_telemetry"`
|
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// 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.
|
2018-06-24 16:40:48 +03:00
|
|
|
type ExecutionContext struct {
|
2018-06-24 16:47:01 +03:00
|
|
|
// CMDName is the name of CMD (os.Args[0]). To be filled in later to
|
|
|
|
// correctly render example strings etc.
|
2018-06-24 16:40:48 +03:00
|
|
|
CMDName string
|
2018-06-24 16:47:01 +03:00
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
// ID is a unique ID for this Execution
|
|
|
|
ID string
|
|
|
|
|
|
|
|
// ServerUUID is the unique ID for the server this execution is contacting.
|
|
|
|
ServerUUID string
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// Spinner is the global spinner object used to show progress across the cli.
|
2018-06-24 16:40:48 +03:00
|
|
|
Spinner *spinner.Spinner
|
2018-06-24 16:47:01 +03:00
|
|
|
// Logger is the global logger object to print logs.
|
|
|
|
Logger *logrus.Logger
|
2018-06-24 16:40:48 +03:00
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// ExecutionDirectory is the directory in which command is being executed.
|
2018-06-24 16:40:48 +03:00
|
|
|
ExecutionDirectory string
|
2018-06-24 16:47:01 +03:00
|
|
|
// 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 file where Hasura metadata is stored.
|
|
|
|
MetadataFile string
|
|
|
|
|
|
|
|
// Config is the configuration object storing the endpoint and access key
|
|
|
|
// information after reading from config file or env var.
|
|
|
|
Config *HasuraGraphQLConfig
|
|
|
|
|
|
|
|
// GlobalConfigDir is the ~/.hasura-graphql directory to store configuration
|
|
|
|
// globally.
|
|
|
|
GlobalConfigDir string
|
|
|
|
// GlobalConfigFile is the file inside GlobalConfigDir where values are
|
|
|
|
// stored.
|
|
|
|
GlobalConfigFile string
|
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
// GlobalConfig holds all the configuration options.
|
|
|
|
GlobalConfig *GlobalConfig
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// IsStableRelease indicates if the CLI release is stable or not.
|
|
|
|
IsStableRelease bool
|
2018-07-04 15:43:52 +03:00
|
|
|
// Version indicates the version object
|
|
|
|
Version *version.Version
|
2018-06-24 16:40:48 +03:00
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// Viper indicates the viper object for the execution
|
|
|
|
Viper *viper.Viper
|
2018-06-24 16:40:48 +03:00
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// LogLevel indicates the logrus default logging level
|
|
|
|
LogLevel string
|
2019-01-28 16:55:28 +03:00
|
|
|
|
|
|
|
// Telemetry collects the telemetry data throughout the execution
|
|
|
|
Telemetry *telemetry.Data
|
|
|
|
}
|
|
|
|
|
|
|
|
// NewExecutionContext returns a new instance of execution context
|
|
|
|
func NewExecutionContext() *ExecutionContext {
|
|
|
|
ec := &ExecutionContext{}
|
|
|
|
ec.Telemetry = telemetry.BuildEvent()
|
|
|
|
ec.Telemetry.Version = version.BuildVersion
|
|
|
|
return ec
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// Prepare as the name suggests, prepares the ExecutionContext ec by
|
|
|
|
// initializing most of the variables to sensible defaults, if it is not already
|
|
|
|
// set.
|
2018-06-24 16:40:48 +03:00
|
|
|
func (ec *ExecutionContext) Prepare() error {
|
2018-06-24 16:47:01 +03:00
|
|
|
// set the command name
|
2018-06-24 16:40:48 +03:00
|
|
|
cmdName := os.Args[0]
|
|
|
|
if len(cmdName) == 0 {
|
|
|
|
cmdName = "hasura"
|
|
|
|
}
|
|
|
|
ec.CMDName = cmdName
|
|
|
|
|
|
|
|
// set spinner
|
|
|
|
ec.setupSpinner()
|
|
|
|
|
|
|
|
// set logger
|
|
|
|
ec.setupLogger()
|
|
|
|
|
2018-06-28 08:40:18 +03:00
|
|
|
// populate version
|
|
|
|
ec.setVersion()
|
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
// setup global config
|
|
|
|
err := ec.setupGlobalConfig()
|
|
|
|
if err != nil {
|
|
|
|
// TODO(shahidhk): should this be a failure?
|
|
|
|
return errors.Wrap(err, "setting up global config directory failed")
|
|
|
|
}
|
|
|
|
|
|
|
|
// read global config
|
|
|
|
err = ec.readGlobalConfig()
|
2018-06-24 16:40:48 +03:00
|
|
|
if err != nil {
|
2019-01-28 16:55:28 +03:00
|
|
|
return errors.Wrap(err, "reading global config failed")
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// initialize a blank config
|
2019-01-28 16:55:28 +03:00
|
|
|
if ec.Config == nil {
|
|
|
|
ec.Config = &HasuraGraphQLConfig{}
|
|
|
|
}
|
|
|
|
|
|
|
|
// 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
|
2018-06-24 16:47:01 +03:00
|
|
|
|
2018-06-24 16:40:48 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// Validate prepares the ExecutionContext ec and then validates the
|
|
|
|
// ExecutionDirectory to see if all the required files and directories are in
|
|
|
|
// place.
|
2018-06-24 16:40:48 +03:00
|
|
|
func (ec *ExecutionContext) Validate() error {
|
2018-06-24 16:47:01 +03:00
|
|
|
// prepare the context
|
2018-06-24 16:40:48 +03:00
|
|
|
err := ec.Prepare()
|
|
|
|
if err != nil {
|
2018-06-24 16:47:01 +03:00
|
|
|
return errors.Wrap(err, "failed preparing context")
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// validate execution directory
|
|
|
|
err = ec.validateDirectory()
|
2018-06-24 16:40:48 +03:00
|
|
|
if err != nil {
|
2018-06-24 16:47:01 +03:00
|
|
|
return errors.Wrap(err, "validating current directory failed")
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// set names of files and directories
|
2018-06-24 16:40:48 +03:00
|
|
|
ec.MigrationDir = filepath.Join(ec.ExecutionDirectory, "migrations")
|
|
|
|
ec.ConfigFile = filepath.Join(ec.ExecutionDirectory, "config.yaml")
|
2018-07-12 15:52:25 +03:00
|
|
|
ec.MetadataFile = filepath.Join(ec.MigrationDir, "metadata.yaml")
|
2018-06-24 16:40:48 +03:00
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// read config and parse the values into Config
|
2018-06-24 16:40:48 +03:00
|
|
|
err = ec.readConfig()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "cannot read config")
|
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
ec.Logger.Debug("graphql engine endpoint: ", ec.Config.Endpoint)
|
|
|
|
ec.Logger.Debug("graphql engine access_key: ", ec.Config.AccessKey)
|
2018-06-24 16:40:48 +03:00
|
|
|
|
2018-07-03 20:10:13 +03:00
|
|
|
// get version from the server and match with the cli version
|
2019-01-28 16:55:28 +03:00
|
|
|
err = ec.checkServerVersion()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "version check")
|
|
|
|
}
|
|
|
|
|
|
|
|
state := util.GetServerState(ec.Config.Endpoint, ec.Config.AccessKey, ec.Version.ServerSemver, ec.Logger)
|
|
|
|
ec.ServerUUID = state.UUID
|
|
|
|
ec.Telemetry.ServerUUID = ec.ServerUUID
|
|
|
|
ec.Logger.Debugf("server: uuid: %s", ec.ServerUUID)
|
|
|
|
|
|
|
|
return nil
|
2018-07-03 20:10:13 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
func (ec *ExecutionContext) checkServerVersion() error {
|
2018-07-04 15:43:52 +03:00
|
|
|
v, err := version.FetchServerVersion(ec.Config.Endpoint)
|
2018-07-03 20:10:13 +03:00
|
|
|
if err != nil {
|
2018-07-04 15:43:52 +03:00
|
|
|
return errors.Wrap(err, "failed to get version from server")
|
2018-07-03 20:10:13 +03:00
|
|
|
}
|
2018-07-04 15:43:52 +03:00
|
|
|
ec.Version.SetServerVersion(v)
|
2019-01-28 16:55:28 +03:00
|
|
|
ec.Telemetry.ServerVersion = ec.Version.GetServerVersion()
|
2018-07-04 15:43:52 +03:00
|
|
|
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)
|
2018-07-03 20:10:13 +03:00
|
|
|
}
|
2018-06-24 16:40:48 +03:00
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
// readGlobalConfig reads the configuration from global config file env vars,
|
|
|
|
// through viper.
|
|
|
|
func (ec *ExecutionContext) readGlobalConfig() error {
|
|
|
|
// need to get existing viper because https://github.com/spf13/viper/issues/233
|
|
|
|
v := viper.New()
|
|
|
|
v.SetEnvPrefix("HASURA_GRAPHQL")
|
|
|
|
v.AutomaticEnv()
|
|
|
|
v.SetConfigName("config")
|
|
|
|
v.AddConfigPath(ec.GlobalConfigDir)
|
|
|
|
err := v.ReadInConfig()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "cannor read global config from file/env")
|
|
|
|
}
|
|
|
|
if ec.GlobalConfig == nil {
|
|
|
|
ec.Logger.Debugf("global config is not pre-set, reading from current env")
|
|
|
|
ec.GlobalConfig = &GlobalConfig{
|
|
|
|
UUID: v.GetString("uuid"),
|
|
|
|
EnableTelemetry: v.GetBool("enable_telemetry"),
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
ec.Logger.Debugf("global config is pre-set to %#v", ec.GlobalConfig)
|
|
|
|
}
|
|
|
|
ec.Logger.Debugf("global config: uuid: %v", ec.GlobalConfig.UUID)
|
|
|
|
ec.Logger.Debugf("global config: enableTelemetry: %v", ec.GlobalConfig.EnableTelemetry)
|
|
|
|
// set if telemetry can be beamed or not
|
|
|
|
ec.Telemetry.CanBeam = ec.GlobalConfig.EnableTelemetry
|
|
|
|
ec.Telemetry.UUID = ec.GlobalConfig.UUID
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// readConfig reads the configuration from config file, flags and env vars,
|
|
|
|
// through viper.
|
2018-06-24 16:40:48 +03:00
|
|
|
func (ec *ExecutionContext) readConfig() error {
|
2018-06-24 16:47:01 +03:00
|
|
|
// 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")
|
2019-01-28 16:55:28 +03:00
|
|
|
v.SetDefault("endpoint", "http://localhost:8080")
|
|
|
|
v.SetDefault("access_key", "")
|
2018-06-24 16:47:01 +03:00
|
|
|
v.AddConfigPath(ec.ExecutionDirectory)
|
|
|
|
err := v.ReadInConfig()
|
2018-06-24 16:40:48 +03:00
|
|
|
if err != nil {
|
2019-01-28 16:55:28 +03:00
|
|
|
return errors.Wrap(err, "cannor read config from file/env")
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
2018-06-24 16:47:01 +03:00
|
|
|
ec.Config = &HasuraGraphQLConfig{
|
|
|
|
Endpoint: v.GetString("endpoint"),
|
|
|
|
AccessKey: v.GetString("access_key"),
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
2019-01-28 16:55:28 +03:00
|
|
|
return ec.Config.ParseEndpoint()
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// setupSpinner creates a default spinner if the context does not already have
|
|
|
|
// one.
|
2018-06-24 16:40:48 +03:00
|
|
|
func (ec *ExecutionContext) setupSpinner() {
|
|
|
|
if ec.Spinner == nil {
|
|
|
|
spnr := spinner.New(spinner.CharSets[7], 100*time.Millisecond)
|
|
|
|
spnr.Writer = os.Stderr
|
|
|
|
ec.Spinner = spnr
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2018-06-27 15:04:09 +03:00
|
|
|
// 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()
|
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// setupLogger creates a default logger if context does not have one set.
|
2018-06-24 16:40:48 +03:00
|
|
|
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
|
|
|
|
}
|
2018-06-24 16:47:01 +03:00
|
|
|
|
|
|
|
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)
|
|
|
|
}
|
2019-01-28 16:55:28 +03:00
|
|
|
|
|
|
|
// set the logger for telemetry
|
|
|
|
if ec.Telemetry.Logger == nil {
|
|
|
|
ec.Telemetry.Logger = ec.Logger
|
|
|
|
}
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
|
|
|
|
2019-01-28 16:55:28 +03:00
|
|
|
// setupGlobConfig ensures that global config directory and file exists and
|
|
|
|
// reads it into the GlobalConfig object.
|
|
|
|
func (ec *ExecutionContext) setupGlobalConfig() error {
|
2018-06-24 16:40:48 +03:00
|
|
|
if len(ec.GlobalConfigDir) == 0 {
|
2019-01-28 16:55:28 +03:00
|
|
|
ec.Logger.Debug("global config directory is not pre-set, defaulting")
|
2018-06-24 16:40:48 +03:00
|
|
|
home, err := homedir.Dir()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "cannot get home directory")
|
|
|
|
}
|
2018-06-27 15:04:09 +03:00
|
|
|
globalConfigDir := filepath.Join(home, GLOBAL_CONFIG_DIR_NAME)
|
2018-06-24 16:40:48 +03:00
|
|
|
ec.GlobalConfigDir = globalConfigDir
|
2019-01-28 16:55:28 +03:00
|
|
|
ec.Logger.Debugf("global config directory set as '%s'", ec.GlobalConfigDir)
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
|
|
|
err := os.MkdirAll(ec.GlobalConfigDir, os.ModePerm)
|
|
|
|
if err != nil {
|
2019-01-28 16:55:28 +03:00
|
|
|
return errors.Wrap(err, "cannot create global config directory")
|
|
|
|
}
|
|
|
|
if len(ec.GlobalConfigFile) == 0 {
|
|
|
|
ec.GlobalConfigFile = filepath.Join(ec.GlobalConfigDir, GLOBAL_CONFIG_FILE_NAME)
|
|
|
|
ec.Logger.Debugf("global config file set as '%s'", ec.GlobalConfigFile)
|
|
|
|
}
|
|
|
|
_, err = os.Stat(ec.GlobalConfigFile)
|
|
|
|
if os.IsNotExist(err) {
|
|
|
|
// file does not exist, teat as first run and create it
|
|
|
|
ec.Logger.Debug("global config file does not exist, this could be the first run, creating it...")
|
|
|
|
u, err := uuid.NewV4()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to generate uuid")
|
|
|
|
}
|
|
|
|
gc := GlobalConfig{
|
|
|
|
UUID: u.String(),
|
|
|
|
EnableTelemetry: true,
|
|
|
|
}
|
|
|
|
data, err := json.MarshalIndent(gc, "", " ")
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "cannot marshal json for config file")
|
|
|
|
}
|
|
|
|
err = ioutil.WriteFile(ec.GlobalConfigFile, data, 0644)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "writing global config file failed")
|
|
|
|
}
|
|
|
|
ec.Logger.Debugf("global config file written at '%s' with content '%v'", ec.GlobalConfigFile, string(data))
|
|
|
|
// also show a notice about telemetry
|
|
|
|
ec.Logger.Info(StrTelemetryNotice)
|
|
|
|
} else if os.IsExist(err) || err == nil {
|
|
|
|
// file exists, verify contents
|
|
|
|
ec.Logger.Debug("global config file exisits, verifying contents")
|
|
|
|
data, err := ioutil.ReadFile(ec.GlobalConfigFile)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "reading global config file failed")
|
|
|
|
}
|
|
|
|
var gc GlobalConfig
|
|
|
|
err = json.Unmarshal(data, &gc)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "global config file not a valid json")
|
|
|
|
}
|
|
|
|
_, err = uuid.FromString(gc.UUID)
|
|
|
|
if err != nil {
|
|
|
|
ec.Logger.Debugf("invalid uuid '%s' in global config: %v", gc.UUID, err)
|
|
|
|
// create a new UUID
|
|
|
|
ec.Logger.Debug("global config file exists, but uuid is invalid, creating a new one...")
|
|
|
|
u, err := uuid.NewV4()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "failed to generate uuid")
|
|
|
|
}
|
|
|
|
gc.UUID = u.String()
|
|
|
|
data, err := json.Marshal(gc)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "cannot marshal json for config file")
|
|
|
|
}
|
|
|
|
err = ioutil.WriteFile(ec.GlobalConfigFile, data, 0644)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "writing global config file failed")
|
|
|
|
}
|
|
|
|
ec.Logger.Debugf("global config file written at '%s' with content '%v'", ec.GlobalConfigFile, string(data))
|
|
|
|
}
|
2018-06-24 16:40:48 +03:00
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// validateDirectory sets execution directory and validate it to see that or any
|
|
|
|
// of the parent directory is a valid project directory. A valid project
|
|
|
|
// directory contains the following:
|
|
|
|
// 1. migrations directory
|
|
|
|
// 2. config.yaml file
|
|
|
|
// 3. metadata.yaml (optional)
|
|
|
|
// If the current directory or any parent directory (upto filesystem root) is
|
|
|
|
// found to have these files, ExecutionDirectory is set as that directory.
|
|
|
|
func (ec *ExecutionContext) validateDirectory() error {
|
2018-06-24 16:40:48 +03:00
|
|
|
if len(ec.ExecutionDirectory) == 0 {
|
|
|
|
cwd, err := os.Getwd()
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "error getting current working directory")
|
|
|
|
}
|
|
|
|
ec.ExecutionDirectory = cwd
|
|
|
|
}
|
|
|
|
|
|
|
|
ed, err := os.Stat(ec.ExecutionDirectory)
|
|
|
|
if err != nil {
|
|
|
|
if os.IsNotExist(err) {
|
2018-06-24 16:47:01 +03:00
|
|
|
return errors.Wrap(err, "did not find required directory. use 'init'?")
|
2018-06-24 16:40:48 +03:00
|
|
|
} else {
|
|
|
|
return errors.Wrap(err, "error getting directory details")
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if !ed.IsDir() {
|
|
|
|
return errors.Errorf("'%s' is not a directory", ed.Name())
|
|
|
|
}
|
|
|
|
// config.yaml
|
|
|
|
// migrations/
|
|
|
|
// (optional) metadata.yaml
|
|
|
|
dir, err := recursivelyValidateDirectory(ec.ExecutionDirectory)
|
|
|
|
if err != nil {
|
|
|
|
return errors.Wrap(err, "validate")
|
|
|
|
}
|
|
|
|
|
|
|
|
ec.ExecutionDirectory = dir
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// filesRequired are the files that are mandatory to qualify for a project
|
|
|
|
// directory.
|
2018-06-24 16:40:48 +03:00
|
|
|
var filesRequired = []string{
|
|
|
|
"config.yaml",
|
|
|
|
"migrations",
|
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// recursivelyValidateDirectory tries to parse 'startFrom' as a project
|
|
|
|
// directory by checking for the 'filesRequired'. If the parent of 'startFrom'
|
|
|
|
// (nextDir) is filesystem root, error is returned. Otherwise, 'nextDir' is
|
|
|
|
// validated, recursively.
|
2018-06-24 16:40:48 +03:00
|
|
|
func recursivelyValidateDirectory(startFrom string) (validDir string, err error) {
|
|
|
|
err = validateDirectory(startFrom)
|
|
|
|
if err != nil {
|
|
|
|
nextDir := filepath.Dir(startFrom)
|
|
|
|
cleaned := filepath.Clean(nextDir)
|
|
|
|
isWindowsRoot, _ := regexp.MatchString(`^[a-zA-Z]:\\$`, cleaned)
|
|
|
|
// return error if filesystem boundary is hit
|
|
|
|
if cleaned == "/" || isWindowsRoot {
|
|
|
|
return nextDir, errors.Errorf("cannot find [%s] | search stopped at filesystem boundary", strings.Join(filesRequired, ", "))
|
|
|
|
|
|
|
|
}
|
|
|
|
return recursivelyValidateDirectory(nextDir)
|
|
|
|
}
|
|
|
|
return startFrom, nil
|
|
|
|
}
|
|
|
|
|
2018-06-24 16:47:01 +03:00
|
|
|
// validateDirectory tries to parse dir for the filesRequired and returns error
|
|
|
|
// if any one of them is missing.
|
2018-06-24 16:40:48 +03:00
|
|
|
func validateDirectory(dir string) error {
|
|
|
|
notFound := []string{}
|
|
|
|
for _, f := range filesRequired {
|
|
|
|
if _, err := os.Stat(filepath.Join(dir, f)); os.IsNotExist(err) {
|
|
|
|
relpath, e := filepath.Rel(dir, f)
|
|
|
|
if e == nil {
|
|
|
|
f = relpath
|
|
|
|
}
|
|
|
|
notFound = append(notFound, f)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if len(notFound) > 0 {
|
|
|
|
return errors.Errorf("cannot validate directory '%s': [%s] not found", dir, strings.Join(notFound, ", "))
|
|
|
|
}
|
|
|
|
return nil
|
|
|
|
}
|
2018-06-24 16:47:01 +03:00
|
|
|
|
2018-06-28 08:40:18 +03:00
|
|
|
// SetVersion sets the version inside context, according to the variable
|
2018-06-24 16:47:01 +03:00
|
|
|
// 'version' set during build context.
|
|
|
|
func (ec *ExecutionContext) setVersion() {
|
2018-07-04 15:43:52 +03:00
|
|
|
if ec.Version == nil {
|
|
|
|
ec.Version = version.New()
|
2018-06-24 16:47:01 +03:00
|
|
|
}
|
|
|
|
}
|