splight-cli 4.9.0__tar.gz → 4.10.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {splight_cli-4.9.0 → splight_cli-4.10.0}/PKG-INFO +2 -3
- {splight_cli-4.9.0 → splight_cli-4.10.0}/pyproject.toml +3 -3
- splight_cli-4.10.0/splight_cli/solution/__init__.py +88 -0
- splight_cli-4.10.0/splight_cli/solution/dict.py +50 -0
- splight_cli-4.10.0/splight_cli/solution/parser.py +103 -0
- splight_cli-4.10.0/splight_cli/solution/resource/__init__.py +5 -0
- splight_cli-4.10.0/splight_cli/solution/resource/diff.py +39 -0
- splight_cli-4.10.0/splight_cli/solution/resource/logger.py +36 -0
- splight_cli-4.10.0/splight_cli/solution/resource/manager.py +237 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/__init__.py +25 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/alert.py +14 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/asset.py +8 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/attribute.py +8 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/base.py +77 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/component.py +12 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/component_object.py +8 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/file.py +8 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/function.py +14 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/metadata.py +8 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/routine.py +9 -0
- splight_cli-4.10.0/splight_cli/solution/resource/models/secret.py +38 -0
- splight_cli-4.10.0/splight_cli/solution/state.py +56 -0
- splight_cli-4.9.0/splight_cli/solution/__init__.py +0 -103
- splight_cli-4.9.0/splight_cli/solution/apply_exec.py +0 -180
- splight_cli-4.9.0/splight_cli/solution/destroyer.py +0 -42
- splight_cli-4.9.0/splight_cli/solution/exceptions.py +0 -6
- splight_cli-4.9.0/splight_cli/solution/importer.py +0 -87
- splight_cli-4.9.0/splight_cli/solution/models.py +0 -45
- splight_cli-4.9.0/splight_cli/solution/plan_exec.py +0 -123
- splight_cli-4.9.0/splight_cli/solution/replacer.py +0 -357
- splight_cli-4.9.0/splight_cli/solution/solution.py +0 -466
- splight_cli-4.9.0/splight_cli/solution/solution_checker.py +0 -409
- splight_cli-4.9.0/splight_cli/solution/tests/constants.py +0 -559
- splight_cli-4.9.0/splight_cli/solution/tests/test_apply.py +0 -244
- splight_cli-4.9.0/splight_cli/solution/tests/test_destroy.py +0 -107
- splight_cli-4.9.0/splight_cli/solution/tests/test_import.py +0 -154
- splight_cli-4.9.0/splight_cli/solution/tests/test_plan.py +0 -52
- splight_cli-4.9.0/splight_cli/solution/utils.py +0 -80
- {splight_cli-4.9.0 → splight_cli-4.10.0}/README.md +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/cli.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/component.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/exceptions.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/loaders.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/templates/.splightignore +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/templates/Initialization +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/templates/README.md +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/templates/auto_readme.md +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/templates/main.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/templates/spec.json +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/templates/tests.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/tests/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/tests/test_component_manager.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/component/utils.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/config/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/constants.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/context/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/context/context.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/context/workspace.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/alert/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/asset/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/attribute/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/component/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/datalake/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/file/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/manager/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/manager/exceptions.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/manager/manager.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/manager/tests/test_datalake_manager.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/manager/tests/test_resource_manager.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/engine/secret/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/hub/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/hub/component/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/hub/component/exceptions.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/hub/component/hub_manager.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/hub/component/tests/test_hub_manager.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/settings.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/tests/test_component/Initialization +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/tests/test_component/README.md +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/tests/test_component/main.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/tests/test_component/spec.json +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/tests/test_component/tests.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/utils/__init__.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/utils/input.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/utils/json.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/utils/loader.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/utils/pprint.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/utils/template.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/utils/yaml.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/version.py +0 -0
- {splight_cli-4.9.0 → splight_cli-4.10.0}/splight_cli/workspace/__init__.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: splight-cli
|
|
3
|
-
Version: 4.
|
|
3
|
+
Version: 4.10.0
|
|
4
4
|
Summary: Splight Command Line Interface
|
|
5
5
|
Author: Splight Dev
|
|
6
6
|
Author-email: dev@splight-ae.com
|
|
@@ -15,8 +15,6 @@ Requires-Dist: case-converter (==1.1.0)
|
|
|
15
15
|
Requires-Dist: click (==8.0.4)
|
|
16
16
|
Requires-Dist: click-default-group (==1.2.2)
|
|
17
17
|
Requires-Dist: colorama (==0.4.4)
|
|
18
|
-
Requires-Dist: deepdiff (==6.3.1)
|
|
19
|
-
Requires-Dist: mock (>=5.1.0,<6.0.0)
|
|
20
18
|
Requires-Dist: packaging (==23.0)
|
|
21
19
|
Requires-Dist: pathspec (==0.11.1)
|
|
22
20
|
Requires-Dist: py7zr (==0.20.8)
|
|
@@ -24,6 +22,7 @@ Requires-Dist: requests (==2.31.0)
|
|
|
24
22
|
Requires-Dist: rich (==13.0.1)
|
|
25
23
|
Requires-Dist: setuptools (==60.9.3)
|
|
26
24
|
Requires-Dist: splight-lib (>=5.2.2,<6.0.0)
|
|
25
|
+
Requires-Dist: toposort (>=1.10,<2.0)
|
|
27
26
|
Requires-Dist: typer (==0.9.0)
|
|
28
27
|
Description-Content-Type: text/markdown
|
|
29
28
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "splight-cli"
|
|
3
|
-
version = "4.
|
|
3
|
+
version = "4.10.0"
|
|
4
4
|
description = "Splight Command Line Interface"
|
|
5
5
|
authors = ["Splight Dev <dev@splight-ae.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -20,8 +20,7 @@ PyYAML = "6.0.1"
|
|
|
20
20
|
rich = "13.0.1"
|
|
21
21
|
pathspec = "0.11.1"
|
|
22
22
|
packaging = "23.0"
|
|
23
|
-
|
|
24
|
-
mock = "^5.1.0"
|
|
23
|
+
toposort = "^1.10"
|
|
25
24
|
|
|
26
25
|
[tool.poetry.group.dev.dependencies]
|
|
27
26
|
black = "23.3.0"
|
|
@@ -32,6 +31,7 @@ flake8 = "^6.1.0"
|
|
|
32
31
|
pre-commit = "3.2.2"
|
|
33
32
|
pytest = "^7.4.3"
|
|
34
33
|
coverage = "^7.3.2"
|
|
34
|
+
mock = "^5.1.0"
|
|
35
35
|
|
|
36
36
|
[tool.poetry.scripts]
|
|
37
37
|
splight = "splight_cli.cli:app"
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from rich.console import Console
|
|
3
|
+
|
|
4
|
+
from splight_cli.context import check_credentials
|
|
5
|
+
from splight_cli.solution.parser import Parser
|
|
6
|
+
from splight_cli.solution.resource import ResourceManager
|
|
7
|
+
from splight_cli.solution.state import State
|
|
8
|
+
|
|
9
|
+
solution_app = typer.Typer(
|
|
10
|
+
name="Splight Solution",
|
|
11
|
+
add_completion=True,
|
|
12
|
+
rich_markup_mode="rich",
|
|
13
|
+
no_args_is_help=True,
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
console = Console()
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@solution_app.callback(invoke_without_command=True)
|
|
20
|
+
def callback(ctx: typer.Context):
|
|
21
|
+
check_credentials(ctx)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# FIXME: find a nice way to print the exceptions.
|
|
25
|
+
# Take into account that most exception messages are short and do not have
|
|
26
|
+
# that much content. I would include the traceback, or at least some part of it.
|
|
27
|
+
# Another option is to not catch them, and show the entire error, so the user can
|
|
28
|
+
# debug it.
|
|
29
|
+
@solution_app.command()
|
|
30
|
+
def apply(
|
|
31
|
+
_: typer.Context,
|
|
32
|
+
spec_file: str = typer.Argument(..., help="Path to the spec file."),
|
|
33
|
+
state_file: str = typer.Option(
|
|
34
|
+
"state.json", "--state", "-s", help="Path to the state file."
|
|
35
|
+
),
|
|
36
|
+
) -> None:
|
|
37
|
+
state = State(path=state_file)
|
|
38
|
+
state.load()
|
|
39
|
+
|
|
40
|
+
parser = Parser(spec_file=spec_file)
|
|
41
|
+
specs, dependency_graph = parser.parse()
|
|
42
|
+
|
|
43
|
+
manager = ResourceManager(
|
|
44
|
+
state=state,
|
|
45
|
+
specs=specs,
|
|
46
|
+
dependency_graph=dependency_graph,
|
|
47
|
+
)
|
|
48
|
+
manager.refresh()
|
|
49
|
+
plan = manager.plan()
|
|
50
|
+
if plan:
|
|
51
|
+
manager.apply()
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
@solution_app.command()
|
|
55
|
+
def plan(
|
|
56
|
+
_: typer.Context,
|
|
57
|
+
spec_file: str = typer.Argument(..., help="Path to the spec file."),
|
|
58
|
+
state_file: str = typer.Option(
|
|
59
|
+
"state.json", "--state", "-s", help="Path to the state file."
|
|
60
|
+
),
|
|
61
|
+
) -> None:
|
|
62
|
+
state = State(path=state_file)
|
|
63
|
+
state.load()
|
|
64
|
+
|
|
65
|
+
parser = Parser(spec_file=spec_file)
|
|
66
|
+
specs, dependency_graph = parser.parse()
|
|
67
|
+
|
|
68
|
+
manager = ResourceManager(
|
|
69
|
+
state=state,
|
|
70
|
+
specs=specs,
|
|
71
|
+
dependency_graph=dependency_graph,
|
|
72
|
+
)
|
|
73
|
+
manager.refresh()
|
|
74
|
+
manager.plan()
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
@solution_app.command()
|
|
78
|
+
def refresh(
|
|
79
|
+
_: typer.Context,
|
|
80
|
+
state_file: str = typer.Option(
|
|
81
|
+
"state.json", "--state", "-s", help="Path to the state file."
|
|
82
|
+
),
|
|
83
|
+
) -> None:
|
|
84
|
+
state = State(path=state_file)
|
|
85
|
+
state.load()
|
|
86
|
+
|
|
87
|
+
manager = ResourceManager(state=state)
|
|
88
|
+
manager.refresh()
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
from typing import Any, Dict, List
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
# FIXME: make this faster using a stack
|
|
5
|
+
def walk_dict(data: Dict, current=[]):
|
|
6
|
+
result = []
|
|
7
|
+
|
|
8
|
+
if isinstance(data, Dict):
|
|
9
|
+
for key, value in data.items():
|
|
10
|
+
current.append(key)
|
|
11
|
+
result.extend(walk_dict(value, current.copy()))
|
|
12
|
+
current.pop()
|
|
13
|
+
elif isinstance(data, List):
|
|
14
|
+
for index, item in enumerate(data):
|
|
15
|
+
current.append(index)
|
|
16
|
+
result.extend(walk_dict(item, current.copy()))
|
|
17
|
+
current.pop()
|
|
18
|
+
else:
|
|
19
|
+
result.append((current.copy(), data))
|
|
20
|
+
|
|
21
|
+
return result
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_dict_value(path: List, data: Dict):
|
|
25
|
+
# Assumes the path exists
|
|
26
|
+
current = data
|
|
27
|
+
for key in path:
|
|
28
|
+
if isinstance(current, dict):
|
|
29
|
+
current = current[key]
|
|
30
|
+
elif isinstance(current, list):
|
|
31
|
+
current = current[int(key)]
|
|
32
|
+
else:
|
|
33
|
+
raise ValueError(f"Invalid path: {path}")
|
|
34
|
+
return current
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def set_dict_value(value: Any, path: List, data: Dict):
|
|
38
|
+
# Assumes the path exists
|
|
39
|
+
current = data
|
|
40
|
+
for key in path[:-1]:
|
|
41
|
+
current = current[key]
|
|
42
|
+
|
|
43
|
+
last_key = path[-1]
|
|
44
|
+
|
|
45
|
+
if isinstance(current, dict):
|
|
46
|
+
current[last_key] = value
|
|
47
|
+
elif isinstance(current, list):
|
|
48
|
+
current[int(last_key)] = value
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(f"Invalid path: {path}")
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import re
|
|
2
|
+
from typing import Dict, List, Optional
|
|
3
|
+
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from splight_cli.solution.dict import walk_dict
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class DuplicateResourceError(Exception):
|
|
10
|
+
pass
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def parse_reference(value: str) -> Optional[Dict]:
|
|
14
|
+
"""Return the reference if it matches the regex or None otherwise"""
|
|
15
|
+
# FIXME: make it so we are able to reference the 'name' of a resource.
|
|
16
|
+
# This is super easy to do:
|
|
17
|
+
# 1. Add '.id' and '.name' at the end of the regex
|
|
18
|
+
# 2. Save that accessor to the references dict in each spec.
|
|
19
|
+
# 3. Change the 'replace_references' function in ResourceManager so it
|
|
20
|
+
# gets the correct attribute.
|
|
21
|
+
|
|
22
|
+
# Only process strings
|
|
23
|
+
if isinstance(value, str):
|
|
24
|
+
# Building this with format strings breaks the expression
|
|
25
|
+
pattern = r"^\${{(Asset|Attribute|Metadata|File|Function|Secret|Alert|Component|Routine)\.(\w+)}}$"
|
|
26
|
+
|
|
27
|
+
# Get the matches
|
|
28
|
+
match = re.match(pattern, value)
|
|
29
|
+
|
|
30
|
+
if match:
|
|
31
|
+
type, name = match.groups()
|
|
32
|
+
key = f"{type}:{name}"
|
|
33
|
+
return key
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class Parser:
|
|
37
|
+
def __init__(self, spec_file: str) -> None:
|
|
38
|
+
try:
|
|
39
|
+
with open(spec_file, "r") as fp:
|
|
40
|
+
self._spec_file_data = yaml.safe_load(fp)
|
|
41
|
+
except FileNotFoundError:
|
|
42
|
+
raise Exception(f"Spec file '{spec_file}' not found.")
|
|
43
|
+
|
|
44
|
+
def parse(self) -> List[Dict]:
|
|
45
|
+
# Here we build the dependency graph using the resource keys
|
|
46
|
+
dependency_graph = {}
|
|
47
|
+
|
|
48
|
+
# - Does not destroy all elements.
|
|
49
|
+
specs = {}
|
|
50
|
+
for resource_spec in self._spec_file_data or []:
|
|
51
|
+
name = resource_spec["name"]
|
|
52
|
+
type = resource_spec["type"]
|
|
53
|
+
arguments = resource_spec["arguments"]
|
|
54
|
+
|
|
55
|
+
# Parse each resource leaf value to see if its a reference
|
|
56
|
+
depends_on = set({})
|
|
57
|
+
references = []
|
|
58
|
+
for path, value in walk_dict(resource_spec["arguments"]):
|
|
59
|
+
result = parse_reference(value)
|
|
60
|
+
if result is not None:
|
|
61
|
+
key = result
|
|
62
|
+
|
|
63
|
+
# Must be done here, otherwise it would affect the dependency graph.
|
|
64
|
+
# Paths and values are validated later on, since we may need data
|
|
65
|
+
# that is available after creation.
|
|
66
|
+
# This also prevents trying to delete a resource which has another
|
|
67
|
+
# one referencing it.
|
|
68
|
+
# Also prevents self-referencing.
|
|
69
|
+
if key not in specs:
|
|
70
|
+
raise ValueError(
|
|
71
|
+
f"Reference '{value}' in resource '{name}' of type '{type}' points to a non existing resource."
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
reference = {
|
|
75
|
+
"key": key, # Key of the referenced resource
|
|
76
|
+
"target": path, # Where to put the value
|
|
77
|
+
"string": value, # The string value of the reference
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
references.append(reference)
|
|
81
|
+
depends_on.add(key)
|
|
82
|
+
|
|
83
|
+
key = f"{type}:{name}"
|
|
84
|
+
if key in specs:
|
|
85
|
+
raise DuplicateResourceError(
|
|
86
|
+
f"Resource '{name}' of type '{type}' is defined twice."
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
specs[key] = {
|
|
90
|
+
"name": name,
|
|
91
|
+
"type": type,
|
|
92
|
+
"depends_on": list(depends_on),
|
|
93
|
+
"references": references,
|
|
94
|
+
"arguments": arguments,
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
# Add its dependecies to the graph
|
|
98
|
+
dependency_graph[key] = depends_on
|
|
99
|
+
|
|
100
|
+
# NOTE: Before adding/changing features of the parser, insert a breakpoint
|
|
101
|
+
# here and inspect the specs and dependency_graph objects, to see the shape
|
|
102
|
+
# of the objects that the rest of the code needs.
|
|
103
|
+
return specs, dependency_graph
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
from typing import Dict
|
|
2
|
+
|
|
3
|
+
from colorama import Fore, Style
|
|
4
|
+
|
|
5
|
+
from splight_cli.solution.dict import get_dict_value, walk_dict
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def strike_text(text):
|
|
9
|
+
return "".join(["\u0336{}".format(c) for c in str(text)])
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def Diff(new_arguments: Dict, old_arguments: Dict) -> Dict:
|
|
13
|
+
# FIXME: this try catch mechanic could be simplified by improving
|
|
14
|
+
# the 'Dict' related functions.
|
|
15
|
+
|
|
16
|
+
# FIXME: Try replacing the QueryFilter in a function for a single value.
|
|
17
|
+
# The diff will show incorrectly.
|
|
18
|
+
diff = []
|
|
19
|
+
for path, new_value in walk_dict(new_arguments):
|
|
20
|
+
try:
|
|
21
|
+
get_dict_value(path, old_arguments)
|
|
22
|
+
except:
|
|
23
|
+
line = f"\t{Fore.GREEN}+{Style.RESET_ALL} {'.'.join([str(key) for key in path])}: {new_value}"
|
|
24
|
+
diff.append(line)
|
|
25
|
+
continue
|
|
26
|
+
|
|
27
|
+
old_value = get_dict_value(path, old_arguments)
|
|
28
|
+
if new_value != old_value:
|
|
29
|
+
line = f"\t{Fore.YELLOW}~{Style.RESET_ALL} {'.'.join([str(key) for key in path])}: {strike_text(old_value)} -> {new_value} "
|
|
30
|
+
diff.append(line)
|
|
31
|
+
|
|
32
|
+
for path, old_value in walk_dict(old_arguments):
|
|
33
|
+
try:
|
|
34
|
+
get_dict_value(path, new_arguments)
|
|
35
|
+
except:
|
|
36
|
+
line = f"\t{Fore.RED}-{Style.RESET_ALL} {'.'.join([str(key) for key in path])}: {strike_text(old_value)}"
|
|
37
|
+
diff.append(line)
|
|
38
|
+
|
|
39
|
+
return diff
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
from colorama import Style
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class ResourceLogger:
|
|
5
|
+
template = '{type}["{name}"]: {message} [id={id}]'
|
|
6
|
+
|
|
7
|
+
@staticmethod
|
|
8
|
+
def _bold(message: str) -> str:
|
|
9
|
+
return f"{Style.BRIGHT}{message}{Style.RESET_ALL}"
|
|
10
|
+
|
|
11
|
+
def resource(self, message: str, resource):
|
|
12
|
+
formatted_message = self.template.format(
|
|
13
|
+
type=resource.type,
|
|
14
|
+
name=resource.name,
|
|
15
|
+
id=resource.id,
|
|
16
|
+
message=message,
|
|
17
|
+
)
|
|
18
|
+
print(self._bold(formatted_message))
|
|
19
|
+
|
|
20
|
+
def event(
|
|
21
|
+
self,
|
|
22
|
+
message: str,
|
|
23
|
+
previous_line: bool = False,
|
|
24
|
+
new_line: bool = False,
|
|
25
|
+
bold: bool = False,
|
|
26
|
+
):
|
|
27
|
+
formatted_message = ""
|
|
28
|
+
if previous_line:
|
|
29
|
+
formatted_message += "\n"
|
|
30
|
+
formatted_message += f"{self._bold(message)}" if bold else message
|
|
31
|
+
if new_line:
|
|
32
|
+
formatted_message += "\n"
|
|
33
|
+
print(formatted_message)
|
|
34
|
+
|
|
35
|
+
def diff(self, diff):
|
|
36
|
+
print("\n".join(diff))
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
from typing import Dict, List
|
|
2
|
+
|
|
3
|
+
from toposort import toposort_flatten as toposort
|
|
4
|
+
from typer import confirm
|
|
5
|
+
|
|
6
|
+
from splight_cli.solution.resource.diff import Diff
|
|
7
|
+
from splight_cli.solution.resource.logger import ResourceLogger
|
|
8
|
+
from splight_cli.solution.resource.models import (
|
|
9
|
+
Asset,
|
|
10
|
+
Attribute,
|
|
11
|
+
File,
|
|
12
|
+
Function,
|
|
13
|
+
Metadata,
|
|
14
|
+
Resource,
|
|
15
|
+
Secret,
|
|
16
|
+
)
|
|
17
|
+
from splight_cli.solution.state import State
|
|
18
|
+
|
|
19
|
+
type_map = {
|
|
20
|
+
"Asset": Asset,
|
|
21
|
+
"Attribute": Attribute,
|
|
22
|
+
"Metadata": Metadata,
|
|
23
|
+
"File": File,
|
|
24
|
+
"Function": Function,
|
|
25
|
+
"Secret": Secret,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# NOTE: One good thing about this approach, is that the code is idempotent.
|
|
30
|
+
# That means, if it fails, you can just fix your spec issues (inspect the traceback)
|
|
31
|
+
# and try again. The state won't loose its consistency.
|
|
32
|
+
# FIXME: Paralelize the processing of non dependent resources.
|
|
33
|
+
# Use toposort instead of toposort_flatten for this purpose.
|
|
34
|
+
class ResourceManager:
|
|
35
|
+
def __init__(
|
|
36
|
+
self,
|
|
37
|
+
state: State,
|
|
38
|
+
specs: List[Resource] = [],
|
|
39
|
+
dependency_graph: Dict = {},
|
|
40
|
+
) -> None:
|
|
41
|
+
self._state = state
|
|
42
|
+
self._specs = specs
|
|
43
|
+
|
|
44
|
+
# Sort the resource specs by dependency order
|
|
45
|
+
self._specs_order = toposort(dependency_graph)
|
|
46
|
+
|
|
47
|
+
self._logger = ResourceLogger()
|
|
48
|
+
|
|
49
|
+
def _create_resource(self, data: Dict):
|
|
50
|
+
type = data.get("type")
|
|
51
|
+
resource = type_map[type](**data)
|
|
52
|
+
return resource
|
|
53
|
+
|
|
54
|
+
def _update_references(
|
|
55
|
+
self, resource: Resource, resource_pool: List[Resource]
|
|
56
|
+
):
|
|
57
|
+
# Replace each reference of this resource
|
|
58
|
+
for reference in resource.references:
|
|
59
|
+
reference_key = reference["key"]
|
|
60
|
+
reference_target = reference["target"]
|
|
61
|
+
reference_string = reference["string"]
|
|
62
|
+
|
|
63
|
+
value = resource_pool[reference_key].id
|
|
64
|
+
if value is None:
|
|
65
|
+
value = reference_string
|
|
66
|
+
|
|
67
|
+
# Replace it in our arguments.
|
|
68
|
+
resource.set_argument_value(reference_target, value)
|
|
69
|
+
|
|
70
|
+
def refresh(self):
|
|
71
|
+
for key in self._state.all():
|
|
72
|
+
data = self._state.get(key)
|
|
73
|
+
resource = self._create_resource(data)
|
|
74
|
+
|
|
75
|
+
self._logger.resource("Refreshing state...", resource)
|
|
76
|
+
resource.refresh()
|
|
77
|
+
|
|
78
|
+
self._state.update(
|
|
79
|
+
resource.key,
|
|
80
|
+
resource.dump(),
|
|
81
|
+
)
|
|
82
|
+
self._state.save()
|
|
83
|
+
|
|
84
|
+
def plan(self):
|
|
85
|
+
# I would not alter the flow of this function since it covers up
|
|
86
|
+
# all the possible edits you make to the spec file.
|
|
87
|
+
|
|
88
|
+
plan = {}
|
|
89
|
+
resources = {}
|
|
90
|
+
|
|
91
|
+
# FIXME: dependency_graph order in delete
|
|
92
|
+
# First remove the state items not present in the spec file
|
|
93
|
+
for key in self._state.all():
|
|
94
|
+
if key not in self._specs:
|
|
95
|
+
data = self._state.get(key)
|
|
96
|
+
|
|
97
|
+
resource = self._create_resource(data)
|
|
98
|
+
|
|
99
|
+
diff = Diff(new_arguments={}, old_arguments=resource.arguments)
|
|
100
|
+
plan[key] = {"operation": "deleted", "diff": diff}
|
|
101
|
+
|
|
102
|
+
# Iterate over the resource specs in the spec file
|
|
103
|
+
for key in self._specs_order:
|
|
104
|
+
# If this resource does not exist yet
|
|
105
|
+
if not self._state.contains(key):
|
|
106
|
+
data = self._specs[key]
|
|
107
|
+
|
|
108
|
+
resource = self._create_resource(data)
|
|
109
|
+
resources[key] = resource
|
|
110
|
+
|
|
111
|
+
# Replace each reference of this resource.
|
|
112
|
+
# If it does not exist, we set the reference text
|
|
113
|
+
# just to show in the plan what value will receive later.
|
|
114
|
+
self._update_references(resource, resources)
|
|
115
|
+
|
|
116
|
+
# We do not have previous arguments because this a new
|
|
117
|
+
# resource.
|
|
118
|
+
diff = Diff(new_arguments=resource.arguments, old_arguments={})
|
|
119
|
+
plan[key] = {"operation": "created", "diff": diff}
|
|
120
|
+
|
|
121
|
+
# Already exists in the engine
|
|
122
|
+
else:
|
|
123
|
+
state_data = self._state.get(key)
|
|
124
|
+
state_resource = self._create_resource(state_data)
|
|
125
|
+
|
|
126
|
+
spec_data = self._specs[key]
|
|
127
|
+
spec_resource = self._create_resource(spec_data)
|
|
128
|
+
|
|
129
|
+
# Replace each reference of this resource
|
|
130
|
+
# Same thing as before.
|
|
131
|
+
self._update_references(spec_resource, resources)
|
|
132
|
+
|
|
133
|
+
diff = Diff(
|
|
134
|
+
new_arguments=spec_resource.arguments,
|
|
135
|
+
old_arguments=state_resource.arguments,
|
|
136
|
+
)
|
|
137
|
+
|
|
138
|
+
resources[key] = state_resource
|
|
139
|
+
|
|
140
|
+
# This resource changed if the diff contains at least a line.
|
|
141
|
+
# We ignore this resource if the arguments stay the same.
|
|
142
|
+
# Note that we are comparing both arguments with their references
|
|
143
|
+
# replaced.
|
|
144
|
+
if diff:
|
|
145
|
+
plan[key] = {"operation": "updated", "diff": diff}
|
|
146
|
+
|
|
147
|
+
if not plan:
|
|
148
|
+
self._logger.event(
|
|
149
|
+
"Your infrastructure matches the configuration."
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
# Just print some messages and the diff for each resource
|
|
153
|
+
self._logger.event(
|
|
154
|
+
"Splight solution will perform the following actions:",
|
|
155
|
+
)
|
|
156
|
+
for key, data in plan.items():
|
|
157
|
+
action = data["operation"]
|
|
158
|
+
diff = data["diff"]
|
|
159
|
+
|
|
160
|
+
type, name = key.split(":")
|
|
161
|
+
self._logger.event(
|
|
162
|
+
f"Resource '{name}' of type '{type}' will be {action}:",
|
|
163
|
+
previous_line=True,
|
|
164
|
+
)
|
|
165
|
+
self._logger.diff(diff)
|
|
166
|
+
|
|
167
|
+
return plan
|
|
168
|
+
|
|
169
|
+
def apply(self):
|
|
170
|
+
# I would not alter the flow of this function since it covers up
|
|
171
|
+
# all the possible edits you make to the spec file.
|
|
172
|
+
|
|
173
|
+
resources = {}
|
|
174
|
+
|
|
175
|
+
self._logger.event(
|
|
176
|
+
"Do you want to apply this changes?", bold=True, previous_line=True
|
|
177
|
+
)
|
|
178
|
+
if not confirm("Insert choice: "):
|
|
179
|
+
self._logger.event("No actions performed")
|
|
180
|
+
return
|
|
181
|
+
|
|
182
|
+
# FIXME: dependency_graph order in delete
|
|
183
|
+
# First remove the state items not present in the spec file
|
|
184
|
+
for key in self._state.all():
|
|
185
|
+
if key not in self._specs:
|
|
186
|
+
data = self._state.get(key)
|
|
187
|
+
|
|
188
|
+
resource = self._create_resource(data)
|
|
189
|
+
|
|
190
|
+
resource.delete()
|
|
191
|
+
self._logger.resource("Resource deleted", resource)
|
|
192
|
+
|
|
193
|
+
self._state.delete(key)
|
|
194
|
+
self._state.save()
|
|
195
|
+
|
|
196
|
+
for key in self._specs_order:
|
|
197
|
+
# If this resource does not exist yet
|
|
198
|
+
if not self._state.contains(key):
|
|
199
|
+
data = self._specs[key]
|
|
200
|
+
|
|
201
|
+
resource = self._create_resource(data)
|
|
202
|
+
resources[key] = resource
|
|
203
|
+
|
|
204
|
+
# Replace each reference of this resource
|
|
205
|
+
self._update_references(resource, resources)
|
|
206
|
+
|
|
207
|
+
resource.create()
|
|
208
|
+
self._logger.resource("Resource created", resource)
|
|
209
|
+
|
|
210
|
+
self._state.add(resource.key, resource.dump())
|
|
211
|
+
self._state.save()
|
|
212
|
+
|
|
213
|
+
# Already exists in the engine
|
|
214
|
+
else:
|
|
215
|
+
state_data = self._state.get(key)
|
|
216
|
+
state_resource = self._create_resource(state_data)
|
|
217
|
+
|
|
218
|
+
spec_data = self._specs[key]
|
|
219
|
+
spec_resource = self._create_resource(spec_data)
|
|
220
|
+
|
|
221
|
+
# Replace each reference of this resource
|
|
222
|
+
self._update_references(spec_resource, resources)
|
|
223
|
+
|
|
224
|
+
diff = Diff(
|
|
225
|
+
new_arguments=spec_resource.arguments,
|
|
226
|
+
old_arguments=state_resource.arguments,
|
|
227
|
+
)
|
|
228
|
+
|
|
229
|
+
resources[key] = state_resource
|
|
230
|
+
|
|
231
|
+
if diff:
|
|
232
|
+
state_resource.update_arguments(spec_resource.arguments)
|
|
233
|
+
state_resource.update()
|
|
234
|
+
self._logger.resource("Resource updated", state_resource)
|
|
235
|
+
|
|
236
|
+
self._state.update(key, state_resource.dump())
|
|
237
|
+
self._state.save()
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .alert import Alert
|
|
2
|
+
from .asset import Asset
|
|
3
|
+
from .attribute import Attribute
|
|
4
|
+
from .base import Resource
|
|
5
|
+
from .component import Component
|
|
6
|
+
from .component_object import ComponentObject
|
|
7
|
+
from .file import File
|
|
8
|
+
from .function import Function
|
|
9
|
+
from .metadata import Metadata
|
|
10
|
+
from .routine import Routine
|
|
11
|
+
from .secret import Secret
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Alert",
|
|
15
|
+
"Component",
|
|
16
|
+
"ComponentObject",
|
|
17
|
+
"Routine",
|
|
18
|
+
"Resource",
|
|
19
|
+
"Asset",
|
|
20
|
+
"Secret",
|
|
21
|
+
"Attribute",
|
|
22
|
+
"Metadata",
|
|
23
|
+
"File",
|
|
24
|
+
"Function",
|
|
25
|
+
]
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from splight_lib.models import Alert as AlertSchema
|
|
2
|
+
from splight_lib.models.base import SplightDatabaseBaseModel
|
|
3
|
+
|
|
4
|
+
from splight_cli.solution.resource.models.base import Resource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
# FIXME: Sometimes the API mixes up the order of the alert items after
|
|
8
|
+
# creating the resource, this causes the plan to show a diff.
|
|
9
|
+
# Another apply will fix it, but would be nice if we always sorted new
|
|
10
|
+
# and older arguments by name.
|
|
11
|
+
# Should be done by overriding init and after refresh retrieves the new
|
|
12
|
+
# arguments.
|
|
13
|
+
class Alert(Resource):
|
|
14
|
+
_schema: SplightDatabaseBaseModel = AlertSchema
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from splight_lib.models import Attribute as AttributeSchema
|
|
2
|
+
from splight_lib.models.base import SplightDatabaseBaseModel
|
|
3
|
+
|
|
4
|
+
from splight_cli.solution.resource.models.base import Resource
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Attribute(Resource):
|
|
8
|
+
_schema: SplightDatabaseBaseModel = AttributeSchema
|