infrahub-server 1.6.3__py3-none-any.whl → 1.7.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- infrahub/actions/tasks.py +4 -2
- infrahub/api/exceptions.py +2 -2
- infrahub/api/schema.py +3 -1
- infrahub/artifacts/tasks.py +1 -0
- infrahub/auth.py +2 -2
- infrahub/cli/db.py +54 -28
- infrahub/computed_attribute/gather.py +3 -4
- infrahub/computed_attribute/tasks.py +23 -6
- infrahub/config.py +8 -0
- infrahub/constants/enums.py +12 -0
- infrahub/core/account.py +4 -4
- infrahub/core/attribute.py +106 -108
- infrahub/core/branch/models.py +44 -71
- infrahub/core/branch/tasks.py +5 -3
- infrahub/core/changelog/diff.py +1 -20
- infrahub/core/changelog/models.py +0 -7
- infrahub/core/constants/__init__.py +17 -0
- infrahub/core/constants/database.py +0 -1
- infrahub/core/constants/schema.py +0 -1
- infrahub/core/convert_object_type/repository_conversion.py +3 -4
- infrahub/core/diff/branch_differ.py +1 -1
- infrahub/core/diff/conflict_transferer.py +1 -1
- infrahub/core/diff/data_check_synchronizer.py +4 -3
- infrahub/core/diff/enricher/cardinality_one.py +2 -2
- infrahub/core/diff/enricher/hierarchy.py +1 -1
- infrahub/core/diff/enricher/labels.py +1 -1
- infrahub/core/diff/merger/merger.py +28 -2
- infrahub/core/diff/merger/serializer.py +3 -10
- infrahub/core/diff/model/diff.py +1 -1
- infrahub/core/diff/query/merge.py +376 -135
- infrahub/core/diff/repository/repository.py +3 -1
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/constraints.py +3 -3
- infrahub/core/graph/schema.py +2 -12
- infrahub/core/ipam/reconciler.py +8 -6
- infrahub/core/ipam/utilization.py +8 -15
- infrahub/core/manager.py +133 -152
- infrahub/core/merge.py +1 -1
- infrahub/core/metadata/__init__.py +0 -0
- infrahub/core/metadata/interface.py +37 -0
- infrahub/core/metadata/model.py +31 -0
- infrahub/core/metadata/query/__init__.py +0 -0
- infrahub/core/metadata/query/node_metadata.py +301 -0
- infrahub/core/migrations/graph/__init__.py +4 -0
- infrahub/core/migrations/graph/m012_convert_account_generic.py +12 -12
- infrahub/core/migrations/graph/m013_convert_git_password_credential.py +7 -12
- infrahub/core/migrations/graph/m017_add_core_profile.py +5 -2
- infrahub/core/migrations/graph/m018_uniqueness_nulls.py +2 -1
- infrahub/core/migrations/graph/m019_restore_rels_to_time.py +0 -10
- infrahub/core/migrations/graph/m020_duplicate_edges.py +0 -8
- infrahub/core/migrations/graph/m025_uniqueness_nulls.py +2 -1
- infrahub/core/migrations/graph/m026_0000_prefix_fix.py +2 -1
- infrahub/core/migrations/graph/m029_duplicates_cleanup.py +0 -1
- infrahub/core/migrations/graph/m031_check_number_attributes.py +2 -2
- infrahub/core/migrations/graph/m038_redo_0000_prefix_fix.py +2 -1
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +1 -1
- infrahub/core/migrations/graph/m049_remove_is_visible_relationship.py +53 -0
- infrahub/core/migrations/graph/m050_backfill_vertex_metadata.py +168 -0
- infrahub/core/migrations/query/__init__.py +2 -2
- infrahub/core/migrations/query/attribute_add.py +17 -6
- infrahub/core/migrations/query/attribute_remove.py +19 -5
- infrahub/core/migrations/query/attribute_rename.py +21 -5
- infrahub/core/migrations/query/node_duplicate.py +19 -4
- infrahub/core/migrations/query/schema_attribute_update.py +1 -1
- infrahub/core/migrations/schema/attribute_kind_update.py +21 -2
- infrahub/core/migrations/schema/attribute_name_update.py +1 -1
- infrahub/core/migrations/schema/attribute_supports_profile.py +5 -3
- infrahub/core/migrations/schema/models.py +3 -0
- infrahub/core/migrations/schema/node_attribute_add.py +5 -2
- infrahub/core/migrations/schema/node_attribute_remove.py +1 -1
- infrahub/core/migrations/schema/node_kind_update.py +1 -1
- infrahub/core/migrations/schema/node_remove.py +24 -2
- infrahub/core/migrations/schema/tasks.py +4 -1
- infrahub/core/migrations/shared.py +13 -6
- infrahub/core/models.py +6 -6
- infrahub/core/node/__init__.py +157 -58
- infrahub/core/node/base.py +9 -5
- infrahub/core/node/create.py +7 -3
- infrahub/core/node/delete_validator.py +1 -1
- infrahub/core/node/standard.py +100 -14
- infrahub/core/order.py +30 -0
- infrahub/core/property.py +0 -1
- infrahub/core/protocols.py +1 -0
- infrahub/core/protocols_base.py +10 -2
- infrahub/core/query/__init__.py +5 -3
- infrahub/core/query/attribute.py +164 -49
- infrahub/core/query/branch.py +58 -70
- infrahub/core/query/delete.py +1 -1
- infrahub/core/query/diff.py +7 -7
- infrahub/core/query/ipam.py +104 -43
- infrahub/core/query/node.py +1072 -281
- infrahub/core/query/relationship.py +531 -325
- infrahub/core/query/resource_manager.py +107 -18
- infrahub/core/query/standard_node.py +25 -5
- infrahub/core/query/utils.py +2 -4
- infrahub/core/relationship/constraints/count.py +1 -1
- infrahub/core/relationship/constraints/peer_kind.py +1 -1
- infrahub/core/relationship/constraints/peer_parent.py +1 -1
- infrahub/core/relationship/constraints/peer_relatives.py +1 -1
- infrahub/core/relationship/constraints/profiles_kind.py +1 -1
- infrahub/core/relationship/constraints/profiles_removal.py +168 -0
- infrahub/core/relationship/model.py +293 -139
- infrahub/core/schema/attribute_schema.py +2 -2
- infrahub/core/schema/basenode_schema.py +3 -0
- infrahub/core/schema/definitions/core/__init__.py +8 -2
- infrahub/core/schema/definitions/core/account.py +10 -10
- infrahub/core/schema/definitions/core/artifact.py +14 -8
- infrahub/core/schema/definitions/core/check.py +10 -4
- infrahub/core/schema/definitions/core/generator.py +26 -6
- infrahub/core/schema/definitions/core/graphql_query.py +1 -1
- infrahub/core/schema/definitions/core/group.py +9 -2
- infrahub/core/schema/definitions/core/ipam.py +80 -10
- infrahub/core/schema/definitions/core/menu.py +41 -7
- infrahub/core/schema/definitions/core/permission.py +16 -2
- infrahub/core/schema/definitions/core/profile.py +16 -2
- infrahub/core/schema/definitions/core/propose_change.py +24 -4
- infrahub/core/schema/definitions/core/propose_change_comment.py +23 -11
- infrahub/core/schema/definitions/core/propose_change_validator.py +50 -21
- infrahub/core/schema/definitions/core/repository.py +10 -0
- infrahub/core/schema/definitions/core/resource_pool.py +8 -1
- infrahub/core/schema/definitions/core/template.py +19 -2
- infrahub/core/schema/definitions/core/transform.py +11 -5
- infrahub/core/schema/definitions/core/webhook.py +27 -9
- infrahub/core/schema/manager.py +63 -43
- infrahub/core/schema/relationship_schema.py +6 -2
- infrahub/core/schema/schema_branch.py +48 -10
- infrahub/core/task/task.py +4 -2
- infrahub/core/utils.py +3 -25
- infrahub/core/validators/aggregated_checker.py +1 -1
- infrahub/core/validators/attribute/choices.py +1 -1
- infrahub/core/validators/attribute/enum.py +1 -1
- infrahub/core/validators/attribute/kind.py +1 -1
- infrahub/core/validators/attribute/length.py +1 -1
- infrahub/core/validators/attribute/min_max.py +1 -1
- infrahub/core/validators/attribute/number_pool.py +1 -1
- infrahub/core/validators/attribute/optional.py +1 -1
- infrahub/core/validators/attribute/regex.py +1 -1
- infrahub/core/validators/determiner.py +3 -3
- infrahub/core/validators/node/attribute.py +1 -1
- infrahub/core/validators/node/relationship.py +1 -1
- infrahub/core/validators/relationship/peer.py +1 -1
- infrahub/database/__init__.py +4 -4
- infrahub/dependencies/builder/constraint/grouped/node_runner.py +2 -0
- infrahub/dependencies/builder/constraint/relationship_manager/profiles_removal.py +8 -0
- infrahub/dependencies/registry.py +2 -0
- infrahub/display_labels/tasks.py +12 -3
- infrahub/git/integrator.py +18 -18
- infrahub/git/tasks.py +1 -1
- infrahub/git/utils.py +1 -1
- infrahub/graphql/constants.py +3 -0
- infrahub/graphql/context.py +1 -1
- infrahub/graphql/field_extractor.py +1 -1
- infrahub/graphql/initialization.py +11 -0
- infrahub/graphql/loaders/account.py +134 -0
- infrahub/graphql/loaders/node.py +5 -12
- infrahub/graphql/loaders/peers.py +5 -7
- infrahub/graphql/manager.py +175 -21
- infrahub/graphql/metadata.py +91 -0
- infrahub/graphql/mutations/account.py +6 -6
- infrahub/graphql/mutations/attribute.py +0 -2
- infrahub/graphql/mutations/branch.py +9 -5
- infrahub/graphql/mutations/computed_attribute.py +1 -1
- infrahub/graphql/mutations/display_label.py +1 -1
- infrahub/graphql/mutations/hfid.py +1 -1
- infrahub/graphql/mutations/ipam.py +4 -6
- infrahub/graphql/mutations/main.py +9 -4
- infrahub/graphql/mutations/profile.py +16 -22
- infrahub/graphql/mutations/proposed_change.py +4 -4
- infrahub/graphql/mutations/relationship.py +40 -10
- infrahub/graphql/mutations/repository.py +14 -12
- infrahub/graphql/mutations/schema.py +2 -2
- infrahub/graphql/order.py +14 -0
- infrahub/graphql/queries/branch.py +62 -6
- infrahub/graphql/queries/resource_manager.py +25 -24
- infrahub/graphql/resolvers/account_metadata.py +84 -0
- infrahub/graphql/resolvers/ipam.py +6 -8
- infrahub/graphql/resolvers/many_relationship.py +77 -35
- infrahub/graphql/resolvers/resolver.py +59 -14
- infrahub/graphql/resolvers/single_relationship.py +87 -23
- infrahub/graphql/subscription/graphql_query.py +2 -0
- infrahub/graphql/types/__init__.py +0 -1
- infrahub/graphql/types/attribute.py +10 -5
- infrahub/graphql/types/branch.py +40 -53
- infrahub/graphql/types/enums.py +3 -0
- infrahub/graphql/types/metadata.py +28 -0
- infrahub/graphql/types/node.py +22 -2
- infrahub/graphql/types/relationship.py +10 -2
- infrahub/graphql/types/standard_node.py +12 -7
- infrahub/hfid/tasks.py +12 -3
- infrahub/lock.py +7 -0
- infrahub/menu/repository.py +1 -1
- infrahub/patch/queries/base.py +1 -1
- infrahub/pools/number.py +1 -8
- infrahub/profiles/gather.py +56 -0
- infrahub/profiles/mandatory_fields_checker.py +116 -0
- infrahub/profiles/models.py +66 -0
- infrahub/profiles/node_applier.py +154 -13
- infrahub/profiles/queries/get_profile_data.py +143 -31
- infrahub/profiles/tasks.py +79 -27
- infrahub/profiles/triggers.py +22 -0
- infrahub/proposed_change/action_checker.py +1 -1
- infrahub/proposed_change/tasks.py +4 -1
- infrahub/services/__init__.py +1 -1
- infrahub/services/adapters/cache/nats.py +1 -1
- infrahub/services/adapters/cache/redis.py +7 -0
- infrahub/tasks/artifact.py +1 -0
- infrahub/transformations/tasks.py +2 -2
- infrahub/trigger/catalogue.py +2 -0
- infrahub/trigger/models.py +1 -0
- infrahub/trigger/setup.py +3 -3
- infrahub/trigger/tasks.py +3 -0
- infrahub/validators/tasks.py +1 -0
- infrahub/webhook/gather.py +1 -1
- infrahub/webhook/models.py +1 -1
- infrahub/webhook/tasks.py +23 -7
- infrahub/workers/dependencies.py +9 -3
- infrahub/workers/infrahub_async.py +13 -4
- infrahub/workflows/catalogue.py +19 -0
- infrahub_sdk/analyzer.py +2 -2
- infrahub_sdk/branch.py +12 -39
- infrahub_sdk/checks.py +4 -4
- infrahub_sdk/client.py +36 -0
- infrahub_sdk/ctl/cli_commands.py +2 -1
- infrahub_sdk/ctl/graphql.py +15 -4
- infrahub_sdk/ctl/utils.py +2 -2
- infrahub_sdk/enums.py +6 -0
- infrahub_sdk/graphql/renderers.py +21 -0
- infrahub_sdk/graphql/utils.py +85 -0
- infrahub_sdk/node/attribute.py +12 -2
- infrahub_sdk/node/constants.py +12 -0
- infrahub_sdk/node/metadata.py +69 -0
- infrahub_sdk/node/node.py +65 -14
- infrahub_sdk/node/property.py +3 -0
- infrahub_sdk/node/related_node.py +37 -5
- infrahub_sdk/node/relationship.py +18 -1
- infrahub_sdk/operation.py +2 -2
- infrahub_sdk/schema/repository.py +1 -2
- infrahub_sdk/transforms.py +2 -2
- infrahub_sdk/types.py +18 -2
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/METADATA +17 -16
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/RECORD +249 -228
- infrahub_testcontainers/container.py +3 -3
- infrahub_testcontainers/docker-compose-cluster.test.yml +7 -7
- infrahub_testcontainers/docker-compose.test.yml +13 -5
- infrahub_testcontainers/models.py +3 -3
- infrahub_testcontainers/performance_test.py +1 -1
- infrahub/graphql/models.py +0 -6
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/entry_points.txt +0 -0
- {infrahub_server-1.6.3.dist-info → infrahub_server-1.7.0.dist-info}/licenses/LICENSE.txt +0 -0
infrahub/menu/repository.py
CHANGED
|
@@ -6,7 +6,7 @@ from .models import MenuDict, MenuItemDefinition, MenuItemDict
|
|
|
6
6
|
|
|
7
7
|
|
|
8
8
|
class MenuRepository:
|
|
9
|
-
def __init__(self, db: InfrahubDatabase):
|
|
9
|
+
def __init__(self, db: InfrahubDatabase) -> None:
|
|
10
10
|
self.db = db
|
|
11
11
|
|
|
12
12
|
async def get_menu(self, nodes: dict[str, CoreMenuItem] | None = None) -> MenuDict:
|
infrahub/patch/queries/base.py
CHANGED
infrahub/pools/number.py
CHANGED
|
@@ -37,14 +37,7 @@ class NumberUtilizationGetter:
|
|
|
37
37
|
query = await NumberPoolGetAllocated.init(db=self.db, pool=self.pool, branch=self.branch, branch_agnostic=True)
|
|
38
38
|
await query.execute(db=self.db)
|
|
39
39
|
|
|
40
|
-
self.used = [
|
|
41
|
-
UsedNumber(
|
|
42
|
-
number=result.get_as_type(label="value", return_type=int),
|
|
43
|
-
branch=result.get_as_type(label="branch", return_type=str),
|
|
44
|
-
)
|
|
45
|
-
for result in query.results
|
|
46
|
-
if result.get_as_optional_type(label="value", return_type=int) is not None
|
|
47
|
-
]
|
|
40
|
+
self.used = [UsedNumber(number=item.value, branch=item.branch) for item in query.get_data()]
|
|
48
41
|
|
|
49
42
|
self.used_default_branch = {entry.number for entry in self.used if entry.branch == registry.default_branch}
|
|
50
43
|
used_branches = {entry.number for entry in self.used if entry.branch != registry.default_branch}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from prefect import task
|
|
4
|
+
from prefect.cache_policies import NONE
|
|
5
|
+
|
|
6
|
+
from infrahub.core.constants import RelationshipKind
|
|
7
|
+
from infrahub.core.registry import registry
|
|
8
|
+
from infrahub.database import InfrahubDatabase # noqa: TC001 needed for prefect flow
|
|
9
|
+
from infrahub.workflows.catalogue import PROFILE_REFRESH_PROCESS
|
|
10
|
+
|
|
11
|
+
from .models import ProfileRefreshTriggerDefinition
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@task(name="gather-trigger-profile-refresh", cache_policy=NONE)
|
|
15
|
+
async def gather_trigger_profile_refresh(
|
|
16
|
+
db: InfrahubDatabase | None = None, # noqa: ARG001 Needed to have a common function signature for gathering functions
|
|
17
|
+
) -> list[ProfileRefreshTriggerDefinition]:
|
|
18
|
+
"""Gather profile refresh triggers for all profile schemas.
|
|
19
|
+
|
|
20
|
+
This function creates trigger definitions for each profile schema that will
|
|
21
|
+
listen for `NodeUpdatedEvent` on profiles. When a profile's attributes or
|
|
22
|
+
relationships change, the trigger will fire and execute the profile refresh
|
|
23
|
+
workflow to re-apply profiles to all related nodes.
|
|
24
|
+
"""
|
|
25
|
+
branches_with_diff_from_main = registry.get_altered_schema_branches()
|
|
26
|
+
branches_to_process: list[tuple[str, list[str]]] = [(branch, []) for branch in branches_with_diff_from_main]
|
|
27
|
+
branches_to_process.append((registry.default_branch, branches_with_diff_from_main))
|
|
28
|
+
|
|
29
|
+
triggers: list[ProfileRefreshTriggerDefinition] = []
|
|
30
|
+
|
|
31
|
+
for branch_scope, branches_out_of_scope in branches_to_process:
|
|
32
|
+
schema_branch = registry.schema.get_schema_branch(name=branch_scope)
|
|
33
|
+
|
|
34
|
+
for profile_name in schema_branch.profile_names:
|
|
35
|
+
profile_schema = schema_branch.get_profile(name=profile_name, duplicate=False)
|
|
36
|
+
|
|
37
|
+
trigger_attr = [attr.name for attr in profile_schema.attributes if attr.name != "profile_name"]
|
|
38
|
+
trigger_rels = [
|
|
39
|
+
rel.name
|
|
40
|
+
for rel in profile_schema.relationships
|
|
41
|
+
if rel.kind in {RelationshipKind.GENERIC, RelationshipKind.ATTRIBUTE} and rel.name != "related_nodes"
|
|
42
|
+
]
|
|
43
|
+
trigger_fields = sorted(trigger_attr + trigger_rels)
|
|
44
|
+
|
|
45
|
+
if trigger_fields:
|
|
46
|
+
triggers.append(
|
|
47
|
+
ProfileRefreshTriggerDefinition.from_profile_schema(
|
|
48
|
+
branch=branch_scope,
|
|
49
|
+
profile_kind=profile_schema.kind,
|
|
50
|
+
trigger_fields=trigger_fields,
|
|
51
|
+
workflow=PROFILE_REFRESH_PROCESS,
|
|
52
|
+
branches_out_of_scope=branches_out_of_scope,
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
return triggers
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from dataclasses import dataclass
|
|
4
|
+
from typing import TYPE_CHECKING, Any
|
|
5
|
+
|
|
6
|
+
from infrahub.core.query.node import NodeGetByHFIDQuery
|
|
7
|
+
|
|
8
|
+
from .queries.get_profile_data import GetProfileDataQuery, RelationshipFilter
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from collections.abc import Sequence
|
|
12
|
+
|
|
13
|
+
from infrahub.core.branch import Branch
|
|
14
|
+
from infrahub.core.schema import NodeSchema
|
|
15
|
+
from infrahub.database import InfrahubDatabase
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
@dataclass
|
|
19
|
+
class ProfileIdentifiers:
|
|
20
|
+
ids: list[str]
|
|
21
|
+
hfids: list[list[str]]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _extract_profile_identifiers_from_input(profiles_data: Sequence[Any] | None) -> ProfileIdentifiers:
|
|
25
|
+
"""Extract profile IDs and HFIDs from input data."""
|
|
26
|
+
if not profiles_data:
|
|
27
|
+
return ProfileIdentifiers(ids=[], hfids=[])
|
|
28
|
+
|
|
29
|
+
ids: list[str] = []
|
|
30
|
+
hfids: list[list[str]] = []
|
|
31
|
+
for item in profiles_data:
|
|
32
|
+
if isinstance(item, str):
|
|
33
|
+
# IDs
|
|
34
|
+
ids.append(item)
|
|
35
|
+
elif isinstance(item, dict):
|
|
36
|
+
# Dicts with `id` or `hfid` keys
|
|
37
|
+
if profile_id := item.get("id"):
|
|
38
|
+
ids.append(profile_id)
|
|
39
|
+
elif profile_hfid := item.get("hfid"):
|
|
40
|
+
hfids.append(profile_hfid)
|
|
41
|
+
elif hasattr(item, "id"):
|
|
42
|
+
# Avoid circular import by not importing Node directly
|
|
43
|
+
ids.append(item.id)
|
|
44
|
+
|
|
45
|
+
return ProfileIdentifiers(ids=ids, hfids=hfids)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
async def _resolve_hfids_to_ids(
|
|
49
|
+
db: InfrahubDatabase, branch: Branch, profile_kind: str, hfids: list[list[str]]
|
|
50
|
+
) -> list[str]:
|
|
51
|
+
query = await NodeGetByHFIDQuery.init(db=db, branch=branch, node_kind=profile_kind, hfids=hfids)
|
|
52
|
+
await query.execute(db=db)
|
|
53
|
+
return query.get_node_uuids()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class ProfilesMandatoryFieldGetter:
|
|
57
|
+
def __init__(self, db: InfrahubDatabase, branch: Branch) -> None:
|
|
58
|
+
self.db = db
|
|
59
|
+
self.branch = branch
|
|
60
|
+
|
|
61
|
+
async def get_mandatory_fields_from_profiles(
|
|
62
|
+
self,
|
|
63
|
+
schema: NodeSchema,
|
|
64
|
+
profiles_data: Sequence[Any] | None,
|
|
65
|
+
mandatory_attr_names: list[str],
|
|
66
|
+
mandatory_rel_names: list[str],
|
|
67
|
+
) -> tuple[set[str], set[str]]:
|
|
68
|
+
"""Get mandatory attributes and relationships that are provided by profiles."""
|
|
69
|
+
identifiers = _extract_profile_identifiers_from_input(profiles_data=profiles_data)
|
|
70
|
+
|
|
71
|
+
profile_ids = list(identifiers.ids)
|
|
72
|
+
if identifiers.hfids:
|
|
73
|
+
resolved_ids = await _resolve_hfids_to_ids(
|
|
74
|
+
db=self.db, branch=self.branch, profile_kind=f"Profile{schema.kind}", hfids=identifiers.hfids
|
|
75
|
+
)
|
|
76
|
+
profile_ids.extend(resolved_ids)
|
|
77
|
+
|
|
78
|
+
if not profile_ids:
|
|
79
|
+
return set(), set()
|
|
80
|
+
|
|
81
|
+
rel_filters: list[RelationshipFilter] = []
|
|
82
|
+
rel_name_to_filter: dict[str, RelationshipFilter] = {}
|
|
83
|
+
for rel_name in mandatory_rel_names:
|
|
84
|
+
rel_schema = schema.get_relationship(name=rel_name)
|
|
85
|
+
if not rel_schema.support_profiles:
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
rel_filter = RelationshipFilter(
|
|
89
|
+
relationship_identifier=f"profile_{rel_schema.get_identifier()}", direction=rel_schema.direction
|
|
90
|
+
)
|
|
91
|
+
rel_filters.append(rel_filter)
|
|
92
|
+
rel_name_to_filter[rel_name] = rel_filter
|
|
93
|
+
|
|
94
|
+
query = await GetProfileDataQuery.init(
|
|
95
|
+
db=self.db,
|
|
96
|
+
branch=self.branch,
|
|
97
|
+
profile_ids=profile_ids,
|
|
98
|
+
attr_names=mandatory_attr_names,
|
|
99
|
+
relationship_filters=rel_filters,
|
|
100
|
+
)
|
|
101
|
+
await query.execute(db=self.db)
|
|
102
|
+
profile_data_list = query.get_profile_data()
|
|
103
|
+
|
|
104
|
+
provided_attrs: set[str] = set()
|
|
105
|
+
provided_rels: set[str] = set()
|
|
106
|
+
|
|
107
|
+
for profile_data in profile_data_list:
|
|
108
|
+
for attr_name in mandatory_attr_names:
|
|
109
|
+
if profile_data.attribute_values.get(attr_name) is not None:
|
|
110
|
+
provided_attrs.add(attr_name)
|
|
111
|
+
|
|
112
|
+
for rel_name, rel_filter in rel_name_to_filter.items():
|
|
113
|
+
if profile_data.relationship_peers.get(rel_filter):
|
|
114
|
+
provided_rels.add(rel_name)
|
|
115
|
+
|
|
116
|
+
return provided_attrs, provided_rels
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from typing import TYPE_CHECKING, Self
|
|
4
|
+
|
|
5
|
+
from infrahub.core.registry import registry
|
|
6
|
+
from infrahub.events import NodeUpdatedEvent
|
|
7
|
+
from infrahub.trigger.constants import NAME_SEPARATOR
|
|
8
|
+
from infrahub.trigger.models import EventTrigger, ExecuteWorkflow, TriggerBranchDefinition, TriggerType
|
|
9
|
+
|
|
10
|
+
if TYPE_CHECKING:
|
|
11
|
+
from infrahub.workflows.models import WorkflowDefinition
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ProfileRefreshTriggerDefinition(TriggerBranchDefinition):
|
|
15
|
+
"""Trigger definition for profile refresh when profile attributes/relationships change."""
|
|
16
|
+
|
|
17
|
+
type: TriggerType = TriggerType.PROFILE
|
|
18
|
+
profile_kind: str
|
|
19
|
+
|
|
20
|
+
@classmethod
|
|
21
|
+
def from_profile_schema(
|
|
22
|
+
cls,
|
|
23
|
+
branch: str,
|
|
24
|
+
profile_kind: str,
|
|
25
|
+
trigger_fields: list[str],
|
|
26
|
+
workflow: WorkflowDefinition,
|
|
27
|
+
branches_out_of_scope: list[str] | None = None,
|
|
28
|
+
) -> Self:
|
|
29
|
+
"""Create a trigger definition for profile refresh when profile attributes/relationships change."""
|
|
30
|
+
event_trigger = EventTrigger()
|
|
31
|
+
event_trigger.events.add(NodeUpdatedEvent.event_name)
|
|
32
|
+
event_trigger.match = {"infrahub.node.kind": profile_kind}
|
|
33
|
+
|
|
34
|
+
if branches_out_of_scope:
|
|
35
|
+
event_trigger.match["infrahub.branch.name"] = [f"!{b}" for b in branches_out_of_scope]
|
|
36
|
+
elif branch != registry.default_branch:
|
|
37
|
+
event_trigger.match["infrahub.branch.name"] = branch
|
|
38
|
+
|
|
39
|
+
event_trigger.match_related = {
|
|
40
|
+
"prefect.resource.role": ["infrahub.node.attribute_update", "infrahub.node.relationship_update"],
|
|
41
|
+
"infrahub.field.name": trigger_fields,
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
workflow_action = ExecuteWorkflow(
|
|
45
|
+
workflow=workflow,
|
|
46
|
+
parameters={
|
|
47
|
+
"branch_name": "{{ event.resource['infrahub.branch.name'] }}",
|
|
48
|
+
"profile_kind": profile_kind,
|
|
49
|
+
"profile_id": "{{ event.resource['infrahub.node.id'] }}",
|
|
50
|
+
"context": {
|
|
51
|
+
"__prefect_kind": "json",
|
|
52
|
+
"value": {
|
|
53
|
+
"__prefect_kind": "jinja",
|
|
54
|
+
"template": "{{ event.payload['context'] | tojson }}",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
return cls(
|
|
61
|
+
name=f"{profile_kind}{NAME_SEPARATOR}refresh",
|
|
62
|
+
branch=branch,
|
|
63
|
+
profile_kind=profile_kind,
|
|
64
|
+
trigger=event_trigger,
|
|
65
|
+
actions=[workflow_action],
|
|
66
|
+
)
|
|
@@ -2,10 +2,13 @@ from typing import Any
|
|
|
2
2
|
|
|
3
3
|
from infrahub.core.attribute import BaseAttribute
|
|
4
4
|
from infrahub.core.branch import Branch
|
|
5
|
+
from infrahub.core.constants import InfrahubKind
|
|
5
6
|
from infrahub.core.node import Node
|
|
7
|
+
from infrahub.core.relationship import RelationshipManager
|
|
8
|
+
from infrahub.core.relationship.model import Relationship
|
|
6
9
|
from infrahub.database import InfrahubDatabase
|
|
7
10
|
|
|
8
|
-
from .queries.get_profile_data import GetProfileDataQuery, ProfileData
|
|
11
|
+
from .queries.get_profile_data import GetProfileDataQuery, ProfileData, RelationshipFilter
|
|
9
12
|
|
|
10
13
|
|
|
11
14
|
class NodeProfilesApplier:
|
|
@@ -18,7 +21,7 @@ class NodeProfilesApplier:
|
|
|
18
21
|
3. Profile priority determines which profile wins when multiple profiles set the same attribute
|
|
19
22
|
"""
|
|
20
23
|
|
|
21
|
-
def __init__(self, db: InfrahubDatabase, branch: Branch):
|
|
24
|
+
def __init__(self, db: InfrahubDatabase, branch: Branch) -> None:
|
|
22
25
|
self.db = db
|
|
23
26
|
self.branch = branch
|
|
24
27
|
|
|
@@ -42,13 +45,51 @@ class NodeProfilesApplier:
|
|
|
42
45
|
attr_names_for_profiles.append(attr_name)
|
|
43
46
|
return attr_names_for_profiles
|
|
44
47
|
|
|
48
|
+
async def _get_rel_names_for_profiles(self, node: Node) -> list[str]:
|
|
49
|
+
node_schema = node.get_schema()
|
|
50
|
+
|
|
51
|
+
rel_names_for_profiles: list[str] = []
|
|
52
|
+
for rel_schema in node_schema.relationships:
|
|
53
|
+
if not rel_schema.support_profiles:
|
|
54
|
+
continue
|
|
55
|
+
|
|
56
|
+
rel_name = rel_schema.name
|
|
57
|
+
node_rel = node.get_relationship(rel_name)
|
|
58
|
+
|
|
59
|
+
current_rels = await node_rel.get_relationships(db=self.db)
|
|
60
|
+
if node_rel.is_from_profile or len(current_rels) == 0:
|
|
61
|
+
rel_names_for_profiles.append(rel_name)
|
|
62
|
+
|
|
63
|
+
return rel_names_for_profiles
|
|
64
|
+
|
|
65
|
+
async def _get_rel_filters_for_profiles(self, node: Node, rel_names: list[str]) -> list[RelationshipFilter]:
|
|
66
|
+
node_schema = node.get_schema()
|
|
67
|
+
|
|
68
|
+
identifiers: list[RelationshipFilter] = []
|
|
69
|
+
for rel_name in rel_names:
|
|
70
|
+
rel_schema = node_schema.get_relationship(name=rel_name)
|
|
71
|
+
identifiers.append(
|
|
72
|
+
RelationshipFilter(
|
|
73
|
+
relationship_identifier=f"profile_{rel_schema.get_identifier()}", direction=rel_schema.direction
|
|
74
|
+
)
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
return identifiers
|
|
78
|
+
|
|
45
79
|
async def _get_sorted_profile_data(
|
|
46
|
-
self,
|
|
80
|
+
self,
|
|
81
|
+
profile_ids: list[str],
|
|
82
|
+
attr_names_for_profiles: list[str],
|
|
83
|
+
relationship_filters: list[RelationshipFilter] | None = None,
|
|
47
84
|
) -> list[ProfileData]:
|
|
48
85
|
if not profile_ids:
|
|
49
86
|
return []
|
|
50
87
|
query = await GetProfileDataQuery.init(
|
|
51
|
-
db=self.db,
|
|
88
|
+
db=self.db,
|
|
89
|
+
branch=self.branch,
|
|
90
|
+
profile_ids=profile_ids,
|
|
91
|
+
attr_names=attr_names_for_profiles,
|
|
92
|
+
relationship_filters=relationship_filters,
|
|
52
93
|
)
|
|
53
94
|
await query.execute(db=self.db)
|
|
54
95
|
profile_data_list = query.get_profile_data()
|
|
@@ -76,35 +117,135 @@ class NodeProfilesApplier:
|
|
|
76
117
|
node_attr.is_default = True
|
|
77
118
|
node_attr.is_from_profile = False
|
|
78
119
|
|
|
120
|
+
async def _apply_profile_to_relationship(
|
|
121
|
+
self, node: Node, node_rel: RelationshipManager, peer_ids: list[str], profile_id: str
|
|
122
|
+
) -> bool:
|
|
123
|
+
"""Apply profile relationship peers to a node relationship.
|
|
124
|
+
|
|
125
|
+
Profile relationships are only applied if the node has no user-set peers for this relationship.
|
|
126
|
+
User-set peers (not from profile) take precedence over profile values.
|
|
127
|
+
"""
|
|
128
|
+
is_changed = False
|
|
129
|
+
|
|
130
|
+
current_rels = await node_rel.get_relationships(db=self.db)
|
|
131
|
+
profile_peer_ids = set(peer_ids)
|
|
132
|
+
|
|
133
|
+
user_set_peer_ids: set[str] = set()
|
|
134
|
+
profile_set_rels: list[Relationship] = []
|
|
135
|
+
for rel in current_rels:
|
|
136
|
+
if rel.peer_id:
|
|
137
|
+
if rel.is_from_profile:
|
|
138
|
+
profile_set_rels.append(rel)
|
|
139
|
+
else:
|
|
140
|
+
user_set_peer_ids.add(rel.peer_id)
|
|
141
|
+
|
|
142
|
+
# If user has set any peers, they override profile values entirely
|
|
143
|
+
if user_set_peer_ids:
|
|
144
|
+
for rel in profile_set_rels:
|
|
145
|
+
if rel.peer_id and rel.peer_id not in user_set_peer_ids:
|
|
146
|
+
await node_rel.remove_locally(peer_id=rel.peer_id, db=self.db)
|
|
147
|
+
is_changed = True
|
|
148
|
+
elif rel.peer_id and rel.peer_id in user_set_peer_ids:
|
|
149
|
+
rel.clear_source()
|
|
150
|
+
is_changed = True
|
|
151
|
+
|
|
152
|
+
if is_changed:
|
|
153
|
+
node_rel.is_from_profile = False
|
|
154
|
+
await node_rel.save(db=self.db)
|
|
155
|
+
return is_changed
|
|
156
|
+
|
|
157
|
+
profile_set_peer_ids = {rel.peer_id for rel in profile_set_rels if rel.peer_id}
|
|
158
|
+
relationships_to_replace: dict[str, list[Relationship]] = {"insert": [], "remove": []}
|
|
159
|
+
|
|
160
|
+
# Remove relationships that are from profile but not in the new profile value
|
|
161
|
+
for rel in profile_set_rels:
|
|
162
|
+
if rel.peer_id and rel.peer_id not in profile_peer_ids:
|
|
163
|
+
relationships_to_replace["remove"].append(rel)
|
|
164
|
+
|
|
165
|
+
# Add relationships that are in profile but not present
|
|
166
|
+
for peer_id in profile_peer_ids:
|
|
167
|
+
if peer_id not in profile_set_peer_ids:
|
|
168
|
+
new_rel = Relationship(
|
|
169
|
+
schema=node_rel.schema,
|
|
170
|
+
branch=self.branch,
|
|
171
|
+
source_kind=InfrahubKind.PROFILE,
|
|
172
|
+
node=node,
|
|
173
|
+
)
|
|
174
|
+
await new_rel.new(db=self.db, data=peer_id)
|
|
175
|
+
new_rel.set_source(value=profile_id)
|
|
176
|
+
relationships_to_replace["insert"].append(new_rel)
|
|
177
|
+
|
|
178
|
+
is_changed |= await node_rel.update_relationships(
|
|
179
|
+
db=self.db,
|
|
180
|
+
relationships_to_remove=relationships_to_replace["remove"],
|
|
181
|
+
relationships_to_insert=relationships_to_replace["insert"],
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if profile_peer_ids:
|
|
185
|
+
node_rel.is_from_profile = True
|
|
186
|
+
|
|
187
|
+
if is_changed:
|
|
188
|
+
await node_rel.save(db=self.db)
|
|
189
|
+
|
|
190
|
+
return is_changed
|
|
191
|
+
|
|
192
|
+
async def _remove_profile_from_relationship(self, relationship_manager: RelationshipManager) -> None:
|
|
193
|
+
relationship_manager.is_from_profile = False
|
|
194
|
+
await relationship_manager.delete(db=self.db)
|
|
195
|
+
|
|
79
196
|
async def apply_profiles(self, node: Node) -> list[str]:
|
|
80
197
|
profile_ids = await self._get_profile_ids(node=node)
|
|
81
198
|
attr_names_for_profiles = await self._get_attr_names_for_profiles(node=node)
|
|
82
|
-
|
|
83
|
-
|
|
199
|
+
rel_names_for_profiles = await self._get_rel_names_for_profiles(node=node)
|
|
200
|
+
rel_filters_for_profiles = await self._get_rel_filters_for_profiles(node=node, rel_names=rel_names_for_profiles)
|
|
201
|
+
if not attr_names_for_profiles and not rel_filters_for_profiles:
|
|
84
202
|
return []
|
|
85
203
|
|
|
86
|
-
# get profiles priorities
|
|
204
|
+
# get profiles priorities, attribute values, and relationship peers on branch
|
|
87
205
|
sorted_profile_data = await self._get_sorted_profile_data(
|
|
88
|
-
profile_ids=profile_ids,
|
|
206
|
+
profile_ids=profile_ids,
|
|
207
|
+
attr_names_for_profiles=attr_names_for_profiles,
|
|
208
|
+
relationship_filters=rel_filters_for_profiles,
|
|
89
209
|
)
|
|
90
210
|
|
|
91
|
-
updated_field_names = []
|
|
211
|
+
updated_field_names: list[str] = []
|
|
92
212
|
# set attribute values/is_default/is_from_profile on nodes
|
|
93
213
|
for attr_name in attr_names_for_profiles:
|
|
94
|
-
|
|
214
|
+
has_profile_attr_data = False
|
|
95
215
|
node_attr = node.get_attribute(attr_name)
|
|
96
216
|
for profile_data in sorted_profile_data:
|
|
97
217
|
profile_value = profile_data.attribute_values.get(attr_name)
|
|
98
218
|
if profile_value is not None:
|
|
99
|
-
|
|
100
|
-
is_changed = False
|
|
219
|
+
has_profile_attr_data = True
|
|
101
220
|
is_changed = self._apply_profile_to_attribute(
|
|
102
221
|
node_attr=node_attr, profile_value=profile_value, profile_id=profile_data.uuid
|
|
103
222
|
)
|
|
104
223
|
if is_changed:
|
|
105
224
|
updated_field_names.append(attr_name)
|
|
106
225
|
break
|
|
107
|
-
if not
|
|
226
|
+
if not has_profile_attr_data and node_attr.is_from_profile:
|
|
108
227
|
self._remove_profile_from_attribute(node_attr=node_attr)
|
|
109
228
|
updated_field_names.append(attr_name)
|
|
229
|
+
|
|
230
|
+
for rel_filter in rel_filters_for_profiles:
|
|
231
|
+
has_profile_rel_data = False
|
|
232
|
+
node_rel = node.get_relationship_by_identifier(rel_filter.relationship_identifier.removeprefix("profile_"))
|
|
233
|
+
|
|
234
|
+
for profile_data in sorted_profile_data:
|
|
235
|
+
profile_peers = profile_data.relationship_peers.get(rel_filter)
|
|
236
|
+
if profile_peers:
|
|
237
|
+
has_profile_rel_data = True
|
|
238
|
+
is_changed = await self._apply_profile_to_relationship(
|
|
239
|
+
node=node, node_rel=node_rel, peer_ids=profile_peers, profile_id=profile_data.uuid
|
|
240
|
+
)
|
|
241
|
+
if is_changed:
|
|
242
|
+
updated_field_names.append(node_rel.name)
|
|
243
|
+
break
|
|
244
|
+
|
|
245
|
+
# Refresh the relationship manager to update the is_from_profile property
|
|
246
|
+
await node_rel.fetch_relationship_ids(db=self.db)
|
|
247
|
+
if not has_profile_rel_data and node_rel.is_from_profile:
|
|
248
|
+
await self._remove_profile_from_relationship(relationship_manager=node_rel)
|
|
249
|
+
updated_field_names.append(node_rel.name)
|
|
250
|
+
|
|
110
251
|
return updated_field_names
|