From 0d71e1e38e6e26dcad2ad439c0925f7f93921692 Mon Sep 17 00:00:00 2001 From: Travis Reeder Date: Mon, 14 Nov 2016 16:40:05 -0800 Subject: [PATCH] Docs update with new fnctl commands (#273) * Added high level roadmap. * Changed to funtion.yaml. * Added logo * updating quickstart code example, WIP, waiting on another merge. * Minor updates. * Changed function.yaml to func.yaml and updated fnctl README. --- Dockerfile | 4 +- README.md | 95 +++++++++++-- api/ifaces/app.go | 1 - build.ps1 | 2 +- docs/README.md | 1 + docs/assets/index.html | 3 + docs/assets/logo-black-400w.png | Bin 0 -> 6575 bytes docs/assets/logo-black-800w.png | Bin 0 -> 15920 bytes docs/faq.md | 4 +- docs/function-file.md | 53 +++++++ docs/operating/production.md | 2 +- docs/packaging.md | 14 +- docs/writing.md | 2 + examples/README.md | 9 ++ examples/hello/go/.gitignore | 2 +- examples/hello/go/README.md | 8 +- examples/hello/go/function.yaml | 4 + examples/hello/go/hello.go | 2 +- examples/hello/node/.gitignore | 2 +- examples/hello/node/README.md | 4 +- examples/hello/node/function.yaml | 4 + examples/hello/php/README.md | 7 +- examples/hello/python/README.md | 8 +- fnctl/README.md | 223 +++++++++++------------------- fnctl/errors.go | 8 +- fnctl/init.go | 2 +- fnctl/run.go | 2 +- 27 files changed, 284 insertions(+), 182 deletions(-) create mode 100644 docs/assets/index.html create mode 100644 docs/assets/logo-black-400w.png create mode 100644 docs/assets/logo-black-800w.png create mode 100644 docs/function-file.md create mode 100644 examples/README.md create mode 100644 examples/hello/go/function.yaml create mode 100644 examples/hello/node/function.yaml diff --git a/Dockerfile b/Dockerfile index f5439d187..335f8ecc9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,7 +1,7 @@ FROM iron/dind -RUN mkdir /app -ADD functions-alpine /app/functions WORKDIR /app +ADD functions-alpine /app/functions + CMD ["./functions"] diff --git a/README.md b/README.md index 3b2b4b402..d4bac67e3 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# IronFunctions +![IronFunctions](docs/assets/logo-black-400w.png) [![CircleCI](https://circleci.com/gh/iron-io/functions.svg?style=svg)](https://circleci.com/gh/iron-io/functions) [![GoDoc](https://godoc.org/github.com/iron-io/functions?status.svg)](https://godoc.org/github.com/iron-io/functions) @@ -8,7 +8,7 @@ Welcome to IronFunctions! The open source serverless platform. ## What is IronFunctions? IronFunctions is an open source serverless platform, or as we like to refer to it, Functions as a -Service (FaaS) platform that you can run anywhere. +Service (FaaS) platform that you can run anywhere. * [Run anywhere](docs/faq.md#where-can-i-run-ironfunctions) * Public cloud, hybrid, on-premise @@ -17,6 +17,8 @@ Service (FaaS) platform that you can run anywhere. * [AWS Lambda support](docs/lambda/README.md) * Easy to use * Easy to scale +* Written in Go +* API Gateway built in ## What is Serverless/FaaS? @@ -55,7 +57,7 @@ and you'll find plenty of information. We have pretty thorough post on the Iron. ## Join Our Community -First off, join the community! +Join our Slack community to get help and give feedback. [![Slack Status](https://open-iron.herokuapp.com/badge.svg)](http://get.iron.io/open-slack) @@ -63,24 +65,77 @@ First off, join the community! This guide will get you up and running in a few minutes. -### Run IronFunctions Container +### Prequisites -To get started quickly with IronFunctions, you can just fire up an `iron/functions` container: +* Docker 1.10 or later installed and running +* Logged into Docker Hub (`docker login`) + +### Run IronFunctions + +To get started quickly with IronFunctions, just fire up an `iron/functions` container: ```sh docker run --rm -it --name functions --privileged -v $PWD/data:/app/data -p 8080:8080 iron/functions ``` -**Note**: A list of configurations via env variables can be found [here](docs/options.md). +This will start IronFunctions in single server mode, using an embedded database and message queue. You can find all the +configuration options [here](docs/options.md). If you are on Windows, see [windows](docs/operating/windows.md). ### CLI tool -The IronFunctions CLI tool is optional, but it makes things easier. Install it with: +Install the IronFunctions CLI tool: ```sh curl -sSL http://get.iron.io/fnctl | sh ``` +### Write a Function + +Functions are small, bite sized bits of code that do one simple thing. Forget about monoliths when using functions, +just focus on the task that you want the function to perform. + +The following is a Go function that just returns "Hello ${NAME}!": + +```go +package main + +import ( + "encoding/json" + "fmt" + "os" +) + +type Person struct { + Name string +} + +func main() { + p := &Person{Name: "World"} + json.NewDecoder(os.Stdin).Decode(p) + fmt.Printf("Hello %v!", p.Name) +} +``` + +Copy and paste the code above into a file called `hello.go`, then run: + +```sh +# create func.yaml file, replace $USERNAME with your Docker Hub username. +fnctl init $USERNAME/hello +# build the function +fnctl build +# test it +fnctl run +# push it to Docker Hub +fnctl push +# create an app +fnctl apps create myapp +# create a route that maps /hello to your new function +fnctl routes create myapp /hello +``` + +You can find a bunch of examples in various languages in the [examples](examples/) directory. You can also +write your functions in AWS's [Lambda format](docs/lambda/README.md). + ### Create an Application An application is essentially a grouping of functions, that put together, form an API. Here's how to create an app. @@ -99,7 +154,7 @@ curl -H "Content-Type: application/json" -X POST -d '{ [More on apps](docs/apps.md). -Now that we have an app, we can map routes to functions. +Now that we have an app, we can route endpoints to functions. ### Add a Route @@ -209,13 +264,27 @@ Read more on [logging](docs/logging.md). See [Writing Functions](docs/writing.md). +And you can find a bunch of examples in the [/examples](/examples) directory. + ## More Documentation See [docs/](docs/README.md) for full documentation. -## Want to contribute to IronFunctions? +## Roadmap -See [contributing](CONTRIBUTING.md). +These are the high level roadmap goals. See [milestones](https://github.com/iron-io/functions/milestones) for detailed issues. + +* Alpha 1 - November 2016 + * Initial release of base framework + * Lambda support +* Alpha 2 - December 2016 + * Streaming input for hot containers #214 + * Logging endpoint(s) for per function debugging #263 +* Beta 1 - January 2017 + * Smart Load Balancer #151 +* Beta 2 - February 2017 + * Cron like scheduler #100 +* GA - March 2017 ## Support @@ -224,4 +293,8 @@ You can get community support via: * [Stack Overflow](http://stackoverflow.com/questions/tagged/ironfunctions) * [Slack](https://get.iron.io/open-slack) -You can get commercial support by contacting [Iron.io](https://iron.io) +You can get commercial support by contacting [Iron.io](https://iron.io/contact) + +## Want to contribute to IronFunctions? + +See [contributing](CONTRIBUTING.md). diff --git a/api/ifaces/app.go b/api/ifaces/app.go index d70c2b3f0..f2a9bffb0 100644 --- a/api/ifaces/app.go +++ b/api/ifaces/app.go @@ -9,7 +9,6 @@ type App interface { } type Route interface { - // AppName() string `json:"appname"` Path() string Image() string Headers() http.Header diff --git a/build.ps1 b/build.ps1 index eee32f174..c12014be8 100644 --- a/build.ps1 +++ b/build.ps1 @@ -11,7 +11,7 @@ function build () { function run () { build - docker run --rm --privileged -it -e LOG_LEVEL=debug -e "DB_URL=bolt:///app/data/bolt.db" -v ${pwd}/data:/app/data -p 8080:8080 iron/functions + docker run --rm --name functions -it -v /var/run/docker.sock:/var/run/docker.sock -e LOG_LEVEL=debug -e "DB_URL=bolt:///app/data/bolt.db" -v $PWD/data:/app/data -p 8080:8080 iron/functions } switch ($cmd) diff --git a/docs/README.md b/docs/README.md index 13d924221..f302a77ac 100644 --- a/docs/README.md +++ b/docs/README.md @@ -10,6 +10,7 @@ If you are a developer using IronFunctions through the API, this section is for * [fnctl (CLI Tool)](/fnctl/README.md) * [Writing functions](writing.md) * [Writing Lambda functions](docs/lambda/create.md) +* [Function file (func.yaml)](docs/function-file.md) * [Packaging functions](packaging.md) * [Open Function Format](function-format.md) * [API Reference](https://app.swaggerhub.com/api/iron/functions/) diff --git a/docs/assets/index.html b/docs/assets/index.html new file mode 100644 index 000000000..0fa5f80f5 --- /dev/null +++ b/docs/assets/index.html @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/logo-black-400w.png b/docs/assets/logo-black-400w.png new file mode 100644 index 0000000000000000000000000000000000000000..a836bf5d45c6e7421e31119a8e0ceb415e5a9711 GIT binary patch literal 6575 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 scLs7{q@FsdQA6s!{Qri^78%# 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 D9k4~u literal 0 HcmV?d00001 diff --git a/docs/faq.md b/docs/faq.md index fa7f08513..71a9babc8 100644 --- a/docs/faq.md +++ b/docs/faq.md @@ -10,9 +10,9 @@ base [function format](function-format.md). Anywhere. Any cloud, on-premise, on your laptop. As long as you can run a Docker container, you can run IronFunctions. -## Which orchestration platforms does IronFunctions support? +## Which orchestration tools does IronFunctions support? -IronFunctions can run using any orchestration tool, any server +IronFunctions can be deployed using any orchestration tool. ## Does IronFunctions require Docker? diff --git a/docs/function-file.md b/docs/function-file.md new file mode 100644 index 000000000..357d3049e --- /dev/null +++ b/docs/function-file.md @@ -0,0 +1,53 @@ +# Function files + +Functions files are used to assist fnctl to help you when creating functions. + +The files can be named as: + +- func.yaml +- func.json + +An example of a function file: + +```yaml +name: iron/hello +version: 0.0.1 +type: sync +memory: 128 +config: + key: value + key2: value2 + keyN: valueN +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. + +`version` represents current version of the function. When publishing, it is +appended to the image as a tag. + +`type` (optional) allows you to set the type of the route. `sync`, for functions +whose response are sent back to the requester; or `async`, for functions that +are started and return a task ID to customer while it executes in background. +Default: `sync`. + +`memory` (optional) allows you to set a maximum memory threshold for this +function. If this function exceeds this limit during execution, it is stopped +and error message is logged. Default: `128`. + +`config` (optional) is a set of configurations to be passed onto the route +setup. These configuration options shall override application configuration +during functions execution. + +`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`. \ No newline at end of file diff --git a/docs/operating/production.md b/docs/operating/production.md index 0303524ca..05e08914a 100644 --- a/docs/operating/production.md +++ b/docs/operating/production.md @@ -9,7 +9,7 @@ to use more production ready components. Here's a rough diagram of what a production deployment looks like: -![IronFunctions Architecture Diagram](/docs/assets/architecture.svg) +![IronFunctions Architecture Diagram](../assets/architecture.png) ## Load Balancer diff --git a/docs/packaging.md b/docs/packaging.md index 1d66175a2..940af46bf 100644 --- a/docs/packaging.md +++ b/docs/packaging.md @@ -7,7 +7,17 @@ Packaging a function has two parts: Once it's pushed to a registry, you can use it by referencing it when adding a route. -## Creating an image +## Using fnctl + +This is the easiest way to build, package and publish your functions. + + + + + +## + +### Creating an image The basic Dockerfile for most languages is along these lines: @@ -35,7 +45,7 @@ Or using [fnctl](../fnctl/README.md): fnctl build ``` -## Push your image +### Push your image This part is simple: diff --git a/docs/writing.md b/docs/writing.md index 494fe4ca7..6a3ab5916 100644 --- a/docs/writing.md +++ b/docs/writing.md @@ -3,6 +3,8 @@ This will give you the basic overview of writing base level functions. You can also use higher level abstractions that make it easier such as [lambda](lambda/README.md). +Also, for complete examples in various languages, see the [examples directory](/examples). + ## Code The most basic code layout in any language is as follows, this is pseudo code and is not meant to run. diff --git a/examples/README.md b/examples/README.md new file mode 100644 index 000000000..3b6ea228f --- /dev/null +++ b/examples/README.md @@ -0,0 +1,9 @@ +# Example Functions + +This directory has a collection of example functions you can look at to learn more about how to write them +or just copy one and build on it to get started faster. + +## Hello World Examples + +The [Hello World examples](hello/) are the most basic functions you can write and we'll try to have an example in most major languages. +This is a good place to start and good examples to copy and build upon. diff --git a/examples/hello/go/.gitignore b/examples/hello/go/.gitignore index c7428d9d4..47868d339 100644 --- a/examples/hello/go/.gitignore +++ b/examples/hello/go/.gitignore @@ -4,4 +4,4 @@ vendor/ /app /__uberscript__ -function.yaml +func.yaml diff --git a/examples/hello/go/README.md b/examples/hello/go/README.md index 166d8970e..02bc28e70 100644 --- a/examples/hello/go/README.md +++ b/examples/hello/go/README.md @@ -3,22 +3,20 @@ This example will show you how to test and deploy Go (Golang) code to IronFunctions. ```sh +# create your func.yaml file fnctl init /hello +# build the function 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 ``` -Now you use your function on IronFunctions: +Now you can call your function on IronFunctions: ```sh curl -H "Content-Type: application/json" -X POST -d @hello.payload.json http://localhost:8080/r/myapp/hello ``` - -Or surf to it: http://localhost:8080/r/myapp/hello diff --git a/examples/hello/go/function.yaml b/examples/hello/go/function.yaml new file mode 100644 index 000000000..946aea6a2 --- /dev/null +++ b/examples/hello/go/function.yaml @@ -0,0 +1,4 @@ +name: treeder/hello4 +version: 0.0.1 +runtime: go +entrypoint: ./func diff --git a/examples/hello/go/hello.go b/examples/hello/go/hello.go index fa1ef4b03..bf0518407 100644 --- a/examples/hello/go/hello.go +++ b/examples/hello/go/hello.go @@ -13,5 +13,5 @@ type Person struct { func main() { p := &Person{Name: "World"} json.NewDecoder(os.Stdin).Decode(p) - fmt.Println("Hello", p.Name, "!!!") + fmt.Printf("Hello %v!", p.Name) } diff --git a/examples/hello/node/.gitignore b/examples/hello/node/.gitignore index 6142b82f2..8977e475d 100644 --- a/examples/hello/node/.gitignore +++ b/examples/hello/node/.gitignore @@ -1,3 +1,3 @@ node_modules/ -function.yaml +func.yaml Dockerfile diff --git a/examples/hello/node/README.md b/examples/hello/node/README.md index c5585083e..c46e3b67c 100644 --- a/examples/hello/node/README.md +++ b/examples/hello/node/README.md @@ -3,15 +3,15 @@ This example will show you how to test and deploy a Node function to IronFunctions. ```sh +# create your func.yaml file fnctl init /hello +# build the function 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 ``` diff --git a/examples/hello/node/function.yaml b/examples/hello/node/function.yaml new file mode 100644 index 000000000..e08c8786a --- /dev/null +++ b/examples/hello/node/function.yaml @@ -0,0 +1,4 @@ +name: treeder/node4 +version: 0.0.2 +runtime: node +entrypoint: node hello.js diff --git a/examples/hello/php/README.md b/examples/hello/php/README.md index 204d2ab17..240723da7 100644 --- a/examples/hello/php/README.md +++ b/examples/hello/php/README.md @@ -2,13 +2,14 @@ This example will show you how to test and deploy Go (Golang) code to IronFunctions. -### 1. Prepare the `functions.yaml` file: +### 1. Prepare the `func.yaml` file: -At functions.yaml you will find: +At func.yaml you will find: ```yml app: phpapp route: /hello -image: USERNAME/hello:0.0.1 +image: USERNAME/hello +version: 0.0.1 build: - docker run --rm -v "$PWD":/worker -w /worker iron/php:dev composer install ``` diff --git a/examples/hello/python/README.md b/examples/hello/python/README.md index debc4f523..8328c524c 100644 --- a/examples/hello/python/README.md +++ b/examples/hello/python/README.md @@ -2,13 +2,15 @@ This example will show you how to test and deploy Go (Golang) code to IronFunctions. -### 1. Prepare the `functions.yaml` file: +### 1. Prepare the `func.yaml` file: + +At func.yaml you will find: -At functions.yaml you will find: ```yml app: pythonapp route: /hello -image: USERNAME/hello:0.0.1 +image: USERNAME/hello +version: 0.0.1 build: - docker run --rm -v "$PWD":/worker -w /worker iron/python:2-dev pip install -t packages -r requirements.txt ``` diff --git a/fnctl/README.md b/fnctl/README.md index 9db261a68..453cc0eb7 100644 --- a/fnctl/README.md +++ b/fnctl/README.md @@ -1,16 +1,49 @@ # IronFunctions CLI -## Init +## Creating Functions -usage: fnctl init [--runtime node] [--entrypoint "node hello.js"] +### init -Init will help you create a function.yaml file for the current directory. +Init will help you create a [function file](../docs/function-file.md) (func.yaml) in the current directory. -If there's a Dockerfile found, this will generate the basic file with just the image name. -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 init [--runtime node] [--entrypoint "node hello.js"] +``` -## Basic +`--runtime` and `--entrypoint` are optional, init will try to figure out it out based on the files in the current directory. +If it can't figure it out, it will tell you. + +If there's a Dockerfile found, it will use that as is + +### Build, Bump, Run, Push + +`fnctl` provides a few commands you'll use while creating and updating your functions: `build`, `bump`, `run` and `push`. + +Build will build the image for your function. + +```sh +fnctl build +``` + +Bump will bump the version number in your func.yaml file. Versions must be in [semver](http://semver.org/) format. + +```sh +fnctl bump +``` + +Run will help you test your function. Functions read input from STDIN, so you can pipe the payload into the function like this: + +```sh +cat `payload.json` | fnctl run +``` + +Push will push the function image to Docker Hub. + +```sh +fnctl push +``` + +## Using the API You can operate IronFunctions from the command line. @@ -40,6 +73,47 @@ $ fnctl routes delete otherapp hello # delete route /hello deleted ``` +## Application level configuration + +When creating an application, you can configure it to tweak its behavior and its +routes' with an appropriate flag, `config`. + +Thus a more complete example of an application creation will look like: +```sh +fnctl apps create --config DB_URL=http://example.org/ otherapp +``` + +`--config` is a map of values passed to the route runtime in the form of +environment variables prefixed with `CONFIG_`. + +Repeated calls to `fnctl apps create` will trigger an update of the given +route, thus you will be able to change any of these attributes later in time +if necessary. + +## Route level configuration + +When creating a route, you can configure it to tweak its behavior, the possible +choices are: `memory`, `type` and `config`. + +Thus a more complete example of route creation will look like: +```sh +fnctl routes create --memory 256 --type async --config DB_URL=http://example.org/ otherapp /hello iron/hello +``` + +`--memory` is number of usable MiB for this function. If during the execution it +exceeds this maximum threshold, it will halt and return an error in the logs. + +`--type` is the type of the function. Either `sync`, in which the client waits +until the request is successfully completed, or `async`, in which the clients +dispatches a new request, gets a task ID back and closes the HTTP connection. + +`--config` is a map of values passed to the route runtime in the form of +environment variables prefixed with `CONFIG_`. + +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 +if necessary. + ## Changing target host `fnctl` is configured by default to talk http://localhost:8080. @@ -101,142 +175,11 @@ 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` +`fnctl update` expects that each directory to contain a file `func.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. -## Functions files (functions.yaml) - -Functions files are used to assist fnctl to execute bulk updates of your -functions. The files can be named as: - -- functions.yaml -- functions.yml -- function.yaml -- function.yml -- functions.json -- function.json -- fn.yaml -- fn.yml -- fn.json - -An example of a function file: -```yaml -app: myapp -name: iron/hello -route: "/custom/route" -version: 0.0.1 -type: sync -memory: 128 -config: - key: value - key2: value2 - keyN: valueN -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. - -`version` represents current version of the function. When publishing, it is -appended to the image as a tag. - -`type` (optional) allows you to set the type of the route. `sync`, for functions -whose response are sent back to the requester; or `async`, for functions that -are started and return a task ID to customer while it executes in background. -Default: `sync`. - -`memory` (optional) allows you to set a maximum memory threshold for this -function. If this function exceeds this limit during execution, it is stopped -and error message is logged. Default: `128`. - -`config` (optional) is a set of configurations to be passed onto the route -setup. These configuration options shall override application configuration -during functions execution. - -`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, Bump, Push - -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 whose `version` key in function file -follows [semver](http://semver.org/) rules and bump their version according. - -`fnctl push` will scan all IronFunctions and push their images to Docker Hub, -and update their routes accordingly. - -## Application level configuration - -When creating an application, you can configure it to tweak its behavior and its -routes' with an appropriate flag, `config`. - -Thus a more complete example of an application creation will look like: -```sh -fnctl apps create --config DB_URL=http://example.org/ otherapp -``` - -`--config` is a map of values passed to the route runtime in the form of -environment variables prefixed with `CONFIG_`. - -Repeated calls to `fnctl apps create` will trigger an update of the given -route, thus you will be able to change any of these attributes later in time -if necessary. - -## Route level configuration - -When creating a route, you can configure it to tweak its behavior, the possible -choices are: `memory`, `type` and `config`. - -Thus a more complete example of route creation will look like: -```sh -fnctl routes create --memory 256 --type async --config DB_URL=http://example.org/ otherapp /hello iron/hello -``` - -`--memory` is number of usable MiB for this function. If during the execution it -exceeds this maximum threshold, it will halt and return an error in the logs. - -`--type` is the type of the function. Either `sync`, in which the client waits -until the request is successfully completed, or `async`, in which the clients -dispatches a new request, gets a task ID back and closes the HTTP connection. - -`--config` is a map of values passed to the route runtime in the form of -environment variables prefixed with `CONFIG_`. - -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 -if necessary. - -## Build +## Contributing Ensure you have Go configured and installed in your environment. Once it is done, run: diff --git a/fnctl/errors.go b/fnctl/errors.go index daa6764cc..ab9072d5a 100644 --- a/fnctl/errors.go +++ b/fnctl/errors.go @@ -1,13 +1,13 @@ package main -type NotFoundError struct { +type notFoundError struct { S string } -func (e *NotFoundError) Error() string { +func (e *notFoundError) Error() string { return e.S } -func newNotFoundError(s string) *NotFoundError { - return &NotFoundError{S: s} +func newNotFoundError(s string) *notFoundError { + return ¬FoundError{S: s} } diff --git a/fnctl/init.go b/fnctl/init.go index c78a3f76e..8eb14892c 100644 --- a/fnctl/init.go +++ b/fnctl/init.go @@ -75,7 +75,7 @@ func initFn() cli.Command { func (a *initFnCmd) init(c *cli.Context) error { if !a.force { ff, err := findFuncfile() - if _, ok := err.(*NotFoundError); !ok && err != nil { + if _, ok := err.(*notFoundError); !ok && err != nil { return err } if ff != nil { diff --git a/fnctl/run.go b/fnctl/run.go index fac6ca328..04b8716bf 100644 --- a/fnctl/run.go +++ b/fnctl/run.go @@ -38,7 +38,7 @@ func (r *runCmd) run(c *cli.Context) error { if image == "" { ff, err := findFuncfile() if err != nil { - if _, ok := err.(*NotFoundError); ok { + if _, ok := err.(*notFoundError); ok { return errors.New("error: image name is missing or no function file found") } else { return err