diff --git a/nmpolicy/internal/resolver/errors.go b/nmpolicy/internal/resolver/errors.go index 181bd263..34cf4bba 100644 --- a/nmpolicy/internal/resolver/errors.go +++ b/nmpolicy/internal/resolver/errors.go @@ -29,3 +29,11 @@ func wrapWithResolveError(err error) error { func wrapWithEqFilterError(err error) error { return fmt.Errorf("eqfilter error: %v", err) } + +func replaceError(format string, a ...interface{}) error { + return wrapWithReplaceError(fmt.Errorf(format, a...)) +} + +func wrapWithReplaceError(err error) error { + return fmt.Errorf("replace error: %v", err) +} diff --git a/nmpolicy/internal/resolver/filter.go b/nmpolicy/internal/resolver/filter.go index aabaa1dc..3289a0ee 100644 --- a/nmpolicy/internal/resolver/filter.go +++ b/nmpolicy/internal/resolver/filter.go @@ -23,7 +23,7 @@ import ( ) func filter(inputState map[string]interface{}, path ast.VariadicOperator, expectedNode ast.Node) (map[string]interface{}, error) { - filtered, err := applyFuncOnPath(inputState, path, expectedNode, mapContainsValue, true) + filtered, err := applyFuncOnMap(path, inputState, expectedNode, mapContainsValue, true, true) if err != nil { return nil, fmt.Errorf("failed applying operation on the path: %v", err) diff --git a/nmpolicy/internal/resolver/path.go b/nmpolicy/internal/resolver/path.go index e7b19290..b3b6bdd2 100644 --- a/nmpolicy/internal/resolver/path.go +++ b/nmpolicy/internal/resolver/path.go @@ -32,7 +32,7 @@ func applyFuncOnPath(inputState interface{}, path []ast.Node, expectedNode ast.Node, funcToApply func(map[string]interface{}, string, ast.Node) (interface{}, error), - shouldFilterSlice bool) (interface{}, error) { + shouldFilterSlice bool, shouldFilterMap bool) (interface{}, error) { if len(path) == 0 { return inputState, nil } @@ -41,7 +41,7 @@ func applyFuncOnPath(inputState interface{}, if len(path) == 1 { return applyFuncOnLastMapOnPath(path, originalMap, expectedNode, inputState, funcToApply) } - return applyFuncOnMap(path, originalMap, expectedNode, funcToApply, shouldFilterSlice) + return applyFuncOnMap(path, originalMap, expectedNode, funcToApply, shouldFilterSlice, shouldFilterMap) } originalSlice, isSlice := inputState.([]interface{}) @@ -57,32 +57,33 @@ func applyFuncOnSlice(originalSlice []interface{}, expectedNode ast.Node, funcToApply func(map[string]interface{}, string, ast.Node) (interface{}, error), shouldFilterSlice bool) (interface{}, error) { - filteredSlice := []interface{}{} + adjustedSlice := []interface{}{} + sliceEmptyAfterApply := true for _, valueToCheck := range originalSlice { - value, err := applyFuncOnPath(valueToCheck, path, expectedNode, funcToApply, false) + valueAfterApply, err := applyFuncOnPath(valueToCheck, path, expectedNode, funcToApply, false, false) if err != nil { return nil, err } - if value != nil { - filteredSlice = append(filteredSlice, valueToCheck) + if valueAfterApply != nil { + sliceEmptyAfterApply = false + adjustedSlice = append(adjustedSlice, valueAfterApply) + } else if !shouldFilterSlice { + adjustedSlice = append(adjustedSlice, valueToCheck) } } - if len(filteredSlice) == 0 { + if sliceEmptyAfterApply { return nil, nil } - if shouldFilterSlice { - return filteredSlice, nil - } - return originalSlice, nil + return adjustedSlice, nil } func applyFuncOnMap(path []ast.Node, originalMap map[string]interface{}, expectedNode ast.Node, funcToApply func(map[string]interface{}, string, ast.Node) (interface{}, error), - shouldFilterSlice bool) (interface{}, error) { + shouldFilterSlice bool, shouldFilterMap bool) (interface{}, error) { currentStep := path[0] if currentStep.Identity == nil { return nil, pathError("%v has unsupported fromat", currentStep) @@ -96,14 +97,22 @@ func applyFuncOnMap(path []ast.Node, return nil, pathError("cannot find key %s in %v", key, originalMap) } - adjuctedValue, err := applyFuncOnPath(value, nextPath, expectedNode, funcToApply, shouldFilterSlice) + adjustedValue, err := applyFuncOnPath(value, nextPath, expectedNode, funcToApply, shouldFilterSlice, shouldFilterMap) if err != nil { return nil, err } - if adjuctedValue != nil { - return map[string]interface{}{key: adjuctedValue}, nil + if adjustedValue == nil { + return nil, nil + } + + adjustedMap := map[string]interface{}{} + if !shouldFilterMap { + for k, v := range originalMap { + adjustedMap[k] = v + } } - return nil, nil + adjustedMap[key] = adjustedValue + return adjustedMap, nil } func applyFuncOnLastMapOnPath(path []ast.Node, diff --git a/nmpolicy/internal/resolver/replace.go b/nmpolicy/internal/resolver/replace.go new file mode 100644 index 00000000..f6e81af7 --- /dev/null +++ b/nmpolicy/internal/resolver/replace.go @@ -0,0 +1,48 @@ +/* + * Copyright 2021 NMPolicy 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 resolver + +import ( + "github.com/nmstate/nmpolicy/nmpolicy/internal/ast" +) + +func replace(inputState map[string]interface{}, path ast.VariadicOperator, expectedNode ast.Node) (map[string]interface{}, error) { + replaced, err := applyFuncOnMap(path, inputState, expectedNode, replaceMapFieldValue, false, false) + + if err != nil { + return nil, replaceError("failed applying operation on the path: %v", err) + } + + replacedMap, ok := replaced.(map[string]interface{}) + if !ok { + return nil, replaceError("failed converting result to a map") + } + return replacedMap, nil +} + +func replaceMapFieldValue(inputMap map[string]interface{}, mapEntryKeyToReplace string, expectedNode ast.Node) (interface{}, error) { + if expectedNode.String == nil { + return false, replaceError("the desired value %+v is not supported. Curretly only string values are supported", expectedNode) + } + modifiedMap := map[string]interface{}{} + for k, v := range inputMap { + modifiedMap[k] = v + } + + modifiedMap[mapEntryKeyToReplace] = *expectedNode.String + return modifiedMap, nil +} diff --git a/nmpolicy/internal/resolver/resolver.go b/nmpolicy/internal/resolver/resolver.go index 92a2a061..7082a90c 100644 --- a/nmpolicy/internal/resolver/resolver.go +++ b/nmpolicy/internal/resolver/resolver.go @@ -93,41 +93,72 @@ func (r *resolver) resolveCaptureEntryName(captureEntryName string) (types.NMSta func (r resolver) resolveCaptureASTEntry(captureASTEntry ast.Node) (types.NMState, error) { if captureASTEntry.EqFilter != nil { return r.resolveEqFilter(captureASTEntry.EqFilter) + } else if captureASTEntry.Replace != nil { + return r.resolveReplace(captureASTEntry.Replace) } - return nil, fmt.Errorf("root node has unsupported operation : %v", captureASTEntry) + return nil, fmt.Errorf("root node has unsupported operation : %+v", captureASTEntry) } func (r resolver) resolveEqFilter(operator *ast.TernaryOperator) (types.NMState, error) { - inputSource, err := r.resolveInputSource((*operator)[0], r.currentState) + filteredState, err := r.resolveTernaryOperator(operator, filter) if err != nil { return nil, wrapWithEqFilterError(err) } + return filteredState, nil +} + +func (r resolver) resolveReplace(operator *ast.TernaryOperator) (types.NMState, error) { + replacedState, err := r.resolveTernaryOperator(operator, replace) + if err != nil { + return nil, wrapWithResolveError(err) + } + return replacedState, nil +} + +func (r resolver) resolveTernaryOperator(operator *ast.TernaryOperator, + resolverFunc func(map[string]interface{}, ast.VariadicOperator, ast.Node) (map[string]interface{}, error)) (types.NMState, error) { + inputSource, err := r.resolveInputSource((*operator)[0]) + if err != nil { + return nil, err + } path, err := r.resolvePath((*operator)[1]) if err != nil { - return nil, wrapWithEqFilterError(err) + return nil, err } - filteredValue, err := r.resolveFilteredValue((*operator)[2]) + value, err := r.resolveStringOrCaptureEntryPath((*operator)[2]) if err != nil { - return nil, wrapWithEqFilterError(err) + return nil, err } - filteredState, err := filter(inputSource, path.steps, *filteredValue) + resolvedState, err := resolverFunc(inputSource, path.steps, *value) if err != nil { - return nil, wrapWithEqFilterError(err) + return nil, err } - return filteredState, nil + return resolvedState, nil } -func (r resolver) resolveInputSource(inputSourceNode ast.Node, - currentState types.NMState) (types.NMState, error) { +func (r resolver) resolveInputSource(inputSourceNode ast.Node) (types.NMState, error) { if ast.CurrentStateIdentity().DeepEqual(inputSourceNode.Terminal) { - return currentState, nil + return r.currentState, nil + } else if inputSourceNode.Path != nil { + resolvedPath, err := r.resolvePath(inputSourceNode) + if err != nil { + return nil, err + } + if resolvedPath.captureEntryName == "" { + return nil, fmt.Errorf("invalid path input source, only capture reference is supported") + } + capturedState, err := r.resolveCaptureEntryName(resolvedPath.captureEntryName) + if err != nil { + return nil, err + } + return capturedState, nil } - return nil, fmt.Errorf("not supported input source %v. Only the current state is supported", inputSourceNode) + return nil, fmt.Errorf("invalid input source %v, only current state or capture reference is supported", inputSourceNode) } -func (r resolver) resolveFilteredValue(filteredValueNode ast.Node) (*ast.Node, error) { +func (r resolver) resolveStringOrCaptureEntryPath(filteredValueNode ast.Node) (*ast.Node, error) { if filteredValueNode.String != nil { return &filteredValueNode, nil } else if filteredValueNode.Path != nil { @@ -143,7 +174,7 @@ func (r resolver) resolveFilteredValue(filteredValueNode ast.Node) (*ast.Node, e Terminal: *terminal, }, nil } else { - return nil, fmt.Errorf("not supported filtered value. Only string or paths are supported") + return nil, fmt.Errorf("not supported value. Only string or capture entry path are supported") } } diff --git a/nmpolicy/internal/resolver/resolver_test.go b/nmpolicy/internal/resolver/resolver_test.go index 4a58cfb5..8e880980 100644 --- a/nmpolicy/internal/resolver/resolver_test.go +++ b/nmpolicy/internal/resolver/resolver_test.go @@ -109,6 +109,8 @@ func TestFilter(t *testing.T) { testFilterInvalidTypeOnPath(t) testFilterInvalidPath(t) testFilterNonCaptureRefPathAtThirdArg(t) + testReplaceCurrentState(t) + testReplaceCapturedState(t) }) } @@ -708,3 +710,137 @@ base-iface-routes: runTest(t, testToRun) }) } + +func testReplaceCurrentState(t *testing.T) { + t.Run("Replace list of structs field from currentState with string value", func(t *testing.T) { + testToRun := test{ + captureASTPool: ` +bridge-routes: + pos: 1 + replace: + - pos: 2 + identity: currentState + - pos: 3 + path: + - pos: 4 + identity: routes + - pos: 5 + identity: running + - pos: 6 + identity: next-hop-interface + - pos: 7 + string: br1 +`, + + expectedCapturedStates: ` + +bridge-routes: + state: + routes: + running: + - destination: 0.0.0.0/0 + next-hop-address: 192.168.100.1 + next-hop-interface: br1 + table-id: 254 + - destination: 1.1.1.0/24 + next-hop-address: 192.168.100.1 + next-hop-interface: br1 + table-id: 254 + - destination: 2.2.2.0/24 + next-hop-address: 192.168.200.1 + next-hop-interface: br1 + table-id: 254 + config: + - destination: 0.0.0.0/0 + next-hop-address: 192.168.100.1 + next-hop-interface: eth1 + table-id: 254 + - destination: 1.1.1.0/24 + next-hop-address: 192.168.100.1 + next-hop-interface: eth1 + table-id: 254 + interfaces: + - name: eth1 + type: ethernet + state: up + ipv4: + address: + - ip: 10.244.0.1 + prefix-length: 24 + - ip: 169.254.1.0 + prefix-length: 16 + dhcp: false + enabled: true + - name: eth2 + type: ethernet + state: down + ipv4: + address: + - ip: 1.2.3.4 + prefix-length: 24 + dhcp: false + enabled: false +`, + } + runTest(t, testToRun) + }) +} + +func testReplaceCapturedState(t *testing.T) { + t.Run("Replace list of structs field from capture reference with string value", func(t *testing.T) { + testToRun := test{ + capturedStatesCache: ` +default-gw: + state: + routes: + running: + - destination: 0.0.0.0/0 + next-hop-address: 192.168.100.1 + next-hop-interface: eth1 + table-id: 254 +`, + + captureASTPool: ` +bridge-routes: + pos: 1 + replace: + - pos: 2 + path: + - pos: 3 + identity: capture + - pos: 4 + identity: default-gw + - pos: 3 + path: + - pos: 4 + identity: routes + - pos: 5 + identity: running + - pos: 6 + identity: next-hop-interface + - pos: 7 + string: br1 +`, + + expectedCapturedStates: ` +default-gw: + state: + routes: + running: + - destination: 0.0.0.0/0 + next-hop-address: 192.168.100.1 + next-hop-interface: eth1 + table-id: 254 +bridge-routes: + state: + routes: + running: + - destination: 0.0.0.0/0 + next-hop-address: 192.168.100.1 + next-hop-interface: br1 + table-id: 254 +`, + } + runTest(t, testToRun) + }) +}