Skip to content

Commit 001b8ff

Browse files
authored
Allow to load data from git repo (#6527)
1 parent 835e25a commit 001b8ff

File tree

20 files changed

+364
-40
lines changed

20 files changed

+364
-40
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/constants/infrahubkind.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@
5858
PROPOSEDCHANGE = "CoreProposedChange"
5959
REFRESHTOKEN = "InternalRefreshToken"
6060
REPOSITORY = "CoreRepository"
61+
REPOSITORYGROUP = "CoreRepositoryGroup"
6162
RESOURCEPOOL = "CoreResourcePool"
6263
GENERICREPOSITORY = "CoreGenericRepository"
6364
READONLYREPOSITORY = "CoreReadOnlyRepository"

backend/infrahub/core/protocols.py

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

499499

500+
class CoreRepositoryGroup(CoreGroup):
501+
content: Dropdown
502+
repository: RelationshipManager
503+
504+
500505
class CoreRepositoryValidator(CoreValidator):
501506
repository: RelationshipManager
502507

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
@@ -116,6 +122,7 @@
116122
core_standard_group,
117123
core_generator_group,
118124
core_graphql_query_group,
125+
core_repository_group,
119126
builtin_tag,
120127
core_account,
121128
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: 102 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -29,18 +29,20 @@
2929
InfrahubPythonTransformConfig,
3030
InfrahubRepositoryConfig,
3131
)
32+
from infrahub_sdk.spec.menu import MenuFile
33+
from infrahub_sdk.spec.object import ObjectFile
3234
from infrahub_sdk.template import Jinja2Template
3335
from infrahub_sdk.template.exceptions import JinjaTemplateError
3436
from infrahub_sdk.utils import compare_lists
35-
from infrahub_sdk.yaml import SchemaFile
37+
from infrahub_sdk.yaml import InfrahubFile, SchemaFile
3638
from prefect import flow, task
3739
from prefect.cache_policies import NONE
3840
from prefect.logging import get_run_logger
3941
from pydantic import BaseModel, Field
4042
from pydantic import ValidationError as PydanticValidationError
4143
from typing_extensions import Self
4244

43-
from infrahub.core.constants import ArtifactStatus, ContentType, InfrahubKind, RepositorySyncStatus
45+
from infrahub.core.constants import ArtifactStatus, ContentType, InfrahubKind, RepositoryObjects, RepositorySyncStatus
4446
from infrahub.core.registry import registry
4547
from infrahub.events.artifact_action import ArtifactCreatedEvent, ArtifactUpdatedEvent
4648
from infrahub.events.models import EventMeta
@@ -54,6 +56,7 @@
5456
import types
5557

5658
from infrahub_sdk.checks import InfrahubCheck
59+
from infrahub_sdk.ctl.utils import YamlFileVar
5760
from infrahub_sdk.schema.repository import InfrahubRepositoryArtifactDefinitionConfig
5861
from infrahub_sdk.transforms import InfrahubTransform
5962

