infrahub-server 1.3.8__py3-none-any.whl → 1.4.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/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 +10 -29
- infrahub/computed_attribute/tasks.py +36 -46
- infrahub/config.py +57 -6
- infrahub/constants/environment.py +1 -0
- infrahub/core/attribute.py +15 -7
- infrahub/core/branch/tasks.py +43 -41
- infrahub/core/constants/__init__.py +21 -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 +81 -47
- infrahub/core/ipam/tasks.py +4 -3
- infrahub/core/merge.py +8 -10
- infrahub/core/migrations/__init__.py +2 -0
- infrahub/core/migrations/graph/__init__.py +4 -0
- infrahub/core/migrations/graph/m036_drop_attr_value_index.py +45 -0
- infrahub/core/migrations/graph/m037_index_attr_vals.py +577 -0
- infrahub/core/migrations/query/attribute_add.py +27 -2
- infrahub/core/migrations/schema/attribute_kind_update.py +156 -0
- infrahub/core/migrations/schema/tasks.py +6 -5
- infrahub/core/models.py +5 -1
- 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/definitions/internal.py +1 -1
- infrahub/core/schema/generated/attribute_schema.py +1 -1
- 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/attribute/kind.py +5 -1
- infrahub/core/validators/checks_runner.py +5 -5
- infrahub/core/validators/determiner.py +22 -2
- 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 +20 -0
- infrahub/events/constants.py +7 -0
- infrahub/events/generator.py +29 -2
- infrahub/events/proposed_change_action.py +203 -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 +230 -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 +68 -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/__init__.py +2 -1
- infrahub/permissions/constants.py +13 -0
- infrahub/permissions/globals.py +31 -2
- 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 +56 -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 +2 -2
- infrahub_sdk/ctl/repository.py +51 -0
- infrahub_sdk/ctl/schema.py +9 -9
- infrahub_sdk/node/node.py +2 -2
- infrahub_sdk/pytest_plugin/items/graphql_query.py +1 -1
- infrahub_sdk/schema/repository.py +1 -1
- infrahub_sdk/testing/docker.py +1 -1
- infrahub_sdk/utils.py +2 -2
- {infrahub_server-1.3.8.dist-info → infrahub_server-1.4.0.dist-info}/METADATA +7 -5
- {infrahub_server-1.3.8.dist-info → infrahub_server-1.4.0.dist-info}/RECORD +172 -156
- 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.8.dist-info → infrahub_server-1.4.0.dist-info}/LICENSE.txt +0 -0
- {infrahub_server-1.3.8.dist-info → infrahub_server-1.4.0.dist-info}/WHEEL +0 -0
- {infrahub_server-1.3.8.dist-info → infrahub_server-1.4.0.dist-info}/entry_points.txt +0 -0
infrahub/permissions/manager.py
CHANGED
|
@@ -48,6 +48,13 @@ class PermissionManager:
|
|
|
48
48
|
specificity += 1
|
|
49
49
|
return specificity
|
|
50
50
|
|
|
51
|
+
def is_super_admin(self) -> bool:
|
|
52
|
+
return self.resolve_global_permission(
|
|
53
|
+
permission_to_check=GlobalPermission(
|
|
54
|
+
action=GlobalPermissions.SUPER_ADMIN, decision=PermissionDecision.ALLOW_ALL
|
|
55
|
+
),
|
|
56
|
+
)
|
|
57
|
+
|
|
51
58
|
def report_object_permission(self, namespace: str, name: str, action: str) -> PermissionDecisionFlag:
|
|
52
59
|
"""Given a set of permissions, return the permission decision for a given kind and action."""
|
|
53
60
|
highest_specificity: int = -1
|
|
@@ -94,11 +101,7 @@ class PermissionManager:
|
|
|
94
101
|
|
|
95
102
|
def has_permission(self, permission: GlobalPermission | ObjectPermission) -> bool:
|
|
96
103
|
"""Tell if a permission is granted given the permissions loaded in memory."""
|
|
97
|
-
is_super_admin = self.
|
|
98
|
-
permission_to_check=GlobalPermission(
|
|
99
|
-
action=GlobalPermissions.SUPER_ADMIN, decision=PermissionDecision.ALLOW_ALL
|
|
100
|
-
),
|
|
101
|
-
)
|
|
104
|
+
is_super_admin = self.is_super_admin()
|
|
102
105
|
|
|
103
106
|
if isinstance(permission, GlobalPermission):
|
|
104
107
|
return self.resolve_global_permission(permission_to_check=permission) or is_super_admin
|
infrahub/pools/prefix.py
CHANGED
|
@@ -9,7 +9,9 @@ if TYPE_CHECKING:
|
|
|
9
9
|
from infrahub.core.ipam.constants import IPNetworkType
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def get_next_available_prefix(
|
|
12
|
+
def get_next_available_prefix(
|
|
13
|
+
pool: IPSet, prefix_length: int | None = None, prefix_ver: Literal[4, 6] = 4
|
|
14
|
+
) -> IPNetworkType:
|
|
13
15
|
"""Get the next available prefix of a given prefix length from an IPSet.
|
|
14
16
|
|
|
15
17
|
Args:
|
|
@@ -20,10 +22,7 @@ def get_next_available_prefix(pool: IPSet, prefix_length: int, prefix_ver: Liter
|
|
|
20
22
|
Raises:
|
|
21
23
|
ValueError: If there are no available subnets in the pool
|
|
22
24
|
"""
|
|
23
|
-
prefix_ver_map = {
|
|
24
|
-
4: ipaddress.IPv4Network,
|
|
25
|
-
6: ipaddress.IPv6Network,
|
|
26
|
-
}
|
|
25
|
+
prefix_ver_map = {4: ipaddress.IPv4Network, 6: ipaddress.IPv6Network}
|
|
27
26
|
|
|
28
27
|
filtered_pool = IPSet([])
|
|
29
28
|
for subnet in pool.iter_cidrs():
|
|
@@ -31,6 +30,9 @@ def get_next_available_prefix(pool: IPSet, prefix_length: int, prefix_ver: Liter
|
|
|
31
30
|
filtered_pool.add(subnet)
|
|
32
31
|
|
|
33
32
|
for cidr in filtered_pool.iter_cidrs():
|
|
33
|
+
if prefix_length is None:
|
|
34
|
+
return cidr
|
|
35
|
+
|
|
34
36
|
if cidr.prefixlen <= prefix_length:
|
|
35
37
|
next_available = ipaddress.ip_network(f"{cidr.network}/{prefix_length}")
|
|
36
38
|
return next_available
|
infrahub/prefect_server/app.py
CHANGED
|
@@ -1,16 +1,47 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
import os
|
|
5
|
+
|
|
3
6
|
from fastapi import APIRouter, FastAPI
|
|
4
7
|
from prefect.server.api.server import create_app
|
|
5
8
|
|
|
6
9
|
from . import events
|
|
10
|
+
from .bootstrap import init_prefect
|
|
11
|
+
|
|
12
|
+
GLOBAL_TASKMGR_INIT_LOCK = "global.taskmgr.init"
|
|
7
13
|
|
|
8
14
|
router = APIRouter(prefix="/infrahub")
|
|
9
15
|
|
|
10
16
|
router.include_router(events.router)
|
|
11
17
|
|
|
12
18
|
|
|
19
|
+
async def _init_prefect() -> None:
|
|
20
|
+
# Import there in case we are running Prefect within a testsuite using the original Prefect container
|
|
21
|
+
from infrahub import lock
|
|
22
|
+
from infrahub.lock import initialize_lock
|
|
23
|
+
from infrahub.services import InfrahubServices
|
|
24
|
+
from infrahub.workers.dependencies import get_cache
|
|
25
|
+
|
|
26
|
+
cache = await get_cache()
|
|
27
|
+
service = await InfrahubServices.new(cache=cache)
|
|
28
|
+
initialize_lock(service=service)
|
|
29
|
+
|
|
30
|
+
async with lock.registry.get(name=GLOBAL_TASKMGR_INIT_LOCK):
|
|
31
|
+
await init_prefect()
|
|
32
|
+
|
|
33
|
+
|
|
13
34
|
def create_infrahub_prefect() -> FastAPI:
|
|
35
|
+
if (
|
|
36
|
+
os.getenv("PREFECT_API_BLOCKS_REGISTER_ON_START") == "false"
|
|
37
|
+
and os.getenv("PREFECT_API_DATABASE_MIGRATE_ON_START") == "false"
|
|
38
|
+
):
|
|
39
|
+
# We are probably running distributed mode
|
|
40
|
+
from infrahub import config
|
|
41
|
+
|
|
42
|
+
config.SETTINGS.initialize_and_exit()
|
|
43
|
+
asyncio.run(_init_prefect())
|
|
44
|
+
|
|
14
45
|
app = create_app()
|
|
15
46
|
api_app: FastAPI = app.__dict__["api_app"]
|
|
16
47
|
api_app.include_router(router=router)
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from prefect.server.database import provide_database_interface
|
|
4
|
+
from prefect.server.models.block_registration import run_block_auto_registration
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
async def init_prefect() -> None:
|
|
8
|
+
db = provide_database_interface()
|
|
9
|
+
|
|
10
|
+
await db.create_db()
|
|
11
|
+
session = await db.session()
|
|
12
|
+
|
|
13
|
+
async with session:
|
|
14
|
+
await run_block_auto_registration(session=session)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if __name__ == "__main__":
|
|
18
|
+
asyncio.run(init_prefect())
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from abc import ABC, abstractmethod
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
from infrahub.core.account import GlobalPermission
|
|
8
|
+
from infrahub.core.constants import GlobalPermissions, PermissionDecision
|
|
9
|
+
from infrahub.exceptions import ValidationError
|
|
10
|
+
|
|
11
|
+
from .checker import verify_proposed_change_is_mergeable
|
|
12
|
+
from .constants import ProposedChangeAction, ProposedChangeState
|
|
13
|
+
|
|
14
|
+
if TYPE_CHECKING:
|
|
15
|
+
from collections.abc import Sequence
|
|
16
|
+
|
|
17
|
+
from infrahub.core.protocols import CoreGenericAccount, CoreProposedChange
|
|
18
|
+
from infrahub.graphql.initialization import GraphqlContext
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Check(ABC):
|
|
22
|
+
@abstractmethod
|
|
23
|
+
async def evaluate(
|
|
24
|
+
self,
|
|
25
|
+
proposed_change: CoreProposedChange,
|
|
26
|
+
proposed_change_author: CoreGenericAccount,
|
|
27
|
+
graphql_context: GraphqlContext,
|
|
28
|
+
) -> None: ...
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class IsAuthor(Check):
|
|
32
|
+
async def evaluate(
|
|
33
|
+
self,
|
|
34
|
+
proposed_change: CoreProposedChange, # noqa: ARG002
|
|
35
|
+
proposed_change_author: CoreGenericAccount,
|
|
36
|
+
graphql_context: GraphqlContext,
|
|
37
|
+
) -> None:
|
|
38
|
+
if proposed_change_author.id != graphql_context.active_account_session.account_id:
|
|
39
|
+
raise ValidationError("You are not the author of the proposed change")
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
class StateIs(Check):
|
|
43
|
+
def __init__(self, expected: Sequence[ProposedChangeState]) -> None:
|
|
44
|
+
self.expected = expected
|
|
45
|
+
|
|
46
|
+
async def evaluate(
|
|
47
|
+
self,
|
|
48
|
+
proposed_change: CoreProposedChange,
|
|
49
|
+
proposed_change_author: CoreGenericAccount, # noqa: ARG002
|
|
50
|
+
graphql_context: GraphqlContext, # noqa: ARG002
|
|
51
|
+
) -> None:
|
|
52
|
+
if proposed_change.state.value.value not in self.expected:
|
|
53
|
+
raise ValidationError(f"The proposed change is not {', '.join([i.value for i in self.expected])}")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class DraftIs(Check):
|
|
57
|
+
def __init__(self, expected: bool) -> None:
|
|
58
|
+
self.expected = expected
|
|
59
|
+
|
|
60
|
+
async def evaluate(
|
|
61
|
+
self,
|
|
62
|
+
proposed_change: CoreProposedChange,
|
|
63
|
+
proposed_change_author: CoreGenericAccount, # noqa: ARG002
|
|
64
|
+
graphql_context: GraphqlContext, # noqa: ARG002
|
|
65
|
+
) -> None:
|
|
66
|
+
if proposed_change.is_draft.value != self.expected:
|
|
67
|
+
if self.expected:
|
|
68
|
+
raise ValidationError("The proposed change is not a draft")
|
|
69
|
+
raise ValidationError("The proposed change is a draft")
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
class HasPermission(Check):
|
|
73
|
+
def __init__(self, permission: GlobalPermission) -> None:
|
|
74
|
+
self.permission = permission
|
|
75
|
+
|
|
76
|
+
async def evaluate(
|
|
77
|
+
self,
|
|
78
|
+
proposed_change: CoreProposedChange, # noqa: ARG002
|
|
79
|
+
proposed_change_author: CoreGenericAccount, # noqa: ARG002
|
|
80
|
+
graphql_context: GraphqlContext,
|
|
81
|
+
) -> None:
|
|
82
|
+
if not graphql_context.active_permissions.has_permission(permission=self.permission):
|
|
83
|
+
raise ValidationError("You do not have the permission to perform this action")
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class IsMergeable(Check):
|
|
87
|
+
async def evaluate(
|
|
88
|
+
self,
|
|
89
|
+
proposed_change: CoreProposedChange,
|
|
90
|
+
proposed_change_author: CoreGenericAccount, # noqa: ARG002
|
|
91
|
+
graphql_context: GraphqlContext,
|
|
92
|
+
) -> None:
|
|
93
|
+
try:
|
|
94
|
+
await verify_proposed_change_is_mergeable(
|
|
95
|
+
proposed_change=proposed_change, # type: ignore[arg-type]
|
|
96
|
+
db=graphql_context.db,
|
|
97
|
+
account_session=graphql_context.active_account_session,
|
|
98
|
+
)
|
|
99
|
+
except ValueError as exc:
|
|
100
|
+
raise ValidationError(str(exc)) from exc
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
@dataclass
|
|
104
|
+
class ActionRule:
|
|
105
|
+
action: ProposedChangeAction
|
|
106
|
+
checks: list[Check]
|
|
107
|
+
|
|
108
|
+
async def evaluate(
|
|
109
|
+
self,
|
|
110
|
+
proposed_change: CoreProposedChange,
|
|
111
|
+
proposed_change_author: CoreGenericAccount,
|
|
112
|
+
graphql_context: GraphqlContext,
|
|
113
|
+
) -> dict[str, str | bool | None]:
|
|
114
|
+
for check in self.checks:
|
|
115
|
+
try:
|
|
116
|
+
await check.evaluate(
|
|
117
|
+
proposed_change=proposed_change,
|
|
118
|
+
proposed_change_author=proposed_change_author,
|
|
119
|
+
graphql_context=graphql_context,
|
|
120
|
+
)
|
|
121
|
+
except ValidationError as exc:
|
|
122
|
+
return {"action": self.action.value, "available": False, "unavailability_reason": exc.message}
|
|
123
|
+
|
|
124
|
+
return {"action": self.action.value, "available": True, "unavailability_reason": None}
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class ActionRulesEvaluator:
|
|
128
|
+
def __init__(self, rules: list[ActionRule]):
|
|
129
|
+
self.rules = rules
|
|
130
|
+
|
|
131
|
+
async def evaluate(
|
|
132
|
+
self,
|
|
133
|
+
proposed_change: CoreProposedChange,
|
|
134
|
+
proposed_change_author: CoreGenericAccount,
|
|
135
|
+
graphql_context: GraphqlContext,
|
|
136
|
+
) -> list[dict[str, str | bool | None]]:
|
|
137
|
+
report: list[dict[str, str | bool | None]] = []
|
|
138
|
+
for rule in self.rules:
|
|
139
|
+
report.append(
|
|
140
|
+
await rule.evaluate(
|
|
141
|
+
proposed_change=proposed_change,
|
|
142
|
+
proposed_change_author=proposed_change_author,
|
|
143
|
+
graphql_context=graphql_context,
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
return report
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
MERGE_PROPOSED_CHANGE_PERMISSION = GlobalPermission(
|
|
150
|
+
action=GlobalPermissions.MERGE_PROPOSED_CHANGE.value,
|
|
151
|
+
decision=PermissionDecision.ALLOW_ALL.value,
|
|
152
|
+
)
|
|
153
|
+
REVIEW_PROPOSED_CHANGE_PERMISSION = GlobalPermission(
|
|
154
|
+
action=GlobalPermissions.REVIEW_PROPOSED_CHANGE.value,
|
|
155
|
+
decision=PermissionDecision.ALLOW_ALL.value,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
ACTION_RULES = [
|
|
159
|
+
ActionRule(action=ProposedChangeAction.OPEN, checks=[StateIs(expected=[ProposedChangeState.CLOSED])]),
|
|
160
|
+
ActionRule(action=ProposedChangeAction.CLOSE, checks=[StateIs(expected=[ProposedChangeState.OPEN])]),
|
|
161
|
+
ActionRule(
|
|
162
|
+
action=ProposedChangeAction.SET_DRAFT,
|
|
163
|
+
checks=[IsAuthor(), StateIs(expected=[ProposedChangeState.OPEN]), DraftIs(expected=False)],
|
|
164
|
+
),
|
|
165
|
+
ActionRule(
|
|
166
|
+
action=ProposedChangeAction.UNSET_DRAFT,
|
|
167
|
+
checks=[IsAuthor(), StateIs(expected=[ProposedChangeState.OPEN]), DraftIs(expected=True)],
|
|
168
|
+
),
|
|
169
|
+
ActionRule(
|
|
170
|
+
action=ProposedChangeAction.APPROVE,
|
|
171
|
+
checks=[
|
|
172
|
+
StateIs(expected=[ProposedChangeState.OPEN]),
|
|
173
|
+
HasPermission(permission=REVIEW_PROPOSED_CHANGE_PERMISSION),
|
|
174
|
+
],
|
|
175
|
+
),
|
|
176
|
+
ActionRule(
|
|
177
|
+
action=ProposedChangeAction.CANCEL_APPROVE,
|
|
178
|
+
checks=[
|
|
179
|
+
StateIs(expected=[ProposedChangeState.OPEN]),
|
|
180
|
+
HasPermission(permission=REVIEW_PROPOSED_CHANGE_PERMISSION),
|
|
181
|
+
],
|
|
182
|
+
),
|
|
183
|
+
ActionRule(
|
|
184
|
+
action=ProposedChangeAction.REJECT,
|
|
185
|
+
checks=[
|
|
186
|
+
StateIs(expected=[ProposedChangeState.OPEN]),
|
|
187
|
+
HasPermission(permission=REVIEW_PROPOSED_CHANGE_PERMISSION),
|
|
188
|
+
],
|
|
189
|
+
),
|
|
190
|
+
ActionRule(
|
|
191
|
+
action=ProposedChangeAction.CANCEL_REJECT,
|
|
192
|
+
checks=[
|
|
193
|
+
StateIs(expected=[ProposedChangeState.OPEN]),
|
|
194
|
+
HasPermission(permission=REVIEW_PROPOSED_CHANGE_PERMISSION),
|
|
195
|
+
],
|
|
196
|
+
),
|
|
197
|
+
ActionRule(
|
|
198
|
+
action=ProposedChangeAction.MERGE,
|
|
199
|
+
checks=[
|
|
200
|
+
StateIs(expected=[ProposedChangeState.OPEN]),
|
|
201
|
+
DraftIs(expected=False),
|
|
202
|
+
HasPermission(permission=MERGE_PROPOSED_CHANGE_PERMISSION),
|
|
203
|
+
IsMergeable(),
|
|
204
|
+
],
|
|
205
|
+
),
|
|
206
|
+
]
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from abc import ABC, abstractmethod
|
|
3
|
+
|
|
4
|
+
from fast_depends import Depends, inject
|
|
5
|
+
|
|
6
|
+
from infrahub.database import InfrahubDatabase
|
|
7
|
+
|
|
8
|
+
log = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class ApprovalRevoker(ABC):
|
|
12
|
+
@abstractmethod
|
|
13
|
+
async def revoke_approvals_on_updated_pcs(
|
|
14
|
+
self,
|
|
15
|
+
db: InfrahubDatabase,
|
|
16
|
+
proposed_changes_ids: list[str] | None,
|
|
17
|
+
) -> None:
|
|
18
|
+
raise NotImplementedError()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ApprovalRevokerCommunity(ApprovalRevoker):
|
|
22
|
+
async def revoke_approvals_on_updated_pcs(
|
|
23
|
+
self,
|
|
24
|
+
db: InfrahubDatabase, # noqa: ARG002
|
|
25
|
+
proposed_changes_ids: list[str] | None, # noqa: ARG002
|
|
26
|
+
) -> None:
|
|
27
|
+
raise ValueError("Revoking existing approvals based on branch changes is an enterprise feature.")
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_approval_revoker() -> ApprovalRevoker:
|
|
31
|
+
return ApprovalRevokerCommunity()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@inject
|
|
35
|
+
async def do_revoke_approvals_on_updated_pcs(
|
|
36
|
+
db: InfrahubDatabase,
|
|
37
|
+
proposed_changes_ids: list[str] | None = None,
|
|
38
|
+
approval_revoker: ApprovalRevoker = Depends(get_approval_revoker), # noqa: B008
|
|
39
|
+
) -> None:
|
|
40
|
+
await approval_revoker.revoke_approvals_on_updated_pcs(db=db, proposed_changes_ids=proposed_changes_ids)
|
|
@@ -6,6 +6,7 @@ from typing import TYPE_CHECKING, cast
|
|
|
6
6
|
|
|
7
7
|
from infrahub.exceptions import ResourceNotFoundError
|
|
8
8
|
from infrahub.message_bus.types import KVTTL
|
|
9
|
+
from infrahub.workers.dependencies import get_cache
|
|
9
10
|
|
|
10
11
|
if TYPE_CHECKING:
|
|
11
12
|
from uuid import UUID
|
|
@@ -54,7 +55,8 @@ async def set_diff_summary_cache(pipeline_id: UUID, diff_summary: list[NodeDiff]
|
|
|
54
55
|
)
|
|
55
56
|
|
|
56
57
|
|
|
57
|
-
async def get_diff_summary_cache(pipeline_id: UUID
|
|
58
|
+
async def get_diff_summary_cache(pipeline_id: UUID) -> list[NodeDiff]:
|
|
59
|
+
cache = await get_cache()
|
|
58
60
|
summary_payload = await cache.get(
|
|
59
61
|
key=f"proposed_change:pipeline:pipeline_id:{pipeline_id}:diff_summary",
|
|
60
62
|
)
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
|
|
3
|
+
from fast_depends import Depends, inject
|
|
4
|
+
|
|
5
|
+
from infrahub.auth import AccountSession
|
|
6
|
+
from infrahub.core.node import Node
|
|
7
|
+
from infrahub.database import InfrahubDatabase
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class ProposedChangeChecker(ABC):
|
|
11
|
+
# We can't use CoreProposedChange type instead of Node as fast_depends enforces pydantic runtime type checks.
|
|
12
|
+
@abstractmethod
|
|
13
|
+
async def verify_proposed_change_is_mergeable(
|
|
14
|
+
self, proposed_change: Node, db: InfrahubDatabase, account_session: AccountSession
|
|
15
|
+
) -> None:
|
|
16
|
+
"""
|
|
17
|
+
Raise an error if proposed change cannot be merged.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
raise NotImplementedError()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class ProposedChangeCheckerCommunity(ProposedChangeChecker):
|
|
24
|
+
async def verify_proposed_change_is_mergeable(
|
|
25
|
+
self, proposed_change: Node, db: InfrahubDatabase, account_session: AccountSession
|
|
26
|
+
) -> None:
|
|
27
|
+
pass
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def get_proposed_change_checker() -> ProposedChangeChecker:
|
|
31
|
+
return ProposedChangeCheckerCommunity()
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# We can't use CoreProposedChange type instead of Node as fast_depends enforces pydantic runtime type checks.
|
|
35
|
+
@inject
|
|
36
|
+
async def verify_proposed_change_is_mergeable(
|
|
37
|
+
proposed_change: Node,
|
|
38
|
+
db: InfrahubDatabase,
|
|
39
|
+
account_session: AccountSession,
|
|
40
|
+
pc_checker: ProposedChangeChecker = Depends(get_proposed_change_checker), # noqa: B008
|
|
41
|
+
) -> None:
|
|
42
|
+
# type ignore due to fast-depends enforcing pydantic checks
|
|
43
|
+
await pc_checker.verify_proposed_change_is_mergeable(
|
|
44
|
+
proposed_change=proposed_change, db=db, account_session=account_session
|
|
45
|
+
)
|
|
@@ -4,6 +4,13 @@ from infrahub.exceptions import ValidationError
|
|
|
4
4
|
from infrahub.utils import InfrahubStringEnum
|
|
5
5
|
|
|
6
6
|
|
|
7
|
+
class ProposedChangeApprovalDecision(InfrahubStringEnum):
|
|
8
|
+
APPROVE = "approve"
|
|
9
|
+
CANCEL_APPROVE = "cancel-approve"
|
|
10
|
+
REJECT = "reject"
|
|
11
|
+
CANCEL_REJECT = "cancel-reject"
|
|
12
|
+
|
|
13
|
+
|
|
7
14
|
class ProposedChangeState(InfrahubStringEnum):
|
|
8
15
|
OPEN = "open"
|
|
9
16
|
MERGED = "merged"
|
|
@@ -11,18 +18,29 @@ class ProposedChangeState(InfrahubStringEnum):
|
|
|
11
18
|
CLOSED = "closed"
|
|
12
19
|
CANCELED = "canceled"
|
|
13
20
|
|
|
21
|
+
@property
|
|
22
|
+
def is_completed(self) -> bool:
|
|
23
|
+
"""Check if the proposed change is in a completed state."""
|
|
24
|
+
return self != ProposedChangeState.OPEN
|
|
25
|
+
|
|
14
26
|
def validate_state_check_run(self) -> None:
|
|
15
27
|
if self == ProposedChangeState.OPEN:
|
|
16
28
|
return
|
|
17
29
|
|
|
18
30
|
raise ValidationError(input_value="Unable to trigger check on proposed changes that aren't in the open state")
|
|
19
31
|
|
|
20
|
-
def
|
|
21
|
-
if self
|
|
32
|
+
def validate_updatable(self) -> None:
|
|
33
|
+
if self.is_completed:
|
|
22
34
|
raise ValidationError(
|
|
23
35
|
input_value=f"A proposed change in the {self.value} state is not allowed to be updated"
|
|
24
36
|
)
|
|
25
37
|
|
|
38
|
+
def validate_reviewable(self) -> None:
|
|
39
|
+
if self.is_completed:
|
|
40
|
+
raise ValidationError(
|
|
41
|
+
input_value=f"A proposed change in the {self.value} state is not allowed to be reviewed"
|
|
42
|
+
)
|
|
43
|
+
|
|
26
44
|
def validate_state_transition(self, updated_state: ProposedChangeState) -> None:
|
|
27
45
|
if self == ProposedChangeState.OPEN:
|
|
28
46
|
return
|
|
@@ -34,3 +52,15 @@ class ProposedChangeState(InfrahubStringEnum):
|
|
|
34
52
|
raise ValidationError(
|
|
35
53
|
input_value="A closed proposed change is only allowed to transition to the open state"
|
|
36
54
|
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
class ProposedChangeAction(InfrahubStringEnum):
|
|
58
|
+
OPEN = "open"
|
|
59
|
+
CLOSE = "close"
|
|
60
|
+
SET_DRAFT = "set-draft"
|
|
61
|
+
UNSET_DRAFT = "unset-draft"
|
|
62
|
+
MERGE = "merge"
|
|
63
|
+
APPROVE = "approve"
|
|
64
|
+
CANCEL_APPROVE = "cancel-approve"
|
|
65
|
+
REJECT = "reject"
|
|
66
|
+
CANCEL_REJECT = "cancel-reject"
|