mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
Add annotations to routes and apps (#866)
Adds 'annotations' attribute to Routes and Apps
This commit is contained in:
6
Gopkg.lock
generated
6
Gopkg.lock
generated
@@ -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
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
27
api/datastore/sql/migrations/8_add_annotations_app.go
Normal file
27
api/datastore/sql/migrations/8_add_annotations_app.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
27
api/datastore/sql/migrations/9_add_annotations_route.go
Normal file
27
api/datastore/sql/migrations/9_add_annotations_route.go
Normal 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,
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -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
238
api/models/annotations.go
Normal 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)
|
||||||
|
}
|
||||||
261
api/models/annotations_test.go
Normal file
261
api/models/annotations_test.go
Normal 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
151
docs/contributors/annotations.md
Normal file
151
docs/contributors/annotations.md
Normal 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.
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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?
|
||||||
|
|||||||
84
test/fn-api-tests/annotation_cases.go
Normal file
84
test/fn-api-tests/annotation_cases.go
Normal 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)
|
||||||
|
}
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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})
|
||||||
if !strings.Contains("a", val) {
|
|
||||||
t.Errorf("App config value is different. Expected: `a`. Actual %v", val)
|
appOk, err := s.Client.Apps.GetAppsApp(&apps.GetAppsAppParams{
|
||||||
}
|
App: s.AppName,
|
||||||
DeleteApp(t, s.Context, s.Client, s.AppName)
|
Context: s.Context,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Expected valid response to get app, got %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppPatchSameConfig(t *testing.T) {
|
if !reflect.DeepEqual(validConfig, appOk.Payload.App.Config) {
|
||||||
|
t.Errorf("Returned config %v does not match requested config %v", appOk.Payload.App.Config, validConfig)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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",
|
|
||||||
}
|
s.GivenAppExists(t, &models.App{
|
||||||
appPayload := CreateUpdateApp(t, s.Context, s.Client, s.AppName, config)
|
Name: s.AppName,
|
||||||
val, ok := appPayload.Payload.App.Config["A"]
|
Config: tc.intialConfig,
|
||||||
if !ok {
|
})
|
||||||
t.Error("Error during app config inspect: config map misses required entity `A` with value `a`.")
|
|
||||||
}
|
patch, err := s.Client.Apps.PatchAppsApp(&apps.PatchAppsAppParams{
|
||||||
if !strings.Contains("b", val) {
|
App: s.AppName,
|
||||||
t.Errorf("App config value is different. Expected: `b`. Actual %v", val)
|
Body: &models.AppWrapper{
|
||||||
}
|
App: &models.App{
|
||||||
DeleteApp(t, s.Context, s.Client, s.AppName)
|
Config: tc.change,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Context: s.Context,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Failed to patch app with valid value %v, %v", tc.change, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAppsPatchConfigAddValue(t *testing.T) {
|
if !ConfigEquivalent(patch.Payload.App.Config, tc.expected) {
|
||||||
t.Parallel()
|
t.Errorf("Expected returned app config to be %v, but was %v", tc.expected, patch.Payload.App.Config)
|
||||||
s := SetupDefaultSuite()
|
|
||||||
config := map[string]string{
|
|
||||||
"B": "b",
|
|
||||||
}
|
}
|
||||||
appPayload := CreateUpdateApp(t, s.Context, s.Client, s.AppName, config)
|
|
||||||
val, ok := appPayload.Payload.App.Config["B"]
|
})
|
||||||
if !ok {
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
29
test/fn-api-tests/config_cases.go
Normal file
29
test/fn-api-tests/config_cases.go
Normal 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)
|
||||||
|
|
||||||
|
}
|
||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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, "😀 👍 🎗")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
update *models.Route
|
||||||
|
extract func(*models.Route) interface{}
|
||||||
|
}{
|
||||||
|
{"route type (sync)", &models.Route{Type: "sync"}, func(m *models.Route) interface{} { return m.Type }},
|
||||||
|
{"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 }},
|
||||||
|
{"format (default)", &models.Route{Format: "default"}, func(m *models.Route) interface{} { return m.Format }},
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
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)
|
|
||||||
|
|
||||||
routeResp, err := UpdateRoute(
|
for _, tci := range validRouteUpdates {
|
||||||
t, s.Context, s.Client,
|
tc := tci
|
||||||
s.AppName, s.RoutePath,
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
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) {
|
|
||||||
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)
|
|
||||||
|
|
||||||
newRouteConf := map[string]string{
|
s.GivenAppExists(t, &models.App{Name: s.AppName})
|
||||||
"A": "a",
|
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())
|
||||||
|
|
||||||
|
_, err := s.Client.Routes.DeleteAppsAppRoutesRoute(&routes.DeleteAppsAppRoutesRouteParams{
|
||||||
|
App: s.AppName,
|
||||||
|
Route: s.RoutePath,
|
||||||
|
Context: s.Context,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("Expected success when deleting existing route, got %v", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCantDeleteRoute(t *testing.T) {
|
func TestCantDeleteMissingRoute(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()
|
||||||
|
|
||||||
|
s.GivenAppExists(t, &models.App{Name: s.AppName})
|
||||||
|
|
||||||
|
_, err := s.Client.Routes.DeleteAppsAppRoutesRoute(&routes.DeleteAppsAppRoutesRouteParams{
|
||||||
|
App: s.AppName,
|
||||||
|
Route: s.RoutePath,
|
||||||
|
Context: s.Context,
|
||||||
|
})
|
||||||
|
|
||||||
_, err := deleteRoute(s.Context, s.Client, s.AppName, "dummy-route")
|
|
||||||
if err == nil {
|
if err == nil {
|
||||||
t.Error("Delete from missing route must fail.")
|
t.Fatalf("Expected error when deleting non-existing route, got none")
|
||||||
}
|
|
||||||
DeleteApp(t, s.Context, s.Client, s.AppName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeployNewApp(t *testing.T) {
|
if _, ok := err.(*routes.DeleteAppsAppRoutesRouteNotFound); !ok {
|
||||||
|
t.Fatalf("Expected not-found when deleting non-existing route, got %v", err)
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPutRouteCreatesNewApp(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)
|
_, 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)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDeployExistingApp(t *testing.T) {
|
s.AppMustExist(t, s.AppName)
|
||||||
s := SetupDefaultSuite()
|
s.RouteMustExist(t, s.AppName, s.RoutePath)
|
||||||
CreateApp(t, s.Context, s.Client, s.AppName, map[string]string{})
|
|
||||||
DeployRoute(t, s.Context, s.Client, s.AppName, s.RoutePath, s.Image, s.RouteType, s.Format, s.RouteConfig, s.RouteHeaders)
|
|
||||||
GetApp(t, s.Context, s.Client, s.AppName)
|
|
||||||
GetRoute(t, s.Context, s.Client, s.AppName, s.RoutePath)
|
|
||||||
DeleteApp(t, s.Context, s.Client, s.AppName)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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)
|
|
||||||
}
|
}
|
||||||
|
|||||||
2
vendor/github.com/fnproject/fn_go/VERSION
generated
vendored
2
vendor/github.com/fnproject/fn_go/VERSION
generated
vendored
@@ -1 +1 @@
|
|||||||
0.2.4
|
0.2.6
|
||||||
|
|||||||
5
vendor/github.com/fnproject/fn_go/models/app.go
generated
vendored
5
vendor/github.com/fnproject/fn_go/models/app.go
generated
vendored
@@ -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.
|
||||||
|
|||||||
3
vendor/github.com/fnproject/fn_go/models/route.go
generated
vendored
3
vendor/github.com/fnproject/fn_go/models/route.go
generated
vendored
@@ -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"`
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user