github.com/tonto/cli@v0.0.0-20180104210444-aec958fa47db/deploy.go (about)

     1  package main
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"strings"
     9  	"time"
    10  
    11  	client "github.com/fnproject/cli/client"
    12  	fnclient "github.com/fnproject/fn_go/client"
    13  	"github.com/fnproject/fn_go/models"
    14  	"github.com/urfave/cli"
    15  )
    16  
    17  func deploy() cli.Command {
    18  	cmd := deploycmd{
    19  		Fn: client.APIClient(),
    20  	}
    21  	var flags []cli.Flag
    22  	flags = append(flags, cmd.flags()...)
    23  	return cli.Command{
    24  		Name:   "deploy",
    25  		Usage:  "deploys a function to the functions server. (bumps, build, pushes and updates route)",
    26  		Flags:  flags,
    27  		Action: cmd.deploy,
    28  	}
    29  }
    30  
    31  type deploycmd struct {
    32  	appName string
    33  	*fnclient.Fn
    34  
    35  	wd       string
    36  	verbose  bool
    37  	local    bool
    38  	noCache  bool
    39  	registry string
    40  	all      bool
    41  }
    42  
    43  func (cmd *deploycmd) Registry() string {
    44  	return cmd.registry
    45  }
    46  
    47  func (p *deploycmd) flags() []cli.Flag {
    48  	return []cli.Flag{
    49  		cli.StringFlag{
    50  			Name:        "app",
    51  			Usage:       "app name to deploy to",
    52  			Destination: &p.appName,
    53  		},
    54  		cli.BoolFlag{
    55  			Name:        "verbose, v",
    56  			Usage:       "verbose mode",
    57  			Destination: &p.verbose,
    58  		},
    59  		cli.BoolFlag{
    60  			Name:        "no-cache",
    61  			Usage:       "Don't use Docker cache for the build",
    62  			Destination: &p.noCache,
    63  		},
    64  		cli.BoolFlag{
    65  			Name:        "local, skip-push", // todo: deprecate skip-push
    66  			Usage:       "does not push Docker built images onto Docker Hub - useful for local development.",
    67  			Destination: &p.local,
    68  		},
    69  		cli.StringFlag{
    70  			Name:        "registry",
    71  			Usage:       "Sets the Docker owner for images and optionally the registry. This will be prefixed to your function name for pushing to Docker registries. eg: `--registry username` will set your Docker Hub owner. `--registry registry.hub.docker.com/username` will set the registry and owner.",
    72  			Destination: &p.registry,
    73  		},
    74  		cli.BoolFlag{
    75  			Name:        "all",
    76  			Usage:       "if in root directory containing `app.yaml`, this will deploy all functions",
    77  			Destination: &p.all,
    78  		},
    79  	}
    80  }
    81  
    82  // deploy deploys a function or a set of functions for an app
    83  // By default this will deploy a single function, either the function in the current directory
    84  // or if an arg is passed in, a function in the path representing that arg, relative to the
    85  // current working directory.
    86  //
    87  // If user passes in --all flag, it will deploy all functions in an app. An app must have an `app.yaml`
    88  // file in it's root directory. The functions will be deployed based on the directory structure
    89  // on the file system (can be overridden using the `path` arg in each `func.yaml`. The index/root function
    90  // is the one that lives in the same directory as the app.yaml.
    91  func (p *deploycmd) deploy(c *cli.Context) error {
    92  	setRegistryEnv(p)
    93  
    94  	appName := ""
    95  
    96  	appf, err := loadAppfile()
    97  	if err != nil {
    98  		if _, ok := err.(*notFoundError); ok {
    99  			if p.all {
   100  				return err
   101  			}
   102  			// otherwise, it's ok
   103  		} else {
   104  			return err
   105  		}
   106  
   107  	} else {
   108  		appName = appf.Name
   109  	}
   110  	if p.appName != "" {
   111  		// flag overrides all
   112  		appName = p.appName
   113  	}
   114  
   115  	if appName == "" {
   116  		return errors.New("app name must be provided, try `--app APP_NAME`.")
   117  	}
   118  
   119  	if p.all {
   120  		return p.deployAll(c, appName, appf)
   121  	}
   122  	return p.deploySingle(c, appName, appf)
   123  }
   124  
   125  // deploySingle deploys a single function, either the current directory or if in the context
   126  // of an app and user provides relative path as the first arg, it will deploy that function.
   127  func (p *deploycmd) deploySingle(c *cli.Context, appName string, appf *appfile) error {
   128  	wd := getWd()
   129  
   130  	dir := wd
   131  	// if we're in the context of an app, first arg is path to the function
   132  	path := c.Args().First()
   133  	if path != "" {
   134  		fmt.Printf("Deploying function at: /%s\n", path)
   135  		dir = filepath.Join(wd, path)
   136  		err := os.Chdir(dir)
   137  		if err != nil {
   138  			return err
   139  		}
   140  		defer os.Chdir(wd) // todo: wrap this so we can log the error if changing back fails
   141  	}
   142  
   143  	fpath, ff, err := findAndParseFuncfile(dir)
   144  	if err != nil {
   145  		return err
   146  	}
   147  	if appf != nil {
   148  		if dir == wd {
   149  			setRootFuncInfo(ff, appf.Name)
   150  		}
   151  	}
   152  	return p.deployFunc(c, appName, wd, fpath, ff)
   153  }
   154  
   155  // deployAll deploys all functions in an app.
   156  func (p *deploycmd) deployAll(c *cli.Context, appName string, appf *appfile) error {
   157  	wd := getWd()
   158  
   159  	var funcFound bool
   160  	err := walkFuncs(wd, func(path string, ff *funcfile, err error) error {
   161  		if err != nil { // probably some issue with funcfile parsing, can decide to handle this differently if we'd like
   162  			return err
   163  		}
   164  		dir := filepath.Dir(path)
   165  		if dir == wd {
   166  			setRootFuncInfo(ff, appName)
   167  		} else {
   168  			// change dirs
   169  			err = os.Chdir(dir)
   170  			if err != nil {
   171  				return err
   172  			}
   173  			p2 := strings.TrimPrefix(dir, wd)
   174  			if ff.Name == "" {
   175  				ff.Name = strings.Replace(p2, "/", "-", -1)
   176  				if strings.HasPrefix(ff.Name, "-") {
   177  					ff.Name = ff.Name[1:]
   178  				}
   179  				// todo: should we prefix appname too?
   180  			}
   181  			if ff.Path == "" {
   182  				ff.Path = p2
   183  			}
   184  		}
   185  		err = p.deployFunc(c, appName, wd, path, ff)
   186  		if err != nil {
   187  			return fmt.Errorf("deploy error on %s: %v", path, err)
   188  		}
   189  
   190  		now := time.Now()
   191  		os.Chtimes(path, now, now)
   192  		funcFound = true
   193  		return nil
   194  	})
   195  	if err != nil {
   196  		return err
   197  	}
   198  
   199  	if !funcFound {
   200  		return errors.New("no functions found to deploy")
   201  	}
   202  	return nil
   203  }
   204  
   205  // deployFunc performs several actions to deploy to a functions server.
   206  // Parse func.yaml file, bump version, build image, push to registry, and
   207  // finally it will update function's route. Optionally,
   208  // the route can be overriden inside the func.yaml file.
   209  func (p *deploycmd) deployFunc(c *cli.Context, appName, baseDir, funcfilePath string, funcfile *funcfile) error {
   210  	if appName == "" {
   211  		return errors.New("app name must be provided, try `--app APP_NAME`.")
   212  	}
   213  	dir := filepath.Dir(funcfilePath)
   214  	// get name from directory if it's not defined
   215  	if funcfile.Name == "" {
   216  		funcfile.Name = filepath.Base(filepath.Dir(funcfilePath)) // todo: should probably make a copy of ff before changing it
   217  	}
   218  	if funcfile.Path == "" {
   219  		if dir == "." {
   220  			funcfile.Path = "/"
   221  		} else {
   222  			funcfile.Path = "/" + filepath.Base(dir)
   223  		}
   224  
   225  	}
   226  	fmt.Printf("Deploying %s to app: %s at path: %s\n", funcfile.Name, appName, funcfile.Path)
   227  
   228  	funcfile2, err := bumpIt(funcfilePath, Patch)
   229  	if err != nil {
   230  		return err
   231  	}
   232  	funcfile.Version = funcfile2.Version
   233  	// TODO: this whole funcfile handling needs some love, way too confusing. Only bump makes permanent changes to it.
   234  
   235  	_, err = buildfunc(c, funcfilePath, funcfile, p.noCache)
   236  	if err != nil {
   237  		return err
   238  	}
   239  
   240  	if !p.local {
   241  		if err := dockerPush(funcfile); err != nil {
   242  			return err
   243  		}
   244  	}
   245  
   246  	return p.updateRoute(c, appName, funcfile)
   247  }
   248  
   249  func setRootFuncInfo(ff *funcfile, appName string) {
   250  	if ff.Name == "" {
   251  		fmt.Println("setting name")
   252  		ff.Name = fmt.Sprintf("%s-root", appName)
   253  	}
   254  	if ff.Path == "" {
   255  		// then in root dir, so this will be deployed at /
   256  		ff.Path = "/"
   257  	}
   258  }
   259  
   260  func (p *deploycmd) updateRoute(c *cli.Context, appName string, ff *funcfile) error {
   261  	fmt.Printf("Updating route %s using image %s...\n", ff.Path, ff.ImageName())
   262  
   263  	routesCmd := routesCmd{client: client.APIClient()}
   264  	rt := &models.Route{}
   265  	if err := routeWithFuncFile(ff, rt); err != nil {
   266  		return fmt.Errorf("error getting route with funcfile: %s", err)
   267  	}
   268  	return routesCmd.putRoute(c, appName, ff.Path, rt)
   269  }
   270  
   271  func expandEnvConfig(configs map[string]string) map[string]string {
   272  	for k, v := range configs {
   273  		configs[k] = os.ExpandEnv(v)
   274  	}
   275  	return configs
   276  }
   277  
   278  // Theory of operation: this takes an optimistic approach to detect whether a
   279  // package must be rebuild/bump/deployed. It loads for all files mtime's and
   280  // compare with functions.json own mtime. If any file is younger than
   281  // functions.json, it triggers a rebuild.
   282  // The problem with this approach is that depending on the OS running it, the
   283  // time granularity of these timestamps might lead to false negatives - that is
   284  // a package that is stale but it is not recompiled. A more elegant solution
   285  // could be applied here, like https://golang.org/src/cmd/go/pkg.go#L1111
   286  func isstale(path string) bool {
   287  	fi, err := os.Stat(path)
   288  	if err != nil {
   289  		return true
   290  	}
   291  
   292  	fnmtime := fi.ModTime()
   293  	dir := filepath.Dir(path)
   294  	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
   295  		if info.IsDir() {
   296  			return nil
   297  		}
   298  		if info.ModTime().After(fnmtime) {
   299  			return errors.New("found stale package")
   300  		}
   301  		return nil
   302  	})
   303  
   304  	return err != nil
   305  }