new commands fnctl build and bump (#204)

New commands & refectoring

* fnctl: refactor code to improve reuse between commands

build, bump and publish (formerly update) share a lot of code,
this refactor ensure their logic are correctly reused. It renames
update to publish, so it would be a strong diff between "update"
and build.

* fnctl: remove unnecessary dependency for build and bump

* fnctl: improve code reuse between bump, build and publish

Unify the use of walker function in all these three commands and
drop dry-run support.

* Code grooming

- errcheck

* fnctl: update README.md to be in sync with actual execution output

* fnctl: move scan function to commoncmd structure

* fnctl: change verbose flag handling does not use global variable anymore
This commit is contained in:
Pedro Nasser
2016-11-01 00:11:29 -02:00
committed by GitHub
parent 7f31267328
commit 4c31c29fb8
10 changed files with 601 additions and 313 deletions

View File

@@ -41,19 +41,18 @@ $ fnctl routes delete otherapp hello # delete route
/hello deleted
```
## Bulk Update
## Publish
Also there is the update command that is going to scan all local directory for
Also there is the publish 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
$ fnctl publish
path result
/app/hello done
/app/hello-sync error: no Dockerfile found for this function
/app/test updated
/app/test done
```
It works by scanning all children directories of the current working directory,
@@ -83,11 +82,10 @@ following this convention:
It will render this pattern of updates:
```sh
$ fnctl update
Updating for all functions.
path action
/myapp/route1/subroute1 updated
/other/route1 updated
$ fnctl publish
path result
/myapp/route1/subroute1 done
/other/route1 done
```
It means that first subdirectory are always considered app names (e.g. `myapp`
@@ -120,3 +118,31 @@ 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`.
## Build and Bump
When dealing with a lot of functions you might find yourself making lots of
individual calls. `fnctl` offers two command to help you with that: `build` and
`bump`.
```sh
$ fnctl build
path result
/app/hello done
/app/test done
```
`fnctl build` is similar to `publish` except it neither publishes the resulting
docker image to Docker Hub nor updates the routes in IronFunctions server.
```sh
$ fnctl bump
path result
/app/hello done
/app/test done
```
`fnctl bump` will scan all IronFunctions for files named `VERSION` and bump
their version according to [semver](http://semver.org/) rules. In their absence,
it will skip.

41
fnctl/build.go Normal file
View File

@@ -0,0 +1,41 @@
package main
import (
"fmt"
"io"
"os"
"github.com/urfave/cli"
)
func build() cli.Command {
cmd := buildcmd{commoncmd: &commoncmd{}}
flags := append([]cli.Flag{}, cmd.flags()...)
return cli.Command{
Name: "build",
Usage: "build function version",
Flags: flags,
Action: cmd.scan,
}
}
type buildcmd struct {
*commoncmd
}
func (b *buildcmd) scan(c *cli.Context) error {
b.commoncmd.scan(b.walker)
return nil
}
func (b *buildcmd) walker(path string, info os.FileInfo, err error, w io.Writer) error {
walker(path, info, err, w, b.build)
return nil
}
// build will take the found valid function and build it
func (b *buildcmd) build(path string) error {
fmt.Fprintln(b.verbwriter, "building", path)
_, err := b.buildfunc(path)
return err
}

73
fnctl/bump.go Normal file
View File

@@ -0,0 +1,73 @@
package main
import (
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
bumper "github.com/giantswarm/semver-bump/bump"
"github.com/giantswarm/semver-bump/storage"
"github.com/urfave/cli"
)
var (
initialVersion = "0.0.1"
errVersionFileNotFound = errors.New("no VERSION file found for this function")
)
func bump() cli.Command {
cmd := bumpcmd{commoncmd: &commoncmd{}}
flags := append([]cli.Flag{}, cmd.flags()...)
return cli.Command{
Name: "bump",
Usage: "bump function version",
Flags: flags,
Action: cmd.scan,
}
}
type bumpcmd struct {
*commoncmd
}
func (b *bumpcmd) scan(c *cli.Context) error {
b.commoncmd.scan(b.walker)
return nil
}
func (b *bumpcmd) walker(path string, info os.FileInfo, err error, w io.Writer) error {
walker(path, info, err, w, b.bump)
return nil
}
// bump will take the found valid function and bump its version
func (b *bumpcmd) bump(path string) error {
fmt.Fprintln(b.verbwriter, "bumping version for", path)
dir := filepath.Dir(path)
versionfile := filepath.Join(dir, "VERSION")
if _, err := os.Stat(versionfile); os.IsNotExist(err) {
return errVersionFileNotFound
}
s, err := storage.NewVersionStorage("file", initialVersion)
if err != nil {
return err
}
version := bumper.NewSemverBumper(s, versionfile)
newver, err := version.BumpPatchVersion("", "")
if err != nil {
return err
}
if err := ioutil.WriteFile(versionfile, []byte(newver.String()), 0666); err != nil {
return err
}
return nil
}

186
fnctl/common.go Normal file
View File

@@ -0,0 +1,186 @@
package main
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"path/filepath"
"text/tabwriter"
"github.com/urfave/cli"
yaml "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")
)
type funcfile struct {
App *string
Image string
Route *string
Build []string
}
func parsefuncfile(path string) (*funcfile, error) {
ext := filepath.Ext(path)
switch ext {
case ".json":
return funcfileJSON(path)
case ".yaml", ".yml":
return funcfileYAML(path)
}
return nil, errUnexpectedFileFormat
}
func funcfileJSON(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 funcfileYAML(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
}
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
}
func walker(path string, info os.FileInfo, err error, w io.Writer, f func(path string) error) {
if !isvalid(path, info) {
return
}
fmt.Fprint(w, path, "\t")
if err := f(path); err != nil {
fmt.Fprintln(w, err)
} else {
fmt.Fprintln(w, "done")
}
}
type commoncmd struct {
wd string
verbose 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,
},
}
}
func (c *commoncmd) scan(walker func(path string, info os.FileInfo, err error, w io.Writer) error) {
c.verbwriter = ioutil.Discard
if c.verbose {
c.verbwriter = os.Stderr
}
w := tabwriter.NewWriter(os.Stdout, 0, 8, 0, '\t', 0)
fmt.Fprint(w, "path", "\t", "result", "\n")
err := filepath.Walk(c.wd, func(path string, info os.FileInfo, err error) error {
return walker(path, info, err, w)
})
if err != nil {
fmt.Fprintf(c.verbwriter, "file walk error: %s\n", err)
}
w.Flush()
}
func (c commoncmd) buildfunc(path string) (*funcfile, error) {
dir := filepath.Dir(path)
dockerfile := filepath.Join(dir, "Dockerfile")
if _, err := os.Stat(dockerfile); os.IsNotExist(err) {
return nil, errDockerFileNotFound
}
funcfile, err := parsefuncfile(path)
if err != nil {
return nil, err
}
if err := c.localbuild(path, funcfile.Build); err != nil {
return nil, err
}
if err := c.dockerbuild(path, funcfile.Image); err != nil {
return nil, err
}
return funcfile, nil
}
func (c commoncmd) localbuild(path string, steps []string) error {
for _, cmd := range steps {
exe := exec.Command("/bin/sh", "-c", cmd)
exe.Dir = filepath.Dir(path)
out, err := exe.CombinedOutput()
fmt.Fprintf(c.verbwriter, "- %s:\n%s\n", cmd, out)
if err != nil {
return fmt.Errorf("error running command %v (%v)", cmd, err)
}
}
return nil
}
func (c commoncmd) dockerbuild(path, image string) error {
out, err := exec.Command("docker", "build", "-t", image, filepath.Dir(path)).CombinedOutput()
fmt.Fprintf(c.verbwriter, "%s\n", out)
if err != nil {
return fmt.Errorf("error running docker build: %v", err)
}
return nil
}

99
fnctl/glide.lock generated
View File

@@ -1,12 +1,107 @@
hash: 9bc670813e50a1c5d7dd9703d2a0b33bcf812dafdde28fc98886bb2a3c5e441b
updated: 2016-10-26T15:13:34.156397533+01:00
hash: 6c0bc544bcabed5a74a7eeefd53bd6e088f10f6deee1a9fa65bb8b5e512b93aa
updated: 2016-10-31T15:05:00.722579591-07:00
imports:
- name: github.com/aws/aws-sdk-go
version: 32cdc88aa5cd2ba4afa049da884aaf9a3d103ef4
subpackages:
- aws/awserr
- aws/credentials
- name: github.com/Azure/go-ansiterm
version: fa152c58bc15761d0200cb75fe958b89a9d4888e
subpackages:
- winterm
- name: github.com/coreos/go-semver
version: 8ab6407b697782a06568d4b7f1db25550ec2e4c6
subpackages:
- semver
- name: github.com/docker/docker
version: 8cced8702261224ffd726774812eb50e8a600e52
subpackages:
- api/types/blkiodev
- api/types/container
- api/types/filters
- api/types/mount
- api/types/strslice
- api/types/swarm
- api/types/versions
- opts
- pkg/archive
- pkg/fileutils
- pkg/homedir
- pkg/idtools
- pkg/ioutils
- pkg/jsonlog
- pkg/jsonmessage
- pkg/longpath
- pkg/pools
- pkg/promise
- pkg/stdcopy
- pkg/system
- pkg/term
- pkg/term/windows
- name: github.com/docker/go-connections
version: f512407a188ecb16f31a33dbc9c4e4814afc1b03
subpackages:
- nat
- name: github.com/docker/go-units
version: 8a7beacffa3009a9ac66bad506b18ffdd110cf97
- name: github.com/fsouza/go-dockerclient
version: 3162ed100df52ad76c94cdf1b8b2a45d4f5e203d
- name: github.com/giantswarm/semver-bump
version: 7ec6ac8985c24dd50b4942f9a908d13cdfe70f23
subpackages:
- bump
- storage
- name: github.com/go-ini/ini
version: 6e4869b434bd001f6983749881c7ead3545887d8
- name: github.com/go-resty/resty
version: 1a3bb60986d90e32c04575111b1ccb8eab24a3e5
- name: github.com/hashicorp/go-cleanhttp
version: ad28ea4487f05916463e2423a55166280e8254b5
- name: github.com/iron-io/functions_go
version: 584f4a6e13b53370f036012347cf0571128209f0
- name: github.com/iron-io/iron_go3
version: b50ecf8ff90187fc5fabccd9d028dd461adce4ee
subpackages:
- api
- config
- worker
- name: github.com/iron-io/lambda
version: 197598b21c6918d143244cc69d4d443f062d3c78
subpackages:
- lambda
- name: github.com/juju/errgo
version: 08cceb5d0b5331634b9826762a8fd53b29b86ad8
subpackages:
- errors
- name: github.com/Microsoft/go-winio
version: ce2922f643c8fd76b46cadc7f404a06282678b34
- name: github.com/opencontainers/runc
version: bc462c96bf7b15b68ab40e86335cefcb692707c1
subpackages:
- libcontainer/system
- libcontainer/user
- name: github.com/satori/go.uuid
version: b061729afc07e77a8aa4fad0a2fd840958f1942a
- name: github.com/Sirupsen/logrus
version: 380f64d344b252a007a59baa61f31820f59cba89
- name: github.com/urfave/cli
version: 55f715e28c46073d0e217e2ce8eb46b0b45e3db6
- name: golang.org/x/crypto
version: 9477e0b78b9ac3d0b03822fd95422e2fe07627cd
subpackages:
- ssh/terminal
- name: golang.org/x/net
version: daba796358cd2742b75aae05761f1b898c9f6a5c
subpackages:
- context
- context/ctxhttp
- publicsuffix
- name: golang.org/x/sys
version: c200b10b5d5e122be351b67af224adc6128af5bf
subpackages:
- unix
- windows
- name: gopkg.in/yaml.v2
version: a5b47d31c556af34a302ce5d659e6fea44d90de0
testImports: []

View File

@@ -1,8 +1,21 @@
package: github.com/iron-io/functions/fnctl
import:
- package: github.com/urfave/cli
- package: github.com/docker/docker
subpackages:
- pkg/jsonmessage
- package: github.com/giantswarm/semver-bump
subpackages:
- bump
- storage
- package: github.com/iron-io/functions_go
- package: github.com/aws/aws-sdk-go
version: ^1.4.20
- package: github.com/iron-io/iron_go3
subpackages:
- config
- package: github.com/iron-io/lambda
version: ^0.1.0
subpackages:
- lambda
- package: github.com/urfave/cli
- package: golang.org/x/crypto
subpackages:
- ssh/terminal
- package: gopkg.in/yaml.v2

View File

@@ -19,9 +19,11 @@ func main() {
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(),
build(),
bump(),
lambda(),
publish(),
routes(),
}
app.Run(os.Args)
}

144
fnctl/publish.go Normal file
View File

@@ -0,0 +1,144 @@
package main
import (
"fmt"
"io"
"os"
"os/exec"
"path/filepath"
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()...)
flags = append(flags, confFlags(&cmd.Configuration)...)
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
dry bool
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, w io.Writer) error {
walker(path, info, err, w, 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.Image); err != nil {
return err
}
if err := p.route(path, funcfile); err != nil {
return err
}
return nil
}
func (p publishcmd) dockerpush(image string) error {
out, err := exec.Command("docker", "push", image).CombinedOutput()
fmt.Fprintf(p.verbwriter, "%s\n", out)
if err != nil {
return fmt.Errorf("error running docker push: %v", err)
}
return nil
}
func (p *publishcmd) route(path string, ff *funcfile) error {
resetBasePath(&p.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(p.verbwriter, "updating API with appName: %s route: %s image: %s \n", *ff.App, *ff.Route, ff.Image)
_, _, err := p.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)
}

View File

@@ -139,7 +139,7 @@ func (a *routesCmd) create(c *cli.Context) error {
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)
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)

View File

@@ -1,292 +0,0 @@
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)
}