mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Merge pull request #339 from fnproject/app-yaml
Example and documentation for deploying full applications
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"`
|
||||
}
|
||||
|
||||
@@ -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
63
docs/developers/apps.md
Normal 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
31
docs/developers/model.md
Normal 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`.
|
||||
@@ -508,7 +508,7 @@ definitions:
|
||||
readOnly: true
|
||||
config:
|
||||
type: object
|
||||
description: Application configuration
|
||||
description: Application configuration, applied to all routes.
|
||||
additionalProperties:
|
||||
type: string
|
||||
|
||||
|
||||
@@ -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
5
examples/app/README.md
Normal 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
3
examples/app/app.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: helloapp
|
||||
config:
|
||||
foo: bar
|
||||
9
examples/app/footer/func.rb
Normal file
9
examples/app/footer/func.rb
Normal 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>
|
||||
}
|
||||
7
examples/app/footer/func.yaml
Normal file
7
examples/app/footer/func.yaml
Normal 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
58
examples/app/func.go
Normal 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
4
examples/app/func.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: app
|
||||
version: 0.0.70
|
||||
runtime: go
|
||||
entrypoint: ./func
|
||||
9
examples/app/header/func.rb
Normal file
9
examples/app/header/func.rb
Normal file
@@ -0,0 +1,9 @@
|
||||
puts %{
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<title>My App</title>
|
||||
</head>
|
||||
<body>
|
||||
}
|
||||
7
examples/app/header/func.yaml
Normal file
7
examples/app/header/func.yaml
Normal 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
2
examples/app/node/.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
node_modules/
|
||||
Dockerfile
|
||||
1
examples/app/node/README.md
Normal file
1
examples/app/node/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Node function
|
||||
10
examples/app/node/func.js
Normal file
10
examples/app/node/func.js
Normal 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);
|
||||
4
examples/app/node/func.yaml
Normal file
4
examples/app/node/func.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
name: node
|
||||
version: 0.0.13
|
||||
runtime: node
|
||||
entrypoint: node func.js
|
||||
7
examples/app/node/package.json
Normal file
7
examples/app/node/package.json
Normal 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
1
examples/app/python/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
||||
packages/
|
||||
1
examples/app/python/README.md
Normal file
1
examples/app/python/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Python function
|
||||
21
examples/app/python/func.py
Normal file
21
examples/app/python/func.py
Normal 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 + "!"
|
||||
4
examples/app/python/func.yaml
Normal file
4
examples/app/python/func.yaml
Normal 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
3
examples/app/ruby/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
||||
bundle/
|
||||
.bundle/
|
||||
Dockerfile
|
||||
3
examples/app/ruby/Gemfile
Normal file
3
examples/app/ruby/Gemfile
Normal file
@@ -0,0 +1,3 @@
|
||||
source 'https://rubygems.org'
|
||||
|
||||
gem 'json', '> 1.8.2'
|
||||
1
examples/app/ruby/README.md
Normal file
1
examples/app/ruby/README.md
Normal file
@@ -0,0 +1 @@
|
||||
# Ruby function
|
||||
25
examples/app/ruby/func.rb
Normal file
25
examples/app/ruby/func.rb
Normal 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
|
||||
7
examples/app/ruby/func.yaml
Normal file
7
examples/app/ruby/func.yaml
Normal file
@@ -0,0 +1,7 @@
|
||||
name: ruby
|
||||
version: 0.0.23
|
||||
runtime: ruby
|
||||
entrypoint: ruby func.rb
|
||||
headers:
|
||||
content-type:
|
||||
- text/html
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
3
examples/tutorial/hello/app.yaml
Normal file
3
examples/tutorial/hello/app.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
name: helloapp
|
||||
config:
|
||||
foo: bar
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user