infrahub-server 1.3.5__py3-none-any.whl → 1.4.0b0__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/api/internal.py +5 -0
- infrahub/artifacts/tasks.py +17 -22
- infrahub/branch/merge_mutation_checker.py +38 -0
- infrahub/cli/__init__.py +2 -2
- infrahub/cli/context.py +7 -3
- infrahub/cli/db.py +5 -16
- infrahub/cli/upgrade.py +7 -29
- infrahub/computed_attribute/tasks.py +36 -46
- infrahub/config.py +53 -2
- infrahub/constants/environment.py +1 -0
- infrahub/core/attribute.py +9 -7
- infrahub/core/branch/tasks.py +43 -41
- infrahub/core/constants/__init__.py +20 -6
- infrahub/core/constants/infrahubkind.py +2 -0
- infrahub/core/diff/coordinator.py +3 -1
- infrahub/core/diff/repository/repository.py +0 -8
- infrahub/core/diff/tasks.py +11 -8
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/graph/index.py +1 -2
- infrahub/core/graph/schema.py +50 -29
- infrahub/core/initialization.py +62 -33
- infrahub/core/ipam/tasks.py +4 -3
- infrahub/core/merge.py +8 -10
- infrahub/core/migrations/graph/__init__.py +2 -0
- infrahub/core/migrations/graph/m035_drop_attr_value_index.py +45 -0
- infrahub/core/migrations/query/attribute_add.py +27 -2
- infrahub/core/migrations/schema/tasks.py +6 -5
- infrahub/core/node/proposed_change.py +43 -0
- infrahub/core/protocols.py +12 -0
- infrahub/core/query/attribute.py +32 -14
- infrahub/core/query/diff.py +11 -0
- infrahub/core/query/ipam.py +13 -7
- infrahub/core/query/node.py +51 -10
- infrahub/core/query/resource_manager.py +3 -3
- infrahub/core/schema/basenode_schema.py +8 -0
- infrahub/core/schema/definitions/core/__init__.py +10 -1
- infrahub/core/schema/definitions/core/ipam.py +28 -2
- infrahub/core/schema/definitions/core/propose_change.py +15 -0
- infrahub/core/schema/definitions/core/webhook.py +3 -0
- infrahub/core/schema/generic_schema.py +10 -0
- infrahub/core/schema/manager.py +10 -1
- infrahub/core/schema/node_schema.py +22 -17
- infrahub/core/schema/profile_schema.py +8 -0
- infrahub/core/schema/schema_branch.py +9 -5
- infrahub/core/schema/template_schema.py +8 -0
- infrahub/core/validators/checks_runner.py +5 -5
- infrahub/core/validators/tasks.py +6 -7
- infrahub/core/validators/uniqueness/checker.py +4 -2
- infrahub/core/validators/uniqueness/model.py +1 -0
- infrahub/core/validators/uniqueness/query.py +57 -7
- infrahub/database/__init__.py +2 -1
- infrahub/events/__init__.py +18 -0
- infrahub/events/constants.py +7 -0
- infrahub/events/generator.py +29 -2
- infrahub/events/proposed_change_action.py +181 -0
- infrahub/generators/tasks.py +24 -20
- infrahub/git/base.py +4 -7
- infrahub/git/integrator.py +21 -12
- infrahub/git/repository.py +15 -30
- infrahub/git/tasks.py +121 -106
- infrahub/graphql/field_extractor.py +69 -0
- infrahub/graphql/manager.py +15 -11
- infrahub/graphql/mutations/account.py +2 -2
- infrahub/graphql/mutations/action.py +8 -2
- infrahub/graphql/mutations/artifact_definition.py +4 -1
- infrahub/graphql/mutations/branch.py +10 -5
- infrahub/graphql/mutations/graphql_query.py +2 -1
- infrahub/graphql/mutations/main.py +14 -8
- infrahub/graphql/mutations/menu.py +2 -1
- infrahub/graphql/mutations/proposed_change.py +225 -8
- infrahub/graphql/mutations/relationship.py +5 -0
- infrahub/graphql/mutations/repository.py +2 -1
- infrahub/graphql/mutations/tasks.py +7 -9
- infrahub/graphql/mutations/webhook.py +4 -1
- infrahub/graphql/parser.py +15 -6
- infrahub/graphql/queries/__init__.py +10 -1
- infrahub/graphql/queries/account.py +3 -3
- infrahub/graphql/queries/branch.py +2 -2
- infrahub/graphql/queries/diff/tree.py +3 -3
- infrahub/graphql/queries/event.py +13 -3
- infrahub/graphql/queries/ipam.py +23 -1
- infrahub/graphql/queries/proposed_change.py +84 -0
- infrahub/graphql/queries/relationship.py +2 -2
- infrahub/graphql/queries/resource_manager.py +3 -3
- infrahub/graphql/queries/search.py +3 -2
- infrahub/graphql/queries/status.py +3 -2
- infrahub/graphql/queries/task.py +2 -2
- infrahub/graphql/resolvers/ipam.py +440 -0
- infrahub/graphql/resolvers/many_relationship.py +4 -3
- infrahub/graphql/resolvers/resolver.py +5 -5
- infrahub/graphql/resolvers/single_relationship.py +3 -2
- infrahub/graphql/schema.py +25 -5
- infrahub/graphql/types/__init__.py +2 -2
- infrahub/graphql/types/attribute.py +3 -3
- infrahub/graphql/types/event.py +60 -0
- infrahub/groups/tasks.py +6 -6
- infrahub/lock.py +3 -2
- infrahub/menu/generator.py +8 -0
- infrahub/message_bus/operations/__init__.py +9 -12
- infrahub/message_bus/operations/git/file.py +6 -5
- infrahub/message_bus/operations/git/repository.py +12 -20
- infrahub/message_bus/operations/refresh/registry.py +15 -9
- infrahub/message_bus/operations/send/echo.py +7 -4
- infrahub/message_bus/types.py +1 -0
- infrahub/permissions/globals.py +1 -4
- infrahub/permissions/manager.py +8 -5
- infrahub/pools/prefix.py +7 -5
- infrahub/prefect_server/app.py +31 -0
- infrahub/prefect_server/bootstrap.py +18 -0
- infrahub/proposed_change/action_checker.py +206 -0
- infrahub/proposed_change/approval_revoker.py +40 -0
- infrahub/proposed_change/branch_diff.py +3 -1
- infrahub/proposed_change/checker.py +45 -0
- infrahub/proposed_change/constants.py +32 -2
- infrahub/proposed_change/tasks.py +182 -150
- infrahub/py.typed +0 -0
- infrahub/server.py +29 -17
- infrahub/services/__init__.py +13 -28
- infrahub/services/adapters/cache/__init__.py +4 -0
- infrahub/services/adapters/cache/nats.py +2 -0
- infrahub/services/adapters/cache/redis.py +3 -0
- infrahub/services/adapters/message_bus/__init__.py +0 -2
- infrahub/services/adapters/message_bus/local.py +1 -2
- infrahub/services/adapters/message_bus/nats.py +6 -8
- infrahub/services/adapters/message_bus/rabbitmq.py +7 -9
- infrahub/services/adapters/workflow/__init__.py +1 -0
- infrahub/services/adapters/workflow/local.py +1 -8
- infrahub/services/component.py +2 -1
- infrahub/task_manager/event.py +52 -0
- infrahub/task_manager/models.py +9 -0
- infrahub/tasks/artifact.py +6 -7
- infrahub/tasks/check.py +4 -7
- infrahub/telemetry/tasks.py +15 -18
- infrahub/transformations/tasks.py +10 -6
- infrahub/trigger/tasks.py +4 -3
- infrahub/types.py +4 -0
- infrahub/validators/events.py +7 -7
- infrahub/validators/tasks.py +6 -7
- infrahub/webhook/models.py +45 -45
- infrahub/webhook/tasks.py +25 -24
- infrahub/workers/dependencies.py +143 -0
- infrahub/workers/infrahub_async.py +19 -43
- infrahub/workflows/catalogue.py +16 -2
- infrahub/workflows/initialization.py +5 -4
- infrahub/workflows/models.py +2 -0
- infrahub_sdk/client.py +6 -6
- infrahub_sdk/ctl/repository.py +51 -0
- infrahub_sdk/ctl/schema.py +9 -9
- infrahub_sdk/protocols.py +40 -6
- {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/METADATA +5 -4
- {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/RECORD +158 -144
- infrahub_testcontainers/container.py +17 -0
- infrahub_testcontainers/docker-compose-cluster.test.yml +56 -1
- infrahub_testcontainers/docker-compose.test.yml +56 -1
- infrahub_testcontainers/helpers.py +4 -1
- {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.3.5.dist-info → infrahub_server-1.4.0b0.dist-info}/entry_points.txt +0 -0
|
@@ -1,8 +1,11 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
1
3
|
from typing import TYPE_CHECKING, Any, Self
|
|
2
4
|
|
|
3
|
-
from graphene import Boolean, Field, InputObjectType, Mutation, String
|
|
5
|
+
from graphene import Boolean, Enum, Field, InputObjectType, List, Mutation, String
|
|
4
6
|
from graphql import GraphQLResolveInfo
|
|
5
7
|
|
|
8
|
+
from infrahub import lock
|
|
6
9
|
from infrahub.core.account import GlobalPermission
|
|
7
10
|
from infrahub.core.branch import Branch
|
|
8
11
|
from infrahub.core.constants import (
|
|
@@ -12,26 +15,45 @@ from infrahub.core.constants import (
|
|
|
12
15
|
PermissionDecision,
|
|
13
16
|
)
|
|
14
17
|
from infrahub.core.manager import NodeManager
|
|
15
|
-
from infrahub.core.
|
|
18
|
+
from infrahub.core.protocols import CoreProposedChange
|
|
16
19
|
from infrahub.core.schema import NodeSchema
|
|
17
20
|
from infrahub.database import InfrahubDatabase, retry_db_transaction
|
|
21
|
+
from infrahub.events import (
|
|
22
|
+
EventMeta,
|
|
23
|
+
ProposedChangeApprovalRevokedEvent,
|
|
24
|
+
ProposedChangeApprovedEvent,
|
|
25
|
+
ProposedChangeRejectedEvent,
|
|
26
|
+
ProposedChangeRejectionRevokedEvent,
|
|
27
|
+
)
|
|
18
28
|
from infrahub.exceptions import BranchNotFoundError, PermissionDeniedError, ValidationError
|
|
19
29
|
from infrahub.graphql.mutations.main import InfrahubMutationMixin
|
|
20
30
|
from infrahub.graphql.types.enums import CheckType as GraphQLCheckType
|
|
21
|
-
from infrahub.
|
|
31
|
+
from infrahub.graphql.types.task import TaskInfo
|
|
32
|
+
from infrahub.lock import InfrahubLock, build_object_lock_name
|
|
33
|
+
from infrahub.proposed_change.approval_revoker import do_revoke_approvals_on_updated_pcs
|
|
34
|
+
from infrahub.proposed_change.constants import ProposedChangeApprovalDecision, ProposedChangeState
|
|
35
|
+
from infrahub.proposed_change.models import RequestProposedChangePipeline
|
|
36
|
+
from infrahub.workers.dependencies import get_event_service
|
|
22
37
|
from infrahub.workflows.catalogue import PROPOSED_CHANGE_MERGE, REQUEST_PROPOSED_CHANGE_PIPELINE
|
|
23
38
|
|
|
24
|
-
from ...proposed_change.models import RequestProposedChangePipeline
|
|
25
|
-
from ..types.task import TaskInfo
|
|
26
39
|
from .main import InfrahubMutationOptions
|
|
27
40
|
|
|
28
41
|
if TYPE_CHECKING:
|
|
42
|
+
from graphql import GraphQLResolveInfo
|
|
43
|
+
|
|
44
|
+
from infrahub.core.node import Node
|
|
45
|
+
from infrahub.events.models import InfrahubEvent
|
|
46
|
+
|
|
29
47
|
from ..initialization import GraphqlContext
|
|
30
48
|
|
|
49
|
+
ProposedChangeApprovalDecisionInput = Enum.from_enum(ProposedChangeApprovalDecision)
|
|
50
|
+
|
|
31
51
|
|
|
32
52
|
class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
|
|
33
53
|
@classmethod
|
|
34
|
-
def __init_subclass_with_meta__(
|
|
54
|
+
def __init_subclass_with_meta__(
|
|
55
|
+
cls, schema: NodeSchema, _meta: InfrahubMutationOptions | None = None, **options: dict[str, Any]
|
|
56
|
+
) -> None:
|
|
35
57
|
# Make sure schema is a valid NodeSchema Node Class
|
|
36
58
|
if not isinstance(schema, NodeSchema):
|
|
37
59
|
raise ValueError(f"You need to pass a valid NodeSchema in '{cls.__name__}.Meta', received '{schema}'")
|
|
@@ -50,11 +72,19 @@ class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
|
|
|
50
72
|
data: InputObjectType,
|
|
51
73
|
branch: Branch,
|
|
52
74
|
database: InfrahubDatabase | None = None, # noqa: ARG003
|
|
75
|
+
override_data: dict[str, Any] | None = None,
|
|
53
76
|
) -> tuple[Node, Self]:
|
|
54
77
|
graphql_context: GraphqlContext = info.context
|
|
55
78
|
|
|
79
|
+
override_data = {"created_by": {"id": graphql_context.active_account_session.account_id}}
|
|
80
|
+
state = data.get("state", {}).get("value")
|
|
81
|
+
if state and state != ProposedChangeState.OPEN.value:
|
|
82
|
+
raise ValidationError(input_value="A proposed change has to be in the open state during creation")
|
|
83
|
+
|
|
56
84
|
async with graphql_context.db.start_transaction() as dbt:
|
|
57
|
-
proposed_change, result = await super().mutate_create(
|
|
85
|
+
proposed_change, result = await super().mutate_create(
|
|
86
|
+
info=info, data=data, branch=branch, database=dbt, override_data=override_data
|
|
87
|
+
)
|
|
58
88
|
destination_branch = proposed_change.destination_branch.value
|
|
59
89
|
source_branch = await _get_source_branch(db=dbt, name=proposed_change.source_branch.value)
|
|
60
90
|
if destination_branch == source_branch.name:
|
|
@@ -100,7 +130,7 @@ class InfrahubProposedChangeMutation(InfrahubMutationMixin, Mutation):
|
|
|
100
130
|
include_source=True,
|
|
101
131
|
)
|
|
102
132
|
state = ProposedChangeState(obj.state.value.value)
|
|
103
|
-
state.
|
|
133
|
+
state.validate_updatable()
|
|
104
134
|
|
|
105
135
|
updated_state = None
|
|
106
136
|
if state_update := data.get("state", {}).get("value"):
|
|
@@ -192,6 +222,165 @@ class ProposedChangeRequestRunCheck(Mutation):
|
|
|
192
222
|
return {"ok": True}
|
|
193
223
|
|
|
194
224
|
|
|
225
|
+
class ProposedChangeReviewInput(InputObjectType):
|
|
226
|
+
id = String(required=True, description="The ID of the proposed change to review.")
|
|
227
|
+
decision = ProposedChangeApprovalDecisionInput(
|
|
228
|
+
required=True, description="The decision for the proposed change review."
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class ProposedChangeReview(Mutation):
|
|
233
|
+
class Arguments:
|
|
234
|
+
data = ProposedChangeReviewInput(required=True)
|
|
235
|
+
|
|
236
|
+
ok = Boolean()
|
|
237
|
+
|
|
238
|
+
@classmethod
|
|
239
|
+
async def mutate(
|
|
240
|
+
cls,
|
|
241
|
+
root: dict, # noqa: ARG003
|
|
242
|
+
info: GraphQLResolveInfo,
|
|
243
|
+
data: ProposedChangeReviewInput,
|
|
244
|
+
) -> dict[str, bool]:
|
|
245
|
+
"""
|
|
246
|
+
This mutation is used to approve or reject a proposed change.
|
|
247
|
+
It can also be used to undo an approval or rejection.
|
|
248
|
+
"""
|
|
249
|
+
|
|
250
|
+
graphql_context: GraphqlContext = info.context
|
|
251
|
+
graphql_context.active_permissions.raise_for_permission(
|
|
252
|
+
permission=GlobalPermission(
|
|
253
|
+
action=GlobalPermissions.REVIEW_PROPOSED_CHANGE.value, decision=PermissionDecision.ALLOW_ALL.value
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
pc_id = str(data.id)
|
|
257
|
+
lock_name = build_object_lock_name(pc_id)
|
|
258
|
+
async with InfrahubLock(name=lock_name, connection=lock.registry.connection):
|
|
259
|
+
proposed_change = await NodeManager.get_one_by_id_or_default_filter(
|
|
260
|
+
id=pc_id, kind=CoreProposedChange, db=graphql_context.db, prefetch_relationships=True
|
|
261
|
+
)
|
|
262
|
+
state = ProposedChangeState(proposed_change.state.value.value)
|
|
263
|
+
state.validate_reviewable()
|
|
264
|
+
|
|
265
|
+
created_by = await proposed_change.created_by.get_peer(db=graphql_context.db)
|
|
266
|
+
if created_by and created_by.id == graphql_context.active_account_session.account_id:
|
|
267
|
+
raise ValidationError(input_value="You cannot review your own proposed changes")
|
|
268
|
+
|
|
269
|
+
current_user = await NodeManager.get_one_by_id_or_default_filter(
|
|
270
|
+
id=graphql_context.active_account_session.account_id,
|
|
271
|
+
kind=InfrahubKind.GENERICACCOUNT,
|
|
272
|
+
db=graphql_context.db,
|
|
273
|
+
)
|
|
274
|
+
|
|
275
|
+
async with graphql_context.db.start_session() as db:
|
|
276
|
+
event = await cls._handle_decision(
|
|
277
|
+
db=db,
|
|
278
|
+
decision=data.decision,
|
|
279
|
+
proposed_change=proposed_change,
|
|
280
|
+
current_user=current_user,
|
|
281
|
+
context=graphql_context,
|
|
282
|
+
)
|
|
283
|
+
await proposed_change.save(db=db)
|
|
284
|
+
|
|
285
|
+
if event:
|
|
286
|
+
event_service = await get_event_service()
|
|
287
|
+
await event_service.send(event=event)
|
|
288
|
+
|
|
289
|
+
return {"ok": True}
|
|
290
|
+
|
|
291
|
+
@classmethod
|
|
292
|
+
async def _handle_decision(
|
|
293
|
+
cls,
|
|
294
|
+
db: InfrahubDatabase,
|
|
295
|
+
decision: ProposedChangeApprovalDecision,
|
|
296
|
+
proposed_change: CoreProposedChange,
|
|
297
|
+
current_user: Node,
|
|
298
|
+
context: GraphqlContext,
|
|
299
|
+
) -> InfrahubEvent | None:
|
|
300
|
+
"""Modify approved_by and rejected_by relationships of the prpoposed change based on the decision."""
|
|
301
|
+
|
|
302
|
+
approved_by = await proposed_change.approved_by.get_peers(db=db)
|
|
303
|
+
rejected_by = await proposed_change.rejected_by.get_peers(db=db)
|
|
304
|
+
approved_by_ids = [node.id for _, node in approved_by.items()]
|
|
305
|
+
rejected_by_ids = [node.id for _, node in rejected_by.items()]
|
|
306
|
+
event: InfrahubEvent | None = None
|
|
307
|
+
event_meta = EventMeta.from_context(context=context.get_context())
|
|
308
|
+
|
|
309
|
+
match decision:
|
|
310
|
+
case ProposedChangeApprovalDecision.APPROVE:
|
|
311
|
+
if current_user.id in approved_by_ids:
|
|
312
|
+
raise ValidationError(input_value="You have already approved this proposed change")
|
|
313
|
+
await proposed_change.approved_by.add(db=db, data=current_user)
|
|
314
|
+
if current_user.id in rejected_by_ids:
|
|
315
|
+
await proposed_change.rejected_by.remove_locally(db=db, peer_id=current_user.id)
|
|
316
|
+
|
|
317
|
+
event = ProposedChangeApprovedEvent(
|
|
318
|
+
proposed_change_id=proposed_change.id,
|
|
319
|
+
proposed_change_name=proposed_change.name.value,
|
|
320
|
+
proposed_change_state=proposed_change.state.value,
|
|
321
|
+
reviewer_account_id=current_user.id,
|
|
322
|
+
reviewer_account_name=current_user.name.value,
|
|
323
|
+
reviewer_decision=decision.value,
|
|
324
|
+
meta=event_meta,
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
case ProposedChangeApprovalDecision.CANCEL_APPROVE:
|
|
328
|
+
if current_user.id not in approved_by_ids:
|
|
329
|
+
raise ValidationError(
|
|
330
|
+
input_value="You did not approve this proposed change yet, it can't be un-approved"
|
|
331
|
+
)
|
|
332
|
+
await proposed_change.approved_by.remove_locally(db=db, peer_id=current_user.id)
|
|
333
|
+
|
|
334
|
+
event = ProposedChangeApprovalRevokedEvent(
|
|
335
|
+
proposed_change_id=proposed_change.id,
|
|
336
|
+
proposed_change_name=proposed_change.name.value,
|
|
337
|
+
proposed_change_state=proposed_change.state.value,
|
|
338
|
+
reviewer_account_id=current_user.id,
|
|
339
|
+
reviewer_account_name=current_user.name.value,
|
|
340
|
+
reviewer_former_decision=ProposedChangeApprovalDecision.APPROVE.value,
|
|
341
|
+
meta=event_meta,
|
|
342
|
+
)
|
|
343
|
+
|
|
344
|
+
case ProposedChangeApprovalDecision.REJECT:
|
|
345
|
+
if current_user.id in rejected_by_ids:
|
|
346
|
+
raise ValidationError(input_value="You have already rejected this proposed change")
|
|
347
|
+
await proposed_change.rejected_by.add(db=db, data=current_user)
|
|
348
|
+
if current_user.id in approved_by_ids:
|
|
349
|
+
await proposed_change.approved_by.remove_locally(db=db, peer_id=current_user.id)
|
|
350
|
+
|
|
351
|
+
event = ProposedChangeRejectedEvent(
|
|
352
|
+
proposed_change_id=proposed_change.id,
|
|
353
|
+
proposed_change_name=proposed_change.name.value,
|
|
354
|
+
proposed_change_state=proposed_change.state.value,
|
|
355
|
+
reviewer_account_id=current_user.id,
|
|
356
|
+
reviewer_account_name=current_user.name.value,
|
|
357
|
+
reviewer_decision=decision.value,
|
|
358
|
+
meta=event_meta,
|
|
359
|
+
)
|
|
360
|
+
|
|
361
|
+
case ProposedChangeApprovalDecision.CANCEL_REJECT:
|
|
362
|
+
if current_user.id not in rejected_by_ids:
|
|
363
|
+
raise ValidationError(
|
|
364
|
+
input_value="You did not reject this proposed change yet, it can't be un-rejected"
|
|
365
|
+
)
|
|
366
|
+
await proposed_change.rejected_by.remove_locally(db=db, peer_id=current_user.id)
|
|
367
|
+
|
|
368
|
+
event = ProposedChangeRejectionRevokedEvent(
|
|
369
|
+
proposed_change_id=proposed_change.id,
|
|
370
|
+
proposed_change_name=proposed_change.name.value,
|
|
371
|
+
proposed_change_state=proposed_change.state.value,
|
|
372
|
+
reviewer_account_id=current_user.id,
|
|
373
|
+
reviewer_account_name=current_user.name.value,
|
|
374
|
+
reviewer_former_decision=ProposedChangeApprovalDecision.REJECT.value,
|
|
375
|
+
meta=event_meta,
|
|
376
|
+
)
|
|
377
|
+
|
|
378
|
+
case _:
|
|
379
|
+
raise ValidationError(input_value=f"Invalid decision {decision}")
|
|
380
|
+
|
|
381
|
+
return event
|
|
382
|
+
|
|
383
|
+
|
|
195
384
|
class ProposedChangeMergeInput(InputObjectType):
|
|
196
385
|
id = String(required=True)
|
|
197
386
|
|
|
@@ -250,6 +439,34 @@ class ProposedChangeMerge(Mutation):
|
|
|
250
439
|
return cls(ok=True, task=task)
|
|
251
440
|
|
|
252
441
|
|
|
442
|
+
class ProposedChangeCheckForApprovalRevokeInput(InputObjectType):
|
|
443
|
+
ids = Field(List(of_type=String, required=True), required=False)
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
class ProposedChangeCheckForApprovalRevoke(Mutation):
|
|
447
|
+
class Arguments:
|
|
448
|
+
data = ProposedChangeCheckForApprovalRevokeInput(required=True)
|
|
449
|
+
|
|
450
|
+
ok = Boolean()
|
|
451
|
+
|
|
452
|
+
@classmethod
|
|
453
|
+
async def mutate(
|
|
454
|
+
cls,
|
|
455
|
+
root: dict, # noqa: ARG003
|
|
456
|
+
info: GraphQLResolveInfo,
|
|
457
|
+
data: dict[str, Any],
|
|
458
|
+
) -> dict[str, bool]:
|
|
459
|
+
db = info.context.db
|
|
460
|
+
ids: list[str] | None
|
|
461
|
+
try:
|
|
462
|
+
ids = data["ids"]
|
|
463
|
+
except KeyError:
|
|
464
|
+
ids = None
|
|
465
|
+
|
|
466
|
+
await do_revoke_approvals_on_updated_pcs(db=db, proposed_changes_ids=ids)
|
|
467
|
+
return cls(ok=True)
|
|
468
|
+
|
|
469
|
+
|
|
253
470
|
async def _get_source_branch(db: InfrahubDatabase, name: str) -> Branch:
|
|
254
471
|
try:
|
|
255
472
|
return await Branch.get_by_name(name=name, db=db)
|
|
@@ -332,6 +332,11 @@ async def _validate_node(info: GraphQLResolveInfo, data: RelationshipNodesInput)
|
|
|
332
332
|
if rel_schema.cardinality != RelationshipCardinality.MANY:
|
|
333
333
|
raise ValidationError({"name": f"'{relationship_name}' must be a relationship of cardinality Many"})
|
|
334
334
|
|
|
335
|
+
if rel_schema.read_only:
|
|
336
|
+
# These mutations should never be allowed to update read-only relationships, as those typically
|
|
337
|
+
# have custom code tied to them such as the approved_by relationship of a CoreProposedChange.
|
|
338
|
+
raise ValidationError({source.get_kind(): f"'{relationship_name}' is a read-only relationship"})
|
|
339
|
+
|
|
335
340
|
return source
|
|
336
341
|
|
|
337
342
|
|
|
@@ -64,12 +64,13 @@ class InfrahubRepositoryMutation(InfrahubMutationMixin, Mutation):
|
|
|
64
64
|
data: InputObjectType,
|
|
65
65
|
branch: Branch,
|
|
66
66
|
database: InfrahubDatabase | None = None, # noqa: ARG003
|
|
67
|
+
override_data: dict[str, Any] | None = None,
|
|
67
68
|
) -> tuple[Node, Self]:
|
|
68
69
|
graphql_context: GraphqlContext = info.context
|
|
69
70
|
|
|
70
71
|
cleanup_payload(data)
|
|
71
72
|
# Create the object in the database
|
|
72
|
-
obj, result = await super().mutate_create(info, data, branch)
|
|
73
|
+
obj, result = await super().mutate_create(info, data, branch, override_data=override_data)
|
|
73
74
|
obj = cast(CoreGenericRepository, obj)
|
|
74
75
|
|
|
75
76
|
# First check the connectivity to the remote repository
|
|
@@ -15,16 +15,17 @@ from infrahub.core.validators.models.validate_migration import SchemaValidateMig
|
|
|
15
15
|
from infrahub.core.validators.tasks import schema_validate_migrations
|
|
16
16
|
from infrahub.dependencies.registry import get_component_registry
|
|
17
17
|
from infrahub.exceptions import ValidationError
|
|
18
|
-
from infrahub.
|
|
18
|
+
from infrahub.workers.dependencies import get_database, get_workflow
|
|
19
19
|
from infrahub.workflows.catalogue import BRANCH_MERGE
|
|
20
20
|
from infrahub.workflows.utils import add_tags
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
@flow(name="merge-branch-mutation", flow_run_name="Merge branch graphQL mutation")
|
|
24
|
-
async def merge_branch_mutation(branch: str, context: InfrahubContext
|
|
24
|
+
async def merge_branch_mutation(branch: str, context: InfrahubContext) -> None:
|
|
25
25
|
await add_tags(branches=[branch])
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
database = await get_database()
|
|
28
|
+
async with database.start_session() as db:
|
|
28
29
|
obj = await Branch.get_by_name(db=db, name=branch)
|
|
29
30
|
base_branch = await Branch.get_by_name(db=db, name=registry.default_branch)
|
|
30
31
|
|
|
@@ -52,7 +53,7 @@ async def merge_branch_mutation(branch: str, context: InfrahubContext, service:
|
|
|
52
53
|
diff_repository=diff_repository,
|
|
53
54
|
source_branch=obj,
|
|
54
55
|
diff_locker=DiffLocker(),
|
|
55
|
-
|
|
56
|
+
workflow=get_workflow(),
|
|
56
57
|
)
|
|
57
58
|
candidate_schema = merger.get_candidate_schema()
|
|
58
59
|
determiner = ConstraintValidatorDeterminer(schema_branch=candidate_schema)
|
|
@@ -62,13 +63,10 @@ async def merge_branch_mutation(branch: str, context: InfrahubContext, service:
|
|
|
62
63
|
|
|
63
64
|
if constraints:
|
|
64
65
|
responses = await schema_validate_migrations(
|
|
65
|
-
message=SchemaValidateMigrationData(
|
|
66
|
-
branch=obj, schema_branch=candidate_schema, constraints=constraints
|
|
67
|
-
),
|
|
68
|
-
service=service,
|
|
66
|
+
message=SchemaValidateMigrationData(branch=obj, schema_branch=candidate_schema, constraints=constraints)
|
|
69
67
|
)
|
|
70
68
|
error_messages = [violation.message for response in responses for violation in response.violations]
|
|
71
69
|
if error_messages:
|
|
72
70
|
raise ValidationError(",\n".join(error_messages))
|
|
73
71
|
|
|
74
|
-
await
|
|
72
|
+
await get_workflow().execute_workflow(workflow=BRANCH_MERGE, context=context, parameters={"branch": obj.name})
|
|
@@ -67,6 +67,7 @@ class InfrahubWebhookMutation(InfrahubMutationMixin, Mutation):
|
|
|
67
67
|
data: InputObjectType,
|
|
68
68
|
branch: Branch,
|
|
69
69
|
database: InfrahubDatabase | None = None,
|
|
70
|
+
override_data: dict[str, Any] | None = None,
|
|
70
71
|
) -> tuple[Node, Self]:
|
|
71
72
|
graphql_context: GraphqlContext = info.context
|
|
72
73
|
|
|
@@ -74,7 +75,9 @@ class InfrahubWebhookMutation(InfrahubMutationMixin, Mutation):
|
|
|
74
75
|
|
|
75
76
|
_validate_input(graphql_context=graphql_context, branch=branch, input_data=input_data)
|
|
76
77
|
|
|
77
|
-
obj, result = await super().mutate_create(
|
|
78
|
+
obj, result = await super().mutate_create(
|
|
79
|
+
info=info, data=data, branch=branch, database=database, override_data=override_data
|
|
80
|
+
)
|
|
78
81
|
|
|
79
82
|
return obj, result
|
|
80
83
|
|
infrahub/graphql/parser.py
CHANGED
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING
|
|
|
6
6
|
from graphql.language import (
|
|
7
7
|
DirectiveNode,
|
|
8
8
|
FieldNode,
|
|
9
|
+
FragmentSpreadNode,
|
|
9
10
|
InlineFragmentNode,
|
|
10
11
|
ListValueNode,
|
|
11
12
|
NameNode,
|
|
@@ -15,7 +16,9 @@ from graphql.language import (
|
|
|
15
16
|
from infrahub_sdk.utils import deep_merge_dict
|
|
16
17
|
|
|
17
18
|
if TYPE_CHECKING:
|
|
18
|
-
from
|
|
19
|
+
from graphql import GraphQLResolveInfo
|
|
20
|
+
|
|
21
|
+
from infrahub.core.schema import GenericSchema, NodeSchema
|
|
19
22
|
|
|
20
23
|
|
|
21
24
|
@dataclass
|
|
@@ -26,14 +29,14 @@ class FieldEnricher:
|
|
|
26
29
|
fields: dict = field(default_factory=dict)
|
|
27
30
|
|
|
28
31
|
|
|
29
|
-
async def extract_selection(
|
|
30
|
-
graphql_extractor = GraphQLExtractor(
|
|
32
|
+
async def extract_selection(info: GraphQLResolveInfo, schema: NodeSchema | GenericSchema) -> dict:
|
|
33
|
+
graphql_extractor = GraphQLExtractor(info=info, schema=schema)
|
|
31
34
|
return await graphql_extractor.get_fields()
|
|
32
35
|
|
|
33
36
|
|
|
34
37
|
class GraphQLExtractor:
|
|
35
|
-
def __init__(self,
|
|
36
|
-
self.
|
|
38
|
+
def __init__(self, info: GraphQLResolveInfo, schema: NodeSchema | GenericSchema) -> None:
|
|
39
|
+
self.info = info
|
|
37
40
|
self.schema = schema
|
|
38
41
|
self.typename_paths: dict[str, list[FieldEnricher]] = {}
|
|
39
42
|
self.node_path: dict[str, list[FieldEnricher]] = {}
|
|
@@ -43,7 +46,7 @@ class GraphQLExtractor:
|
|
|
43
46
|
self.node_path[path] = []
|
|
44
47
|
|
|
45
48
|
async def get_fields(self) -> dict:
|
|
46
|
-
return await self.extract_fields(selection_set=self.
|
|
49
|
+
return await self.extract_fields(selection_set=self.info.field_nodes[0].selection_set) or {}
|
|
47
50
|
|
|
48
51
|
def _process_expand_directive(self, path: str, directive: DirectiveNode) -> None:
|
|
49
52
|
excluded_fields = []
|
|
@@ -204,6 +207,12 @@ class GraphQLExtractor:
|
|
|
204
207
|
elif isinstance(fields[sub_node.name.value], dict) and isinstance(value, dict):
|
|
205
208
|
fields[sub_node.name.value].update(value) # type: ignore[union-attr]
|
|
206
209
|
|
|
210
|
+
elif isinstance(node, FragmentSpreadNode):
|
|
211
|
+
if node.name.value in self.info.fragments:
|
|
212
|
+
fragment_fields = await self.extract_fields(self.info.fragments[node.name.value].selection_set)
|
|
213
|
+
if fragment_fields:
|
|
214
|
+
fields.update(fragment_fields)
|
|
215
|
+
|
|
207
216
|
return self.apply_directives(selection_set=selection_set, fields=fields, path=path)
|
|
208
217
|
|
|
209
218
|
|
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
from .account import AccountPermissions, AccountToken
|
|
2
2
|
from .branch import BranchQueryList
|
|
3
3
|
from .internal import InfrahubInfo
|
|
4
|
-
from .ipam import
|
|
4
|
+
from .ipam import (
|
|
5
|
+
DeprecatedIPAddressGetNextAvailable,
|
|
6
|
+
DeprecatedIPPrefixGetNextAvailable,
|
|
7
|
+
InfrahubIPAddressGetNextAvailable,
|
|
8
|
+
InfrahubIPPrefixGetNextAvailable,
|
|
9
|
+
)
|
|
10
|
+
from .proposed_change import ProposedChangeAvailableActions
|
|
5
11
|
from .relationship import Relationship
|
|
6
12
|
from .resource_manager import InfrahubResourcePoolAllocated, InfrahubResourcePoolUtilization
|
|
7
13
|
from .search import InfrahubSearchAnywhere
|
|
@@ -12,6 +18,8 @@ __all__ = [
|
|
|
12
18
|
"AccountPermissions",
|
|
13
19
|
"AccountToken",
|
|
14
20
|
"BranchQueryList",
|
|
21
|
+
"DeprecatedIPAddressGetNextAvailable",
|
|
22
|
+
"DeprecatedIPPrefixGetNextAvailable",
|
|
15
23
|
"InfrahubIPAddressGetNextAvailable",
|
|
16
24
|
"InfrahubIPPrefixGetNextAvailable",
|
|
17
25
|
"InfrahubInfo",
|
|
@@ -19,6 +27,7 @@ __all__ = [
|
|
|
19
27
|
"InfrahubResourcePoolUtilization",
|
|
20
28
|
"InfrahubSearchAnywhere",
|
|
21
29
|
"InfrahubStatus",
|
|
30
|
+
"ProposedChangeAvailableActions",
|
|
22
31
|
"Relationship",
|
|
23
32
|
"Task",
|
|
24
33
|
]
|
|
@@ -3,11 +3,11 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from graphene import Field, Int, List, NonNull, ObjectType, String
|
|
6
|
-
from infrahub_sdk.utils import extract_fields_first_node
|
|
7
6
|
|
|
8
7
|
from infrahub.core.manager import NodeManager
|
|
9
8
|
from infrahub.core.protocols import InternalAccountToken
|
|
10
9
|
from infrahub.exceptions import PermissionDeniedError
|
|
10
|
+
from infrahub.graphql.field_extractor import extract_graphql_fields
|
|
11
11
|
|
|
12
12
|
if TYPE_CHECKING:
|
|
13
13
|
from graphql import GraphQLResolveInfo
|
|
@@ -44,7 +44,7 @@ async def resolve_account_tokens(
|
|
|
44
44
|
if not graphql_context.account_session.authenticated_by_jwt:
|
|
45
45
|
raise PermissionDeniedError("This operation requires authentication with a JWT token")
|
|
46
46
|
|
|
47
|
-
fields =
|
|
47
|
+
fields = extract_graphql_fields(info)
|
|
48
48
|
|
|
49
49
|
filters = {"account__ids": [graphql_context.account_session.account_id]}
|
|
50
50
|
response: dict[str, Any] = {}
|
|
@@ -121,7 +121,7 @@ async def resolve_account_permissions(
|
|
|
121
121
|
if not graphql_context.account_session:
|
|
122
122
|
raise ValueError("An account_session is mandatory to execute this query")
|
|
123
123
|
|
|
124
|
-
fields =
|
|
124
|
+
fields = extract_graphql_fields(info)
|
|
125
125
|
|
|
126
126
|
response: dict[str, dict[str, Any]] = {}
|
|
127
127
|
if "global_permissions" in fields:
|
|
@@ -3,8 +3,8 @@ from __future__ import annotations
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
5
|
from graphene import ID, Field, List, NonNull, String
|
|
6
|
-
from infrahub_sdk.utils import extract_fields_first_node
|
|
7
6
|
|
|
7
|
+
from infrahub.graphql.field_extractor import extract_graphql_fields
|
|
8
8
|
from infrahub.graphql.types import BranchType
|
|
9
9
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
@@ -16,7 +16,7 @@ async def branch_resolver(
|
|
|
16
16
|
info: GraphQLResolveInfo,
|
|
17
17
|
**kwargs: Any,
|
|
18
18
|
) -> list[dict[str, Any]]:
|
|
19
|
-
fields =
|
|
19
|
+
fields = extract_graphql_fields(info)
|
|
20
20
|
return await BranchType.get_list(graphql_context=info.context, fields=fields, **kwargs)
|
|
21
21
|
|
|
22
22
|
|
|
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Any
|
|
|
4
4
|
|
|
5
5
|
from graphene import Argument, Boolean, DateTime, Field, InputObjectType, Int, List, NonNull, ObjectType, String
|
|
6
6
|
from graphene import Enum as GrapheneEnum
|
|
7
|
-
from infrahub_sdk.utils import extract_fields
|
|
8
7
|
|
|
9
8
|
from infrahub.core import registry
|
|
10
9
|
from infrahub.core.constants import DiffAction, RelationshipCardinality
|
|
@@ -16,6 +15,7 @@ from infrahub.core.query.diff import DiffCountChanges
|
|
|
16
15
|
from infrahub.core.timestamp import Timestamp
|
|
17
16
|
from infrahub.dependencies.registry import get_component_registry
|
|
18
17
|
from infrahub.graphql.enums import ConflictSelection as GraphQLConflictSelection
|
|
18
|
+
from infrahub.graphql.field_extractor import extract_graphql_fields
|
|
19
19
|
|
|
20
20
|
if TYPE_CHECKING:
|
|
21
21
|
from datetime import datetime
|
|
@@ -438,7 +438,7 @@ class DiffTreeResolver:
|
|
|
438
438
|
else:
|
|
439
439
|
enriched_diff = enriched_diffs[0]
|
|
440
440
|
|
|
441
|
-
full_fields =
|
|
441
|
+
full_fields = extract_graphql_fields(info=info)
|
|
442
442
|
diff_tree = await self.to_diff_tree(enriched_diff_root=enriched_diff, graphql_context=graphql_context)
|
|
443
443
|
need_base_changes = "num_untracked_base_changes" in full_fields
|
|
444
444
|
need_branch_changes = "num_untracked_diff_changes" in full_fields
|
|
@@ -495,7 +495,7 @@ class DiffTreeResolver:
|
|
|
495
495
|
to_time=summary.to_time.to_datetime(),
|
|
496
496
|
**summary.model_dump(exclude={"from_time", "to_time"}),
|
|
497
497
|
)
|
|
498
|
-
full_fields =
|
|
498
|
+
full_fields = extract_graphql_fields(info=info)
|
|
499
499
|
need_base_changes = "num_untracked_base_changes" in full_fields
|
|
500
500
|
need_branch_changes = "num_untracked_diff_changes" in full_fields
|
|
501
501
|
if need_base_changes or need_branch_changes:
|
|
@@ -2,10 +2,11 @@ from __future__ import annotations
|
|
|
2
2
|
|
|
3
3
|
from typing import TYPE_CHECKING, Any
|
|
4
4
|
|
|
5
|
-
from graphene import Argument, Boolean, DateTime, Field, Int, List, NonNull, ObjectType, String
|
|
6
|
-
from infrahub_sdk.utils import extract_fields_first_node
|
|
5
|
+
from graphene import Argument, Boolean, DateTime, Enum, Field, Int, List, NonNull, ObjectType, String
|
|
7
6
|
|
|
7
|
+
from infrahub.events.constants import EventSortOrder
|
|
8
8
|
from infrahub.exceptions import ValidationError
|
|
9
|
+
from infrahub.graphql.field_extractor import extract_graphql_fields
|
|
9
10
|
from infrahub.graphql.types.event import EventNodes, EventTypeFilter
|
|
10
11
|
from infrahub.task_manager.event import PrefectEvent
|
|
11
12
|
from infrahub.task_manager.models import InfrahubEventFilter
|
|
@@ -15,6 +16,8 @@ if TYPE_CHECKING:
|
|
|
15
16
|
|
|
16
17
|
from graphql import GraphQLResolveInfo
|
|
17
18
|
|
|
19
|
+
InfrahubEventSortOrder = Enum.from_enum(EventSortOrder)
|
|
20
|
+
|
|
18
21
|
|
|
19
22
|
class Events(ObjectType):
|
|
20
23
|
edges = List(NonNull(EventNodes), required=True)
|
|
@@ -24,6 +27,7 @@ class Events(ObjectType):
|
|
|
24
27
|
async def resolve(
|
|
25
28
|
root: dict, # noqa: ARG004
|
|
26
29
|
info: GraphQLResolveInfo,
|
|
30
|
+
order: EventSortOrder,
|
|
27
31
|
limit: int = 10,
|
|
28
32
|
has_children: bool | None = None,
|
|
29
33
|
level: int | None = None,
|
|
@@ -57,6 +61,7 @@ class Events(ObjectType):
|
|
|
57
61
|
since=since,
|
|
58
62
|
until=until,
|
|
59
63
|
level=level,
|
|
64
|
+
order=order,
|
|
60
65
|
)
|
|
61
66
|
|
|
62
67
|
return await Events.query(
|
|
@@ -74,7 +79,7 @@ class Events(ObjectType):
|
|
|
74
79
|
limit: int,
|
|
75
80
|
offset: int | None = None,
|
|
76
81
|
) -> dict[str, Any]:
|
|
77
|
-
fields =
|
|
82
|
+
fields = extract_graphql_fields(info)
|
|
78
83
|
|
|
79
84
|
prefect_tasks = await PrefectEvent.query(
|
|
80
85
|
fields=fields,
|
|
@@ -110,6 +115,11 @@ Event = Field(
|
|
|
110
115
|
branches=List(NonNull(String), required=False, description="Filter the query to specific branches"),
|
|
111
116
|
account__ids=List(NonNull(String), required=False, description="Filter the query to specific accounts"),
|
|
112
117
|
ids=List(NonNull(String)),
|
|
118
|
+
order=InfrahubEventSortOrder(
|
|
119
|
+
required=False,
|
|
120
|
+
default_value=EventSortOrder.DESC,
|
|
121
|
+
description="Sort order of the events, defaults to descending order",
|
|
122
|
+
),
|
|
113
123
|
resolver=Events.resolve,
|
|
114
124
|
required=True,
|
|
115
125
|
)
|
infrahub/graphql/queries/ipam.py
CHANGED
|
@@ -74,7 +74,7 @@ class IPPrefixGetNextAvailable(ObjectType):
|
|
|
74
74
|
root: dict, # noqa: ARG004
|
|
75
75
|
info: GraphQLResolveInfo,
|
|
76
76
|
prefix_id: str,
|
|
77
|
-
prefix_length: int,
|
|
77
|
+
prefix_length: int | None = None,
|
|
78
78
|
) -> dict[str, str]:
|
|
79
79
|
graphql_context: GraphqlContext = info.context
|
|
80
80
|
|
|
@@ -119,3 +119,25 @@ InfrahubIPPrefixGetNextAvailable = Field(
|
|
|
119
119
|
resolver=IPPrefixGetNextAvailable.resolve,
|
|
120
120
|
required=True,
|
|
121
121
|
)
|
|
122
|
+
|
|
123
|
+
# The following two query fields must be removed once we are sure that people are not using the old queries anymore. Those fields only exist to
|
|
124
|
+
# expose a deprecation message.
|
|
125
|
+
|
|
126
|
+
DeprecatedIPAddressGetNextAvailable = Field(
|
|
127
|
+
IPAddressGetNextAvailable,
|
|
128
|
+
prefix_id=String(required=True),
|
|
129
|
+
prefix_length=Int(required=False),
|
|
130
|
+
resolver=IPAddressGetNextAvailable.resolve,
|
|
131
|
+
required=True,
|
|
132
|
+
deprecation_reason="This query has been renamed to 'InfrahubIPAddressGetNextAvailable'. It will be removed in the next version of Infrahub.",
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
DeprecatedIPPrefixGetNextAvailable = Field(
|
|
137
|
+
IPPrefixGetNextAvailable,
|
|
138
|
+
prefix_id=String(required=True),
|
|
139
|
+
prefix_length=Int(required=False),
|
|
140
|
+
resolver=IPPrefixGetNextAvailable.resolve,
|
|
141
|
+
required=True,
|
|
142
|
+
deprecation_reason="This query has been renamed to 'InfrahubIPPrefixGetNextAvailable'. It will be removed in the next version of Infrahub.",
|
|
143
|
+
)
|