Skip to content

Commit e2a62a0

Browse files
committed
Add group trackingé
1 parent 9d619b7 commit e2a62a0

File tree

11 files changed

+217
-21
lines changed

11 files changed

+217
-21
lines changed

backend/infrahub/core/constants/__init__.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -146,6 +146,11 @@ class AllowOverrideType(InfrahubStringEnum):
146146
ANY = "any"
147147

148148

149+
class RepositoryObjects(InfrahubStringEnum):
150+
OBJECT = "object"
151+
MENU = "menu"
152+
153+
149154
class ContentType(InfrahubStringEnum):
150155
APPLICATION_JSON = "application/json"
151156
APPLICATION_YAML = "application/yaml"

backend/infrahub/core/protocols.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -493,6 +493,11 @@ class CoreRepository(LineageOwner, LineageSource, CoreGenericRepository, CoreTas
493493
commit: StringOptional
494494

495495

496+
class CoreRepositoryGroup(CoreGroup):
497+
content: Dropdown
498+
repository: RelationshipManager
499+
500+
496501
class CoreRepositoryValidator(CoreValidator):
497502
repository: RelationshipManager
498503

backend/infrahub/core/schema/definitions/core/__init__.py

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,13 @@
2828
from .core import core_node, core_task_target
2929
from .generator import core_generator_definition, core_generator_instance
3030
from .graphql_query import core_graphql_query
31-
from .group import core_generator_group, core_graphql_query_group, core_group, core_standard_group
31+
from .group import (
32+
core_generator_group,
33+
core_graphql_query_group,
34+
core_group,
35+
core_repository_group,
36+
core_standard_group,
37+
)
3238
from .ipam import builtin_ip_address, builtin_ip_prefix, builtin_ipam, core_ipam_namespace
3339
from .lineage import lineage_owner, lineage_source
3440
from .menu import generic_menu_item, menu_item
@@ -109,6 +115,7 @@
109115
core_standard_group,
110116
core_generator_group,
111117
core_graphql_query_group,
118+
core_repository_group,
112119
builtin_tag,
113120
core_account,
114121
core_account_token,

backend/infrahub/core/schema/definitions/core/group.py

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
from infrahub.core.constants import (
22
BranchSupportType,
33
InfrahubKind,
4+
RepositoryObjects,
45
)
56
from infrahub.core.constants import RelationshipCardinality as Cardinality
67
from infrahub.core.constants import RelationshipKind as RelKind
8+
from infrahub.core.schema.dropdown import DropdownChoice
79

810
from ...attribute_schema import AttributeSchema as Attr
911
from ...generic_schema import GenericSchema
@@ -80,6 +82,7 @@
8082
generate_profile=False,
8183
)
8284

85+
8386
core_graphql_query_group = NodeSchema(
8487
name="GraphQLQueryGroup",
8588
namespace="Core",
@@ -106,3 +109,45 @@
106109
),
107110
],
108111
)
112+
113+
114+
core_repository_group = NodeSchema(
115+
name="RepositoryGroup",
116+
namespace="Core",
117+
description="Group of nodes associated with a given repository.",
118+
include_in_menu=False,
119+
icon="mdi:account-group",
120+
label="Repository Group",
121+
default_filter="name__value",
122+
order_by=["name__value"],
123+
display_labels=["name__value"],
124+
branch=BranchSupportType.LOCAL,
125+
inherit_from=[InfrahubKind.GENERICGROUP],
126+
generate_profile=False,
127+
attributes=[
128+
Attr(
129+
name="content",
130+
kind="Dropdown",
131+
description="Type of data to load, can be either `object` or `menu`",
132+
choices=[
133+
DropdownChoice(
134+
name=RepositoryObjects.OBJECT.value,
135+
label="Objects",
136+
),
137+
DropdownChoice(
138+
name=RepositoryObjects.MENU.value,
139+
label="Menus",
140+
),
141+
],
142+
optional=False,
143+
),
144+
],
145+
relationships=[
146+
Rel(
147+
name="repository",
148+
peer=InfrahubKind.GENERICREPOSITORY,
149+
optional=False,
150+
cardinality=Cardinality.ONE,
151+
),
152+
],
153+
)

