Add annotations to routes and apps (#866)

Adds 'annotations' attribute to Routes and Apps
This commit is contained in:
Owen Cliffe
2018-03-20 18:02:49 +00:00
committed by GitHub
parent 845f40ee86
commit d25b5af59d
31 changed files with 1838 additions and 669 deletions

6
Gopkg.lock generated
View File

@@ -193,8 +193,8 @@
"client/routes", "client/routes",
"models" "models"
] ]
revision = "1c5ec475d4536388b366b1f075945cd64d8c1cb4" revision = "e2f92e36625a4b93c596ac3b912a4994ae574f64"
version = "0.2.4" version = "0.2.6"
[[projects]] [[projects]]
name = "github.com/fsouza/go-dockerclient" name = "github.com/fsouza/go-dockerclient"
@@ -653,6 +653,6 @@
[solve-meta] [solve-meta]
analyzer-name = "dep" analyzer-name = "dep"
analyzer-version = 1 analyzer-version = 1
inputs-digest = "d8661bd78eda741b449a3675c52207af1bd738c63cb4f74d018edbaf0e9ef77d" inputs-digest = "bd152d9d0bb0ac9975ecc30f368747c21a047f30bd5037fb19e4835631baae52"
solver-name = "gps-cdcl" solver-name = "gps-cdcl"
solver-version = 1 solver-version = 1

View File

@@ -27,7 +27,7 @@ ignored = ["github.com/fnproject/fn/cli"]
[[constraint]] [[constraint]]
name = "github.com/fnproject/fn_go" name = "github.com/fnproject/fn_go"
version = "0.2.0" version = "0.2.6"
[[constraint]] [[constraint]]
name = "github.com/gin-gonic/gin" name = "github.com/gin-gonic/gin"

View File

@@ -51,6 +51,7 @@ func (v *validator) UpdateApp(ctx context.Context, app *models.App) (*models.App
if app.Name == "" { if app.Name == "" {
return nil, models.ErrAppsMissingName return nil, models.ErrAppsMissingName
} }
return v.Datastore.UpdateApp(ctx, app) return v.Datastore.UpdateApp(ctx, app)
} }

View File

@@ -0,0 +1,27 @@
package migrations
import (
"context"
"github.com/fnproject/fn/api/datastore/sql/migratex"
"github.com/jmoiron/sqlx"
)
func up8(ctx context.Context, tx *sqlx.Tx) error {
_, err := tx.ExecContext(ctx, "ALTER TABLE apps ADD annotations TEXT;")
return err
}
func down8(ctx context.Context, tx *sqlx.Tx) error {
_, err := tx.ExecContext(ctx, "ALTER TABLE apps DROP COLUMN annotations;")
return err
}
func init() {
Migrations = append(Migrations, &migratex.MigFields{
VersionFunc: vfunc(8),
UpFunc: up8,
DownFunc: down8,
})
}

View File

@@ -0,0 +1,27 @@
package migrations
import (
"context"
"github.com/fnproject/fn/api/datastore/sql/migratex"
"github.com/jmoiron/sqlx"
)
func up9(ctx context.Context, tx *sqlx.Tx) error {
_, err := tx.ExecContext(ctx, "ALTER TABLE routes ADD annotations TEXT;")
return err
}
func down9(ctx context.Context, tx *sqlx.Tx) error {
_, err := tx.ExecContext(ctx, "ALTER TABLE routes DROP COLUMN annotations;")
return err
}
func init() {
Migrations = append(Migrations, &migratex.MigFields{
VersionFunc: vfunc(9),
UpFunc: up9,
DownFunc: down9,
})
}

View File

