github.com/didip/deis@v1.4.1/deisctl/cmd/cmd.go (about)

     1  package cmd
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io/ioutil"
     7  	"net/http"
     8  	"os"
     9  	"path/filepath"
    10  	"regexp"
    11  	"strconv"
    12  	"strings"
    13  	"sync"
    14  	"time"
    15  
    16  	"github.com/deis/deis/deisctl/backend"
    17  	"github.com/deis/deis/deisctl/config"
    18  	"github.com/deis/deis/deisctl/utils"
    19  
    20  	docopt "github.com/docopt/docopt-go"
    21  )
    22  
    23  const (
    24  	// PlatformCommand is shorthand for "all the Deis components."
    25  	PlatformCommand string = "platform"
    26  )
    27  
    28  // ListUnits prints a list of installed units.
    29  func ListUnits(argv []string, b backend.Backend) error {
    30  	usage := `Prints a list of installed units.
    31  
    32  Usage:
    33    deisctl list [options]
    34  `
    35  	// parse command-line arguments
    36  	if _, err := docopt.Parse(usage, argv, true, "", false); err != nil {
    37  		return err
    38  	}
    39  	return b.ListUnits()
    40  }
    41  
    42  // ListUnitFiles prints the contents of all defined unit files.
    43  func ListUnitFiles(argv []string, b backend.Backend) error {
    44  	err := b.ListUnitFiles()
    45  	return err
    46  }
    47  
    48  // Scale grows or shrinks the number of running components.
    49  // Currently "router", "registry" and "store-gateway" are the only types that can be scaled.
    50  func Scale(argv []string, b backend.Backend) error {
    51  	usage := `Grows or shrinks the number of running components.
    52  
    53  Currently "router", "registry" and "store-gateway" are the only types that can be scaled.
    54  
    55  Usage:
    56    deisctl scale [<target>...] [options]
    57  `
    58  	// parse command-line arguments
    59  	args, err := docopt.Parse(usage, argv, true, "", false)
    60  	if err != nil {
    61  		return err
    62  	}
    63  	targets := args["<target>"].([]string)
    64  
    65  	outchan := make(chan string)
    66  	errchan := make(chan error)
    67  	var wg sync.WaitGroup
    68  
    69  	go printState(outchan, errchan, 500*time.Millisecond)
    70  
    71  	for _, target := range targets {
    72  		component, num, err := splitScaleTarget(target)
    73  		if err != nil {
    74  			return err
    75  		}
    76  		// the router is the only component that can scale at the moment
    77  		if !strings.Contains(component, "router") && !strings.Contains(component, "registry") && !strings.Contains(component, "store-gateway") {
    78  			return fmt.Errorf("cannot scale %s components", component)
    79  		}
    80  		b.Scale(component, num, &wg, outchan, errchan)
    81  		wg.Wait()
    82  	}
    83  	close(outchan)
    84  	return nil
    85  }
    86  
    87  // Start activates the specified components.
    88  func Start(argv []string, b backend.Backend) error {
    89  	usage := `Activates the specified components.
    90  
    91  Usage:
    92    deisctl start [<target>...] [options]
    93  `
    94  	// parse command-line arguments
    95  	args, err := docopt.Parse(usage, argv, true, "", false)
    96  	if err != nil {
    97  		return err
    98  	}
    99  
   100  	// if target is platform, install all services
   101  	targets := args["<target>"].([]string)
   102  
   103  	if len(targets) == 1 && targets[0] == PlatformCommand {
   104  		return StartPlatform(b)
   105  	}
   106  
   107  	outchan := make(chan string)
   108  	errchan := make(chan error)
   109  	var wg sync.WaitGroup
   110  
   111  	go printState(outchan, errchan, 500*time.Millisecond)
   112  
   113  	b.Start(targets, &wg, outchan, errchan)
   114  	wg.Wait()
   115  	close(outchan)
   116  
   117  	return nil
   118  }
   119  
   120  // checkRequiredKeys exist in etcd
   121  func checkRequiredKeys() error {
   122  	if err := config.CheckConfig("/deis/platform/", "domain"); err != nil {
   123  		return fmt.Errorf(`Missing platform domain, use:
   124  deisctl config platform set domain=<your-domain>`)
   125  	}
   126  
   127  	if err := config.CheckConfig("/deis/platform/", "sshPrivateKey"); err != nil {
   128  		fmt.Printf(`Warning: Missing sshPrivateKey, "deis run" will be unavailable. Use:
   129  deisctl config platform set sshPrivateKey=<path-to-key>
   130  `)
   131  	}
   132  	return nil
   133  }
   134  
   135  // StartPlatform activates all components.
   136  func StartPlatform(b backend.Backend) error {
   137  
   138  	outchan := make(chan string)
   139  	errchan := make(chan error)
   140  	var wg sync.WaitGroup
   141  
   142  	go printState(outchan, errchan, 500*time.Millisecond)
   143  
   144  	outchan <- utils.DeisIfy("Starting Deis...")
   145  
   146  	startDefaultServices(b, &wg, outchan, errchan)
   147  
   148  	wg.Wait()
   149  	close(outchan)
   150  
   151  	fmt.Println("Done.")
   152  	fmt.Println()
   153  	fmt.Println("Please use `deis register` to setup an administrator account.")
   154  	return nil
   155  }
   156  
   157  func startDefaultServices(b backend.Backend, wg *sync.WaitGroup, outchan chan string, errchan chan error) {
   158  
   159  	// create separate channels for background tasks
   160  	_outchan := make(chan string)
   161  	_errchan := make(chan error)
   162  	var _wg sync.WaitGroup
   163  
   164  	// wait for groups to come up
   165  	outchan <- fmt.Sprintf("Storage subsystem...")
   166  	b.Start([]string{"store-monitor"}, wg, outchan, errchan)
   167  	wg.Wait()
   168  	b.Start([]string{"store-daemon"}, wg, outchan, errchan)
   169  	wg.Wait()
   170  	b.Start([]string{"store-metadata"}, wg, outchan, errchan)
   171  	wg.Wait()
   172  
   173  	// we start gateway first to give metadata time to come up for volume
   174  	b.Start([]string{"store-gateway@*"}, wg, outchan, errchan)
   175  	wg.Wait()
   176  	b.Start([]string{"store-volume"}, wg, outchan, errchan)
   177  	wg.Wait()
   178  
   179  	// start logging subsystem first to collect logs from other components
   180  	outchan <- fmt.Sprintf("Logging subsystem...")
   181  	b.Start([]string{"logger"}, wg, outchan, errchan)
   182  	wg.Wait()
   183  	b.Start([]string{"logspout"}, wg, outchan, errchan)
   184  	wg.Wait()
   185  
   186  	// optimization: start all remaining services in the background
   187  	// cache is a special case because it must start before registry
   188  	b.Start([]string{"cache"}, &_wg, _outchan, _errchan)
   189  	wg.Wait()
   190  	b.Start([]string{
   191  		"database", "registry@*", "controller", "builder",
   192  		"publisher", "router@*"},
   193  		&_wg, _outchan, _errchan)
   194  
   195  	outchan <- fmt.Sprintf("Control plane...")
   196  	b.Start([]string{"cache", "database", "registry@*", "controller"}, wg, outchan, errchan)
   197  	wg.Wait()
   198  	b.Start([]string{"builder"}, wg, outchan, errchan)
   199  	wg.Wait()
   200  
   201  	outchan <- fmt.Sprintf("Data plane...")
   202  	b.Start([]string{"publisher"}, wg, outchan, errchan)
   203  	wg.Wait()
   204  
   205  	outchan <- fmt.Sprintf("Routing mesh...")
   206  	b.Start([]string{"router@*"}, wg, outchan, errchan)
   207  	wg.Wait()
   208  }
   209  
   210  // Stop deactivates the specified components.
   211  func Stop(argv []string, b backend.Backend) error {
   212  	usage := `Deactivates the specified components.
   213  
   214  Usage:
   215    deisctl stop [<target>...] [options]
   216  `
   217  	// parse command-line arguments
   218  	args, err := docopt.Parse(usage, argv, true, "", false)
   219  	if err != nil {
   220  		return err
   221  	}
   222  	targets := args["<target>"].([]string)
   223  
   224  	// if target is platform, stop all services
   225  	if len(targets) == 1 && targets[0] == PlatformCommand {
   226  		return StopPlatform(b)
   227  	}
   228  
   229  	outchan := make(chan string)
   230  	errchan := make(chan error)
   231  	var wg sync.WaitGroup
   232  
   233  	go printState(outchan, errchan, 500*time.Millisecond)
   234  
   235  	b.Stop(targets, &wg, outchan, errchan)
   236  	wg.Wait()
   237  	close(outchan)
   238  
   239  	return nil
   240  }
   241  
   242  // StopPlatform deactivates all components.
   243  func StopPlatform(b backend.Backend) error {
   244  
   245  	outchan := make(chan string)
   246  	errchan := make(chan error)
   247  	var wg sync.WaitGroup
   248  
   249  	go printState(outchan, errchan, 500*time.Millisecond)
   250  
   251  	outchan <- utils.DeisIfy("Stopping Deis...")
   252  
   253  	stopDefaultServices(b, &wg, outchan, errchan)
   254  
   255  	wg.Wait()
   256  	close(outchan)
   257  
   258  	fmt.Println("Done.")
   259  	fmt.Println()
   260  	fmt.Println("Please run `deisctl start platform` to restart Deis.")
   261  	return nil
   262  }
   263  
   264  func stopDefaultServices(b backend.Backend, wg *sync.WaitGroup, outchan chan string, errchan chan error) {
   265  
   266  	outchan <- fmt.Sprintf("Routing mesh...")
   267  	b.Stop([]string{"router@*"}, wg, outchan, errchan)
   268  	wg.Wait()
   269  
   270  	outchan <- fmt.Sprintf("Data plane...")
   271  	b.Stop([]string{"publisher"}, wg, outchan, errchan)
   272  	wg.Wait()
   273  
   274  	outchan <- fmt.Sprintf("Control plane...")
   275  	b.Stop([]string{"controller", "builder", "database", "registry@*"}, wg, outchan, errchan)
   276  	wg.Wait()
   277  	b.Stop([]string{"cache"}, wg, outchan, errchan)
   278  	wg.Wait()
   279  
   280  	outchan <- fmt.Sprintf("Logging subsystem...")
   281  	b.Stop([]string{"logger", "logspout"}, wg, outchan, errchan)
   282  	wg.Wait()
   283  
   284  	outchan <- fmt.Sprintf("Storage subsystem...")
   285  	b.Stop([]string{"store-volume", "store-gateway@*"}, wg, outchan, errchan)
   286  	wg.Wait()
   287  	b.Stop([]string{"store-metadata"}, wg, outchan, errchan)
   288  	wg.Wait()
   289  	b.Stop([]string{"store-daemon"}, wg, outchan, errchan)
   290  	wg.Wait()
   291  	b.Stop([]string{"store-monitor"}, wg, outchan, errchan)
   292  	wg.Wait()
   293  }
   294  
   295  // Restart stops and then starts the specified components.
   296  func Restart(argv []string, b backend.Backend) error {
   297  	usage := `Stops and then starts the specified components.
   298  
   299  Usage:
   300    deisctl restart [<target>...] [options]
   301  `
   302  	// parse command-line arguments
   303  	if _, err := docopt.Parse(usage, argv, true, "", false); err != nil {
   304  		return err
   305  	}
   306  
   307  	// act as if the user called "stop" and then "start"
   308  	argv[0] = "stop"
   309  	if err := Stop(argv, b); err != nil {
   310  		return err
   311  	}
   312  	argv[0] = "start"
   313  	return Start(argv, b)
   314  }
   315  
   316  // Status prints the current status of components.
   317  func Status(argv []string, b backend.Backend) error {
   318  	usage := `Prints the current status of components.
   319  
   320  Usage:
   321    deisctl status [<target>...] [options]
   322  `
   323  	// parse command-line arguments
   324  	args, err := docopt.Parse(usage, argv, true, "", false)
   325  	if err != nil {
   326  		return err
   327  	}
   328  
   329  	targets := args["<target>"].([]string)
   330  	for _, target := range targets {
   331  		if err := b.Status(target); err != nil {
   332  			return err
   333  		}
   334  	}
   335  	return nil
   336  }
   337  
   338  // Journal prints log output for the specified components.
   339  func Journal(argv []string, b backend.Backend) error {
   340  	usage := `Prints log output for the specified components.
   341  
   342  Usage:
   343    deisctl journal [<target>...] [options]
   344  `
   345  	// parse command-line arguments
   346  	args, err := docopt.Parse(usage, argv, true, "", false)
   347  	if err != nil {
   348  		return err
   349  	}
   350  
   351  	targets := args["<target>"].([]string)
   352  	for _, target := range targets {
   353  		if err := b.Journal(target); err != nil {
   354  			return err
   355  		}
   356  	}
   357  	return nil
   358  }
   359  
   360  // Install loads the definitions of components from local unit files.
   361  // After Install, the components will be available to Start.
   362  func Install(argv []string, b backend.Backend) error {
   363  	usage := `Loads the definitions of components from local unit files.
   364  
   365  After install, the components will be available to start.
   366  
   367  "deisctl install" looks for unit files in these directories, in this order:
   368  - the $DEISCTL_UNITS environment variable, if set
   369  - $HOME/.deis/units
   370  - /var/lib/deis/units
   371  
   372  Usage:
   373    deisctl install [<target>...] [options]
   374  `
   375  	// parse command-line arguments
   376  	args, err := docopt.Parse(usage, argv, true, "", false)
   377  	if err != nil {
   378  		return err
   379  	}
   380  
   381  	// if target is platform, install all services
   382  	targets := args["<target>"].([]string)
   383  	if len(targets) == 1 && targets[0] == PlatformCommand {
   384  		return InstallPlatform(b)
   385  	}
   386  
   387  	outchan := make(chan string)
   388  	errchan := make(chan error)
   389  	var wg sync.WaitGroup
   390  
   391  	go printState(outchan, errchan, 500*time.Millisecond)
   392  
   393  	// otherwise create the specific targets
   394  	b.Create(targets, &wg, outchan, errchan)
   395  	wg.Wait()
   396  
   397  	close(outchan)
   398  	return nil
   399  }
   400  
   401  // InstallPlatform loads all components' definitions from local unit files.
   402  // After InstallPlatform, all components will be available for StartPlatform.
   403  func InstallPlatform(b backend.Backend) error {
   404  
   405  	if err := checkRequiredKeys(); err != nil {
   406  		return err
   407  	}
   408  
   409  	outchan := make(chan string)
   410  	errchan := make(chan error)
   411  	var wg sync.WaitGroup
   412  
   413  	go printState(outchan, errchan, 500*time.Millisecond)
   414  
   415  	outchan <- utils.DeisIfy("Installing Deis...")
   416  
   417  	installDefaultServices(b, &wg, outchan, errchan)
   418  
   419  	wg.Wait()
   420  	close(outchan)
   421  
   422  	fmt.Println("Done.")
   423  	fmt.Println()
   424  	fmt.Println("Please run `deisctl start platform` to boot up Deis.")
   425  	return nil
   426  }
   427  
   428  func installDefaultServices(b backend.Backend, wg *sync.WaitGroup, outchan chan string, errchan chan error) {
   429  
   430  	outchan <- fmt.Sprintf("Storage subsystem...")
   431  	b.Create([]string{"store-daemon", "store-monitor", "store-metadata", "store-volume", "store-gateway@1"}, wg, outchan, errchan)
   432  	wg.Wait()
   433  
   434  	outchan <- fmt.Sprintf("Logging subsystem...")
   435  	b.Create([]string{"logger", "logspout"}, wg, outchan, errchan)
   436  	wg.Wait()
   437  
   438  	outchan <- fmt.Sprintf("Control plane...")
   439  	b.Create([]string{"cache", "database", "registry@1", "controller", "builder"}, wg, outchan, errchan)
   440  	wg.Wait()
   441  
   442  	outchan <- fmt.Sprintf("Data plane...")
   443  	b.Create([]string{"publisher"}, wg, outchan, errchan)
   444  	wg.Wait()
   445  
   446  	outchan <- fmt.Sprintf("Routing mesh...")
   447  	b.Create([]string{"router@1", "router@2", "router@3"}, wg, outchan, errchan)
   448  	wg.Wait()
   449  }
   450  
   451  // Uninstall unloads the definitions of the specified components.
   452  // After Uninstall, the components will be unavailable until Install is called.
   453  func Uninstall(argv []string, b backend.Backend) error {
   454  	usage := `Unloads the definitions of the specified components.
   455  
   456  After uninstall, the components will be unavailable until install is called.
   457  
   458  Usage:
   459    deisctl uninstall [<target>...] [options]
   460  `
   461  	// parse command-line arguments
   462  	args, err := docopt.Parse(usage, argv, true, "", false)
   463  	if err != nil {
   464  		return err
   465  	}
   466  
   467  	// if target is platform, uninstall all services
   468  	targets := args["<target>"].([]string)
   469  	if len(targets) == 1 && targets[0] == PlatformCommand {
   470  		return UninstallPlatform(b)
   471  	}
   472  
   473  	outchan := make(chan string)
   474  	errchan := make(chan error)
   475  	var wg sync.WaitGroup
   476  
   477  	go printState(outchan, errchan, 500*time.Millisecond)
   478  
   479  	// uninstall the specific target
   480  	b.Destroy(targets, &wg, outchan, errchan)
   481  	wg.Wait()
   482  	close(outchan)
   483  
   484  	return nil
   485  }
   486  
   487  // UninstallPlatform unloads all components' definitions.
   488  // After UninstallPlatform, all components will be unavailable.
   489  func UninstallPlatform(b backend.Backend) error {
   490  
   491  	outchan := make(chan string)
   492  	errchan := make(chan error)
   493  	var wg sync.WaitGroup
   494  
   495  	go printState(outchan, errchan, 500*time.Millisecond)
   496  
   497  	outchan <- utils.DeisIfy("Uninstalling Deis...")
   498  
   499  	uninstallAllServices(b, &wg, outchan, errchan)
   500  
   501  	wg.Wait()
   502  	close(outchan)
   503  
   504  	fmt.Println("Done.")
   505  	return nil
   506  }
   507  
   508  func uninstallAllServices(b backend.Backend, wg *sync.WaitGroup, outchan chan string, errchan chan error) error {
   509  
   510  	outchan <- fmt.Sprintf("Routing mesh...")
   511  	b.Destroy([]string{"router@*"}, wg, outchan, errchan)
   512  	wg.Wait()
   513  
   514  	outchan <- fmt.Sprintf("Data plane...")
   515  	b.Destroy([]string{"publisher"}, wg, outchan, errchan)
   516  	wg.Wait()
   517  
   518  	outchan <- fmt.Sprintf("Control plane...")
   519  	b.Destroy([]string{"controller", "builder", "cache", "database", "registry@*"}, wg, outchan, errchan)
   520  	wg.Wait()
   521  
   522  	outchan <- fmt.Sprintf("Logging subsystem...")
   523  	b.Destroy([]string{"logger", "logspout"}, wg, outchan, errchan)
   524  	wg.Wait()
   525  
   526  	outchan <- fmt.Sprintf("Storage subsystem...")
   527  	b.Destroy([]string{"store-volume", "store-gateway@*"}, wg, outchan, errchan)
   528  	wg.Wait()
   529  	b.Destroy([]string{"store-metadata"}, wg, outchan, errchan)
   530  	wg.Wait()
   531  	b.Destroy([]string{"store-daemon"}, wg, outchan, errchan)
   532  	wg.Wait()
   533  	b.Destroy([]string{"store-monitor"}, wg, outchan, errchan)
   534  	wg.Wait()
   535  
   536  	return nil
   537  }
   538  
   539  func printState(outchan chan string, errchan chan error, interval time.Duration) error {
   540  	for {
   541  		select {
   542  		case out := <-outchan:
   543  			// done on closed channel
   544  			if out == "" {
   545  				return nil
   546  			}
   547  			fmt.Println(out)
   548  		case err := <-errchan:
   549  			if err != nil {
   550  				fmt.Println(err.Error())
   551  				return err
   552  			}
   553  		}
   554  		time.Sleep(interval)
   555  	}
   556  }
   557  
   558  func splitScaleTarget(target string) (c string, num int, err error) {
   559  	r := regexp.MustCompile(`([a-z-]+)=([\d]+)`)
   560  	match := r.FindStringSubmatch(target)
   561  	if len(match) == 0 {
   562  		err = fmt.Errorf("Could not parse: %v", target)
   563  		return
   564  	}
   565  	c = match[1]
   566  	num, err = strconv.Atoi(match[2])
   567  	if err != nil {
   568  		return
   569  	}
   570  	return
   571  }
   572  
   573  // Config gets or sets a configuration value from the cluster.
   574  //
   575  // A configuration value is stored and retrieved from a key/value store (in this case, etcd)
   576  // at /deis/<component>/<config>. Configuration values are typically used for component-level
   577  // configuration, such as enabling TLS for the routers.
   578  func Config(argv []string) error {
   579  	usage := `Gets or sets a configuration value from the cluster.
   580  
   581  A configuration value is stored and retrieved from a key/value store
   582  (in this case, etcd) at /deis/<component>/<config>. Configuration
   583  values are typically used for component-level configuration, such as
   584  enabling TLS for the routers.
   585  
   586  Note: "deisctl config platform set sshPrivateKey=" expects a path
   587  to a private key.
   588  
   589  Usage:
   590    deisctl config <target> get [<key>...]
   591    deisctl config <target> set <key=val>...
   592  
   593  Examples:
   594    deisctl config platform set domain=mydomain.com
   595    deisctl config platform set sshPrivateKey=$HOME/.ssh/deis
   596    deisctl config controller get webEnabled
   597  `
   598  	// parse command-line arguments
   599  	args, err := docopt.Parse(usage, argv, true, "", false)
   600  	if err != nil {
   601  		return err
   602  	}
   603  	if err := config.Config(args); err != nil {
   604  		return err
   605  	}
   606  	return nil
   607  }
   608  
   609  // RefreshUnits overwrites local unit files with those requested.
   610  // Downloading from the Deis project GitHub URL by tag or SHA is the only mechanism
   611  // currently supported.
   612  func RefreshUnits(argv []string) error {
   613  	usage := `Overwrites local unit files with those requested.
   614  
   615  Downloading from the Deis project GitHub URL by tag or SHA is the only mechanism
   616  currently supported.
   617  
   618  "deisctl install" looks for unit files in these directories, in this order:
   619  - the $DEISCTL_UNITS environment variable, if set
   620  - $HOME/.deis/units
   621  - /var/lib/deis/units
   622  
   623  Usage:
   624    deisctl refresh-units [-p <target>] [-t <tag>]
   625  
   626  Options:
   627    -p --path=<target>   where to save unit files [default: $HOME/.deis/units]
   628    -t --tag=<tag>       git tag, branch, or SHA to use when downloading unit files
   629                         [default: v1.4.1]
   630  `
   631  	// parse command-line arguments
   632  	args, err := docopt.Parse(usage, argv, true, "", false)
   633  	if err != nil {
   634  		fmt.Printf("Error: %v\n", err)
   635  		os.Exit(2)
   636  	}
   637  	dir := args["--path"].(string)
   638  	dir = utils.ResolvePath(dir)
   639  	// create the target dir if necessary
   640  	if err := os.MkdirAll(dir, 0755); err != nil {
   641  		return err
   642  	}
   643  	// download and save the unit files to the specified path
   644  	rootURL := "https://raw.githubusercontent.com/deis/deis/"
   645  	tag := args["--tag"].(string)
   646  	units := []string{
   647  		"deis-builder.service",
   648  		"deis-cache.service",
   649  		"deis-controller.service",
   650  		"deis-database.service",
   651  		"deis-logger.service",
   652  		"deis-logspout.service",
   653  		"deis-publisher.service",
   654  		"deis-registry.service",
   655  		"deis-router.service",
   656  		"deis-store-admin.service",
   657  		"deis-store-daemon.service",
   658  		"deis-store-gateway.service",
   659  		"deis-store-metadata.service",
   660  		"deis-store-monitor.service",
   661  		"deis-store-volume.service",
   662  	}
   663  	for _, unit := range units {
   664  		src := rootURL + tag + "/deisctl/units/" + unit
   665  		dest := filepath.Join(dir, unit)
   666  		res, err := http.Get(src)
   667  		if err != nil {
   668  			return err
   669  		}
   670  		if res.StatusCode != 200 {
   671  			return errors.New(res.Status)
   672  		}
   673  		defer res.Body.Close()
   674  		data, err := ioutil.ReadAll(res.Body)
   675  		if err != nil {
   676  			return err
   677  		}
   678  		if err = ioutil.WriteFile(dest, data, 0644); err != nil {
   679  			return err
   680  		}
   681  		fmt.Printf("Refreshed %s from %s\n", unit, tag)
   682  	}
   683  	return nil
   684  }