backend/infrahub/git/base.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,11 @@ class InfrahubRepositoryBase(BaseModel, ABC):
162162
infrahub_branch_name: str | None = Field(None, description="Infrahub branch on which to sync the remote repository")
163163
model_config = ConfigDict(arbitrary_types_allowed=True, ignored_types=(Flow, Task))
164164

165+
def get_client(self) -> InfrahubClient:
166+
if self.client is None:
167+
raise ValueError("Client is not set")
168+
return self.client
169+
165170
@property
166171
def sdk(self) -> InfrahubClient:
167172
if self.client:
@@ -445,9 +450,6 @@ def get_worktrees(self) -> list[Worktree]:
445450

446451
return [Worktree.init(response) for response in responses]
447452

448-
def get_client(self) -> InfrahubClient:
449-
return self.sdk
450-
451453
def get_location(self) -> str:
452454
if self.location:
453455
return self.location

backend/infrahub/git/integrator.py

Lines changed: 74 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -29,19 +29,20 @@
2929
InfrahubPythonTransformConfig,
3030
InfrahubRepositoryConfig,
3131
)
32+
from infrahub_sdk.spec.menu import MenuFile
3233
from infrahub_sdk.spec.object import ObjectFile
3334
from infrahub_sdk.template import Jinja2Template
3435
from infrahub_sdk.template.exceptions import JinjaTemplateError
3536
from infrahub_sdk.utils import compare_lists
36-
from infrahub_sdk.yaml import SchemaFile
37+
from infrahub_sdk.yaml import InfrahubFile, SchemaFile
3738
from prefect import flow, task
3839
from prefect.cache_policies import NONE
3940
from prefect.logging import get_run_logger
4041
from pydantic import BaseModel, Field
4142
from pydantic import ValidationError as PydanticValidationError
4243
from typing_extensions import Self
4344

