Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions backend/infrahub/core/constants/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,11 @@ class AllowOverrideType(InfrahubStringEnum):
ANY = "any"


class RepositoryObjects(InfrahubStringEnum):
OBJECT = "object"
MENU = "menu"


class ContentType(InfrahubStringEnum):
APPLICATION_JSON = "application/json"
APPLICATION_YAML = "application/yaml"
Expand Down
1 change: 1 addition & 0 deletions backend/infrahub/core/constants/infrahubkind.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@
PROPOSEDCHANGE = "CoreProposedChange"
REFRESHTOKEN = "InternalRefreshToken"
REPOSITORY = "CoreRepository"
REPOSITORYGROUP = "CoreRepositoryGroup"
RESOURCEPOOL = "CoreResourcePool"
GENERICREPOSITORY = "CoreGenericRepository"
READONLYREPOSITORY = "CoreReadOnlyRepository"
Expand Down
5 changes: 5 additions & 0 deletions backend/infrahub/core/protocols.py
Original file line number Diff line number Diff line change
Expand Up @@ -497,6 +497,11 @@ class CoreRepository(LineageOwner, LineageSource, CoreGenericRepository, CoreTas
commit: StringOptional


class CoreRepositoryGroup(CoreGroup):
content: Dropdown
repository: RelationshipManager


class CoreRepositoryValidator(CoreValidator):
repository: RelationshipManager

Expand Down
9 changes: 8 additions & 1 deletion backend/infrahub/core/schema/definitions/core/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,13 @@
from .core import core_node, core_task_target
from .generator import core_generator_definition, core_generator_instance
from .graphql_query import core_graphql_query
from .group import core_generator_group, core_graphql_query_group, core_group, core_standard_group
from .group import (
core_generator_group,
core_graphql_query_group,
core_group,
core_repository_group,
core_standard_group,
)
from .ipam import builtin_ip_address, builtin_ip_prefix, builtin_ipam, core_ipam_namespace
from .lineage import lineage_owner, lineage_source
from .menu import generic_menu_item, menu_item
Expand Down Expand Up @@ -116,6 +122,7 @@
core_standard_group,
core_generator_group,
core_graphql_query_group,
core_repository_group,
builtin_tag,
core_account,
core_account_token,
Expand Down
45 changes: 45 additions & 0 deletions backend/infrahub/core/schema/definitions/core/group.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
from infrahub.core.constants import (
BranchSupportType,
InfrahubKind,
RepositoryObjects,
)
from infrahub.core.constants import RelationshipCardinality as Cardinality
from infrahub.core.constants import RelationshipKind as RelKind
from infrahub.core.schema.dropdown import DropdownChoice

from ...attribute_schema import AttributeSchema as Attr
from ...generic_schema import GenericSchema
Expand Down Expand Up @@ -80,6 +82,7 @@
generate_profile=False,
)


core_graphql_query_group = NodeSchema(
name="GraphQLQueryGroup",
namespace="Core",
Expand All @@ -106,3 +109,45 @@
),
],
)


core_repository_group = NodeSchema(
name="RepositoryGroup",
namespace="Core",
description="Group of nodes associated with a given repository.",
include_in_menu=False,
icon="mdi:account-group",
label="Repository Group",
default_filter="name__value",
order_by=["name__value"],
display_labels=["name__value"],
branch=BranchSupportType.LOCAL,
inherit_from=[InfrahubKind.GENERICGROUP],
generate_profile=False,
attributes=[
Attr(
name="content",
kind="Dropdown",
description="Type of data to load, can be either `object` or `menu`",
choices=[
DropdownChoice(
name=RepositoryObjects.OBJECT.value,
label="Objects",
),
DropdownChoice(
name=RepositoryObjects.MENU.value,
label="Menus",
),
],
optional=False,
),
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the infrahub/actions/schema.py I defined Dropdowns in a different way so that we have them as an Enum that can be used elsewhere for verification is needed. I'm not sure this is needed here but just wanted to highlight that as a possibility.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright, I think I'll keep the current way here, but good to know

],
relationships=[
Rel(
name="repository",
peer=InfrahubKind.GENERICREPOSITORY,
optional=False,
cardinality=Cardinality.ONE,
),
],
)
8 changes: 5 additions & 3 deletions backend/infrahub/git/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,11 @@ class InfrahubRepositoryBase(BaseModel, ABC):
infrahub_branch_name: str | None = Field(None, description="Infrahub branch on which to sync the remote repository")
model_config = ConfigDict(arbitrary_types_allowed=True, ignored_types=(Flow, Task))

