-
Notifications
You must be signed in to change notification settings - Fork 155
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
State reconciliation with dependency graphs
This commit adds packages "depgraph" and "reconciler" under EVE/libs, implementing a dependency graph [1] and a state reconciliation algorithm, respectively. It will be used in EVE to generically solve the problem of reconciliation between the actual/current (configuration) state and the latest intended/desired state. Every time the desired state changes, a certain sequence of Create, Modify and Delete operations has to be scheduled and executed to update the current state accordingly. The order of operations often matters as it has to respect all dependencies that exist between configuration items. The plan is to first refactor NIM and use dependency graph and reconciler for configuration items that device networks are built with. Based on how NIM refactoring goes, this solution could be used for zedrouter next. For more detailed reasoning and motivation behind depgraph+reconciler, please refer to the proposal presented on the LF Edge wiki [2]. [1]: https://en.wikipedia.org/wiki/Dependency_graph [2]: https://wiki.lfedge.org/display/EVE/Dependency+graph+for+configuration+items Signed-off-by: Milan Lenco <milan@zededa.com>
- Loading branch information
1 parent
100c3af
commit 76bb8fa
Showing
34 changed files
with
9,120 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,300 @@ | ||
# Dependency Graph (depgraph) | ||
|
||
## Use-case | ||
|
||
This package implements a [dependency graph](https://en.wikipedia.org/wiki/Dependency_graph). | ||
The main use-case is to represent configuration items (network interfaces, routes, | ||
volumes, etc.) or any managed stateful objects (incl. processes, containers, files, etc.) | ||
as graph nodes, here called *items*, and their dependencies as directed graph edges. | ||
|
||
For example, if there are items `A` and `B` with edge (dependency) `A->B`, | ||
it means that `B` should be created before `A`. Conversely, the removal of these | ||
two items should proceed in the opposite order, i.e. `A` should be removed first | ||
(think of the dependency as "`A` cannot exist without `B`"). Edges of this dependency | ||
graph slightly diverge from the standard definition as it is allowed for an edge | ||
to point to an item which is not present in the graph, representing a scenario | ||
of a missing dependency. | ||
|
||
The graph can be for example used to model the intended or the current state | ||
of a managed system. Note that depgraph alone only provides a data structure | ||
to store the modeled state. However, in combination with the [Reconciler](../reconciler/README.md), | ||
it can help to solve the challenge of state reconciliation. A management | ||
agent will typically maintain two graphs, one for the intended state, updated | ||
based on the input from a user/controller, and the other for the current state. | ||
The agent will use APIs of the managed system to learn the actual state | ||
and will update the graph accordingly. The reconciler will take both of these | ||
graphs as an input and will perform all state transitions needed to reach | ||
the intended state, updating the graph representing the current state | ||
in the process. For more information on the topic of state reconciliation, | ||
please refer to the [readme file of the Reconciler](../reconciler/README.md). | ||
|
||
## Subgraphs | ||
|
||
Apart from items and edges, the graph also supports a notion of subgraphs. | ||
A subgraph is a subset of graph items, including all edges that originate or point | ||
from/to any of these items (again, a slight deviation from the standard definition). | ||
Subgraph is given a name and optionally also a description (just like the top-level | ||
graph). The main use-case is to group related items and allow to select and edit | ||
them together. | ||
|
||
Here is a visual depiction of an example graph with some subgraphs: | ||
|
||
![Subgraphs](./pics/subgraphs.png) | ||
|
||
For example, all components of a virtualized network (bridge, routes, dns server, etc.) | ||
can be grouped into one subgraph with the logical name of the network: | ||
|
||
```go | ||
virtNetwork := depgraph.New( | ||
depgraph.InitArgs{ | ||
Name: virtNetworkName, | ||
Items: []depgraph.Item{bridge, route1, route2, dnsServer} | ||
}) | ||
``` | ||
|
||
Then, to add or replace all the components of the network in the graph, only one function | ||
call is needed: | ||
|
||
```go | ||
fullStateGraph.PutSubGraph(virtNetwork) | ||
``` | ||
|
||
Also, the entire content of the subgraph can be removed with just: | ||
|
||
```go | ||
fullStateGraph.DelSubGraph(virtNetworkName) | ||
``` | ||
|
||
Subgraphs can be also nested and thus compose a hierarchical tree structure. | ||
This is very similar to directory structure of a filesystem if you think of subgraphs | ||
as directories and items as files. | ||
Currently, subgraphs are not related to and does not affect dependencies. | ||
|
||
Note that in terms of API, subgraph is also a graph - it implements the `Graph` | ||
interface. Single item can also be viewed as a subgraph - `Graph.ItemAsSubGraph(item)` | ||
returns a (read-only) graph handle. | ||
|
||
## API & Usage | ||
|
||
Configuration items modeled as graph nodes (in API simply called items) should | ||
implement the `Item` interface. | ||
This means that for every distinct item type, there needs to be a structure | ||
with methods as required by the interface. For example, it is required to provide | ||
a name for every item instance (based on the item configuration) through the method | ||
`Name() string`. Two distinct items of the same type should have different names. | ||
Distinct here means that the manifestation of these items are two separate objects | ||
in the managed system. A graph-wide unique item identifier/reference is therefore | ||
a combination of the type (returned by `Type() string`) and the name. This is captured | ||
by the `ItemRef` structure. To obtain a reference for an item (which is needed for | ||
several graph methods), call `Reference(Item)`. | ||
Another notable method defined by the `Item` interface is `Dependencies()`. It lists | ||
all the dependencies of an item and therefore determines the outgoing edges of the item. | ||
|
||
Here is a simplified example for Items representing Linux interfaces and routes. | ||
Only `Name()`, `Type()` and `Dependencies()` methods are shown here. For other | ||
required methods, please see the `Item` interface definition. | ||
|
||
```go | ||
import "github.com/lf-edge/eve/libs/depgraph" | ||
|
||
type LinuxInterface struct { | ||
name string | ||
} | ||
|
||
func (intf LinuxInterface) Name() string { | ||
return intf.name | ||
} | ||
|
||
func (intf LinuxInterface) Type() string { | ||
return "Linux interface" | ||
} | ||
|
||
func (intf LinuxInterface) Dependencies() []depgraph.Dependency { | ||
// no dependencies | ||
return nil | ||
} | ||
|
||
// Other Item methods for LinuxInterface are omitted. | ||
|
||
type LinuxRoute struct { | ||
via string | ||
dst string | ||
metric int | ||
} | ||
|
||
func (route LinuxRoute) Name() string { | ||
// Both dst and via should be included to uniquely identify the route | ||
// among all the routes. | ||
return route.dst + " via " + route.via | ||
} | ||
|
||
func (route LinuxRoute) Type() string { | ||
return "Linux route" | ||
} | ||
|
||
func (route LinuxRoute) Dependencies() []depgraph.Dependency { | ||
return []depgraph.Dependency{ | ||
{ | ||
Item: depgraph.RequiredItem{ | ||
Type: "Linux interface", // can also use LinuxInterface{}.Type() | ||
Name: route.via, // can also use LinuxInterface{name: route.via}.Name() | ||
}, | ||
Description: "Route requires outgoing interface to be configured first", | ||
}, | ||
} | ||
} | ||
|
||
// Other Item methods for LinuxRoute are omitted. | ||
``` | ||
|
||
With each item instance the graph also allows to store some state data, implementing | ||
the `ItemState` interface. This can be for example used to store information about the last | ||
state transition performed for the item and any errors that resulted from it. | ||
Think of `Item` as the configuration and `ItemState` as a run-time state. | ||
State data are mostly transparent to the graph with only few methods defined to help to enhance | ||
the [visualization of the graph](#visualization). | ||
Item state data are completely optional and can be passed as nil. They are typically omitted | ||
when the graph is used to model the intended state and provided when the currently running | ||
state is modeled. | ||
|
||
Once all needed `Item` (and possibly `ItemState`) implementations are available, you are ready | ||
to build a dependency graph to model some kind of system state. | ||
Graph with some initial content is created using `New()`: | ||
|
||
```go | ||
import "github.com/lf-edge/eve/libs/depgraph" | ||
|
||
// using LinuxRoute and LinuxInterface defined above | ||
|
||
g := depgraph.New(depgraph.InitArgs{ | ||
Name: "MyGraph", | ||
Description: "This is my example graph", | ||
ItemsWithState: []ItemWithState{ | ||
{Item: LinuxInterface{name: "eth0"}, State: LinuxInterfaceState{lastOpErr: nil}}, | ||
}, | ||
// For items without state data you can use Items, avoiding to pass ItemState as nil | ||
// and therefore making the code shorter and easier to read. | ||
// But do not put the same Item into both Items and ItemsWithState. | ||
Items: []Item{ | ||
LinuxRoute{via: "eth0", dst: "10.10.0.0/16", metric: 10}, | ||
LinuxRoute{via: "eth0", dst: "192.168.16.0/24", metric: 10}, | ||
} | ||
}) | ||
``` | ||
|
||
Graph will automatically build edges for all items based on their dependencies. | ||
In the example above, there will be one edge from each route pointing to the `eth0` interface. | ||
`InitArgs` can also contain the initial content of subgraphs. | ||
|
||
A single item is manipulated using `Item()`, `PutItem()` and `DelItem()` methods: | ||
|
||
```go | ||
import ( | ||
"fmt" | ||
"github.com/lf-edge/eve/libs/depgraph" | ||
) | ||
|
||
// Add new Linux route (nil ItemState). | ||
item := LinuxRoute{via: "eth0", dst: "10.20.0.0/16", metric: 100} | ||
g.PutItem(item, nil) | ||
|
||
// Update the item. | ||
item.routeMetric = 50 | ||
g.PutItem(item, nil) | ||
|
||
// Delete the item. | ||
itemRef := Reference(item) | ||
g.DelItem(itemRef) | ||
|
||
// Read single graph item. | ||
item, found := g.Item(itemRef) | ||
if found { | ||
fmt.Printf("Linux interface %+v\n", item) | ||
} | ||
``` | ||
|
||
To iterate all items in the graph: | ||
|
||
```go | ||
inclSubGraphs := true | ||
iter := g.Items(inclSubGraphs) | ||
for iter.Next() { | ||
item, state := iter.Item() | ||
fmt.Printf("Item: %+v, with state: %+v\n", item, state) | ||
} | ||
``` | ||
|
||
To iterate all edges originating from an item: | ||
|
||
```go | ||
iter := g.OutgoingEdges(itemRef) | ||
for iter.Next() { | ||
e := iter.Edge() | ||
fmt.Printf("Edge from %s to %s for dep: %+v\n", | ||
e.FromItem, e.ToItem, e.Dependency) | ||
} | ||
``` | ||
|
||
Lastly, sub-graphs are manipulated using `SubGraph()`, `PutSubGraph()`, `DelSubGraph()` | ||
and `EditSubGraph()` methods: | ||
|
||
```go | ||
item1 := LinuxInterface{name: "eth0.1"} | ||
item2 := LinuxInterface{name: "eth0.2"} | ||
subG := depgraph.New(depgraph.InitArgs{ | ||
Name: "MySubGraph", | ||
Description: "This is my example sub-graph", | ||
Items: []Items{item1, item2}, | ||
}) | ||
|
||
// Add new subgraph | ||
g.PutSubGraph(subG) | ||
|
||
// Can use the same handle to edit the subgraph... | ||
subG.DelItem(Reference(item1)) | ||
|
||
// ... or a read-write handle can be retrieved: | ||
readSubG := g.SubGraph("MySubGraph") // can only read nodes, edges, etc. | ||
subG = g.EditSubGraph(readSubG) // elevate to read-write | ||
subG.Putitem(item1, nil) | ||
|
||
// Remove the subgraph: | ||
g.DelSubGraph("MySubGraph") | ||
``` | ||
|
||
## Visualization | ||
|
||
The graph content can be exported into [DOT](https://en.wikipedia.org/wiki/DOT_(graph_description_language)) | ||
using `DotExporter` and then visualized for example using [Graphviz](https://graphviz.org/). | ||
Subgraphs are drawn as [clusters](https://graphviz.org/Gallery/directed/cluster.html), | ||
i.e. items they contain are plotted near each other and contained within a rectangle. | ||
|
||
Example usage (incl. Graphviz with a wrapper for Go): | ||
|
||
```go | ||
// import "github.com/goccy/go-graphviz" | ||
|
||
// Initialize DOT exporter | ||
exporter := &DotExporter{CheckDeps: true} | ||
|
||
// Render DOT representation of the dependency graph. | ||
dot, err := exporter.Export(graph) | ||
if err != nil { | ||
log.Fatalf("depgraph DOT rendering failed: %v", err) | ||
} | ||
|
||
// Use go-graphviz - a Graphviz wrapper for Go. | ||
gvizGraph, err := graphviz.ParseBytes([]byte(dot)) | ||
if err != nil { | ||
log.Fatalf("failed to parse DOT: %v", err) | ||
} | ||
gviz := graphviz.New() | ||
err = gviz.RenderFilename(gvizGraph, graphviz.PNG, "/path/to/graph.png") | ||
if err != nil { | ||
log.Fatal(err) | ||
} | ||
``` | ||
|
||
Example of a rendered depgraph: | ||
|
||
![graph visualization example](./pics/graph-example.png) |
Oops, something went wrong.