Files
fn-serverless/fn/deploy.go
2016-12-13 19:18:57 +01:00

244 lines
5.3 KiB
Go

package main
import (
"errors"
"fmt"
"io"
"net/http"
"os"
"path/filepath"
"time"
functions "github.com/iron-io/functions_go"
"github.com/urfave/cli"
)
func deploy() cli.Command {
cmd := deploycmd{
RoutesApi: functions.NewRoutesApi(),
}
var flags []cli.Flag
flags = append(flags, cmd.flags()...)
return cli.Command{
Name: "deploy",
ArgsUsage: "`APPNAME`",
Usage: "scan local directory for functions, build and push all of them to `APPNAME`.",
Flags: flags,
Action: cmd.scan,
}
}
type deploycmd struct {
appName string
*functions.RoutesApi
wd string
verbose bool
incremental bool
skippush bool
verbwriter io.Writer
}
func (p *deploycmd) flags() []cli.Flag {
return []cli.Flag{
cli.BoolFlag{
Name: "v",
Usage: "verbose mode",
Destination: &p.verbose,
},
cli.StringFlag{
Name: "d",
Usage: "working directory",
Destination: &p.wd,
EnvVar: "WORK_DIR",
Value: "./",
},
cli.BoolFlag{
Name: "i",
Usage: "uses incremental building",
Destination: &p.incremental,
},
cli.BoolFlag{
Name: "skip-push",
Usage: "does not push Docker built images onto Docker Hub - useful for local development.",
Destination: &p.skippush,
},
}
}
func (p *deploycmd) scan(c *cli.Context) error {
if c.Args().First() == "" {
return errors.New("application name is missing")
}
p.appName = c.Args().First()
p.verbwriter = verbwriter(p.verbose)
var walked bool
err := filepath.Walk(p.wd, func(path string, info os.FileInfo, err error) error {
if path != p.wd && info.IsDir() {
return filepath.SkipDir
}
if !isFuncfile(path, info) {
return nil
}
if p.incremental && !isstale(path) {
return nil
}
e := p.deploy(path)
if err != nil {
fmt.Fprintln(p.verbwriter, path, e)
}
now := time.Now()
os.Chtimes(path, now, now)
walked = true
return e
})
if err != nil {
fmt.Fprintf(p.verbwriter, "file walk error: %s\n", err)
}
if !walked {
return errors.New("No function file found.")
}
return nil
}
// deploy will take the found function and check for the presence of a
// Dockerfile, and run a three step process: parse functions file, build and
// push the container, and finally it will update function's route. Optionally,
// the route can be overriden inside the functions file.
func (p *deploycmd) deploy(path string) error {
fmt.Fprintln(p.verbwriter, "deploying", path)
funcfile, err := buildfunc(p.verbwriter, path)
if err != nil {
return err
}
if p.skippush {
return nil
}
if err := dockerpush(funcfile); err != nil {
return err
}
return p.route(path, funcfile)
}
func (p *deploycmd) route(path string, ff *funcfile) error {
if err := resetBasePath(p.Configuration); err != nil {
return fmt.Errorf("error setting endpoint: %v", err)
}
if ff.Path == nil {
_, path := appNamePath(ff.FullName())
ff.Path = &path
}
if ff.Memory == nil {
ff.Memory = new(int64)
}
if ff.Type == nil {
ff.Type = new(string)
}
if ff.Format == nil {
ff.Format = new(string)
}
if ff.MaxConcurrency == nil {
ff.MaxConcurrency = new(int)
}
if ff.Timeout == nil {
dur := time.Duration(0)
ff.Timeout = &dur
}
headers := make(map[string][]string)
for k, v := range ff.Headers {
headers[k] = []string{v}
}
body := functions.RouteWrapper{
Route: functions.Route{
Path: *ff.Path,
Image: ff.FullName(),
Memory: *ff.Memory,
Type_: *ff.Type,
Config: expandEnvConfig(ff.Config),
Headers: headers,
Format: *ff.Format,
MaxConcurrency: int32(*ff.MaxConcurrency),
Timeout: int32(ff.Timeout.Seconds()),
},
}
fmt.Fprintf(p.verbwriter, "updating API with app: %s route: %s name: %s \n", p.appName, *ff.Path, ff.Name)
wrapper, resp, err := p.AppsAppRoutesPost(p.appName, body)
if err != nil {
return fmt.Errorf("error getting routes: %v", err)
}
if resp.StatusCode == http.StatusBadRequest {
return fmt.Errorf("error storing this route: %s", wrapper.Error_.Message)
}
return nil
}
func expandEnvConfig(configs map[string]string) map[string]string {
for k, v := range configs {
configs[k] = os.ExpandEnv(v)
}
return configs
}
func isFuncfile(path string, info os.FileInfo) bool {
if info.IsDir() {
return false
}
basefn := filepath.Base(path)
for _, fn := range validfn {
if basefn == fn {
return true
}
}
return false
}
// Theory of operation: this takes an optimistic approach to detect whether a
// package must be rebuild/bump/deployed. It loads for all files mtime's and
// compare with functions.json own mtime. If any file is younger than
// functions.json, it triggers a rebuild.
// The problem with this approach is that depending on the OS running it, the
// time granularity of these timestamps might lead to false negatives - that is
// a package that is stale but it is not recompiled. A more elegant solution
// could be applied here, like https://golang.org/src/cmd/go/pkg.go#L1111
func isstale(path string) bool {
fi, err := os.Stat(path)
if err != nil {
return true
}
fnmtime := fi.ModTime()
dir := filepath.Dir(path)
err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
if info.IsDir() {
return nil
}
if info.ModTime().After(fnmtime) {
return errors.New("found stale package")
}
return nil
})
return err != nil
}