fn: improve UX and publish/deploy command (#359)

* fn: improve UX and publish/deploy command

* fn: remove wrong use cases for deploy

* fn: fix regression introduced by merge
This commit is contained in:
C Cirello
2016-12-01 18:11:13 +01:00
committed by GitHub
parent c5696b1bbc
commit 9ac2539aeb
39 changed files with 453 additions and 469 deletions

View File

@@ -23,16 +23,13 @@ build:
- make test - make test
``` ```
`app` (optional) is the application name to which this function will be pushed `name` is the name and tag to which this function will be pushed to and the
to.
`image` is the name and tag to which this function will be pushed to and the
route updated to use it. route updated to use it.
`route` (optional) allows you to overwrite the calculated route from the path `path` (optional) allows you to overwrite the calculated route from the path
position. You may use it to override the calculated route. position. You may use it to override the calculated route.
`version` represents current version of the function. When publishing, it is `version` represents current version of the function. When deploying, it is
appended to the image as a tag. appended to the image as a tag.
`type` (optional) allows you to set the type of the route. `sync`, for functions `type` (optional) allows you to set the type of the route. `sync`, for functions

View File

@@ -42,12 +42,12 @@ the name of the function to run, in the form that python expects
(`module.function`). Where you would package the files into a `.zip` to upload (`module.function`). Where you would package the files into a `.zip` to upload
to Lambda, we just pass the list of files to `fn`. to Lambda, we just pass the list of files to `fn`.
## Publishing the function to IronFunctions ## Deploying the function to IronFunctions
Next we want to publish the function to our IronFunctions Next we want to deploy the function to our IronFunctions
```sh ```sh
$ fn publish -v -f -d ./irontest $ fn deploy -v -d ./irontest irontest
publishing irontest/hello_world:1/function.yaml deploying irontest/hello_world:1/function.yaml
Sending build context to Docker daemon 4.096 kB Sending build context to Docker daemon 4.096 kB
Step 1 : FROM iron/lambda-python2.7 Step 1 : FROM iron/lambda-python2.7
latest: Pulling from iron/lambda-python2.7 latest: Pulling from iron/lambda-python2.7
@@ -82,7 +82,7 @@ Next we want to publish the function to our IronFunctions
irontest/hello_world:1/function.yaml done irontest/hello_world:1/function.yaml done
``` ```
This will publish the generated function under the app `irontest` with `hello_world` as a route, e.g: This will deploy the generated function under the app `irontest` with `hello_world` as a route, e.g:
`http://<hostname>/r/irontest/hello_world:1`, `http://<hostname>/r/irontest/hello_world:1`,
You should also now see the generated Docker image. You should also now see the generated Docker image.
@@ -108,7 +108,7 @@ You should see the output.
## Calling the function from IronFunctions ## Calling the function from IronFunctions
The `fn call` command can call the published version with a given payload. The `fn call` command can call the deployed version with a given payload.
```sh ```sh
$ echo '{ "first_name": "Jon", "last_name": "Snow" }' | ./fn call irontest /hello_world:1 $ echo '{ "first_name": "Jon", "last_name": "Snow" }' | ./fn call irontest /hello_world:1

View File

@@ -48,8 +48,8 @@ If you only want to download the code, pass the `--download-only` flag. The
you tweak the settings on a command level. Finally, you can import a different version of your lambda function than the latest one you tweak the settings on a command level. Finally, you can import a different version of your lambda function than the latest one
by passing `--version <version>.` by passing `--version <version>.`
You can then publish the imported lambda as follows: You can then deploy the imported lambda as follows:
``` ```
./fn publish -d ./user/my-function ./fn deploy -d ./user/my-function user
```` ````
Now the function can be reached via ```http://$HOSTNAME/r/user/my-function``` Now the function can be reached via ```http://$HOSTNAME/r/user/my-function```

View File

@@ -9,7 +9,7 @@ Once it's pushed to a registry, you can use it by referencing it when adding a r
## Using fn ## Using fn
This is the easiest way to build, package and publish your functions. This is the easiest way to build, package and deploy your functions.

3
examples/blog/func.yaml Normal file
View File

@@ -0,0 +1,3 @@
name: iron/func-blog
build:
- ./build.sh

View File

@@ -1,3 +0,0 @@
image: iron/func-blog
build:
- ./build.sh

View File

@@ -0,0 +1,3 @@
name: iron/func-caddy-lb
build:
- ./build.sh

View File

@@ -1,3 +0,0 @@
image: iron/func-caddy-lb
build:
- ./build.sh

View File

@@ -0,0 +1,3 @@
name: iron/func-checker
build:
- ./build.sh

View File

@@ -1,3 +0,0 @@
image: iron/func-checker
build:
- ./build.sh

3
examples/echo/func.yaml Normal file
View File

@@ -0,0 +1,3 @@
name: iron/func-echo
build:
- ./build.sh

View File

@@ -1,3 +0,0 @@
image: iron/func-echo
build:
- ./build.sh

3
examples/error/func.yaml Normal file
View File

@@ -0,0 +1,3 @@
name: iron/func-error
build:
- ./build.sh

View File

@@ -1,3 +0,0 @@
image: iron/func-error
build:
- ./build.sh

View File

@@ -9,7 +9,7 @@ fn init <YOUR_DOCKERHUB_USERNAME>/hello
fn build fn build
# test it # test it
cat hello.payload.json | fn run cat hello.payload.json | fn run
# push it to Docker Hub for use with IronFunctions # push it to Docker Hub
fn push fn push
# Create a route to this function on IronFunctions # Create a route to this function on IronFunctions
fn routes create myapp /hello fn routes create myapp /hello

View File

@@ -5,11 +5,11 @@ This example will show you how to test and deploy Go (Golang) code to IronFuncti
### 1. Prepare the `func.yaml` file: ### 1. Prepare the `func.yaml` file:
At func.yaml you will find: At func.yaml you will find:
```yml ```yml
app: phpapp name: USERNAME/hello
route: /hello
image: USERNAME/hello
version: 0.0.1 version: 0.0.1
path: /hello
build: build:
- docker run --rm -v "$PWD":/worker -w /worker iron/php:dev composer install - docker run --rm -v "$PWD":/worker -w /worker iron/php:dev composer install
``` ```
@@ -21,7 +21,14 @@ the moment you try to test this function.
### 2. Build: ### 2. Build:
```sh ```sh
fn publish # build the function
fn build
# test it
cat hello.payload.json | fn run
# push it to Docker Hub
fn push
# Create a route to this function on IronFunctions
fn routes create phpapp /hello
``` ```
`-v` is optional, but it allows you to see how this function is being built. `-v` is optional, but it allows you to see how this function is being built.
@@ -31,7 +38,7 @@ fn publish
Now you can start jobs on your function. Let's quickly queue up a job to try it out. Now you can start jobs on your function. Let's quickly queue up a job to try it out.
```sh ```sh
cat hello.payload.json | fn run phpapp /hello cat hello.payload.json | fn call phpapp /hello
``` ```
Here's a curl example to show how easy it is to do in any language: Here's a curl example to show how easy it is to do in any language:

View File

@@ -1,5 +1,5 @@
app: phpapp name: USERNAME/hello
route: /hello version: 0.0.1
image: USERNAME/hello:0.0.1 path: /hello
build: build:
- docker run --rm -v "$PWD":/worker -w /worker iron/php:dev composer install - docker run --rm -v "$PWD":/worker -w /worker iron/php:dev composer install

View File

@@ -7,10 +7,9 @@ This example will show you how to test and deploy Go (Golang) code to IronFuncti
At func.yaml you will find: At func.yaml you will find:
```yml ```yml
app: pythonapp name: USERNAME/hello
route: /hello
image: USERNAME/hello
version: 0.0.1 version: 0.0.1
path: /hello
build: build:
- docker run --rm -v "$PWD":/worker -w /worker iron/python:2-dev pip install -t packages -r requirements.txt - docker run --rm -v "$PWD":/worker -w /worker iron/python:2-dev pip install -t packages -r requirements.txt
``` ```
@@ -22,7 +21,14 @@ the moment you try to test this function.
### 2. Build: ### 2. Build:
```sh ```sh
fn publish # build the function
fn build
# test it
cat hello.payload.json | fn run
# push it to Docker Hub
fn push
# Create a route to this function on IronFunctions
fn routes create pythonapp /hello
``` ```
`-v` is optional, but it allows you to see how this function is being built. `-v` is optional, but it allows you to see how this function is being built.
@@ -32,7 +38,7 @@ fn publish
Now you can start jobs on your function. Let's quickly queue up a job to try it out. Now you can start jobs on your function. Let's quickly queue up a job to try it out.
```sh ```sh
cat hello.payload.json | fn run pythonapp /hello cat hello.payload.json | fn call pythonapp /hello
``` ```
Here's a curl example to show how easy it is to do in any language: Here's a curl example to show how easy it is to do in any language:

View File

@@ -1,5 +0,0 @@
app: pythonapp
route: /hello
image: USERNAME/hello:0.0.1
build:
- docker run --rm -v "$PWD":/worker -w /worker iron/python:2-dev pip install -t packages -r requirements.txt

View File

@@ -11,10 +11,8 @@ docker run --rm -v "$PWD":/worker -w /worker iron/ruby:dev bundle install --stan
fn build fn build
# test it # test it
cat hello.payload.json | fn run cat hello.payload.json | fn run
# push it to Docker Hub for use with IronFunctions
fn push
# Create a route to this function on IronFunctions # Create a route to this function on IronFunctions
fn routes create myapp /hello fn deploy myapp
``` ```
Now surf to: http://localhost:8080/r/myapp/hello Now surf to: http://localhost:8080/r/myapp/hello

3
examples/redis/func.yaml Normal file
View File

@@ -0,0 +1,3 @@
name: iron/func-redis
build:
- ./build.sh

View File

@@ -1,3 +0,0 @@
image: iron/func-redis
build:
- ./build.sh

View File

@@ -11,10 +11,8 @@ docker run --rm -v "$PWD":/worker -w /worker iron/ruby:dev bundle install --stan
fn build fn build
# test it # test it
cat slack.payload | fn run cat slack.payload | fn run
# push it to Docker Hub for use with IronFunctions
fn push
# Create a route to this function on IronFunctions # Create a route to this function on IronFunctions
fn routes create slackbot /guppy fn deploy slackbot
# Change the route response header content-type to application/json # Change the route response header content-type to application/json
curl -X PUT http://127.0.0.1:8080/v1/apps/slackbot/routes/guppy -d '{ "route": { "headers": { "Content-type": ["application/json"] } } }' curl -X PUT http://127.0.0.1:8080/v1/apps/slackbot/routes/guppy -d '{ "route": { "headers": { "Content-type": ["application/json"] } } }'
``` ```

View File

@@ -0,0 +1,3 @@
name: iron/func-twitter
build:
- ./build.sh

View File

@@ -1,3 +0,0 @@
image: iron/func-twitter
build:
- ./build.sh

View File

@@ -62,9 +62,8 @@ myapp
$ fn apps create otherapp # create new app $ fn apps create otherapp # create new app
otherapp created otherapp created
$ fn apps describe otherapp # describe an app $ fn apps config otherapp # show app-specific configuration
app: otherapp this application has no configurations
no specific configuration
$ fn apps $ fn apps
myapp myapp
@@ -156,60 +155,20 @@ $ export API_URL="http://myfunctions.example.org/"
$ fn ... $ fn ...
``` ```
## Publish ## Bulk deploy
Also there is the publish command that is going to scan all local directory for Also there is the `deploy` command that is going to scan all local directory for
functions, rebuild them and push them to Docker Hub and update them in functions, rebuild them and push them to Docker Hub and update them in
IronFunction. IronFunction. It will use the `route` entry in the existing function file to
see the update in the daemon.
```sh ```sh
$ fn publish $ fn deploy APP
path result
/app/hello done
/app/hello-sync error: no Dockerfile found for this function
/app/test done
``` ```
It works by scanning all children directories of the current working directory, `fn deploy` expects that each directory to contain a file `func.yaml`
following this convention: which instructs `fn` on how to act with that particular update.
<pre><code>┌───────┐
│ ./ │
└───┬───┘
│ ┌───────┐
├────▶│ myapp │
│ └───┬───┘
│ │ ┌───────┐
│ ├────▶│route1 │
│ │ └───────┘
│ │ │ ┌─────────┐
│ │ ├────▶│subroute1│
│ │ │ └─────────┘
│ ┌───────┐
├────▶│ other │
│ └───┬───┘
│ │ ┌───────┐
│ ├────▶│route1 │
│ │ └───────┘</code></pre>
It will render this pattern of updates:
```sh
$ fn publish
path result
/myapp/route1/subroute1 done
/other/route1 done
```
It means that first subdirectory are always considered app names (e.g. `myapp`
and `other`), each subdirectory of these firsts are considered part of the route
(e.g. `route1/subroute1`).
`fn publish` expects that each directory to contain a file `func.yaml`
which instructs `fn` on how to act with that particular update, and a
Dockerfile which it is going to use to build the image and push to Docker Hub.
## Contributing ## Contributing

View File

@@ -8,35 +8,45 @@ import (
) )
func build() cli.Command { func build() cli.Command {
cmd := buildcmd{commoncmd: &commoncmd{}} cmd := buildcmd{}
flags := append([]cli.Flag{}, cmd.flags()...) flags := append([]cli.Flag{}, cmd.flags()...)
return cli.Command{ return cli.Command{
Name: "build", Name: "build",
Usage: "build function version", Usage: "build function version",
Flags: flags, Flags: flags,
Action: cmd.scan, Action: cmd.build,
} }
} }
type buildcmd struct { type buildcmd struct {
*commoncmd verbose bool
} }
func (b *buildcmd) scan(c *cli.Context) error { func (b *buildcmd) flags() []cli.Flag {
b.commoncmd.scan(b.walker) return []cli.Flag{
return nil cli.BoolFlag{
Name: "v",
Usage: "verbose mode",
Destination: &b.verbose,
},
} }
func (b *buildcmd) walker(path string, info os.FileInfo, err error) error {
walker(path, info, err, b.build)
return nil
} }
// build will take the found valid function and build it // build will take the found valid function and build it
func (b *buildcmd) build(path string) error { func (b *buildcmd) build(c *cli.Context) error {
fmt.Fprintln(b.verbwriter, "building", path) verbwriter := verbwriter(b.verbose)
ff, err := b.buildfunc(path) path, err := os.Getwd()
if err != nil {
return err
}
fn, err := findFuncfile(path)
if err != nil {
return err
}
fmt.Fprintln(verbwriter, "building", fn)
ff, err := buildfunc(verbwriter, fn)
if err != nil { if err != nil {
return err return err
} }

View File

@@ -15,35 +15,46 @@ var (
) )
func bump() cli.Command { func bump() cli.Command {
cmd := bumpcmd{commoncmd: &commoncmd{}} cmd := bumpcmd{}
flags := append([]cli.Flag{}, cmd.flags()...) flags := append([]cli.Flag{}, cmd.flags()...)
return cli.Command{ return cli.Command{
Name: "bump", Name: "bump",
Usage: "bump function version", Usage: "bump function version",
Flags: flags, Flags: flags,
Action: cmd.scan, Action: cmd.bump,
} }
} }
type bumpcmd struct { type bumpcmd struct {
*commoncmd verbose bool
} }
func (b *bumpcmd) scan(c *cli.Context) error { func (b *bumpcmd) flags() []cli.Flag {
b.commoncmd.scan(b.walker) return []cli.Flag{
return nil cli.BoolFlag{
Name: "v",
Usage: "verbose mode",
Destination: &b.verbose,
},
} }
func (b *bumpcmd) walker(path string, info os.FileInfo, err error) error {
walker(path, info, err, b.bump)
return nil
} }
// bump will take the found valid function and bump its version // bump will take the found valid function and bump its version
func (b *bumpcmd) bump(path string) error { func (b *bumpcmd) bump(c *cli.Context) error {
fmt.Fprintln(b.verbwriter, "bumping version for", path) verbwriter := verbwriter(b.verbose)
funcfile, err := parsefuncfile(path) path, err := os.Getwd()
if err != nil {
return err
}
fn, err := findFuncfile(path)
if err != nil {
return err
}
fmt.Fprintln(verbwriter, "bumping version for", fn)
funcfile, err := parsefuncfile(fn)
if err != nil { if err != nil {
return err return err
} }
@@ -66,7 +77,7 @@ func (b *bumpcmd) bump(path string) error {
funcfile.Version = newver.String() funcfile.Version = newver.String()
if err := storefuncfile(path, funcfile); err != nil { if err := storefuncfile(fn, funcfile); err != nil {
return err return err
} }

View File

@@ -11,159 +11,41 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"text/template" "text/template"
"time"
"github.com/iron-io/functions/fn/langs" "github.com/iron-io/functions/fn/langs"
"github.com/urfave/cli"
) )
func isFuncfile(path string, info os.FileInfo) bool { func verbwriter(verbose bool) io.Writer {
if info.IsDir() { verbwriter := ioutil.Discard
return false if verbose {
verbwriter = os.Stderr
}
return verbwriter
} }
basefn := filepath.Base(path) func buildfunc(verbwriter io.Writer, path string) (*funcfile, error) {
for _, fn := range validfn {
if basefn == fn {
return true
}
}
return false
}
func walker(path string, info os.FileInfo, err error, f func(path string) error) {
if err := f(path); err != nil {
fmt.Fprintln(os.Stderr, path, err)
}
}
type commoncmd struct {
wd string
verbose bool
force bool
recursively bool
verbwriter io.Writer
}
func (c *commoncmd) flags() []cli.Flag {
return []cli.Flag{
cli.StringFlag{
Name: "d",
Usage: "working directory",
Destination: &c.wd,
EnvVar: "WORK_DIR",
Value: "./",
},
cli.BoolFlag{
Name: "v",
Usage: "verbose mode",
Destination: &c.verbose,
},
cli.BoolFlag{
Name: "f",
Usage: "force updating of all functions that are already up-to-date",
Destination: &c.force,
},
cli.BoolFlag{
Name: "r",
Usage: "recursively scan all functions",
Destination: &c.recursively,
},
}
}
func (c *commoncmd) scan(walker func(path string, info os.FileInfo, err error) error) {
c.verbwriter = ioutil.Discard
if c.verbose {
c.verbwriter = os.Stderr
}
var walked bool
err := filepath.Walk(c.wd, func(path string, info os.FileInfo, err error) error {
if !c.recursively && path != c.wd && info.IsDir() {
return filepath.SkipDir
}
if !isFuncfile(path, info) {
return nil
}
if c.recursively && !c.force && !isstale(path) {
return nil
}
e := walker(path, info, err)
now := time.Now()
os.Chtimes(path, now, now)
walked = true
return e
})
if err != nil {
fmt.Fprintf(c.verbwriter, "file walk error: %s\n", err)
}
if !walked {
fmt.Println("No function file found.")
return
}
}
// Theory of operation: this takes an optimistic approach to detect whether a
// package must be rebuild/bump/published. 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
}
func (c commoncmd) buildfunc(path string) (*funcfile, error) {
funcfile, err := parsefuncfile(path) funcfile, err := parsefuncfile(path)
if err != nil { if err != nil {
return nil, err return nil, err
} }
if err := c.localbuild(path, funcfile.Build); err != nil { if err := localbuild(verbwriter, path, funcfile.Build); err != nil {
return nil, err return nil, err
} }
if err := c.dockerbuild(path, funcfile); err != nil { if err := dockerbuild(verbwriter, path, funcfile); err != nil {
return nil, err return nil, err
} }
return funcfile, nil return funcfile, nil
} }
func (c commoncmd) localbuild(path string, steps []string) error { func localbuild(verbwriter io.Writer, path string, steps []string) error {
for _, cmd := range steps { for _, cmd := range steps {
exe := exec.Command("/bin/sh", "-c", cmd) exe := exec.Command("/bin/sh", "-c", cmd)
exe.Dir = filepath.Dir(path) exe.Dir = filepath.Dir(path)
exe.Stderr = c.verbwriter exe.Stderr = verbwriter
exe.Stdout = c.verbwriter exe.Stdout = verbwriter
fmt.Fprintf(c.verbwriter, "- %s:\n", cmd)
if err := exe.Run(); err != nil { if err := exe.Run(); err != nil {
return fmt.Errorf("error running command %v (%v)", cmd, err) return fmt.Errorf("error running command %v (%v)", cmd, err)
} }
@@ -172,7 +54,7 @@ func (c commoncmd) localbuild(path string, steps []string) error {
return nil return nil
} }
func (c commoncmd) dockerbuild(path string, ff *funcfile) error { func dockerbuild(verbwriter io.Writer, path string, ff *funcfile) error {
dir := filepath.Dir(path) dir := filepath.Dir(path)
var helper langs.LangHelper var helper langs.LangHelper
@@ -275,7 +157,6 @@ func writeTmpDockerfile(dir string, ff *funcfile) error {
buffer.WriteString(s) buffer.WriteString(s)
buffer.WriteString("\"") buffer.WriteString("\"")
} }
fmt.Println(buffer.String())
t := template.Must(template.New("Dockerfile").Parse(tplDockerfile)) t := template.Must(template.New("Dockerfile").Parse(tplDockerfile))
err = t.Execute(fd, struct { err = t.Execute(fd, struct {
@@ -293,3 +174,25 @@ func extractEnvConfig(configs []string) map[string]string {
} }
return c return c
} }
func dockerpush(ff *funcfile) error {
cmd := exec.Command("docker", "push", ff.FullName())
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("error running docker push: %v", err)
}
return nil
}
func appNamePath(img string) (string, string) {
sep := strings.Index(img, "/")
if sep < 0 {
return "", ""
}
tag := strings.Index(img[sep:], ":")
if tag < 0 {
tag = len(img[sep:])
}
return img[:sep], img[sep : sep+tag]
}

239
fn/deploy.go Normal file
View File

@@ -0,0 +1,239 @@
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
}
body := functions.RouteWrapper{
Route: functions.Route{
Path: *ff.Path,
Image: ff.FullName(),
Memory: *ff.Memory,
Type_: *ff.Type,
Config: expandEnvConfig(ff.Config),
Headers: ff.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
}

View File

@@ -24,12 +24,11 @@ var (
) )
type funcfile struct { type funcfile struct {
App *string `yaml:"app,omitempty",json:"app,omitempty"`
Name string `yaml:"name,omitempty",json:"name,omitempty"` Name string `yaml:"name,omitempty",json:"name,omitempty"`
Version string `yaml:"version,omitempty",json:"version,omitempty"` Version string `yaml:"version,omitempty",json:"version,omitempty"`
Runtime *string `yaml:"runtime,omitempty",json:"runtime,omitempty"` Runtime *string `yaml:"runtime,omitempty",json:"runtime,omitempty"`
Entrypoint *string `yaml:"entrypoint,omitempty",json:"entrypoint,omitempty"` Entrypoint *string `yaml:"entrypoint,omitempty",json:"entrypoint,omitempty"`
Route *string `yaml:"route,omitempty",json:"route,omitempty"` Path *string `yaml:"path,omitempty",json:"path,omitempty"`
Type *string `yaml:"type,omitempty",json:"type,omitempty"` Type *string `yaml:"type,omitempty",json:"type,omitempty"`
Memory *int64 `yaml:"memory,omitempty",json:"memory,omitempty"` Memory *int64 `yaml:"memory,omitempty",json:"memory,omitempty"`
Format *string `yaml:"format,omitempty",json:"format,omitempty"` Format *string `yaml:"format,omitempty",json:"format,omitempty"`
@@ -62,14 +61,23 @@ func (ff *funcfile) RuntimeTag() (runtime, tag string) {
return rt[:tagpos], rt[tagpos+1:] return rt[:tagpos], rt[tagpos+1:]
} }
func findFuncfile() (*funcfile, error) { func findFuncfile(path string) (string, error) {
for _, fn := range validfn { for _, fn := range validfn {
if exists(fn) { fullfn := filepath.Join(path, fn)
if exists(fullfn) {
return fullfn, nil
}
}
return "", newNotFoundError("could not find function file")
}
func loadFuncfile() (*funcfile, error) {
fn, err := findFuncfile(".")
if err != nil {
return nil, err
}
return parsefuncfile(fn) return parsefuncfile(fn)
} }
}
return nil, newNotFoundError("could not find function file")
}
func parsefuncfile(path string) (*funcfile, error) { func parsefuncfile(path string) (*funcfile, error) {
ext := filepath.Ext(path) ext := filepath.Ext(path)

View File

@@ -91,7 +91,7 @@ func initFn() cli.Command {
func (a *initFnCmd) init(c *cli.Context) error { func (a *initFnCmd) init(c *cli.Context) error {
if !a.force { if !a.force {
ff, err := findFuncfile() ff, err := loadFuncfile()
if _, ok := err.(*notFoundError); !ok && err != nil { if _, ok := err.(*notFoundError); !ok && err != nil {
return err return err
} }
@@ -105,15 +105,23 @@ func (a *initFnCmd) init(c *cli.Context) error {
return err return err
} }
var ffmt *string
if a.format != "" {
ffmt = &a.format
}
ff := &funcfile{ ff := &funcfile{
Name: a.name, Name: a.name,
Runtime: &a.runtime, Runtime: &a.runtime,
Version: initialVersion, Version: initialVersion,
Entrypoint: &a.entrypoint, Entrypoint: &a.entrypoint,
Format: &a.format, Format: ffmt,
MaxConcurrency: &a.maxConcurrency, MaxConcurrency: &a.maxConcurrency,
} }
_, path := appNamePath(ff.FullName())
ff.Path = &path
if err := encodeFuncfileYAML("func.yaml", ff); err != nil { if err := encodeFuncfileYAML("func.yaml", ff); err != nil {
return err return err
} }
@@ -130,7 +138,7 @@ func (a *initFnCmd) buildFuncFile(c *cli.Context) error {
a.name = c.Args().First() a.name = c.Args().First()
if a.name == "" || strings.Contains(a.name, ":") { if a.name == "" || strings.Contains(a.name, ":") {
return errors.New("Please specify a name for your function in the following format <DOCKERHUB_USERNAME>/<FUNCTION_NAME>") return errors.New("Please specify a name for your function in the following format <DOCKERHUB_USERNAME>/<FUNCTION_NAME>.\nTry: fn init <DOCKERHUB_USERNAME>/<FUNCTION_NAME>")
} }
if exists("Dockerfile") { if exists("Dockerfile") {

View File

@@ -276,11 +276,10 @@ func basicImportHandler(functionName, tmpFileName string, opts *createImageOptio
func createFunctionYaml(opts createImageOptions) error { func createFunctionYaml(opts createImageOptions) error {
strs := strings.Split(opts.Name, "/") strs := strings.Split(opts.Name, "/")
route := fmt.Sprintf("/%s", strs[1]) path := fmt.Sprintf("/%s", strs[1])
funcDesc := &funcfile{ funcDesc := &funcfile{
App: &strs[0],
Name: opts.Name, Name: opts.Name,
Route: &route, Path: &path,
Config: opts.Config, Config: opts.Config,
} }

View File

@@ -27,9 +27,9 @@ ENVIRONMENT VARIABLES:
build(), build(),
bump(), bump(),
call(), call(),
deploy(),
initFn(), initFn(),
lambda(), lambda(),
publish(),
push(), push(),
routes(), routes(),
run(), run(),

View File

@@ -1,153 +0,0 @@
package main
import (
"fmt"
"net/http"
"os"
"os/exec"
"strings"
functions "github.com/iron-io/functions_go"
"github.com/urfave/cli"
)
func publish() cli.Command {
cmd := publishcmd{
commoncmd: &commoncmd{},
RoutesApi: functions.NewRoutesApi(),
}
var flags []cli.Flag
flags = append(flags, cmd.flags()...)
flags = append(flags, cmd.commoncmd.flags()...)
return cli.Command{
Name: "publish",
Usage: "scan local directory for functions, build and publish them.",
Flags: flags,
Action: cmd.scan,
}
}
type publishcmd struct {
*commoncmd
*functions.RoutesApi
skippush bool
}
func (p *publishcmd) flags() []cli.Flag {
return []cli.Flag{
cli.BoolFlag{
Name: "skip-push",
Usage: "does not push Docker built images onto Docker Hub - useful for local development.",
Destination: &p.skippush,
},
}
}
func (p *publishcmd) scan(c *cli.Context) error {
p.commoncmd.scan(p.walker)
return nil
}
func (p *publishcmd) walker(path string, info os.FileInfo, err error) error {
walker(path, info, err, p.publish)
return nil
}
// publish 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 *publishcmd) publish(path string) error {
fmt.Fprintln(p.verbwriter, "publishing", path)
funcfile, err := p.buildfunc(path)
if err != nil {
return err
}
if p.skippush {
return nil
}
if err := p.dockerpush(funcfile); err != nil {
return err
}
return p.route(path, funcfile)
}
func (p publishcmd) dockerpush(ff *funcfile) error {
cmd := exec.Command("docker", "push", ff.FullName())
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
return fmt.Errorf("error running docker push: %v", err)
}
return nil
}
func (p *publishcmd) route(path string, ff *funcfile) error {
if err := resetBasePath(p.Configuration); err != nil {
return fmt.Errorf("error setting endpoint: %v", err)
}
// TODO: This is just a nasty hack and should be cleaned up all the way
pathsSplit := strings.Split(ff.FullName(), "/")
if ff.App == nil {
ff.App = &pathsSplit[0]
}
if ff.Route == nil {
path := "/" + strings.Split(pathsSplit[1], ":")[0]
ff.Route = &path
}
if ff.Memory == nil {
ff.Memory = new(int64)
}
if ff.Type == nil {
ff.Type = new(string)
}
body := functions.RouteWrapper{
Route: functions.Route{
Path: *ff.Route,
Image: ff.FullName(),
AppName: *ff.App,
Memory: *ff.Memory,
Type_: *ff.Type,
Config: expandEnvConfig(ff.Config),
Headers: ff.Headers,
Timeout: int32(ff.Timeout.Seconds()),
MaxConcurrency: int32(*ff.MaxConcurrency),
},
}
fmt.Fprintf(p.verbwriter, "updating API with app: %s route: %s name: %s \n", *ff.App, *ff.Route, ff.Name)
wrapper, resp, err := p.AppsAppRoutesPost(*ff.App, 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 reverse(s string) string {
r := []rune(s)
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
r[i], r[j] = r[j], r[i]
}
return string(r)
}

View File

@@ -1,60 +1,59 @@
package main package main
import ( import (
"errors"
"fmt" "fmt"
"os"
functions "github.com/iron-io/functions_go"
"github.com/urfave/cli" "github.com/urfave/cli"
) )
func push() cli.Command { func push() cli.Command {
cmd := pushcmd{ cmd := pushcmd{}
publishcmd: &publishcmd{
commoncmd: &commoncmd{},
RoutesApi: functions.NewRoutesApi(),
},
}
var flags []cli.Flag var flags []cli.Flag
flags = append(flags, cmd.commoncmd.flags()...) flags = append(flags, cmd.flags()...)
return cli.Command{ return cli.Command{
Name: "push", Name: "push",
Usage: "push function to Docker Hub", Usage: "push function to Docker Hub",
Flags: flags, Flags: flags,
Action: cmd.scan, Action: cmd.push,
} }
} }
type pushcmd struct { type pushcmd struct {
*publishcmd verbose bool
} }
func (p *pushcmd) scan(c *cli.Context) error { func (p *pushcmd) flags() []cli.Flag {
p.commoncmd.scan(p.walker) return []cli.Flag{
return nil cli.BoolFlag{
Name: "v",
Usage: "verbose mode",
Destination: &p.verbose,
},
} }
func (p *pushcmd) walker(path string, info os.FileInfo, err error) error {
walker(path, info, err, p.push)
return nil
} }
// push will take the found function and check for the presence of a // push will take the found function and check for the presence of a
// Dockerfile, and run a three step process: parse functions file, // Dockerfile, and run a three step process: parse functions file,
// push the container, and finally it will update function's route. Optionally, // push the container, and finally it will update function's route. Optionally,
// the route can be overriden inside the functions file. // the route can be overriden inside the functions file.
func (p *pushcmd) push(path string) error { func (p *pushcmd) push(c *cli.Context) error {
fmt.Fprintln(p.verbwriter, "pushing", path) verbwriter := verbwriter(p.verbose)
funcfile, err := parsefuncfile(path) ff, err := loadFuncfile()
if err != nil { if err != nil {
if _, ok := err.(*notFoundError); ok {
return errors.New("error: image name is missing or no function file found")
}
return err return err
} }
if err := p.dockerpush(funcfile); err != nil { fmt.Fprintln(verbwriter, "pushing", ff.FullName())
if err := dockerpush(ff); err != nil {
return err return err
} }
fmt.Printf("Function %v pushed successfully to Docker Hub.\n", funcfile.FullName()) fmt.Printf("Function %v pushed successfully to Docker Hub.\n", ff.FullName())
return nil return nil
} }

View File

@@ -279,7 +279,7 @@ func (a *routesCmd) create(c *cli.Context) error {
timeout time.Duration timeout time.Duration
) )
if image == "" { if image == "" {
ff, err := findFuncfile() ff, err := loadFuncfile()
if err != nil { if err != nil {
if _, ok := err.(*notFoundError); ok { if _, ok := err.(*notFoundError); ok {
return errors.New("error: image name is missing or no function file found") return errors.New("error: image name is missing or no function file found")
@@ -512,7 +512,7 @@ func (a *routesCmd) headersList(c *cli.Context) error {
return errors.New("this route has no headers") return errors.New("this route has no headers")
} }
fmt.Println(wrapper.Route.AppName, wrapper.Route.Path, "headers:") fmt.Println(appName, wrapper.Route.Path, "headers:")
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0) w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0)
for k, v := range headers { for k, v := range headers {
fmt.Fprint(w, k, ":\t", v, "\n") fmt.Fprint(w, k, ":\t", v, "\n")
@@ -557,7 +557,7 @@ func (a *routesCmd) headersSet(c *cli.Context) error {
return fmt.Errorf("error updating route configuration: %v", err) return fmt.Errorf("error updating route configuration: %v", err)
} }
fmt.Println(wrapper.Route.AppName, wrapper.Route.Path, "headers updated", key, "with", value) fmt.Println(appName, wrapper.Route.Path, "headers updated", key, "with", value)
return nil return nil
} }
@@ -600,6 +600,6 @@ func (a *routesCmd) headersUnset(c *cli.Context) error {
return fmt.Errorf("error updating route configuration: %v", err) return fmt.Errorf("error updating route configuration: %v", err)
} }
fmt.Println(wrapper.Route.AppName, wrapper.Route.Path, "removed header", key) fmt.Println(appName, wrapper.Route.Path, "removed header", key)
return nil return nil
} }

View File

@@ -36,7 +36,7 @@ func runflags() []cli.Flag {
func (r *runCmd) run(c *cli.Context) error { func (r *runCmd) run(c *cli.Context) error {
image := c.Args().First() image := c.Args().First()
if image == "" { if image == "" {
ff, err := findFuncfile() ff, err := loadFuncfile()
if err != nil { if err != nil {
if _, ok := err.(*notFoundError); ok { if _, ok := err.(*notFoundError); ok {
return errors.New("error: image name is missing or no function file found") return errors.New("error: image name is missing or no function file found")