Issue/6025 implement discovery handler#6264
Conversation
…6025-implement-discovery-handler
| # The DiscoveryHandler is generic in both the handler's Discovery Resource type (DRT) | ||
| # and the Unmanaged Resource type (URT) it reports to the server. The second has to be serializable. | ||
|
|
||
| # This class deploys instances of DRT and reports URT to the server. |
There was a problem hiding this comment.
deploys instances of DRT
I'm not sure this is effectively the case with my implementation
There was a problem hiding this comment.
Indeed, this is still missing. The main resource entry-point (at the moment, depends very slightly on the final outcome of the other PR) is deploy so that method should be implemented in this class to call discover_resources and report_discovered_resources.
| def discover_resources(self, ctx: HandlerContext, discovery_resource: DRT) -> abc.Mapping[ResourceIdStr, URT]: | ||
| raise NotImplementedError | ||
|
|
||
| def report_discovered_resources(self, ctx: HandlerContext, resource: DRT) -> None: |
There was a problem hiding this comment.
I'm unsure of whether this logic should happen in a synchronous or asynchronous context.
The testcase currently uses the async version of this (async_report_discovered_resources) and passes.
I was having second thoughts about it and tried the synchronous version (report_discovered_resources) and it looks like it is hanging somewhere.
There was a problem hiding this comment.
All agent ABC methods are synchronous so this should be synchronous as well. I would even go as far as to drop the async method. For testing purposes, using the sync client is more complicated because if both server and client run on the same thread, a synchronous call would hang the server so it can never respond. I'm pretty sure we have some fixtures to work with that, I'll add a suggestion to your test cases.
| # resource_handler.report_discovered_resources(ctx, discovery_resource) | ||
| await resource_handler.async_report_discovered_resources(ctx, discovery_resource) |
There was a problem hiding this comment.
See comment on the method themselves, I'm not confident about that part
sanderr
left a comment
There was a problem hiding this comment.
Intermediate review: I haven't looked at test cases or inmanta.resources yet.
| def discover_resources(self, ctx: HandlerContext, discovery_resource: DRT) -> abc.Mapping[ResourceIdStr, URT]: | ||
| raise NotImplementedError | ||
|
|
||
| def report_discovered_resources(self, ctx: HandlerContext, resource: DRT) -> None: |
There was a problem hiding this comment.
This should be deploy. Could optionally be split into discover -> serialize -> report, so deploy just has to chain them together. No strong preference on the last part, unless the serialization turns out to be very verbose.
| try: | ||
| self.pre(ctx, resource) | ||
| # report to the server | ||
| discovered_resources: List[DiscoveredResource] = [ |
There was a problem hiding this comment.
Should be typed as lowercase list or abc.Sequence. You're not modifying it so abc.Sequence presents the clearer picture.
| self.pre(ctx, resource) | ||
| # report to the server | ||
| discovered_resources: List[DiscoveredResource] = [ | ||
| DiscoveredResource(discovered_resource_id=resource_id, values=values) |
There was a problem hiding this comment.
These types do not match, we should serialize here.
| ), | ||
| urt=str(URT), | ||
| resource_id=resource.id, | ||
| exception=f"{e.__class__.__name__}('{e}')", |
There was a problem hiding this comment.
I think repr() does pretty much this:
>>> e = Exception("hellow orld")
>>> str(e)
'hellow orld'
>>> repr(e)
"Exception('hellow orld')"Same for other message
There was a problem hiding this comment.
Ah turns out it is not the same regarding formatting:
e = Exception("An\nError\tMessage")
print(str(e))
print(repr(e))
print(f"{e.__class__.__name__}('{e}')")An
Error Message
Exception('An\nError\tMessage')
Exception('An
Error Message')And this test fails if we change it to repr
There was a problem hiding this comment.
Interesting. To be honest I personally prefer the repr output but if we were already using the other approach (I hadn't realized when I commented) I guess we should stick with that.
Co-authored-by: Sander Van Balen <git@sandervanbalen.be>
…6025-implement-discovery-handler
Co-authored-by: arnaudsjs <2684622+arnaudsjs@users.noreply.github.com>
…6025-implement-discovery-handler
|
Still a couple of mypy errors that I didn't manage to handle on my own: + src/inmanta/agent/handler.py:1018: error: Argument 2 of "execute" is incompatible with supertype "HandlerAPI"; supertype defines the argument type as "Resource" [override]
+ src/inmanta/agent/handler.py:1036: error: Argument 2 of "deploy" is incompatible with supertype "HandlerAPI"; supertype defines the argument type as "Resource" [override]
+ src/inmanta/agent/handler.py:1036: note: This violates the Liskov substitution principle
+ src/inmanta/agent/handler.py:1036: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
+ src/inmanta/agent/handler.py:1071: error: Argument 2 of "execute" is incompatible with supertype "HandlerAPI"; supertype defines the argument type as "Resource" [override]
+ src/inmanta/agent/handler.py:1071: note: This violates the Liskov substitution principle
+ src/inmanta/agent/handler.py:1071: note: See https://mypy.readthedocs.io/en/stable/common_issues.html#incompatible-overrides
+ src/inmanta/agent/handler.py:1089: error: Argument 1 to "json_encode" has incompatible type "D"; expected "Union[Union[BaseModel, Union[UUID, StrictNonIntBool, int, float, datetime, str]], Sequence[Union[BaseModel, Union[UUID, StrictNonIntBool, int, float, datetime, str]]], Mapping[str, Union[BaseModel, Union[UUID, StrictNonIntBool, int, float, datetime, str]]], None]" [arg-type]
|
| resource: R, | ||
| requires: abc.Mapping[ResourceIdStr, ResourceState], | ||
| ) -> None: | ||
| self.execute(ctx, resource) |
There was a problem hiding this comment.
We may want to copy over some of the this code
resources_in_unexpected_state = filter_resources_in_unexpected_state(requires)
if resources_in_unexpected_state:
ctx.set_status(const.ResourceState.skipped)
ctx.warning(
"Resource %(resource)s skipped because a dependency is in an unexpected state: %(unexpected_states)s",
resource=resource.id.resource_version_str(),
unexpected_states=str({rid: state.value for rid, state in resources_in_unexpected_state.items()}),
)
return
failed_dependencies = [req for req, status in requires.items() if status != ResourceState.deployed]
if not any(failed_dependencies):
self.execute(ctx, resource)
if _should_reload():
self.do_reload(ctx, resource)
else:
ctx.set_status(const.ResourceState.skipped)
ctx.info(
"Resource %(resource)s skipped due to failed dependencies: %(failed)s",
resource=resource.id.resource_version_str(),
failed=str(failed_dependencies),
)
To handle skip and failed dependencies
There was a problem hiding this comment.
I lifted this logic in the HandlerAPI's deploy in the other PR
| # serialize resources and report to the server | ||
| discovered_resources: abc.Sequence[DiscoveredResource] = [ | ||
| DiscoveredResource(discovered_resource_id=resource_id, values=json.loads(json_encode(values))) | ||
| for resource_id, values in self.discover_resources(ctx, resource).items() |
There was a problem hiding this comment.
purely a matter of taste, but I would put the self.discover_resources(ctx, resource) on a line of its own, as it is the main thing here. It is kind of hidden now.
| try: | ||
| self.pre(ctx, resource) | ||
| # serialize resources and report to the server | ||
| discovered_resources: abc.Sequence[DiscoveredResource] = [ |
There was a problem hiding this comment.
If we run a dryrun, do we want to execute all this?
| def discover_resources( | ||
| self, ctx: HandlerContext, discovery_resource: MyDiscoveryResource | ||
| ) -> abc.Mapping[ResourceIdStr, MyUnmanagedResource]: | ||
| dirs = os.listdir(self._top_dir_path) |
There was a problem hiding this comment.
Why files, why not just an array of strings?
| from inmanta.resources import DiscoveryResource, Id, resource | ||
|
|
||
|
|
||
| async def test_discovery_resource_handler( |
There was a problem hiding this comment.
I would also add tests for dryrun, get_fact and a few requires-provides scenarios.
…#6025, PR #6415) # Description - [x] implement `DiscoveryHandler` - [x] Verify that the `Id` constructor and `resource_str` method are part of the stable API and documented. This PR replaces #6264 Part of #6025 # Self Check: - [x] Attached issue to pull request - [ ] Changelog entry - [x] Type annotations are present - [x] Code is clear and sufficiently documented - [x] No (preventable) type errors (check using make mypy or make mypy-diff) - [x] Sufficient test cases (reproduces the bug/tests the requested feature) - [x] Correct, in line with design - [x] End user documentation is included or an issue is created for end-user documentation (#6270) - [ ] ~~If this PR fixes a race condition in the test suite, also push the fix to the relevant stable branche(s) (see [test-fixes](https://internal.inmanta.com/development/core/tasks/build-master.html#test-fixes) for more info)~~
Description
DiscoveryHandlerIdconstructor andresource_strmethod are part of the stable API and documented.Part of #6025
Self Check:
Strike through any lines that are not applicable (
~~line~~) then check the box