mirror of
https://github.com/fnproject/fn.git
synced 2022-10-28 21:29:17 +03:00
255 lines
5.9 KiB
Go
255 lines
5.9 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
|
|
}
|
|
|
|
// GetString returns a string value if the annotation value is a string, otherwise an error
|
|
func (m Annotations) GetString(key string) (string, error) {
|
|
if v, ok := m[key]; ok {
|
|
var s string
|
|
if err := json.Unmarshal([]byte(*v), &s); err != nil {
|
|
return "", err
|
|
}
|
|
return s, nil
|
|
}
|
|
return "", errors.New("Annotation not found")
|
|
}
|
|
|
|
// 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)
|
|
}
|