github.com/m3db/m3@v1.5.0/src/integration/resources/inprocess/coordinator.go (about)

     1  // Copyright (c) 2021  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 inprocess contains code for spinning up M3 resources in-process for
    22  // the sake of integration testing.
    23  package inprocess
    24  
    25  import (
    26  	"errors"
    27  	"fmt"
    28  	"io/ioutil"
    29  	"net"
    30  	"net/http"
    31  	"os"
    32  	"strconv"
    33  	"time"
    34  
    35  	"github.com/prometheus/common/model"
    36  	"go.uber.org/zap"
    37  	"gopkg.in/yaml.v2"
    38  
    39  	"github.com/m3db/m3/src/cmd/services/m3query/config"
    40  	"github.com/m3db/m3/src/integration/resources"
    41  	nettest "github.com/m3db/m3/src/integration/resources/net"
    42  	"github.com/m3db/m3/src/query/api/v1/options"
    43  	"github.com/m3db/m3/src/query/generated/proto/admin"
    44  	"github.com/m3db/m3/src/query/generated/proto/prompb"
    45  	"github.com/m3db/m3/src/query/server"
    46  	xconfig "github.com/m3db/m3/src/x/config"
    47  	"github.com/m3db/m3/src/x/headers"
    48  	xos "github.com/m3db/m3/src/x/os"
    49  )
    50  
    51  const (
    52  	interruptTimeout = 5 * time.Second
    53  	shutdownTimeout  = time.Minute
    54  )
    55  
    56  //nolint:maligned
    57  // Coordinator is an in-process implementation of resources.Coordinator for use
    58  // in integration tests.
    59  type Coordinator struct {
    60  	cfg      config.Configuration
    61  	client   resources.CoordinatorClient
    62  	logger   *zap.Logger
    63  	tmpDirs  []string
    64  	embedded bool
    65  	startFn  CoordinatorStartFn
    66  	started  bool
    67  
    68  	interruptCh chan<- error
    69  	shutdownCh  <-chan struct{}
    70  }
    71  
    72  //nolint:maligned
    73  // CoordinatorOptions are options for starting a coordinator server.
    74  type CoordinatorOptions struct {
    75  	// GeneratePorts will automatically update the config to use open ports
    76  	// if set to true. If false, configuration is used as-is re: ports.
    77  	GeneratePorts bool
    78  	// StartFn is a custom function that can be used to start the Coordinator.
    79  	StartFn CoordinatorStartFn
    80  	// Start indicates whether to start the coordinator instance.
    81  	Start bool
    82  	// Logger is the logger to use for the coordinator. If not provided,
    83  	// a default one will be created.
    84  	Logger *zap.Logger
    85  }
    86  
    87  // NewCoordinatorFromConfigFile creates a new in-process coordinator based on the config file
    88  // and options provided.
    89  func NewCoordinatorFromConfigFile(pathToCfg string, opts CoordinatorOptions) (resources.Coordinator, error) {
    90  	var cfg config.Configuration
    91  	if err := xconfig.LoadFile(&cfg, pathToCfg, xconfig.Options{}); err != nil {
    92  		return nil, err
    93  	}
    94  
    95  	return NewCoordinator(cfg, opts)
    96  }
    97  
    98  // NewCoordinatorFromYAML creates a new in-process coordinator based on the YAML configuration string
    99  // and options provided.
   100  func NewCoordinatorFromYAML(yamlCfg string, opts CoordinatorOptions) (resources.Coordinator, error) {
   101  	var cfg config.Configuration
   102  	if err := yaml.Unmarshal([]byte(yamlCfg), &cfg); err != nil {
   103  		return nil, err
   104  	}
   105  
   106  	return NewCoordinator(cfg, opts)
   107  }
   108  
   109  // NewCoordinator creates a new in-process coordinator based on the configuration
   110  // and options provided. Use NewCoordinator or any of the convenience constructors
   111  // (e.g. NewCoordinatorFromYAML, NewCoordinatorFromConfigFile) to get a running
   112  // coordinator.
   113  //
   114  // The most typical usage of this method will be in an integration test to validate
   115  // some behavior. For example, assuming we have a running DB node already, we could
   116  // do the following to create a new namespace and write to it (note: ignoring error checking):
   117  //
   118  //    coord, _ := NewCoordinatorFromYAML(defaultCoordConfig, CoordinatorOptions{})
   119  //    coord.AddNamespace(admin.NamespaceAddRequest{...})
   120  //    coord.WaitForNamespace(namespaceName)
   121  //    coord.WriteProm("cpu", map[string]string{"host", host}, samples)
   122  //
   123  // The coordinator will start up as you specify in your config. However, there is some
   124  // helper logic to avoid port and filesystem collisions when spinning up multiple components
   125  // within the process. If you specify a GeneratePorts: true in the CoordinatorOptions, address ports
   126  // will be replaced with an open port.
   127  //
   128  // Similarly, filepath fields will  be updated with a temp directory that will be cleaned up
   129  // when the coordinator is destroyed. This should ensure that many of the same component can be
   130  // spun up in-process without any issues with collisions.
   131  func NewCoordinator(cfg config.Configuration, opts CoordinatorOptions) (resources.Coordinator, error) {
   132  	// Massage config so it runs properly in tests.
   133  	cfg, tmpDirs, err := updateCoordinatorConfig(cfg, opts)
   134  	if err != nil {
   135  		return nil, err
   136  	}
   137  
   138  	logging := cfg.LoggingOrDefault()
   139  	if len(logging.Fields) == 0 {
   140  		logging.Fields = make(map[string]interface{})
   141  	}
   142  	logging.Fields["component"] = "coordinator"
   143  	cfg.Logging = &logging
   144  
   145  	// Configure logger
   146  	if opts.Logger == nil {
   147  		opts.Logger, err = resources.NewLogger()
   148  		if err != nil {
   149  			return nil, err
   150  		}
   151  	}
   152  
   153  	// Get HTTP port
   154  	_, p, err := net.SplitHostPort(cfg.ListenAddressOrDefault())
   155  	if err != nil {
   156  		return nil, err
   157  	}
   158  
   159  	port, err := strconv.Atoi(p)
   160  	if err != nil {
   161  		return nil, err
   162  	}
   163  
   164  	// Start the coordinator
   165  	coord := &Coordinator{
   166  		cfg: cfg,
   167  		client: resources.NewCoordinatorClient(resources.CoordinatorClientOptions{
   168  			Client:    &http.Client{},
   169  			HTTPPort:  port,
   170  			Logger:    opts.Logger,
   171  			RetryFunc: resources.Retry,
   172  		}),
   173  		logger:  opts.Logger,
   174  		tmpDirs: tmpDirs,
   175  		startFn: opts.StartFn,
   176  	}
   177  	if opts.Start {
   178  		coord.Start()
   179  	}
   180  
   181  	return coord, nil
   182  }
   183  
   184  // NewEmbeddedCoordinator creates a coordinator from one embedded within an existing
   185  // db node. This method expects that the DB node has already been started before
   186  // being called.
   187  func NewEmbeddedCoordinator(d *DBNode) (resources.Coordinator, error) {
   188  	if !d.started {
   189  		return nil, errors.New("dbnode must be started to create the embedded coordinator")
   190  	}
   191  
   192  	_, p, err := net.SplitHostPort(d.cfg.Coordinator.ListenAddressOrDefault())
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	port, err := strconv.Atoi(p)
   198  	if err != nil {
   199  		return nil, err
   200  	}
   201  
   202  	return &Coordinator{
   203  		cfg: *d.cfg.Coordinator,
   204  		client: resources.NewCoordinatorClient(resources.CoordinatorClientOptions{
   205  			Client:    &http.Client{},
   206  			HTTPPort:  port,
   207  			Logger:    d.logger,
   208  			RetryFunc: resources.Retry,
   209  		}),
   210  		embedded:    true,
   211  		logger:      d.logger,
   212  		interruptCh: d.interruptCh,
   213  		shutdownCh:  d.shutdownCh,
   214  	}, nil
   215  }
   216  
   217  // Start is the start method for the coordinator.
   218  //nolint:dupl
   219  func (c *Coordinator) Start() {
   220  	if c.started {
   221  		c.logger.Debug("coordinator already started")
   222  		return
   223  	}
   224  	c.started = true
   225  
   226  	if c.startFn != nil {
   227  		c.interruptCh, c.shutdownCh = c.startFn(&c.cfg)
   228  		return
   229  	}
   230  
   231  	interruptCh := make(chan error, 1)
   232  	shutdownCh := make(chan struct{}, 1)
   233  
   234  	go func() {
   235  		server.Run(server.RunOptions{
   236  			Config:      c.cfg,
   237  			InterruptCh: interruptCh,
   238  			ShutdownCh:  shutdownCh,
   239  		})
   240  	}()
   241  
   242  	c.interruptCh = interruptCh
   243  	c.shutdownCh = shutdownCh
   244  }
   245  
   246  // HostDetails returns the coordinator's host details.
   247  func (c *Coordinator) HostDetails() (*resources.InstanceInfo, error) {
   248  	addr, p, err := net.SplitHostPort(c.cfg.ListenAddressOrDefault())
   249  	if err != nil {
   250  		return nil, err
   251  	}
   252  
   253  	port, err := strconv.Atoi(p)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  
   258  	var (
   259  		m3msgAddr string
   260  		m3msgPort int
   261  	)
   262  	if c.cfg.Ingest != nil {
   263  		a, p, err := net.SplitHostPort(c.cfg.Ingest.M3Msg.Server.ListenAddress)
   264  		if err != nil {
   265  			return nil, err
   266  		}
   267  
   268  		mp, err := strconv.Atoi(p)
   269  		if err != nil {
   270  			return nil, err
   271  		}
   272  
   273  		m3msgAddr, m3msgPort = a, mp
   274  	}
   275  
   276  	zone := headers.DefaultServiceZone
   277  	if len(c.cfg.Clusters) > 0 && c.cfg.Clusters[0].Client.EnvironmentConfig != nil {
   278  		envCfg := c.cfg.Clusters[0].Client.EnvironmentConfig
   279  		if len(envCfg.Services) > 0 && envCfg.Services[0].Service != nil {
   280  			zone = envCfg.Services[0].Service.Zone
   281  		}
   282  	}
   283  
   284  	return &resources.InstanceInfo{
   285  		ID:           "m3coordinator",
   286  		Zone:         zone,
   287  		Address:      addr,
   288  		Port:         uint32(port),
   289  		M3msgAddress: m3msgAddr,
   290  		M3msgPort:    uint32(m3msgPort),
   291  	}, nil
   292  }
   293  
   294  // GetNamespace gets namespaces.
   295  func (c *Coordinator) GetNamespace() (admin.NamespaceGetResponse, error) {
   296  	return c.client.GetNamespace()
   297  }
   298  
   299  // WaitForNamespace blocks until the given namespace is enabled.
   300  func (c *Coordinator) WaitForNamespace(name string) error {
   301  	return c.client.WaitForNamespace(name)
   302  }
   303  
   304  // AddNamespace adds a namespace.
   305  func (c *Coordinator) AddNamespace(request admin.NamespaceAddRequest) (admin.NamespaceGetResponse, error) {
   306  	return c.client.AddNamespace(request)
   307  }
   308  
   309  // UpdateNamespace updates the namespace.
   310  func (c *Coordinator) UpdateNamespace(request admin.NamespaceUpdateRequest) (admin.NamespaceGetResponse, error) {
   311  	return c.client.UpdateNamespace(request)
   312  }
   313  
   314  // DeleteNamespace removes the namespace.
   315  func (c *Coordinator) DeleteNamespace(namespaceID string) error {
   316  	return c.client.DeleteNamespace(namespaceID)
   317  }
   318  
   319  // CreateDatabase creates a database.
   320  func (c *Coordinator) CreateDatabase(request admin.DatabaseCreateRequest) (admin.DatabaseCreateResponse, error) {
   321  	return c.client.CreateDatabase(request)
   322  }
   323  
   324  // GetPlacement gets placements.
   325  func (c *Coordinator) GetPlacement(
   326  	opts resources.PlacementRequestOptions,
   327  ) (admin.PlacementGetResponse, error) {
   328  	return c.client.GetPlacement(opts)
   329  }
   330  
   331  // InitPlacement initializes placements.
   332  func (c *Coordinator) InitPlacement(
   333  	opts resources.PlacementRequestOptions,
   334  	req admin.PlacementInitRequest,
   335  ) (admin.PlacementGetResponse, error) {
   336  	return c.client.InitPlacement(opts, req)
   337  }
   338  
   339  // DeleteAllPlacements deletes all placements for the service specified
   340  // in the PlacementRequestOptions.
   341  func (c *Coordinator) DeleteAllPlacements(
   342  	opts resources.PlacementRequestOptions,
   343  ) error {
   344  	return c.client.DeleteAllPlacements(opts)
   345  }
   346  
   347  // WaitForInstances blocks until the given instance is available.
   348  func (c *Coordinator) WaitForInstances(ids []string) error {
   349  	return c.client.WaitForInstances(ids)
   350  }
   351  
   352  // WaitForShardsReady waits until all shards gets ready.
   353  func (c *Coordinator) WaitForShardsReady() error {
   354  	return c.client.WaitForShardsReady()
   355  }
   356  
   357  // WaitForClusterReady waits until the cluster is ready to receive reads and writes.
   358  func (c *Coordinator) WaitForClusterReady() error {
   359  	return c.client.WaitForClusterReady()
   360  }
   361  
   362  // Close closes the wrapper and releases any held resources, including
   363  // deleting docker containers.
   364  func (c *Coordinator) Close() error {
   365  	if c.embedded {
   366  		// NB(nate): for embedded coordinators, close is handled by the dbnode that
   367  		// it is spun up inside of.
   368  		return nil
   369  	}
   370  
   371  	defer func() {
   372  		for _, dir := range c.tmpDirs {
   373  			if err := os.RemoveAll(dir); err != nil {
   374  				c.logger.Error("error removing temp directory", zap.String("dir", dir), zap.Error(err))
   375  			}
   376  		}
   377  	}()
   378  
   379  	select {
   380  	case c.interruptCh <- xos.NewInterruptError("in-process coordinator being shut down"):
   381  	case <-time.After(interruptTimeout):
   382  		return errors.New("timeout sending interrupt. closing without graceful shutdown")
   383  	}
   384  
   385  	select {
   386  	case <-c.shutdownCh:
   387  	case <-time.After(shutdownTimeout):
   388  		return errors.New("timeout waiting for shutdown notification. coordinator closing may" +
   389  			" not be completely graceful")
   390  	}
   391  
   392  	c.started = false
   393  
   394  	return nil
   395  }
   396  
   397  // InitM3msgTopic initializes an m3msg topic.
   398  func (c *Coordinator) InitM3msgTopic(
   399  	opts resources.M3msgTopicOptions,
   400  	req admin.TopicInitRequest,
   401  ) (admin.TopicGetResponse, error) {
   402  	return c.client.InitM3msgTopic(opts, req)
   403  }
   404  
   405  // GetM3msgTopic gets an m3msg topic.
   406  func (c *Coordinator) GetM3msgTopic(
   407  	opts resources.M3msgTopicOptions,
   408  ) (admin.TopicGetResponse, error) {
   409  	return c.client.GetM3msgTopic(opts)
   410  }
   411  
   412  // AddM3msgTopicConsumer adds a consumer service to an m3msg topic.
   413  func (c *Coordinator) AddM3msgTopicConsumer(
   414  	opts resources.M3msgTopicOptions,
   415  	req admin.TopicAddRequest,
   416  ) (admin.TopicGetResponse, error) {
   417  	return c.client.AddM3msgTopicConsumer(opts, req)
   418  }
   419  
   420  // ApplyKVUpdate applies a KV update.
   421  func (c *Coordinator) ApplyKVUpdate(update string) error {
   422  	return c.client.ApplyKVUpdate(update)
   423  }
   424  
   425  // WriteCarbon writes a carbon metric datapoint at a given time.
   426  func (c *Coordinator) WriteCarbon(port int, metric string, v float64, t time.Time) error {
   427  	return c.client.WriteCarbon(fmt.Sprintf("0.0.0.0:%d", port), metric, v, t)
   428  }
   429  
   430  // WriteProm writes a prometheus metric. Takes tags/labels as a map for convenience.
   431  func (c *Coordinator) WriteProm(
   432  	name string,
   433  	tags map[string]string,
   434  	samples []prompb.Sample,
   435  	headers resources.Headers,
   436  ) error {
   437  	return c.client.WriteProm(name, tags, samples, headers)
   438  }
   439  
   440  // WritePromWithRequest executes a prometheus write request. Allows you to
   441  // provide the request directly which is useful for batch metric requests.
   442  func (c *Coordinator) WritePromWithRequest(writeRequest prompb.WriteRequest, headers resources.Headers) error {
   443  	return c.client.WritePromWithRequest(writeRequest, headers)
   444  }
   445  
   446  // RunQuery runs the given query with a given verification function.
   447  func (c *Coordinator) RunQuery(
   448  	verifier resources.ResponseVerifier,
   449  	query string,
   450  	headers resources.Headers,
   451  ) error {
   452  	return c.client.RunQuery(verifier, query, headers)
   453  }
   454  
   455  // InstantQuery runs an instant query with provided headers
   456  func (c *Coordinator) InstantQuery(
   457  	req resources.QueryRequest,
   458  	headers resources.Headers,
   459  ) (model.Vector, error) {
   460  	return c.client.InstantQuery(req, headers)
   461  }
   462  
   463  // InstantQueryWithEngine runs an instant query with provided headers and the specified
   464  // query engine.
   465  func (c *Coordinator) InstantQueryWithEngine(
   466  	req resources.QueryRequest,
   467  	engine options.QueryEngine,
   468  	headers resources.Headers,
   469  ) (model.Vector, error) {
   470  	return c.client.InstantQueryWithEngine(req, engine, headers)
   471  }
   472  
   473  // RangeQuery runs a range query with provided headers
   474  func (c *Coordinator) RangeQuery(
   475  	req resources.RangeQueryRequest,
   476  	headers resources.Headers,
   477  ) (model.Matrix, error) {
   478  	return c.client.RangeQuery(req, headers)
   479  }
   480  
   481  // GraphiteQuery retrieves graphite raw data.
   482  func (c *Coordinator) GraphiteQuery(req resources.GraphiteQueryRequest) ([]resources.Datapoint, error) {
   483  	return c.client.GraphiteQuery(req)
   484  }
   485  
   486  // RangeQueryWithEngine runs a range query with provided headers and the specified
   487  // query engine.
   488  func (c *Coordinator) RangeQueryWithEngine(
   489  	req resources.RangeQueryRequest,
   490  	engine options.QueryEngine,
   491  	headers resources.Headers,
   492  ) (model.Matrix, error) {
   493  	return c.client.RangeQueryWithEngine(req, engine, headers)
   494  }
   495  
   496  // LabelNames return matching label names based on the request.
   497  func (c *Coordinator) LabelNames(
   498  	req resources.LabelNamesRequest,
   499  	headers resources.Headers,
   500  ) (model.LabelNames, error) {
   501  	return c.client.LabelNames(req, headers)
   502  }
   503  
   504  // LabelValues returns matching label values based on the request.
   505  func (c *Coordinator) LabelValues(
   506  	req resources.LabelValuesRequest,
   507  	headers resources.Headers,
   508  ) (model.LabelValues, error) {
   509  	return c.client.LabelValues(req, headers)
   510  }
   511  
   512  // Series returns matching series based on the request.
   513  func (c *Coordinator) Series(
   514  	req resources.SeriesRequest,
   515  	headers resources.Headers,
   516  ) ([]model.Metric, error) {
   517  	return c.client.Series(req, headers)
   518  }
   519  
   520  // Configuration returns a copy of the configuration used to
   521  // start this coordinator.
   522  func (c *Coordinator) Configuration() config.Configuration {
   523  	return c.cfg
   524  }
   525  
   526  func updateCoordinatorConfig(
   527  	cfg config.Configuration,
   528  	opts CoordinatorOptions,
   529  ) (config.Configuration, []string, error) {
   530  	var (
   531  		tmpDirs []string
   532  		err     error
   533  	)
   534  	if opts.GeneratePorts {
   535  		// Replace any port with an open port
   536  		cfg, err = updateCoordinatorPorts(cfg)
   537  		if err != nil {
   538  			return config.Configuration{}, nil, err
   539  		}
   540  	}
   541  
   542  	// Replace any filepath with a temporary directory
   543  	cfg, tmpDirs, err = updateCoordinatorFilepaths(cfg)
   544  	if err != nil {
   545  		return config.Configuration{}, nil, err
   546  	}
   547  
   548  	return cfg, tmpDirs, nil
   549  }
   550  
   551  func updateCoordinatorPorts(cfg config.Configuration) (config.Configuration, error) {
   552  	addr, _, err := nettest.GeneratePort(cfg.ListenAddressOrDefault())
   553  	if err != nil {
   554  		return cfg, err
   555  	}
   556  	cfg.ListenAddress = &addr
   557  
   558  	metrics := cfg.MetricsOrDefault()
   559  	if metrics.PrometheusReporter != nil && metrics.PrometheusReporter.ListenAddress != "" {
   560  		addr, _, err := nettest.GeneratePort(metrics.PrometheusReporter.ListenAddress)
   561  		if err != nil {
   562  			return cfg, err
   563  		}
   564  		metrics.PrometheusReporter.ListenAddress = addr
   565  	}
   566  	cfg.Metrics = metrics
   567  
   568  	if cfg.RPC != nil && cfg.RPC.ListenAddress != "" {
   569  		addr, _, err := nettest.GeneratePort(cfg.RPC.ListenAddress)
   570  		if err != nil {
   571  			return cfg, err
   572  		}
   573  		cfg.RPC.ListenAddress = addr
   574  	}
   575  
   576  	if cfg.Ingest != nil && cfg.Ingest.M3Msg.Server.ListenAddress != "" {
   577  		addr, _, err := nettest.GeneratePort(cfg.Ingest.M3Msg.Server.ListenAddress)
   578  		if err != nil {
   579  			return cfg, err
   580  		}
   581  		cfg.Ingest.M3Msg.Server.ListenAddress = addr
   582  	}
   583  
   584  	if cfg.Carbon != nil && cfg.Carbon.Ingester != nil {
   585  		addr, _, err := nettest.GeneratePort(cfg.Carbon.Ingester.ListenAddressOrDefault())
   586  		if err != nil {
   587  			return cfg, err
   588  		}
   589  		cfg.Carbon.Ingester.ListenAddress = addr
   590  	}
   591  
   592  	return cfg, nil
   593  }
   594  
   595  func updateCoordinatorFilepaths(cfg config.Configuration) (config.Configuration, []string, error) {
   596  	tmpDirs := make([]string, 0, 1)
   597  
   598  	for _, cluster := range cfg.Clusters {
   599  		ec := cluster.Client.EnvironmentConfig
   600  		if ec != nil {
   601  			for _, svc := range ec.Services {
   602  				if svc != nil && svc.Service != nil {
   603  					dir, err := ioutil.TempDir("", "m3kv-*")
   604  					if err != nil {
   605  						return cfg, tmpDirs, err
   606  					}
   607  
   608  					tmpDirs = append(tmpDirs, dir)
   609  					svc.Service.CacheDir = dir
   610  				}
   611  			}
   612  		}
   613  	}
   614  
   615  	if cfg.ClusterManagement.Etcd != nil {
   616  		dir, err := ioutil.TempDir("", "m3kv-*")
   617  		if err != nil {
   618  			return cfg, tmpDirs, err
   619  		}
   620  
   621  		tmpDirs = append(tmpDirs, dir)
   622  		cfg.ClusterManagement.Etcd.CacheDir = dir
   623  	}
   624  
   625  	return cfg, tmpDirs, nil
   626  }