github.com/iron-io/functions@v0.0.0-20180820112432-d59d7d1c40b2/fn/commands/images/deploy.go (about)

     1  package commands
     2  
     3  import (
     4  	"errors"
     5  	"fmt"
     6  	"io"
     7  	"net/http"
     8  	"os"
     9  	"path/filepath"
    10  	"time"
    11  
    12  	"github.com/iron-io/functions/fn/common"
    13  	functions "github.com/iron-io/functions_go"
    14  	"github.com/urfave/cli"
    15  )
    16  
    17  func Deploy() cli.Command {
    18  	cmd := deploycmd{
    19  		RoutesApi: functions.NewRoutesApi(),
    20  	}
    21  	var flags []cli.Flag
    22  	flags = append(flags, cmd.flags()...)
    23  	return cli.Command{
    24  		Name:      "deploy",
    25  		ArgsUsage: "<appName>",
    26  		Usage:     "scan local directory for functions, build and push all of them to `APPNAME`.",
    27  		Flags:     flags,
    28  		Action:    cmd.scan,
    29  	}
    30  }
    31  
    32  type deploycmd struct {
    33  	appName string
    34  	*functions.RoutesApi
    35  
    36  	wd          string
    37  	verbose     bool
    38  	incremental bool
    39  	skippush    bool
    40  
    41  	verbwriter io.Writer
    42  }
    43  
    44  func (p *deploycmd) flags() []cli.Flag {
    45  	return []cli.Flag{
    46  		cli.BoolFlag{
    47  			Name:        "v",
    48  			Usage:       "verbose mode",
    49  			Destination: &p.verbose,
    50  		},
    51  		cli.StringFlag{
    52  			Name:        "d",
    53  			Usage:       "working directory",
    54  			Destination: &p.wd,
    55  			EnvVar:      "WORK_DIR",
    56  			Value:       "./",
    57  		},
    58  		cli.BoolFlag{
    59  			Name:        "i",
    60  			Usage:       "uses incremental building",
    61  			Destination: &p.incremental,
    62  		},
    63  		cli.BoolFlag{
    64  			Name:        "skip-push",
    65  			Usage:       "does not push Docker built images onto Docker Hub - useful for local development.",
    66  			Destination: &p.skippush,
    67  		},
    68  	}
    69  }
    70  
    71  func (p *deploycmd) scan(c *cli.Context) error {
    72  	p.appName = c.Args().First()
    73  	p.verbwriter = common.Verbwriter(p.verbose)
    74  
    75  	var walked bool
    76  
    77  	err := filepath.Walk(p.wd, func(path string, info os.FileInfo, err error) error {
    78  		if path != p.wd && info.IsDir() {
    79  			return filepath.SkipDir
    80  		}
    81  
    82  		if !isFuncfile(path, info) {
    83  			return nil
    84  		}
    85  
    86  		if p.incremental && !isstale(path) {
    87  			return nil
    88  		}
    89  
    90  		e := p.deploy(path)
    91  		if err != nil {
    92  			fmt.Fprintln(p.verbwriter, path, e)
    93  		}
    94  
    95  		now := time.Now()
    96  		os.Chtimes(path, now, now)
    97  		walked = true
    98  		return e
    99  	})
   100  	if err != nil {
   101  		fmt.Fprintf(p.verbwriter, "file walk error: %s\n", err)
   102  	}
   103  
   104  	if !walked {
   105  		return errors.New("No function file found.")
   106  	}
   107  
   108  	return nil
   109  }
   110  
   111  // deploy will take the found function and check for the presence of a
   112  // Dockerfile, and run a three step process: parse functions file, build and
   113  // push the container, and finally it will update function's route. Optionally,
   114  // the route can be overriden inside the functions file.
   115  func (p *deploycmd) deploy(path string) error {
   116  	fmt.Fprintln(p.verbwriter, "deploying", path)
   117  
   118  	funcfile, err := common.Buildfunc(p.verbwriter, path)
   119  	if err != nil {
   120  		return err
   121  	}
   122  
   123  	if p.skippush {
   124  		return nil
   125  	}
   126  
   127  	if err := common.Dockerpush(funcfile); err != nil {
   128  		return err
   129  	}
   130  
   131  	return p.route(path, funcfile)
   132  }
   133  
   134  func (p *deploycmd) route(path string, ff *common.Funcfile) error {
   135  	if err := common.ResetBasePath(p.Configuration); err != nil {
   136  		return fmt.Errorf("error setting endpoint: %v", err)
   137  	}
   138  
   139  	if ff.Path == nil {
   140  		_, path := common.AppNamePath(ff.FullName())
   141  		ff.Path = &path
   142  	}
   143  
   144  	if ff.Memory == nil {
   145  		ff.Memory = new(int64)
   146  	}
   147  	if ff.Type == nil {
   148  		ff.Type = new(string)
   149  	}
   150  	if ff.Format == nil {
   151  		ff.Format = new(string)
   152  	}
   153  	if ff.MaxConcurrency == nil {
   154  		ff.MaxConcurrency = new(int)
   155  	}
   156  	if ff.Timeout == nil {
   157  		dur := time.Duration(0)
   158  		ff.Timeout = &dur
   159  	}
   160  
   161  	headers := make(map[string][]string)
   162  	for k, v := range ff.Headers {
   163  		headers[k] = []string{v}
   164  	}
   165  	body := functions.RouteWrapper{
   166  		Route: functions.Route{
   167  			Path:           *ff.Path,
   168  			Image:          ff.FullName(),
   169  			Memory:         *ff.Memory,
   170  			Type_:          *ff.Type,
   171  			Config:         expandEnvConfig(ff.Config),
   172  			Headers:        headers,
   173  			Format:         *ff.Format,
   174  			MaxConcurrency: int32(*ff.MaxConcurrency),
   175  			Timeout:        int32(ff.Timeout.Seconds()),
   176  		},
   177  	}
   178  
   179  	fmt.Fprintf(p.verbwriter, "updating API with app: %s route: %s name: %s \n", p.appName, *ff.Path, ff.Name)
   180  
   181  	wrapper, resp, err := p.AppsAppRoutesPost(p.appName, body)
   182  	if err != nil {
   183  		return fmt.Errorf("error getting routes: %v", err)
   184  	}
   185  	if resp.StatusCode == http.StatusBadRequest {
   186  		return fmt.Errorf("error storing this route: %s", wrapper.Error_.Message)
   187  	}
   188  
   189  	return nil
   190  }
   191  
   192  func expandEnvConfig(configs map[string]string) map[string]string {
   193  	for k, v := range configs {
   194  		configs[k] = os.ExpandEnv(v)
   195  	}
   196  	return configs
   197  }
   198  
   199  func isFuncfile(path string, info os.FileInfo) bool {
   200  	if info.IsDir() {
   201  		return false
   202  	}
   203  
   204  	basefn := filepath.Base(path)
   205  	for _, fn := range common.Validfn {
   206  		if basefn == fn {
   207  			return true
   208  		}
   209  	}
   210  
   211  	return false
   212  }
   213  
   214  // Theory of operation: this takes an optimistic approach to detect whether a
   215  // package must be rebuild/bump/deployed. It loads for all files mtime's and
   216  // compare with functions.json own mtime. If any file is younger than
   217  // functions.json, it triggers a rebuild.
   218  // The problem with this approach is that depending on the OS running it, the
   219  // time granularity of these timestamps might lead to false negatives - that is
   220  // a package that is stale but it is not recompiled. A more elegant solution
   221  // could be applied here, like https://golang.org/src/cmd/go/pkg.go#L1111
   222  func isstale(path string) bool {
   223  	fi, err := os.Stat(path)
   224  	if err != nil {
   225  		return true
   226  	}
   227  
   228  	fnmtime := fi.ModTime()
   229  	dir := filepath.Dir(path)
   230  	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
   231  		if info.IsDir() {
   232  			return nil
   233  		}
   234  		if info.ModTime().After(fnmtime) {
   235  			return errors.New("found stale package")
   236  		}
   237  		return nil
   238  	})
   239  
   240  	return err != nil
   241  }