infrahub-server 1.6.0b0__py3-none-any.whl → 1.6.2__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/oauth2.py +33 -6
- infrahub/api/oidc.py +36 -6
- infrahub/auth.py +11 -0
- infrahub/auth_pkce.py +41 -0
- infrahub/config.py +9 -3
- infrahub/core/branch/models.py +3 -2
- infrahub/core/branch/tasks.py +6 -1
- infrahub/core/changelog/models.py +2 -2
- infrahub/core/constants/__init__.py +1 -0
- infrahub/core/graph/__init__.py +1 -1
- infrahub/core/integrity/object_conflict/conflict_recorder.py +1 -1
- infrahub/core/manager.py +36 -31
- infrahub/core/migrations/graph/__init__.py +4 -0
- infrahub/core/migrations/graph/m041_deleted_dup_edges.py +30 -12
- infrahub/core/migrations/graph/m047_backfill_or_null_display_label.py +606 -0
- infrahub/core/migrations/graph/m048_undelete_rel_props.py +161 -0
- infrahub/core/models.py +5 -6
- infrahub/core/node/__init__.py +16 -13
- infrahub/core/node/create.py +36 -8
- infrahub/core/node/proposed_change.py +5 -3
- infrahub/core/node/standard.py +1 -1
- infrahub/core/protocols.py +1 -7
- infrahub/core/query/attribute.py +1 -1
- infrahub/core/query/node.py +9 -5
- infrahub/core/relationship/model.py +21 -4
- infrahub/core/schema/generic_schema.py +1 -1
- infrahub/core/schema/manager.py +8 -3
- infrahub/core/schema/schema_branch.py +35 -16
- infrahub/core/validators/attribute/choices.py +2 -2
- infrahub/core/validators/determiner.py +3 -6
- infrahub/database/__init__.py +1 -1
- infrahub/git/base.py +2 -3
- infrahub/git/models.py +13 -0
- infrahub/git/tasks.py +23 -19
- infrahub/git/utils.py +16 -9
- infrahub/graphql/app.py +6 -6
- infrahub/graphql/loaders/peers.py +6 -0
- infrahub/graphql/mutations/action.py +15 -7
- infrahub/graphql/mutations/hfid.py +1 -1
- infrahub/graphql/mutations/profile.py +8 -1
- infrahub/graphql/mutations/repository.py +3 -3
- infrahub/graphql/mutations/schema.py +4 -4
- infrahub/graphql/mutations/webhook.py +2 -2
- infrahub/graphql/queries/resource_manager.py +2 -3
- infrahub/graphql/queries/search.py +2 -3
- infrahub/graphql/resolvers/ipam.py +20 -0
- infrahub/graphql/resolvers/many_relationship.py +12 -11
- infrahub/graphql/resolvers/resolver.py +6 -2
- infrahub/graphql/resolvers/single_relationship.py +1 -11
- infrahub/log.py +1 -1
- infrahub/message_bus/messages/__init__.py +0 -12
- infrahub/profiles/node_applier.py +9 -0
- infrahub/proposed_change/branch_diff.py +1 -1
- infrahub/proposed_change/tasks.py +1 -1
- infrahub/repositories/create_repository.py +3 -3
- infrahub/task_manager/models.py +1 -1
- infrahub/task_manager/task.py +5 -5
- infrahub/trigger/setup.py +6 -9
- infrahub/utils.py +18 -0
- infrahub/validators/tasks.py +1 -1
- infrahub/workers/infrahub_async.py +7 -6
- infrahub_sdk/client.py +113 -1
- infrahub_sdk/ctl/AGENTS.md +67 -0
- infrahub_sdk/ctl/branch.py +175 -1
- infrahub_sdk/ctl/check.py +3 -3
- infrahub_sdk/ctl/cli_commands.py +9 -9
- infrahub_sdk/ctl/generator.py +2 -2
- infrahub_sdk/ctl/graphql.py +1 -2
- infrahub_sdk/ctl/importer.py +1 -2
- infrahub_sdk/ctl/repository.py +6 -49
- infrahub_sdk/ctl/task.py +2 -4
- infrahub_sdk/ctl/utils.py +2 -2
- infrahub_sdk/ctl/validate.py +1 -2
- infrahub_sdk/diff.py +80 -3
- infrahub_sdk/graphql/constants.py +14 -1
- infrahub_sdk/graphql/renderers.py +5 -1
- infrahub_sdk/node/attribute.py +0 -1
- infrahub_sdk/node/constants.py +3 -1
- infrahub_sdk/node/node.py +303 -3
- infrahub_sdk/node/related_node.py +1 -2
- infrahub_sdk/node/relationship.py +1 -2
- infrahub_sdk/protocols_base.py +0 -1
- infrahub_sdk/pytest_plugin/AGENTS.md +67 -0
- infrahub_sdk/schema/__init__.py +0 -3
- infrahub_sdk/timestamp.py +7 -7
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/METADATA +2 -3
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/RECORD +91 -86
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/WHEEL +1 -1
- infrahub_testcontainers/container.py +2 -2
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/entry_points.txt +0 -0
- {infrahub_server-1.6.0b0.dist-info → infrahub_server-1.6.2.dist-info}/licenses/LICENSE.txt +0 -0
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
|
+
import contextlib
|
|
3
4
|
import ipaddress
|
|
4
5
|
from typing import TYPE_CHECKING, Any
|
|
5
6
|
|
|
@@ -117,11 +118,9 @@ async def search_resolver(
|
|
|
117
118
|
if matching:
|
|
118
119
|
results.append(matching)
|
|
119
120
|
else:
|
|
120
|
-
|
|
121
|
+
with contextlib.suppress(ValueError, ipaddress.AddressValueError):
|
|
121
122
|
# Convert any IPv6 address, network or partial address to collapsed format as it might be stored in db.
|
|
122
123
|
q = _collapse_ipv6(q)
|
|
123
|
-
except (ValueError, ipaddress.AddressValueError):
|
|
124
|
-
pass
|
|
125
124
|
|
|
126
125
|
for kind in [InfrahubKind.NODE, InfrahubKind.GENERICGROUP]:
|
|
127
126
|
objs = await NodeManager.query(
|
|
@@ -4,6 +4,7 @@ import ipaddress
|
|
|
4
4
|
from typing import TYPE_CHECKING, Any
|
|
5
5
|
|
|
6
6
|
from graphql.type.definition import GraphQLNonNull
|
|
7
|
+
from infrahub_sdk.utils import deep_merge_dict
|
|
7
8
|
from netaddr import IPSet
|
|
8
9
|
from opentelemetry import trace
|
|
9
10
|
|
|
@@ -233,6 +234,23 @@ async def _resolve_available_prefix_nodes(
|
|
|
233
234
|
return available_nodes
|
|
234
235
|
|
|
235
236
|
|
|
237
|
+
def _ensure_display_label_fields(
|
|
238
|
+
db: InfrahubDatabase, branch: Branch, schema: NodeSchema | GenericSchema, node_fields: dict[str, Any]
|
|
239
|
+
) -> None:
|
|
240
|
+
"""Ensure fields needed to compute display_label are included in node_fields.
|
|
241
|
+
|
|
242
|
+
This is mostly for virtual nodes (InternalIPPrefixAvailable, InternalIPRangeAvailable) that are not stored in the
|
|
243
|
+
database.
|
|
244
|
+
"""
|
|
245
|
+
if "display_label" not in node_fields or schema.kind not in [InfrahubKind.IPPREFIX, InfrahubKind.IPADDRESS]:
|
|
246
|
+
return
|
|
247
|
+
|
|
248
|
+
schema_branch = db.schema.get_schema_branch(name=branch.name)
|
|
249
|
+
display_label_fields = schema_branch.generate_fields_for_display_label(name=schema.kind)
|
|
250
|
+
if display_label_fields:
|
|
251
|
+
deep_merge_dict(dicta=node_fields, dictb=display_label_fields)
|
|
252
|
+
|
|
253
|
+
|
|
236
254
|
def _filter_kinds(nodes: list[Node], kinds: list[str], limit: int | None) -> list[Node]:
|
|
237
255
|
filtered: list[Node] = []
|
|
238
256
|
available_node_kinds = [InfrahubKind.IPPREFIXAVAILABLE, InfrahubKind.IPRANGEAVAILABLE]
|
|
@@ -324,6 +342,8 @@ async def ipam_paginated_list_resolver( # noqa: PLR0915
|
|
|
324
342
|
edges = fields.get("edges", {})
|
|
325
343
|
node_fields = edges.get("node", {})
|
|
326
344
|
|
|
345
|
+
_ensure_display_label_fields(db=db, branch=graphql_context.branch, schema=schema, node_fields=node_fields)
|
|
346
|
+
|
|
327
347
|
permission_set: dict[str, Any] | None = None
|
|
328
348
|
permissions = (
|
|
329
349
|
await get_permissions(schema=schema, graphql_context=graphql_context)
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
from typing import TYPE_CHECKING, Any
|
|
2
2
|
|
|
3
3
|
from graphql import GraphQLResolveInfo
|
|
4
|
-
from infrahub_sdk.utils import deep_merge_dict
|
|
5
4
|
|
|
6
5
|
from infrahub.core.branch.models import Branch
|
|
7
6
|
from infrahub.core.constants import BranchSupportType, RelationshipHierarchyDirection
|
|
@@ -12,6 +11,7 @@ from infrahub.core.schema.relationship_schema import RelationshipSchema
|
|
|
12
11
|
from infrahub.core.timestamp import Timestamp
|
|
13
12
|
from infrahub.database import InfrahubDatabase
|
|
14
13
|
from infrahub.graphql.field_extractor import extract_graphql_fields
|
|
14
|
+
from infrahub.utils import has_any_key
|
|
15
15
|
|
|
16
16
|
from ..loaders.peers import PeerRelationshipsDataLoader, QueryPeerParams
|
|
17
17
|
from ..types import RELATIONS_PROPERTY_MAP, RELATIONS_PROPERTY_MAP_REVERSED
|
|
@@ -195,6 +195,9 @@ class ManyRelationshipResolver:
|
|
|
195
195
|
offset: int | None = None,
|
|
196
196
|
limit: int | None = None,
|
|
197
197
|
) -> list[dict[str, Any]] | None:
|
|
198
|
+
include_source = has_any_key(data=node_fields, keys=["_relation__source", "source"])
|
|
199
|
+
include_owner = has_any_key(data=node_fields, keys=["_relation__owner", "owner"])
|
|
200
|
+
|
|
198
201
|
async with db.start_session(read_only=True) as dbs:
|
|
199
202
|
objs = await NodeManager.query_peers(
|
|
200
203
|
db=dbs,
|
|
@@ -209,6 +212,8 @@ class ManyRelationshipResolver:
|
|
|
209
212
|
branch=branch,
|
|
210
213
|
branch_agnostic=rel_schema.branch is BranchSupportType.AGNOSTIC,
|
|
211
214
|
fetch_peers=True,
|
|
215
|
+
include_source=include_source,
|
|
216
|
+
include_owner=include_owner,
|
|
212
217
|
)
|
|
213
218
|
if not objs:
|
|
214
219
|
return None
|
|
@@ -226,17 +231,11 @@ class ManyRelationshipResolver:
|
|
|
226
231
|
filters: dict[str, Any],
|
|
227
232
|
node_fields: dict[str, Any],
|
|
228
233
|
) -> list[dict[str, Any]] | None:
|
|
229
|
-
if node_fields and "display_label" in node_fields:
|
|
230
|
-
schema_branch = db.schema.get_schema_branch(name=branch.name)
|
|
231
|
-
display_label_fields = schema_branch.generate_fields_for_display_label(name=rel_schema.peer)
|
|
232
|
-
if display_label_fields:
|
|
233
|
-
node_fields = deep_merge_dict(dicta=node_fields, dictb=display_label_fields)
|
|
234
|
-
|
|
235
234
|
if node_fields and "hfid" in node_fields:
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
235
|
+
node_fields["human_friendly_id"] = None
|
|
236
|
+
|
|
237
|
+
include_source = has_any_key(data=node_fields, keys=["_relation__source", "source"])
|
|
238
|
+
include_owner = has_any_key(data=node_fields, keys=["_relation__owner", "owner"])
|
|
240
239
|
|
|
241
240
|
query_params = QueryPeerParams(
|
|
242
241
|
branch=branch,
|
|
@@ -246,6 +245,8 @@ class ManyRelationshipResolver:
|
|
|
246
245
|
fields=node_fields,
|
|
247
246
|
at=at,
|
|
248
247
|
branch_agnostic=rel_schema.branch is BranchSupportType.AGNOSTIC,
|
|
248
|
+
include_source=include_source,
|
|
249
|
+
include_owner=include_owner,
|
|
249
250
|
)
|
|
250
251
|
if query_params in self._data_loader_instances:
|
|
251
252
|
loader = self._data_loader_instances[query_params]
|
|
@@ -9,6 +9,7 @@ from infrahub.core.constants import BranchSupportType, InfrahubKind, Relationshi
|
|
|
9
9
|
from infrahub.core.manager import NodeManager
|
|
10
10
|
from infrahub.exceptions import NodeNotFoundError
|
|
11
11
|
from infrahub.graphql.field_extractor import extract_graphql_fields
|
|
12
|
+
from infrahub.utils import has_any_key
|
|
12
13
|
|
|
13
14
|
from ..models import OrderModel
|
|
14
15
|
from ..parser import extract_selection
|
|
@@ -185,6 +186,9 @@ async def default_paginated_list_resolver(
|
|
|
185
186
|
|
|
186
187
|
objs = []
|
|
187
188
|
if edges or "hfid" in filters:
|
|
189
|
+
include_source = has_any_key(data=node_fields, keys=["_relation__source", "source"])
|
|
190
|
+
include_owner = has_any_key(data=node_fields, keys=["_relation__owner", "owner"])
|
|
191
|
+
|
|
188
192
|
objs = await NodeManager.query(
|
|
189
193
|
db=db,
|
|
190
194
|
schema=schema,
|
|
@@ -195,8 +199,8 @@ async def default_paginated_list_resolver(
|
|
|
195
199
|
limit=limit,
|
|
196
200
|
offset=offset,
|
|
197
201
|
account=graphql_context.account_session,
|
|
198
|
-
include_source=
|
|
199
|
-
include_owner=
|
|
202
|
+
include_source=include_source,
|
|
203
|
+
include_owner=include_owner,
|
|
200
204
|
partial_match=partial_match,
|
|
201
205
|
order=order,
|
|
202
206
|
)
|
|
@@ -2,7 +2,6 @@ from typing import TYPE_CHECKING, Any
|
|
|
2
2
|
|
|
3
3
|
from graphql import GraphQLResolveInfo
|
|
4
4
|
from graphql.type.definition import GraphQLNonNull
|
|
5
|
-
from infrahub_sdk.utils import deep_merge_dict
|
|
6
5
|
|
|
7
6
|
from infrahub.core.branch.models import Branch
|
|
8
7
|
from infrahub.core.constants import BranchSupportType
|
|
@@ -142,17 +141,8 @@ class SingleRelationshipResolver:
|
|
|
142
141
|
except (KeyError, IndexError):
|
|
143
142
|
return None
|
|
144
143
|
|
|
145
|
-
if node_fields and "display_label" in node_fields:
|
|
146
|
-
schema_branch = db.schema.get_schema_branch(name=branch.name)
|
|
147
|
-
display_label_fields = schema_branch.generate_fields_for_display_label(name=rel_schema.peer)
|
|
148
|
-
if display_label_fields:
|
|
149
|
-
node_fields = deep_merge_dict(dicta=node_fields, dictb=display_label_fields)
|
|
150
|
-
|
|
151
144
|
if node_fields and "hfid" in node_fields:
|
|
152
|
-
|
|
153
|
-
hfid_fields = peer_schema.generate_fields_for_hfid()
|
|
154
|
-
if hfid_fields:
|
|
155
|
-
node_fields = deep_merge_dict(dicta=node_fields, dictb=hfid_fields)
|
|
145
|
+
node_fields["human_friendly_id"] = None
|
|
156
146
|
|
|
157
147
|
query_params = GetManyParams(
|
|
158
148
|
fields=node_fields,
|
infrahub/log.py
CHANGED
|
@@ -10,7 +10,7 @@ from structlog.dev import plain_traceback
|
|
|
10
10
|
if TYPE_CHECKING:
|
|
11
11
|
from structlog.types import Processor
|
|
12
12
|
|
|
13
|
-
INFRAHUB_PRODUCTION = TypeAdapter(bool).validate_python(os.environ.get("INFRAHUB_PRODUCTION",
|
|
13
|
+
INFRAHUB_PRODUCTION = TypeAdapter(bool).validate_python(os.environ.get("INFRAHUB_PRODUCTION", "true"))
|
|
14
14
|
INFRAHUB_LOG_LEVEL = os.environ.get("INFRAHUB_LOG_LEVEL", "INFO")
|
|
15
15
|
|
|
16
16
|
|
|
@@ -22,20 +22,8 @@ RESPONSE_MAP: dict[str, type[InfrahubResponse]] = {
|
|
|
22
22
|
}
|
|
23
23
|
|
|
24
24
|
PRIORITY_MAP = {
|
|
25
|
-
"check.artifact.create": 2,
|
|
26
|
-
"check.repository.check_definition": 2,
|
|
27
|
-
"check.repository.merge_conflicts": 2,
|
|
28
25
|
"send.echo.request": 5, # Currently only for testing purposes, will be removed once all message bus have been migrated to prefect
|
|
29
|
-
"event.branch.delete": 5,
|
|
30
|
-
"event.branch.merge": 5,
|
|
31
|
-
"event.schema.update": 5,
|
|
32
|
-
"git.diff.names_only": 4,
|
|
33
26
|
"git.file.get": 4,
|
|
34
|
-
"request.artifact.generate": 2,
|
|
35
|
-
"request.git.sync": 4,
|
|
36
|
-
"request.proposed_change.pipeline": 5,
|
|
37
|
-
"transform.jinja.template": 4,
|
|
38
|
-
"transform.python.data": 4,
|
|
39
27
|
}
|
|
40
28
|
|
|
41
29
|
|
|
@@ -9,6 +9,15 @@ from .queries.get_profile_data import GetProfileDataQuery, ProfileData
|
|
|
9
9
|
|
|
10
10
|
|
|
11
11
|
class NodeProfilesApplier:
|
|
12
|
+
"""Applies profile values to nodes and templates.
|
|
13
|
+
|
|
14
|
+
Profile values take precedence over both default values and template-sourced values.
|
|
15
|
+
When a template has profiles assigned:
|
|
16
|
+
1. Profile values are applied to the template itself
|
|
17
|
+
2. Nodes created from that template inherit the profile values (not the template's own values)
|
|
18
|
+
3. Profile priority determines which profile wins when multiple profiles set the same attribute
|
|
19
|
+
"""
|
|
20
|
+
|
|
12
21
|
def __init__(self, db: InfrahubDatabase, branch: Branch):
|
|
13
22
|
self.db = db
|
|
14
23
|
self.branch = branch
|
|
@@ -64,4 +64,4 @@ async def get_diff_summary_cache(pipeline_id: UUID) -> list[NodeDiff]:
|
|
|
64
64
|
if not summary_payload:
|
|
65
65
|
raise ResourceNotFoundError(message=f"Diff summary for pipeline {pipeline_id} was not found in the cache")
|
|
66
66
|
|
|
67
|
-
return cast(list[
|
|
67
|
+
return cast("list[NodeDiff]", json.loads(summary_payload))
|
|
@@ -480,7 +480,7 @@ async def _get_proposed_change_schema_integrity_constraints(
|
|
|
480
480
|
DiffElementType.RELATIONSHIP_ONE.value.lower(),
|
|
481
481
|
):
|
|
482
482
|
field_summary.relationship_names.add(element_name)
|
|
483
|
-
elif element_type.lower()
|
|
483
|
+
elif element_type.lower() == DiffElementType.ATTRIBUTE.value.lower():
|
|
484
484
|
field_summary.attribute_names.add(element_name)
|
|
485
485
|
|
|
486
486
|
determiner = ConstraintValidatorDeterminer(schema_branch=schema)
|
|
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, cast
|
|
|
4
4
|
|
|
5
5
|
from infrahub.core.constants import RepositoryInternalStatus
|
|
6
6
|
from infrahub.core.constants.infrahubkind import READONLYREPOSITORY, REPOSITORY
|
|
7
|
-
from infrahub.core.protocols import CoreGenericRepository, CoreReadOnlyRepository, CoreRepository
|
|
8
7
|
from infrahub.exceptions import ValidationError
|
|
9
8
|
from infrahub.git.models import GitRepositoryAdd, GitRepositoryAddReadOnly
|
|
10
9
|
from infrahub.log import get_logger
|
|
@@ -16,6 +15,7 @@ if TYPE_CHECKING:
|
|
|
16
15
|
from infrahub.auth import AccountSession
|
|
17
16
|
from infrahub.context import InfrahubContext
|
|
18
17
|
from infrahub.core.branch import Branch
|
|
18
|
+
from infrahub.core.protocols import CoreGenericRepository, CoreReadOnlyRepository, CoreRepository
|
|
19
19
|
from infrahub.database import InfrahubDatabase
|
|
20
20
|
from infrahub.services import InfrahubServices
|
|
21
21
|
|
|
@@ -74,7 +74,7 @@ class RepositoryFinalizer:
|
|
|
74
74
|
authenticated_user = self.account_session.account_id
|
|
75
75
|
|
|
76
76
|
if obj.get_kind() == READONLYREPOSITORY:
|
|
77
|
-
obj = cast(CoreReadOnlyRepository, obj)
|
|
77
|
+
obj = cast("CoreReadOnlyRepository", obj)
|
|
78
78
|
model = GitRepositoryAddReadOnly(
|
|
79
79
|
repository_id=obj.id,
|
|
80
80
|
repository_name=obj.name.value,
|
|
@@ -92,7 +92,7 @@ class RepositoryFinalizer:
|
|
|
92
92
|
)
|
|
93
93
|
|
|
94
94
|
elif obj.get_kind() == REPOSITORY:
|
|
95
|
-
obj = cast(CoreRepository, obj)
|
|
95
|
+
obj = cast("CoreRepository", obj)
|
|
96
96
|
git_repo_add_model = GitRepositoryAdd(
|
|
97
97
|
repository_id=obj.id,
|
|
98
98
|
repository_name=obj.name.value,
|
infrahub/task_manager/models.py
CHANGED
|
@@ -74,7 +74,7 @@ class FlowLogs(BaseModel):
|
|
|
74
74
|
"node": {
|
|
75
75
|
"message": log.message,
|
|
76
76
|
"severity": LOG_LEVEL_MAPPING.get(log.level, "error"),
|
|
77
|
-
"timestamp": log.timestamp.
|
|
77
|
+
"timestamp": log.timestamp.isoformat(),
|
|
78
78
|
}
|
|
79
79
|
}
|
|
80
80
|
for log in self.logs[flow_id]
|
infrahub/task_manager/task.py
CHANGED
|
@@ -151,7 +151,7 @@ class PrefectTask:
|
|
|
151
151
|
remaining -= nb_fetched
|
|
152
152
|
|
|
153
153
|
for flow_log in all_logs:
|
|
154
|
-
if flow_log.flow_run_id and flow_log.message
|
|
154
|
+
if flow_log.flow_run_id and flow_log.message != "Finished in state Completed()":
|
|
155
155
|
logs_flow.logs[flow_log.flow_run_id].append(flow_log)
|
|
156
156
|
|
|
157
157
|
return logs_flow
|
|
@@ -325,13 +325,13 @@ class PrefectTask:
|
|
|
325
325
|
"parameters": flow.parameters,
|
|
326
326
|
"branch": await cls._extract_branch_name(flow=flow),
|
|
327
327
|
"tags": flow.tags,
|
|
328
|
-
"workflow": workflow_names.get(flow.flow_id
|
|
328
|
+
"workflow": workflow_names.get(flow.flow_id),
|
|
329
329
|
"related_node": related_node.id if related_node else None,
|
|
330
330
|
"related_node_kind": related_node.kind if related_node else None,
|
|
331
331
|
"related_nodes": related_nodes_info.get_related_nodes_as_dict(flow_id=flow.id),
|
|
332
|
-
"created_at": flow.created.
|
|
333
|
-
"updated_at": flow.updated.
|
|
334
|
-
"start_time": flow.start_time.
|
|
332
|
+
"created_at": flow.created.isoformat() if flow.created else None,
|
|
333
|
+
"updated_at": flow.updated.isoformat() if flow.updated else None,
|
|
334
|
+
"start_time": flow.start_time.isoformat() if flow.start_time else None,
|
|
335
335
|
"id": flow.id,
|
|
336
336
|
"logs": {"edges": logs, "count": len(logs)},
|
|
337
337
|
}
|
infrahub/trigger/setup.py
CHANGED
|
@@ -122,7 +122,7 @@ async def setup_triggers(
|
|
|
122
122
|
actions=[action.get_prefect(mapping=deployments_mapping) for action in trigger.actions],
|
|
123
123
|
)
|
|
124
124
|
|
|
125
|
-
existing_automation = existing_automations.get(trigger.generate_name()
|
|
125
|
+
existing_automation = existing_automations.get(trigger.generate_name())
|
|
126
126
|
|
|
127
127
|
if existing_automation:
|
|
128
128
|
trigger_comparison = compare_automations(
|
|
@@ -171,19 +171,16 @@ async def gather_all_automations(client: PrefectClient) -> list[Automation]:
|
|
|
171
171
|
retrieves them all by paginating through the results. The default within Prefect is 200 items,
|
|
172
172
|
and client.read_automations() doesn't support pagination parameters.
|
|
173
173
|
"""
|
|
174
|
-
automation_count_response = await client.request("POST", "/automations/count")
|
|
175
|
-
automation_count_response.raise_for_status()
|
|
176
|
-
automation_count: int = automation_count_response.json()
|
|
177
174
|
offset = 0
|
|
178
175
|
limit = 200
|
|
179
|
-
missing_automations = True
|
|
180
176
|
automations: list[Automation] = []
|
|
181
|
-
while
|
|
177
|
+
while True:
|
|
182
178
|
response = await client.request("POST", "/automations/filter", json={"limit": limit, "offset": offset})
|
|
183
179
|
response.raise_for_status()
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
180
|
+
batch = Automation.model_validate_list(response.json())
|
|
181
|
+
automations.extend(batch)
|
|
182
|
+
if len(batch) < limit:
|
|
183
|
+
break
|
|
187
184
|
offset += limit
|
|
188
185
|
|
|
189
186
|
return automations
|
infrahub/utils.py
CHANGED
|
@@ -83,3 +83,21 @@ def get_all_subclasses[AnyClass: type](cls: AnyClass) -> list[AnyClass]:
|
|
|
83
83
|
subclasses.append(subclass)
|
|
84
84
|
subclasses.extend(get_all_subclasses(subclass))
|
|
85
85
|
return subclasses
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def has_any_key(data: dict[str, Any], keys: list[str]) -> bool:
|
|
89
|
+
"""Recursively check if any of the specified keys exist in the dictionary at any level.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
data: The dictionary to search through
|
|
93
|
+
keys: List of key names to search for
|
|
94
|
+
|
|
95
|
+
Returns:
|
|
96
|
+
True if any of the keys are found at any level of the dictionary, False otherwise
|
|
97
|
+
"""
|
|
98
|
+
for key, value in data.items():
|
|
99
|
+
if key in keys:
|
|
100
|
+
return True
|
|
101
|
+
if isinstance(value, dict) and has_any_key(data=value, keys=keys):
|
|
102
|
+
return True
|
|
103
|
+
return False
|
infrahub/validators/tasks.py
CHANGED
|
@@ -26,7 +26,7 @@ async def start_validator[ValidatorType: CoreValidator](
|
|
|
26
26
|
validator.started_at.value = ""
|
|
27
27
|
validator.completed_at.value = ""
|
|
28
28
|
await validator.save()
|
|
29
|
-
validator = cast(ValidatorType, validator)
|
|
29
|
+
validator = cast("ValidatorType", validator)
|
|
30
30
|
else:
|
|
31
31
|
data["proposed_change"] = proposed_change
|
|
32
32
|
validator = await client.create(kind=validator_type, data=data)
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import asyncio
|
|
2
|
+
import contextlib
|
|
2
3
|
import logging
|
|
3
4
|
import os
|
|
4
5
|
from pathlib import Path
|
|
@@ -107,7 +108,7 @@ class InfrahubWorkerAsync(BaseWorker):
|
|
|
107
108
|
|
|
108
109
|
# Start metric endpoint
|
|
109
110
|
if metric_port is None or metric_port != 0:
|
|
110
|
-
metric_port = metric_port or int(os.environ.get("INFRAHUB_METRICS_PORT", 8000))
|
|
111
|
+
metric_port = metric_port or int(os.environ.get("INFRAHUB_METRICS_PORT", "8000"))
|
|
111
112
|
self._logger.info(f"Starting metric endpoint on port {metric_port}")
|
|
112
113
|
start_http_server(metric_port)
|
|
113
114
|
|
|
@@ -212,18 +213,18 @@ class InfrahubWorkerAsync(BaseWorker):
|
|
|
212
213
|
global_config_file = config.SETTINGS.git.global_config_file
|
|
213
214
|
if not os.getenv("GIT_CONFIG_GLOBAL") and global_config_file:
|
|
214
215
|
config_dir = Path(global_config_file).parent
|
|
215
|
-
|
|
216
|
+
with contextlib.suppress(FileExistsError):
|
|
216
217
|
config_dir.mkdir(exist_ok=True, parents=True)
|
|
217
|
-
except FileExistsError:
|
|
218
|
-
pass
|
|
219
218
|
os.environ["GIT_CONFIG_GLOBAL"] = global_config_file
|
|
220
219
|
self._logger.info(f"Set git config file to {global_config_file}")
|
|
221
220
|
|
|
222
221
|
await self._run_git_config_global(config.SETTINGS.git.user_name, setting_name="user.name")
|
|
223
222
|
await self._run_git_config_global(config.SETTINGS.git.user_email, setting_name="user.email")
|
|
224
|
-
await self._run_git_config_global("
|
|
223
|
+
await self._run_git_config_global("*", "--replace-all", setting_name="safe.directory")
|
|
225
224
|
await self._run_git_config_global("true", setting_name="credential.usehttppath")
|
|
226
|
-
await self._run_git_config_global(
|
|
225
|
+
await self._run_git_config_global(
|
|
226
|
+
f"/usr/bin/env {config.SETTINGS.dev.git_credential_helper}", setting_name="credential.helper"
|
|
227
|
+
)
|
|
227
228
|
|
|
228
229
|
async def _run_git_config_global(self, *args: str, setting_name: str) -> None:
|
|
229
230
|
proc = await asyncio.create_subprocess_exec(
|
infrahub_sdk/client.py
CHANGED
|
@@ -34,7 +34,7 @@ from .config import Config
|
|
|
34
34
|
from .constants import InfrahubClientMode
|
|
35
35
|
from .convert_object_type import CONVERT_OBJECT_MUTATION, ConversionFieldInput
|
|
36
36
|
from .data import RepositoryBranchInfo, RepositoryData
|
|
37
|
-
from .diff import NodeDiff, diff_tree_node_to_node_diff, get_diff_summary_query
|
|
37
|
+
from .diff import DiffTreeData, NodeDiff, diff_tree_node_to_node_diff, get_diff_summary_query, get_diff_tree_query
|
|
38
38
|
from .exceptions import (
|
|
39
39
|
AuthenticationError,
|
|
40
40
|
Error,
|
|
@@ -1282,6 +1282,62 @@ class InfrahubClient(BaseClient):
|
|
|
1282
1282
|
|
|
1283
1283
|
return node_diffs
|
|
1284
1284
|
|
|
1285
|
+
async def get_diff_tree(
|
|
1286
|
+
self,
|
|
1287
|
+
branch: str,
|
|
1288
|
+
name: str | None = None,
|
|
1289
|
+
from_time: datetime | None = None,
|
|
1290
|
+
to_time: datetime | None = None,
|
|
1291
|
+
timeout: int | None = None,
|
|
1292
|
+
tracker: str | None = None,
|
|
1293
|
+
) -> DiffTreeData | None:
|
|
1294
|
+
"""Get complete diff tree with metadata and nodes.
|
|
1295
|
+
|
|
1296
|
+
Returns None if no diff exists.
|
|
1297
|
+
"""
|
|
1298
|
+
query = get_diff_tree_query()
|
|
1299
|
+
input_data = {"branch_name": branch}
|
|
1300
|
+
if name:
|
|
1301
|
+
input_data["name"] = name
|
|
1302
|
+
if from_time and to_time and from_time > to_time:
|
|
1303
|
+
raise ValueError("from_time must be <= to_time")
|
|
1304
|
+
if from_time:
|
|
1305
|
+
input_data["from_time"] = from_time.isoformat()
|
|
1306
|
+
if to_time:
|
|
1307
|
+
input_data["to_time"] = to_time.isoformat()
|
|
1308
|
+
|
|
1309
|
+
response = await self.execute_graphql(
|
|
1310
|
+
query=query.render(),
|
|
1311
|
+
branch_name=branch,
|
|
1312
|
+
timeout=timeout,
|
|
1313
|
+
tracker=tracker,
|
|
1314
|
+
variables=input_data,
|
|
1315
|
+
)
|
|
1316
|
+
|
|
1317
|
+
diff_tree = response["DiffTree"]
|
|
1318
|
+
if diff_tree is None:
|
|
1319
|
+
return None
|
|
1320
|
+
|
|
1321
|
+
# Convert nodes to NodeDiff objects
|
|
1322
|
+
node_diffs: list[NodeDiff] = []
|
|
1323
|
+
if "nodes" in diff_tree:
|
|
1324
|
+
for node_dict in diff_tree["nodes"]:
|
|
1325
|
+
node_diff = diff_tree_node_to_node_diff(node_dict=node_dict, branch_name=branch)
|
|
1326
|
+
node_diffs.append(node_diff)
|
|
1327
|
+
|
|
1328
|
+
return DiffTreeData(
|
|
1329
|
+
num_added=diff_tree.get("num_added") or 0,
|
|
1330
|
+
num_updated=diff_tree.get("num_updated") or 0,
|
|
1331
|
+
num_removed=diff_tree.get("num_removed") or 0,
|
|
1332
|
+
num_conflicts=diff_tree.get("num_conflicts") or 0,
|
|
1333
|
+
to_time=diff_tree["to_time"],
|
|
1334
|
+
from_time=diff_tree["from_time"],
|
|
1335
|
+
base_branch=diff_tree["base_branch"],
|
|
1336
|
+
diff_branch=diff_tree["diff_branch"],
|
|
1337
|
+
name=diff_tree.get("name"),
|
|
1338
|
+
nodes=node_diffs,
|
|
1339
|
+
)
|
|
1340
|
+
|
|
1285
1341
|
@overload
|
|
1286
1342
|
async def allocate_next_ip_address(
|
|
1287
1343
|
self,
|
|
@@ -2520,6 +2576,62 @@ class InfrahubClientSync(BaseClient):
|
|
|
2520
2576
|
|
|
2521
2577
|
return node_diffs
|
|
2522
2578
|
|
|
2579
|
+
def get_diff_tree(
|
|
2580
|
+
self,
|
|
2581
|
+
branch: str,
|
|
2582
|
+
name: str | None = None,
|
|
2583
|
+
from_time: datetime | None = None,
|
|
2584
|
+
to_time: datetime | None = None,
|
|
2585
|
+
timeout: int | None = None,
|
|
2586
|
+
tracker: str | None = None,
|
|
2587
|
+
) -> DiffTreeData | None:
|
|
2588
|
+
"""Get complete diff tree with metadata and nodes.
|
|
2589
|
+
|
|
2590
|
+
Returns None if no diff exists.
|
|
2591
|
+
"""
|
|
2592
|
+
query = get_diff_tree_query()
|
|
2593
|
+
input_data = {"branch_name": branch}
|
|
2594
|
+
if name:
|
|
2595
|
+
input_data["name"] = name
|
|
2596
|
+
if from_time and to_time and from_time > to_time:
|
|
2597
|
+
raise ValueError("from_time must be <= to_time")
|
|
2598
|
+
if from_time:
|
|
2599
|
+
input_data["from_time"] = from_time.isoformat()
|
|
2600
|
+
if to_time:
|
|
2601
|
+
input_data["to_time"] = to_time.isoformat()
|
|
2602
|
+
|
|
2603
|
+
response = self.execute_graphql(
|
|
2604
|
+
query=query.render(),
|
|
2605
|
+
branch_name=branch,
|
|
2606
|
+
timeout=timeout,
|
|
2607
|
+
tracker=tracker,
|
|
2608
|
+
variables=input_data,
|
|
2609
|
+
)
|
|
2610
|
+
|
|
2611
|
+
diff_tree = response["DiffTree"]
|
|
2612
|
+
if diff_tree is None:
|
|
2613
|
+
return None
|
|
2614
|
+
|
|
2615
|
+
# Convert nodes to NodeDiff objects
|
|
2616
|
+
node_diffs: list[NodeDiff] = []
|
|
2617
|
+
if "nodes" in diff_tree:
|
|
2618
|
+
for node_dict in diff_tree["nodes"]:
|
|
2619
|
+
node_diff = diff_tree_node_to_node_diff(node_dict=node_dict, branch_name=branch)
|
|
2620
|
+
node_diffs.append(node_diff)
|
|
2621
|
+
|
|
2622
|
+
return DiffTreeData(
|
|
2623
|
+
num_added=diff_tree.get("num_added") or 0,
|
|
2624
|
+
num_updated=diff_tree.get("num_updated") or 0,
|
|
2625
|
+
num_removed=diff_tree.get("num_removed") or 0,
|
|
2626
|
+
num_conflicts=diff_tree.get("num_conflicts") or 0,
|
|
2627
|
+
to_time=diff_tree["to_time"],
|
|
2628
|
+
from_time=diff_tree["from_time"],
|
|
2629
|
+
base_branch=diff_tree["base_branch"],
|
|
2630
|
+
diff_branch=diff_tree["diff_branch"],
|
|
2631
|
+
name=diff_tree.get("name"),
|
|
2632
|
+
nodes=node_diffs,
|
|
2633
|
+
)
|
|
2634
|
+
|
|
2523
2635
|
@overload
|
|
2524
2636
|
def allocate_next_ip_address(
|
|
2525
2637
|
self,
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# infrahub_sdk/ctl/AGENTS.md
|
|
2
|
+
|
|
3
|
+
CLI tool (`infrahubctl`) built with Typer/AsyncTyper.
|
|
4
|
+
|
|
5
|
+
## Command Pattern
|
|
6
|
+
|
|
7
|
+
```python
|
|
8
|
+
from rich.console import Console
|
|
9
|
+
from ..async_typer import AsyncTyper
|
|
10
|
+
from ..ctl.client import initialize_client
|
|
11
|
+
from ..ctl.utils import catch_exception
|
|
12
|
+
from .parameters import CONFIG_PARAM
|
|
13
|
+
|
|
14
|
+
console = Console()
|
|
15
|
+
app = AsyncTyper()
|
|
16
|
+
|
|
17
|
+
@app.command(name="my-command")
|
|
18
|
+
@catch_exception(console=console)
|
|
19
|
+
async def my_command(
|
|
20
|
+
path: str = typer.Option(".", help="Path to file"),
|
|
21
|
+
branch: Optional[str] = None,
|
|
22
|
+
_: str = CONFIG_PARAM, # Always include, even if unused
|
|
23
|
+
):
|
|
24
|
+
client = initialize_client(branch=branch)
|
|
25
|
+
# implementation using Rich for output
|
|
26
|
+
console.print(Panel("Result", title="Success"))
|
|
27
|
+
```
|
|
28
|
+
|
|
29
|
+
## File Organization
|
|
30
|
+
|
|
31
|
+
```text
|
|
32
|
+
infrahub_sdk/ctl/
|
|
33
|
+
├── cli_commands.py # Entry point, registers subcommands
|
|
34
|
+
├── client.py # initialize_client(), initialize_client_sync()
|
|
35
|
+
├── utils.py # catch_exception decorator, parse_cli_vars
|
|
36
|
+
├── parameters.py # CONFIG_PARAM and shared parameters
|
|
37
|
+
├── branch.py # Branch subcommands
|
|
38
|
+
├── schema.py # Schema subcommands
|
|
39
|
+
└── object.py # Object subcommands
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## Registering Commands
|
|
43
|
+
|
|
44
|
+
```python
|
|
45
|
+
# In cli_commands.py
|
|
46
|
+
from ..ctl.branch import app as branch_app
|
|
47
|
+
app.add_typer(branch_app, name="branch")
|
|
48
|
+
|
|
49
|
+
# Or for top-level commands
|
|
50
|
+
from .exporter import dump
|
|
51
|
+
app.command(name="dump")(dump)
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
## Boundaries
|
|
55
|
+
|
|
56
|
+
✅ **Always**
|
|
57
|
+
|
|
58
|
+
- Use `@catch_exception(console=console)` decorator
|
|
59
|
+
- Include `CONFIG_PARAM` in all commands
|
|
60
|
+
- Use `initialize_client()` for client creation
|
|
61
|
+
- Use Rich for output (tables, panels, console.print)
|
|
62
|
+
|
|
63
|
+
🚫 **Never**
|
|
64
|
+
|
|
65
|
+
- Use plain `print()` statements
|
|
66
|
+
- Instantiate `InfrahubClient` directly (use `initialize_client`)
|
|
67
|
+
- Forget error handling decorator
|