github.com/m3db/m3@v1.5.0/src/cmd/tools/m3ctl/main/main.go (about)

     1  // Copyright (c) 2020 Uber Technologies, Inc.
     2  //
     3  // Permission is hereby granted, free of charge, to any person obtaining a copy
     4  // of this software and associated documentation files (the "Software"), to deal
     5  // in the Software without restriction, including without limitation the rights
     6  // to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
     7  // copies of the Software, and to permit persons to whom the Software is
     8  // furnished to do so, subject to the following conditions:
     9  //
    10  // The above copyright notice and this permission notice shall be included in
    11  // all copies or substantial portions of the Software.
    12  //
    13  // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
    14  // IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
    15  // FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
    16  // AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
    17  // LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
    18  // OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
    19  // THE SOFTWARE.
    20  
    21  package main
    22  
    23  import (
    24  	"bytes"
    25  	"encoding/json"
    26  	"fmt"
    27  	"os"
    28  	"strings"
    29  
    30  	"github.com/m3db/m3/src/cmd/tools/m3ctl/apply"
    31  	"github.com/m3db/m3/src/cmd/tools/m3ctl/namespaces"
    32  	"github.com/m3db/m3/src/cmd/tools/m3ctl/placements"
    33  	"github.com/m3db/m3/src/cmd/tools/m3ctl/topics"
    34  	"github.com/m3db/m3/src/query/generated/proto/admin"
    35  
    36  	"github.com/gogo/protobuf/jsonpb"
    37  	"github.com/spf13/cobra"
    38  	"go.uber.org/zap"
    39  	"go.uber.org/zap/zapcore"
    40  )
    41  
    42  const (
    43  	defaultEndpoint = "http://localhost:7201"
    44  )
    45  
    46  // Defaults are (so output is easily consumable by JSON tools like "jq").
    47  // - Error log level so usually not printing anything unless error encountered
    48  // so the output can be completely JSON.
    49  // - Do not print log stack traces so errors aren't overwhelming output.
    50  var defaultLoggerOptions = loggerOptions{
    51  	level:            zapcore.ErrorLevel,
    52  	enableStacktrace: false,
    53  }
    54  
    55  type loggerOptions struct {
    56  	level            zapcore.Level
    57  	enableStacktrace bool
    58  }
    59  
    60  func mustNewLogger(opts loggerOptions) *zap.Logger {
    61  	loggerCfg := zap.NewDevelopmentConfig()
    62  	loggerCfg.Level = zap.NewAtomicLevelAt(opts.level)
    63  	loggerCfg.DisableStacktrace = !opts.enableStacktrace
    64  	logger, err := loggerCfg.Build()
    65  	if err != nil {
    66  		fmt.Fprintf(os.Stderr, err.Error())
    67  		os.Exit(1)
    68  	}
    69  
    70  	return logger
    71  }
    72  
    73  func main() {
    74  	var (
    75  		debug     bool
    76  		endPoint  string
    77  		headers   = make(map[string]string)
    78  		yamlPath  string
    79  		showAll   bool
    80  		deleteAll bool
    81  		nodeName  string
    82  	)
    83  
    84  	logger := mustNewLogger(defaultLoggerOptions)
    85  	defer func() {
    86  		logger.Sync()
    87  		fmt.Printf("\n") // End line since most commands finish without an endpoint.
    88  	}()
    89  
    90  	rootCmd := &cobra.Command{
    91  		Use: "m3ctl",
    92  	}
    93  
    94  	getCmd := &cobra.Command{
    95  		Use:   "get",
    96  		Short: "Get specified resources from the remote",
    97  	}
    98  
    99  	deleteCmd := &cobra.Command{
   100  		Use:   "delete",
   101  		Short: "Delete specified resources from the remote",
   102  	}
   103  
   104  	applyCmd := &cobra.Command{
   105  		Use:   "apply",
   106  		Short: "Apply various yamls to remote endpoint",
   107  		Long: `This will take specific yamls and send them over to the remote
   108  endpoint.  See the yaml/examples directory for examples. Operations such as
   109  database creation, database init, adding a node, and replacing a node, are supported.
   110  `,
   111  		Run: func(cmd *cobra.Command, args []string) {
   112  			fileArg := cmd.LocalFlags().Lookup("file").Value.String()
   113  			logger.Debug("running command", zap.String("name", cmd.Name()), zap.String("args", fileArg))
   114  
   115  			if len(fileArg) == 0 {
   116  				logger.Fatal("need to specify a path to YAML file")
   117  			}
   118  
   119  			resp, err := apply.DoApply(endPoint, headers, yamlPath, logger)
   120  			if err != nil {
   121  				logger.Fatal("apply failed", zap.Error(err))
   122  			}
   123  
   124  			os.Stdout.Write(resp)
   125  		},
   126  	}
   127  
   128  	getNamespaceCmd := &cobra.Command{
   129  		Use:     "namespace []",
   130  		Short:   "Get the namespaces from the remote endpoint",
   131  		Aliases: []string{"ns"},
   132  		Run: func(cmd *cobra.Command, args []string) {
   133  			logger.Debug("running command", zap.String("command", cmd.Name()))
   134  
   135  			resp, err := namespaces.DoGet(endPoint, headers, logger)
   136  			if err != nil {
   137  				logger.Fatal("get namespace failed", zap.Error(err))
   138  			}
   139  
   140  			if !showAll {
   141  				var registry admin.NamespaceGetResponse
   142  				unmarshaller := &jsonpb.Unmarshaler{AllowUnknownFields: true}
   143  				reader := bytes.NewReader(resp)
   144  				if err := unmarshaller.Unmarshal(reader, &registry); err != nil {
   145  					logger.Fatal("could not unmarshal response", zap.Error(err))
   146  				}
   147  				var namespaces []string
   148  				for k := range registry.Registry.Namespaces {
   149  					namespaces = append(namespaces, k)
   150  				}
   151  				// Keep output consistent and output JSON.
   152  				if err := json.NewEncoder(os.Stdout).Encode(namespaces); err != nil {
   153  					logger.Fatal("could not encode output", zap.Error(err))
   154  				}
   155  				return
   156  			}
   157  
   158  			os.Stdout.Write(resp)
   159  		},
   160  	}
   161  
   162  	getPlacementCmd := &cobra.Command{
   163  		Use:       "placement <m3db/m3coordinator/m3aggregator>",
   164  		Short:     "Get service placement from the remote endpoint",
   165  		Args:      cobra.ExactValidArgs(1),
   166  		ValidArgs: []string{"m3db", "m3coordinator", "m3aggregator"},
   167  		Aliases:   []string{"pl"},
   168  		Run: func(cmd *cobra.Command, args []string) {
   169  			logger.Debug("running command", zap.String("command", cmd.Name()))
   170  
   171  			resp, err := placements.DoGet(endPoint, args[0], headers, logger)
   172  			if err != nil {
   173  				logger.Fatal("get placement failed", zap.Error(err))
   174  			}
   175  
   176  			os.Stdout.Write(resp)
   177  		},
   178  	}
   179  
   180  	deletePlacementCmd := &cobra.Command{
   181  		Use:       "placement <m3db/m3coordinator/m3aggregator>",
   182  		Short:     "Delete service placement from the remote endpoint",
   183  		Args:      cobra.ExactValidArgs(1),
   184  		ValidArgs: []string{"m3db", "m3coordinator", "m3aggregator"},
   185  		Aliases:   []string{"pl"},
   186  		Run: func(cmd *cobra.Command, args []string) {
   187  			logger.Debug("running command", zap.String("command", cmd.Name()))
   188  
   189  			resp, err := placements.DoDelete(endPoint, args[0], headers, nodeName, deleteAll, logger)
   190  			if err != nil {
   191  				logger.Fatal("delete placement failed", zap.Error(err))
   192  			}
   193  
   194  			os.Stdout.Write(resp)
   195  		},
   196  	}
   197  
   198  	deleteNamespaceCmd := &cobra.Command{
   199  		Use:     "namespace",
   200  		Short:   "Delete the namespace from the remote endpoint",
   201  		Aliases: []string{"ns"},
   202  		Run: func(cmd *cobra.Command, args []string) {
   203  			logger.Debug("running command", zap.String("command", cmd.Name()))
   204  
   205  			resp, err := namespaces.DoDelete(endPoint, headers, nodeName, logger)
   206  			if err != nil {
   207  				logger.Fatal("delete namespace failed", zap.Error(err))
   208  			}
   209  
   210  			os.Stdout.Write(resp)
   211  		},
   212  	}
   213  
   214  	getTopicCmd := &cobra.Command{
   215  		Use:     "topic",
   216  		Short:   "Get topic from the remote endpoint",
   217  		Aliases: []string{"t"},
   218  		Run: func(cmd *cobra.Command, args []string) {
   219  			logger.Debug("running command", zap.String("command", cmd.Name()))
   220  
   221  			resp, err := topics.DoGet(endPoint, headers, logger)
   222  			if err != nil {
   223  				logger.Fatal("get topic failed", zap.Error(err))
   224  			}
   225  
   226  			os.Stdout.Write(resp) //nolint:errcheck
   227  		},
   228  	}
   229  
   230  	deleteTopicCmd := &cobra.Command{
   231  		Use:     "topic",
   232  		Short:   "Delete topic from the remote endpoint",
   233  		Aliases: []string{"t"},
   234  		Run: func(cmd *cobra.Command, args []string) {
   235  			logger.Debug("running command", zap.String("command", cmd.Name()))
   236  
   237  			resp, err := topics.DoDelete(endPoint, headers, logger)
   238  			if err != nil {
   239  				logger.Fatal("delete topic failed", zap.Error(err))
   240  			}
   241  
   242  			os.Stdout.Write(resp) //nolint:errcheck
   243  		},
   244  	}
   245  
   246  	rootCmd.AddCommand(getCmd, applyCmd, deleteCmd)
   247  	getCmd.AddCommand(getNamespaceCmd)
   248  	getCmd.AddCommand(getPlacementCmd)
   249  	getCmd.AddCommand(getTopicCmd)
   250  	deleteCmd.AddCommand(deletePlacementCmd)
   251  	deleteCmd.AddCommand(deleteNamespaceCmd)
   252  	deleteCmd.AddCommand(deleteTopicCmd)
   253  
   254  	var headersSlice []string
   255  	rootCmd.PersistentFlags().BoolVarP(&debug, "debug", "d", false, "debug log output level (cannot use JSON output)")
   256  	rootCmd.PersistentFlags().StringVar(&endPoint, "endpoint", defaultEndpoint, "m3coordinator endpoint URL")
   257  	rootCmd.PersistentFlags().StringSliceVarP(&headersSlice, "header", "H", []string{}, "headers to append to requests")
   258  	applyCmd.Flags().StringVarP(&yamlPath, "file", "f", "", "times to echo the input")
   259  	getNamespaceCmd.Flags().BoolVarP(&showAll, "show-all", "a", false, "times to echo the input")
   260  	deletePlacementCmd.Flags().BoolVarP(&deleteAll, "delete-all", "a", false, "delete the entire placement")
   261  	deleteCmd.PersistentFlags().StringVarP(&nodeName, "name", "n", "", "which namespace or node to delete")
   262  
   263  	rootCmd.PersistentPreRunE = func(cmd *cobra.Command, args []string) error {
   264  		// Override logger if debug flag set.
   265  		if debug {
   266  			logger = mustNewLogger(loggerOptions{
   267  				level:            zapcore.DebugLevel,
   268  				enableStacktrace: true,
   269  			})
   270  		}
   271  
   272  		// Parse headers slice.
   273  		for _, h := range headersSlice {
   274  			parts := strings.Split(h, ":")
   275  			if len(parts) != 2 {
   276  				return fmt.Errorf(
   277  					"header must be of format 'name: value': actual='%s'", h)
   278  			}
   279  
   280  			name, value := strings.TrimSpace(parts[0]), strings.TrimSpace(parts[1])
   281  			headers[name] = value
   282  		}
   283  
   284  		return nil
   285  	}
   286  
   287  	rootCmd.Execute()
   288  }