From 47251a5248c6fb6c3fc6ff49bbbd797d984d54dd Mon Sep 17 00:00:00 2001 From: Enrique Llorente Pastora Date: Tue, 19 Jan 2021 14:04:46 +0100 Subject: [PATCH] Decrease NNS refresh frequency and remove dyn attr (#682) Trying to refresh NNS every 5 seconds makes nmstatectl to call python everytime consuming CPU resources, decreasing the frequency will reduce CPU consumption. Also increasing the refresh period to 1 minute make CI fail on timeout, to overcome that we force NNS refresh if a NNCP is applied. And filter gc-timer and hello-timer hey are already not reflecting reality since we remove them before compare them so the value that is present at NNS is not accurate, let's just remove them from NNS so we can use FilterOut to compare states too, so we don't compare filtered stuff like veth or calico. Proper solution is to use varlink so python is already started up at different container, this is done at #663. Signed-off-by: Quique Llorente --- controllers/labels.go | 5 + controllers/node_controller.go | 18 +- controllers/node_controller_test.go | 77 ++++- ...denetworkconfigurationpolicy_controller.go | 22 ++ controllers/nodenetworkstate_controller.go | 26 +- go.mod | 1 + go.sum | 1 + pkg/helper/client.go | 71 +--- pkg/helper/client_test.go | 117 ------- pkg/node/constants.go | 2 +- pkg/state/filter.go | 92 +++++- pkg/state/filter_test.go | 310 ++++++++++++++++++ .../state_suite_test.go} | 4 +- test/e2e/handler/nns_update_timestamp_test.go | 21 +- test/e2e/handler/nodes_test.go | 10 +- vendor/github.com/andreyvit/diff/.gitignore | 24 ++ vendor/github.com/andreyvit/diff/LICENSE | 21 ++ vendor/github.com/andreyvit/diff/README.md | 28 ++ vendor/github.com/andreyvit/diff/diff.go | 128 ++++++++ vendor/github.com/andreyvit/diff/doc.go | 2 + vendor/github.com/andreyvit/diff/trim.go | 19 ++ vendor/modules.txt | 3 + 22 files changed, 774 insertions(+), 228 deletions(-) create mode 100644 controllers/labels.go delete mode 100644 pkg/helper/client_test.go create mode 100644 pkg/state/filter_test.go rename pkg/{helper/helper_suite_test.go => state/state_suite_test.go} (73%) create mode 100644 vendor/github.com/andreyvit/diff/.gitignore create mode 100644 vendor/github.com/andreyvit/diff/LICENSE create mode 100644 vendor/github.com/andreyvit/diff/README.md create mode 100644 vendor/github.com/andreyvit/diff/diff.go create mode 100644 vendor/github.com/andreyvit/diff/doc.go create mode 100644 vendor/github.com/andreyvit/diff/trim.go diff --git a/controllers/labels.go b/controllers/labels.go new file mode 100644 index 0000000000..19cfc811a6 --- /dev/null +++ b/controllers/labels.go @@ -0,0 +1,5 @@ +package controllers + +const ( + forceRefreshLabel = "nmstate.io/force-nns-refresh" +) diff --git a/controllers/node_controller.go b/controllers/node_controller.go index 5b4f4cc2bf..aec0e1bef6 100644 --- a/controllers/node_controller.go +++ b/controllers/node_controller.go @@ -30,6 +30,7 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" + "github.com/nmstate/kubernetes-nmstate/api/shared" nmstate "github.com/nmstate/kubernetes-nmstate/pkg/helper" "github.com/nmstate/kubernetes-nmstate/pkg/nmstatectl" "github.com/nmstate/kubernetes-nmstate/pkg/node" @@ -38,7 +39,7 @@ import ( ) // Added for test purposes -type NmstateUpdater func(client client.Client, node *corev1.Node, namespace client.ObjectKey, observedStateRaw string) error +type NmstateUpdater func(client client.Client, node *corev1.Node, namespace client.ObjectKey, observedState shared.State) error type NmstatectlShow func() (string, error) // NodeReconciler reconciles a Node object @@ -46,7 +47,7 @@ type NodeReconciler struct { client.Client Log logr.Logger Scheme *runtime.Scheme - lastState string + lastState shared.State nmstateUpdater NmstateUpdater nmstatectlShow NmstatectlShow } @@ -57,14 +58,19 @@ type NodeReconciler struct { // The Controller will requeue the Request to be processed again if the returned error is non-nil or // Result.Requeue is true, otherwise upon completion it will remove the work from the queue. func (r *NodeReconciler) Reconcile(request ctrl.Request) (ctrl.Result, error) { - currentState, err := r.nmstatectlShow() + currentStateRaw, err := r.nmstatectlShow() if err != nil { // We cannot call nmstatectl show let's reconcile again return ctrl.Result{}, err } + currentState, err := state.FilterOut(shared.NewState(currentStateRaw)) + if err != nil { + return ctrl.Result{}, err + } + // Reduce apiserver hits by checking node's network state with last one - if r.lastState != "" && !r.networkStateChanged(currentState) { + if r.lastState.String() == currentState.String() { return ctrl.Result{RequeueAfter: node.NetworkStateRefresh}, err } else { r.Log.Info("Network configuration changed, updating NodeNetworkState") @@ -122,7 +128,3 @@ func (r *NodeReconciler) SetupWithManager(mgr ctrl.Manager) error { WithEventFilter(onCreationForThisNode). Complete(r) } - -func (r *NodeReconciler) networkStateChanged(currentState string) bool { - return state.RemoveDynamicAttributes(r.lastState) != state.RemoveDynamicAttributes(currentState) -} diff --git a/controllers/node_controller_test.go b/controllers/node_controller_test.go index 60d6271ab8..5dc6d3026c 100644 --- a/controllers/node_controller_test.go +++ b/controllers/node_controller_test.go @@ -19,17 +19,21 @@ import ( "sigs.k8s.io/controller-runtime/pkg/client/fake" "sigs.k8s.io/controller-runtime/pkg/reconcile" + "github.com/nmstate/kubernetes-nmstate/api/shared" nmstatev1beta1 "github.com/nmstate/kubernetes-nmstate/api/v1beta1" "github.com/nmstate/kubernetes-nmstate/pkg/nmstatectl" nmstatenode "github.com/nmstate/kubernetes-nmstate/pkg/node" + "github.com/nmstate/kubernetes-nmstate/pkg/state" ) var _ = Describe("Node controller reconcile", func() { var ( - cl client.Client - reconciler NodeReconciler - existingNodeName = "node01" - node = corev1.Node{ + cl client.Client + reconciler NodeReconciler + observedState string + filteredOutObservedState shared.State + existingNodeName = "node01" + node = corev1.Node{ ObjectMeta: metav1.ObjectMeta{ Name: existingNodeName, UID: "12345", @@ -58,9 +62,23 @@ var _ = Describe("Node controller reconcile", func() { reconciler.Scheme = s reconciler.nmstateUpdater = nmstate.CreateOrUpdateNodeNetworkState reconciler.nmstatectlShow = nmstatectl.Show - reconciler.lastState = "lastState" + reconciler.lastState = shared.NewState("lastState") + observedState = ` +--- +interfaces: + - name: eth1 + type: ethernet + state: up +routes: + running: [] + config: [] +` + var err error + filteredOutObservedState, err = state.FilterOut(shared.NewState(observedState)) + Expect(err).ToNot(HaveOccurred()) + reconciler.nmstatectlShow = func() (string, error) { - return "currentState", nil + return observedState, nil } }) Context("and nmstatectl show is failing", func() { @@ -82,13 +100,13 @@ var _ = Describe("Node controller reconcile", func() { request reconcile.Request ) BeforeEach(func() { - reconciler.lastState = "currentState" + reconciler.lastState = filteredOutObservedState reconciler.nmstateUpdater = func(client.Client, *corev1.Node, - client.ObjectKey, string) error { + client.ObjectKey, shared.State) error { return fmt.Errorf("we are not suppose to catch this error") } }) - It("should return a Result with RequeueAfter set", func() { + It("should not call nmstateUpdater and return a Result with RequeueAfter set", func() { result, err := reconciler.Reconcile(request) Expect(err).ToNot(HaveOccurred()) Expect(result).To(Equal(reconcile.Result{RequeueAfter: nmstatenode.NetworkStateRefresh})) @@ -114,19 +132,42 @@ var _ = Describe("Node controller reconcile", func() { BeforeEach(func() { request.Name = existingNodeName }) - Context("and nodenetworkstate is there too", func() { - AfterEach(func() { - reconciler.nmstateUpdater = nmstate.CreateOrUpdateNodeNetworkState - }) - It("should return a Result with RequeueAfter set (trigger re-reconciliation)", func() { - // Mocking nmstatectl.Show - reconciler.nmstateUpdater = func(client client.Client, node *corev1.Node, - namespace client.ObjectKey, observedStateRaw string) error { - return nil + Context(", nodenetworkstate is there too with last state and observed state is different", func() { + var ( + expectedStateRaw = `--- +interfaces: + - name: eth1 + type: ethernet + state: up + - name: eth2 + type: ethernet + state: up +routes: + running: [] + config: [] +` + ) + BeforeEach(func() { + By("Create the NNS with last state") + err := reconciler.nmstateUpdater(cl, &node, types.NamespacedName{Name: node.Name}, filteredOutObservedState) + Expect(err).ToNot(HaveOccurred()) + + By("Mock nmstate show so we return different value from last state") + reconciler.nmstatectlShow = func() (string, error) { + return expectedStateRaw, nil } + + }) + It("should call nmstateUpdater and return a Result with RequeueAfter set (trigger re-reconciliation)", func() { result, err := reconciler.Reconcile(request) Expect(err).ToNot(HaveOccurred()) Expect(result).To(Equal(reconcile.Result{RequeueAfter: nmstatenode.NetworkStateRefresh})) + obtainedNNS := nmstatev1beta1.NodeNetworkState{} + err = cl.Get(context.TODO(), types.NamespacedName{Name: existingNodeName}, &obtainedNNS) + Expect(err).ToNot(HaveOccurred()) + filteredOutExpectedState, err := state.FilterOut(shared.NewState(expectedStateRaw)) + Expect(err).ToNot(HaveOccurred()) + Expect(obtainedNNS.Status.CurrentState.String()).To(Equal(filteredOutExpectedState.String())) }) }) Context("and nodenetworkstate is not there", func() { diff --git a/controllers/nodenetworkconfigurationpolicy_controller.go b/controllers/nodenetworkconfigurationpolicy_controller.go index 2dd152e863..cbac1b9e7c 100644 --- a/controllers/nodenetworkconfigurationpolicy_controller.go +++ b/controllers/nodenetworkconfigurationpolicy_controller.go @@ -281,6 +281,8 @@ func (r *NodeNetworkConfigurationPolicyReconciler) Reconcile(request ctrl.Reques enactmentConditions.NotifySuccess() + r.forceNNSRefresh(nodeName) + return ctrl.Result{}, nil } @@ -326,6 +328,26 @@ func (r *NodeNetworkConfigurationPolicyReconciler) SetupWithManager(mgr ctrl.Man return nil } +func (r *NodeNetworkConfigurationPolicyReconciler) forceNNSRefresh(name string) { + log := r.Log.WithName("forceNNSRefresh").WithValues("node", name) + log.Info("forcing NodeNetworkState refresh after NNCP applied") + nns := &nmstatev1beta1.NodeNetworkState{} + err := r.Client.Get(context.TODO(), types.NamespacedName{Name: name}, nns) + if err != nil { + log.WithValues("error", err).Info("WARNING: failed retrieving NodeNetworkState to force refresh, it will be refreshed after regular period") + return + } + if nns.Labels == nil { + nns.Labels = map[string]string{} + } + nns.Labels[forceRefreshLabel] = fmt.Sprintf("%d", time.Now().UnixNano()) + + err = r.Client.Update(context.Background(), nns) + if err != nil { + log.WithValues("error", err).Info("WARNING: failed forcing NNS refresh, it will be refreshed after regular period") + } +} + func desiredState(object runtime.Object) (nmstateapi.State, error) { var state nmstateapi.State switch v := object.(type) { diff --git a/controllers/nodenetworkstate_controller.go b/controllers/nodenetworkstate_controller.go index 62b48643f3..f7ce482c80 100644 --- a/controllers/nodenetworkstate_controller.go +++ b/controllers/nodenetworkstate_controller.go @@ -30,9 +30,11 @@ import ( "sigs.k8s.io/controller-runtime/pkg/event" "sigs.k8s.io/controller-runtime/pkg/predicate" + "github.com/nmstate/kubernetes-nmstate/api/shared" nmstatev1beta1 "github.com/nmstate/kubernetes-nmstate/api/v1beta1" nmstate "github.com/nmstate/kubernetes-nmstate/pkg/helper" "github.com/nmstate/kubernetes-nmstate/pkg/nmstatectl" + "github.com/nmstate/kubernetes-nmstate/pkg/state" corev1 "k8s.io/api/core/v1" ) @@ -63,12 +65,17 @@ func (r *NodeNetworkStateReconciler) Reconcile(request ctrl.Request) (ctrl.Resul return ctrl.Result{}, err } - currentState, err := nmstatectl.Show() + currentStateRaw, err := nmstatectl.Show() if err != nil { // We cannot call nmstatectl show let's reconcile again return ctrl.Result{}, err } + currentState, err := state.FilterOut(shared.NewState(currentStateRaw)) + if err != nil { + return ctrl.Result{}, err + } + nmstate.CreateOrUpdateNodeNetworkState(r.Client, node, request.NamespacedName, currentState) if err != nil { err = errors.Wrap(err, "error at node reconcile creating NodeNetworkStateNetworkState") @@ -89,8 +96,9 @@ func (r *NodeNetworkStateReconciler) SetupWithManager(mgr ctrl.Manager) error { DeleteFunc: func(deleteEvent event.DeleteEvent) bool { return nmstate.EventIsForThisNode(deleteEvent.Meta) }, - UpdateFunc: func(event.UpdateEvent) bool { - return false + UpdateFunc: func(updateEvent event.UpdateEvent) bool { + return nmstate.EventIsForThisNode(updateEvent.MetaNew) && + shouldForceRefresh(updateEvent) }, GenericFunc: func(event.GenericEvent) bool { return false @@ -102,3 +110,15 @@ func (r *NodeNetworkStateReconciler) SetupWithManager(mgr ctrl.Manager) error { WithEventFilter(onDeleteForThisNode). Complete(r) } + +func shouldForceRefresh(updateEvent event.UpdateEvent) bool { + newForceRefresh, hasForceRefreshNow := updateEvent.MetaNew.GetLabels()[forceRefreshLabel] + if !hasForceRefreshNow { + return false + } + oldForceRefresh, hasForceRefreshLabelPreviously := updateEvent.MetaOld.GetLabels()[forceRefreshLabel] + if !hasForceRefreshLabelPreviously { + return true + } + return oldForceRefresh != newForceRefresh +} diff --git a/go.mod b/go.mod index ecbd39d4b5..c964c7ba46 100644 --- a/go.mod +++ b/go.mod @@ -4,6 +4,7 @@ go 1.15 require ( github.com/Masterminds/semver v1.5.0 + github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 github.com/evanphx/json-patch v4.9.0+incompatible github.com/github-release/github-release v0.8.1 github.com/go-logr/logr v0.1.0 diff --git a/go.sum b/go.sum index 20cb47a726..848c711472 100644 --- a/go.sum +++ b/go.sum @@ -127,6 +127,7 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d h1:UQZhZ2O0vMHr2cI+DC1Mbh0TJxzA3RcLoMsFw+aXw7E= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/aliyun/aliyun-oss-go-sdk v2.0.4+incompatible/go.mod h1:T/Aws4fEfogEE9v+HPhhw+CntffsBHJ8nXQCwKr0/g8= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239 h1:kFOfPq6dUM1hTo4JG6LR5AXSUEsOjtdm0kw0FtQtMJA= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= diff --git a/pkg/helper/client.go b/pkg/helper/client.go index 7c3423080a..3fd877a7e7 100644 --- a/pkg/helper/client.go +++ b/pkg/helper/client.go @@ -4,7 +4,6 @@ import ( "bytes" "context" "fmt" - "os" "os/exec" "time" @@ -15,12 +14,9 @@ import ( "k8s.io/apimachinery/pkg/types" "sigs.k8s.io/controller-runtime/pkg/client" logf "sigs.k8s.io/controller-runtime/pkg/runtime/log" - yaml "sigs.k8s.io/yaml" - "github.com/gobwas/glob" - nmstate "github.com/nmstate/kubernetes-nmstate/api/shared" + "github.com/nmstate/kubernetes-nmstate/api/shared" nmstatev1beta1 "github.com/nmstate/kubernetes-nmstate/api/v1beta1" - "github.com/nmstate/kubernetes-nmstate/pkg/environment" "github.com/nmstate/kubernetes-nmstate/pkg/nmstatectl" "github.com/nmstate/kubernetes-nmstate/pkg/probe" ) @@ -34,21 +30,6 @@ const defaultGwRetrieveTimeout = 120 * time.Second const defaultGwProbeTimeout = 120 * time.Second const apiServerProbeTimeout = 120 * time.Second -var ( - interfacesFilterGlob glob.Glob -) - -func init() { - if !environment.IsHandler() { - return - } - interfacesFilter, isSet := os.LookupEnv("INTERFACES_FILTER") - if !isSet { - panic("INTERFACES_FILTER is mandatory") - } - interfacesFilterGlob = glob.MustCompile(interfacesFilter) -} - func applyVlanFiltering(bridgeName string, ports []string) (string, error) { command := []string{bridgeName} command = append(command, ports...) @@ -91,7 +72,7 @@ func InitializeNodeNetworkState(client client.Client, node *corev1.Node) (*nmsta return &nodeNetworkState, nil } -func CreateOrUpdateNodeNetworkState(client client.Client, node *corev1.Node, namespace client.ObjectKey, observedStateRaw string) error { +func CreateOrUpdateNodeNetworkState(client client.Client, node *corev1.Node, namespace client.ObjectKey, observedState shared.State) error { nnsInstance := &nmstatev1beta1.NodeNetworkState{} err := client.Get(context.TODO(), namespace, nnsInstance) if err != nil { @@ -104,22 +85,20 @@ func CreateOrUpdateNodeNetworkState(client client.Client, node *corev1.Node, nam } } } - return UpdateCurrentState(client, nnsInstance, observedStateRaw) + return UpdateCurrentState(client, nnsInstance, observedState) } -func UpdateCurrentState(client client.Client, nodeNetworkState *nmstatev1beta1.NodeNetworkState, observedStateRaw string) error { - observedState := nmstate.State{Raw: []byte(observedStateRaw)} +func UpdateCurrentState(client client.Client, nodeNetworkState *nmstatev1beta1.NodeNetworkState, observedState shared.State) error { - stateToReport, err := filterOut(observedState, interfacesFilterGlob) - if err != nil { - log.Error(err, "failed filtering out interfaces from NodeNetworkState, keeping orignal content, please fix the glob") - stateToReport = observedState + if observedState.String() == nodeNetworkState.Status.CurrentState.String() { + log.Info("Skipping NodeNetworkState update, node network configuration not changed") + return nil } - nodeNetworkState.Status.CurrentState = stateToReport + nodeNetworkState.Status.CurrentState = observedState nodeNetworkState.Status.LastSuccessfulUpdateTime = metav1.Time{Time: time.Now()} - err = client.Status().Update(context.Background(), nodeNetworkState) + err := client.Status().Update(context.Background(), nodeNetworkState) if err != nil { if apierrors.IsNotFound(err) { return errors.Wrap(err, "Request object not found, could have been deleted after reconcile request") @@ -146,7 +125,7 @@ func rollback(client client.Client, probes []probe.Probe, cause error) error { return errors.New(message) } -func ApplyDesiredState(client client.Client, desiredState nmstate.State) (string, error) { +func ApplyDesiredState(client client.Client, desiredState shared.State) (string, error) { if len(string(desiredState.Raw)) == 0 { return "Ignoring empty desired state", nil } @@ -196,33 +175,3 @@ func ApplyDesiredState(client client.Client, desiredState nmstate.State) (string commandOutput += fmt.Sprintf("setOutput: %s \n", setOutput) return commandOutput, nil } - -func filterOut(currentState nmstate.State, interfacesFilterGlob glob.Glob) (nmstate.State, error) { - if interfacesFilterGlob.Match("") { - return currentState, nil - } - - var state map[string]interface{} - err := yaml.Unmarshal(currentState.Raw, &state) - if err != nil { - return currentState, err - } - - interfaces := state["interfaces"] - var filteredInterfaces []interface{} - - for _, iface := range interfaces.([]interface{}) { - name := iface.(map[string]interface{})["name"] - if !interfacesFilterGlob.Match(name.(string)) { - filteredInterfaces = append(filteredInterfaces, iface) - } - } - - state["interfaces"] = filteredInterfaces - filteredState, err := yaml.Marshal(state) - if err != nil { - return currentState, err - } - - return nmstate.State{Raw: filteredState}, nil -} diff --git a/pkg/helper/client_test.go b/pkg/helper/client_test.go deleted file mode 100644 index 99e1825566..0000000000 --- a/pkg/helper/client_test.go +++ /dev/null @@ -1,117 +0,0 @@ -package helper - -import ( - "github.com/gobwas/glob" - . "github.com/onsi/ginkgo" - . "github.com/onsi/gomega" - - nmstate "github.com/nmstate/kubernetes-nmstate/api/shared" -) - -var _ = Describe("FilterOut", func() { - var ( - state, filteredState nmstate.State - interfacesFilterGlob glob.Glob - ) - - Context("when the filter is set to empty and there is a list of interfaces", func() { - BeforeEach(func() { - state = nmstate.NewState(`interfaces: -- name: eth1 - state: up - type: ethernet -- name: vethab6030bd - state: down - type: ethernet -`) - interfacesFilterGlob = glob.MustCompile("") - }) - - It("should keep all interfaces intact", func() { - returnedState, err := filterOut(state, interfacesFilterGlob) - Expect(err).ToNot(HaveOccurred()) - Expect(returnedState).To(Equal(state)) - }) - }) - - Context("when the filter is matching one of the interfaces in the list", func() { - BeforeEach(func() { - state = nmstate.NewState(`interfaces: -- name: eth1 - state: up - type: ethernet -- name: vethab6030bd - state: down - type: ethernet -`) - filteredState = nmstate.NewState(`interfaces: -- name: eth1 - state: up - type: ethernet -`) - interfacesFilterGlob = glob.MustCompile("veth*") - }) - - It("should filter out matching interface and keep the others", func() { - returnedState, err := filterOut(state, interfacesFilterGlob) - Expect(err).NotTo(HaveOccurred()) - Expect(returnedState).To(Equal(filteredState)) - }) - }) - - Context("when the filter is matching multiple interfaces in the list", func() { - BeforeEach(func() { - state = nmstate.NewState(`interfaces: -- name: eth1 - state: up - type: ethernet -- name: vethab6030bd - state: down - type: ethernet -- name: vethjyuftrgv - state: down - type: ethernet -`) - filteredState = nmstate.NewState(`interfaces: -- name: eth1 - state: up - type: ethernet -`) - interfacesFilterGlob = glob.MustCompile("veth*") - }) - - It("should filter out all matching interfaces and keep the others", func() { - returnedState, err := filterOut(state, interfacesFilterGlob) - Expect(err).ToNot(HaveOccurred()) - Expect(returnedState).To(Equal(filteredState)) - }) - }) - - Context("when the filter is matching multiple prefixes", func() { - BeforeEach(func() { - state = nmstate.NewState(`interfaces: -- name: eth1 - state: up - type: ethernet -- name: vethab6030bd - state: down - type: ethernet -- name: vnet2b730a2b@if3 - state: down - type: ethernet -`) - filteredState = nmstate.NewState(`interfaces: -- name: eth1 - state: up - type: ethernet -`) - interfacesFilterGlob = glob.MustCompile("{veth*,vnet*}") - }) - - It("it should filter out all interfaces matching any of these prefixes and keep the others", func() { - returnedState, err := filterOut(state, interfacesFilterGlob) - Expect(err).ToNot(HaveOccurred()) - Expect(returnedState).To(Equal(filteredState)) - }) - }) -}) diff --git a/pkg/node/constants.go b/pkg/node/constants.go index d27190e54a..9deec5b876 100644 --- a/pkg/node/constants.go +++ b/pkg/node/constants.go @@ -5,5 +5,5 @@ import ( ) const ( - NetworkStateRefresh = 5 * time.Second + NetworkStateRefresh = time.Minute ) diff --git a/pkg/state/filter.go b/pkg/state/filter.go index 0827ab6a15..cb7d4c55f0 100644 --- a/pkg/state/filter.go +++ b/pkg/state/filter.go @@ -1,21 +1,99 @@ package state import ( - "regexp" + "os" + "github.com/gobwas/glob" "github.com/nmstate/kubernetes-nmstate/api/shared" + "github.com/nmstate/kubernetes-nmstate/pkg/environment" + + yaml "sigs.k8s.io/yaml" ) var ( - gcTimerRexp = regexp.MustCompile(` *gc-timer: *[0-9]*\n`) + interfacesFilterGlobFromEnv glob.Glob ) -func RemoveDynamicAttributes(state string) string { +func init() { + if !environment.IsHandler() { + return + } + interfacesFilter, isSet := os.LookupEnv("INTERFACES_FILTER") + if !isSet { + panic("INTERFACES_FILTER is mandatory") + } + interfacesFilterGlobFromEnv = glob.MustCompile(interfacesFilter) +} - // Remove attributes that make network state always different - return gcTimerRexp.ReplaceAllLiteralString(state, "") +func FilterOut(currentState shared.State) (shared.State, error) { + return filterOut(currentState, interfacesFilterGlobFromEnv) } -func RemoveDynamicAttributesFromStruct(state shared.State) string { - return RemoveDynamicAttributes(state.String()) +func filterOutRoutes(kind string, state map[string]interface{}, interfacesFilterGlob glob.Glob) { + routes := state["routes"].(map[string]interface{}) + routesByKind := routes[kind].([]interface{}) + + if routesByKind == nil { + return + } + + filteredRoutes := []interface{}{} + for _, route := range routesByKind { + name := route.(map[string]interface{})["next-hop-interface"] + if !interfacesFilterGlob.Match(name.(string)) { + filteredRoutes = append(filteredRoutes, route) + } + } + + state["routes"].(map[string]interface{})[kind] = filteredRoutes +} + +func filterOutDynamicAttributes(iface map[string]interface{}) { + // The gc-timer and hello-time are deep into linux-bridge like this + // - bridge: + // options: + // gc-timer: 13715 + // hello-timer: 0 + if iface["type"] != "linux-bridge" { + return + } + + bridge := iface["bridge"].(map[string]interface{}) + + options := bridge["options"].(map[string]interface{}) + delete(options, "gc-timer") + delete(options, "hello-timer") +} + +func filterOutInterfaces(state map[string]interface{}, interfacesFilterGlob glob.Glob) { + interfaces := state["interfaces"] + filteredInterfaces := []interface{}{} + + for _, iface := range interfaces.([]interface{}) { + name := iface.(map[string]interface{})["name"] + if !interfacesFilterGlob.Match(name.(string)) { + filterOutDynamicAttributes(iface.(map[string]interface{})) + filteredInterfaces = append(filteredInterfaces, iface) + } + } + state["interfaces"] = filteredInterfaces +} + +func filterOut(currentState shared.State, interfacesFilterGlob glob.Glob) (shared.State, error) { + var state map[string]interface{} + err := yaml.Unmarshal(currentState.Raw, &state) + if err != nil { + return currentState, err + } + + filterOutInterfaces(state, interfacesFilterGlob) + filterOutRoutes("running", state, interfacesFilterGlob) + filterOutRoutes("config", state, interfacesFilterGlob) + + filteredState, err := yaml.Marshal(state) + if err != nil { + return currentState, err + } + + return shared.NewState(string(filteredState)), nil } diff --git a/pkg/state/filter_test.go b/pkg/state/filter_test.go new file mode 100644 index 0000000000..3f49f13a09 --- /dev/null +++ b/pkg/state/filter_test.go @@ -0,0 +1,310 @@ +package state + +import ( + "github.com/gobwas/glob" + . "github.com/onsi/ginkgo" + . "github.com/onsi/gomega" + + nmstate "github.com/nmstate/kubernetes-nmstate/api/shared" +) + +var _ = Describe("FilterOut", func() { + var ( + state, filteredState nmstate.State + interfacesFilterGlob glob.Glob + ) + + Context("when there is a linux bridge with gc-timer and hello-timer", func() { + BeforeEach(func() { + state = nmstate.NewState(` +interfaces: +- name: eth1 + state: up + type: ethernet +- name: br1 + bridge: + options: + gc-timer: 13715 + group-addr: 01:80:C2:00:00:00 + group-forward-mask: 0 + hash-max: 512 + hello-timer: 0 + mac-ageing-time: 300 + stp: + enabled: false + port: [] + ipv4: + address: + - ip: 172.17.0.1 + prefix-length: 16 + dhcp: false + enabled: true + ipv6: + address: + - ip: 2001:db9:1::1 + prefix-length: 64 + - ip: fe80::1 + prefix-length: 64 + autoconf: false + dhcp: false + enabled: true + lldp: + enabled: false + mac-address: 02:42:BB:10:B8:9F + mtu: 1500 + name: br1 + state: up + type: linux-bridge +routes: + config: [] + running: + - destination: 0.0.0.0/0 + metric: 102 + next-hop-address: 192.168.66.2 + next-hop-interface: eth1 + table-id: 254 +`) + + filteredState = nmstate.NewState(` +interfaces: +- name: eth1 + state: up + type: ethernet +- name: br1 + bridge: + options: + group-addr: 01:80:C2:00:00:00 + group-forward-mask: 0 + hash-max: 512 + mac-ageing-time: 300 + stp: + enabled: false + port: [] + ipv4: + address: + - ip: 172.17.0.1 + prefix-length: 16 + dhcp: false + enabled: true + ipv6: + address: + - ip: 2001:db9:1::1 + prefix-length: 64 + - ip: fe80::1 + prefix-length: 64 + autoconf: false + dhcp: false + enabled: true + lldp: + enabled: false + mac-address: 02:42:BB:10:B8:9F + mtu: 1500 + name: br1 + state: up + type: linux-bridge +routes: + config: [] + running: + - destination: 0.0.0.0/0 + metric: 102 + next-hop-address: 192.168.66.2 + next-hop-interface: eth1 + table-id: 254 +`) + interfacesFilterGlob = glob.MustCompile("") + }) + It("should remove them from linux-bridge", func() { + returnedState, err := filterOut(state, interfacesFilterGlob) + Expect(err).ToNot(HaveOccurred()) + Expect(returnedState).To(MatchYAML(filteredState)) + }) + }) + Context("when the filter is set to empty and there is a list of interfaces", func() { + BeforeEach(func() { + state = nmstate.NewState(` +interfaces: +- name: eth1 + state: up + type: ethernet +- name: vethab6030bd + state: down + type: ethernet +routes: + config: [] + running: + - destination: fd10:244::8c40/128 + metric: 1024 + next-hop-address: "" + next-hop-interface: vethab6030bd + table-id: 254 + - destination: 0.0.0.0/0 + metric: 102 + next-hop-address: 192.168.66.2 + next-hop-interface: eth1 + table-id: 254 +`) + interfacesFilterGlob = glob.MustCompile("") + }) + + It("should keep all interfaces intact", func() { + returnedState, err := filterOut(state, interfacesFilterGlob) + Expect(err).ToNot(HaveOccurred()) + Expect(returnedState).To(MatchYAML(state)) + }) + }) + + Context("when the filter is matching one of the interfaces in the list", func() { + BeforeEach(func() { + state = nmstate.NewState(`interfaces: +- name: eth1 + state: up + type: ethernet +- name: vethab6030bd + state: down + type: ethernet +routes: + config: [] + running: + - destination: fd10:244::8c40/128 + metric: 1024 + next-hop-address: "" + next-hop-interface: vethab6030bd + table-id: 254 + - destination: 0.0.0.0/0 + metric: 102 + next-hop-address: 192.168.66.2 + next-hop-interface: eth1 + table-id: 254 + +`) + filteredState = nmstate.NewState(`interfaces: +- name: eth1 + state: up + type: ethernet +routes: + config: [] + running: + - destination: 0.0.0.0/0 + metric: 102 + next-hop-address: 192.168.66.2 + next-hop-interface: eth1 + table-id: 254 +`) + interfacesFilterGlob = glob.MustCompile("veth*") + }) + + It("should filter out matching interface and keep the others", func() { + returnedState, err := filterOut(state, interfacesFilterGlob) + Expect(err).NotTo(HaveOccurred()) + Expect(returnedState).To(MatchYAML(filteredState)) + }) + }) + + Context("when the filter is matching multiple interfaces in the list", func() { + BeforeEach(func() { + state = nmstate.NewState(`interfaces: +- name: eth1 + state: up + type: ethernet +- name: vethab6030bd + state: down + type: ethernet +- name: vethjyuftrgv + state: down + type: ethernet +routes: + config: [] + running: + - destination: fd10:244::8c40/128 + metric: 1024 + next-hop-address: "" + next-hop-interface: vethab6030bd + table-id: 254 + - destination: fd10:244::8c40/128 + metric: 1024 + next-hop-address: "" + next-hop-interface: vethjyuftrgv + table-id: 254 + - destination: 0.0.0.0/0 + metric: 102 + next-hop-address: 192.168.66.2 + next-hop-interface: eth1 + table-id: 254 +`) + filteredState = nmstate.NewState(`interfaces: +- name: eth1 + state: up + type: ethernet +routes: + config: [] + running: + - destination: 0.0.0.0/0 + metric: 102 + next-hop-address: 192.168.66.2 + next-hop-interface: eth1 + table-id: 254 +`) + interfacesFilterGlob = glob.MustCompile("veth*") + }) + + It("should filter out all matching interfaces and keep the others", func() { + returnedState, err := filterOut(state, interfacesFilterGlob) + Expect(err).ToNot(HaveOccurred()) + Expect(returnedState).To(MatchYAML(filteredState)) + }) + }) + + Context("when the filter is matching multiple prefixes", func() { + BeforeEach(func() { + state = nmstate.NewState(`interfaces: +- name: eth1 + state: up + type: ethernet +- name: vethab6030bd + state: down + type: ethernet +- name: vnet2b730a2b@if3 + state: down + type: ethernet +routes: + config: [] + running: + - destination: fd10:244::8c40/128 + metric: 1024 + next-hop-address: "" + next-hop-interface: vethab6030bd + table-id: 254 + - destination: fd10:244::8c40/128 + metric: 1024 + next-hop-address: "" + next-hop-interface: vnet2b730a2b + table-id: 254 + - destination: 0.0.0.0/0 + metric: 102 + next-hop-address: 192.168.66.2 + next-hop-interface: eth1 + table-id: 254 +`) + filteredState = nmstate.NewState(`interfaces: +- name: eth1 + state: up + type: ethernet +routes: + config: [] + running: + - destination: 0.0.0.0/0 + metric: 102 + next-hop-address: 192.168.66.2 + next-hop-interface: eth1 + table-id: 254 +`) + interfacesFilterGlob = glob.MustCompile("{veth*,vnet*}") + }) + + It("it should filter out all interfaces matching any of these prefixes and keep the others", func() { + returnedState, err := filterOut(state, interfacesFilterGlob) + Expect(err).ToNot(HaveOccurred()) + Expect(returnedState).To(MatchYAML(filteredState)) + }) + }) +}) diff --git a/pkg/helper/helper_suite_test.go b/pkg/state/state_suite_test.go similarity index 73% rename from pkg/helper/helper_suite_test.go rename to pkg/state/state_suite_test.go index 77cdbb0dc7..86dc41154d 100644 --- a/pkg/helper/helper_suite_test.go +++ b/pkg/state/state_suite_test.go @@ -1,4 +1,4 @@ -package helper +package state import ( "testing" @@ -11,6 +11,6 @@ import ( func TestUnit(t *testing.T) { RegisterFailHandler(Fail) - junitReporter := reporters.NewJUnitReporter("junit.helper-helper_suite_test.xml") + junitReporter := reporters.NewJUnitReporter("junit.state-state_suite_test.xml") RunSpecsWithDefaultAndCustomReporters(t, "Helper Test Suite", []Reporter{junitReporter}) } diff --git a/test/e2e/handler/nns_update_timestamp_test.go b/test/e2e/handler/nns_update_timestamp_test.go index 7380739b65..1f2e0a5641 100644 --- a/test/e2e/handler/nns_update_timestamp_test.go +++ b/test/e2e/handler/nns_update_timestamp_test.go @@ -6,12 +6,14 @@ import ( . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" . "github.com/onsi/gomega/gstruct" + + "github.com/andreyvit/diff" + "k8s.io/apimachinery/pkg/types" "github.com/nmstate/kubernetes-nmstate/api/shared" nmstatev1beta1 "github.com/nmstate/kubernetes-nmstate/api/v1beta1" nmstatenode "github.com/nmstate/kubernetes-nmstate/pkg/node" - "github.com/nmstate/kubernetes-nmstate/pkg/state" ) var _ = Describe("[nns] NNS LastSuccessfulUpdateTime", func() { @@ -32,18 +34,21 @@ var _ = Describe("[nns] NNS LastSuccessfulUpdateTime", func() { timeout := 3 * nmstatenode.NetworkStateRefresh key := types.NamespacedName{Name: node} + obtainedStatus := shared.NodeNetworkStateStatus{} Consistently(func() shared.NodeNetworkStateStatus { - return nodeNetworkState(key).Status + obtainedStatus = nodeNetworkState(key).Status + return obtainedStatus }, timeout, time.Second).Should(MatchAllFields(Fields{ - "CurrentState": WithTransform(state.RemoveDynamicAttributesFromStruct, Equal(state.RemoveDynamicAttributes(originalNNS.Status.CurrentState.String()))), + "CurrentState": WithTransform(shared.State.String, Equal(originalNNS.Status.CurrentState.String())), "LastSuccessfulUpdateTime": Equal(originalNNS.Status.LastSuccessfulUpdateTime), "Conditions": Equal(originalNNS.Status.Conditions), - })) + }), "currentState diff: ", diff.LineDiff(originalNNS.Status.CurrentState.String(), obtainedStatus.CurrentState.String())) } }) }) - Context("when network configuration changed", func() { + Context("when network configuration is changed by a NNCP", func() { BeforeEach(func() { + // We want to test all the NNS so we apply policies to masters and workers setDesiredStateWithPolicyWithoutNodeSelector(TestPolicy, linuxBrUp(bridge1)) waitForAvailableTestPolicy() }) @@ -54,16 +59,14 @@ var _ = Describe("[nns] NNS LastSuccessfulUpdateTime", func() { waitForAvailableTestPolicy() deletePolicy(TestPolicy) }) - It("should be updated", func() { + It("should be immediately updated", func() { for node, originalNNS := range originalNNSs { - // Give enough time for the NNS to be updated (3 interval times) - timeout := 3 * nmstatenode.NetworkStateRefresh key := types.NamespacedName{Name: node} Eventually(func() time.Time { updatedTime := nodeNetworkState(key).Status.LastSuccessfulUpdateTime return updatedTime.Time - }, timeout, time.Second).Should(BeTemporally(">", originalNNS.Status.LastSuccessfulUpdateTime.Time)) + }, time.Second*5, time.Second).Should(BeTemporally(">", originalNNS.Status.LastSuccessfulUpdateTime.Time)) } }) }) diff --git a/test/e2e/handler/nodes_test.go b/test/e2e/handler/nodes_test.go index fedb344eec..843904643c 100644 --- a/test/e2e/handler/nodes_test.go +++ b/test/e2e/handler/nodes_test.go @@ -1,8 +1,12 @@ package handler import ( + "time" + . "github.com/onsi/ginkgo" . "github.com/onsi/gomega" + + "github.com/nmstate/kubernetes-nmstate/pkg/node" ) var _ = Describe("[rfe_id:3503][crit:medium][vendor:cnv-qe@redhat.com][level:component][nns]Nodes", func() { @@ -41,8 +45,10 @@ var _ = Describe("[rfe_id:3503][crit:medium][vendor:cnv-qe@redhat.com][level:com } }) It("[test_id:3794]should update node network state with it", func() { - for _, node := range nodes { - interfacesNameForNodeEventually(node).Should(ContainElement(expectedDummyName)) + for _, nodeName := range nodes { + Eventually(func() []string { + return interfacesNameForNode(nodeName) + }, node.NetworkStateRefresh, time.Second).Should(ContainElement(expectedDummyName)) } }) }) diff --git a/vendor/github.com/andreyvit/diff/.gitignore b/vendor/github.com/andreyvit/diff/.gitignore new file mode 100644 index 0000000000..daf913b1b3 --- /dev/null +++ b/vendor/github.com/andreyvit/diff/.gitignore @@ -0,0 +1,24 @@ +# Compiled Object files, Static and Dynamic libs (Shared Objects) +*.o +*.a +*.so + +# Folders +_obj +_test + +# Architecture specific extensions/prefixes +*.[568vq] +[568vq].out + +*.cgo1.go +*.cgo2.c +_cgo_defun.c +_cgo_gotypes.go +_cgo_export.* + +_testmain.go + +*.exe +*.test +*.prof diff --git a/vendor/github.com/andreyvit/diff/LICENSE b/vendor/github.com/andreyvit/diff/LICENSE new file mode 100644 index 0000000000..05b98e1f78 --- /dev/null +++ b/vendor/github.com/andreyvit/diff/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2017 Andrey Tarantsov + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/vendor/github.com/andreyvit/diff/README.md b/vendor/github.com/andreyvit/diff/README.md new file mode 100644 index 0000000000..5b52fea8af --- /dev/null +++ b/vendor/github.com/andreyvit/diff/README.md @@ -0,0 +1,28 @@ +# diff + +Quick'n'easy string diffing functions for Golang based on [github.com/sergi/go-diff](https://github.com/sergi/go-diff). Mainly for diffing strings in tests. + +See [the docs on GoDoc](https://godoc.org/github.com/andreyvit/diff). + +Get it: + + go get -u github.com/andreyvit/diff + +Example: + + import ( + "strings" + "testing" + "github.com/andreyvit/diff" + ) + + const expected = ` + ... + ` + + func TestFoo(t *testing.T) { + actual := Foo(...) + if a, e := strings.TrimSpace(actual), strings.TrimSpace(expected); a != e { + t.Errorf("Result not as expected:\n%v", diff.LineDiff(e, a)) + } + } diff --git a/vendor/github.com/andreyvit/diff/diff.go b/vendor/github.com/andreyvit/diff/diff.go new file mode 100644 index 0000000000..12953c83a3 --- /dev/null +++ b/vendor/github.com/andreyvit/diff/diff.go @@ -0,0 +1,128 @@ +package diff + +import ( + "bytes" + "strings" + + "github.com/sergi/go-diff/diffmatchpatch" +) + +func diff(a, b string) []diffmatchpatch.Diff { + dmp := diffmatchpatch.New() + diffs := dmp.DiffMain(a, b, true) + if len(diffs) > 2 { + diffs = dmp.DiffCleanupSemantic(diffs) + diffs = dmp.DiffCleanupEfficiency(diffs) + } + return diffs +} + +// CharacterDiff returns an inline diff between the two strings, using (++added++) and (~~deleted~~) markup. +func CharacterDiff(a, b string) string { + return diffsToString(diff(a, b)) +} + +func diffsToString(diffs []diffmatchpatch.Diff) string { + var buff bytes.Buffer + for _, diff := range diffs { + text := diff.Text + switch diff.Type { + case diffmatchpatch.DiffInsert: + buff.WriteString("(++") + buff.WriteString(text) + buff.WriteString("++)") + case diffmatchpatch.DiffDelete: + buff.WriteString("(~~") + buff.WriteString(text) + buff.WriteString("~~)") + case diffmatchpatch.DiffEqual: + buff.WriteString(text) + } + } + return buff.String() +} + +// LineDiff returns a normal linewise diff between the two given strings. +func LineDiff(a, b string) string { + return strings.Join(LineDiffAsLines(a, b), "\n") +} + +// LineDiffAsLines returns the lines of a linewise diff between the two given strings. +func LineDiffAsLines(a, b string) []string { + return diffsToPatchLines(diff(a, b)) +} + +type patchBuilder struct { + output []string + oldLines []string + newLines []string + newLineBuffer bytes.Buffer + oldLineBuffer bytes.Buffer +} + +func (b *patchBuilder) AddCharacters(text string, op diffmatchpatch.Operation) { + if op == diffmatchpatch.DiffInsert || op == diffmatchpatch.DiffEqual { + b.newLineBuffer.WriteString(text) + } + if op == diffmatchpatch.DiffDelete || op == diffmatchpatch.DiffEqual { + b.oldLineBuffer.WriteString(text) + } +} +func (b *patchBuilder) AddNewline(op diffmatchpatch.Operation) { + oldLine := b.oldLineBuffer.String() + newLine := b.newLineBuffer.String() + + if op == diffmatchpatch.DiffEqual && (oldLine == newLine) { + b.FlushChunk() + b.output = append(b.output, " "+newLine) + b.oldLineBuffer.Reset() + b.newLineBuffer.Reset() + } else { + if op == diffmatchpatch.DiffDelete || op == diffmatchpatch.DiffEqual { + b.oldLines = append(b.oldLines, "-"+oldLine) + b.oldLineBuffer.Reset() + } + if op == diffmatchpatch.DiffInsert || op == diffmatchpatch.DiffEqual { + b.newLines = append(b.newLines, "+"+newLine) + b.newLineBuffer.Reset() + } + } +} +func (b *patchBuilder) FlushChunk() { + if b.oldLines != nil { + b.output = append(b.output, b.oldLines...) + b.oldLines = nil + } + if b.newLines != nil { + b.output = append(b.output, b.newLines...) + b.newLines = nil + } +} +func (b *patchBuilder) Flush() { + if b.oldLineBuffer.Len() > 0 && b.newLineBuffer.Len() > 0 { + b.AddNewline(diffmatchpatch.DiffEqual) + } else if b.oldLineBuffer.Len() > 0 { + b.AddNewline(diffmatchpatch.DiffDelete) + } else if b.newLineBuffer.Len() > 0 { + b.AddNewline(diffmatchpatch.DiffInsert) + } + b.FlushChunk() +} + +func diffsToPatchLines(diffs []diffmatchpatch.Diff) []string { + b := new(patchBuilder) + b.output = make([]string, 0, len(diffs)) + + for _, diff := range diffs { + lines := strings.Split(diff.Text, "\n") + for idx, line := range lines { + if idx > 0 { + b.AddNewline(diff.Type) + } + b.AddCharacters(line, diff.Type) + } + } + + b.Flush() + return b.output +} diff --git a/vendor/github.com/andreyvit/diff/doc.go b/vendor/github.com/andreyvit/diff/doc.go new file mode 100644 index 0000000000..98e2470833 --- /dev/null +++ b/vendor/github.com/andreyvit/diff/doc.go @@ -0,0 +1,2 @@ +// diff provides quick and easy string diffing functions based on github.com/sergi/go-diff, mainly for diffing strings in tests +package diff diff --git a/vendor/github.com/andreyvit/diff/trim.go b/vendor/github.com/andreyvit/diff/trim.go new file mode 100644 index 0000000000..87871f99a1 --- /dev/null +++ b/vendor/github.com/andreyvit/diff/trim.go @@ -0,0 +1,19 @@ +package diff + +import ( + "strings" +) + +// TrimLines applies TrimSpace to each string in the given array. +func TrimLines(input []string) []string { + result := make([]string, 0, len(input)) + for _, el := range input { + result = append(result, strings.TrimSpace(el)) + } + return result +} + +// TrimLinesInString applies TrimSpace to each line in the given string, and returns the new trimmed string. Empty lines are not removed. +func TrimLinesInString(input string) string { + return strings.Join(TrimLines(strings.Split(strings.TrimSpace(input), "\n")), "\n") +} diff --git a/vendor/modules.txt b/vendor/modules.txt index 808db9e1ae..a3cdee61d7 100644 --- a/vendor/modules.txt +++ b/vendor/modules.txt @@ -74,6 +74,9 @@ github.com/Microsoft/hcsshim/osversion github.com/PuerkitoBio/purell # github.com/PuerkitoBio/urlesc v0.0.0-20170810143723-de5bf2ad4578 github.com/PuerkitoBio/urlesc +# github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 +## explicit +github.com/andreyvit/diff # github.com/antihax/optional v0.0.0-20180407024304-ca021399b1a6 github.com/antihax/optional # github.com/asaskevich/govalidator v0.0.0-20200428143746-21a406dcc535