@@ -50,6 +50,7 @@ var tables = [...]string{`CREATE TABLE IF NOT EXISTS routes (
type varchar(16) NOT NULL, type varchar(16) NOT NULL,
headers text NOT NULL, headers text NOT NULL,
config text NOT NULL, config text NOT NULL,
annotations text NOT NULL,
created_at text, created_at text,
updated_at varchar(256), updated_at varchar(256),
PRIMARY KEY (app_name, path) PRIMARY KEY (app_name, path)
@@ -58,6 +59,7 @@ var tables = [...]string{`CREATE TABLE IF NOT EXISTS routes (
`CREATE TABLE IF NOT EXISTS apps ( `CREATE TABLE IF NOT EXISTS apps (
name varchar(256) NOT NULL PRIMARY KEY, name varchar(256) NOT NULL PRIMARY KEY,
config text NOT NULL, config text NOT NULL,
annotations text NOT NULL,
created_at varchar(256), created_at varchar(256),
updated_at varchar(256) updated_at varchar(256)
);`, );`,
@@ -83,7 +85,7 @@ var tables = [...]string{`CREATE TABLE IF NOT EXISTS routes (
} }
const ( const (
routeSelector = `SELECT app_name, path, image, format, memory, cpus, type, timeout, idle_timeout, headers, config, created_at, updated_at FROM routes` routeSelector = `SELECT app_name, path, image, format, memory, cpus, type, timeout, idle_timeout, headers, config, annotations, created_at, updated_at FROM routes`
callSelector = `SELECT id, created_at, started_at, completed_at, status, app_name, path, stats, error FROM calls` callSelector = `SELECT id, created_at, started_at, completed_at, status, app_name, path, stats, error FROM calls`
) )
@@ -254,12 +256,14 @@ func (ds *sqlStore) InsertApp(ctx context.Context, app *models.App) (*models.App
query := ds.db.Rebind(`INSERT INTO apps ( query := ds.db.Rebind(`INSERT INTO apps (
name, name,
config, config,
annotations,
created_at, created_at,
updated_at updated_at
) )
VALUES ( VALUES (
:name, :name,
:config, :config,
:annotations,
:created_at, :created_at,
:updated_at :updated_at
);`) );`)
@@ -290,7 +294,7 @@ func (ds *sqlStore) UpdateApp(ctx context.Context, newapp *models.App) (*models.
err := ds.Tx(func(tx *sqlx.Tx) error { err := ds.Tx(func(tx *sqlx.Tx) error {
// NOTE: must query whole object since we're returning app, Update logic // NOTE: must query whole object since we're returning app, Update logic
// must only modify modifiable fields (as seen here). need to fix brittle.. // must only modify modifiable fields (as seen here). need to fix brittle..
query := tx.Rebind(`SELECT name, config, created_at, updated_at FROM apps WHERE name=?`) query := tx.Rebind(`SELECT name, config, annotations, created_at, updated_at FROM apps WHERE name=?`)
row := tx.QueryRowxContext(ctx, query, app.Name) row := tx.QueryRowxContext(ctx, query, app.Name)
err := row.StructScan(app) err := row.StructScan(app)
@@ -301,8 +305,12 @@ func (ds *sqlStore) UpdateApp(ctx context.Context, newapp *models.App) (*models.
} }
app.Update(newapp) app.Update(newapp)
err = app.Validate()
if err != nil {
return err
}
query = tx.Rebind(`UPDATE apps SET config=:config, updated_at=:updated_at WHERE name=:name`) query = tx.Rebind(`UPDATE apps SET config=:config, annotations=:annotations, updated_at=:updated_at WHERE name=:name`)
res, err := tx.NamedExecContext(ctx, query, app) res, err := tx.NamedExecContext(ctx, query, app)
if err != nil { if err != nil {
return err return err
@@ -355,7 +363,7 @@ func (ds *sqlStore) RemoveApp(ctx context.Context, appName string) error {
} }
func (ds *sqlStore) GetApp(ctx context.Context, name string) (*models.App, error) { func (ds *sqlStore) GetApp(ctx context.Context, name string) (*models.App, error) {
query := ds.db.Rebind(`SELECT name, config, created_at, updated_at FROM apps WHERE name=?`) query := ds.db.Rebind(`SELECT name, config, annotations, created_at, updated_at FROM apps WHERE name=?`)
row := ds.db.QueryRowxContext(ctx, query, name) row := ds.db.QueryRowxContext(ctx, query, name)
var res models.App var res models.App
@@ -378,7 +386,7 @@ func (ds *sqlStore) GetApps(ctx context.Context, filter *models.AppFilter) ([]*m
if err != nil { if err != nil {
return nil, err return nil, err
} }
query = ds.db.Rebind(fmt.Sprintf("SELECT DISTINCT name, config, created_at, updated_at FROM apps %s", query)) query = ds.db.Rebind(fmt.Sprintf("SELECT DISTINCT name, config, annotations, created_at, updated_at FROM apps %s", query))
rows, err := ds.db.QueryxContext(ctx, query, args...) rows, err := ds.db.QueryxContext(ctx, query, args...)
if err != nil { if err != nil {
return nil, err return nil, err
@@ -434,6 +442,7 @@ func (ds *sqlStore) InsertRoute(ctx context.Context, route *models.Route) (*mode
idle_timeout, idle_timeout,
headers, headers,
config, config,
annotations,
created_at, created_at,
updated_at updated_at
) )
@@ -449,6 +458,7 @@ func (ds *sqlStore) InsertRoute(ctx context.Context, route *models.Route) (*mode
:idle_timeout, :idle_timeout,
:headers, :headers,
:config, :config,
:annotations,
:created_at, :created_at,
:updated_at :updated_at
);`) );`)
@@ -490,6 +500,7 @@ func (ds *sqlStore) UpdateRoute(ctx context.Context, newroute *models.Route) (*m
idle_timeout = :idle_timeout, idle_timeout = :idle_timeout,
headers = :headers, headers = :headers,
config = :config, config = :config,
annotations = :annotations,
updated_at = :updated_at updated_at = :updated_at
WHERE app_name=:app_name AND path=:path;`) WHERE app_name=:app_name AND path=:path;`)

238
api/models/annotations.go Normal file
View File

@@ -0,0 +1,238 @@
package models
import (
"bytes"
"database/sql/driver"
"encoding/json"
"errors"
"fmt"
"regexp"
)
// Annotations encapsulates key-value metadata associated with resource. The structure is immutable via its public API and nil-safe for its contract
// permissive nilability is here to simplify updates and reduce the need for nil handling in extensions - annotations should be updated by over-writing the original object:
// target.Annotations = target.Annotations.With("fooKey",1)
// old MD remains empty
// Annotations is lenable
type Annotations map[string]*annotationValue
// annotationValue encapsulates a value in the annotations map,
// This is stored in its compacted, un-parsed JSON format for later (re-) parsing into specific structs or values
// annotationValue objects are immutable after JSON load
type annotationValue []byte
const (
maxAnnotationValueBytes = 512
maxAnnotationKeyBytes = 128
maxAnnotationsKeys = 100
)
// Equals is defined based on un-ordered k/v comparison at of the annotation keys and (compacted) values of annotations, JSON object-value equality for values is property-order dependent
func (m Annotations) Equals(other Annotations) bool {
if len(m) != len(other) {
return false
}
for k1, v1 := range m {
v2, _ := other[k1]
if v2 == nil {
return false
}
if !bytes.Equal(*v1, *v2) {
return false
}
}
return true
}
func EmptyAnnotations() Annotations {
return nil
}
func (mv *annotationValue) String() string {
return string(*mv)
}
func (v *annotationValue) MarshalJSON() ([]byte, error) {
return *v, nil
}
func (mv *annotationValue) isEmptyValue() bool {
sval := string(*mv)
return sval == "\"\"" || sval == "null"
}
// UnmarshalJSON compacts annotation values but does not alter key-ordering for keys
func (mv *annotationValue) UnmarshalJSON(val []byte) error {
buf := bytes.Buffer{}
err := json.Compact(&buf, val)
if err != nil {
return err
}
if err != nil {
return err
}
*mv = buf.Bytes()
return nil
}
var validKeyRegex = regexp.MustCompile("^[!-~]+$")
func validateField(key string, value annotationValue) APIError {
if !validKeyRegex.Match([]byte(key)) {
return ErrInvalidAnnotationKey
}
keyLen := len([]byte(key))
if keyLen > maxAnnotationKeyBytes {
return ErrInvalidAnnotationKeyLength
}
if value.isEmptyValue() {
return ErrInvalidAnnotationValue
}
if len(value) > maxAnnotationValueBytes {
return ErrInvalidAnnotationValueLength
}
return nil
}
// With Creates a new annotations object containing the specified value - this does not perform size checks on the total number of keys
// this validates the correctness of the key and value. this returns a new the annotations object with the key set.
func (m Annotations) With(key string, data interface{}) (Annotations, error) {
if data == nil || data == "" {
return nil, errors.New("empty annotation value")
}
jsonBytes, err := json.Marshal(data)
if err != nil {
return nil, err
}
newVal := jsonBytes
err = validateField(key, newVal)
if err != nil {
return nil, err
}
var newMd Annotations
if m == nil {
newMd = make(Annotations, 1)
} else {
newMd = m.clone()
}
mv := annotationValue(newVal)
newMd[key] = &mv
return newMd, nil
}
// Validate validates a final annotations object prior to store,
// This will reject partial/patch changes with empty values (containing deletes)
func (m Annotations) Validate() APIError {
for k, v := range m {
err := validateField(k, *v)
if err != nil {
return err
}
}
if len(m) > maxAnnotationsKeys {
return ErrTooManyAnnotationKeys
}
return nil
}
// Get returns a raw JSON value of a annotation key
func (m Annotations) Get(key string) ([]byte, bool) {
if v, ok := m[key]; ok {
return *v, ok
}
return nil, false
}
// Without returns a new annotations object with a value excluded
func (m Annotations) Without(key string) Annotations {
nuVal := m.clone()
delete(nuVal, key)
return nuVal
}
// MergeChange merges a delta (possibly including deletes) with an existing annotations object and returns a new (copy) annotations object or an error.
// This assumes that both old and new annotations objects contain only valid keys and only newVs may contain deletes
func (m Annotations) MergeChange(newVs Annotations) Annotations {
newMd := m.clone()
for k, v := range newVs {
if v.isEmptyValue() {
delete(newMd, k)
} else {
if newMd == nil {
newMd = make(Annotations)
}
newMd[k] = v
}
}
if len(newMd) == 0 {
return EmptyAnnotations()
}
return newMd
}
// clone produces a key-wise copy of the underlying annotations
// publically MD can be copied by reference as it's (by contract) immutable
func (m Annotations) clone() Annotations {
if m == nil {
return nil
}
newMd := make(Annotations, len(m))
for ok, ov := range m {
newMd[ok] = ov
}
return newMd
}
// Value implements sql.Valuer, returning a string
func (m Annotations) Value() (driver.Value, error) {
if len(m) < 1 {
return driver.Value(string("")), nil
}
var b bytes.Buffer
err := json.NewEncoder(&b).Encode(m)
return driver.Value(b.String()), err
}
// Scan implements sql.Scanner
func (m *Annotations) Scan(value interface{}) error {
if value == nil || value == "" {
*m = nil
return nil
}
bv, err := driver.String.ConvertValue(value)
if err == nil {
var b []byte
switch x := bv.(type) {
case []byte:
b = x
case string:
b = []byte(x)
}
if len(b) > 0 {
return json.Unmarshal(b, m)
}
*m = nil
return nil
}
// otherwise, return an error
return fmt.Errorf("annotations invalid db format: %T %T value, err: %v", value, bv, err)
}

View File

@@ -0,0 +1,261 @@
package models
import (
"encoding/json"
"fmt"
"reflect"
"strings"
"testing"
)
type testObj struct {
Md Annotations `json:"annotations,omitempty"`
}
type myJson struct {
Foo string `json:"foo,omitempty"`
Bar string `json:"bar,omitempty"`
}
func (m Annotations) withRawKey(key string, val string) Annotations {
newMd := make(Annotations)
for k, v := range m {
newMd[k] = v
}
v := annotationValue([]byte(val))
newMd[key] = &v
return newMd
}
func mustParseMd(t *testing.T, md string) Annotations {
mdObj := make(Annotations)
err := json.Unmarshal([]byte(md), &mdObj)
if err != nil {
t.Fatalf("Failed to parse must-parse value %s %v", md, err)
}
return mdObj
}
func TestAnnotationsEqual(t *testing.T) {
annWithVal, _ := EmptyAnnotations().With("foo", "Bar")
tcs := []struct {
a Annotations
b Annotations
equals bool
}{
{EmptyAnnotations(), EmptyAnnotations(), true},
{annWithVal, EmptyAnnotations(), false},
{annWithVal, annWithVal, true},
{EmptyAnnotations().withRawKey("v1", `"a"`), EmptyAnnotations().withRawKey("v1", `"b"`), false},
{EmptyAnnotations().withRawKey("v1", `"a"`), EmptyAnnotations().withRawKey("v2", `"a"`), false},
{annWithVal.Without("foo"), EmptyAnnotations(), true},
{mustParseMd(t,
"{ \r\n\t"+`"md.1":{ `+"\r\n\t"+`
"subkey1": "value\n with\n newlines",
"subkey2": true
}
}`), mustParseMd(t, `{"md.1":{"subkey1":"value\n with\n newlines", "subkey2":true}}`), true},
}
for _, tc := range tcs {
if tc.a.Equals(tc.b) != tc.equals {
t.Errorf("Annotations equality mismatch - expecting (%v == %v) = %v", tc.b, tc.a, tc.equals)
}
if tc.b.Equals(tc.a) != tc.equals {
t.Errorf("Annotations reflexive equality mismatch - expecting (%v == %v) = %v", tc.b, tc.a, tc.equals)
}
}
}
var annCases = []struct {
val *testObj
valString string
}{
{val: &testObj{Md: EmptyAnnotations()}, valString: "{}"},
{val: &testObj{Md: EmptyAnnotations().withRawKey("stringval", `"bar"`)}, valString: `{"annotations":{"stringval":"bar"}}`},
{val: &testObj{Md: EmptyAnnotations().withRawKey("intval", `1001`)}, valString: `{"annotations":{"intval":1001}}`},
{val: &testObj{Md: EmptyAnnotations().withRawKey("floatval", "3.141")}, valString: `{"annotations":{"floatval":3.141}}`},
{val: &testObj{Md: EmptyAnnotations().withRawKey("objval", `{"foo":"fooval","bar":"barval"}`)}, valString: `{"annotations":{"objval":{"foo":"fooval","bar":"barval"}}}`},
{val: &testObj{Md: EmptyAnnotations().withRawKey("objval", `{"foo":"fooval","bar":{"barbar":"barbarval"}}`)}, valString: `{"annotations":{"objval":{"foo":"fooval","bar":{"barbar":"barbarval"}}}}`},
{val: &testObj{Md: EmptyAnnotations().withRawKey("objval", `{"foo":"JSON newline \\n string"}`)}, valString: `{"annotations":{"objval":{"foo":"JSON newline \\n string"}}}`},
}
func TestAnnotationsJSONMarshalling(t *testing.T) {
for _, tc := range annCases {
v, err := json.Marshal(tc.val)
if err != nil {
t.Fatalf("Failed to marshal json into %s: %v", tc.valString, err)
}
if string(v) != tc.valString {
t.Errorf("Invalid annotations value, expected %s, got %s", tc.valString, string(v))
}
}
}
func TestAnnotationsJSONUnMarshalling(t *testing.T) {
for _, tc := range annCases {
tv := testObj{}
err := json.Unmarshal([]byte(tc.valString), &tv)
if err != nil {
t.Fatalf("Failed to unmarshal json into %s: %v", tc.valString, err)
}
if !reflect.DeepEqual(&tv, tc.val) {
t.Errorf("Invalid annotations value, expected %v, got %v", tc.val, tv)
}
}
}
func TestAnnotationsWithHonorsKeyLimits(t *testing.T) {
var validKeys = []string{
"ok",
strings.Repeat("a", maxAnnotationKeyBytes),
"fnproject/internal/foo",
"foo.bar.com.baz",
"foo$bar!_+-()[]:@/<>$",
}
for _, k := range validKeys {
m, err := EmptyAnnotations().With(k, "value")
if err != nil {
t.Errorf("Should have accepted valid key %s,%v", k, err)
}
err = m.Validate()
if err != nil {
t.Errorf("Should have validate valid key %s,%v", k, err)
}
}
var invalidKeys = []struct {
key string
err APIError
}{
{"", ErrInvalidAnnotationKey},
{" ", ErrInvalidAnnotationKey},
{"\u00e9", ErrInvalidAnnotationKey},
{"foo bar", ErrInvalidAnnotationKey},
{strings.Repeat("a", maxAnnotationKeyBytes+1), ErrInvalidAnnotationKeyLength},
}
for _, kc := range invalidKeys {
_, err := EmptyAnnotations().With(kc.key, "value")
if err == nil {
t.Errorf("Should have rejected invalid key %s", kc.key)
}
m := EmptyAnnotations().withRawKey(kc.key, "\"data\"")
err = m.Validate()
if err != kc.err {
t.Errorf("Should have returned validation error %v, for key %s got %v", kc.err, kc.key, err)
}
}
}
func TestAnnotationsHonorsValueLimits(t *testing.T) {
validValues := []interface{}{
"ok",
&myJson{Foo: "foo"},
strings.Repeat("a", maxAnnotationValueBytes-2),
[]string{strings.Repeat("a", maxAnnotationValueBytes-4)},
1,
[]string{"a", "b", "c"},
true,
}
for _, v := range validValues {
_, err := EmptyAnnotations().With("key", v)
if err != nil {
t.Errorf("Should have accepted valid value %s,%v", v, err)
}
rawJson, err := json.Marshal(v)
if err != nil {
panic(err)
}
md := EmptyAnnotations().withRawKey("key", string(rawJson))
err = md.Validate()
if err != nil {
t.Errorf("Should have validated valid value successfully %s, got error %v", string(rawJson), err)
}
}
invalidValues := []struct {
val interface{}
err APIError
}{
{"", ErrInvalidAnnotationValue},
{nil, ErrInvalidAnnotationValue},
{strings.Repeat("a", maxAnnotationValueBytes-1), ErrInvalidAnnotationValueLength},
{[]string{strings.Repeat("a", maxAnnotationValueBytes-3)}, ErrInvalidAnnotationValueLength},
}
for _, v := range invalidValues {
_, err := EmptyAnnotations().With("key", v.val)
if err == nil {
t.Errorf("Should have rejected invalid value \"%v\"", v)
}
rawJson, err := json.Marshal(v.val)
if err != nil {
panic(err)
}
md := EmptyAnnotations().withRawKey("key", string(rawJson))
err = md.Validate()
if err != v.err {
t.Errorf("Expected validation error %v for '%s', got %v", v.err, string(rawJson), err)
}
}
}
func TestMergeAnnotations(t *testing.T) {
mdWithNKeys := func(n int) Annotations {
md := EmptyAnnotations()
for i := 0; i < n; i++ {
md = md.withRawKey(fmt.Sprintf("key-%d", i), "val")
}
return md
}
validCases := []struct {
first Annotations
second Annotations
result Annotations
}{
{first: EmptyAnnotations(), second: EmptyAnnotations(), result: EmptyAnnotations()},
{first: EmptyAnnotations().withRawKey("key1", "\"val\""), second: EmptyAnnotations(), result: EmptyAnnotations().withRawKey("key1", "\"val\"")},
{first: EmptyAnnotations(), second: EmptyAnnotations().withRawKey("key1", "\"val\""), result: EmptyAnnotations().withRawKey("key1", "\"val\"")},
{first: EmptyAnnotations().withRawKey("key1", "\"val\""), second: EmptyAnnotations().withRawKey("key1", "\"val\""), result: EmptyAnnotations().withRawKey("key1", "\"val\"")},
{first: EmptyAnnotations().withRawKey("key1", "\"val1\""), second: EmptyAnnotations().withRawKey("key2", "\"val2\""), result: EmptyAnnotations().withRawKey("key1", "\"val1\"").withRawKey("key2", "\"val2\"")},
{first: EmptyAnnotations().withRawKey("key1", "\"val1\""), second: EmptyAnnotations().withRawKey("key1", "\"\""), result: EmptyAnnotations()},
{first: EmptyAnnotations().withRawKey("key1", "\"val1\""), second: EmptyAnnotations().withRawKey("key2", "\"\""), result: EmptyAnnotations().withRawKey("key1", "\"val1\"")},
{first: mdWithNKeys(maxAnnotationsKeys - 1), second: EmptyAnnotations().withRawKey("newkey", "\"val\""), result: mdWithNKeys(maxAnnotationsKeys-1).withRawKey("newkey", "\"val\"")},
}
for _, v := range validCases {
newMd := v.first.MergeChange(v.second)
if !reflect.DeepEqual(newMd, v.result) {
t.Errorf("Change %v + %v : expected %v got %v", v.first, v.second, v.result, newMd)
}
}
}

View File

@@ -10,6 +10,7 @@ import (
type App struct { type App struct {
Name string `json:"name" db:"name"` Name string `json:"name" db:"name"`
Config Config `json:"config,omitempty" db:"config"` Config Config `json:"config,omitempty" db:"config"`
Annotations Annotations `json:"annotations,omitempty" db:"annotations"`
CreatedAt strfmt.DateTime `json:"created_at,omitempty" db:"created_at"` CreatedAt strfmt.DateTime `json:"created_at,omitempty" db:"created_at"`
UpdatedAt strfmt.DateTime `json:"updated_at,omitempty" db:"updated_at"` UpdatedAt strfmt.DateTime `json:"updated_at,omitempty" db:"updated_at"`
} }
@@ -39,6 +40,10 @@ func (a *App) Validate() error {
return ErrAppsInvalidName return ErrAppsInvalidName
} }
} }
err := a.Annotations.Validate()
if err != nil {
return err
}
return nil return nil
} }
@@ -53,6 +58,7 @@ func (a *App) Clone() *App {
clone.Config[k] = v clone.Config[k] = v
} }
} }
return clone return clone
} }
@@ -63,6 +69,7 @@ func (a1 *App) Equals(a2 *App) bool {
eq := true eq := true
eq = eq && a1.Name == a2.Name eq = eq && a1.Name == a2.Name
eq = eq && a1.Config.Equals(a2.Config) eq = eq && a1.Config.Equals(a2.Config)
eq = eq && a1.Annotations.Equals(a2.Annotations)
// NOTE: datastore tests are not very fun to write with timestamp checks, // NOTE: datastore tests are not very fun to write with timestamp checks,
// and these are not values the user may set so we kind of don't care. // and these are not values the user may set so we kind of don't care.
//eq = eq && time.Time(a1.CreatedAt).Equal(time.Time(a2.CreatedAt)) //eq = eq && time.Time(a1.CreatedAt).Equal(time.Time(a2.CreatedAt))
@@ -70,15 +77,15 @@ func (a1 *App) Equals(a2 *App) bool {
return eq return eq
} }
// Update adds entries from patch to a.Config, and removes entries with empty values. // Update adds entries from patch to a.Config and a.Annotations, and removes entries with empty values.
func (a *App) Update(src *App) { func (a *App) Update(patch *App) {
original := a.Clone() original := a.Clone()
if src.Config != nil { if patch.Config != nil {
if a.Config == nil { if a.Config == nil {
a.Config = make(Config) a.Config = make(Config)
} }
for k, v := range src.Config { for k, v := range patch.Config {
if v == "" { if v == "" {
delete(a.Config, k) delete(a.Config, k)
} else { } else {
@@ -87,6 +94,8 @@ func (a *App) Update(src *App) {
} }
} }
a.Annotations = a.Annotations.MergeChange(patch.Annotations)
if !a.Equals(original) { if !a.Equals(original) {
a.UpdatedAt = strfmt.DateTime(time.Now()) a.UpdatedAt = strfmt.DateTime(time.Now())
} }

View File

@@ -185,6 +185,26 @@ var (
code: http.StatusBadGateway, code: http.StatusBadGateway,
error: fmt.Errorf("function response too large"), error: fmt.Errorf("function response too large"),
} }
ErrInvalidAnnotationKey = err{
code: http.StatusBadRequest,
error: errors.New("Invalid annotation key, annotation keys must be non-empty ascii strings excluding whitespace"),
}
ErrInvalidAnnotationKeyLength = err{
code: http.StatusBadRequest,
error: fmt.Errorf("Invalid annotation key length, annotation keys may not be larger than %d bytes", maxAnnotationKeyBytes),
}
ErrInvalidAnnotationValue = err{
code: http.StatusBadRequest,
error: errors.New("Invalid annotation value, annotation values may only be non-empty strings, numbers, objects, or arrays"),
}
ErrInvalidAnnotationValueLength = err{
code: http.StatusBadRequest,
error: fmt.Errorf("Invalid annotation value length, annotation values may not be larger than %d bytes when serialized as JSON", maxAnnotationValueBytes),
}
ErrTooManyAnnotationKeys = err{
code: http.StatusBadRequest,
error: fmt.Errorf("Invalid annotation change, new key(s) exceed maximum permitted number of annotations keys (%d)", maxAnnotationsKeys),
}
) )
// APIError any error that implements this interface will return an API response // APIError any error that implements this interface will return an API response

View File

@@ -36,6 +36,7 @@ type Route struct {
Timeout int32 `json:"timeout" db:"timeout"` Timeout int32 `json:"timeout" db:"timeout"`
IdleTimeout int32 `json:"idle_timeout" db:"idle_timeout"` IdleTimeout int32 `json:"idle_timeout" db:"idle_timeout"`
Config Config `json:"config,omitempty" db:"config"` Config Config `json:"config,omitempty" db:"config"`
Annotations Annotations `json:"annotations,omitempty" db:"annotations"`
CreatedAt strfmt.DateTime `json:"created_at,omitempty" db:"created_at"` CreatedAt strfmt.DateTime `json:"created_at,omitempty" db:"created_at"`
UpdatedAt strfmt.DateTime `json:"updated_at,omitempty" db:"updated_at"` UpdatedAt strfmt.DateTime `json:"updated_at,omitempty" db:"updated_at"`
} }
@@ -129,6 +130,11 @@ func (r *Route) Validate() error {
return ErrRoutesInvalidMemory return ErrRoutesInvalidMemory
} }
err = r.Annotations.Validate()
if err != nil {
return err
}
return nil return nil
} }
@@ -169,6 +175,7 @@ func (r1 *Route) Equals(r2 *Route) bool {
eq = eq && r1.Timeout == r2.Timeout eq = eq && r1.Timeout == r2.Timeout
eq = eq && r1.IdleTimeout == r2.IdleTimeout eq = eq && r1.IdleTimeout == r2.IdleTimeout
eq = eq && r1.Config.Equals(r2.Config) eq = eq && r1.Config.Equals(r2.Config)
eq = eq && r1.Annotations.Equals(r2.Annotations)
// NOTE: datastore tests are not very fun to write with timestamp checks, // NOTE: datastore tests are not very fun to write with timestamp checks,
// and these are not values the user may set so we kind of don't care. // and these are not values the user may set so we kind of don't care.
//eq = eq && time.Time(r1.CreatedAt).Equal(time.Time(r2.CreatedAt)) //eq = eq && time.Time(r1.CreatedAt).Equal(time.Time(r2.CreatedAt))
@@ -179,35 +186,35 @@ func (r1 *Route) Equals(r2 *Route) bool {
// Update updates fields in r with non-zero field values from new, and sets // Update updates fields in r with non-zero field values from new, and sets
// updated_at if any of the fields change. 0-length slice Header values, and // updated_at if any of the fields change. 0-length slice Header values, and
// empty-string Config values trigger removal of map entry. // empty-string Config values trigger removal of map entry.
func (r *Route) Update(new *Route) { func (r *Route) Update(patch *Route) {
original := r.Clone() original := r.Clone()
if new.Image != "" { if patch.Image != "" {
r.Image = new.Image r.Image = patch.Image
} }
if new.Memory != 0 { if patch.Memory != 0 {
r.Memory = new.Memory r.Memory = patch.Memory
} }
if new.CPUs != 0 { if patch.CPUs != 0 {
r.CPUs = new.CPUs r.CPUs = patch.CPUs
} }
if new.Type != "" { if patch.Type != "" {
r.Type = new.Type r.Type = patch.Type
} }
if new.Timeout != 0 { if patch.Timeout != 0 {
r.Timeout = new.Timeout r.Timeout = patch.Timeout
} }
if new.IdleTimeout != 0 { if patch.IdleTimeout != 0 {
r.IdleTimeout = new.IdleTimeout r.IdleTimeout = patch.IdleTimeout
} }
if new.Format != "" { if patch.Format != "" {
r.Format = new.Format r.Format = patch.Format
} }
if new.Headers != nil { if patch.Headers != nil {
if r.Headers == nil { if r.Headers == nil {
r.Headers = Headers(make(http.Header)) r.Headers = Headers(make(http.Header))
} }
for k, v := range new.Headers { for k, v := range patch.Headers {
if len(v) == 0 { if len(v) == 0 {
http.Header(r.Headers).Del(k) http.Header(r.Headers).Del(k)
} else { } else {
@@ -215,11 +222,11 @@ func (r *Route) Update(new *Route) {
} }
} }
} }
if new.Config != nil { if patch.Config != nil {
if r.Config == nil { if r.Config == nil {
r.Config = make(Config) r.Config = make(Config)
} }
for k, v := range new.Config { for k, v := range patch.Config {
if v == "" { if v == "" {
delete(r.Config, k) delete(r.Config, k)
} else { } else {
@@ -228,6 +235,8 @@ func (r *Route) Update(new *Route) {
} }
} }
r.Annotations = r.Annotations.MergeChange(patch.Annotations)
if !r.Equals(original) { if !r.Equals(original) {
r.UpdatedAt = strfmt.DateTime(time.Now()) r.UpdatedAt = strfmt.DateTime(time.Now())
} }

View File

@@ -52,9 +52,11 @@ func TestAppCreate(t *testing.T) {
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "1234567890123456789012345678901" } }`, http.StatusBadRequest, models.ErrAppsTooLongName}, {datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "1234567890123456789012345678901" } }`, http.StatusBadRequest, models.ErrAppsTooLongName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsInvalidName}, {datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsInvalidName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsInvalidName}, {datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "&&%@!#$#@$" } }`, http.StatusBadRequest, models.ErrAppsInvalidName},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "app", "annotations" : { "":"val" }}}`, http.StatusBadRequest, models.ErrInvalidAnnotationKey},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "app", "annotations" : { "key":"" }}}`, http.StatusBadRequest, models.ErrInvalidAnnotationValue},
// success // success
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "teste" } }`, http.StatusOK, nil}, {datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "teste" } }`, http.StatusOK, nil},
{datastore.NewMock(), logs.NewMock(), "/v1/apps", `{ "app": { "name": "teste" , "annotations": {"k1":"v1", "k2":[]}}}`, http.StatusOK, nil},
} { } {
rnr, cancel := testRunner(t) rnr, cancel := testRunner(t)
srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull) srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
@@ -72,8 +74,8 @@ func TestAppCreate(t *testing.T) {
resp := getErrorResponse(t, rec) resp := getErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`", t.Errorf("Test %d: Expected error message to have `%s` but got `%s`",
i, test.expectedError.Error()) i, test.expectedError.Error(), resp.Error.Message)
} }
} }
@@ -279,6 +281,20 @@ func TestAppUpdate(t *testing.T) {
// errors // errors
{datastore.NewMock(), logs.NewMock(), "/v1/apps/myapp", ``, http.StatusBadRequest, models.ErrInvalidJSON}, {datastore.NewMock(), logs.NewMock(), "/v1/apps/myapp", ``, http.StatusBadRequest, models.ErrInvalidJSON},
// Addresses #380
{datastore.NewMockInit(
[]*models.App{{
Name: "myapp",
}}, nil, nil,
), logs.NewMock(), "/v1/apps/myapp", `{ "app": { "name": "othername" } }`, http.StatusConflict, nil},
// success: add/set MD key
{datastore.NewMockInit(
[]*models.App{{
Name: "myapp",
}}, nil, nil,
), logs.NewMock(), "/v1/apps/myapp", `{ "app": { "annotations": {"k-0" : "val"} } }`, http.StatusOK, nil},
// success // success
{datastore.NewMockInit( {datastore.NewMockInit(
[]*models.App{{ []*models.App{{
@@ -286,12 +302,12 @@ func TestAppUpdate(t *testing.T) {
}}, nil, nil, }}, nil, nil,
), logs.NewMock(), "/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil}, ), logs.NewMock(), "/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil},
// Addresses #380 // success
{datastore.NewMockInit( {datastore.NewMockInit(
[]*models.App{{ []*models.App{{
Name: "myapp", Name: "myapp",
}}, nil, nil, }}, nil, nil,
), logs.NewMock(), "/v1/apps/myapp", `{ "app": { "name": "othername" } }`, http.StatusConflict, nil}, ), logs.NewMock(), "/v1/apps/myapp", `{ "app": { "config": { "test": "1" } } }`, http.StatusOK, nil},
} { } {
rnr, cancel := testRunner(t) rnr, cancel := testRunner(t)
srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull) srv := testServer(test.mock, &mqs.Mock{}, test.logDB, rnr, ServerTypeFull)
@@ -308,8 +324,8 @@ func TestAppUpdate(t *testing.T) {
resp := getErrorResponse(t, rec) resp := getErrorResponse(t, rec)
if !strings.Contains(resp.Error.Message, test.expectedError.Error()) { if !strings.Contains(resp.Error.Message, test.expectedError.Error()) {
t.Errorf("Test %d: Expected error message to have `%s`", t.Errorf("Test %d: Expected error message to have `%s` but was `%s`",
i, test.expectedError.Error()) i, test.expectedError.Error(), resp.Error.Message)
} }
} }

View File

@@ -57,3 +57,4 @@ If you are operating Fn, this section is for you.
If you are working on the Fn Project, want to work on it or are creating extensions, this section is for you. If you are working on the Fn Project, want to work on it or are creating extensions, this section is for you.
* [Writing Extensions](contributors/extensions.md) * [Writing Extensions](contributors/extensions.md)
* [Using Annotations](contributors/annotations.md)

View File

@@ -0,0 +1,151 @@
# App and Route Annotations
App and Route annotations allow users building on the Fn platform to encode consumer-specific data inline with Apps and Routes.
Annotations can be used to either communicate and carry information externally (only by API users) or to communicate between external applications and Fn extensions.
## Use cases/Examples:
### Externally defined/consumed annotations
Software using Fn as a service, attaches non-identifying metadata annotatinos to Fn resources for subsequent reads (e.g. my reference for this function/app )
Writer : API user, Reader: API user
E.g. platform "platX" creates/modifies functions, has an internal reference it wants to associate with an object for later retrieval (it can't query/search Fn by this attribute)
```
POST /v1/apps
{
...
"annotations" : {
"platx.com/ref" : "adsfasdfads"
}
...
}
```
### Extensions: Allow passing data from user or upstream input to extensions (configuration of extensions)
Writer : API user, Reader: Fn platform extension (use), API user (informational)
```
POST /v1/apps
...
{
...
"annotations" : {
"my_cloud_provider.com/network_id" : "network.id"
}
...
}
```
### Extensions: Allow indicating internally derived/set values to user (API extension sets/generates annotations, prevents user from changing it)
Writer : Internal platform extension, Reader: API user.
```
GET /v1/apps/myapp
...
{
...
"annotations" : {
"my_cloud_provider.com/create_user" : "foo@foo.com"
}
...
}
```
```
PATCH /v1/apps/myapp
...
{
...
"annotations" : {
"my_cloud_provider.com/create_user" : "foo@foo.com"
}
...
}
HTTP/1.1 400 Invalid operation
{
"error": "annotation key cannot be changed",
}
```
## Content Examples
example : user attaches local annotations
```json
PUT /v1/apps/foo
{
app: {
...
"annotations": {
"mylabel": "super-cool-fn",
"myMetaData": {
"k1": "foo",
"number": 5000
"array" : [1,2,3]
}
}
...
}
}
```
User sets extension-specific annotations:
```
PUT /v1/apps/foo
{
...
"annotations": {
"example.extension.com/v1/myval" : "val"
}
...
}
```
## Key Syntax and Namespaces
A key consists of any printable (non-extended) ascii characters excluding whitespace characters.
The maximum (byte) size of a key is 128 bytes (excluding quotes).
Keys are stored as free text with the object. Normatively extensions and systems using annotations *must* use a namespace prefix based on an identified domain and followed by at least one '/' character.
Systems *should* use independent annotation keys for any value that can be changed independently.
Extensions *should not* interact with annotations keys that are not prefixed with a domain they own.
## Value syntax
Values may contain any valid JSON value (object/array/string/number) except the empty string `""` and `null`
The serialised JSON representation (rendered without excess whitespace as a string) of a single value must not exceed a 512 bytes.
## Modifying and deleting annotation keys
A key can be modified by a PATCH operation containing a partial `annotations` object indicating the keys to update (or delete)
A key can be deleted by a PATCH operation by setting its value to an empty string.
For each element that of data that can be changed independently, you *should* use a new top-level annotation key.
## Maximum number of keys
A user may not add keys in a PATCH or PUT operation if the total number of keys after changes exceeds 100 keys.
Fn may return a larger number of keys.
## Extension interaction with resource modification
An extension may prevent a PUT,PATCH or POST operation on a domain object based on the value of an annotation passed in by a user, in this case this should result in an HTTP 400 error with an informational message indicating that an error was present in the annotations and containing the exact key or keys which caused the error.

View File

@@ -519,6 +519,11 @@ definitions:
default: 30 default: 30
format: int32 format: int32
description: Hot functions idle timeout before termination. Value in Seconds description: Hot functions idle timeout before termination. Value in Seconds
annotations:
type: object
description: Route annotations - this is a map of annotations attached to this route, keys must not exceed 128 bytes and must consist of non-whitespace printable ascii characters, and the seralized representation of individual values must not exeed 512 bytes
additionalProperties:
type: object
created_at: created_at:
type: string type: string
format: date-time format: date-time
@@ -539,9 +544,14 @@ definitions:
readOnly: true readOnly: true
config: config:
type: object type: object
description: Application configuration, applied to all routes. description: Application function configuration, applied to all routes.
additionalProperties: additionalProperties:
type: string type: string
annotations:
type: object
description: Application annotations - this is a map of annotations attached to this app, keys must not exceed 128 bytes and must consist of non-whitespace printable ascii characters, and the seralized representation of individual values must not exeed 512 bytes
additionalProperties:
type: object
created_at: created_at:
type: string type: string
format: date-time format: date-time

View File

@@ -2,12 +2,15 @@ FN integration API tests
====================================== ======================================
These are tests that can either run locally against the current codebase (e.g. in an IDE) or remotely against a running Fn instance.
Test dependencies Test dependencies
----------------- -----------------
```bash ```bash
DOCKER_HOST - for building images DOCKER_HOST - for building images
FN_API_URL - Fn API endpoint FN_API_URL - Fn API endpoint - leave this unset to test using the local codebase
``` ```
How to run tests? How to run tests?

View File

@@ -0,0 +1,84 @@
package tests
import (
"fmt"
"reflect"
"strings"
)
// common test cases around annotations (shared by any objects that support it)
const (
maxAnnotationKeys = 100
maxAnnotationValueSize = 512
maxAnnotationKeySize = 128
)
var emptyAnnMap = map[string]interface{}{}
func makeAnnMap(size int) map[string]interface{} {
md := make(map[string]interface{}, size)
for i := 0; i < size; i++ {
md[fmt.Sprintf("k-%d", i)] = "val"
}
return md
}
var createAnnotationsValidCases = []struct {
name string
annotations map[string]interface{}
}{
{"valid_string", map[string]interface{}{"key": "value"}},
{"valid_array", map[string]interface{}{"key": []interface{}{"value1", "value2"}}},
{"valid_object", map[string]interface{}{"key": map[string]interface{}{"foo": "bar"}}},
{"max_value_size", map[string]interface{}{"key": strings.Repeat("a", maxAnnotationValueSize-2)}},
{"max_key_size", map[string]interface{}{strings.Repeat("a", maxAnnotationKeySize): "val"}},
{"max_map_size", makeAnnMap(maxAnnotationKeys)},
}
var createAnnotationsErrorCases = []struct {
name string
annotations map[string]interface{}
}{
{"value_too_long", map[string]interface{}{"key": strings.Repeat("a", maxAnnotationValueSize-1)}},
{"key_too_long", map[string]interface{}{strings.Repeat("a", maxAnnotationKeySize+1): "value"}},
{"whitespace_in_key", map[string]interface{}{" bad key ": "value"}},
{"too_many_keys", makeAnnMap(maxAnnotationKeys + 1)},
}
var updateAnnotationsValidCases = []struct {
name string
initial map[string]interface{}
change map[string]interface{}
expected map[string]interface{}
}{
{"overwrite_existing_annotation_keys", map[string]interface{}{"key": "value1"}, map[string]interface{}{"key": "value2"}, map[string]interface{}{"key": "value2"}},
{"delete_annotation_key", map[string]interface{}{"key": "value1"}, map[string]interface{}{"key": ""}, map[string]interface{}{}},
{"set_to_max_size_with_deletes", map[string]interface{}{"key": "value1"}, func() map[string]interface{} {
md := makeAnnMap(100)
md["key"] = ""
return md
}(), makeAnnMap(100)},
{"noop_with_max_keys", makeAnnMap(maxAnnotationKeys), emptyAnnMap, makeAnnMap(maxAnnotationKeys)},
}
var updateAnnotationsErrorCases = []struct {
name string
initial map[string]interface{}
change map[string]interface{}
}{
{"too_many_key_after_update", makeAnnMap(100), map[string]interface{}{"key": "value1"}},
{"value_too_long", map[string]interface{}{}, map[string]interface{}{"key": strings.Repeat("a", maxAnnotationValueSize-1)}},
{"key_too_long", map[string]interface{}{}, map[string]interface{}{strings.Repeat("a", maxAnnotationKeySize+1): "value"}},
{"whitespace_in_key", map[string]interface{}{}, map[string]interface{}{" bad key ": "value"}},
{"too_many_keys_in_update", map[string]interface{}{}, makeAnnMap(maxAnnotationKeys + 1)},
}
//AnnotationsEquivalent checks if two annotations maps are semantically equivalent, including nil == empty map
func AnnotationsEquivalent(md1, md2 map[string]interface{}) bool {
if len(md1) == 0 && len(md2) == 0 {
return true
}
return reflect.DeepEqual(md1, md2)
}

View File

@@ -2,6 +2,7 @@ package tests
import ( import (
"context" "context"
"log"
"strings" "strings"
"testing" "testing"
"time" "time"
@@ -11,117 +12,57 @@ import (
"github.com/fnproject/fn_go/models" "github.com/fnproject/fn_go/models"
) )
func CheckAppResponseError(t *testing.T, e error) { // PostApp creates an app and esures it is deleted on teardown if it was created
if e != nil { func (s *TestHarness) PostApp(app *models.App) (*apps.PostAppsOK, error) {
switch err := e.(type) {
case *apps.DeleteAppsAppNotFound:
t.Errorf("Unexpected error occurred: %v Original Location: %s", err.Payload.Error.Message, MyCaller())
t.FailNow()
case *apps.DeleteAppsAppDefault:
t.Errorf("Unexpected error occurred: %v. Status code: %v Orig Location: %s", err.Payload.Error.Message, err.Code(), MyCaller())
t.FailNow()
case *apps.PostAppsDefault:
t.Errorf("Unexpected error occurred: %v. Status code: %v Orig Location: %s", err.Payload.Error.Message, err.Code(), MyCaller())
t.FailNow()
case *apps.GetAppsAppNotFound:
if !strings.Contains("App not found", err.Payload.Error.Message) {
t.Errorf("Unexpected error occurred: %v Original Location: %s", err.Payload.Error.Message, MyCaller())
t.FailNow()
}
case *apps.GetAppsAppDefault:
t.Errorf("Unexpected error occurred: %v. Status code: %v Orig Location: %s", err.Payload.Error.Message, err.Code(), MyCaller())
t.FailNow()
case *apps.PatchAppsAppDefault:
t.Errorf("Unexpected error occurred: %v. Status code: %v Orig Location: %s", err.Payload.Error.Message, err.Code(), MyCaller())
t.FailNow()
case *apps.PatchAppsAppNotFound:
t.Errorf("Unexpected error occurred: %v. Original Location: %s", err.Payload.Error.Message, MyCaller())
t.FailNow()
case *apps.PatchAppsAppBadRequest:
t.Errorf("Unexpected error occurred: %v. Original Location: %s", err.Payload.Error.Message, MyCaller())
t.FailNow()
default:
t.Errorf("Unable to determine type of error: %s Original Location: %s", err, MyCaller())
t.FailNow()
}
}
}
func CreateAppNoAssert(ctx context.Context, fnclient *client.Fn, appName string, config map[string]string) (*apps.PostAppsOK, error) {
cfg := &apps.PostAppsParams{ cfg := &apps.PostAppsParams{
Body: &models.AppWrapper{ Body: &models.AppWrapper{
App: &models.App{ App: app,
Config: config,
Name: appName,
}, },
}, Context: s.Context,
Context: ctx,
} }
ok, err := fnclient.Apps.PostApps(cfg) ok, err := s.Client.Apps.PostApps(cfg)
if err == nil { if err == nil {
approutesLock.Lock() s.createdApps[ok.Payload.App.Name] = true
_, got := appsandroutes[appName]
if !got {
appsandroutes[appName] = []string{}
}
approutesLock.Unlock()
} }
return ok, err return ok, err
} }
func CreateApp(t *testing.T, ctx context.Context, fnclient *client.Fn, appName string, config map[string]string) { // GivenAppExists creates an app and ensures it is deleted on teardown, this fatals if the app is not created
appPayload, err := CreateAppNoAssert(ctx, fnclient, appName, config) func (s *TestHarness) GivenAppExists(t *testing.T, app *models.App) {
CheckAppResponseError(t, err)
if !strings.Contains(appName, appPayload.Payload.App.Name) { appPayload, err := s.PostApp(app)
t.Errorf("App name mismatch.\nExpected: %v\nActual: %v", if err != nil {
appName, appPayload.Payload.App.Name) t.Fatalf("Failed to create app %v", app)
}
if !strings.Contains(app.Name, appPayload.Payload.App.Name) {
t.Fatalf("App name mismatch.\nExpected: %v\nActual: %v",
app.Name, appPayload.Payload.App.Name)
} }
} }
func CreateUpdateApp(t *testing.T, ctx context.Context, fnclient *client.Fn, appName string, config map[string]string) *apps.PatchAppsAppOK { // AppMustExist fails the test if the specified app does not exist
CreateApp(t, ctx, fnclient, appName, map[string]string{"A": "a"}) func (s *TestHarness) AppMustExist(t *testing.T, appName string) *models.App {
cfg := &apps.PatchAppsAppParams{ app, err := s.Client.Apps.GetAppsApp(&apps.GetAppsAppParams{
App: appName, App: s.AppName,
Body: &models.AppWrapper{ Context: s.Context,
App: &models.App{ })
Config: config, if err != nil {
Name: "", t.Fatalf("Expected new route to create app got %v", err)
}, return nil
},
Context: ctx,
} }
appPayload, err := fnclient.Apps.PatchAppsApp(cfg)
CheckAppResponseError(t, err)
return appPayload
}
func DeleteApp(t *testing.T, ctx context.Context, fnclient *client.Fn, appName string) {
cfg := &apps.DeleteAppsAppParams{
App: appName,
Context: ctx,
}
_, err := fnclient.Apps.DeleteAppsApp(cfg)
CheckAppResponseError(t, err)
}
func GetApp(t *testing.T, ctx context.Context, fnclient *client.Fn, appName string) *models.App {
cfg := &apps.GetAppsAppParams{
App: appName,
Context: ctx,
}
app, err := fnclient.Apps.GetAppsApp(cfg)
CheckAppResponseError(t, err)
return app.Payload.App return app.Payload.App
} }
func DeleteAppNoT(ctx context.Context, fnclient *client.Fn, appName string) { func safeDeleteApp(ctx context.Context, fnclient *client.Fn, appName string) {
cfg := &apps.DeleteAppsAppParams{ cfg := &apps.DeleteAppsAppParams{
App: appName, App: appName,
Context: ctx, Context: ctx,
} }
cfg.WithTimeout(time.Second * 60) cfg.WithTimeout(time.Second * 60)
fnclient.Apps.DeleteAppsApp(cfg) _, err := fnclient.Apps.DeleteAppsApp(cfg)
if _, ok := err.(*apps.DeleteAppsAppNotFound); err != nil && !ok {
log.Printf("Error cleaning up app %s: %v", appName, err)
}
} }

View File

@@ -1,126 +1,300 @@
package tests package tests
import ( import (
"github.com/fnproject/fn_go/client/apps"
"github.com/fnproject/fn_go/models"
"reflect" "reflect"
"strings" "strings"
"testing" "testing"
"time"
"github.com/fnproject/fn_go/client/apps"
) )
func TestAppDeleteNotFound(t *testing.T) { func TestAppDeleteNotFound(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
defer s.Cleanup()
cfg := &apps.DeleteAppsAppParams{ cfg := &apps.DeleteAppsAppParams{
App: "missing-app", App: "missing-app",
Context: s.Context, Context: s.Context,
} }
cfg.WithTimeout(time.Second * 60)
_, err := s.Client.Apps.DeleteAppsApp(cfg) _, err := s.Client.Apps.DeleteAppsApp(cfg)
if err == nil {
if _, ok := err.(*apps.DeleteAppsAppNotFound); !ok {
t.Errorf("Error during app delete: we should get HTTP 404, but got: %s", err.Error()) t.Errorf("Error during app delete: we should get HTTP 404, but got: %s", err.Error())
} }
} }
func TestAppGetNotFound(t *testing.T) { func TestAppGetNotFound(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
defer s.Cleanup()
cfg := &apps.GetAppsAppParams{ cfg := &apps.GetAppsAppParams{
App: "missing-app", App: "missing-app",
Context: s.Context, Context: s.Context,
} }
cfg.WithTimeout(time.Second * 60)
_, err := s.Client.Apps.GetAppsApp(cfg) _, err := s.Client.Apps.GetAppsApp(cfg)
CheckAppResponseError(t, err)
if _, ok := err.(*apps.GetAppsAppNotFound); !ok {
t.Errorf("Error during get: we should get HTTP 404, but got: %s", err.Error())
}
if !strings.Contains(err.(*apps.GetAppsAppNotFound).Payload.Error.Message, "App not found") {
t.Errorf("Error during app delete: unexpeted error `%s`, wanted `App not found`", err.Error())
}
} }
func TestAppCreateNoConfigSuccess(t *testing.T) { func TestAppCreateNoConfigSuccess(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
DeleteApp(t, s.Context, s.Client, s.AppName)
resp, err := s.PostApp(&models.App{
Name: s.AppName,
})
if err != nil {
t.Errorf("Failed to create simple app %v", err)
return
}
if resp.Payload.App.Name != s.AppName {
t.Errorf("app name in response %s does not match new app %s ", resp.Payload.App.Name, s.AppName)
}
}
func TestSetAppAnnotationsOnCreate(t *testing.T) {
t.Parallel()
for _, tci := range createAnnotationsValidCases {
// iterator mutation meets parallelism... pfft
tc := tci
t.Run("valid_"+tc.name, func(t *testing.T) {
t.Parallel()
s := SetupHarness()
defer s.Cleanup()
app, err := s.PostApp(&models.App{
Name: s.AppName,
Annotations: tc.annotations,
})
if err != nil {
t.Fatalf("Failed to create app with valid annotations %v got error %v", tc.annotations, err)
}
gotMd := app.Payload.App.Annotations
if !AnnotationsEquivalent(gotMd, tc.annotations) {
t.Errorf("Returned annotations %v does not match set annotations %v", gotMd, tc.annotations)
}
getApp := s.AppMustExist(t, s.AppName)
if !AnnotationsEquivalent(getApp.Annotations, tc.annotations) {
t.Errorf("GET annotations '%v' does not match set annotations %v", getApp.Annotations, tc.annotations)
}
})
}
for _, tci := range createAnnotationsErrorCases {
// iterator mutation meets parallelism... pfft
tc := tci
t.Run("invalid_"+tc.name, func(ti *testing.T) {
ti.Parallel()
s := SetupHarness()
defer s.Cleanup()
_, err := s.PostApp(&models.App{
Name: s.AppName,
Annotations: tc.annotations,
})
if err == nil {
t.Fatalf("Created app with invalid annotations %v but expected error", tc.annotations)
}
if _, ok := err.(*apps.PostAppsBadRequest); !ok {
t.Errorf("Expecting bad request for invalid annotations, got %v", err)
}
})
}
}
func TestUpdateAppAnnotationsOnPatch(t *testing.T) {
t.Parallel()
for _, tci := range updateAnnotationsValidCases {
// iterator mutation meets parallelism... pfft
tc := tci
t.Run("valid_"+tc.name, func(t *testing.T) {
t.Parallel()
s := SetupHarness()
defer s.Cleanup()
s.GivenAppExists(t, &models.App{
Name: s.AppName,
Annotations: tc.initial,
})
res, err := s.Client.Apps.PatchAppsApp(&apps.PatchAppsAppParams{
App: s.AppName,
Context: s.Context,
Body: &models.AppWrapper{
App: &models.App{
Annotations: tc.change,
},
},
})
if err != nil {
t.Fatalf("Failed to patch annotations with %v on app: %v", tc.change, err)
}
gotMd := res.Payload.App.Annotations
if !AnnotationsEquivalent(gotMd, tc.expected) {
t.Errorf("Returned annotations %v does not match set annotations %v", gotMd, tc.expected)
}
getApp := s.AppMustExist(t, s.AppName)
if !AnnotationsEquivalent(getApp.Annotations, tc.expected) {
t.Errorf("GET annotations '%v' does not match set annotations %v", getApp.Annotations, tc.expected)
}
})
}
for _, tci := range updateAnnotationsErrorCases {
// iterator mutation meets parallelism... pfft
tc := tci
t.Run("invalid_"+tc.name, func(t *testing.T) {
t.Parallel()
s := SetupHarness()
defer s.Cleanup()
s.GivenAppExists(t, &models.App{
Name: s.AppName,
Annotations: tc.initial,
})
_, err := s.Client.Apps.PatchAppsApp(&apps.PatchAppsAppParams{
App: s.AppName,
Context: s.Context,
Body: &models.AppWrapper{
App: &models.App{
Annotations: tc.change,
},
},
})
if err == nil {
t.Errorf("patched app with invalid annotations %v but expected error", tc.change)
}
if _, ok := err.(*apps.PatchAppsAppBadRequest); !ok {
t.Errorf("Expecting bad request for invalid annotations, got %v", err)
}
})
}
} }
func TestAppCreateWithConfigSuccess(t *testing.T) { func TestAppCreateWithConfigSuccess(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{"A": "a"}) defer s.Cleanup()
DeleteApp(t, s.Context, s.Client, s.AppName)
validConfig := map[string]string{"A": "a"}
appPayload, err := s.PostApp(&models.App{
Name: s.AppName,
Config: validConfig,
})
if err != nil {
t.Fatalf("Failed to create app with valid config got %v", err)
}
if !reflect.DeepEqual(validConfig, appPayload.Payload.App.Config) {
t.Errorf("Expecting config %v but got %v in response", validConfig, appPayload.Payload.App.Config)
}
} }
func TestAppInsect(t *testing.T) { func TestAppInsect(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{"A": "a"}) defer s.Cleanup()
app := GetApp(t, s.Context, s.Client, s.AppName)
val, ok := app.Config["A"] validConfig := map[string]string{"A": "a"}
if !ok {
t.Error("Error during app config inspect: config map misses required entity `A` with value `a`.") s.GivenAppExists(t, &models.App{Name: s.AppName,
Config: validConfig})
appOk, err := s.Client.Apps.GetAppsApp(&apps.GetAppsAppParams{
App: s.AppName,
Context: s.Context,
})
if err != nil {
t.Fatalf("Expected valid response to get app, got %v", err)
} }
if !strings.Contains("a", val) {
t.Errorf("App config value is different. Expected: `a`. Actual %v", val) if !reflect.DeepEqual(validConfig, appOk.Payload.App.Config) {
t.Errorf("Returned config %v does not match requested config %v", appOk.Payload.App.Config, validConfig)
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestAppPatchSameConfig(t *testing.T) { func TestAppPatchConfig(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite()
config := map[string]string{
"A": "a",
}
appUpdatePayload := CreateUpdateApp(t, s.Context, s.Client, s.AppName, config) for _, tci := range updateConfigCases {
_, ok := appUpdatePayload.Payload.App.Config["A"] // iterator mutation meets parallelism... pfft
if !ok { tc := tci
t.Error("Error during app update: config map misses required entity `A` with value `a`.") t.Run(tc.name, func(t *testing.T) {
}
DeleteApp(t, s.Context, s.Client, s.AppName)
}
func TestAppPatchOverwriteConfig(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
config := map[string]string{ defer s.Cleanup()
"A": "b",
}
appPayload := CreateUpdateApp(t, s.Context, s.Client, s.AppName, config)
val, ok := appPayload.Payload.App.Config["A"]
if !ok {
t.Error("Error during app config inspect: config map misses required entity `A` with value `a`.")
}
if !strings.Contains("b", val) {
t.Errorf("App config value is different. Expected: `b`. Actual %v", val)
}
DeleteApp(t, s.Context, s.Client, s.AppName)
}
func TestAppsPatchConfigAddValue(t *testing.T) { s.GivenAppExists(t, &models.App{
t.Parallel() Name: s.AppName,
s := SetupDefaultSuite() Config: tc.intialConfig,
config := map[string]string{ })
"B": "b",
patch, err := s.Client.Apps.PatchAppsApp(&apps.PatchAppsAppParams{
App: s.AppName,
Body: &models.AppWrapper{
App: &models.App{
Config: tc.change,
},
},
Context: s.Context,
})
if err != nil {
t.Fatalf("Failed to patch app with valid value %v, %v", tc.change, err)
} }
appPayload := CreateUpdateApp(t, s.Context, s.Client, s.AppName, config)
val, ok := appPayload.Payload.App.Config["B"] if !ConfigEquivalent(patch.Payload.App.Config, tc.expected) {
if !ok { t.Errorf("Expected returned app config to be %v, but was %v", tc.expected, patch.Payload.App.Config)
t.Error("Error during app config inspect: config map misses required entity `B` with value `b`.")
} }
if !strings.Contains("b", val) {
t.Errorf("App config value is different. Expected: `b`. Actual %v", val) })
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestAppDuplicate(t *testing.T) { func TestAppDuplicate(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
_, err := CreateAppNoAssert(s.Context, s.Client, s.AppName, map[string]string{})
if reflect.TypeOf(err) != reflect.TypeOf(apps.NewPostAppsConflict()) { s.GivenAppExists(t, &models.App{Name: s.AppName})
CheckAppResponseError(t, err)
_, err := s.PostApp(&models.App{Name: s.AppName})
if _, ok := err.(*apps.PostAppsConflict); !ok {
t.Errorf("Expecting conflict response on duplicate app, got %v", err)
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }

View File

@@ -8,11 +8,12 @@ import (
"time" "time"
"github.com/fnproject/fn_go/client/call" "github.com/fnproject/fn_go/client/call"
"github.com/fnproject/fn_go/models"
) )
func TestCallsMissingApp(t *testing.T) { func TestCallsMissingApp(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
cfg := &call.GetAppsAppCallsParams{ cfg := &call.GetAppsAppCallsParams{
App: s.AppName, App: s.AppName,
Path: &s.RoutePath, Path: &s.RoutePath,
@@ -26,10 +27,11 @@ func TestCallsMissingApp(t *testing.T) {
func TestCallsDummy(t *testing.T) { func TestCallsDummy(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType,
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) s.GivenAppExists(t, &models.App{Name: s.AppName})
s.GivenRouteExists(t, s.AppName, s.BasicRoute())
cfg := &call.GetAppsAppCallsCallParams{ cfg := &call.GetAppsAppCallsCallParams{
Call: "dummy", Call: "dummy",
@@ -42,15 +44,15 @@ func TestCallsDummy(t *testing.T) {
t.Error("Must fail because `dummy` call does not exist.") t.Error("Must fail because `dummy` call does not exist.")
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestGetExactCall(t *testing.T) { func TestGetExactCall(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType,
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) s.GivenAppExists(t, &models.App{Name: s.AppName})
s.GivenRouteExists(t, s.AppName, s.BasicRoute())
u := url.URL{ u := url.URL{
Scheme: "http", Scheme: "http",
@@ -76,5 +78,4 @@ func TestGetExactCall(t *testing.T) {
t.Error(retryErr.Error()) t.Error(retryErr.Error())
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }

View File

@@ -0,0 +1,29 @@
package tests
import "reflect"
// common test cases around config for apps/routes
var updateConfigCases = []struct {
name string
intialConfig map[string]string
change map[string]string
expected map[string]string
}{
{"preserve existing config keys with nop", map[string]string{"key": "value1"}, map[string]string{}, map[string]string{"key": "value1"}},
{"preserve existing config keys with change", map[string]string{"key": "value1"}, map[string]string{"key": "value1"}, map[string]string{"key": "value1"}},
{"overwrite existing config keys", map[string]string{"key": "value1"}, map[string]string{"key": "value2"}, map[string]string{"key": "value2"}},
{"delete config key", map[string]string{"key": "value1"}, map[string]string{"key": ""}, map[string]string{}},
}
//ConfigEquivalent checks if two config objects are semantically equivalent (including nils)
func ConfigEquivalent(a map[string]string, b map[string]string) bool {
if len(a) == 0 && len(b) == 0 {
return true
}
return reflect.DeepEqual(a, b)
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/fnproject/fn_go/client/call" "github.com/fnproject/fn_go/client/call"
"github.com/fnproject/fn_go/client/operations" "github.com/fnproject/fn_go/client/operations"
"github.com/fnproject/fn_go/models"
) )
func CallAsync(t *testing.T, u url.URL, content io.Reader) string { func CallAsync(t *testing.T, u url.URL, content io.Reader) string {
@@ -58,10 +59,13 @@ func CallSync(t *testing.T, u url.URL, content io.Reader) string {
func TestCanCallfunction(t *testing.T) { func TestCanCallfunction(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, "sync",
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) s.GivenAppExists(t, &models.App{Name: s.AppName})
rt := s.BasicRoute()
rt.Type = "sync"
s.GivenRouteExists(t, s.AppName, rt)
u := url.URL{ u := url.URL{
Scheme: "http", Scheme: "http",
@@ -79,15 +83,15 @@ func TestCanCallfunction(t *testing.T) {
if !strings.Contains(expectedOutput, output.String()) { if !strings.Contains(expectedOutput, output.String()) {
t.Errorf("Assertion error.\n\tExpected: %v\n\tActual: %v", expectedOutput, output.String()) t.Errorf("Assertion error.\n\tExpected: %v\n\tActual: %v", expectedOutput, output.String())
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestCallOutputMatch(t *testing.T) { func TestCallOutputMatch(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) s.GivenAppExists(t, &models.App{Name: s.AppName})
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, "sync", rt := s.BasicRoute()
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) rt.Type = "sync"
s.GivenRouteExists(t, s.AppName, rt)
u := url.URL{ u := url.URL{
Scheme: "http", Scheme: "http",
@@ -108,16 +112,17 @@ func TestCallOutputMatch(t *testing.T) {
if !strings.Contains(expectedOutput, output.String()) { if !strings.Contains(expectedOutput, output.String()) {
t.Errorf("Assertion error.\n\tExpected: %v\n\tActual: %v", expectedOutput, output.String()) t.Errorf("Assertion error.\n\tExpected: %v\n\tActual: %v", expectedOutput, output.String())
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestCanCallAsync(t *testing.T) { func TestCanCallAsync(t *testing.T) {
newRouteType := "async" newRouteType := "async"
t.Parallel() t.Parallel()
s := SetupDefaultSuite()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) s := SetupHarness()
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, "sync", s.GivenAppExists(t, &models.App{Name: s.AppName})
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) rt := s.BasicRoute()
rt.Type = "sync"
s.GivenRouteExists(t, s.AppName, rt)
u := url.URL{ u := url.URL{
Scheme: "http", Scheme: "http",
@@ -125,25 +130,22 @@ func TestCanCallAsync(t *testing.T) {
} }
u.Path = path.Join(u.Path, "r", s.AppName, s.RoutePath) u.Path = path.Join(u.Path, "r", s.AppName, s.RoutePath)
_, err := UpdateRoute( s.GivenRoutePatched(t, s.AppName, s.RoutePath, &models.Route{
t, s.Context, s.Client, Type: newRouteType,
s.AppName, s.RoutePath, })
s.Image, newRouteType, s.Format,
s.Memory, s.RouteConfig, s.RouteHeaders, "")
CheckRouteResponseError(t, err)
CallAsync(t, u, &bytes.Buffer{}) CallAsync(t, u, &bytes.Buffer{})
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestCanGetAsyncState(t *testing.T) { func TestCanGetAsyncState(t *testing.T) {
newRouteType := "async" newRouteType := "async"
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{})
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, "sync", s.GivenAppExists(t, &models.App{Name: s.AppName})
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) rt := s.BasicRoute()
rt.Type = "sync"
s.GivenRouteExists(t, s.AppName, rt)
u := url.URL{ u := url.URL{
Scheme: "http", Scheme: "http",
@@ -151,13 +153,9 @@ func TestCanGetAsyncState(t *testing.T) {
} }
u.Path = path.Join(u.Path, "r", s.AppName, s.RoutePath) u.Path = path.Join(u.Path, "r", s.AppName, s.RoutePath)
_, err := UpdateRoute( s.GivenRoutePatched(t, s.AppName, rt.Path, &models.Route{
t, s.Context, s.Client, Type: newRouteType,
s.AppName, s.RoutePath, })
s.Image, newRouteType, s.Format,
s.Memory, s.RouteConfig, s.RouteHeaders, "")
CheckRouteResponseError(t, err)
callID := CallAsync(t, u, &bytes.Buffer{}) callID := CallAsync(t, u, &bytes.Buffer{})
cfg := &call.GetAppsAppCallsCallParams{ cfg := &call.GetAppsAppCallsCallParams{
@@ -198,26 +196,27 @@ func TestCanGetAsyncState(t *testing.T) {
t.Errorf("Call object status mismatch.\n\tExpected: %v\n\tActual:%v", "success", callObject.Status) t.Errorf("Call object status mismatch.\n\tExpected: %v\n\tActual:%v", "success", callObject.Status)
} }
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestCanCauseTimeout(t *testing.T) { func TestCanCauseTimeout(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
routePath := "/" + RandStringBytes(10) defer s.Cleanup()
image := "funcy/timeout:0.0.1"
routeType := "sync"
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) s.GivenAppExists(t, &models.App{Name: s.AppName})
CreateRoute(t, s.Context, s.Client, s.AppName, routePath, image, routeType,
s.Format, int32(10), s.IdleTimeout, s.RouteConfig, s.RouteHeaders) rt := s.BasicRoute()
timeout := int32(10)
rt.Timeout = &timeout
rt.Type = "sync"
rt.Image = "funcy/timeout:0.0.1"
s.GivenRouteExists(t, s.AppName, rt)
u := url.URL{ u := url.URL{
Scheme: "http", Scheme: "http",
Host: Host(), Host: Host(),
} }
u.Path = path.Join(u.Path, "r", s.AppName, routePath) u.Path = path.Join(u.Path, "r", s.AppName, rt.Path)
content := &bytes.Buffer{} content := &bytes.Buffer{}
json.NewEncoder(content).Encode(struct { json.NewEncoder(content).Encode(struct {
@@ -254,24 +253,24 @@ func TestCanCauseTimeout(t *testing.T) {
"output", "callObj.Payload.Call.Status") "output", "callObj.Payload.Call.Status")
} }
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestCallResponseHeadersMatch(t *testing.T) { func TestCallResponseHeadersMatch(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
routePath := "/os.environ"
image := "denismakogon/os.environ" s.GivenAppExists(t, &models.App{Name: s.AppName})
routeType := "sync" rt := s.BasicRoute()
CreateRoute(t, s.Context, s.Client, s.AppName, routePath, image, routeType, rt.Image = "denismakogon/os.environ"
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) rt.Type = "sync"
s.GivenRouteExists(t, s.AppName, rt)
u := url.URL{ u := url.URL{
Scheme: "http", Scheme: "http",
Host: Host(), Host: Host(),
} }
u.Path = path.Join(u.Path, "r", s.AppName, routePath) u.Path = path.Join(u.Path, "r", s.AppName, rt.Path)
content := &bytes.Buffer{} content := &bytes.Buffer{}
output := &bytes.Buffer{} output := &bytes.Buffer{}
CallFN(u.String(), content, output, "POST", CallFN(u.String(), content, output, "POST",
@@ -284,26 +283,26 @@ func TestCallResponseHeadersMatch(t *testing.T) {
t.Errorf("HEADER_ACCEPT='application/xml, application/json; q=0.2' "+ t.Errorf("HEADER_ACCEPT='application/xml, application/json; q=0.2' "+
"should be in output, have:%s\n", res) "should be in output, have:%s\n", res)
} }
DeleteRoute(t, s.Context, s.Client, s.AppName, routePath)
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestCanWriteLogs(t *testing.T) { func TestCanWriteLogs(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
routePath := "/log" defer s.Cleanup()
image := "funcy/log:0.0.1"
routeType := "sync"
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) rt := s.BasicRoute()
CreateRoute(t, s.Context, s.Client, s.AppName, routePath, image, routeType, rt.Path = "/log"
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) rt.Image = "funcy/log:0.0.1"
rt.Type = "sync"
s.GivenAppExists(t, &models.App{Name: s.AppName})
s.GivenRouteExists(t, s.AppName, rt)
u := url.URL{ u := url.URL{
Scheme: "http", Scheme: "http",
Host: Host(), Host: Host(),
} }
u.Path = path.Join(u.Path, "r", s.AppName, routePath) u.Path = path.Join(u.Path, "r", s.AppName, rt.Path)
content := &bytes.Buffer{} content := &bytes.Buffer{}
json.NewEncoder(content).Encode(struct { json.NewEncoder(content).Encode(struct {
Size int Size int
@@ -323,26 +322,27 @@ func TestCanWriteLogs(t *testing.T) {
t.Error(err.Error()) t.Error(err.Error())
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestOversizedLog(t *testing.T) { func TestOversizedLog(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
routePath := "/log" defer s.Cleanup()
image := "funcy/log:0.0.1"
routeType := "sync"
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) rt := s.BasicRoute()
CreateRoute(t, s.Context, s.Client, s.AppName, routePath, image, routeType, rt.Path = "/log"
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) rt.Image = "funcy/log:0.0.1"
rt.Type = "sync"
s.GivenAppExists(t, &models.App{Name: s.AppName})
s.GivenRouteExists(t, s.AppName, rt)
size := 1 * 1024 * 1024 * 1024 size := 1 * 1024 * 1024 * 1024
u := url.URL{ u := url.URL{
Scheme: "http", Scheme: "http",
Host: Host(), Host: Host(),
} }
u.Path = path.Join(u.Path, "r", s.AppName, routePath) u.Path = path.Join(u.Path, "r", s.AppName, rt.Path)
content := &bytes.Buffer{} content := &bytes.Buffer{}
json.NewEncoder(content).Encode(struct { json.NewEncoder(content).Encode(struct {
Size int Size int
@@ -366,5 +366,4 @@ func TestOversizedLog(t *testing.T) {
size/1024, len(log)) size/1024, len(log))
} }
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }

View File

@@ -3,6 +3,7 @@ package tests
import ( import (
"bytes" "bytes"
"encoding/json" "encoding/json"
"github.com/fnproject/fn_go/models"
"net/url" "net/url"
"path" "path"
"strconv" "strconv"
@@ -16,16 +17,16 @@ type JSONResponse struct {
func TestFnJSONFormats(t *testing.T) { func TestFnJSONFormats(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
defer s.Cleanup()
// TODO(treeder): put image in fnproject @ dockerhub // TODO(treeder): put image in fnproject @ dockerhub
image := "denismakogon/test-hot-json-go:0.0.1"
format := "json"
route := "/test-hot-json-go"
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) s.GivenAppExists(t, &models.App{Name: s.AppName})
CreateRoute(t, s.Context, s.Client, s.AppName, route, image, "sync", rt := s.BasicRoute()
format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) rt.Image = "denismakogon/test-hot-json-go:0.0.1"
rt.Format = "json"
s.GivenRouteExists(t, s.AppName, rt)
u := url.URL{ u := url.URL{
Scheme: "http", Scheme: "http",
@@ -63,5 +64,4 @@ func TestFnJSONFormats(t *testing.T) {
} }
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }

View File

@@ -8,10 +8,8 @@ import (
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
// call flag.Parse() here if TestMain uses flags // call flag.Parse() here if TestMain uses flags
s := SetupDefaultSuite()
result := m.Run() result := m.Run()
Cleanup()
s.Cancel()
if result == 0 { if result == 0 {
fmt.Fprintln(os.Stdout, "😀 👍 🎗") fmt.Fprintln(os.Stdout, "😀 👍 🎗")
} }

View File

@@ -1,228 +1,97 @@
package tests package tests
import ( import (
"context"
"testing" "testing"
"github.com/fnproject/fn_go/client"
"github.com/fnproject/fn_go/client/routes" "github.com/fnproject/fn_go/client/routes"
"github.com/fnproject/fn_go/models" "github.com/fnproject/fn_go/models"
) )
func CheckRouteResponseError(t *testing.T, e error) { func AssertRouteMatches(t *testing.T, expected *models.Route, got *models.Route) {
if e != nil {
switch err := e.(type) {
case *routes.PostAppsAppRoutesDefault:
t.Errorf("Unexpected error occurred: %v. Status code: %v", err.Payload.Error.Message, err.Code())
t.FailNow()
case *routes.PostAppsAppRoutesBadRequest:
t.Errorf("Unexpected error occurred: %v.", err.Payload.Error.Message)
t.FailNow()
case *routes.PostAppsAppRoutesConflict:
t.Errorf("Unexpected error occurred: %v.", err.Payload.Error.Message)
t.FailNow()
case *routes.GetAppsAppRoutesRouteNotFound:
t.Errorf("Unexpected error occurred: %v.", err.Payload.Error.Message)
t.FailNow()
case *routes.GetAppsAppRoutesRouteDefault:
t.Errorf("Unexpected error occurred: %v. Status code: %v", err.Payload.Error.Message, err.Code())
t.FailNow()
case *routes.DeleteAppsAppRoutesRouteNotFound:
t.Errorf("Unexpected error occurred: %v.", err.Payload.Error.Message)
t.FailNow()
case *routes.DeleteAppsAppRoutesRouteDefault:
t.Errorf("Unexpected error occurred: %v. Status code: %v", err.Payload.Error.Message, err.Code())
t.FailNow()
case *routes.GetAppsAppRoutesNotFound:
t.Errorf("Unexpected error occurred: %v.", err.Payload.Error.Message)
t.FailNow()
case *routes.GetAppsAppRoutesDefault:
t.Errorf("Unexpected error occurred: %v. Status code: %v", err.Payload.Error.Message, err.Code())
t.FailNow()
case *routes.PatchAppsAppRoutesRouteBadRequest:
t.Errorf("Unexpected error occurred: %v.", err.Payload.Error.Message)
t.FailNow()
case *routes.PatchAppsAppRoutesRouteNotFound:
t.Errorf("Unexpected error occurred: %v.", err.Payload.Error.Message)
t.FailNow()
case *routes.PatchAppsAppRoutesRouteDefault:
t.Errorf("Unexpected error occurred: %v. Status code: %v", err.Payload.Error.Message, err.Code())
case *routes.PutAppsAppRoutesRouteBadRequest:
t.Errorf("Unexpected error occurred: %v.", err.Payload.Error.Message)
case *routes.PutAppsAppRoutesRouteDefault:
t.Errorf("Unexpected error occurred: %v. Status code: %v", err.Payload.Error.Message, err.Code())
t.FailNow()
default:
t.Errorf("Unable to determine type of error: %s", err)
t.FailNow()
}
}
}
func assertRouteFields(t *testing.T, routeObject *models.Route, path, image, routeType, routeFormat string) { if expected.Path != got.Path {
t.Errorf("Route path mismatch. Expected: %v. Actual: %v", expected.Path, got.Path)
rPath := routeObject.Path
rImage := routeObject.Image
rType := routeObject.Type
rTimeout := *routeObject.Timeout
rIdleTimeout := *routeObject.IDLETimeout
rFormat := routeObject.Format
if rPath != path {
t.Errorf("Route path mismatch. Expected: %v. Actual: %v", path, rPath)
} }
if rImage != image { if expected.Image != got.Image {
t.Errorf("Route image mismatch. Expected: %v. Actual: %v", image, rImage) t.Errorf("Route image mismatch. Expected: %v. Actual: %v", expected.Image, got.Image)
} }
if rType != routeType { if expected.Image != got.Image {
t.Errorf("Route type mismatch. Expected: %v. Actual: %v", routeType, rType) t.Errorf("Route type mismatch. Expected: %v. Actual: %v", expected.Image, got.Image)
} }
if rTimeout == 0 { if expected.Format != got.Format {
t.Error("Route timeout should have default value of 30 seconds, but got 0 seconds") t.Errorf("Route format mismatch. Expected: %v. Actual: %v", expected.Format, got.Format)
}
if rIdleTimeout == 0 {
t.Error("Route idle timeout should have default value of 30 seconds, but got 0 seconds")
}
if rFormat != routeFormat {
t.Errorf("Route format mismatch. Expected: %v. Actual: %v", routeFormat, rFormat)
} }
} }
func createRoute(ctx context.Context, fnclient *client.Fn, appName, image, routePath, routeType, routeFormat string, timeout, idleTimeout int32, routeConfig map[string]string, headers map[string][]string) (*routes.PostAppsAppRoutesOK, error) { // PostRoute Creates a route and deletes the corresponding app (if created) on teardown
func (s *TestHarness) PostRoute(appName string, route *models.Route) (*routes.PostAppsAppRoutesOK, error) {
cfg := &routes.PostAppsAppRoutesParams{ cfg := &routes.PostAppsAppRoutesParams{
App: appName, App: appName,
Body: &models.RouteWrapper{ Body: &models.RouteWrapper{
Route: &models.Route{ Route: route,
Config: routeConfig,
Headers: headers,
Image: image,
Path: routePath,
Type: routeType,
Format: routeFormat,
Timeout: &timeout,
IDLETimeout: &idleTimeout,
}, },
}, Context: s.Context,
Context: ctx,
} }
ok, err := fnclient.Routes.PostAppsAppRoutes(cfg) ok, err := s.Client.Routes.PostAppsAppRoutes(cfg)
if err == nil { if err == nil {
approutesLock.Lock() s.createdApps[appName] = true
r, got := appsandroutes[appName]
if got {
appsandroutes[appName] = append(r, routePath)
} else {
appsandroutes[appName] = []string{routePath}
}
approutesLock.Unlock()
} }
return ok, err return ok, err
} }
func CreateRoute(t *testing.T, ctx context.Context, fnclient *client.Fn, appName, routePath, image, routeType, routeFormat string, timeout, idleTimeout int32, routeConfig map[string]string, headers map[string][]string) { func (s *TestHarness) BasicRoute() *models.Route {
routeResponse, err := createRoute(ctx, fnclient, appName, image, routePath, routeType, routeFormat, timeout, idleTimeout, routeConfig, headers) return &models.Route{
CheckRouteResponseError(t, err) Format: s.Format,
Path: s.RoutePath,
assertRouteFields(t, routeResponse.Payload.Route, routePath, image, routeType, routeFormat) Image: s.Image,
Type: s.RouteType,
Timeout: &s.Timeout,
IDLETimeout: &s.IdleTimeout,
}
} }
func deleteRoute(ctx context.Context, fnclient *client.Fn, appName, routePath string) (*routes.DeleteAppsAppRoutesRouteOK, error) { //GivenRouteExists creates a route using the specified arguments, failing the test if the creation fails, this tears down any apps that are created when the test is complete
cfg := &routes.DeleteAppsAppRoutesRouteParams{ func (s *TestHarness) GivenRouteExists(t *testing.T, appName string, route *models.Route) {
App: appName, _, err := s.PostRoute(appName, route)
Route: routePath, if err != nil {
Context: ctx, t.Fatalf("Expected route to be created, got %v", err)
} }
return fnclient.Routes.DeleteAppsAppRoutesRoute(cfg)
} }
func DeleteRoute(t *testing.T, ctx context.Context, fnclient *client.Fn, appName, routePath string) { //RouteMustExist checks that a route exists, failing the test if it doesn't, returns the route
_, err := deleteRoute(ctx, fnclient, appName, routePath) func (s *TestHarness) RouteMustExist(t *testing.T, appName string, routePath string) *models.Route {
CheckRouteResponseError(t, err)
}
func ListRoutes(t *testing.T, ctx context.Context, fnclient *client.Fn, appName string) []*models.Route {
cfg := &routes.GetAppsAppRoutesParams{
App: appName,
Context: ctx,
}
routesResponse, err := fnclient.Routes.GetAppsAppRoutes(cfg)
CheckRouteResponseError(t, err)
return routesResponse.Payload.Routes
}
func GetRoute(t *testing.T, ctx context.Context, fnclient *client.Fn, appName, routePath string) *models.Route {
cfg := &routes.GetAppsAppRoutesRouteParams{ cfg := &routes.GetAppsAppRoutesRouteParams{
App: appName, App: appName,
Route: routePath[1:], Route: routePath[1:],
Context: ctx, Context: s.Context,
} }
routeResponse, err := fnclient.Routes.GetAppsAppRoutesRoute(cfg) routeResponse, err := s.Client.Routes.GetAppsAppRoutesRoute(cfg)
CheckRouteResponseError(t, err) if err != nil {
t.Fatalf("Expected route %s %s to exist but got %v", appName, routePath, err)
}
return routeResponse.Payload.Route return routeResponse.Payload.Route
} }
func UpdateRoute(t *testing.T, ctx context.Context, fnclient *client.Fn, appName, routePath, image, routeType, format string, memory uint64, routeConfig map[string]string, headers map[string][]string, newRoutePath string) (*routes.PatchAppsAppRoutesRouteOK, error) { //GivenRoutePatched applies a patch to a route, failing the test if this fails.
func (s *TestHarness) GivenRoutePatched(t *testing.T, appName, routeName string, rt *models.Route) {
routeObject := GetRoute(t, ctx, fnclient, appName, routePath) _, err := s.Client.Routes.PatchAppsAppRoutesRoute(&routes.PatchAppsAppRoutesRouteParams{
if routeObject.Config == nil {
routeObject.Config = map[string]string{}
}
if routeObject.Headers == nil {
routeObject.Headers = map[string][]string{}
}
routeObject.Path = ""
if newRoutePath != "" {
routeObject.Path = newRoutePath
}
if routeConfig != nil {
for k, v := range routeConfig {
if string(k[0]) == "-" {
delete(routeObject.Config, string(k[1:]))
continue
}
routeObject.Config[k] = v
}
}
if headers != nil {
for k, v := range headers {
if string(k[0]) == "-" {
delete(routeObject.Headers, k)
continue
}
routeObject.Headers[k] = v
}
}
if image != "" {
routeObject.Image = image
}
if format != "" {
routeObject.Format = format
}
if routeType != "" {
routeObject.Type = routeType
}
if memory > 0 {
routeObject.Memory = memory
}
cfg := &routes.PatchAppsAppRoutesRouteParams{
App: appName, App: appName,
Context: ctx, Route: routeName,
Context: s.Context,
Body: &models.RouteWrapper{ Body: &models.RouteWrapper{
Route: routeObject, Route: rt,
}, },
Route: routePath, })
}
return fnclient.Routes.PatchAppsAppRoutesRoute(cfg) if err != nil {
t.Fatalf("Failed to patch route %s %s : %v", appName, routeName, err)
}
} }
func assertContainsRoute(routeModels []*models.Route, expectedRoute string) bool { func assertContainsRoute(routeModels []*models.Route, expectedRoute string) bool {
@@ -234,24 +103,22 @@ func assertContainsRoute(routeModels []*models.Route, expectedRoute string) bool
return false return false
} }
func DeployRoute(t *testing.T, ctx context.Context, fnclient *client.Fn, appName, routePath, image, routeType, routeFormat string, routeConfig map[string]string, headers map[string][]string) *models.Route { //PutRoute creates a route via PUT, tearing down any apps that are created when the test is complete
func (s *TestHarness) PutRoute(appName string, routePath string, route *models.Route) (*routes.PutAppsAppRoutesRouteOK, error) {
cfg := &routes.PutAppsAppRoutesRouteParams{ cfg := &routes.PutAppsAppRoutesRouteParams{
App: appName, App: appName,
Context: ctx, Context: s.Context,
Route: routePath, Route: routePath,
Body: &models.RouteWrapper{ Body: &models.RouteWrapper{
Route: &models.Route{ Route: route,
Config: routeConfig,
Headers: headers,
Image: image,
Path: routePath,
Type: routeType,
Format: routeFormat,
},
}, },
} }
route, err := fnclient.Routes.PutAppsAppRoutesRoute(cfg) resp, err := s.Client.Routes.PutAppsAppRoutesRoute(cfg)
CheckRouteResponseError(t, err)
return route.Payload.Route if err == nil {
s.createdApps[appName] = true
}
return resp, err
} }

View File

@@ -6,206 +6,497 @@ import (
"reflect" "reflect"
"github.com/fnproject/fn/api/id" "github.com/fnproject/fn/api/id"
"github.com/fnproject/fn_go/client/apps"
"github.com/fnproject/fn_go/client/routes"
"github.com/fnproject/fn_go/models" "github.com/fnproject/fn_go/models"
) )
func TestCreateRouteEmptyType(t *testing.T) { func TestShouldRejectEmptyRouteType(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
_, err := createRoute(s.Context, s.Client, s.AppName, s.RoutePath, s.Image, "",
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) s.GivenAppExists(t, &models.App{Name: s.AppName})
_, err := s.PostRoute(s.AppName, &models.Route{
Path: s.RoutePath,
Image: s.Image,
Type: "v",
Format: s.Format,
})
if err == nil { if err == nil {
t.Errorf("Should fail with Invalid route Type.") t.Errorf("Should fail with Invalid route Type.")
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestCanCreateRoute(t *testing.T) { func TestCanCreateRoute(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType,
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) s.GivenAppExists(t, &models.App{Name: s.AppName})
DeleteApp(t, s.Context, s.Client, s.AppName) _, err := s.PostRoute(s.AppName, &models.Route{
Path: s.RoutePath,
Image: s.Image,
Format: s.Format,
})
if err != nil {
t.Errorf("expected route success, got %v", err)
}
// TODO validate route returned matches request
} }
func TestListRoutes(t *testing.T) { func TestListRoutes(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType,
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) s.GivenAppExists(t, &models.App{Name: s.AppName})
if !assertContainsRoute(ListRoutes(t, s.Context, s.Client, s.AppName), s.RoutePath) { s.GivenRouteExists(t, s.AppName, s.BasicRoute())
cfg := &routes.GetAppsAppRoutesParams{
App: s.AppName,
Context: s.Context,
}
routesResponse, err := s.Client.Routes.GetAppsAppRoutes(cfg)
if err != nil {
t.Fatalf("Expecting list routes to be successful, got %v", err)
}
if !assertContainsRoute(routesResponse.Payload.Routes, s.RoutePath) {
t.Errorf("Unable to find corresponding route `%v` in list", s.RoutePath) t.Errorf("Unable to find corresponding route `%v` in list", s.RoutePath)
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestInspectRoute(t *testing.T) { func TestInspectRoute(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType,
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders)
rObjects := []*models.Route{GetRoute(t, s.Context, s.Client, s.AppName, s.RoutePath)} s.GivenAppExists(t, &models.App{Name: s.AppName})
if !assertContainsRoute(rObjects, s.RoutePath) { newRt := s.BasicRoute()
t.Errorf("Unable to find corresponding route `%v` in list", s.RoutePath) s.GivenRouteExists(t, s.AppName, newRt)
resp, err := s.Client.Routes.GetAppsAppRoutesRoute(&routes.GetAppsAppRoutesRouteParams{
App: s.AppName,
Route: newRt.Path[1:],
Context: s.Context,
})
if err != nil {
t.Fatalf("Failed to get route %s, %v", s.RoutePath, err)
} }
DeleteApp(t, s.Context, s.Client, s.AppName) gotRt := resp.Payload.Route
AssertRouteMatches(t, newRt, gotRt)
} }
func TestCanUpdateRouteType(t *testing.T) { var validRouteUpdates = []struct {
newRouteType := "sync" name string
t.Parallel() update *models.Route
s := SetupDefaultSuite() extract func(*models.Route) interface{}
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) }{
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType, {"route type (sync)", &models.Route{Type: "sync"}, func(m *models.Route) interface{} { return m.Type }},
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) {"route type (async)", &models.Route{Type: "async"}, func(m *models.Route) interface{} { return m.Type }},
{"format (json)", &models.Route{Format: "json"}, func(m *models.Route) interface{} { return m.Format }},
routeResp, err := UpdateRoute( {"format (default)", &models.Route{Format: "default"}, func(m *models.Route) interface{} { return m.Format }},
t, s.Context, s.Client, // ...
s.AppName, s.RoutePath,
s.Image, newRouteType, s.Format,
s.Memory, s.RouteConfig, s.RouteHeaders, "")
CheckRouteResponseError(t, err)
assertRouteFields(t, routeResp.Payload.Route, s.RoutePath, s.Image, newRouteType, s.Format)
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestCanUpdateRouteConfig(t *testing.T) { func TestCanUpdateRouteAttributes(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{})
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType,
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders)
newRouteConf := map[string]string{ for _, tci := range validRouteUpdates {
"A": "a", tc := tci
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
s := SetupHarness()
defer s.Cleanup()
s.GivenAppExists(t, &models.App{Name: s.AppName})
s.GivenRouteExists(t, s.AppName, s.BasicRoute())
routeResp, err := s.Client.Routes.PatchAppsAppRoutesRoute(
&routes.PatchAppsAppRoutesRouteParams{
App: s.AppName,
Context: s.Context,
Route: s.RoutePath,
Body: &models.RouteWrapper{
Route: tc.update,
},
},
)
if err != nil {
t.Fatalf("Failed to patch route, got %v", err)
} }
routeResp, err := UpdateRoute( got := tc.extract(routeResp.Payload.Route)
t, s.Context, s.Client, change := tc.extract(tc.update)
s.AppName, s.RoutePath, if !reflect.DeepEqual(got, change) {
s.Image, s.RouteType, s.Format, t.Errorf("Expected value in response tobe %v but was %v", change, got)
s.Memory, newRouteConf, s.RouteHeaders, "") }
})
}
CheckRouteResponseError(t, err) }
assertRouteFields(t, routeResp.Payload.Route, s.RoutePath, s.Image, s.RouteType, s.Format)
DeleteApp(t, s.Context, s.Client, s.AppName) func TestRoutePatchConfig(t *testing.T) {
t.Parallel()
for _, tci := range updateConfigCases {
tc := tci
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
s := SetupHarness()
defer s.Cleanup()
s.GivenAppExists(t, &models.App{Name: s.AppName})
route := s.BasicRoute()
route.Config = tc.intialConfig
s.GivenRouteExists(t, s.AppName, route)
routeResp, err := s.Client.Routes.PatchAppsAppRoutesRoute(
&routes.PatchAppsAppRoutesRouteParams{
App: s.AppName,
Route: s.RoutePath,
Body: &models.RouteWrapper{
Route: &models.Route{
Config: tc.change,
},
},
Context: s.Context,
},
)
if err != nil {
t.Fatalf("Failed to patch route, got %v", err)
}
actual := routeResp.Payload.Route.Config
if !ConfigEquivalent(actual, tc.expected) {
t.Errorf("Expected config : %v after update, got %v", tc.expected, actual)
}
})
}
}
func TestSetRouteAnnotationsOnCreate(t *testing.T) {
t.Parallel()
for _, tci := range createAnnotationsValidCases {
// iterator mutation meets parallelism... pfft
tc := tci
t.Run("valid_"+tc.name, func(t *testing.T) {
t.Parallel()
s := SetupHarness()
defer s.Cleanup()
s.GivenAppExists(t, &models.App{
Name: s.AppName,
})
rt := s.BasicRoute()
rt.Annotations = tc.annotations
route, err := s.Client.Routes.PostAppsAppRoutes(&routes.PostAppsAppRoutesParams{
App: s.AppName,
Context: s.Context,
Body: &models.RouteWrapper{
Route: rt,
},
})
if err != nil {
t.Fatalf("Failed to create route with valid annotations %v got error %v", tc.annotations, err)
}
gotMd := route.Payload.Route.Annotations
if !AnnotationsEquivalent(gotMd, tc.annotations) {
t.Errorf("Returned annotations %v does not match set annotations %v", gotMd, tc.annotations)
}
getRoute := s.RouteMustExist(t, s.AppName, s.RoutePath)
if !AnnotationsEquivalent(getRoute.Annotations, tc.annotations) {
t.Errorf("GET annotations '%v' does not match set annotations %v", getRoute.Annotations, tc.annotations)
}
})
}
for _, tci := range createAnnotationsErrorCases {
// iterator mutation meets parallelism... pfft
tc := tci
t.Run("invalid_"+tc.name, func(ti *testing.T) {
ti.Parallel()
s := SetupHarness()
defer s.Cleanup()
_, err := s.PostApp(&models.App{
Name: s.AppName,
Annotations: tc.annotations,
})
if err == nil {
t.Fatalf("Created app with invalid annotations %v but expected error", tc.annotations)
}
if _, ok := err.(*apps.PostAppsBadRequest); !ok {
t.Errorf("Expecting bad request for invalid annotations, got %v", err)
}
})
}
}
func TestSetRouteMetadataOnPatch(t *testing.T) {
t.Parallel()
for _, tci := range updateAnnotationsValidCases {
// iterator mutation meets parallelism... pfft
tc := tci
t.Run("valid_"+tc.name, func(t *testing.T) {
t.Parallel()
s := SetupHarness()
defer s.Cleanup()
s.GivenAppExists(t, &models.App{Name: s.AppName})
rt := s.BasicRoute()
rt.Annotations = tc.initial
s.GivenRouteExists(t, s.AppName, rt)
res, err := s.Client.Routes.PatchAppsAppRoutesRoute(&routes.PatchAppsAppRoutesRouteParams{
App: s.AppName,
Route: s.RoutePath[1:],
Context: s.Context,
Body: &models.RouteWrapper{
Route: &models.Route{
Annotations: tc.change,
},
},
})
if err != nil {
t.Fatalf("Failed to patch annotations with %v on route: %v", tc.change, err)
}
gotMd := res.Payload.Route.Annotations
if !AnnotationsEquivalent(gotMd, tc.expected) {
t.Errorf("Returned annotations %v does not match set annotations %v", gotMd, tc.expected)
}
getRoute := s.RouteMustExist(t, s.AppName, s.RoutePath)
if !AnnotationsEquivalent(getRoute.Annotations, tc.expected) {
t.Errorf("GET annotations '%v' does not match set annotations %v", getRoute.Annotations, tc.expected)
}
})
}
for _, tci := range updateAnnotationsErrorCases {
// iterator mutation meets parallelism... pfft
tc := tci
t.Run("invalid_"+tc.name, func(t *testing.T) {
t.Parallel()
s := SetupHarness()
defer s.Cleanup()
s.GivenAppExists(t, &models.App{
Name: s.AppName,
})
rt := s.BasicRoute()
rt.Annotations = tc.initial
s.GivenRouteExists(t, s.AppName, rt)
_, err := s.Client.Routes.PatchAppsAppRoutesRoute(&routes.PatchAppsAppRoutesRouteParams{
App: s.AppName,
Route: s.RoutePath[1:],
Context: s.Context,
Body: &models.RouteWrapper{
Route: &models.Route{
Annotations: tc.change,
},
},
})
if err == nil {
t.Errorf("patched route with invalid annotations %v but expected error", tc.change)
}
if _, ok := err.(*routes.PatchAppsAppRoutesRouteBadRequest); !ok {
t.Errorf("Expecting bad request for invalid annotations, got %v", err)
}
})
}
} }
func TestCantUpdateRoutePath(t *testing.T) { func TestCantUpdateRoutePath(t *testing.T) {
t.Parallel() t.Parallel()
newRoutePath := id.New().String() s := SetupHarness()
s := SetupDefaultSuite() defer s.Cleanup()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{})
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType,
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders)
_, err := UpdateRoute( s.GivenAppExists(t, &models.App{Name: s.AppName})
t, s.Context, s.Client, s.GivenRouteExists(t, s.AppName, s.BasicRoute())
s.AppName, s.RoutePath,
s.Image, s.RouteType, s.Format, _, err := s.Client.Routes.PatchAppsAppRoutesRoute(
s.Memory, s.RouteConfig, s.RouteHeaders, newRoutePath) &routes.PatchAppsAppRoutesRouteParams{
App: s.AppName,
Route: s.RoutePath,
Body: &models.RouteWrapper{
Route: &models.Route{
Path: id.New().String(),
},
},
})
if err == nil { if err == nil {
t.Errorf("Route path suppose to be immutable, but it's not.") t.Fatalf("Expected error when patching route")
}
if _, ok := err.(*routes.PatchAppsAppRoutesRouteBadRequest); ok {
t.Errorf("Error should be bad request when updating route path ")
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestRouteDuplicate(t *testing.T) { func TestRoutePreventsDuplicate(t *testing.T) {
t.Parallel() t.Parallel()
newRouteType := "async" s := SetupHarness()
s := SetupDefaultSuite() defer s.Cleanup()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{})
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType, s.GivenAppExists(t, &models.App{Name: s.AppName})
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) s.GivenRouteExists(t, s.AppName, s.BasicRoute())
_, err := s.PostRoute(s.AppName, s.BasicRoute())
_, err := createRoute(s.Context, s.Client, s.AppName, s.Image, s.RoutePath,
newRouteType, s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders)
if err == nil { if err == nil {
t.Errorf("Route duplicate error should appear, but it didn't") t.Errorf("Route duplicate error should appear, but it didn't")
} }
if _, ok := err.(*routes.PostAppsAppRoutesConflict); !ok {
DeleteApp(t, s.Context, s.Client, s.AppName) t.Errorf("Error should be a conflict when creating a new route, got %v", err)
}
} }
func TestCanDeleteRoute(t *testing.T) { func TestCanDeleteRoute(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType,
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders)
DeleteRoute(t, s.Context, s.Client, s.AppName, s.RoutePath) s.GivenAppExists(t, &models.App{Name: s.AppName})
DeleteApp(t, s.Context, s.Client, s.AppName) s.GivenRouteExists(t, s.AppName, s.BasicRoute())
}
func TestCantDeleteRoute(t *testing.T) { _, err := s.Client.Routes.DeleteAppsAppRoutesRoute(&routes.DeleteAppsAppRoutesRouteParams{
t.Parallel() App: s.AppName,
s := SetupDefaultSuite() Route: s.RoutePath,
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) Context: s.Context,
})
_, err := deleteRoute(s.Context, s.Client, s.AppName, "dummy-route") if err != nil {
if err == nil { t.Errorf("Expected success when deleting existing route, got %v", err)
t.Error("Delete from missing route must fail.")
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestDeployNewApp(t *testing.T) { func TestCantDeleteMissingRoute(t *testing.T) {
t.Parallel() t.Parallel()
s := SetupDefaultSuite() s := SetupHarness()
DeployRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType, s.Format, s.RouteConfig, s.RouteHeaders) defer s.Cleanup()
GetApp(t, s.Context, s.Client, s.AppName)
GetRoute(t, s.Context, s.Client, s.AppName, s.RoutePath) s.GivenAppExists(t, &models.App{Name: s.AppName})
DeleteApp(t, s.Context, s.Client, s.AppName)
_, err := s.Client.Routes.DeleteAppsAppRoutesRoute(&routes.DeleteAppsAppRoutesRouteParams{
App: s.AppName,
Route: s.RoutePath,
Context: s.Context,
})
if err == nil {
t.Fatalf("Expected error when deleting non-existing route, got none")
}
if _, ok := err.(*routes.DeleteAppsAppRoutesRouteNotFound); !ok {
t.Fatalf("Expected not-found when deleting non-existing route, got %v", err)
}
} }
func TestDeployExistingApp(t *testing.T) { func TestPutRouteCreatesNewApp(t *testing.T) {
s := SetupDefaultSuite() t.Parallel()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) s := SetupHarness()
DeployRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType, s.Format, s.RouteConfig, s.RouteHeaders) defer s.Cleanup()
GetApp(t, s.Context, s.Client, s.AppName)
GetRoute(t, s.Context, s.Client, s.AppName, s.RoutePath) _, err := s.PutRoute(s.AppName, s.RoutePath, s.BasicRoute())
DeleteApp(t, s.Context, s.Client, s.AppName)
if err != nil {
t.Fatalf("Expected new route to be created, got %v", err)
}
s.AppMustExist(t, s.AppName)
s.RouteMustExist(t, s.AppName, s.RoutePath)
} }
func TestDeployUpdate(t *testing.T) { func TestPutRouteToExistingApp(t *testing.T) {
s := SetupHarness()
defer s.Cleanup()
s.GivenAppExists(t, &models.App{Name: s.AppName})
_, err := s.PutRoute(s.AppName, s.RoutePath, s.BasicRoute())
if err != nil {
t.Fatalf("Failed to create route, got error %v", err)
}
s.AppMustExist(t, s.AppName)
s.RouteMustExist(t, s.AppName, s.RoutePath)
}
func TestPutRouteUpdatesRoute(t *testing.T) {
newRouteType := "sync" newRouteType := "sync"
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType,
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders)
updatedRoute := DeployRoute( s.GivenAppExists(t, &models.App{Name: s.AppName})
t, s.Context, s.Client, s.GivenRouteExists(t, s.AppName, s.BasicRoute())
s.AppName, s.RoutePath,
s.Image, newRouteType,
s.Format, s.RouteConfig, s.RouteHeaders)
assertRouteFields(t, updatedRoute, s.RoutePath, s.Image, newRouteType, s.Format)
DeleteApp(t, s.Context, s.Client, s.AppName) changed := s.BasicRoute()
changed.Type = newRouteType
updatedRoute, err := s.PutRoute(s.AppName, s.RoutePath, changed)
if err != nil {
t.Fatalf("Failed to update route, got %v", err)
}
got := updatedRoute.Payload.Route.Type
if got != newRouteType {
t.Errorf("expected type to be %v after update, got %v", newRouteType, got)
}
} }
func TestMulpileDeployExistingApp(t *testing.T) { func TestPutIsIdempotentForHeaders(t *testing.T) {
s := SetupDefaultSuite() s := SetupHarness()
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) defer s.Cleanup()
s.GivenAppExists(t, &models.App{Name: s.AppName})
routeHeaders := map[string][]string{} routeHeaders := map[string][]string{}
routeHeaders["A"] = []string{"a"} routeHeaders["A"] = []string{"a"}
routeHeaders["B"] = []string{"b"} routeHeaders["B"] = []string{"b"}
DeployRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType, s.Format, s.RouteConfig, routeHeaders)
sameRoute := DeployRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType, s.Format, s.RouteConfig, routeHeaders) r1 := s.BasicRoute()
if ok := reflect.DeepEqual(sameRoute.Headers, routeHeaders); !ok { r1.Headers = routeHeaders
updatedRoute1, err := s.PutRoute(s.AppName, s.RoutePath, r1)
if err != nil {
t.Fatalf("Failed to update route, got %v", err)
}
if firstMatches := reflect.DeepEqual(routeHeaders, updatedRoute1.Payload.Route.Headers); !firstMatches {
t.Errorf("Route headers should remain the same after multiple deploys with exact the same parameters '%v' != '%v'", routeHeaders, updatedRoute1.Payload.Route.Headers)
}
updatedRoute2, err := s.PutRoute(s.AppName, s.RoutePath, r1)
if bothmatch := reflect.DeepEqual(updatedRoute1.Payload.Route.Headers, updatedRoute2.Payload.Route.Headers); !bothmatch {
t.Error("Route headers should remain the same after multiple deploys with exact the same parameters") t.Error("Route headers should remain the same after multiple deploys with exact the same parameters")
} }
DeleteApp(t, s.Context, s.Client, s.AppName)
} }

View File

@@ -9,7 +9,6 @@ import (
"net/http" "net/http"
"net/url" "net/url"
"os" "os"
"runtime"
"strings" "strings"
"sync" "sync"
"testing" "testing"
@@ -51,8 +50,6 @@ var (
getServer sync.Once getServer sync.Once
cancel2 context.CancelFunc cancel2 context.CancelFunc
s *server.Server s *server.Server
appsandroutes = make(map[string][]string)
approutesLock sync.Mutex
) )
func getServerWithCancel() (*server.Server, context.CancelFunc) { func getServerWithCancel() (*server.Server, context.CancelFunc) {
@@ -95,7 +92,9 @@ func getServerWithCancel() (*server.Server, context.CancelFunc) {
return s, cancel2 return s, cancel2
} }
type SuiteSetup struct { // TestHarness provides context and pre-configured clients to an individual test, it has some helper functions to create Apps and Routes that mirror the underlying client operations and clean them up after the test is complete
// This is not goroutine safe and each test case should use its own harness.
type TestHarness struct {
Context context.Context Context context.Context
Client *client.Fn Client *client.Fn
AppName string AppName string
@@ -109,6 +108,8 @@ type SuiteSetup struct {
RouteConfig map[string]string RouteConfig map[string]string
RouteHeaders map[string][]string RouteHeaders map[string][]string
Cancel context.CancelFunc Cancel context.CancelFunc
createdApps map[string]bool
} }
func RandStringBytes(n int) string { func RandStringBytes(n int) string {
@@ -119,9 +120,10 @@ func RandStringBytes(n int) string {
return strings.ToLower(string(b)) return strings.ToLower(string(b))
} }
func SetupDefaultSuite() *SuiteSetup { // SetupHarness creates a test harness for a test case - this picks up external options and
func SetupHarness() *TestHarness {
ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 60*time.Second)
ss := &SuiteSetup{ ss := &TestHarness{
Context: ctx, Context: ctx,
Client: APIClient(), Client: APIClient(),
AppName: "fnintegrationtestapp" + RandStringBytes(10), AppName: "fnintegrationtestapp" + RandStringBytes(10),
@@ -135,6 +137,7 @@ func SetupDefaultSuite() *SuiteSetup {
Memory: uint64(256), Memory: uint64(256),
Timeout: int32(30), Timeout: int32(30),
IdleTimeout: int32(30), IdleTimeout: int32(30),
createdApps: make(map[string]bool),
} }
if Host() != "localhost:8080" { if Host() != "localhost:8080" {
@@ -153,18 +156,16 @@ func SetupDefaultSuite() *SuiteSetup {
return ss return ss
} }
func Cleanup() { func (s *TestHarness) Cleanup() {
ctx := context.Background() ctx := context.Background()
c := APIClient()
approutesLock.Lock() //for _,ar := range s.createdRoutes {
defer approutesLock.Unlock() // deleteRoute(ctx, s.Client, ar.appName, ar.routeName)
for appName, rs := range appsandroutes { //}
for _, routePath := range rs {
deleteRoute(ctx, c, appName, routePath) for app, _ := range s.createdApps {
safeDeleteApp(ctx, s.Client, app)
} }
DeleteAppNoT(ctx, c, appName)
}
appsandroutes = make(map[string][]string)
} }
func EnvAsHeader(req *http.Request, selectedEnv []string) { func EnvAsHeader(req *http.Request, selectedEnv []string) {
@@ -214,20 +215,6 @@ func init() {
rand.Seed(time.Now().UnixNano()) rand.Seed(time.Now().UnixNano())
} }
func MyCaller() string {
fpcs := make([]uintptr, 1)
n := runtime.Callers(3, fpcs)
if n == 0 {
return "n/a"
}
fun := runtime.FuncForPC(fpcs[0] - 1)
if fun == nil {
return "n/a"
}
f, l := fun.FileLine(fpcs[0] - 1)
return fmt.Sprintf("%s:%d", f, l)
}
func APICallWithRetry(t *testing.T, attempts int, sleep time.Duration, callback func() error) (err error) { func APICallWithRetry(t *testing.T, attempts int, sleep time.Duration, callback func() error) (err error) {
for i := 0; i < attempts; i++ { for i := 0; i < attempts; i++ {
err = callback() err = callback()

View File

@@ -10,6 +10,7 @@ import (
"testing" "testing"
apiutils "github.com/fnproject/fn/test/fn-api-tests" apiutils "github.com/fnproject/fn/test/fn-api-tests"
"github.com/fnproject/fn_go/models"
) )
func LB() (string, error) { func LB() (string, error) {
@@ -23,10 +24,14 @@ func LB() (string, error) {
} }
func TestCanExecuteFunction(t *testing.T) { func TestCanExecuteFunction(t *testing.T) {
s := apiutils.SetupDefaultSuite() s := apiutils.SetupHarness()
apiutils.CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{}) s.GivenAppExists(t, &models.App{Name: s.AppName})
apiutils.CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, "sync", defer s.Cleanup()
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders)
rt := s.BasicRoute()
rt.Type = "sync"
s.GivenRouteExists(t, s.AppName, rt)
lb, err := LB() lb, err := LB()
if err != nil { if err != nil {
@@ -48,14 +53,18 @@ func TestCanExecuteFunction(t *testing.T) {
if !strings.Contains(expectedOutput, output.String()) { if !strings.Contains(expectedOutput, output.String()) {
t.Errorf("Assertion error.\n\tExpected: %v\n\tActual: %v", expectedOutput, output.String()) t.Errorf("Assertion error.\n\tExpected: %v\n\tActual: %v", expectedOutput, output.String())
} }
apiutils.DeleteApp(t, s.Context, s.Client, s.AppName)
} }
func TestBasicConcurrentExecution(t *testing.T) { func TestBasicConcurrentExecution(t *testing.T) {
s := apiutils.SetupDefaultSuite() s := apiutils.SetupHarness()
apiutils.CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{})
apiutils.CreateRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, "sync", s.GivenAppExists(t, &models.App{Name: s.AppName})
s.Format, s.Timeout, s.IdleTimeout, s.RouteConfig, s.RouteHeaders) defer s.Cleanup()
rt := s.BasicRoute()
rt.Type = "sync"
s.GivenRouteExists(t, s.AppName, rt)
lb, err := LB() lb, err := LB()
if err != nil { if err != nil {
@@ -93,5 +102,4 @@ func TestBasicConcurrentExecution(t *testing.T) {
} }
} }
apiutils.DeleteApp(t, s.Context, s.Client, s.AppName)
} }

View File

@@ -1 +1 @@
0.2.4 0.2.6

View File

@@ -17,7 +17,10 @@ import (
// swagger:model App // swagger:model App
type App struct { type App struct {
// Application configuration, applied to all routes. // Application annotations - this is a map of annotations attached to this app, keys must not exceed 128 bytes and must consist of non-whitespace printable ascii characters, and the seralized representation of individual values must not exeed 512 bytes
Annotations map[string]interface{} `json:"annotations,omitempty"`
// Application function configuration, applied to all routes.
Config map[string]string `json:"config,omitempty"` Config map[string]string `json:"config,omitempty"`
// Time when app was created. Always in UTC. // Time when app was created. Always in UTC.

View File

@@ -19,6 +19,9 @@ import (
// swagger:model Route // swagger:model Route
type Route struct { type Route struct {
// Route annotations - this is a map of annotations attached to this route, keys must not exceed 128 bytes and must consist of non-whitespace printable ascii characters, and the seralized representation of individual values must not exeed 512 bytes
Annotations map[string]interface{} `json:"annotations,omitempty"`
// Route configuration - overrides application configuration // Route configuration - overrides application configuration
Config map[string]string `json:"config,omitempty"` Config map[string]string `json:"config,omitempty"`