def get_client(self) -> InfrahubClient:
if self.client is None:
raise ValueError("Client is not set")
return self.client

@property
def sdk(self) -> InfrahubClient:
if self.client:
Expand Down Expand Up @@ -445,9 +450,6 @@ def get_worktrees(self) -> list[Worktree]:

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

def get_client(self) -> InfrahubClient:
return self.sdk

def get_location(self) -> str:
if self.location:
return self.location
Expand Down
87 changes: 84 additions & 3 deletions backend/infrahub/git/integrator.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,18 +29,20 @@
InfrahubPythonTransformConfig,
InfrahubRepositoryConfig,
)
from infrahub_sdk.spec.menu import MenuFile
from infrahub_sdk.spec.object import ObjectFile
from infrahub_sdk.template import Jinja2Template
from infrahub_sdk.template.exceptions import JinjaTemplateError
from infrahub_sdk.utils import compare_lists
from infrahub_sdk.yaml import SchemaFile
from infrahub_sdk.yaml import InfrahubFile, SchemaFile
from prefect import flow, task
from prefect.cache_policies import NONE
from prefect.logging import get_run_logger
from pydantic import BaseModel, Field
from pydantic import ValidationError as PydanticValidationError
from typing_extensions import Self

from infrahub.core.constants import ArtifactStatus, ContentType, InfrahubKind, RepositorySyncStatus
from infrahub.core.constants import ArtifactStatus, ContentType, InfrahubKind, RepositoryObjects, RepositorySyncStatus
from infrahub.core.registry import registry
from infrahub.events.artifact_action import ArtifactCreatedEvent, ArtifactUpdatedEvent
from infrahub.events.models import EventMeta
Expand All @@ -54,6 +56,7 @@
import types

from infrahub_sdk.checks import InfrahubCheck
from infrahub_sdk.ctl.utils import YamlFileVar
from infrahub_sdk.schema.repository import InfrahubRepositoryArtifactDefinitionConfig
from infrahub_sdk.transforms import InfrahubTransform

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

config_file = await self.get_repository_config(branch_name=infrahub_branch_name, commit=commit) # type: ignore[misc]
sync_status = RepositorySyncStatus.IN_SYNC if config_file else RepositorySyncStatus.ERROR_IMPORT

error: Exception | None = None

try:
Expand All @@ -189,6 +193,19 @@ async def import_objects_from_files(
branch_name=infrahub_branch_name, commit=commit, config_file=config_file
) # type: ignore[misc]

await self.import_objects(
branch_name=infrahub_branch_name,
commit=commit,
files_pathes=config_file.objects,
object_type=RepositoryObjects.OBJECT,
) # type: ignore[misc]
await self.import_objects(
branch_name=infrahub_branch_name,
commit=commit,
files_pathes=config_file.menus,
object_type=RepositoryObjects.MENU,
) # type: ignore[misc]

