mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
4
Makefile
4
Makefile
@@ -14,14 +14,14 @@ build-docker:
|
||||
docker build -t iron/functions:latest .
|
||||
|
||||
test:
|
||||
go test -v $(shell glide nv | grep -v examples | grep -v tool)
|
||||
go test -v $(shell glide nv | grep -v examples | grep -v tool | grep -v fnctl)
|
||||
|
||||
test-docker:
|
||||
docker run -ti --privileged --rm -e LOG_LEVEL=debug \
|
||||
-v /var/run/docker.sock:/var/run/docker.sock \
|
||||
-v $(DIR):/go/src/github.com/iron-io/functions \
|
||||
-w /go/src/github.com/iron-io/functions iron/go:dev go test \
|
||||
-v $(shell glide nv | grep -v examples | grep -v tool)
|
||||
-v $(shell glide nv | grep -v examples | grep -v tool | grep -v fnctl)
|
||||
|
||||
run:
|
||||
./functions
|
||||
|
||||
61
README.md
61
README.md
@@ -14,7 +14,7 @@ This guide will get you up and running in a few minutes.
|
||||
|
||||
### Run IronFunctions Container
|
||||
|
||||
To get started quickly with IronFunctions, you can just fire up an `iron/functions` container:
|
||||
To get started quickly with IronFunctions, you can just fire up an `iron/functions` container:
|
||||
|
||||
```sh
|
||||
docker run --rm --name functions --privileged -it -v $PWD/data:/app/data -p 8080:8080 iron/functions
|
||||
@@ -22,23 +22,50 @@ docker run --rm --name functions --privileged -it -v $PWD/data:/app/data -p 8080
|
||||
|
||||
**Note**: A list of configurations via env variables can be found [here](docs/api.md).*
|
||||
|
||||
### CLI tool
|
||||
|
||||
You can easily operate IronFunctions with its CLI tool. Install it with:
|
||||
|
||||
```sh
|
||||
curl -sSL https://fn.iron.io/install | sh
|
||||
```
|
||||
|
||||
If you're concerned about the [potential insecurity](http://curlpipesh.tumblr.com/)
|
||||
of using `curl | sh`, feel free to use a two-step version of our installation and examine our
|
||||
installation script:
|
||||
|
||||
```bash
|
||||
curl -f -sSL https://fn.iron.io/install -O
|
||||
sh install
|
||||
```
|
||||
|
||||
### Create an Application
|
||||
|
||||
An application is essentially a grouping of functions, that put together, form an API. Here's how to create an app.
|
||||
An application is essentially a grouping of functions, that put together, form an API. Here's how to create an app.
|
||||
|
||||
```sh
|
||||
fnctl apps create myapp
|
||||
```
|
||||
|
||||
Or using a cURL call:
|
||||
```sh
|
||||
curl -H "Content-Type: application/json" -X POST -d '{
|
||||
"app": { "name":"myapp" }
|
||||
}' http://localhost:8080/v1/apps
|
||||
```
|
||||
|
||||
Now that we have an app, we can map routes to functions.
|
||||
Now that we have an app, we can map routes to functions.
|
||||
|
||||
### Add a Route
|
||||
|
||||
A route is a way to define a path in your application that maps to a function. In this example, we'll map
|
||||
`/path` to a simple `Hello World!` image called `iron/hello`.
|
||||
`/path` to a simple `Hello World!` image called `iron/hello`.
|
||||
|
||||
```sh
|
||||
fnctl routes create myapp /hello iron/hello
|
||||
```
|
||||
|
||||
Or using a cURL call:
|
||||
```sh
|
||||
curl -H "Content-Type: application/json" -X POST -d '{
|
||||
"route": {
|
||||
@@ -50,33 +77,43 @@ curl -H "Content-Type: application/json" -X POST -d '{
|
||||
|
||||
### Calling your Function
|
||||
|
||||
Calling your function is as simple as requesting a URL. Each app has it's own namespace and each route mapped to the app.
|
||||
The app `myapp` that we created above along with the `/hello` route we added would be called via the following URL.
|
||||
Calling your function is as simple as requesting a URL. Each app has it's own namespace and each route mapped to the app.
|
||||
The app `myapp` that we created above along with the `/hello` route we added would be called via the following URL.
|
||||
|
||||
```sh
|
||||
fnctl routes run myapp /hello
|
||||
```
|
||||
|
||||
Or using a cURL call:
|
||||
```sh
|
||||
curl http://localhost:8080/r/myapp/hello
|
||||
```
|
||||
|
||||
Or just surf to it: http://localhost:8080/r/myapp/hello
|
||||
You also may just surf to it: http://localhost:8080/r/myapp/hello
|
||||
|
||||
### Passing data into a function
|
||||
|
||||
Your function will get the body of the HTTP request via STDIN, and the headers of the request will be passed in as env vars. Try this:
|
||||
|
||||
```sh
|
||||
echo '{"name":"Johnny"}' | fnctl routes run myapp /hello
|
||||
```
|
||||
|
||||
Or using a cURL call:
|
||||
```sh
|
||||
curl -H "Content-Type: application/json" -X POST -d '{
|
||||
"name":"Johnny"
|
||||
}' http://localhost:8080/r/myapp/hello
|
||||
```
|
||||
|
||||
You should see it say `Hello Johnny!` now instead of `Hello World!`.
|
||||
You should see it say `Hello Johnny!` now instead of `Hello World!`.
|
||||
|
||||
### Add an asynchronous function
|
||||
|
||||
IronFunctions supports synchronous function calls like we just tried above, and asynchronous for background processing.
|
||||
IronFunctions supports synchronous function calls like we just tried above, and asynchronous for background processing.
|
||||
|
||||
Asynchronous function calls are great for tasks that are CPU heavy or take more than a few seconds to complete.
|
||||
For instance, image processing, video processing, data processing, ETL, etc.
|
||||
Asynchronous function calls are great for tasks that are CPU heavy or take more than a few seconds to complete.
|
||||
For instance, image processing, video processing, data processing, ETL, etc.
|
||||
Architecturally, the main difference between synchronous and asynchronous is that requests
|
||||
to asynchronous functions are put in a queue and executed on upon resource availability so that they do not interfere with the fast synchronous responses required for an API.
|
||||
Also, since it uses a message queue, you can queue up millions of function calls without worrying about capacity as requests will
|
||||
@@ -126,4 +163,4 @@ TODO:
|
||||
|
||||
## More Documentation
|
||||
|
||||
See [docs/](docs/) for full documentation.
|
||||
See [docs/](docs/) for full documentation.
|
||||
|
||||
@@ -5,8 +5,8 @@
|
||||
swagger: '2.0'
|
||||
info:
|
||||
title: IronFunctions
|
||||
description:
|
||||
version: "0.0.8"
|
||||
description:
|
||||
version: "0.0.9"
|
||||
# the domain of the service
|
||||
host: "127.0.0.1:8080"
|
||||
# array of all schemes that your API supports
|
||||
@@ -140,15 +140,15 @@ paths:
|
||||
type: string
|
||||
- name: body
|
||||
in: body
|
||||
description: Array of routes to post.
|
||||
description: One route to post.
|
||||
required: true
|
||||
schema:
|
||||
$ref: '#/definitions/RoutesWrapper'
|
||||
$ref: '#/definitions/RouteWrapper'
|
||||
responses:
|
||||
201:
|
||||
description: Route created
|
||||
schema:
|
||||
$ref: '#/definitions/RoutesWrapper'
|
||||
$ref: '#/definitions/RouteWrapper'
|
||||
400:
|
||||
description: One or more of the routes were invalid due to parameters being missing or invalid.
|
||||
schema:
|
||||
@@ -269,9 +269,9 @@ definitions:
|
||||
readOnly: true
|
||||
path:
|
||||
type: string
|
||||
description: URL path that will be matched to this route
|
||||
description: URL path that will be matched to this route
|
||||
image:
|
||||
description: Name of Docker image to use in this route. You should include the image tag, which should be a version number, to be more accurate. Can be overridden on a per route basis with route.image.
|
||||
description: Name of Docker image to use in this route. You should include the image tag, which should be a version number, to be more accurate. Can be overridden on a per route basis with route.image.
|
||||
type: string
|
||||
headers:
|
||||
type: string
|
||||
@@ -296,7 +296,7 @@ definitions:
|
||||
$ref: '#/definitions/Route'
|
||||
cursor:
|
||||
type: string
|
||||
description: Used to paginate results. If this is returned, pass it into the same query again to get more results.
|
||||
description: Used to paginate results. If this is returned, pass it into the same query again to get more results.
|
||||
error:
|
||||
$ref: '#/definitions/ErrorBody'
|
||||
|
||||
@@ -376,7 +376,7 @@ definitions:
|
||||
env_vars:
|
||||
# this is a map: https://github.com/OAI/OpenAPI-Specification/blob/master/versions/2.0.md#model-with-mapdictionary-properties
|
||||
type: object
|
||||
description: Env vars for the task. Comes from the ones set on the Group.
|
||||
description: Env vars for the task. Comes from the ones set on the Group.
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
||||
@@ -404,7 +404,7 @@ definitions:
|
||||
properties:
|
||||
image:
|
||||
type: string
|
||||
description: Name of Docker image to use. This is optional and can be used to override the image defined at the group level.
|
||||
description: Name of Docker image to use. This is optional and can be used to override the image defined at the group level.
|
||||
payload:
|
||||
type: string
|
||||
# 256k
|
||||
|
||||
1
fnctl/.gitignore
vendored
Normal file
1
fnctl/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
fnctl
|
||||
13
fnctl/Dockerfile
Normal file
13
fnctl/Dockerfile
Normal file
@@ -0,0 +1,13 @@
|
||||
FROM alpine
|
||||
|
||||
|
||||
RUN apk --update upgrade && \
|
||||
apk add curl ca-certificates && \
|
||||
update-ca-certificates && \
|
||||
rm -rf /var/cache/apk/*
|
||||
|
||||
COPY entrypoint.sh /
|
||||
COPY fnctl /
|
||||
RUN chmod +x /entrypoint.sh
|
||||
|
||||
ENTRYPOINT ["/entrypoint.sh"]
|
||||
11
fnctl/Makefile
Normal file
11
fnctl/Makefile
Normal file
@@ -0,0 +1,11 @@
|
||||
all: vendor
|
||||
go build -o fnctl
|
||||
./fnctl
|
||||
|
||||
docker: vendor
|
||||
GOOS=linux go build -o fnctl
|
||||
docker build -t iron/fnctl .
|
||||
docker push iron/fnctl
|
||||
|
||||
vendor:
|
||||
go get -u .
|
||||
122
fnctl/README.md
Normal file
122
fnctl/README.md
Normal file
@@ -0,0 +1,122 @@
|
||||
# IronFunctions CLI
|
||||
|
||||
## Build
|
||||
|
||||
Ensure you have Go configured and installed in your environment. Once it is
|
||||
done, run:
|
||||
|
||||
```sh
|
||||
$ make
|
||||
```
|
||||
|
||||
It will build fnctl compatible with your local environment. You can test this
|
||||
CLI, right away with:
|
||||
|
||||
```sh
|
||||
$ ./fnctl
|
||||
```
|
||||
|
||||
## Basic
|
||||
You can operate IronFunctions from the command line.
|
||||
|
||||
```sh
|
||||
$ fnctl apps # list apps
|
||||
myapp
|
||||
|
||||
$ fnctl apps create otherapp # create new app
|
||||
otherapp created
|
||||
|
||||
$ fnctl apps
|
||||
myapp
|
||||
otherapp
|
||||
|
||||
$ fnctl routes myapp # list routes of an app
|
||||
path image
|
||||
/hello iron/hello
|
||||
|
||||
$ fnctl routes create otherapp /hello iron/hello # create route
|
||||
/hello created with iron/hello
|
||||
|
||||
$ fnctl routes delete otherapp hello # delete route
|
||||
/hello deleted
|
||||
```
|
||||
|
||||
## Bulk Update
|
||||
|
||||
Also there is the update command that is going to scan all local directory for
|
||||
functions, rebuild them and push them to Docker Hub and update them in
|
||||
IronFunction.
|
||||
|
||||
```sh
|
||||
$ fnctl update
|
||||
Updating for all functions.
|
||||
path action
|
||||
/app/hello updated
|
||||
/app/hello-sync error: no Dockerfile found for this function
|
||||
/app/test updated
|
||||
```
|
||||
|
||||
It works by scanning all children directories of the current working directory,
|
||||
following this convention:
|
||||
|
||||
<pre><code>┌───────┐
|
||||
│ ./ │
|
||||
└───┬───┘
|
||||
│ ┌───────┐
|
||||
├────▶│ myapp │
|
||||
│ └───┬───┘
|
||||
│ │ ┌───────┐
|
||||
│ ├────▶│route1 │
|
||||
│ │ └───────┘
|
||||
│ │ │ ┌─────────┐
|
||||
│ │ ├────▶│subroute1│
|
||||
│ │ │ └─────────┘
|
||||
│
|
||||
│ ┌───────┐
|
||||
├────▶│ other │
|
||||
│ └───┬───┘
|
||||
│ │ ┌───────┐
|
||||
│ ├────▶│route1 │
|
||||
│ │ └───────┘</code></pre>
|
||||
|
||||
|
||||
It will render this pattern of updates:
|
||||
|
||||
```sh
|
||||
$ fnctl update
|
||||
Updating for all functions.
|
||||
path action
|
||||
/myapp/route1/subroute1 updated
|
||||
/other/route1 updated
|
||||
```
|
||||
|
||||
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`).
|
||||
|
||||
`fnctl update` expects that each directory to contain a file `functions.yaml`
|
||||
which instructs `fnctl` 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.
|
||||
|
||||
```
|
||||
$ cat functions.yaml
|
||||
app: myapp
|
||||
image: iron/hello
|
||||
route: "/custom/route"
|
||||
build:
|
||||
- make
|
||||
- make test
|
||||
```
|
||||
|
||||
`app` (optional) is the application name to which this function will be pushed
|
||||
to.
|
||||
|
||||
`image` is the name and tag to which this function will be pushed to and the
|
||||
route updated to use it.
|
||||
|
||||
`route` (optional) allows you to overwrite the calculated route from the path
|
||||
position. You may use it to override the calculated route.
|
||||
|
||||
`build` (optional) is an array of shell calls which are used to helping building
|
||||
the image. These calls are executed before `fnctl` calls `docker build` and
|
||||
`docker push`.
|
||||
70
fnctl/apps.go
Normal file
70
fnctl/apps.go
Normal file
@@ -0,0 +1,70 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"github.com/iron-io/functions_go"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
type appsCmd struct {
|
||||
*functions.AppsApi
|
||||
}
|
||||
|
||||
func apps() cli.Command {
|
||||
a := appsCmd{AppsApi: functions.NewAppsApi()}
|
||||
|
||||
return cli.Command{
|
||||
Name: "apps",
|
||||
Usage: "list apps",
|
||||
ArgsUsage: "fnclt apps",
|
||||
Flags: append(confFlags(&a.Configuration), []cli.Flag{}...),
|
||||
Action: a.list,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "create",
|
||||
Usage: "create a new app",
|
||||
Action: a.create,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *appsCmd) list(c *cli.Context) error {
|
||||
resetBasePath(&a.Configuration)
|
||||
|
||||
wrapper, _, err := a.AppsGet()
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting app: %v", err)
|
||||
}
|
||||
|
||||
if len(wrapper.Apps) == 0 {
|
||||
fmt.Println("no apps found")
|
||||
return nil
|
||||
}
|
||||
|
||||
for _, app := range wrapper.Apps {
|
||||
fmt.Println(app.Name)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *appsCmd) create(c *cli.Context) error {
|
||||
if c.Args().First() == "" {
|
||||
return errors.New("error: app creating takes one argument, an app name")
|
||||
}
|
||||
|
||||
resetBasePath(&a.Configuration)
|
||||
|
||||
appName := c.Args().Get(0)
|
||||
body := functions.AppWrapper{App: functions.App{Name: appName}}
|
||||
wrapper, _, err := a.AppsPost(body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating app: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(wrapper.App.Name, "created")
|
||||
return nil
|
||||
}
|
||||
7
fnctl/entrypoint.sh
Normal file
7
fnctl/entrypoint.sh
Normal file
@@ -0,0 +1,7 @@
|
||||
#!/bin/sh
|
||||
|
||||
HOST=$(/sbin/ip route|awk '/default/ { print $3 }')
|
||||
|
||||
echo "$HOST default localhost localhost.local" > /etc/hosts
|
||||
|
||||
/fnctl "$@"
|
||||
14
fnctl/glide.lock
generated
Normal file
14
fnctl/glide.lock
generated
Normal file
@@ -0,0 +1,14 @@
|
||||
hash: ba755dafecdd69ab73589e55d3b10216684aa885d65fa459fca69a0c4da6d959
|
||||
updated: 2016-10-20T18:19:35.100655097-07:00
|
||||
imports:
|
||||
- name: github.com/go-resty/resty
|
||||
version: 1a3bb60986d90e32c04575111b1ccb8eab24a3e5
|
||||
- name: github.com/iron-io/functions_go
|
||||
version: 584f4a6e13b53370f036012347cf0571128209f0
|
||||
- name: github.com/urfave/cli
|
||||
version: 55f715e28c46073d0e217e2ce8eb46b0b45e3db6
|
||||
- name: golang.org/x/net
|
||||
version: daba796358cd2742b75aae05761f1b898c9f6a5c
|
||||
subpackages:
|
||||
- publicsuffix
|
||||
testImports: []
|
||||
4
fnctl/glide.yaml
Normal file
4
fnctl/glide.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
package: github.com/iron-io/functions/fnctl
|
||||
import:
|
||||
- package: github.com/urfave/cli
|
||||
- package: github.com/iron-io/functions_go
|
||||
53
fnctl/main.go
Normal file
53
fnctl/main.go
Normal file
@@ -0,0 +1,53 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/url"
|
||||
"os"
|
||||
|
||||
functions "github.com/iron-io/functions_go"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
func main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "fnctl"
|
||||
app.Version = "0.0.1"
|
||||
app.Authors = []cli.Author{{Name: "iron.io"}}
|
||||
app.Usage = "IronFunctions command line tools"
|
||||
app.UsageText = "Check the manual at https://github.com/iron-io/functions/blob/master/fnctl/README.md"
|
||||
app.CommandNotFound = func(c *cli.Context, cmd string) { fmt.Fprintf(os.Stderr, "command not found: %v\n", cmd) }
|
||||
app.Commands = []cli.Command{
|
||||
apps(),
|
||||
routes(),
|
||||
update(),
|
||||
}
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func resetBasePath(c *functions.Configuration) {
|
||||
var u url.URL
|
||||
u.Scheme = c.Scheme
|
||||
u.Host = c.Host
|
||||
u.Path = "/v1"
|
||||
c.BasePath = u.String()
|
||||
}
|
||||
|
||||
func confFlags(c *functions.Configuration) []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "host",
|
||||
Usage: "raw host path to functions api, e.g. functions.iron.io",
|
||||
Destination: &c.Host,
|
||||
EnvVar: "HOST",
|
||||
Value: "localhost:8080",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "scheme",
|
||||
Usage: "http/https",
|
||||
Destination: &c.Scheme,
|
||||
EnvVar: "SCHEME",
|
||||
Value: "http",
|
||||
},
|
||||
}
|
||||
}
|
||||
165
fnctl/routes.go
Normal file
165
fnctl/routes.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"path"
|
||||
"text/tabwriter"
|
||||
|
||||
"github.com/iron-io/functions_go"
|
||||
"github.com/urfave/cli"
|
||||
"golang.org/x/crypto/ssh/terminal"
|
||||
)
|
||||
|
||||
type routesCmd struct {
|
||||
*functions.RoutesApi
|
||||
}
|
||||
|
||||
func routes() cli.Command {
|
||||
r := routesCmd{RoutesApi: functions.NewRoutesApi()}
|
||||
|
||||
return cli.Command{
|
||||
Name: "routes",
|
||||
Usage: "list routes",
|
||||
ArgsUsage: "fnclt routes",
|
||||
Flags: append(confFlags(&r.Configuration), []cli.Flag{}...),
|
||||
Action: r.list,
|
||||
Subcommands: []cli.Command{
|
||||
{
|
||||
Name: "run",
|
||||
Usage: "run a route",
|
||||
ArgsUsage: "appName /path",
|
||||
Action: r.run,
|
||||
},
|
||||
{
|
||||
Name: "create",
|
||||
Usage: "create a route",
|
||||
ArgsUsage: "appName /path image/name",
|
||||
Action: r.create,
|
||||
},
|
||||
{
|
||||
Name: "delete",
|
||||
Usage: "delete a route",
|
||||
ArgsUsage: "appName /path",
|
||||
Action: r.delete,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (a *routesCmd) list(c *cli.Context) error {
|
||||
if c.Args().First() == "" {
|
||||
return errors.New("error: routes listing takes one argument, an app name")
|
||||
}
|
||||
|
||||
resetBasePath(&a.Configuration)
|
||||
|
||||
appName := c.Args().Get(0)
|
||||
wrapper, _, err := a.AppsAppRoutesGet(appName)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting routes: %v", err)
|
||||
}
|
||||
|
||||
baseURL, err := url.Parse(a.Configuration.BasePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing base path: %v", err)
|
||||
}
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
|
||||
fmt.Fprint(w, "path", "\t", "image", "\t", "endpoint", "\n")
|
||||
for _, route := range wrapper.Routes {
|
||||
u, err := url.Parse("../")
|
||||
u.Path = path.Join(u.Path, "r", appName, route.Path)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing functions route path: %v", err)
|
||||
}
|
||||
|
||||
fmt.Fprint(w, route.Path, "\t", route.Image, "\t", baseURL.ResolveReference(u).String(), "\n")
|
||||
}
|
||||
w.Flush()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *routesCmd) run(c *cli.Context) error {
|
||||
if c.Args().Get(0) == "" || c.Args().Get(1) == "" {
|
||||
return errors.New("error: routes listing takes three arguments: an app name and a route")
|
||||
}
|
||||
|
||||
resetBasePath(&a.Configuration)
|
||||
|
||||
baseURL, err := url.Parse(a.Configuration.BasePath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error parsing base path: %v", err)
|
||||
}
|
||||
|
||||
appName := c.Args().Get(0)
|
||||
route := c.Args().Get(1)
|
||||
|
||||
u, err := url.Parse("../")
|
||||
u.Path = path.Join(u.Path, "r", appName, route)
|
||||
|
||||
var content io.Reader
|
||||
if !terminal.IsTerminal(int(os.Stdin.Fd())) {
|
||||
content = os.Stdin
|
||||
}
|
||||
|
||||
resp, err := http.Post(baseURL.ResolveReference(u).String(), "application/json", content)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running route: %v", err)
|
||||
}
|
||||
|
||||
io.Copy(os.Stdout, resp.Body)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *routesCmd) create(c *cli.Context) error {
|
||||
if c.Args().Get(0) == "" || c.Args().Get(1) == "" || c.Args().Get(2) == "" {
|
||||
return errors.New("error: routes listing takes three arguments: an app name, a route path and an image")
|
||||
}
|
||||
|
||||
resetBasePath(&a.Configuration)
|
||||
|
||||
appName := c.Args().Get(0)
|
||||
route := c.Args().Get(1)
|
||||
image := c.Args().Get(2)
|
||||
body := functions.RouteWrapper{
|
||||
Route: functions.Route{
|
||||
AppName: appName,
|
||||
Path: route,
|
||||
Image: image,
|
||||
},
|
||||
}
|
||||
wrapper, _, err := a.AppsAppRoutesPost(appName, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error creating route: %v", err)
|
||||
}
|
||||
if wrapper.Route.Path == "" || wrapper.Route.Image == "" {
|
||||
return fmt.Errorf("could not create this route (%s at %s), check if route path is correct.", route, appName)
|
||||
}
|
||||
|
||||
fmt.Println(wrapper.Route.Path, "created with", wrapper.Route.Image)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *routesCmd) delete(c *cli.Context) error {
|
||||
if c.Args().Get(0) == "" || c.Args().Get(1) == "" {
|
||||
return errors.New("error: routes listing takes three arguments: an app name and a path")
|
||||
}
|
||||
|
||||
resetBasePath(&a.Configuration)
|
||||
|
||||
appName := c.Args().Get(0)
|
||||
route := c.Args().Get(1)
|
||||
_, err := a.AppsAppRoutesRouteDelete(appName, route)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error deleting route: %v", err)
|
||||
}
|
||||
|
||||
fmt.Println(route, "deleted")
|
||||
return nil
|
||||
}
|
||||
292
fnctl/update.go
Normal file
292
fnctl/update.go
Normal file
@@ -0,0 +1,292 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"text/tabwriter"
|
||||
|
||||
functions "github.com/iron-io/functions_go"
|
||||
"github.com/urfave/cli"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
var (
|
||||
validfn = [...]string{
|
||||
"functions.yaml",
|
||||
"functions.yml",
|
||||
"fn.yaml",
|
||||
"fn.yml",
|
||||
"functions.json",
|
||||
"fn.json",
|
||||
}
|
||||
|
||||
errDockerFileNotFound = errors.New("no Dockerfile found for this function")
|
||||
errUnexpectedFileFormat = errors.New("unexpected file format for function file")
|
||||
verbwriter = ioutil.Discard
|
||||
)
|
||||
|
||||
func update() cli.Command {
|
||||
cmd := updatecmd{RoutesApi: functions.NewRoutesApi()}
|
||||
var flags []cli.Flag
|
||||
flags = append(flags, cmd.flags()...)
|
||||
flags = append(flags, confFlags(&cmd.Configuration)...)
|
||||
return cli.Command{
|
||||
Name: "update",
|
||||
Usage: "scan local directory for functions, build and update them.",
|
||||
Flags: flags,
|
||||
Action: cmd.scan,
|
||||
}
|
||||
}
|
||||
|
||||
type updatecmd struct {
|
||||
*functions.RoutesApi
|
||||
|
||||
wd string
|
||||
dry bool
|
||||
skippush bool
|
||||
verbose bool
|
||||
}
|
||||
|
||||
func (u *updatecmd) flags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "d",
|
||||
Usage: "working directory",
|
||||
Destination: &u.wd,
|
||||
EnvVar: "WORK_DIR",
|
||||
Value: "./",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "skip-push",
|
||||
Usage: "does not push Docker built images onto Docker Hub - useful for local development.",
|
||||
Destination: &u.skippush,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "dry-run",
|
||||
Usage: "display how update will proceed when executed",
|
||||
Destination: &u.dry,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "v",
|
||||
Usage: "verbose mode",
|
||||
Destination: &u.verbose,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (u *updatecmd) scan(c *cli.Context) error {
|
||||
if u.verbose {
|
||||
verbwriter = os.Stderr
|
||||
}
|
||||
|
||||
os.Chdir(u.wd)
|
||||
|
||||
w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
|
||||
fmt.Fprint(w, "path", "\t", "action", "\n")
|
||||
|
||||
filepath.Walk(u.wd, func(path string, info os.FileInfo, err error) error {
|
||||
return u.walker(path, info, err, w)
|
||||
})
|
||||
|
||||
w.Flush()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updatecmd) walker(path string, info os.FileInfo, err error, w io.Writer) error {
|
||||
if !isvalid(path, info) {
|
||||
return nil
|
||||
}
|
||||
|
||||
fmt.Fprint(w, path, "\t")
|
||||
if u.dry {
|
||||
fmt.Fprintln(w, "dry-run")
|
||||
} else if err := u.update(path); err != nil {
|
||||
fmt.Fprintln(w, err)
|
||||
} else {
|
||||
fmt.Fprintln(w, "updated")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func isvalid(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
|
||||
}
|
||||
|
||||
// update 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 (u *updatecmd) update(path string) error {
|
||||
fmt.Fprintln(verbwriter, "deploying", path)
|
||||
|
||||
dir := filepath.Dir(path)
|
||||
dockerfile := filepath.Join(dir, "Dockerfile")
|
||||
if _, err := os.Stat(dockerfile); os.IsNotExist(err) {
|
||||
return errDockerFileNotFound
|
||||
}
|
||||
|
||||
funcfile, err := u.parse(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if funcfile.Build != nil {
|
||||
if err := u.localbuild(path, funcfile.Build); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err := u.dockerbuild(path, funcfile.Image); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := u.route(path, funcfile); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updatecmd) parse(path string) (*funcfile, error) {
|
||||
ext := filepath.Ext(path)
|
||||
switch ext {
|
||||
case ".json":
|
||||
return parseJSON(path)
|
||||
case ".yaml", ".yml":
|
||||
return parseYAML(path)
|
||||
}
|
||||
return nil, errUnexpectedFileFormat
|
||||
}
|
||||
|
||||
func parseJSON(path string) (*funcfile, error) {
|
||||
f, err := os.Open(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open %s for parsing. Error: %v", path, err)
|
||||
}
|
||||
ff := new(funcfile)
|
||||
err = json.NewDecoder(f).Decode(ff)
|
||||
return ff, err
|
||||
}
|
||||
|
||||
func parseYAML(path string) (*funcfile, error) {
|
||||
b, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("could not open %s for parsing. Error: %v", path, err)
|
||||
}
|
||||
ff := new(funcfile)
|
||||
err = yaml.Unmarshal(b, ff)
|
||||
return ff, err
|
||||
}
|
||||
|
||||
type funcfile struct {
|
||||
App *string
|
||||
Image string
|
||||
Route *string
|
||||
Build []string
|
||||
}
|
||||
|
||||
func (u *updatecmd) localbuild(path string, steps []string) error {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return fmt.Errorf("cannot get current working directory. err: %v", err)
|
||||
}
|
||||
|
||||
fullwd := filepath.Join(wd, filepath.Dir(path))
|
||||
for _, cmd := range steps {
|
||||
c := exec.Command("/bin/sh", "-c", cmd)
|
||||
c.Dir = fullwd
|
||||
out, err := c.CombinedOutput()
|
||||
fmt.Fprintf(verbwriter, "- %s:\n%s\n", cmd, out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running command %v (%v)", cmd, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updatecmd) dockerbuild(path, image string) error {
|
||||
cmds := [][]string{
|
||||
{"docker", "build", "-t", image, filepath.Dir(path)},
|
||||
}
|
||||
if !u.skippush {
|
||||
cmds = append(cmds, []string{"docker", "push", image})
|
||||
}
|
||||
|
||||
for _, cmd := range cmds {
|
||||
out, err := exec.Command(cmd[0], cmd[1:]...).CombinedOutput()
|
||||
fmt.Fprintf(verbwriter, "%s\n", out)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error running command %v (%v)", cmd, err)
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (u *updatecmd) route(path string, ff *funcfile) error {
|
||||
resetBasePath(&u.Configuration)
|
||||
|
||||
an, r := extractAppNameRoute(path)
|
||||
if ff.App == nil {
|
||||
ff.App = &an
|
||||
}
|
||||
if ff.Route == nil {
|
||||
ff.Route = &r
|
||||
}
|
||||
|
||||
body := functions.RouteWrapper{
|
||||
Route: functions.Route{
|
||||
Path: *ff.Route,
|
||||
Image: ff.Image,
|
||||
},
|
||||
}
|
||||
|
||||
fmt.Fprintf(verbwriter, "updating API with appName: %s route: %s image: %s \n", *ff.App, *ff.Route, ff.Image)
|
||||
|
||||
_, _, err := u.AppsAppRoutesPost(*ff.App, body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("error getting routes: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func extractAppNameRoute(path string) (appName, route string) {
|
||||
|
||||
// The idea here is to extract the root-most directory name
|
||||
// as application name, it turns out that stdlib tools are great to
|
||||
// extract the deepest one. Thus, we revert the string and use the
|
||||
// stdlib as it is - and revert back to its normal content. Not fastest
|
||||
// ever, but it is simple.
|
||||
|
||||
rpath := reverse(path)
|
||||
rroute, rappName := filepath.Split(rpath)
|
||||
route = filepath.Dir(reverse(rroute))
|
||||
return reverse(rappName), route
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user