Functions CLI (#191)

First iteration of CLI tool.
This commit is contained in:
C Cirello
2016-10-24 09:21:07 -07:00
committed by GitHub
parent 946ba1b188
commit b8cc8ad1d5
14 changed files with 813 additions and 24 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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
View File

@@ -0,0 +1 @@
fnctl

13
fnctl/Dockerfile Normal file
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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
View 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)
}