await self.import_all_python_files( # type: ignore[call-overload]
branch_name=infrahub_branch_name, commit=commit, config_file=config_file
) # type: ignore[misc]
Expand Down Expand Up @@ -815,6 +832,70 @@ async def import_python_transforms(
log.info(f"TransformPython {transform_name!r} not found locally, deleting")
await transform_definition_in_graph[transform_name].delete()

async def _load_yamlfile_from_disk(self, paths: list[Path], file_type: type[YamlFileVar]) -> list[YamlFileVar]:
data_files = file_type.load_from_disk(paths=paths)

for data_file in data_files:
if not data_file.valid or not data_file.content:
raise ValueError(f"{data_file.error_message} ({data_file.location})")

return data_files

async def _load_objects(
self,
paths: list[Path],
branch: str,
file_type: type[InfrahubFile],
) -> None:
"""Load one or multiple objects files into Infrahub."""

log = get_run_logger()
files = await self._load_yamlfile_from_disk(paths=paths, file_type=file_type)

for file in files:
await file.validate_format(client=self.get_client(), branch=branch)
schema = await self.get_client().schema.get(kind=file.spec.kind, branch=branch)
if not schema.human_friendly_id and not schema.default_filter:
raise ValueError(
f"Schemas of objects or menus defined within {file.location} "
"should have a `human_friendly_id` defined to avoid creating duplicated objects."
)

for file in files:
log.info(f"Loading objects defined in {file.location}")
await file.process(client=self.get_client(), branch=branch)

@task(name="import-objects", task_run_name="Import Objects", cache_policy=NONE) # type: ignore[arg-type]
async def import_objects(
self, branch_name: str, commit: str, files_pathes: list[Path], object_type: RepositoryObjects
) -> None:
branch_wt = self.get_worktree(identifier=commit or branch_name)
file_pathes = [branch_wt.directory / file_path for file_path in files_pathes]

if self.is_read_only:
sdk_repo_obj = await self.get_client().get(
kind=InfrahubKind.READONLYREPOSITORY, id=str(self.id), raise_when_missing=True
)
else:
sdk_repo_obj = await self.get_client().get(
kind=InfrahubKind.REPOSITORY, id=str(self.id), raise_when_missing=True
)

# We currently assume there can't be concurrent imports, but if so, we might need to clone the client before tracking here.
async with self.get_client().start_tracking(
identifier=f"group-repo-{object_type.value}-{self.id}",
delete_unused_nodes=True,
branch=branch_name,
group_type="CoreRepositoryGroup",
group_params={"content": object_type.value, "repository": sdk_repo_obj},
):
file_type = ObjectFile if object_type == RepositoryObjects.OBJECT else MenuFile
await self._load_objects(
paths=file_pathes,
branch=branch_name,
file_type=file_type,
)

@task(name="check-definition-get", task_run_name="Get Check Definition", cache_policy=NONE) # type: ignore[arg-type]
async def get_check_definition(
self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,3 +98,11 @@ queries:
file_path: "generators/cartags.gql"
- name: person_with_cars
file_path: "templates/person_with_cars.gql"

objects:
- "objects/persons.yml"
- "objects/manufacturers.yml"

menus:
- "menus/person_base.yml"
- "menus/manufacturer_base.yml"
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
apiVersion: infrahub.app/v1
kind: Menu
spec:
data:
- namespace: Testing
name: Manufacturer
label: Manufacturer
kind: TestingManufacturer
children:
data:
- namespace: Testing
name: Car
label: Car
kind: TestingCar
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
---
apiVersion: infrahub.app/v1
kind: Menu
spec:
data:
- namespace: Testing
name: Person
label: Person
kind: TestingPerson
children:
data:
- namespace: Testing
name: Manufacturer
label: Manufacturer
kind: TestingManufacturer
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
---
apiVersion: infrahub.app/v1
kind: Object
spec:
kind: TestingManufacturer
data:
- name: Mercedes
customers:
- "Ethan Carter"
- name: Ford
customers:
- "Olivia Bennett"
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
---
apiVersion: infrahub.app/v1
kind: Object
spec:
kind: TestingPerson
data:
- name: Ethan Carter
height: 180
- name: Olivia Bennett
height: 170
18 changes: 13 additions & 5 deletions backend/tests/integration/git/test_repository.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from tests.helpers.file_repo import FileRepo
from tests.helpers.schema import CAR_SCHEMA, load_schema
from tests.helpers.test_app import TestInfrahubApp
from tests.integration.git.utils import check_repo_correctly_created

if TYPE_CHECKING:
from pathlib import Path
Expand Down Expand Up @@ -50,6 +51,7 @@ async def test_create_repository(
initial_dataset: None,
git_repos_source_dir_module_scope: Path,
client: InfrahubClient,
default_branch,
) -> None:
"""Validate that we can create a repository, that it gets updated with the commit id and that objects are created."""
client_repository = await client.create(
Expand All @@ -59,18 +61,24 @@ async def test_create_repository(
await client_repository.save()

repository: CoreRepository = await NodeManager.get_one(
db=db, id=client_repository.id, kind=InfrahubKind.REPOSITORY, raise_on_error=True
db=db,
id=client_repository.id,
kind=InfrahubKind.REPOSITORY,
raise_on_error=True,
)

check_definition: CoreCheckDefinition = await NodeManager.get_one_by_default_filter(
db=db, id="car_description_check", kind=InfrahubKind.CHECKDEFINITION, raise_on_error=True
db=db,
id="car_description_check",
kind=InfrahubKind.CHECKDEFINITION,
raise_on_error=True,
)

assert repository.commit.value
assert repository.internal_status.value == "active"
assert repository.internal_status.value == "active", f"{repository.internal_status.value=}"
assert repository.operational_status.value == "online"
assert check_definition.file_path.value == "checks/car_overview.py"

await check_repo_correctly_created(repo_id=client_repository.id, db=db, branch_name=default_branch.name)

@pytest.mark.parametrize(
"stderr,expected_operational_status",
[
Expand Down
Loading