mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
* Don't try to delete an app that wasn't successfully created in the case of failure * Allow datastore implementations to inject additional annotations on objects * Allow for datastores transparently adding annotations on apps, fns and triggers. Change NameIn filter to Name for apps. * Move *List types including JSON annotations for App, Fn and Trigger into models * Change return types for GetApps, GetFns and GetTriggers on datastore to be models.*List and ove cursor generation into datastore * Trigger cursor handling fixed into db layer Also changes the name generation so that it is not in the same order as the id (well is random), this means we are now testing our name ordering. * GetFns now respects cursors * Apps now feeds cursor back * Mock fixes * Fixing up api level cursor decoding * Tidy up treatment of cursors in the db layer * Adding conditions for non nil items lists * fix mock test
243 lines
5.6 KiB
Go
243 lines
5.6 KiB
Go
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
|
|
}
|
|
return m.Subset(other)
|
|
}
|
|
|
|
func (m Annotations) Subset(other Annotations) bool {
|
|
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)
|
|
}
|