Skip to content

Commit 722b692

Browse files
authored
feat: Collect from Resoto directly (#47)
1 parent 0affa1d commit 722b692

File tree

4 files changed

+151
-2
lines changed

4 files changed

+151
-2
lines changed

cloud2sql/collect_plugins.py

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@
3333
from cloud2sql.arrow.config import ArrowOutputConfig
3434
from cloud2sql.show_progress import CollectInfo
3535
from cloud2sql.sql import SqlUpdater, sql_updater
36+
from cloud2sql.remote_graph import RemoteGraphCollector
3637

3738
try:
3839
from cloud2sql.arrow.model import ArrowModel
@@ -65,10 +66,18 @@ def collectors(raw_config: Json, feedback: CoreFeedback) -> Dict[str, BaseCollec
6566
log.info(f"Found collector {plugin_class.cloud} ({plugin_class.__name__})")
6667
plugin_class.add_config(config)
6768
plugin = plugin_class()
68-
if hasattr(plugin, "core_feedback"):
69-
setattr(plugin, "core_feedback", feedback.with_context(plugin.cloud))
7069
result[plugin_class.cloud] = plugin
7170

71+
# lookup local plugins
72+
if RemoteGraphCollector.cloud in raw_config:
73+
log.info(f"Found collector {RemoteGraphCollector.cloud} ({RemoteGraphCollector.__name__})")
74+
result[RemoteGraphCollector.cloud] = RemoteGraphCollector()
75+
RemoteGraphCollector.add_config(config)
76+
77+
for plugin in result.values():
78+
if hasattr(plugin, "core_feedback"):
79+
setattr(plugin, "core_feedback", feedback.with_context(plugin.cloud))
80+
7281
Config.init_default_config()
7382
Config.running_config.data = {**Config.running_config.data, **Config.read_config(raw_config)}
7483
return result

cloud2sql/remote_graph.py

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
from typing import Dict, Iterator
2+
from typing import Optional, ClassVar
3+
4+
from attr import define, field
5+
from resotoclient import ResotoClient, JsObject
6+
from resotolib.baseplugin import BaseCollectorPlugin
7+
from resotolib.baseresources import (
8+
BaseResource,
9+
Cloud,
10+
EdgeType,
11+
UnknownZone,
12+
UnknownRegion,
13+
UnknownAccount,
14+
)
15+
from resotolib.config import Config
16+
from resotolib.core.actions import CoreFeedback
17+
from resotolib.core.model_export import node_from_dict
18+
from resotolib.graph import Graph
19+
from resotolib.json import value_in_path
20+
from resotolib.logger import log
21+
from resotolib.types import Json
22+
23+
24+
@define
25+
class RemoteGraphConfig:
26+
kind: ClassVar[str] = "remote_graph"
27+
resoto_url: str = field(default="https://localhost:8900", metadata={"description": "URL of the resoto server"})
28+
psk: Optional[str] = field(default=None, metadata={"description": "Pre-shared key for the resoto server"})
29+
graph: str = field(default="resoto", metadata={"description": "Name of the graph to use"})
30+
search: Optional[str] = field(
31+
default=None, metadata={"description": "Search string to filter resources. None to get all resources."}
32+
)
33+
34+
35+
carz = {"cloud": Cloud, "account": UnknownAccount, "region": UnknownRegion, "zone": UnknownZone}
36+
37+
38+
class RemoteGraphCollector(BaseCollectorPlugin):
39+
cloud = "remote_graph"
40+
41+
def __init__(self) -> None:
42+
super().__init__()
43+
self.core_feedback: Optional[CoreFeedback] = None
44+
45+
@staticmethod
46+
def add_config(cfg: Config) -> None:
47+
cfg.add_config(RemoteGraphConfig)
48+
49+
def collect(self) -> None:
50+
try:
51+
self.graph = self._collect_remote_graph()
52+
except Exception as ex:
53+
if self.core_feedback:
54+
self.core_feedback.error(f"Unhandled exception in Remote Plugin: {ex}", log)
55+
else:
56+
log.error(f"No CoreFeedback available! Unhandled exception in RemoteGraph Plugin: {ex}")
57+
raise
58+
59+
def _collect_remote_graph(self) -> Graph:
60+
config: RemoteGraphConfig = Config.remote_graph
61+
client = ResotoClient(config.resoto_url, psk=config.psk)
62+
search = config.search or "is(graph_root) -[2:]->"
63+
return self._collect_from_graph_iterator(client.search_graph(search, graph=config.graph))
64+
65+
def _collect_from_graph_iterator(self, graph_iterator: Iterator[JsObject]) -> Graph:
66+
assert self.core_feedback, "No CoreFeedback available!"
67+
graph = Graph()
68+
lookup: Dict[str, BaseResource] = {}
69+
self.core_feedback.progress_done("Remote Graph", 0, 1)
70+
71+
def set_carz(jsc: Json, rs: BaseResource) -> None:
72+
for ancestor, clazz in carz.items():
73+
path = ["ancestors", ancestor, "reported"]
74+
if (cv := value_in_path(jsc, path)) and (cid := cv.get("id")) and (cname := cv.get("name")):
75+
resource = lookup.get(cid, clazz(id=cid, name=cname)) # type: ignore
76+
lookup[cid] = resource
77+
setattr(rs, f"_{ancestor}", resource) # xxx is defined by _xxx property
78+
79+
for js in graph_iterator:
80+
if js.get("type") == "node" and isinstance(js, dict):
81+
node = node_from_dict(js)
82+
set_carz(js, node)
83+
lookup[js["id"]] = node
84+
graph.add_node(node)
85+
elif js.get("type") == "edge":
86+
if (node_from := lookup.get(js["from"])) and (node_to := lookup.get(js["to"])):
87+
graph.add_edge(node_from, node_to, edge_type=EdgeType.default)
88+
else:
89+
raise ValueError(f"Unknown type: {js.get('type')}")
90+
self.core_feedback.progress_done("Remote Graph", 1, 1)
91+
return graph

tests/conftest.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
from queue import Queue
22
from typing import List, Iterator
33

4+
from resoto_plugin_example_collector import ExampleAccount, ExampleRegion, ExampleInstance, ExampleVolume
45
from resotoclient.models import Model, Kind, Property
56
from pytest import fixture
67
from resotolib.args import Namespace
8+
from resotolib.baseresources import GraphRoot, Cloud
79
from resotolib.core.actions import CoreFeedback
10+
from resotolib.graph import Graph
811
from resotolib.types import Json
912
from sqlalchemy.engine import create_engine, Engine
1013

@@ -60,6 +63,27 @@ def model() -> Model:
6063
return Model({k.fqn: k for k in kinds})
6164

6265

66+
@fixture
67+
def example_collector_graph() -> Graph:
68+
root = GraphRoot(id="root")
69+
graph = Graph(root=root)
70+
cloud = Cloud(id="example")
71+
account = ExampleAccount(id="example-account")
72+
region = ExampleRegion(id="example-region")
73+
i1 = ExampleInstance(id="example-instance-1")
74+
iv1 = ExampleVolume(id="example-volume-1")
75+
i2 = ExampleInstance(id="example-instance-2")
76+
iv2 = ExampleVolume(id="example-volume-1")
77+
graph.add_resource(root, cloud)
78+
graph.add_resource(cloud, account)
79+
graph.add_resource(account, region)
80+
graph.add_resource(region, i1)
81+
graph.add_resource(i1, iv1)
82+
graph.add_resource(region, i2)
83+
graph.add_resource(i2, iv2)
84+
return graph
85+
86+
6387
@fixture()
6488
def args() -> Namespace:
6589
return Namespace()

tests/remote_graph_test.py

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from typing import Iterator
2+
3+
from resotoclient import JsObject
4+
from resotolib.core.actions import CoreFeedback
5+
from resotolib.core.model_export import node_to_dict
6+
from resotolib.graph import Graph
7+
8+
from cloud2sql.remote_graph import RemoteGraphCollector
9+
10+
11+
def test_remote_graph_collector(example_collector_graph: Graph, core_feedback: CoreFeedback) -> None:
12+
def graph_iterator() -> Iterator[JsObject]:
13+
for node in example_collector_graph.nodes:
14+
fn = node_to_dict(node)
15+
fn["type"] = "node"
16+
yield fn
17+
18+
for from_node, to_node in example_collector_graph.edges():
19+
yield {"type": "edge", "from": from_node.chksum, "to": to_node.chksum, "edge_type": "default"}
20+
21+
collector = RemoteGraphCollector()
22+
collector.core_feedback = core_feedback
23+
graph_again = collector._collect_from_graph_iterator(graph_iterator())
24+
assert len(graph_again.nodes) == len(example_collector_graph.nodes)
25+
assert len(graph_again.edges) == len(example_collector_graph.edges)

0 commit comments

Comments
 (0)