Skip to content

Commit 9e42164

Browse files
committed
Refactor state directories
1 parent f64988a commit 9e42164

File tree

9 files changed

+121
-108
lines changed

9 files changed

+121
-108
lines changed

opslib/__init__.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,12 @@
55
from .local import run
66
from .places import Command, Directory, File, LocalHost, SshHost
77
from .props import Prop
8-
from .state import JsonState
98

109
__all__ = [
1110
"Command",
1211
"Component",
1312
"Directory",
1413
"File",
15-
"JsonState",
1614
"Lazy",
1715
"LocalHost",
1816
"MaybeLazy",

opslib/ansible.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
from ansible.playbook.play import Play
1212
from ansible.plugins.callback import CallbackBase
1313
from ansible.vars.manager import VariableManager
14+
from opslib.state import StatefulMixin
1415

1516
from .callbacks import Callbacks
1617
from .components import Component
@@ -146,7 +147,7 @@ def run_ansible(hostname, ansible_variables, action, check=False):
146147
return result
147148

148149

149-
class AnsibleAction(Component):
150+
class AnsibleAction(StatefulMixin, Component):
150151
"""
151152
The AnsibleAction component executes an Ansible module.
152153

opslib/cli.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,9 @@
77

88
import click
99

10+
import opslib
1011
from .operations import apply, print_report
1112
from .results import OperationError
12-
from .state import run_gc
1313

1414
logger = logging.getLogger(__name__)
1515

@@ -89,7 +89,7 @@ def decorator(func):
8989
return decorator
9090

9191

92-
def get_cli(component) -> click.Group:
92+
def get_cli(component: "opslib.Component") -> click.Group:
9393
@click.group(cls=ComponentGroup)
9494
def cli():
9595
pass
@@ -110,7 +110,8 @@ def shell():
110110
@cli.command()
111111
@click.option("-n", "--dry-run", is_flag=True)
112112
def gc(dry_run):
113-
run_gc(component, dry_run=dry_run)
113+
provider = component._meta.stack._state_provider
114+
provider.run_gc(component, dry_run=dry_run)
114115

115116
@cli.forward_command("component")
116117
@click.pass_context

opslib/components.py

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,23 +3,20 @@
33
import sys
44
from functools import cached_property
55
from pathlib import Path
6-
from typing import Any, Type, TypeVar
6+
from typing import Any, Type, TypeVar, cast
77

88
from .props import get_instance_props
99
from .results import Result
10-
from .state import StateDirectory
10+
from .state import FilesystemStateProvider
1111

1212
logger = logging.getLogger(__name__)
1313

1414

1515
class Meta:
16-
statedir = StateDirectory()
17-
18-
def __init__(self, component, name, parent, stateroot=None):
16+
def __init__(self, component: "Component", name: str, parent: "Component | None"):
1917
self.component = component
2018
self.name = name
2119
self.parent = parent
22-
self.stateroot = stateroot
2320

2421
@cached_property
2522
def full_name(self):
@@ -29,8 +26,12 @@ def full_name(self):
2926
return f"{self.parent._meta.full_name}.{self.name}"
3027

3128
@cached_property
32-
def stack(self):
33-
return self.component if self.parent is None else self.parent._meta.stack
29+
def stack(self) -> "Stack":
30+
return (
31+
cast(Stack, self.component)
32+
if self.parent is None
33+
else self.parent._meta.stack
34+
)
3435

3536

3637
class Component:
@@ -146,18 +147,17 @@ def __init__(self, import_name=None, stateroot=None, **kwargs):
146147
if import_name is None and stateroot is None:
147148
raise ValueError("Either `import_name` or `stateroot` must be set")
148149

150+
self._state_provider = FilesystemStateProvider(
151+
stateroot or get_stateroot(import_name)
152+
)
153+
149154
super().__init__(**kwargs)
150155

151-
self._meta = self.Meta(
152-
component=self,
153-
name="__root__",
154-
parent=None,
155-
stateroot=stateroot or get_stateroot(import_name),
156-
)
156+
self._meta = self.Meta(component=self, name="__root__", parent=None)
157157
self.build()
158158

159159

160-
def walk(component):
160+
def walk(component) -> Iterator[Component]:
161161
"""
162162
Iterate depth-first over all child components. The first item is
163163
``component`` itself.

opslib/extras/restic.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@
99
from opslib.local import run
1010
from opslib.props import Prop
1111
from opslib.results import OperationError, Result
12-
from opslib.state import JsonState
12+
from opslib.state import JsonState, StatefulMixin
1313

1414
BASH_PREAMBLE = """\
1515
#!/bin/bash
1616
set -euo pipefail
1717
"""
1818

1919

20-
class ResticRepository(Component):
20+
class ResticRepository(StatefulMixin, Component):
2121
class Props:
2222
repository = Prop(str)
2323
password = Prop(str, lazy=True)

opslib/places.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
from .local import LocalRunResult, run
1212
from .props import Prop
1313
from .results import Result
14-
from .state import JsonState
14+
from .state import JsonState, StatefulMixin
1515
from .utils import diff
1616

1717

@@ -404,7 +404,7 @@ def run(self, *args, **kwargs):
404404
return self.host.run(cwd=self.path, *args, **kwargs)
405405

406406

407-
class Command(Component):
407+
class Command(StatefulMixin, Component):
408408
"""
409409
The Command component represents a command that should be run on the
410410
host during deployment.

opslib/state.py

Lines changed: 55 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,75 +1,85 @@
1+
from collections.abc import Iterator
2+
from contextlib import contextmanager
13
import json
24
import logging
35
from pathlib import Path
46
import shutil
7+
from typing import cast
58

6-
logger = logging.getLogger(__name__)
9+
import opslib
710

11+
logger = logging.getLogger(__name__)
812

9-
class ComponentStateDirectory:
10-
def __init__(self, meta):
11-
self.meta = meta
1213

13-
@property
14-
def prefix(self):
15-
return self.get_prefix(create=True)
14+
class FilesystemStateProvider:
15+
def __init__(self, stateroot: Path):
16+
self.stateroot = stateroot
1617

17-
def get_prefix(self, create=False) -> Path:
18-
if self.meta.parent is None:
19-
prefix = self.meta.stateroot
18+
def _get_directory(self, component: "opslib.Component") -> Path:
19+
if component._meta.parent is None:
20+
return self.stateroot
2021

21-
else:
22-
parent_meta = self.meta.parent._meta
23-
prefix = parent_meta.statedir.get_prefix(create=create) / self.meta.name
22+
return self._get_directory(component._meta.parent) / component._meta.name
2423

25-
if create:
26-
self._mkdir(prefix)
24+
def _get_state_directory(self, component: "opslib.Component"):
25+
return self._get_directory(component) / "_statedir"
2726

28-
return prefix
27+
@contextmanager
28+
def state_directory(self, component: "opslib.Component"):
29+
statedir = self._get_state_directory(component)
30+
if not statedir.exists():
31+
statedir.mkdir(parents=True)
32+
yield statedir
2933

30-
@property
31-
def path(self):
32-
return self.get_path(create=True)
34+
def run_gc(self, component: "opslib.Component", dry_run=False):
35+
child_names = {child._meta.name for child in component}
3336

34-
def get_path(self, create=False) -> Path:
35-
path = self.get_prefix(create=create) / "_statedir"
37+
def unexpected(item):
38+
if isinstance(component, StatefulMixin) and item.name == "_statedir":
39+
return False
3640

37-
if create:
38-
self._mkdir(path)
41+
if not item.is_dir():
42+
return False
3943

40-
return path
44+
return item.name not in child_names
4145

42-
def _mkdir(self, path):
43-
if not path.is_dir():
44-
logger.debug("ComponentState init %s", path)
45-
path.mkdir(mode=0o700)
46+
directory = self._get_directory(component)
47+
if directory.exists():
48+
for item in directory.iterdir():
49+
if unexpected(item):
50+
print(item)
51+
if dry_run:
52+
continue
4653

54+
shutil.rmtree(item)
4755

48-
class StateDirectory:
49-
def __get__(self, obj, objtype=None):
50-
return ComponentStateDirectory(obj)
56+
for child in component:
57+
self.run_gc(child, dry_run=dry_run)
5158

5259

5360
class ComponentJsonState:
5461
def __init__(self, component):
5562
self.component = component
5663

57-
@property
58-
def _path(self):
59-
return self.component._meta.statedir.path / "state.json"
64+
@contextmanager
65+
def json_path(self) -> Iterator[Path]:
66+
with self.component.state_directory() as statedir:
67+
yield statedir / "state.json"
6068

6169
@property
6270
def _data(self):
6371
try:
64-
with self._path.open() as f:
65-
return json.load(f)
72+
with self.json_path() as json_path:
73+
with json_path.open() as f:
74+
return json.load(f)
6675

6776
except FileNotFoundError:
6877
return {}
6978

7079
def save(self, data=(), **kwargs):
71-
with self._path.open("w") as f:
72-
json.dump(dict(data, **kwargs), f, indent=2)
80+
with self.json_path() as json_path:
81+
with json_path.open("w") as f:
82+
json.dump(dict(data, **kwargs), f, indent=2)
7383

7484
self.__dict__["_data"] = data
7585

@@ -99,26 +109,11 @@ def __get__(self, obj, objtype=None):
99109
return ComponentJsonState(obj)
100110

101111

102-
def run_gc(component, dry_run=False):
103-
child_names = {child._meta.name for child in component}
104-
statedir_prefix = component._meta.statedir.prefix
105-
106-
def unexpected(item):
107-
if item.name.startswith("_"):
108-
return False
109-
110-
if not item.is_dir():
111-
return False
112-
113-
return item.name not in child_names
114-
115-
for item in statedir_prefix.iterdir():
116-
if unexpected(item):
117-
print(item)
118-
if dry_run:
119-
continue
120-
121-
shutil.rmtree(item)
112+
class StatefulMixin:
113+
_meta: "opslib.components.Meta"
122114

123-
for child in component:
124-
run_gc(child, dry_run=dry_run)
115+
@contextmanager
116+
def state_directory(self):
117+
provider = self._meta.stack._state_provider
118+
with provider.state_directory(cast(opslib.Component, self)) as statedir:
119+
yield statedir

0 commit comments

Comments
 (0)