Skip to content

Commit

Permalink
ADCM-6268 Support with_dependencies argument for services add (#71)
Browse files Browse the repository at this point in the history
Co-authored-by: Araslanov Egor <[email protected]>
  • Loading branch information
Sealwing and Araslanov Egor authored Jan 15, 2025
1 parent 8127907 commit a7ed7ff
Show file tree
Hide file tree
Showing 3 changed files with 146 additions and 1 deletion.
42 changes: 41 additions & 1 deletion adcm_aio_client/core/objects/cm.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from collections.abc import AsyncGenerator, Awaitable, Callable, Iterable
from datetime import datetime, timedelta
from functools import cached_property
from itertools import chain
from pathlib import Path
from typing import Any, Literal, Self
import asyncio
Expand Down Expand Up @@ -279,13 +280,19 @@ class ServicesNode(PaginatedChildAccessor[Cluster, Service]):
filtering = Filtering(FilterByName, FilterByDisplayName, FilterByStatus)
service_add_filtering = Filtering(FilterByName, FilterByDisplayName)

async def add(self: Self, filter_: Filter, *, accept_license: bool = False) -> list[Service]:
async def add(
self: Self, filter_: Filter, *, accept_license: bool = False, with_dependencies: bool = False
) -> list[Service]:
candidates = await self._retrieve_service_candidates(filter_=filter_)

if not candidates:
message = "No services to add by given filters"
raise NotFoundError(message)

if with_dependencies:
dependencies_candidates = await self._find_missing_service_dependencies(candidates)
candidates.extend(dependencies_candidates)

if accept_license:
await self._accept_licenses_safe(candidates)

Expand All @@ -296,6 +303,39 @@ async def _retrieve_service_candidates(self: Self, filter_: Filter) -> list[dict
response = await self._requester.get(*self._parent.get_own_path(), "service-candidates", query=query)
return response.as_list()

async def _find_missing_service_dependencies(self: Self, candidates: list[dict]) -> list[dict]:
response = await self._requester.get(*self._parent.get_own_path(), "service-prototypes")
all_service_prototypes = response.as_list()

dependencies = {
proto["id"]: {dep["servicePrototype"]["id"] for dep in (proto["dependOn"] or ())}
for proto in all_service_prototypes
}

candidate_ids = {c["id"] for c in candidates}

all_candidate_dependencies = self._detect_missing_dependencies(
dependencies=dependencies, to_add=candidate_ids, processed=set()
)
missing_dependencies = all_candidate_dependencies - candidate_ids

return [proto for proto in all_service_prototypes if proto["id"] in missing_dependencies]

def _detect_missing_dependencies(
self: Self, dependencies: dict[int, set[int]], to_add: set[int], processed: set[int]
) -> set[int]:
unprocessed = to_add - processed
if not unprocessed:
return to_add

deps_of_unprocessed = set(chain.from_iterable(map(dependencies.__getitem__, unprocessed)))
if not deps_of_unprocessed:
return to_add

return self._detect_missing_dependencies(
dependencies=dependencies, to_add=to_add | deps_of_unprocessed, processed=processed | unprocessed
)

async def _accept_licenses_safe(self: Self, candidates: list[dict]) -> None:
unaccepted: deque[int] = deque()

Expand Down
71 changes: 71 additions & 0 deletions tests/integration/bundles/complex_cluster/config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -341,3 +341,74 @@

c2:
actions: *component_actions

# requires cases section

- type: service
version: 1.0
name: my_service
components:
my_component:

- type: service
version: 1.0
name: service_with_requires_my_component
components:
componentB:
requires:
- service: my_service
component: my_component

- type: service
version: 1.0
name: service_with_requires_my_service
requires:
- service: my_service

- type: service
version: 1.0
name: component_with_requires_my_component
components:
component_with_requires_my_component:
requires:
- service: my_service
component: my_component

- type: service
version: 1.0
name: component_with_requires_my_service
components:
component_with_requires_my_service:
requires:
- service: my_service

- type: service
version: 2.3
name: A

requires:
- service: B

components:
a1:
requires:
- service: C

- type: service
version: 2.4
name: B

requires:
- service: A

- type: service
version: 2.1
name: C

requires:
- service: A

components:
c1:
requires:
- service: B
34 changes: 34 additions & 0 deletions tests/integration/test_cluster.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
from collections.abc import Collection
from functools import partial
import random
import string
import asyncio
Expand Down Expand Up @@ -87,6 +88,9 @@ async def host(adcm_client: ADCMClient, simple_hostprovider_bundle: Bundle) -> H
return await adcm_client.hosts.get(name__eq=name)


# test_cluster


async def test_cluster(
adcm_client: ADCMClient,
complex_cluster_bundle: Bundle,
Expand Down Expand Up @@ -256,3 +260,33 @@ async def _test_cluster_object_api(httpx_client: AsyncClient, cluster: Cluster,
assert cluster.name == new_name

return cluster_id, bundle_id, description, status


# test_add_services_with_dependencies


async def test_add_services_with_dependencies(adcm_client: ADCMClient, complex_cluster_bundle: Bundle) -> None:
bundle = complex_cluster_bundle
service_name = partial(Filter, attr="name", op="eq")

cluster = await adcm_client.clusters.create(bundle=bundle, name="Add C With Deps")
services = await cluster.services.add(service_name(value="C"), with_dependencies=True)
assert len(services) == 3
assert {s.name for s in services} == {"A", "B", "C"}

cluster = await adcm_client.clusters.create(bundle=bundle, name="Add C Without Deps")
services = await cluster.services.add(service_name(value="C"), with_dependencies=False)
assert len(services) == 1
assert {s.name for s in services} == {"C"}

cluster = await adcm_client.clusters.create(bundle=bundle, name="Add my_service With Deps")
services = await cluster.services.add(service_name(value="my_service"), with_dependencies=True)
assert len(services) == 1
assert {s.name for s in services} == {"my_service"}

cluster = await adcm_client.clusters.create(bundle=bundle, name="Add two With Deps")
to_add = ["service_with_requires_my_component", "service_with_requires_my_service"]
filter_ = Filter(attr="name", op="in", value=to_add)
services = await cluster.services.add(filter_, with_dependencies=True)
assert len(services) == 3
assert {s.name for s in services} == {"my_service", *to_add}

0 comments on commit a7ed7ff

Please sign in to comment.