From 8ca1e34d4a34eb877e92a9c538828f86e000ee35 Mon Sep 17 00:00:00 2001
From: David Morrison <drmorr@appliedcomputing.io>
Date: Thu, 25 Jan 2024 15:38:35 -0800
Subject: [PATCH] compute pod recreation for deployments

---
 fireconfig/__init__.py |  6 +++---
 fireconfig/output.py   | 10 +++++-----
 fireconfig/plan.py     | 38 ++++++++++++++++++++++++++++----------
 fireconfig/subgraph.py |  9 +++++----
 4 files changed, 41 insertions(+), 22 deletions(-)

diff --git a/fireconfig/__init__.py b/fireconfig/__init__.py
index aa11bd5..1dada0a 100644
--- a/fireconfig/__init__.py
+++ b/fireconfig/__init__.py
@@ -63,8 +63,8 @@ def compile(pkgs: T.Dict[str, T.List[AppPackage]], dag_filename: T.Optional[str]
 
     for obj in DependencyGraph(app.node).root.outbound:
         walk_dep_graph(obj, subgraphs)
-    diff = compute_diff(app)
-    resource_changes = get_resource_changes(diff)
+    diff, kinds = compute_diff(app)
+    resource_changes = get_resource_changes(diff, kinds)
 
     try:
         find_deleted_nodes(subgraphs, resource_changes, dag_filename)
@@ -74,6 +74,6 @@ def compile(pkgs: T.Dict[str, T.List[AppPackage]], dag_filename: T.Optional[str]
     graph_str = format_mermaid_graph(subgraph_dag, subgraphs, dag_filename, resource_changes)
     diff_str = format_diff(resource_changes)
 
-    app.synth()
+    # app.synth()
 
     return graph_str, diff_str
diff --git a/fireconfig/output.py b/fireconfig/output.py
index fa74538..7b2bc82 100644
--- a/fireconfig/output.py
+++ b/fireconfig/output.py
@@ -12,9 +12,9 @@
 from fireconfig.subgraph import ChartSubgraph
 
 
-def _format_node_label(n: str, ty: str) -> str:
-    name = n.split("/")[-1]
-    return f"  {n}[<b>{ty}</b><br>{name}]\n"
+def _format_node_label(node: str, kind: str) -> str:
+    name = node.split("/")[-1]
+    return f"  {node}[<b>{kind}</b><br>{name}]\n"
 
 
 def format_mermaid_graph(
@@ -34,8 +34,8 @@ def format_mermaid_graph(
     for chart, sg in subgraphs.items():
         mermaid += f"subgraph {chart}\n"
         mermaid += "  direction LR\n"
-        for n, ty in sg.nodes():
-            mermaid += _format_node_label(n, ty)
+        for n, k in sg.nodes():
+            mermaid += _format_node_label(n, k)
 
         for s, e in sg.edges():
             mermaid += f"  {s}--->{e}\n"
diff --git a/fireconfig/plan.py b/fireconfig/plan.py
index df3f930..f23c0f6 100644
--- a/fireconfig/plan.py
+++ b/fireconfig/plan.py
@@ -30,6 +30,7 @@ class ResourceState(Enum):
     ChangedWithPodRecreate = "#cb4"
     Added = "#283"
     Removed = "#e67"
+    Unknown = "#f00"
 
 
 class ResourceChanges:
@@ -45,14 +46,29 @@ def state(self) -> ResourceState:
     def changes(self) -> T.List[ChangeTuple]:
         return self._changes
 
-    def update_state(self, change_type: str, path: str):
-        if self._state not in {ResourceState.Unchanged, ResourceState.Changed}:
+    def update_state(self, change_type: str, path: str, kind: T.Optional[str]):
+        if self._state in {ResourceState.Added, ResourceState.Removed}:
             return
 
-        if change_type == "dictionary_item_removed":
-            self._state = ResourceState.Removed if path == "root" else ResourceState.Changed
-        elif change_type == "dictionary_item_added":
-            self._state = ResourceState.Added if path == "root" else ResourceState.Changed
+        if path == "root":
+            if change_type == "dictionary_item_removed":
+                self._state = ResourceState.Removed
+            elif change_type == "dictionary_item_added":
+                self._state = ResourceState.Added
+            else:
+                self._state = ResourceState.Unknown
+        elif self._state == ResourceState.ChangedWithPodRecreate:
+            return
+        elif kind == "Deployment":
+            # TODO - this is obviously incomplete, it will not detect all cases
+            # when pod recreation happens
+            if (
+                path.startswith("root['spec']['template']['spec']")
+                or path.startswith("root['spec']['selector']")
+            ):
+                self._state = ResourceState.ChangedWithPodRecreate
+            else:
+                self._state = ResourceState.Changed
         else:
             self._state = ResourceState.Changed
 
@@ -60,7 +76,8 @@ def add_change(self, path: str, r1: T.Union[T.Mapping, notpresent], r2: T.Union[
         self._changes.append((path, r1, r2))
 
 
-def compute_diff(app: App) -> T.Mapping[str, T.Any]:
+def compute_diff(app: App) -> T.Tuple[T.Mapping[str, T.Any], T.Mapping[str, str]]:
+    kinds = {}
     old_defs = {}
     for filename in glob(f"{app.outdir}/*{app.output_file_extension}"):
         with open(filename) as f:
@@ -77,8 +94,9 @@ def compute_diff(app: App) -> T.Mapping[str, T.Any]:
         for new_obj in chart.api_objects:
             node_id = owned_name(new_obj)
             new_defs[node_id] = new_obj.to_json()
+            kinds[node_id] = new_obj.kind
 
-    return DeepDiff(old_defs, new_defs, view="tree")
+    return DeepDiff(old_defs, new_defs, view="tree"), kinds
 
 
 def walk_dep_graph(v: DependencyVertex, subgraphs: T.Mapping[str, ChartSubgraph]):
@@ -98,13 +116,13 @@ def walk_dep_graph(v: DependencyVertex, subgraphs: T.Mapping[str, ChartSubgraph]
         walk_dep_graph(dep, subgraphs)
 
 
-def get_resource_changes(diff: T.Mapping[str, T.Any]) -> T.Mapping[str, ResourceChanges]:
+def get_resource_changes(diff: T.Mapping[str, T.Any], kinds: T.Mapping[str, str]) -> T.Mapping[str, ResourceChanges]:
     resource_changes: T.MutableMapping[str, ResourceChanges] = defaultdict(lambda: ResourceChanges())
     for change_type, items in diff.items():
         for i in items:
             root_item = i.path(output_format="list")[0]
             path = re.sub(r"\[" + f"'{root_item}'" + r"\]", "", i.path())
-            resource_changes[root_item].update_state(change_type, path)
+            resource_changes[root_item].update_state(change_type, path, kinds.get(root_item))
             resource_changes[root_item].add_change(path, i.t1, i.t2)
 
     return resource_changes
diff --git a/fireconfig/subgraph.py b/fireconfig/subgraph.py
index daab37f..55e16b9 100644
--- a/fireconfig/subgraph.py
+++ b/fireconfig/subgraph.py
@@ -11,12 +11,13 @@ class ChartSubgraph:
     def __init__(self, name: str) -> None:
         self._name = name
         self._dag: T.MutableMapping[str, T.List[str]] = defaultdict(list)
-        self._resource_types: T.MutableMapping[str, str] = {}
+        self._kinds: T.MutableMapping[str, str] = {}
         self._deleted_lines: T.Set[str] = set()
 
     def add_node(self, v: DependencyVertex) -> str:
-        name = owned_name(T.cast(ApiObject, v.value))
-        self._resource_types[name] = type(v.value).__name__.replace("Kube", "")
+        obj = T.cast(ApiObject, v.value)
+        name = owned_name(obj)
+        self._kinds[name] = obj.kind
         self._dag[name]
         return name
 
@@ -29,7 +30,7 @@ def add_deleted_line(self, l: str):
         self._deleted_lines.add(l)
 
     def nodes(self) -> T.List[T.Tuple[str, str]]:
-        return [(n, self._resource_types[n]) for n in self._dag.keys()]
+        return [(n, self._kinds[n]) for n in self._dag.keys()]
 
     def edges(self) -> T.List[T.Tuple[str, str]]:
         return [(s, e) for s, l in self._dag.items() for e in l]