@@ -159,7 +162,7 @@ async def init(cls, service: InfrahubServices, commit: str | None = None, **kwar
159162
async def ensure_location_is_defined(self) -> None:
160163
if self.location:
161164
return
162-
client = self.get_client()
165+
client = self.sdk
163166
repo = await client.get(
164167
kind=CoreGenericRepository, name__value=self.name, exclude=["tags", "credential"], raise_when_missing=True
165168
)
@@ -179,6 +182,7 @@ async def import_objects_from_files(
179182

180183
config_file = await self.get_repository_config(branch_name=infrahub_branch_name, commit=commit) # type: ignore[misc]
181184
sync_status = RepositorySyncStatus.IN_SYNC if config_file else RepositorySyncStatus.ERROR_IMPORT
185+
182186
error: Exception | None = None
183187

184188
try:
@@ -189,6 +193,17 @@ async def import_objects_from_files(
189193
branch_name=infrahub_branch_name, commit=commit, config_file=config_file
190194
) # type: ignore[misc]
191195

196+
await self.import_objects(
197+
branch_name=infrahub_branch_name,
198+
commit=commit,
199+
config_file=config_file,
200+
) # type: ignore[misc]
201+
await self.import_objects(
202+
branch_name=infrahub_branch_name,
203+
commit=commit,
204+
config_file=config_file,
205+
) # type: ignore[misc]
206+
192207
await self.import_all_python_files( # type: ignore[call-overload]
193208
branch_name=infrahub_branch_name, commit=commit, config_file=config_file
194209
) # type: ignore[misc]
@@ -815,6 +830,80 @@ async def import_python_transforms(
815830
log.info(f"TransformPython {transform_name!r} not found locally, deleting")
816831
await transform_definition_in_graph[transform_name].delete()
817832

833+
async def _load_yamlfile_from_disk(self, paths: list[Path], file_type: type[YamlFileVar]) -> list[YamlFileVar]:
834+
data_files = file_type.load_from_disk(paths=paths)
835+
836+
for data_file in data_files:
837+
if not data_file.valid or not data_file.content:
838+
raise ValueError(f"{data_file.error_message} ({data_file.location})")
839+
840+
return data_files
841+
842+
async def _load_objects(
843+
self,
844+
paths: list[Path],
845+
branch: str,
846+
file_type: type[InfrahubFile],
847+
) -> None:
848+
"""Load one or multiple objects files into Infrahub."""
849+
850+
log = get_run_logger()
851+
files = await self._load_yamlfile_from_disk(paths=paths, file_type=file_type)
852+
853+
for file in files:
854+
await file.validate_format(client=self.sdk, branch=branch)
855+
schema = await self.sdk.schema.get(kind=file.spec.kind, branch=branch)
856+
if not schema.human_friendly_id and not schema.default_filter:
857+
raise ValueError(
858+
f"Schemas of objects or menus defined within {file.location} "
859+
"should have a `human_friendly_id` defined to avoid creating duplicated objects."
860+
)
861+
862+
for file in files:
863+
log.info(f"Loading objects defined in {file.location}")
864+
await file.process(client=self.sdk, branch=branch)
865+
866+
async def _import_file_paths(
867+
self, branch_name: str, commit: str, files_pathes: list[Path], object_type: RepositoryObjects
868+
) -> None:
869+
branch_wt = self.get_worktree(identifier=commit or branch_name)
870+
file_pathes = [branch_wt.directory / file_path for file_path in files_pathes]
871+
872+
# We currently assume there can't be concurrent imports, but if so, we might need to clone the client before tracking here.
873+
async with self.sdk.start_tracking(
874+
identifier=f"group-repo-{object_type.value}-{self.id}",
875+
delete_unused_nodes=True,
876+
branch=branch_name,
877+
group_type="CoreRepositoryGroup",
878+
group_params={"content": object_type.value, "repository": str(self.id)},
879+
):
880+
file_type = repo_object_type_to_file_type(object_type)
881+
await self._load_objects(
882+
paths=file_pathes,
883+
branch=branch_name,
884+
file_type=file_type,
885+
)
886+
887+
@task(name="import-objects", task_run_name="Import Objects", cache_policy=NONE) # type: ignore[arg-type]
888+
async def import_objects(
889+
self,
890+
branch_name: str,
891+
commit: str,
892+
config_file: InfrahubRepositoryConfig,
893+
) -> None:
894+
await self._import_file_paths(
895+
branch_name=branch_name,
896+
commit=commit,
897+
files_pathes=config_file.objects,
898+
object_type=RepositoryObjects.OBJECT,
899+
)
900+
await self._import_file_paths(
901+
branch_name=branch_name,
902+
commit=commit,
903+
files_pathes=config_file.menus,
904+
object_type=RepositoryObjects.MENU,
905+
)
906+
818907
@task(name="check-definition-get", task_run_name="Get Check Definition", cache_policy=NONE) # type: ignore[arg-type]
819908
async def get_check_definition(
820909
self,
@@ -1342,3 +1431,13 @@ async def render_artifact(
13421431

13431432
await self.service.event.send(event=event)
13441433
return ArtifactGenerateResult(changed=True, checksum=checksum, storage_id=storage_id, artifact_id=artifact.id)
1434+
1435+
1436+
def repo_object_type_to_file_type(repo_object: RepositoryObjects) -> type[InfrahubFile]:
1437+
match repo_object:
1438+
case RepositoryObjects.OBJECT:
1439+
return ObjectFile
1440+
case RepositoryObjects.MENU:
1441+
return MenuFile
1442+
case _:
1443+
raise ValueError(f"Unknown repository object type: {repo_object}")

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

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -98,3 +98,11 @@ queries:
9898
file_path: "generators/cartags.gql"
9999
- name: person_with_cars
100100
file_path: "templates/person_with_cars.gql"
101+
102+
objects:
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

0 commit comments

Comments
 (0)