Skip to content

Commit 1cb2a46

Browse files
committed
Support converting repository type
1 parent c306518 commit 1cb2a46

File tree

12 files changed

+773
-229
lines changed

12 files changed

+773
-229
lines changed

backend/infrahub/core/convert_object_type/conversion.py

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,21 @@
22

33
from pydantic import BaseModel
44

5+
from infrahub.auth import AccountSession
6+
from infrahub.context import InfrahubContext
57
from infrahub.core.attribute import BaseAttribute
68
from infrahub.core.branch import Branch
79
from infrahub.core.constants import RelationshipCardinality
10+
from infrahub.core.constants.infrahubkind import READONLYREPOSITORY, REPOSITORY
811
from infrahub.core.manager import NodeManager
912
from infrahub.core.node import Node
1013
from infrahub.core.node.create import create_node
1114
from infrahub.core.query.relationship import GetAllPeersIds
1215
from infrahub.core.relationship import RelationshipManager
16+
from infrahub.core.repositories.create_repository import post_create_repository
1317
from infrahub.core.schema import NodeSchema
1418
from infrahub.database import InfrahubDatabase
19+
from infrahub.services import InfrahubServices
1520

1621

1722
class InputDataForDestField(BaseModel): # Only one of these fields can be not None
@@ -88,7 +93,14 @@ async def get_unidirectional_rels_peers_ids(node: Node, branch: Branch, db: Infr
8893

8994

9095
async def convert_object_type(
91-
node: Node, target_schema: NodeSchema, mapping: dict[str, InputForDestField], branch: Branch, db: InfrahubDatabase
96+
node: Node,
97+
target_schema: NodeSchema,
98+
mapping: dict[str, InputForDestField],
99+
branch: Branch,
100+
db: InfrahubDatabase,
101+
account_session: AccountSession,
102+
services: InfrahubServices,
103+
context: InfrahubContext,
92104
) -> Node:
93105
"""Delete the node and return the new created one. If creation fails, the node is not deleted, and raise an error.
94106
An extra check is performed on input node peers relationships to make sure they are still valid."""
@@ -106,6 +118,7 @@ async def convert_object_type(
106118
raise ValueError(f"Deleted {len(deleted_nodes)} nodes instead of 1")
107119

108120
data_new_node = await build_data_new_node(dbt, mapping, node)
121+
109122
node_created = await create_node(
110123
data=data_new_node,
111124
db=dbt,
@@ -119,4 +132,17 @@ async def convert_object_type(
119132
for peer in peers.values():
120133
peer.validate_relationships()
121134

122-
return node_created
135+
# We can't apply post creation steps within above transaction as a sdk call tries to fetch the node
136+
# created within the transaction from a different process, therefore that would not run within this transaction
137+
# so the node ends up being not found
138+
if target_schema.kind in [REPOSITORY, READONLYREPOSITORY]:
139+
await post_create_repository(
140+
obj=node_created, # type: ignore
141+
db=db,
142+
branch=branch,
143+
account_session=account_session,
144+
services=services,
145+
context=context,
146+
)
147+
148+
return node_created

backend/infrahub/core/node/create.py

Lines changed: 47 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,17 @@
22

33
from typing import TYPE_CHECKING, Any, Mapping
44

5+
from infrahub import lock
56
from infrahub.core import registry
67
from infrahub.core.constants import RelationshipCardinality, RelationshipKind
78
from infrahub.core.constraint.node.runner import NodeConstraintRunner
89
from infrahub.core.manager import NodeManager
910
from infrahub.core.node import Node
11+
from infrahub.core.node.lock_utils import get_kind_lock_names_on_object_mutation
1012
from infrahub.core.protocols import CoreObjectTemplate
13+
from infrahub.core.schema import GenericSchema
1114
from infrahub.dependencies.registry import get_component_registry
15+
from infrahub.lock import InfrahubMultiLock
1216

1317
if TYPE_CHECKING:
1418
from infrahub.core.branch import Branch
@@ -170,10 +174,13 @@ async def create_node(
170174
data: dict,
171175
db: InfrahubDatabase,
172176
branch: Branch,
173-
schema: NonGenericSchemaTypes,
177+
schema: MainSchemaTypes,
174178
) -> Node:
175179
"""Create a node in the database if constraint checks succeed."""
176180

181+
if isinstance(schema, GenericSchema):
182+
raise ValueError(f"Node of generic schema `{schema.name=}` can not be instantiated.")
183+
177184
component_registry = get_component_registry()
178185
node_constraint_runner = await component_registry.get_component(
179186
NodeConstraintRunner, db=db.start_session() if not db.is_transaction else db, branch=branch
@@ -183,27 +190,54 @@ async def create_node(
183190
node_class = registry.node[schema.kind]
184191

185192
fields_to_validate = list(data)
193+
schema_branch = db.schema.get_schema_branch(name=branch.name)
194+
lock_names = get_kind_lock_names_on_object_mutation(kind=schema.kind, branch=branch, schema_branch=schema_branch)
195+
186196
if db.is_transaction:
187-
obj = await _do_create_node(
188-
node_class=node_class,
189-
node_constraint_runner=node_constraint_runner,
190-
db=db,
191-
schema=schema,
192-
branch=branch,
193-
fields_to_validate=fields_to_validate,
194-
data=data,
195-
)
196-
else:
197-
async with db.start_transaction() as dbt:
197+
if lock_names:
198+
async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
199+
obj = await _do_create_node(
200+
node_class=node_class,
201+
node_constraint_runner=node_constraint_runner,
202+
db=db,
203+
schema=schema,
204+
branch=branch,
205+
fields_to_validate=fields_to_validate,
206+
data=data,
207+
)
208+
else:
198209
obj = await _do_create_node(
199210
node_class=node_class,
200211
node_constraint_runner=node_constraint_runner,
201-
db=dbt,
212+
db=db,
202213
schema=schema,
203214
branch=branch,
204215
fields_to_validate=fields_to_validate,
205216
data=data,
206217
)
218+
else:
219+
async with db.start_transaction() as dbt:
220+
if lock_names:
221+
async with InfrahubMultiLock(lock_registry=lock.registry, locks=lock_names):
222+
obj = await _do_create_node(
223+
node_class=node_class,
224+
node_constraint_runner=node_constraint_runner,
225+
db=dbt,
226+
schema=schema,
227+
branch=branch,
228+
fields_to_validate=fields_to_validate,
229+
data=data,
230+
)
231+
else:
232+
obj = await _do_create_node(
233+
node_class=node_class,
234+
node_constraint_runner=node_constraint_runner,
235+
db=dbt,
236+
schema=schema,
237+
branch=branch,
238+
fields_to_validate=fields_to_validate,
239+
data=data,
240+
)
207241

208242
if await get_profile_ids(db=db, obj=obj):
209243
obj = await refresh_for_profile_update(db=db, branch=branch, schema=schema, obj=obj)
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING
4+
5+
from infrahub.core.constants.infrahubkind import GENERICGROUP
6+
from infrahub.core.schema import GenericSchema
7+
from infrahub.lock import build_object_lock_name
8+
9+
if TYPE_CHECKING:
10+
from infrahub.core.branch import Branch
11+
from infrahub.core.schema.schema_branch import SchemaBranch
12+
13+
14+
KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED = [GENERICGROUP]
15+
16+
17+
def get_kind_lock_names_on_object_mutation(kind: str, branch: Branch, schema_branch: SchemaBranch) -> list[str]:
18+
"""
19+
Return objects kind for which we want to avoid concurrent mutation (create/update). Except for some specific kinds,
20+
concurrent mutations are only allowed on non-main branch as objects validations will be performed at least when merging in main branch.
21+
"""
22+
23+
if not branch.is_default and not _should_kind_be_locked_on_any_branch(kind, schema_branch):
24+
return []
25+
26+
lock_kinds = _get_kinds_to_lock_on_object_mutation(kind, schema_branch)
27+
lock_names = [build_object_lock_name(kind) for kind in lock_kinds]
28+
return lock_names
29+
30+
31+
def _should_kind_be_locked_on_any_branch(kind: str, schema_branch: SchemaBranch) -> bool:
32+
"""
33+
Check whether kind or any kind generic is in KINDS_TO_LOCK_ON_ANY_BRANCH.
34+
"""
35+
36+
if kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
37+
return True
38+
39+
node_schema = schema_branch.get(name=kind)
40+
if isinstance(node_schema, GenericSchema):
41+
return False
42+
43+
for generic_kind in node_schema.inherit_from:
44+
if generic_kind in KINDS_CONCURRENT_MUTATIONS_NOT_ALLOWED:
45+
return True
46+
return False
47+
48+
49+
def _get_kinds_to_lock_on_object_mutation(kind: str, schema_branch: SchemaBranch) -> list[str]:
50+
"""
51+
Return kinds for which we want to lock during creating / updating an object of a given schema node.
52+
Lock should be performed on schema kind and its generics having a uniqueness_constraint defined.
53+
If a generic uniqueness constraint is the same as the node schema one,
54+
it means node schema overrided this constraint, in which case we only need to lock on the generic.
55+
"""
56+
57+
node_schema = schema_branch.get(name=kind)
58+
59+
schema_uc = None
60+
kinds = []
61+
if node_schema.uniqueness_constraints:
62+
kinds.append(node_schema.kind)
63+
schema_uc = node_schema.uniqueness_constraints
64+
65+
if isinstance(node_schema, GenericSchema):
66+
return kinds
67+
68+
generics_kinds = node_schema.inherit_from
69+
70+
node_schema_kind_removed = False
71+
for generic_kind in generics_kinds:
72+
generic_uc = schema_branch.get(name=generic_kind).uniqueness_constraints
73+
if generic_uc:
74+
kinds.append(generic_kind)
75+
if not node_schema_kind_removed and generic_uc == schema_uc:
76+
# Check whether we should remove original schema kind as it simply overrides uniqueness_constraint
77+
# of a generic
78+
kinds.pop(0)
79+
node_schema_kind_removed = True
80+
return kinds

backend/infrahub/core/repositories/__init__.py

Whitespace-only changes.
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
from __future__ import annotations
2+
3+
from typing import TYPE_CHECKING, cast
4+
5+
from infrahub.core.constants import RepositoryInternalStatus
6+
from infrahub.core.constants.infrahubkind import READONLYREPOSITORY
7+
from infrahub.core.protocols import CoreGenericRepository, CoreReadOnlyRepository, CoreRepository
8+
from infrahub.exceptions import ValidationError
9+
from infrahub.git.models import GitRepositoryAdd, GitRepositoryAddReadOnly
10+
from infrahub.log import get_logger
11+
from infrahub.message_bus import messages
12+
from infrahub.message_bus.messages.git_repository_connectivity import GitRepositoryConnectivityResponse
13+
from infrahub.workflows.catalogue import GIT_REPOSITORY_ADD, GIT_REPOSITORY_ADD_READ_ONLY
14+
15+
if TYPE_CHECKING:
16+
from infrahub.auth import AccountSession
17+
from infrahub.context import InfrahubContext
18+
from infrahub.core.branch import Branch
19+
from infrahub.database import InfrahubDatabase
20+
from infrahub.services import InfrahubServices
21+
22+
log = get_logger()
23+
24+
25+
async def post_create_repository(
26+
obj: CoreGenericRepository,
27+
branch: Branch,
28+
db: InfrahubDatabase,
29+
account_session: AccountSession,
30+
services: InfrahubServices,
31+
context: InfrahubContext,
32+
) -> None:
33+
# First check the connectivity to the remote repository
34+
# If the connectivity is not good, we remove the repository to allow the user to add a new one
35+
36+
message = messages.GitRepositoryConnectivity(
37+
repository_name=obj.name.value,
38+
repository_location=obj.location.value,
39+
)
40+
response = await services.message_bus.rpc(message=message, response_class=GitRepositoryConnectivityResponse)
41+
42+
if response.data.success is False:
43+
await obj.delete(db=db)
44+
raise ValidationError(response.data.message)
45+
# If we are in the default branch, we set the sync status to Active
46+
# If we are in another branch, we set the sync status to Staging
47+
if branch.is_default:
48+
obj.internal_status.value = RepositoryInternalStatus.ACTIVE.value
49+
else:
50+
obj.internal_status.value = RepositoryInternalStatus.STAGING.value
51+
await obj.save(db=db)
52+
53+
# Create the new repository in the filesystem.
54+
log.info("create_repository", name=obj.name.value)
55+
authenticated_user = None
56+
if account_session and account_session.authenticated:
57+
authenticated_user = account_session.account_id
58+
if obj.get_kind() == READONLYREPOSITORY:
59+
obj = cast(CoreReadOnlyRepository, obj)
60+
model = GitRepositoryAddReadOnly(
61+
repository_id=obj.id,
62+
repository_name=obj.name.value,
63+
location=obj.location.value,
64+
ref=obj.ref.value,
65+
infrahub_branch_name=branch.name,
66+
infrahub_branch_id=str(branch.get_uuid()),
67+
internal_status=obj.internal_status.value,
68+
created_by=authenticated_user,
69+
)
70+
await services.workflow.submit_workflow(
71+
workflow=GIT_REPOSITORY_ADD_READ_ONLY,
72+
context=context,
73+
parameters={"model": model},
74+
)
75+
76+
else:
77+
obj = cast(CoreRepository, obj)
78+
git_repo_add_model = GitRepositoryAdd(
79+
repository_id=obj.id,
80+
repository_name=obj.name.value,
81+
location=obj.location.value,
82+
default_branch_name=obj.default_branch.value,
83+
infrahub_branch_name=branch.name,
84+
infrahub_branch_id=str(branch.get_uuid()),
85+
internal_status=obj.internal_status.value,
86+
created_by=authenticated_user,
87+
)
88+
89+
await services.workflow.submit_workflow(
90+
workflow=GIT_REPOSITORY_ADD,
91+
context=context,
92+
parameters={"model": git_repo_add_model},
93+
)
94+
# TODO Validate that the creation of the repository went as expected

backend/infrahub/graphql/mutations/convert_object_type.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,9 @@ async def mutate(
5454
mapping=fields_mapping,
5555
branch=graphql_context.branch,
5656
db=graphql_context.db,
57+
account_session=graphql_context.active_account_session,
58+
services=graphql_context.active_service,
59+
context=graphql_context.get_context(),
5760
)
5861

5962
dict_node = await new_node.to_graphql(db=graphql_context.db, fields={})

0 commit comments

Comments
 (0)