crossplane-runtime/pkg/fieldpath/paved.go

613 lines
16 KiB
Go

/*
Copyright 2019 The Crossplane Authors.
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package fieldpath
import (
"strconv"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/util/json"
"github.com/crossplane/crossplane-runtime/pkg/errors"
)
// DefaultMaxFieldPathIndex is the max allowed index in a field path.
const DefaultMaxFieldPathIndex = 1024
type errNotFound struct {
error
}
func (e errNotFound) IsNotFound() bool {
return true
}
// IsNotFound returns true if the supplied error indicates a field path was not
// found, for example because a field did not exist within an object or an
// index was out of bounds in an array.
func IsNotFound(err error) bool {
cause := errors.Cause(err)
_, ok := cause.(interface {
IsNotFound() bool
})
return ok
}
// A Paved JSON object supports getting and setting values by their field path.
type Paved struct {
object map[string]any
maxFieldPathIndex uint
}
// PavedOption can be used to configure a Paved behavior.
type PavedOption func(paved *Paved)
// PaveObject paves a runtime.Object, making it possible to get and set values
// by field path. o must be a non-nil pointer to an object.
func PaveObject(o runtime.Object, opts ...PavedOption) (*Paved, error) {
u, err := runtime.DefaultUnstructuredConverter.ToUnstructured(o)
return Pave(u, opts...), errors.Wrap(err, "cannot convert object to unstructured data")
}
// Pave a JSON object, making it possible to get and set values by field path.
func Pave(object map[string]any, opts ...PavedOption) *Paved {
p := &Paved{object: object, maxFieldPathIndex: DefaultMaxFieldPathIndex}
for _, opt := range opts {
opt(p)
}
return p
}
// WithMaxFieldPathIndex returns a PavedOption that sets the max allowed index for field paths, 0 means no limit.
func WithMaxFieldPathIndex(max uint) PavedOption {
return func(paved *Paved) {
paved.maxFieldPathIndex = max
}
}
func (p *Paved) maxFieldPathIndexEnabled() bool {
return p.maxFieldPathIndex > 0
}
// MarshalJSON to the underlying object.
func (p Paved) MarshalJSON() ([]byte, error) {
return json.Marshal(p.object)
}
// UnmarshalJSON from the underlying object.
func (p *Paved) UnmarshalJSON(data []byte) error {
return json.Unmarshal(data, &p.object)
}
// UnstructuredContent returns the JSON serialisable content of this Paved.
func (p *Paved) UnstructuredContent() map[string]any {
if p.object == nil {
return make(map[string]any)
}
return p.object
}
// SetUnstructuredContent sets the JSON serialisable content of this Paved.
func (p *Paved) SetUnstructuredContent(content map[string]any) {
p.object = content
}
func (p *Paved) getValue(s Segments) (any, error) {
return getValueFromInterface(p.object, s)
}
func getValueFromInterface(it any, s Segments) (any, error) {
for i, current := range s {
final := i == len(s)-1
switch current.Type {
case SegmentIndex:
array, ok := it.([]any)
if !ok {
return nil, errors.Errorf("%s: not an array", s[:i])
}
if int(current.Index) >= len(array) {
return nil, errNotFound{errors.Errorf("%s: no such element", s[:i+1])}
}
if final {
return array[current.Index], nil
}
it = array[current.Index]
case SegmentField:
switch object := it.(type) {
case map[string]any:
v, ok := object[current.Field]
if !ok {
return nil, errNotFound{errors.Errorf("%s: no such field", s[:i+1])}
}
if final {
return v, nil
}
it = object[current.Field]
case nil:
return nil, errNotFound{errors.Errorf("%s: expected map, got nil", s[:i])}
default:
return nil, errors.Errorf("%s: not an object", s[:i])
}
}
}
// This should be unreachable.
return nil, nil
}
// ExpandWildcards expands wildcards for a given field path. It returns an
// array of field paths with expanded values. Please note that expanded paths
// depend on the input data which is paved.object.
//
// Example:
//
// For a Paved object with the following data: []byte(`{"spec":{"containers":[{"name":"cool", "image": "latest", "args": ["start", "now", "debug"]}]}}`),
// ExpandWildcards("spec.containers[*].args[*]") returns:
// []string{"spec.containers[0].args[0]", "spec.containers[0].args[1]", "spec.containers[0].args[2]"},.
func (p *Paved) ExpandWildcards(path string) ([]string, error) {
segments, err := Parse(path)
if err != nil {
return nil, errors.Wrapf(err, "cannot parse path %q", path)
}
segmentsArray, err := expandWildcards(p.object, segments)
if err != nil {
return nil, errors.Wrapf(err, "cannot expand wildcards for segments: %q", segments)
}
paths := make([]string, len(segmentsArray))
for i, s := range segmentsArray {
paths[i] = s.String()
}
return paths, nil
}
func expandWildcards(data any, segments Segments) ([]Segments, error) { //nolint:gocognit // See note below.
// Even complexity turns out to be high, it is mostly because we have duplicate
// logic for arrays and maps and a couple of error handling.
var res []Segments
it := data
for i, current := range segments {
// wildcards are regular fields with "*" as string
if current.Type == SegmentField && current.Field == wildcard {
switch mapOrArray := it.(type) {
case []any:
for ix := range mapOrArray {
expanded := make(Segments, len(segments))
copy(expanded, segments)
expanded = append(append(expanded[:i], FieldOrIndex(strconv.Itoa(ix))), expanded[i+1:]...)
r, err := expandWildcards(data, expanded)
if err != nil {
return nil, errors.Wrapf(err, "%q: cannot expand wildcards", expanded)
}
res = append(res, r...)
}
case map[string]any:
for k := range mapOrArray {
expanded := make(Segments, len(segments))
copy(expanded, segments)
expanded = append(append(expanded[:i], Field(k)), expanded[i+1:]...)
r, err := expandWildcards(data, expanded)
if err != nil {
return nil, errors.Wrapf(err, "%q: cannot expand wildcards", expanded)
}
res = append(res, r...)
}
case nil:
return nil, errNotFound{errors.Errorf("wildcard field %q is not found in the path", segments[:i])}
default:
return nil, errors.Errorf("%q: unexpected wildcard usage", segments[:i])
}
return res, nil
}
var err error
it, err = getValueFromInterface(data, segments[:i+1])
if IsNotFound(err) {
return nil, nil
}
if err != nil {
return nil, err
}
}
return append(res, segments), nil
}
// GetValue of the supplied field path.
func (p *Paved) GetValue(path string) (any, error) {
segments, err := Parse(path)
if err != nil {
return nil, errors.Wrapf(err, "cannot parse path %q", path)
}
return p.getValue(segments)
}
// GetValueInto the supplied type.
func (p *Paved) GetValueInto(path string, out any) error {
val, err := p.GetValue(path)
if err != nil {
return err
}
js, err := json.Marshal(val)
if err != nil {
return errors.Wrap(err, "cannot marshal value to JSON")
}
return errors.Wrap(json.Unmarshal(js, out), "cannot unmarshal value from JSON")
}
// GetString value of the supplied field path.
func (p *Paved) GetString(path string) (string, error) {
v, err := p.GetValue(path)
if err != nil {
return "", err
}
s, ok := v.(string)
if !ok {
return "", errors.Errorf("%s: not a string", path)
}
return s, nil
}
// GetStringArray value of the supplied field path.
func (p *Paved) GetStringArray(path string) ([]string, error) {
v, err := p.GetValue(path)
if err != nil {
return nil, err
}
a, ok := v.([]any)
if !ok {
return nil, errors.Errorf("%s: not an array", path)
}
sa := make([]string, len(a))
for i := range a {
s, ok := a[i].(string)
if !ok {
return nil, errors.Errorf("%s: not an array of strings", path)
}
sa[i] = s
}
return sa, nil
}
// GetStringObject value of the supplied field path.
func (p *Paved) GetStringObject(path string) (map[string]string, error) {
v, err := p.GetValue(path)
if err != nil {
return nil, err
}
o, ok := v.(map[string]any)
if !ok {
return nil, errors.Errorf("%s: not an object", path)
}
so := make(map[string]string)
for k, in := range o {
s, ok := in.(string)
if !ok {
return nil, errors.Errorf("%s: not an object with string field values", path)
}
so[k] = s
}
return so, nil
}
// GetBool value of the supplied field path.
func (p *Paved) GetBool(path string) (bool, error) {
v, err := p.GetValue(path)
if err != nil {
return false, err
}
b, ok := v.(bool)
if !ok {
return false, errors.Errorf("%s: not a bool", path)
}
return b, nil
}
// GetInteger value of the supplied field path.
func (p *Paved) GetInteger(path string) (int64, error) {
v, err := p.GetValue(path)
if err != nil {
return 0, err
}
f, ok := v.(int64)
if !ok {
return 0, errors.Errorf("%s: not a (int64) number", path)
}
return f, nil
}
func (p *Paved) setValue(s Segments, value any) error {
// We expect p.object to look like JSON data that was unmarshalled into an
// any per https://golang.org/pkg/encoding/json/#Unmarshal. We
// marshal our value to JSON and unmarshal it into an any to ensure
// it meets these criteria before setting it within p.object.
v, err := toValidJSON(value)
if err != nil {
return err
}
if err := p.validateSegments(s); err != nil {
return err
}
var in any = p.object
for i, current := range s {
final := i == len(s)-1
switch current.Type {
case SegmentIndex:
array, ok := in.([]any)
if !ok {
return errors.Errorf("%s is not an array", s[:i])
}
if final {
array[current.Index] = v
return nil
}
prepareElement(array, current, s[i+1])
in = array[current.Index]
case SegmentField:
object, ok := in.(map[string]any)
if !ok {
return errors.Errorf("%s is not an object", s[:i])
}
if final {
object[current.Field] = v
return nil
}
prepareField(object, current, s[i+1])
in = object[current.Field]
}
}
return nil
}
func toValidJSON(value any) (any, error) {
var v any
j, err := json.Marshal(value)
if err != nil {
return nil, errors.Wrap(err, "cannot marshal value to JSON")
}
if err := json.Unmarshal(j, &v); err != nil {
return nil, errors.Wrap(err, "cannot unmarshal value from JSON")
}
return v, nil
}
func prepareElement(array []any, current, next Segment) {
// If this segment is not the final one and doesn't exist we need to
// create it for our next segment.
if array[current.Index] == nil {
switch next.Type {
case SegmentIndex:
array[current.Index] = make([]any, next.Index+1)
case SegmentField:
array[current.Index] = make(map[string]any)
}
return
}
// If our next segment indexes an array that exists in our current segment's
// element we must ensure the array is long enough to set the next segment.
if next.Type != SegmentIndex {
return
}
na, ok := array[current.Index].([]any)
if !ok {
return
}
if int(next.Index) < len(na) {
return
}
array[current.Index] = append(na, make([]any, int(next.Index)-len(na)+1)...)
}
func prepareField(object map[string]any, current, next Segment) {
// If this segment is not the final one and doesn't exist we need to
// create it for our next segment.
if _, ok := object[current.Field]; !ok {
switch next.Type {
case SegmentIndex:
object[current.Field] = make([]any, next.Index+1)
case SegmentField:
object[current.Field] = make(map[string]any)
}
return
}
// If our next segment indexes an array that exists in our current segment's
// field we must ensure the array is long enough to set the next segment.
if next.Type != SegmentIndex {
return
}
na, ok := object[current.Field].([]any)
if !ok {
return
}
if int(next.Index) < len(na) {
return
}
object[current.Field] = append(na, make([]any, int(next.Index)-len(na)+1)...)
}
// SetValue at the supplied field path.
func (p *Paved) SetValue(path string, value any) error {
segments, err := Parse(path)
if err != nil {
return errors.Wrapf(err, "cannot parse path %q", path)
}
return p.setValue(segments, value)
}
func (p *Paved) validateSegments(s Segments) error {
if !p.maxFieldPathIndexEnabled() {
return nil
}
for _, segment := range s {
if segment.Type == SegmentIndex && segment.Index > p.maxFieldPathIndex {
return errors.Errorf("index %v is greater than max allowed index %d", segment.Index, p.maxFieldPathIndex)
}
}
return nil
}
// SetString value at the supplied field path.
func (p *Paved) SetString(path, value string) error {
return p.SetValue(path, value)
}
// SetBool value at the supplied field path.
func (p *Paved) SetBool(path string, value bool) error {
return p.SetValue(path, value)
}
// SetNumber value at the supplied field path.
func (p *Paved) SetNumber(path string, value float64) error {
return p.SetValue(path, value)
}
// DeleteField deletes the field from the object.
// If the path points to an entry in an array, the element
// on that index is removed and the next ones are pulled
// back. If it is a field on a map, the field is
// removed from the map.
func (p *Paved) DeleteField(path string) error {
segments, err := Parse(path)
if err != nil {
return errors.Wrapf(err, "cannot parse path %q", path)
}
return p.delete(segments)
}
func (p *Paved) delete(segments Segments) error { //nolint:gocognit // See note below.
// NOTE(muvaf): I could not reduce the cyclomatic complexity
// more than that without disturbing the reading flow.
if len(segments) == 1 {
o, err := deleteField(p.object, segments[0])
if err != nil {
return errors.Wrapf(err, "cannot delete %s", segments)
}
p.object = o.(map[string]any) //nolint:forcetypeassert // We're deleting from the root of the paved object, which is always a map[string]any.
return nil
}
var in any = p.object
for i, current := range segments {
// beforeLast is true for the element before the last one because
// slices cannot be changed in place and Go does not allow
// taking address of map elements which prevents us from
// assigning a new array for that entry unless we have the
// map available in the context, which is achieved by iterating
// until the element before the last one as opposed to
// Set/Get functions in this file.
beforeLast := i == len(segments)-2
switch current.Type {
case SegmentIndex:
array, ok := in.([]any)
if !ok {
return errors.Errorf("%s is not an array", segments[:i])
}
// It doesn't exist anyway.
if len(array) <= int(current.Index) {
return nil
}
if beforeLast {
o, err := deleteField(array[current.Index], segments[len(segments)-1])
if err != nil {
return errors.Wrapf(err, "cannot delete %s", segments)
}
array[current.Index] = o
return nil
}
in = array[current.Index]
case SegmentField:
object, ok := in.(map[string]any)
if !ok {
return errors.Errorf("%s is not an object", segments[:i])
}
// It doesn't exist anyway.
if _, ok := object[current.Field]; !ok {
return nil
}
if beforeLast {
o, err := deleteField(object[current.Field], segments[len(segments)-1])
if err != nil {
return errors.Wrapf(err, "cannot delete %s", segments)
}
object[current.Field] = o
return nil
}
in = object[current.Field]
}
}
return nil
}
// deleteField deletes the object in obj pointed by
// the given Segment and returns it. Returned object
// may or may not have the same address in memory.
func deleteField(obj any, s Segment) (any, error) {
switch s.Type {
case SegmentIndex:
array, ok := obj.([]any)
if !ok {
return nil, errors.New("not an array")
}
if len(array) == 0 || len(array) <= int(s.Index) {
return array, nil
}
for i := int(s.Index); i < len(array)-1; i++ {
array[i] = array[i+1]
}
return array[:len(array)-1], nil
case SegmentField:
object, ok := obj.(map[string]any)
if !ok {
return nil, errors.New("not an object")
}
delete(object, s.Field)
return object, nil
}
return nil, nil
}