mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Updates to fnctl to make UX better (#272)
* See the hello/go README for how this all works now. * Node support for fnctl auto build * Updated based on PR comments.
This commit is contained in:
2
examples/hello/go/.gitignore
vendored
2
examples/hello/go/.gitignore
vendored
@@ -3,3 +3,5 @@ vendor/
|
|||||||
/go
|
/go
|
||||||
/app
|
/app
|
||||||
/__uberscript__
|
/__uberscript__
|
||||||
|
|
||||||
|
function.yaml
|
||||||
|
|||||||
@@ -1,6 +1,4 @@
|
|||||||
FROM iron/go
|
FROM iron/go
|
||||||
|
WORKDIR /function
|
||||||
WORKDIR /app
|
ADD . /function/
|
||||||
ADD . /app
|
ENTRYPOINT ["./func"]
|
||||||
|
|
||||||
ENTRYPOINT ["./hello"]
|
|
||||||
|
|||||||
@@ -1,40 +1,24 @@
|
|||||||
## Quick Example for a Go Function (3 minutes)
|
# Quick Example for a Go Function (3 minutes)
|
||||||
|
|
||||||
This example will show you how to test and deploy Go (Golang) code to IronFunctions.
|
This example will show you how to test and deploy Go (Golang) code to IronFunctions.
|
||||||
|
|
||||||
### 1. Prepare the `functions.yaml` file:
|
|
||||||
|
|
||||||
At functions.yaml you will find:
|
|
||||||
```yml
|
|
||||||
app: goapp
|
|
||||||
route: /hello
|
|
||||||
image: USERNAME/hello:0.0.1
|
|
||||||
build:
|
|
||||||
- docker run --rm -v "$PWD":/go/src/ -w /go/src/ -e "GOPATH=/go/src/vendor:/go" iron/go:dev go build -o hello
|
|
||||||
```
|
|
||||||
|
|
||||||
The important step here is to ensure you replace `USERNAME` with your Docker Hub account name. Some points of note:
|
|
||||||
the application name is `goapp` and the route for incoming requests is `/hello`. These informations are relevant for
|
|
||||||
the moment you try to test this function.
|
|
||||||
|
|
||||||
### 2. Build:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fnctl publish
|
fnctl init <YOUR_DOCKERHUB_USERNAME>/hello
|
||||||
|
fnctl build
|
||||||
|
# test it
|
||||||
|
cat hello.payload.json | fnctl run
|
||||||
|
# push it to Docker Hub
|
||||||
|
fnctl push
|
||||||
|
# Create a route to this function on IronFunctions
|
||||||
|
fnctl routes create myapp /hello YOUR_DOCKERHUB_USERNAME/hello:0.0.X
|
||||||
|
# todo: Image name could be optional if we read the function file for creating the route. Then command could be:
|
||||||
|
fnctl routes create myapp /hello
|
||||||
```
|
```
|
||||||
|
|
||||||
`-v` is optional, but it allows you to see how this function is being built.
|
Now you use your function on IronFunctions:
|
||||||
|
|
||||||
### 3. Queue jobs for your function
|
```sh
|
||||||
|
curl -H "Content-Type: application/json" -X POST -d @hello.payload.json http://localhost:8080/r/myapp/hello
|
||||||
|
```
|
||||||
|
|
||||||
Now you can start jobs on your function. Let's quickly queue up a job to try it out.
|
Or surf to it: http://localhost:8080/r/myapp/hello
|
||||||
|
|
||||||
```sh
|
|
||||||
cat hello.payload.json | fnctl run goapp /hello
|
|
||||||
```
|
|
||||||
|
|
||||||
Here's a curl example to show how easy it is to do in any language:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl -H "Content-Type: application/json" -X POST -d @hello.payload.json http://localhost:8080/r/goapp/hello
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
app: goapp
|
|
||||||
route: /hello
|
|
||||||
image: USERNAME/hello:0.0.1
|
|
||||||
build:
|
|
||||||
- docker run --rm -v "$PWD":/go/src/ -w /go/src/ -e "GOPATH=/go/src/vendor:/go" iron/go:dev go build -o hello
|
|
||||||
2
examples/hello/node/.gitignore
vendored
2
examples/hello/node/.gitignore
vendored
@@ -1 +1,3 @@
|
|||||||
node_modules/
|
node_modules/
|
||||||
|
function.yaml
|
||||||
|
Dockerfile
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
FROM iron/node
|
|
||||||
|
|
||||||
WORKDIR /app
|
|
||||||
ADD . /app
|
|
||||||
|
|
||||||
ENTRYPOINT ["node", "hello.js"]
|
|
||||||
@@ -1,40 +1,18 @@
|
|||||||
## Quick Example for a NodeJS Function (4 minutes)
|
## Quick Example for a NodeJS Function (4 minutes)
|
||||||
|
|
||||||
This example will show you how to test and deploy Go (Golang) code to IronFunctions.
|
This example will show you how to test and deploy a Node function to IronFunctions.
|
||||||
|
|
||||||
### 1. Prepare the `functions.yaml` file:
|
|
||||||
|
|
||||||
At functions.yaml you will find:
|
|
||||||
```yml
|
|
||||||
app: nodeapp
|
|
||||||
route: /hello
|
|
||||||
image: USERNAME/hello:0.0.1
|
|
||||||
build:
|
|
||||||
- docker run --rm -v "$PWD":/worker -w /worker iron/node:dev npm install
|
|
||||||
```
|
|
||||||
|
|
||||||
The important step here is to ensure you replace `USERNAME` with your Docker Hub account name. Some points of note:
|
|
||||||
the application name is `nodeapp` and the route for incoming requests is `/hello`. These informations are relevant for
|
|
||||||
the moment you try to test this function.
|
|
||||||
|
|
||||||
### 2. Build:
|
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
fnctl publish
|
fnctl init <YOUR_DOCKERHUB_USERNAME>/hello
|
||||||
|
fnctl build
|
||||||
|
# test it
|
||||||
|
cat hello.payload.json | fnctl run
|
||||||
|
# push it to Docker Hub for use with IronFunctions
|
||||||
|
fnctl push
|
||||||
|
# Create a route to this function on IronFunctions
|
||||||
|
fnctl routes create myapp /hello YOUR_DOCKERHUB_USERNAME/hello:0.0.X
|
||||||
|
# todo: Image name could be optional if we read the function file for creating the route. Then command could be:
|
||||||
|
fnctl routes create myapp /hello
|
||||||
```
|
```
|
||||||
|
|
||||||
`-v` is optional, but it allows you to see how this function is being built.
|
Now surf to: http://localhost:8080/r/myapp/hello
|
||||||
|
|
||||||
### 3. Queue jobs for your function
|
|
||||||
|
|
||||||
Now you can start jobs on your function. Let's quickly queue up a job to try it out.
|
|
||||||
|
|
||||||
```sh
|
|
||||||
cat hello.payload.json | fnctl run nodeapp /hello
|
|
||||||
```
|
|
||||||
|
|
||||||
Here's a curl example to show how easy it is to do in any language:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
curl -H "Content-Type: application/json" -X POST -d @hello.payload.json http://localhost:8080/r/nodeapp/hello
|
|
||||||
```
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
app: nodeapp
|
|
||||||
route: /hello
|
|
||||||
image: USERNAME/hello:0.0.1
|
|
||||||
build:
|
|
||||||
- docker run --rm -v "$PWD":/worker -w /worker iron/node:dev npm install
|
|
||||||
@@ -6,4 +6,4 @@ try {
|
|||||||
name = obj.name
|
name = obj.name
|
||||||
}
|
}
|
||||||
} catch(e) {}
|
} catch(e) {}
|
||||||
console.log("Hello", name, "from Node!");
|
console.log("Hello", name, "from Node!");
|
||||||
|
|||||||
1
fnctl/.gitignore
vendored
1
fnctl/.gitignore
vendored
@@ -1,2 +1,3 @@
|
|||||||
fnctl
|
fnctl
|
||||||
vendor/
|
vendor/
|
||||||
|
/fnctl.exe
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ docker: vendor
|
|||||||
docker push iron/fnctl
|
docker push iron/fnctl
|
||||||
|
|
||||||
vendor:
|
vendor:
|
||||||
glide install
|
glide install -v
|
||||||
|
|
||||||
test:
|
test:
|
||||||
go test -v $(shell glide nv)
|
go test -v $(shell glide nv)
|
||||||
@@ -1,22 +1,17 @@
|
|||||||
# IronFunctions CLI
|
# IronFunctions CLI
|
||||||
|
|
||||||
## Build
|
## Init
|
||||||
|
|
||||||
Ensure you have Go configured and installed in your environment. Once it is
|
usage: fnctl init [--runtime node] [--entrypoint "node hello.js"] <name>
|
||||||
done, run:
|
|
||||||
|
|
||||||
```sh
|
Init will help you create a function.yaml file for the current directory.
|
||||||
$ make
|
|
||||||
```
|
|
||||||
|
|
||||||
It will build fnctl compatible with your local environment. You can test this
|
If there's a Dockerfile found, this will generate the basic file with just the image name.
|
||||||
CLI, right away with:
|
It will then try to decipher the runtime based on the files in the current directory, if it can't figure it out, it will ask.
|
||||||
|
It will then take a best guess for what the entrypoint will be based on the language, it it can't guess, it will ask.
|
||||||
```sh
|
|
||||||
$ ./fnctl
|
|
||||||
```
|
|
||||||
|
|
||||||
## Basic
|
## Basic
|
||||||
|
|
||||||
You can operate IronFunctions from the command line.
|
You can operate IronFunctions from the command line.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
@@ -47,7 +42,7 @@ $ fnctl routes delete otherapp hello # delete route
|
|||||||
|
|
||||||
## Changing target host
|
## Changing target host
|
||||||
|
|
||||||
`fnctl` is configured by default to talk to a locally installed IronFunctions.
|
`fnctl` is configured by default to talk http://localhost:8080.
|
||||||
You may reconfigure it to talk to a remote installation by updating a local
|
You may reconfigure it to talk to a remote installation by updating a local
|
||||||
environment variable (`$API_URL`):
|
environment variable (`$API_URL`):
|
||||||
```sh
|
```sh
|
||||||
@@ -240,3 +235,19 @@ environment variables prefixed with `CONFIG_`.
|
|||||||
Repeated calls to `fnctl route create` will trigger an update of the given
|
Repeated calls to `fnctl route create` will trigger an update of the given
|
||||||
route, thus you will be able to change any of these attributes later in time
|
route, thus you will be able to change any of these attributes later in time
|
||||||
if necessary.
|
if necessary.
|
||||||
|
|
||||||
|
## 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
|
||||||
|
```
|
||||||
|
|||||||
@@ -36,6 +36,10 @@ func (b *buildcmd) walker(path string, info os.FileInfo, err error, w io.Writer)
|
|||||||
// 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(path string) error {
|
||||||
fmt.Fprintln(b.verbwriter, "building", path)
|
fmt.Fprintln(b.verbwriter, "building", path)
|
||||||
_, err := b.buildfunc(path)
|
ff, err := b.buildfunc(path)
|
||||||
return err
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Printf("Function %v built successfully.\n", ff.FullName())
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,16 +50,8 @@ func (b *bumpcmd) bump(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if funcfile.Version == "" {
|
if funcfile.Version == "" {
|
||||||
img, ver := imageversion(funcfile.Name)
|
funcfile.Version = initialVersion
|
||||||
if ver == "" {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
funcfile.Name = img
|
|
||||||
funcfile.Version = ver
|
|
||||||
} else if funcfile.Version != "" && strings.Contains(funcfile.Name, ":") {
|
|
||||||
return fmt.Errorf("cannot do version bump: this function has tag in its image name and version at same time. name: %s. version: %s", funcfile.Name, funcfile.Version)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
s, err := storage.NewVersionStorage("local", funcfile.Version)
|
s, err := storage.NewVersionStorage("local", funcfile.Version)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -73,7 +65,12 @@ func (b *bumpcmd) bump(path string) error {
|
|||||||
|
|
||||||
funcfile.Version = newver.String()
|
funcfile.Version = newver.String()
|
||||||
|
|
||||||
return storefuncfile(path, funcfile)
|
err = storefuncfile(path, funcfile)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
fmt.Println("Bumped to version", funcfile.Version)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func imageversion(image string) (name, ver string) {
|
func imageversion(image string) (name, ver string) {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
@@ -13,12 +14,13 @@ import (
|
|||||||
"text/template"
|
"text/template"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/iron-io/functions/fnctl/langs"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
var errDockerFileNotFound = errors.New("no Dockerfile found for this function")
|
var errDockerFileNotFound = errors.New("no Dockerfile found for this function")
|
||||||
|
|
||||||
func isvalid(path string, info os.FileInfo) bool {
|
func isFuncfile(path string, info os.FileInfo) bool {
|
||||||
if info.IsDir() {
|
if info.IsDir() {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
@@ -38,7 +40,7 @@ func walker(path string, info os.FileInfo, err error, w io.Writer, f func(path s
|
|||||||
if err := f(path); err != nil {
|
if err := f(path); err != nil {
|
||||||
fmt.Fprintln(w, err)
|
fmt.Fprintln(w, err)
|
||||||
} else {
|
} else {
|
||||||
fmt.Fprintln(w, "done")
|
// fmt.Fprintln(w, "done")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -87,15 +89,15 @@ func (c *commoncmd) scan(walker func(path string, info os.FileInfo, err error, w
|
|||||||
var walked bool
|
var walked bool
|
||||||
|
|
||||||
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0)
|
w := tabwriter.NewWriter(os.Stdout, 0, 8, 1, ' ', 0)
|
||||||
fmt.Fprint(w, "path", "\t", "result", "\n")
|
// fmt.Fprint(w, "path", "\t", "result", "\n")
|
||||||
|
|
||||||
err := filepath.Walk(c.wd, func(path string, info os.FileInfo, err error) error {
|
err := filepath.Walk(c.wd, func(path string, info os.FileInfo, err error) error {
|
||||||
|
// fmt.Println("walking", info.Name())
|
||||||
if !c.recursively && path != c.wd && info.IsDir() {
|
if !c.recursively && path != c.wd && info.IsDir() {
|
||||||
return filepath.SkipDir
|
return filepath.SkipDir
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isvalid(path, info) {
|
if !isFuncfile(path, info) {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +116,7 @@ func (c *commoncmd) scan(walker func(path string, info os.FileInfo, err error, w
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !walked {
|
if !walked {
|
||||||
fmt.Println("all functions are up-to-date.")
|
fmt.Println("No function file found.")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -184,26 +186,52 @@ func (c commoncmd) localbuild(path string, steps []string) error {
|
|||||||
func (c commoncmd) dockerbuild(path string, ff *funcfile) error {
|
func (c commoncmd) dockerbuild(path string, ff *funcfile) error {
|
||||||
dir := filepath.Dir(path)
|
dir := filepath.Dir(path)
|
||||||
|
|
||||||
|
var helper langs.LangHelper
|
||||||
dockerfile := filepath.Join(dir, "Dockerfile")
|
dockerfile := filepath.Join(dir, "Dockerfile")
|
||||||
if _, err := os.Stat(dockerfile); os.IsNotExist(err) {
|
if !exists(dockerfile) {
|
||||||
err := writeTmpDockerfile(dir, ff)
|
err := writeTmpDockerfile(dir, ff)
|
||||||
defer os.Remove(filepath.Join(dir, "Dockerfile"))
|
defer os.Remove(filepath.Join(dir, "Dockerfile"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
helper, err = langs.GetLangHelper(*ff.Runtime)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if helper.HasPreBuild() {
|
||||||
|
err := helper.PreBuild()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fmt.Printf("Building image %v\n", ff.FullName())
|
||||||
cmd := exec.Command("docker", "build", "-t", ff.FullName(), ".")
|
cmd := exec.Command("docker", "build", "-t", ff.FullName(), ".")
|
||||||
cmd.Dir = dir
|
cmd.Dir = dir
|
||||||
cmd.Stderr = c.verbwriter
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Stdout = c.verbwriter
|
cmd.Stdout = os.Stdout
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("error running docker build: %v", err)
|
return fmt.Errorf("error running docker build: %v", err)
|
||||||
}
|
}
|
||||||
|
if helper != nil {
|
||||||
|
err := helper.AfterBuild()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func exists(name string) bool {
|
||||||
|
if _, err := os.Stat(name); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
var acceptableFnRuntimes = map[string]string{
|
var acceptableFnRuntimes = map[string]string{
|
||||||
"elixir": "iron/elixir",
|
"elixir": "iron/elixir",
|
||||||
"erlang": "iron/erlang",
|
"erlang": "iron/erlang",
|
||||||
@@ -221,10 +249,9 @@ var acceptableFnRuntimes = map[string]string{
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tplDockerfile = `FROM {{ .BaseImage }}
|
const tplDockerfile = `FROM {{ .BaseImage }}
|
||||||
|
WORKDIR /function
|
||||||
ADD ./ /
|
ADD . /function/
|
||||||
|
ENTRYPOINT [{{ .Entrypoint }}]
|
||||||
ENTRYPOINT ["{{ .Entrypoint }}"]
|
|
||||||
`
|
`
|
||||||
|
|
||||||
func writeTmpDockerfile(dir string, ff *funcfile) error {
|
func writeTmpDockerfile(dir string, ff *funcfile) error {
|
||||||
@@ -247,10 +274,23 @@ func writeTmpDockerfile(dir string, ff *funcfile) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// convert entrypoint string to slice
|
||||||
|
epvals := strings.Fields(*ff.Entrypoint)
|
||||||
|
var buffer bytes.Buffer
|
||||||
|
for i, s := range epvals {
|
||||||
|
if i > 0 {
|
||||||
|
buffer.WriteString(", ")
|
||||||
|
}
|
||||||
|
buffer.WriteString("\"")
|
||||||
|
buffer.WriteString(s)
|
||||||
|
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 {
|
||||||
BaseImage, Entrypoint string
|
BaseImage, Entrypoint string
|
||||||
}{rt, *ff.Entrypoint})
|
}{rt, buffer.String()})
|
||||||
fd.Close()
|
fd.Close()
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
13
fnctl/errors.go
Normal file
13
fnctl/errors.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
type NotFoundError struct {
|
||||||
|
S string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *NotFoundError) Error() string {
|
||||||
|
return e.S
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNotFoundError(s string) *NotFoundError {
|
||||||
|
return &NotFoundError{S: s}
|
||||||
|
}
|
||||||
@@ -14,15 +14,9 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
validfn = [...]string{
|
validfn = [...]string{
|
||||||
"functions.yaml",
|
|
||||||
"functions.yml",
|
|
||||||
"function.yaml",
|
"function.yaml",
|
||||||
"function.yml",
|
"function.yml",
|
||||||
"fn.yaml",
|
|
||||||
"fn.yml",
|
|
||||||
"functions.json",
|
|
||||||
"function.json",
|
"function.json",
|
||||||
"fn.json",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
errUnexpectedFileFormat = errors.New("unexpected file format for function file")
|
errUnexpectedFileFormat = errors.New("unexpected file format for function file")
|
||||||
@@ -63,6 +57,15 @@ func (ff *funcfile) RuntimeTag() (runtime, tag string) {
|
|||||||
return rt[:tagpos], rt[tagpos+1:]
|
return rt[:tagpos], rt[tagpos+1:]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func findFuncfile() (*funcfile, error) {
|
||||||
|
for _, fn := range validfn {
|
||||||
|
if exists(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)
|
||||||
switch ext {
|
switch ext {
|
||||||
|
|||||||
166
fnctl/init.go
166
fnctl/init.go
@@ -1,13 +1,24 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
|
/*
|
||||||
|
usage: fnctl init <name>
|
||||||
|
|
||||||
|
If there's a Dockerfile found, this will generate the basic file with just the image name. exit
|
||||||
|
It will then try to decipher the runtime based on the files in the current directory, if it can't figure it out, it will ask.
|
||||||
|
It will then take a best guess for what the entrypoint will be based on the language, it it can't guess, it will ask.
|
||||||
|
|
||||||
|
*/
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/iron-io/functions/fnctl/langs"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -27,6 +38,7 @@ var (
|
|||||||
".pl": "perl",
|
".pl": "perl",
|
||||||
".py": "python",
|
".py": "python",
|
||||||
".scala": "scala",
|
".scala": "scala",
|
||||||
|
".rb": "ruby",
|
||||||
}
|
}
|
||||||
|
|
||||||
fnRuntimes []string
|
fnRuntimes []string
|
||||||
@@ -39,8 +51,10 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type initFnCmd struct {
|
type initFnCmd struct {
|
||||||
force bool
|
name string
|
||||||
runtime string
|
force bool
|
||||||
|
runtime *string
|
||||||
|
entrypoint *string
|
||||||
}
|
}
|
||||||
|
|
||||||
func initFn() cli.Command {
|
func initFn() cli.Command {
|
||||||
@@ -49,19 +63,24 @@ func initFn() cli.Command {
|
|||||||
return cli.Command{
|
return cli.Command{
|
||||||
Name: "init",
|
Name: "init",
|
||||||
Usage: "create a local function.yaml file",
|
Usage: "create a local function.yaml file",
|
||||||
Description: "Entrypoint is the binary file which the container engine will invoke when the request comes in - equivalent to Dockerfile ENTRYPOINT.",
|
Description: "Creates a function.yaml file in the current directory. ",
|
||||||
ArgsUsage: "<entrypoint>",
|
ArgsUsage: "<DOCKERHUB_USERNAME:FUNCTION_NAME>",
|
||||||
Action: a.init,
|
Action: a.init,
|
||||||
Flags: []cli.Flag{
|
Flags: []cli.Flag{
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "f",
|
Name: "force, f",
|
||||||
Usage: "overwrite existing function.yaml",
|
Usage: "overwrite existing function.yaml",
|
||||||
Destination: &a.force,
|
Destination: &a.force,
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "runtime",
|
Name: "runtime",
|
||||||
Usage: "choose an existing runtime - " + strings.Join(fnRuntimes, ", "),
|
Usage: "choose an existing runtime - " + strings.Join(fnRuntimes, ", "),
|
||||||
Destination: &a.runtime,
|
Destination: a.runtime,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "entrypoint",
|
||||||
|
Usage: "entrypoint is the command to run to start this function - equivalent to Dockerfile ENTRYPOINT.",
|
||||||
|
Destination: a.entrypoint,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@@ -69,57 +88,125 @@ 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 {
|
||||||
for _, fn := range validfn {
|
ff, err := findFuncfile()
|
||||||
if _, err := os.Stat(fn); !os.IsNotExist(err) {
|
if err != nil {
|
||||||
return errors.New("function file already exists")
|
if _, ok := err.(*NotFoundError); ok {
|
||||||
|
// great, we're about to make one
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if ff != nil {
|
||||||
|
return errors.New("function file already exists")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
entrypoint := c.Args().First()
|
err := a.buildFuncFile(c)
|
||||||
if entrypoint == "" {
|
|
||||||
fmt.Print("Please, specify an entrypoint for your function: ")
|
|
||||||
fmt.Scanln(&entrypoint)
|
|
||||||
}
|
|
||||||
if entrypoint == "" {
|
|
||||||
return errors.New("entrypoint is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
pwd, err := os.Getwd()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("error detecting current working directory: %s\n", err)
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if a.runtime == "" {
|
/*
|
||||||
rt, err := detectRuntime(pwd)
|
Now we can make some guesses for the entrypoint based on runtime.
|
||||||
if err != nil {
|
If Go, use ./foldername, if ruby, use ruby and a filename. If node, node + filename
|
||||||
return err
|
*/
|
||||||
}
|
|
||||||
var ok bool
|
|
||||||
a.runtime, ok = fileExtToRuntime[rt]
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("could not detect language of this function: %s\n", a.runtime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, ok := acceptableFnRuntimes[a.runtime]; !ok {
|
|
||||||
return fmt.Errorf("cannot use runtime %s", a.runtime)
|
|
||||||
}
|
|
||||||
|
|
||||||
ff := &funcfile{
|
ff := &funcfile{
|
||||||
Runtime: &a.runtime,
|
Name: a.name,
|
||||||
|
Runtime: a.runtime,
|
||||||
Version: initialVersion,
|
Version: initialVersion,
|
||||||
Entrypoint: &entrypoint,
|
Entrypoint: a.entrypoint,
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := encodeFuncfileYAML("function.yaml", ff); err != nil {
|
if err := encodeFuncfileYAML("function.yaml", ff); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
fmt.Println("function.yaml written")
|
fmt.Println("function.yaml created.")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func detectRuntime(path string) (string, error) {
|
func (a *initFnCmd) buildFuncFile(c *cli.Context) error {
|
||||||
|
pwd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("error detecting current working directory: %s\n", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
a.name = c.Args().First()
|
||||||
|
if a.name == "" {
|
||||||
|
// todo: also check that it's valid image name format
|
||||||
|
return errors.New("Please specify a name for your function in the following format <DOCKERHUB_USERNAME>:<FUNCTION_NAME>")
|
||||||
|
}
|
||||||
|
|
||||||
|
if exists("Dockerfile") {
|
||||||
|
// then don't need anything else
|
||||||
|
fmt.Println("Dockerfile found, will use that to build.")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var rt string
|
||||||
|
var filename string
|
||||||
|
if a.runtime == nil || *a.runtime == "" {
|
||||||
|
filename, rt, err = detectRuntime(pwd)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
a.runtime = &rt
|
||||||
|
fmt.Printf("assuming %v runtime\n", rt)
|
||||||
|
}
|
||||||
|
if _, ok := acceptableFnRuntimes[*a.runtime]; !ok {
|
||||||
|
return fmt.Errorf("init does not support the %s runtime, you'll have to create your own Dockerfile for this function", *a.runtime)
|
||||||
|
}
|
||||||
|
|
||||||
|
if a.entrypoint == nil || *a.entrypoint == "" {
|
||||||
|
ep, err := detectEntrypoint(filename, *a.runtime, pwd)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not detect entrypoint for %v, use --entrypoint to add it explicitly. %v", *a.runtime, err)
|
||||||
|
}
|
||||||
|
a.entrypoint = &ep
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// detectRuntime this looks at the files in the directory and if it finds a support file extension, it
|
||||||
|
// returns the filename and runtime for that extension.
|
||||||
|
func detectRuntime(path string) (filename string, runtime string, err error) {
|
||||||
|
err = filepath.Walk(path, func(_ string, info os.FileInfo, _ error) error {
|
||||||
|
if info.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
ext := strings.ToLower(filepath.Ext(info.Name()))
|
||||||
|
if ext == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var ok bool
|
||||||
|
runtime, ok = fileExtToRuntime[ext]
|
||||||
|
if ok {
|
||||||
|
// first match, exiting - http://stackoverflow.com/a/36713726/105562
|
||||||
|
filename = info.Name()
|
||||||
|
return io.EOF
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
if err == io.EOF {
|
||||||
|
return filename, runtime, nil
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("file walk error: %s\n", err)
|
||||||
|
}
|
||||||
|
return "", "", fmt.Errorf("no supported files found to guess runtime, please set runtime explicitly with --runtime flag")
|
||||||
|
}
|
||||||
|
|
||||||
|
func detectEntrypoint(filename, runtime, pwd string) (string, error) {
|
||||||
|
helper, err := langs.GetLangHelper(runtime)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return helper.Entrypoint(filename)
|
||||||
|
}
|
||||||
|
|
||||||
|
func scoreExtension(path string) (string, error) {
|
||||||
scores := map[string]uint{
|
scores := map[string]uint{
|
||||||
"": 0,
|
"": 0,
|
||||||
}
|
}
|
||||||
@@ -145,7 +232,6 @@ func detectRuntime(path string) (string, error) {
|
|||||||
biggest = ext
|
biggest = ext
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return biggest, nil
|
return biggest, nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
21
fnctl/langs/base.go
Normal file
21
fnctl/langs/base.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package langs
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
// GetLangHelper returns a LangHelper for the passed in language
|
||||||
|
func GetLangHelper(lang string) (LangHelper, error) {
|
||||||
|
switch lang {
|
||||||
|
case "go":
|
||||||
|
return &GoLangHelper{}, nil
|
||||||
|
case "node":
|
||||||
|
return &NodeLangHelper{}, nil
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("No language helper found for %v", lang)
|
||||||
|
}
|
||||||
|
|
||||||
|
type LangHelper interface {
|
||||||
|
Entrypoint(filename string) (string, error)
|
||||||
|
HasPreBuild() bool
|
||||||
|
PreBuild() error
|
||||||
|
AfterBuild() error
|
||||||
|
}
|
||||||
48
fnctl/langs/go.go
Normal file
48
fnctl/langs/go.go
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
package langs
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GoLangHelper struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lh *GoLangHelper) Entrypoint(filename string) (string, error) {
|
||||||
|
// uses a common binary name: func
|
||||||
|
// return fmt.Sprintf("./%v", filepath.Base(pwd)), nil
|
||||||
|
return "./func", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lh *GoLangHelper) HasPreBuild() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreBuild for Go builds the binary so the final image can be as small as possible
|
||||||
|
func (lh *GoLangHelper) PreBuild() error {
|
||||||
|
wd, err := os.Getwd()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// todo: this won't work if the function is more complex since the import paths won't match up, need to fix
|
||||||
|
pbcmd := fmt.Sprintf("docker run --rm -v %s:/go/src/github.com/x/y -w /go/src/github.com/x/y iron/go:dev go build -o func", wd)
|
||||||
|
fmt.Println("Running prebuild command:", pbcmd)
|
||||||
|
parts := strings.Fields(pbcmd)
|
||||||
|
head := parts[0]
|
||||||
|
parts = parts[1:len(parts)]
|
||||||
|
cmd := exec.Command(head, parts...)
|
||||||
|
// cmd.Dir = dir
|
||||||
|
cmd.Stderr = os.Stderr
|
||||||
|
cmd.Stdout = os.Stdout
|
||||||
|
if err := cmd.Run(); err != nil {
|
||||||
|
return fmt.Errorf("error running docker build: %v", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lh *GoLangHelper) AfterBuild() error {
|
||||||
|
return os.Remove("func")
|
||||||
|
|
||||||
|
}
|
||||||
23
fnctl/langs/node.go
Normal file
23
fnctl/langs/node.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
package langs
|
||||||
|
|
||||||
|
import "fmt"
|
||||||
|
|
||||||
|
type NodeLangHelper struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lh *NodeLangHelper) Entrypoint(filename string) (string, error) {
|
||||||
|
return fmt.Sprintf("node %v", filename), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lh *NodeLangHelper) HasPreBuild() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// PreBuild for Go builds the binary so the final image can be as small as possible
|
||||||
|
func (lh *NodeLangHelper) PreBuild() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lh *NodeLangHelper) AfterBuild() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
@@ -83,9 +83,10 @@ func (p *publishcmd) publish(path string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p publishcmd) dockerpush(ff *funcfile) error {
|
func (p publishcmd) dockerpush(ff *funcfile) error {
|
||||||
|
fmt.Printf("Pushing function %v to Docker Hub.\n", ff.FullName())
|
||||||
cmd := exec.Command("docker", "push", ff.FullName())
|
cmd := exec.Command("docker", "push", ff.FullName())
|
||||||
cmd.Stderr = p.verbwriter
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Stdout = p.verbwriter
|
cmd.Stdout = os.Stdout
|
||||||
if err := cmd.Run(); err != nil {
|
if err := cmd.Run(); err != nil {
|
||||||
return fmt.Errorf("error running docker push: %v", err)
|
return fmt.Errorf("error running docker push: %v", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ func push() cli.Command {
|
|||||||
flags = append(flags, cmd.commoncmd.flags()...)
|
flags = append(flags, cmd.commoncmd.flags()...)
|
||||||
return cli.Command{
|
return cli.Command{
|
||||||
Name: "push",
|
Name: "push",
|
||||||
Usage: "scan local directory for functions and push them.",
|
Usage: "push function to Docker Hub",
|
||||||
Flags: flags,
|
Flags: flags,
|
||||||
Action: cmd.scan,
|
Action: cmd.scan,
|
||||||
}
|
}
|
||||||
@@ -55,10 +55,5 @@ func (p *pushcmd) push(path string) error {
|
|||||||
if err := p.dockerpush(funcfile); err != nil {
|
if err := p.dockerpush(funcfile); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := p.route(path, funcfile); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
30
fnctl/run.go
30
fnctl/run.go
@@ -36,7 +36,16 @@ 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 == "" {
|
||||||
return errors.New("error: image name is missing")
|
// check for a funcfile
|
||||||
|
ff, err := findFuncfile()
|
||||||
|
if err != nil {
|
||||||
|
if _, ok := err.(*NotFoundError); ok {
|
||||||
|
return errors.New("error: image name is missing or no function file found")
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
image = ff.FullName()
|
||||||
}
|
}
|
||||||
|
|
||||||
sh := []string{"docker", "run", "--rm", "-i"}
|
sh := []string{"docker", "run", "--rm", "-i"}
|
||||||
@@ -60,7 +69,24 @@ func (r *runCmd) run(c *cli.Context) error {
|
|||||||
|
|
||||||
sh = append(sh, image)
|
sh = append(sh, image)
|
||||||
cmd := exec.Command(sh[0], sh[1:]...)
|
cmd := exec.Command(sh[0], sh[1:]...)
|
||||||
cmd.Stdin = os.Stdin
|
// Check if stdin is being piped, and if not, create our own pipe with nothing in it
|
||||||
|
// http://stackoverflow.com/questions/22744443/check-if-there-is-something-to-read-on-stdin-in-golang
|
||||||
|
stat, err := os.Stdin.Stat()
|
||||||
|
if err != nil {
|
||||||
|
// On Windows, this gets an error if nothing is piped in.
|
||||||
|
// If something is piped in, it works fine.
|
||||||
|
// Turns out, this works just fine in our case as the piped stuff works properly and the non-piped doesn't hang either.
|
||||||
|
// See: https://github.com/golang/go/issues/14853#issuecomment-260170423
|
||||||
|
// log.Println("Warning: couldn't stat stdin, you are probably on Windows. Be sure to pipe something into this command, eg: 'echo \"hello\" | fnctl run'")
|
||||||
|
} else {
|
||||||
|
if (stat.Mode() & os.ModeCharDevice) == 0 {
|
||||||
|
// log.Println("data is being piped to stdin")
|
||||||
|
cmd.Stdin = os.Stdin
|
||||||
|
} else {
|
||||||
|
// log.Println("stdin is from a terminal")
|
||||||
|
cmd.Stdin = strings.NewReader("")
|
||||||
|
}
|
||||||
|
}
|
||||||
cmd.Stdout = os.Stdout
|
cmd.Stdout = os.Stdout
|
||||||
cmd.Stderr = os.Stderr
|
cmd.Stderr = os.Stderr
|
||||||
cmd.Env = env
|
cmd.Env = env
|
||||||
|
|||||||
Reference in New Issue
Block a user