cmd/root.go (170 lines of code) (raw):
// Licensed to Elasticsearch B.V. under one or more contributor
// license agreements. See the NOTICE file distributed with
// this work for additional information regarding copyright
// ownership. Elasticsearch B.V. licenses this file to you under
// the Apache License, Version 2.0 (the "License"); you may
// not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing,
// software distributed under the License is distributed on an
// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
// KIND, either express or implied. See the License for the
// specific language governing permissions and limitations
// under the License.
package cmd
import (
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/elastic/cloud-sdk-go/pkg/api"
"github.com/elastic/cloud-sdk-go/pkg/multierror"
"github.com/elastic/cloud-sdk-go/pkg/output"
"github.com/spf13/cobra"
"github.com/spf13/viper"
cmdutil "github.com/elastic/ecctl/cmd/util"
"github.com/elastic/ecctl/pkg/ecctl"
)
const (
homePrefix = "$HOME"
)
var (
defaultClient = new(http.Client)
defaultOutput = os.Stdout
defaultInput = os.Stdin
defaultError = os.Stderr
defaultViper = viper.New()
ecctlHomePath = filepath.Join(homePrefix, ".ecctl")
bashCompletionFunc = `__ecctl_valid_regions()
{
COMPREPLY=($(echo ${EC_REGIONS}))
}
` + cmdutil.StatelessKindsCompFunc + "\n" +
cmdutil.AllKindsCompFunc
)
var (
versionInfo ecctl.VersionInfo
excludedApplicationCommands = []string{
"help", "version", "generate", "docs", "completions", "init",
}
messageErrHasNoPreRunCheck = "command %s/%s has no PreRunE check set"
)
// RootCmd represents the base command when called without any subcommands
var RootCmd = &cobra.Command{
Use: "ecctl",
Short: "Elastic Cloud Control",
SilenceErrors: true,
SilenceUsage: true,
DisableAutoGenTag: true,
CompletionOptions: cobra.CompletionOptions{
DisableDefaultCmd: true,
},
BashCompletionFunction: bashCompletionFunc,
PersistentPreRunE: func(cmd *cobra.Command, args []string) error {
setupViper(defaultViper)
if err := setupDebug(defaultViper.GetBool("trace"), defaultViper.GetBool("pprof")); err != nil {
return err
}
return initApp(cmd, defaultClient, defaultViper)
},
}
// Execute adds all child commands to the root command sets flags appropriately.
// This is called by main.main(). It only needs to happen once to the rootCmd.
// It returns the statuscode to be used by os.Exit.
func Execute(v ecctl.VersionInfo) int {
defer stopDebug(defaultViper)
populateValidArgs(RootCmd)
versionInfo = v
if err := RootCmd.Execute(); err != nil {
fmt.Fprintln(RootCmd.OutOrStderr(), err)
if ret, ok := err.(ecctl.ReturnCodeError); ok {
return ret.ReturnCode()
}
return -1
}
return 0
}
func init() {
RootCmd.PersistentFlags().String("config", "config", "Config name, used to have multiple configs in $HOME/.ecctl/<env>")
RootCmd.PersistentFlags().String("host", "", "Base URL to use")
RootCmd.PersistentFlags().String("user", "", "Username to use to authenticate (If empty will look for EC_USER environment variable)")
RootCmd.PersistentFlags().String("pass", "", "Password to use to authenticate (If empty will look for EC_PASS environment variable)")
RootCmd.PersistentFlags().String("api-key", "", "API key to use to authenticate (If empty will look for EC_API_KEY environment variable)")
RootCmd.PersistentFlags().Bool("verbose", false, "Enable verbose mode")
RootCmd.PersistentFlags().Bool("verbose-credentials", false, "When set, Authorization headers on the request/response trail will be displayed as plain text")
RootCmd.PersistentFlags().String("verbose-file", "", "When set, the verbose request/response trail will be written to the defined file")
RootCmd.PersistentFlags().String("output", "text", "Output format [text|json]")
RootCmd.PersistentFlags().Bool("force", false, "Do not ask for confirmation")
RootCmd.PersistentFlags().String("message", "", "A message to set on cluster operation")
RootCmd.PersistentFlags().String("format", "", "Formats the output using a Go template")
RootCmd.PersistentFlags().Bool("trace", false, "Enables tracing saves the trace to trace-20060102150405")
RootCmd.PersistentFlags().Bool("pprof", false, "Enables pprofing and saves the profile to pprof-20060102150405")
RootCmd.PersistentFlags().Bool("insecure", false, "Skips all TLS validation")
RootCmd.PersistentFlags().BoolP("quiet", "q", false, "Suppresses the configuration file used for the run, if any")
RootCmd.PersistentFlags().Duration("timeout", time.Second*30, "Timeout to use on all HTTP calls")
RootCmd.PersistentFlags().String("region", "", "Elasticsearch Service region")
RootCmd.Flag("region").Annotations = map[string][]string{
cobra.BashCompCustom: {"__ecctl_valid_regions"},
}
defaultViper.BindPFlags(RootCmd.PersistentFlags())
}
// setupViper configures a `*viper.Viper` instance for ecctl use.
func setupViper(v *viper.Viper) {
v.SetEnvPrefix("EC")
v.SetEnvKeyReplacer(strings.NewReplacer("-", "_"))
v.AutomaticEnv()
v.AddConfigPath(ecctlHomePath) // adding home directory as first search path
v.SetConfigName(v.GetString("config")) // name of config file (without extension)
// If a config file is found, read it in.
if err := v.ReadInConfig(); err == nil && v.GetBool("verbose") {
fmt.Fprintln(os.Stderr, "Using config file:", v.ConfigFileUsed())
}
// Register an alias value after the config file has been read.
v.RegisterAlias("api_key", "api-key")
v.RegisterAlias("verbose_file", "verbose-file")
v.RegisterAlias("verbose_credentials", "verbose-credentials")
}
// populateValidArgs dynamically generates the validargs for all of the cobra
// commands and subcommands
func populateValidArgs(cmd *cobra.Command) {
for _, c := range cmd.Commands() {
var args = append(c.Aliases, c.Name())
cmd.ValidArgs = append(cmd.ValidArgs, args...)
if cmd.HasAvailableSubCommands() {
populateValidArgs(c)
}
}
}
// GetCommand returns a child command from the command that is passed.
// If the command is not found, the parent is returned.
func GetCommand(command *cobra.Command, path ...string) *cobra.Command {
for _, p := range path {
for _, c := range command.Commands() {
if c.Name() == p {
return GetCommand(c, path[1:]...)
}
}
}
return command
}
func initApp(cmd *cobra.Command, client *http.Client, v *viper.Viper) error {
for _, cmdName := range excludedApplicationCommands {
if cmd.Name() == cmdName {
return nil
}
}
if client == nil {
return errors.New("cmd: root http client cannot be nil")
}
var c = ecctl.Config{
Client: client,
OutputDevice: output.NewDevice(defaultOutput),
ErrorDevice: defaultError,
UserAgent: strings.Join([]string{"ecctl", versionInfo.Version}, "/"),
}
if err := v.Unmarshal(&c); err != nil {
return err
}
// Set the default region to `ece-region` when the endpoint is not the ESS endpoint.
if c.Region == "" && c.Host != api.ESSEndpoint {
c.Region = cmdutil.DefaultECERegion
}
err := api.ReturnErrOnly(ecctl.Instance(c))
// When no config file has been read and initApp returns an error, tell
// the user how to initialize the application.
if err != nil && v.ConfigFileUsed() == "" {
return multierror.NewPrefixed(
`missing ecctl config file, please use the "ecctl init" command to initialize ecctl`, err,
)
}
return err
}
func checkPreRunE(command *cobra.Command) error {
var err = multierror.NewPrefixed("")
for _, c := range command.Commands() {
if command.HasSubCommands() {
err = err.Append(checkPreRunE(c))
}
if c.PreRunE == nil {
var message = fmt.Sprintf(messageErrHasNoPreRunCheck, command.Name(), c.Name())
err = err.Append(errors.New(message))
}
}
return err.ErrorOrNil()
}