44-
from infrahub.core.constants import ArtifactStatus, ContentType, InfrahubKind, RepositorySyncStatus
45+
from infrahub.core.constants import ArtifactStatus, ContentType, InfrahubKind, RepositoryObjects, RepositorySyncStatus
4546
from infrahub.core.registry import registry
4647
from infrahub.events.artifact_action import ArtifactCreatedEvent, ArtifactUpdatedEvent
4748
from infrahub.events.models import EventMeta
@@ -161,7 +162,7 @@ async def init(cls, service: InfrahubServices, commit: str | None = None, **kwar
161162
async def ensure_location_is_defined(self) -> None:
162163
if self.location:
163164
return
164-
client = self.get_client()
165+
client = self.sdk
165166
repo = await client.get(
166167
kind=CoreGenericRepository, name__value=self.name, exclude=["tags", "credential"], raise_when_missing=True
167168
)
@@ -181,6 +182,9 @@ async def import_objects_from_files(
181182

182183
config_file = await self.get_repository_config(branch_name=infrahub_branch_name, commit=commit) # type: ignore[misc]
183184
sync_status = RepositorySyncStatus.IN_SYNC if config_file else RepositorySyncStatus.ERROR_IMPORT
185+
if sync_status == RepositorySyncStatus.ERROR_IMPORT:
186+
raise ValueError("Unable to import the repository here")
187+
184188
error: Exception | None = None
185189

186190
try:
@@ -191,6 +195,19 @@ async def import_objects_from_files(
191195
branch_name=infrahub_branch_name, commit=commit, config_file=config_file
192196
) # type: ignore[misc]
193197

198+
await self.import_objects(
199+
branch_name=infrahub_branch_name,
200+
commit=commit,
201+
files_pathes=config_file.objects,
202+
object_type=RepositoryObjects.OBJECT,
203+
) # type: ignore[misc]
204+
await self.import_objects(
205+
branch_name=infrahub_branch_name,
206+
commit=commit,
207+
files_pathes=config_file.menus,
208+
object_type=RepositoryObjects.MENU,
209+
) # type: ignore[misc]
210+
194211
await self.import_all_python_files( # type: ignore[call-overload]
195212
branch_name=infrahub_branch_name, commit=commit, config_file=config_file
196213
) # type: ignore[misc]
@@ -200,11 +217,15 @@ async def import_objects_from_files(
200217
await self.import_artifact_definitions(
201218
branch_name=infrahub_branch_name, commit=commit, config_file=config_file
202219
) # type: ignore[misc]
203-
await self.import_objects(branch_name=infrahub_branch_name, commit=commit, config_file=config_file) # type: ignore[misc]
220+
221+
# TODO: import menu, check def load() in menu.^y + add a deidcated group for menu, work as for objects
222+
# "CoreRepositoryMenuGroup"
204223

205224
except Exception as exc:
206225
sync_status = RepositorySyncStatus.ERROR_IMPORT
207226
error = exc
227+
log = get_run_logger()
228+
log.error(f"Error importing the repository {self.name} : {exc}")
208229

209230
await self._update_sync_status(branch_name=infrahub_branch_name, status=sync_status)
210231

@@ -831,23 +852,65 @@ async def _load_objects(
831852
self,
832853
paths: list[Path],
833854
branch: str,
855+
file_type: type[InfrahubFile],
834856
) -> None:
835857
"""Load one or multiple objects files into Infrahub."""
836858

837-
files = await self._load_yamlfile_from_disk(paths=paths, file_type=ObjectFile)
859+
log = get_run_logger()
860+
files = await self._load_yamlfile_from_disk(paths=paths, file_type=file_type)
861+
log.info(f"Loaded files {files=}")
862+
838863
for file in files:
839-
await file.validate_format(client=self.client, branch=branch)
864+
await file.validate_format(client=self.get_client(), branch=branch)
865+
schema = await self.get_client().schema.get(kind=file.spec.kind, branch=branch)
866+
if not schema.human_friendly_id and not schema.default_filter:
867+
raise ValueError(
868+
f"Schemas of objects or menus defined within {file.location} "
869+
"should have a `human_friendly_id` defined to avoid creating duplicated objects."
870+
)
871+
840872
for file in files:
841-
await file.process(client=self.client, branch=branch)
873+
# TODO: client.log that is then used should log into infrahub.tasks
874+
log.info(f"Loading objects defined in {file.location}")
875+
await file.process(client=self.get_client(), branch=branch)
842876

843877
@task(name="import-objects", task_run_name="Import Objects", cache_policy=NONE) # type: ignore[arg-type]
844-
async def import_objects(self, branch_name: str, commit: str, config_file: InfrahubRepositoryConfig) -> None:
878+
async def import_objects(
879+
self, branch_name: str, commit: str, files_pathes: list[Path], object_type: RepositoryObjects
880+
) -> None:
881+
log = get_run_logger()
882+
log.info(f"Importing {object_type.value} from {files_pathes=}")
883+
845884
branch_wt = self.get_worktree(identifier=commit or branch_name)
846885

847-
file_pathes = [branch_wt.directory / obj.file_path for obj in config_file.objects]
848-
await self._load_objects(paths=file_pathes, branch=branch_name)
886+
file_pathes = [branch_wt.directory / file_path for file_path in files_pathes]
887+
888+
log.info("Before getting the repository object")
889+
# do not clone for now, assume only one import at a time
890+
if self.is_read_only:
891+
sdk_repo_obj = await self.get_client().get(
892+
kind=InfrahubKind.READONLYREPOSITORY, id=str(self.id), raise_when_missing=True
893+
)
894+
else:
895+
sdk_repo_obj = await self.get_client().get(
896+
kind=InfrahubKind.REPOSITORY, id=str(self.id), raise_when_missing=True
897+
)
898+
899+
log.info(f"After getting the repository object {sdk_repo_obj.id}")
849900

850-
# TODO attach to a group?
901+
async with self.get_client().start_tracking(
902+
identifier=f"group-repo-{object_type.value}-{self.id}",
903+
delete_unused_nodes=True,
904+
group_type="CoreRepositoryGroup",
905+
group_params={"content": object_type.value, "repository": sdk_repo_obj},
906+
):
907+
log.info("Started to track")
908+
file_type = ObjectFile if object_type == RepositoryObjects.OBJECT else MenuFile
909+
await self._load_objects(
910+
paths=file_pathes,
911+
branch=branch_name,
912+
file_type=file_type,
913+
)
851914

852915
@task(name="check-definition-get", task_run_name="Get Check Definition", cache_policy=NONE) # type: ignore[arg-type]
853916
async def get_check_definition(

backend/tests/fixtures/repos/car-dealership/initial__main/.infrahub.yml

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -100,7 +100,9 @@ queries:
100100
file_path: "templates/person_with_cars.gql"
101101

102102
objects:
103-
- name: "persons"
104-
file_path: "objects/persons.yml"
105-
- name: "manufacturers"
106-
file_path: "objects/manufacturers.yml"
103+
- "objects/persons.yml"
104+
- "objects/manufacturers.yml"
105+
106+
menus:
107+
- "menus/person_base.yml"
108+
- "menus/manufacturer_base.yml"
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
apiVersion: infrahub.app/v1
3+
kind: Menu
4+
spec:
5+
data:
6+
- namespace: Testing
7+
name: Manufacturer
8+
label: Manufacturer
9+
kind: TestingManufacturer
10+
children:
11+
data:
12+
- namespace: Testing
13+
name: Car
14+
label: Car
15+
kind: TestingCar
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
---
2+
apiVersion: infrahub.app/v1
3+
kind: Menu
4+
spec:
5+
data:
6+
- namespace: Testing
7+
name: Person
8+
label: Person
9+
kind: TestingPerson
10+
children:
11+
data:
12+
- namespace: Testing
13+
name: Manufacturer
14+
label: Manufacturer
15+
kind: TestingManufacturer

backend/tests/integration/git/test_repository.py

Lines changed: 38 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
import pytest
77
from git import GitCommandError
88

9-
from infrahub.core.constants import InfrahubKind, RepositoryOperationalStatus
9+
from infrahub.core.constants import InfrahubKind, RepositoryObjects, RepositoryOperationalStatus
1010
from infrahub.core.manager import NodeManager
1111
from infrahub.core.node import Node
1212
from infrahub.exceptions import RepositoryError
@@ -83,6 +83,43 @@ async def test_create_repository(
8383
assert manufacturer_mercedes.name.value == "Mercedes"
8484
assert list((await manufacturer_mercedes.customers.get_peers(db=db)).values())[0].name.value == "Ethan Carter"
8585

86+
repository_group = await NodeManager.get_one_by_default_filter(
87+
db=db,
88+
id=f"group-repo-{RepositoryObjects.OBJECT.value}-{repository.id}",
89+
kind="CoreRepositoryGroup",
90+
raise_on_error=True,
91+
prefetch_relationships=True,
92+
)
93+
assert repository_group.content.value == RepositoryObjects.OBJECT.value
94+
members = (await repository_group.members.get_peers(db=db)).values()
95+
assert len(members) == 4
96+
assert manufacturer_mercedes.id in {m.id for m in members}
97+
assert person_ethan.id in {m.id for m in members}
98+
99+
# TODO Retrieve menus
100+
101+
repository_group_menus = await NodeManager.get_one_by_default_filter(
102+
db=db,
103+
id=f"group-repo-{RepositoryObjects.MENU.value}-{repository.id}",
104+
kind="CoreRepositoryGroup",
105+
raise_on_error=True,
106+
prefetch_relationships=True,
107+
)
108+
109+
assert repository_group_menus.content.value == RepositoryObjects.MENU.value
110+
111+
_ = await NodeManager.get_one_by_hfid(
112+
db=db,
113+
hfid=["Testing", "Manufacturer"],
114+
kind="CoreMenu",
115+
raise_on_error=True,
116+
prefetch_relationships=True,
117+
)
118+
119+
_ = await NodeManager.get_one_by_hfid(
120+
db=db, hfid=["Testing", "Person"], kind="CoreMenu", raise_on_error=True, prefetch_relationships=True
121+
)
122+
86123
# TODO add a test with invalid yml file OR invalid order of objects in the yml file, and make sure the repository ends
87124
# up in error import state
88125

0 commit comments

Comments
 (0)