Merge pull request #339 from fnproject/app-yaml

Example and documentation for deploying full applications
This commit is contained in:
Reed Allman
2017-09-20 12:38:49 -07:00
committed by GitHub
35 changed files with 309 additions and 16 deletions

View File

@@ -66,7 +66,7 @@ func TestCallConfigurationRequest(t *testing.T) {
req.Header.Add("MYREALHEADER", "FOOLORD")
req.Header.Add("MYREALHEADER", "FOOPEASANT")
req.Header.Add("Content-Length", contentLength)
req.Header.Add("FN_ROUTE", "thewrongroute") // ensures that this doesn't leak out, should be overwritten
req.Header.Add("FN_PATH", "thewrongroute") // ensures that this doesn't leak out, should be overwritten
call, err := a.GetCall(
WithWriter(w), // XXX (reed): order matters [for now]
@@ -119,7 +119,7 @@ func TestCallConfigurationRequest(t *testing.T) {
expectedBase := map[string]string{
"FN_FORMAT": format,
"FN_APP_NAME": appName,
"FN_ROUTE": path,
"FN_PATH": path,
"FN_MEMORY": strconv.Itoa(memory),
"FN_TYPE": typ,
"APP_VAR": "FOO",
@@ -210,7 +210,7 @@ func TestCallConfigurationModel(t *testing.T) {
env := map[string]string{
"FN_FORMAT": format,
"FN_APP_NAME": appName,
"FN_ROUTE": path,
"FN_PATH": path,
"FN_MEMORY": strconv.Itoa(memory),
"FN_TYPE": typ,
"APP_VAR": "FOO",

View File

@@ -79,7 +79,8 @@ func FromRequest(appName, path string, req *http.Request) CallOpt {
baseVars["FN_FORMAT"] = route.Format
baseVars["FN_APP_NAME"] = appName
baseVars["FN_ROUTE"] = route.Path
baseVars["FN_PATH"] = route.Path
// TODO: might be a good idea to pass in: envVars["FN_BASE_PATH"] = fmt.Sprintf("/r/%s", appName) || "/" if using DNS entries per app
baseVars["FN_MEMORY"] = fmt.Sprintf("%d", route.Memory)
baseVars["FN_TYPE"] = route.Type
@@ -188,7 +189,7 @@ func noOverrideVars(key string) bool {
var overrideVars = map[string]bool{
"FN_FORMAT": true,
"FN_APP_NAME": true,
"FN_ROUTE": true,
"FN_PATH": true,
"FN_MEMORY": true,
"FN_TYPE": true,
"FN_CALL_ID": true,

View File

@@ -154,7 +154,7 @@ var (
}
)
// any error that implements this interface will return an API response
// APIError any error that implements this interface will return an API response
// with the provided status code and error message body
type APIError interface {
Code() int
@@ -170,7 +170,7 @@ func (e err) Code() int { return e.code }
func NewAPIError(code int, e error) APIError { return err{code, e} }
// uniform error output
// Error uniform error output
type Error struct {
Error *ErrorBody `json:"error,omitempty"`
}

View File

@@ -8,13 +8,15 @@ If you are a developer using Fn through the API, this section is for you.
* [Usage](usage.md)
* [Writing functions](writing.md)
* [fn (CLI Tool)](https://github.com/fnproject/cli/blob/master/README.md)
* [Hot functions](hot-functions.md)
* [Async functions](async.md)
* [Organizing functions into an application](developers/apps.md)
* [Function file (func.yaml)](function-file.md)
* [Client Libraries](developers/clients.md)
* [Packaging functions](packaging.md)
* [Open Function Format](function-format.md)
* API Reference (coming soon)
* [Hot functions](hot-functions.md)
* [Async functions](async.md)
* [Object Model](developers/model.md)
* [FAQ](faq.md)
## For Operators

63
docs/developers/apps.md Normal file
View File

@@ -0,0 +1,63 @@
# Applications
In `fn`, an application is a group of functions with path mappings (routes) to each function ([learn more](model.md)).
We've tried to make it easy to work with full applications by providing tools that work with all the applications functions.
## Creating an Application
All you have to do is create a file called `app.yaml` in your applications root directory, and the only required field is a name:
```yaml
name: myawesomeapp
```
Once you have that file in place, the `fn` commands will work in the context of that application.
## The Index Function (aka: Root Function)
The root app directory can also contain a `func.yaml` which will be the function access at `/`.
## Function paths
By default, the function name and path will be the same as the directory structure. For instance, if you
have a structure like this:
```txt
- app.yaml
- func.yaml
- func.go
- hello/
- func.yaml
- func.js
- users/
- func.yaml
- func.rb
```
The URL's to access those functions will be:
```
http://abc.io/ -> root function
http://abc.io/hello -> function in hello/ directory
http://abc.io/users -> function in users/ directory
```
## Deploying an entire app at once
```sh
fn deploy --all
```
If you're just testing locally, you can speed it up with the `--local` flag.
## Deploying a single function in the app
To deploy the `hello` function only, from the root dir, run:
```sh
fn deploy hello
```
## Example app
See https://github.com/fnproject/fn/tree/master/examples/app for a simple example.

31
docs/developers/model.md Normal file
View File

@@ -0,0 +1,31 @@
# Object Model
This document describes the different objects we store and the relationships between them.
## Applications
At the root of everything are applications. In `fn`, an application is essentially a grouping of functions
with path mappings (routes) to each function. For instance, consider the following URLs for the app called `myapp`:
```
http://myapp.com/hello
http://myapp.com/users
```
This is an app with 2 routes:
1. A mapping of the path `/hello` to a function called `hello`
1. A mapping of the path `/users` to a function called `users`
## Routes
An app consists of 1 or more routes. A route stores the mapping between URL paths and functions (ie: container iamges).
## Calls
A call represents an invocation of a function. Every request for a URL as defined in the routes, a call is created.
The `call_id` for each request will show up in all logs and the status of the call, as well as the logs, can be retrieved using the `call_id`.
## Logs
Logs are stored for each `call` that is made and can be retrieved with the `call_id`.

View File

@@ -508,7 +508,7 @@ definitions:
readOnly: true
config:
type: object
description: Application configuration
description: Application configuration, applied to all routes.
additionalProperties:
type: string

View File

@@ -31,7 +31,7 @@ You will also have access to a set of environment variables.
* `FN_REQUEST_URL` - the full URL for the request ([parsing example](https://github.com/fnproject/fn/tree/master/examples/tutorial/params))
* `FN_APP_NAME` - the name of the application that matched this route, eg: `myapp`
* `FN_ROUTE` - the matched route, eg: `/hello`
* `FN_PATH` - the matched route, eg: `/hello`
* `FN_METHOD` - the HTTP method for the request, eg: `GET` or `POST`
* `FN_CALL_ID` - a unique ID for each function execution.
* `FN_FORMAT` - a string representing one of the [function formats](function-format.md), currently either `default` or `http`. Default is `default`.

5
examples/app/README.md Normal file
View File

@@ -0,0 +1,5 @@
# App Example
This shows you how to organize functions into a full application and deploy them easily with one command.
See [apps documentation](/docs/developers/app.md) for details on how to use this.

3
examples/app/app.yaml Normal file
View File

@@ -0,0 +1,3 @@
name: helloapp
config:
foo: bar

View File

@@ -0,0 +1,9 @@
puts %{
<div style="margin-top: 20px; border-top: 1px solid gray;">
<div><a href="/r/#{ENV['FN_APP_NAME']}/ruby">Ruby</a></div>
<div><a href="/r/#{ENV['FN_APP_NAME']}/node">Node</a></div>
<div><a href="/r/#{ENV['FN_APP_NAME']}/python">Python</a></div>
</div>
</body>
</html>
}

View File

@@ -0,0 +1,7 @@
name: footer
version: 0.0.13
runtime: ruby
entrypoint: ruby func.rb
headers:
content-type:
- text/html

58
examples/app/func.go Normal file
View File

@@ -0,0 +1,58 @@
package main
import (
"fmt"
"html/template"
"log"
"os"
)
type Link struct {
Text string
Href string
}
func main() {
const tpl = `
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>{{.Title}}</title>
</head>
<body>
<h1>{{.Title}}</h1>
<p>{{.Body}}</p>
<div>
{{range .Items}}<div><a href="{{.Href}}">{{ .Text }}</a></div>{{else}}<div><strong>no rows</strong></div>{{end}}
</div>
</body>
</html>`
check := func(err error) {
if err != nil {
log.Fatal(err)
}
}
t, err := template.New("webpage").Parse(tpl)
check(err)
appName := os.Getenv("FN_APP_NAME")
data := struct {
Title string
Body string
Items []Link
}{
Title: "My App",
Body: "This is my app. It may not be the best app, but it's my app. And it's multilingual!",
Items: []Link{
Link{"Ruby", fmt.Sprintf("/r/%s/ruby", appName)},
Link{"Node", fmt.Sprintf("/r/%s/node", appName)},
Link{"Python", fmt.Sprintf("/r/%s/python", appName)},
},
}
err = t.Execute(os.Stdout, data)
check(err)
}

4
examples/app/func.yaml Normal file
View File

@@ -0,0 +1,4 @@
name: app
version: 0.0.70
runtime: go
entrypoint: ./func

View File

@@ -0,0 +1,9 @@
puts %{
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>My App</title>
</head>
<body>
}

View File

@@ -0,0 +1,7 @@
name: header
version: 0.0.11
runtime: ruby
entrypoint: ruby func.rb
headers:
content-type:
- text/html

2
examples/app/node/.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
node_modules/
Dockerfile

View File

@@ -0,0 +1 @@
# Node function

10
examples/app/node/func.js Normal file
View File

@@ -0,0 +1,10 @@
fs = require('fs');
name = "do you speak node?";
try {
obj = JSON.parse(fs.readFileSync('/dev/stdin').toString())
if (obj.name != "") {
name = obj.name
}
} catch(e) {}
console.log("Hello, " + name);

View File

@@ -0,0 +1,4 @@
name: node
version: 0.0.13
runtime: node
entrypoint: node func.js

View File

@@ -0,0 +1,7 @@
{
"name": "my-awesome-func",
"version": "1.0.0",
"dependencies": {
"is-positive": "^3.1.0"
}
}

1
examples/app/python/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
packages/

View File

@@ -0,0 +1 @@
# Python function

View File

@@ -0,0 +1,21 @@
import sys
import os
import json
sys.stderr.write("Starting Python Function\n")
name = "I speak Python too"
try:
if not os.isatty(sys.stdin.fileno()):
try:
obj = json.loads(sys.stdin.read())
if obj["name"] != "":
name = obj["name"]
except ValueError:
# ignore it
sys.stderr.write("no input, but that's ok\n")
except:
pass
print "Hello, " + name + "!"

View File

@@ -0,0 +1,4 @@
name: python
version: 0.0.11
runtime: python
entrypoint: python2 func.py

3
examples/app/ruby/.gitignore vendored Normal file
View File

@@ -0,0 +1,3 @@
bundle/
.bundle/
Dockerfile

View File

@@ -0,0 +1,3 @@
source 'https://rubygems.org'
gem 'json', '> 1.8.2'

View File

@@ -0,0 +1 @@
# Ruby function

25
examples/app/ruby/func.rb Normal file
View File

@@ -0,0 +1,25 @@
require 'uri'
require 'net/http'
require 'json'
name = "I love rubies"
payload = STDIN.read
if payload != ""
payload = JSON.parse(payload)
name = payload['name']
end
def open(url)
Net::HTTP.get(URI.parse(url))
end
h = "docker.for.mac.localhost" # ENV['HOSTNAME']
header = open("http://#{h}:8080/r/#{ENV['FN_APP_NAME']}/header") # todo: grab env vars to construct this
puts header
puts "Hello, #{name}! YOOO"
footer = open("http://#{h}:8080/r/#{ENV['FN_APP_NAME']}/footer") # todo: grab env vars to construct this
puts footer

View File

@@ -0,0 +1,7 @@
name: ruby
version: 0.0.23
runtime: ruby
entrypoint: ruby func.rb
headers:
content-type:
- text/html

View File

@@ -13,7 +13,7 @@ import (
var noAuth = map[string]interface{}{}
func main() {
request := fmt.Sprintf("%s %s", os.Getenv("FN_METHOD"), os.Getenv("FN_ROUTE"))
request := fmt.Sprintf("%s %s", os.Getenv("FN_METHOD"), os.Getenv("FN_PATH"))
dbURI := os.Getenv("DB")
if dbURI == "" {
@@ -36,7 +36,7 @@ func main() {
}
// GETTING TOKEN
if os.Getenv("FN_ROUTE") == "/token" {
if os.Getenv("FN_PATH") == "/token" {
route.HandleToken(db)
return
}

View File

@@ -9,8 +9,8 @@ docker rm test-mongo-func
docker run -p 27017:27017 --name test-mongo-func -d mongo
echo '{ "title": "My New Post", "body": "Hello world!", "user": "test" }' | docker run --rm -i -e FN_METHOD=POST -e FN_ROUTE=/posts -e DB=mongo:27017 --link test-mongo-func:mongo -e TEST=1 username/func-blog
docker run --rm -i -e FN_METHOD=GET -e FN_ROUTE=/posts -e DB=mongo:27017 --link test-mongo-func:mongo -e TEST=1 username/func-blog
echo '{ "title": "My New Post", "body": "Hello world!", "user": "test" }' | docker run --rm -i -e FN_METHOD=POST -e FN_PATH=/posts -e DB=mongo:27017 --link test-mongo-func:mongo -e TEST=1 username/func-blog
docker run --rm -i -e FN_METHOD=GET -e FN_PATH=/posts -e DB=mongo:27017 --link test-mongo-func:mongo -e TEST=1 username/func-blog
docker stop test-mongo-func
docker rm test-mongo-func

View File

@@ -35,7 +35,7 @@ e = ENV["FN_APP_NAME"]
if e == nil || e == ''
raise "No APP_NAME found"
end
e = ENV["FN_ROUTE"]
e = ENV["FN_PATH"]
if e == nil || e == ''
raise "No ROUTE found"
end

View File

@@ -0,0 +1,3 @@
name: helloapp
config:
foo: bar

View File

@@ -18,6 +18,7 @@ func main() {
mapB, _ := json.Marshal(mapD)
fmt.Println(string(mapB))
// TODO: move these lines to a test, this was for testing log output issues
log.Println("---> stderr goes to the server logs.")
log.Println("---> LINE 2")
log.Println("---> LINE 3 with a break right here\nand LINE 4")