/* Copyright 2017 The Kubernetes 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 apply import ( "fmt" ) // Element contains the record, local, and remote value for a field in an object // and metadata about the field read from openapi. // Calling Merge on an element will apply the passed in strategy to Element - // e.g. either replacing the whole element with the local copy or merging each // of the recorded, local and remote fields of the element. type Element interface { // FieldMeta specifies which merge strategy to use for this element FieldMeta // Merge merges the recorded, local and remote values in the element using the Strategy // provided as an argument. Calls the type specific method on the Strategy - following the // "Accept" method from the "Visitor" pattern. // e.g. Merge on a ListElement will call Strategy.MergeList(self) // Returns the Result of the merged elements Merge(Strategy) (Result, error) // HasRecorded returns true if the field was explicitly // present in the recorded source. This is to differentiate between // undefined and set to null HasRecorded() bool // GetRecorded returns the field value from the recorded source of the object GetRecorded() interface{} // HasLocal returns true if the field was explicitly // present in the local source. This is to differentiate between // undefined and set to null HasLocal() bool // GetLocal returns the field value from the local source of the object GetLocal() interface{} // HasRemote returns true if the field was explicitly // present in the remote source. This is to differentiate between // undefined and set to null HasRemote() bool // GetRemote returns the field value from the remote source of the object GetRemote() interface{} } // FieldMeta defines the strategy used to apply a Patch for an element type FieldMeta interface { // GetFieldMergeType returns the type of merge strategy to use for this field // maybe "merge", "replace" or "retainkeys" // TODO: There maybe multiple strategies, so this may need to be a slice, map, or struct // Address this in a follow up in the PR to introduce retainkeys strategy GetFieldMergeType() string // GetFieldMergeKeys returns the merge key to use when the MergeType is "merge" and underlying type is a list GetFieldMergeKeys() MergeKeys // GetFieldType returns the openapi field type - e.g. primitive, array, map, type, reference GetFieldType() string } // FieldMetaImpl implements FieldMeta type FieldMetaImpl struct { // MergeType is the type of merge strategy to use for this field // maybe "merge", "replace" or "retainkeys" MergeType string // MergeKeys are the merge keys to use when the MergeType is "merge" and underlying type is a list MergeKeys MergeKeys // Type is the openapi type of the field - "list", "primitive", "map" Type string // Name contains name of the field Name string } // GetFieldMergeType implements FieldMeta.GetFieldMergeType func (s FieldMetaImpl) GetFieldMergeType() string { return s.MergeType } // GetFieldMergeKeys implements FieldMeta.GetFieldMergeKeys func (s FieldMetaImpl) GetFieldMergeKeys() MergeKeys { return s.MergeKeys } // GetFieldType implements FieldMeta.GetFieldType func (s FieldMetaImpl) GetFieldType() string { return s.Type } // MergeKeyValue records the value of the mergekey for an item in a list type MergeKeyValue map[string]string // Equal returns true if the MergeKeyValues share the same value, // representing the same item in a list func (v MergeKeyValue) Equal(o MergeKeyValue) bool { if len(v) != len(o) { return false } for key, v1 := range v { if v2, found := o[key]; !found || v1 != v2 { return false } } return true } // MergeKeys is the set of fields on an object that uniquely identify // and is used when merging lists to identify the "same" object // independent of the ordering of the objects type MergeKeys []string // GetMergeKeyValue parses the MergeKeyValue from an item in a list func (mk MergeKeys) GetMergeKeyValue(i interface{}) (MergeKeyValue, error) { result := MergeKeyValue{} if len(mk) <= 0 { return result, fmt.Errorf("merge key must have at least 1 value to merge") } m, ok := i.(map[string]interface{}) if !ok { return result, fmt.Errorf("cannot use mergekey %v for primitive item in list %v", mk, i) } for _, field := range mk { if value, found := m[field]; !found { result[field] = "" } else { result[field] = fmt.Sprintf("%v", value) } } return result, nil } // CombinedPrimitiveSlice implements a slice of primitives type CombinedPrimitiveSlice struct { Items []*PrimitiveListItem } // PrimitiveListItem represents a single value in a slice of primitives type PrimitiveListItem struct { // Value is the value of the primitive, should match recorded, local and remote Value interface{} RawElementData } // Contains returns true if the slice contains the l func (s *CombinedPrimitiveSlice) lookup(l interface{}) *PrimitiveListItem { val := fmt.Sprintf("%v", l) for _, i := range s.Items { if fmt.Sprintf("%v", i.Value) == val { return i } } return nil } func (s *CombinedPrimitiveSlice) upsert(l interface{}) *PrimitiveListItem { // Return the item if it exists if item := s.lookup(l); item != nil { return item } // Otherwise create a new item and append to the list item := &PrimitiveListItem{ Value: l, } s.Items = append(s.Items, item) return item } // UpsertRecorded adds l to the slice. If there is already a value of l in the // slice for either the local or remote, set on that value as the recorded value // Otherwise append a new item to the list with the recorded value. func (s *CombinedPrimitiveSlice) UpsertRecorded(l interface{}) { v := s.upsert(l) v.recorded = l v.recordedSet = true } // UpsertLocal adds l to the slice. If there is already a value of l in the // slice for either the recorded or remote, set on that value as the local value // Otherwise append a new item to the list with the local value. func (s *CombinedPrimitiveSlice) UpsertLocal(l interface{}) { v := s.upsert(l) v.local = l v.localSet = true } // UpsertRemote adds l to the slice. If there is already a value of l in the // slice for either the local or recorded, set on that value as the remote value // Otherwise append a new item to the list with the remote value. func (s *CombinedPrimitiveSlice) UpsertRemote(l interface{}) { v := s.upsert(l) v.remote = l v.remoteSet = true } // ListItem represents a single value in a slice of maps or types type ListItem struct { // KeyValue is the merge key value of the item KeyValue MergeKeyValue // RawElementData contains the field values RawElementData } // CombinedMapSlice is a slice of maps or types with merge keys type CombinedMapSlice struct { Items []*ListItem } // Lookup returns the ListItem matching the merge key, or nil if not found. func (s *CombinedMapSlice) lookup(v MergeKeyValue) *ListItem { for _, i := range s.Items { if i.KeyValue.Equal(v) { return i } } return nil } func (s *CombinedMapSlice) upsert(key MergeKeys, l interface{}) (*ListItem, error) { // Get the identity of the item val, err := key.GetMergeKeyValue(l) if err != nil { return nil, err } // Return the item if it exists if item := s.lookup(val); item != nil { return item, nil } // Otherwise create a new item and append to the list item := &ListItem{ KeyValue: val, } s.Items = append(s.Items, item) return item, nil } // UpsertRecorded adds l to the slice. If there is already a value of l sharing // l's merge key in the slice for either the local or remote, set l the recorded value // Otherwise append a new item to the list with the recorded value. func (s *CombinedMapSlice) UpsertRecorded(key MergeKeys, l interface{}) error { item, err := s.upsert(key, l) if err != nil { return err } item.SetRecorded(l) return nil } // UpsertLocal adds l to the slice. If there is already a value of l sharing // l's merge key in the slice for either the recorded or remote, set l the local value // Otherwise append a new item to the list with the local value. func (s *CombinedMapSlice) UpsertLocal(key MergeKeys, l interface{}) error { item, err := s.upsert(key, l) if err != nil { return err } item.SetLocal(l) return nil } // UpsertRemote adds l to the slice. If there is already a value of l sharing // l's merge key in the slice for either the recorded or local, set l the remote value // Otherwise append a new item to the list with the remote value. func (s *CombinedMapSlice) UpsertRemote(key MergeKeys, l interface{}) error { item, err := s.upsert(key, l) if err != nil { return err } item.SetRemote(l) return nil } // IsDrop returns true if the field represented by e should be dropped from the merged object func IsDrop(e Element) bool { // Specified in the last value recorded value and since deleted from the local removed := e.HasRecorded() && !e.HasLocal() // Specified locally and explicitly set to null setToNil := e.HasLocal() && e.GetLocal() == nil return removed || setToNil } // IsAdd returns true if the field represented by e should have the local value directly // added to the merged object instead of merging the recorded, local and remote values func IsAdd(e Element) bool { // If it isn't already present in the remote value and is present in the local value return e.HasLocal() && !e.HasRemote() } // NewRawElementData returns a new RawElementData, setting IsSet to true for // non-nil values, and leaving IsSet false for nil values. // Note: use this only when you want a nil-value to be considered "unspecified" // (ignore) and not "unset" (deleted). func NewRawElementData(recorded, local, remote interface{}) RawElementData { data := RawElementData{} if recorded != nil { data.SetRecorded(recorded) } if local != nil { data.SetLocal(local) } if remote != nil { data.SetRemote(remote) } return data } // RawElementData contains the raw recorded, local and remote data // and metadata about whethere or not each was set type RawElementData struct { HasElementData recorded interface{} local interface{} remote interface{} } // SetRecorded sets the recorded value func (b *RawElementData) SetRecorded(value interface{}) { b.recorded = value b.recordedSet = true } // SetLocal sets the local value func (b *RawElementData) SetLocal(value interface{}) { b.local = value b.localSet = true } // SetRemote sets the remote value func (b *RawElementData) SetRemote(value interface{}) { b.remote = value b.remoteSet = true } // GetRecorded implements Element.GetRecorded func (b RawElementData) GetRecorded() interface{} { // https://golang.org/doc/faq#nil_error if b.recorded == nil { return nil } return b.recorded } // GetLocal implements Element.GetLocal func (b RawElementData) GetLocal() interface{} { // https://golang.org/doc/faq#nil_error if b.local == nil { return nil } return b.local } // GetRemote implements Element.GetRemote func (b RawElementData) GetRemote() interface{} { // https://golang.org/doc/faq#nil_error if b.remote == nil { return nil } return b.remote } // HasElementData contains whether a field was set in the recorded, local and remote sources type HasElementData struct { recordedSet bool localSet bool remoteSet bool } // HasRecorded implements Element.HasRecorded func (e HasElementData) HasRecorded() bool { return e.recordedSet } // HasLocal implements Element.HasLocal func (e HasElementData) HasLocal() bool { return e.localSet } // HasRemote implements Element.HasRemote func (e HasElementData) HasRemote() bool { return e.remoteSet } // ConflictDetector defines the capability to detect conflict. An element can examine remote/recorded value to detect conflict. type ConflictDetector interface { HasConflict() error }