infrahub-server 1.4.9__py3-none-any.whl → 1.5.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.
Files changed (103) hide show
  1. infrahub/actions/tasks.py +200 -16
  2. infrahub/api/artifact.py +3 -0
  3. infrahub/api/query.py +2 -0
  4. infrahub/api/schema.py +3 -0
  5. infrahub/auth.py +5 -5
  6. infrahub/cli/db.py +2 -2
  7. infrahub/config.py +7 -2
  8. infrahub/core/attribute.py +22 -19
  9. infrahub/core/branch/models.py +2 -2
  10. infrahub/core/branch/needs_rebase_status.py +11 -0
  11. infrahub/core/branch/tasks.py +2 -2
  12. infrahub/core/constants/__init__.py +1 -0
  13. infrahub/core/convert_object_type/object_conversion.py +201 -0
  14. infrahub/core/convert_object_type/repository_conversion.py +89 -0
  15. infrahub/core/convert_object_type/schema_mapping.py +27 -3
  16. infrahub/core/diff/query/artifact.py +12 -9
  17. infrahub/core/graph/__init__.py +1 -1
  18. infrahub/core/initialization.py +2 -2
  19. infrahub/core/manager.py +3 -81
  20. infrahub/core/migrations/graph/__init__.py +2 -0
  21. infrahub/core/migrations/graph/m040_profile_attrs_in_db.py +166 -0
  22. infrahub/core/node/__init__.py +26 -3
  23. infrahub/core/node/create.py +79 -38
  24. infrahub/core/node/lock_utils.py +98 -0
  25. infrahub/core/property.py +11 -0
  26. infrahub/core/protocols.py +1 -0
  27. infrahub/core/query/attribute.py +27 -15
  28. infrahub/core/query/node.py +47 -184
  29. infrahub/core/query/relationship.py +43 -26
  30. infrahub/core/query/subquery.py +0 -8
  31. infrahub/core/relationship/model.py +59 -19
  32. infrahub/core/schema/attribute_schema.py +0 -2
  33. infrahub/core/schema/definitions/core/repository.py +7 -0
  34. infrahub/core/schema/relationship_schema.py +0 -1
  35. infrahub/core/schema/schema_branch.py +3 -2
  36. infrahub/generators/models.py +31 -12
  37. infrahub/generators/tasks.py +3 -1
  38. infrahub/git/base.py +38 -1
  39. infrahub/graphql/api/dependencies.py +2 -4
  40. infrahub/graphql/api/endpoints.py +2 -2
  41. infrahub/graphql/app.py +2 -4
  42. infrahub/graphql/initialization.py +2 -3
  43. infrahub/graphql/manager.py +212 -137
  44. infrahub/graphql/middleware.py +12 -0
  45. infrahub/graphql/mutations/branch.py +11 -0
  46. infrahub/graphql/mutations/computed_attribute.py +110 -3
  47. infrahub/graphql/mutations/convert_object_type.py +34 -13
  48. infrahub/graphql/mutations/ipam.py +21 -8
  49. infrahub/graphql/mutations/main.py +37 -153
  50. infrahub/graphql/mutations/profile.py +195 -0
  51. infrahub/graphql/mutations/proposed_change.py +2 -1
  52. infrahub/graphql/mutations/repository.py +22 -83
  53. infrahub/graphql/mutations/webhook.py +1 -1
  54. infrahub/graphql/registry.py +173 -0
  55. infrahub/graphql/schema.py +4 -1
  56. infrahub/lock.py +52 -26
  57. infrahub/locks/__init__.py +0 -0
  58. infrahub/locks/tasks.py +37 -0
  59. infrahub/patch/plan_writer.py +2 -2
  60. infrahub/profiles/__init__.py +0 -0
  61. infrahub/profiles/node_applier.py +101 -0
  62. infrahub/profiles/queries/__init__.py +0 -0
  63. infrahub/profiles/queries/get_profile_data.py +99 -0
  64. infrahub/profiles/tasks.py +63 -0
  65. infrahub/repositories/__init__.py +0 -0
  66. infrahub/repositories/create_repository.py +113 -0
  67. infrahub/tasks/registry.py +6 -4
  68. infrahub/webhook/models.py +1 -1
  69. infrahub/workflows/catalogue.py +38 -3
  70. infrahub/workflows/models.py +17 -2
  71. infrahub_sdk/branch.py +5 -8
  72. infrahub_sdk/client.py +364 -84
  73. infrahub_sdk/convert_object_type.py +61 -0
  74. infrahub_sdk/ctl/check.py +2 -3
  75. infrahub_sdk/ctl/cli_commands.py +16 -12
  76. infrahub_sdk/ctl/config.py +8 -2
  77. infrahub_sdk/ctl/generator.py +2 -3
  78. infrahub_sdk/ctl/repository.py +39 -1
  79. infrahub_sdk/ctl/schema.py +12 -1
  80. infrahub_sdk/ctl/utils.py +4 -0
  81. infrahub_sdk/ctl/validate.py +5 -3
  82. infrahub_sdk/diff.py +4 -5
  83. infrahub_sdk/exceptions.py +2 -0
  84. infrahub_sdk/graphql.py +7 -2
  85. infrahub_sdk/node/attribute.py +2 -0
  86. infrahub_sdk/node/node.py +28 -20
  87. infrahub_sdk/playback.py +1 -2
  88. infrahub_sdk/protocols.py +40 -6
  89. infrahub_sdk/pytest_plugin/plugin.py +7 -4
  90. infrahub_sdk/pytest_plugin/utils.py +40 -0
  91. infrahub_sdk/repository.py +1 -2
  92. infrahub_sdk/schema/main.py +1 -0
  93. infrahub_sdk/spec/object.py +43 -4
  94. infrahub_sdk/spec/range_expansion.py +118 -0
  95. infrahub_sdk/timestamp.py +18 -6
  96. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/METADATA +20 -24
  97. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/RECORD +102 -84
  98. infrahub_testcontainers/models.py +2 -2
  99. infrahub_testcontainers/performance_test.py +4 -4
  100. infrahub/core/convert_object_type/conversion.py +0 -134
  101. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/LICENSE.txt +0 -0
  102. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/WHEEL +0 -0
  103. {infrahub_server-1.4.9.dist-info → infrahub_server-1.5.0b0.dist-info}/entry_points.txt +0 -0
@@ -1,5 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import hashlib
3
4
  from dataclasses import dataclass
4
5
  from typing import TYPE_CHECKING, Any, Iterable
5
6
 
@@ -17,9 +18,9 @@ from infrahub.core.schema import (
17
18
  RelationshipSchema,
18
19
  TemplateSchema,
19
20
  )
20
- from infrahub.core.timestamp import Timestamp
21
21
  from infrahub.graphql.mutations.attribute import BaseAttributeCreate, BaseAttributeUpdate
22
22
  from infrahub.graphql.mutations.graphql_query import InfrahubGraphQLQueryMutation
23
+ from infrahub.graphql.mutations.profile import InfrahubProfileMutation
23
24
  from infrahub.types import ATTRIBUTE_TYPES, InfrahubDataType, get_attribute_type
24
25
 
25
26
  from .directives import DIRECTIVES
@@ -40,6 +41,7 @@ from .mutations.resource_manager import (
40
41
  InfrahubNumberPoolMutation,
41
42
  )
42
43
  from .mutations.webhook import InfrahubWebhookMutation
44
+ from .registry import registry
43
45
  from .resolvers.ipam import ipam_paginated_list_resolver
44
46
  from .resolvers.resolver import (
45
47
  account_resolver,
@@ -69,7 +71,6 @@ from .types.event import EVENT_TYPES
69
71
  if TYPE_CHECKING:
70
72
  from graphql import GraphQLSchema
71
73
 
72
- from infrahub.core.branch import Branch
73
74
  from infrahub.core.schema.schema_branch import SchemaBranch
74
75
 
75
76
 
@@ -93,81 +94,34 @@ class GraphqlMutations:
93
94
  delete: type[InfrahubMutation]
94
95
 
95
96
 
96
- def get_attr_kind(node_schema: MainSchemaTypes, attr_schema: AttributeSchema) -> str:
97
- if not config.SETTINGS.experimental_features.graphql_enums or not attr_schema.enum:
98
- return attr_schema.kind
99
- return get_enum_attribute_type_name(node_schema=node_schema, attr_schema=attr_schema)
97
+ @dataclass
98
+ class InterfaceReference:
99
+ reference: type[graphene.Interface]
100
+ reference_hash: str
100
101
 
101
102
 
102
103
  @dataclass
103
- class BranchDetails:
104
- branch_name: str
105
- schema_changed_at: Timestamp
106
- schema_hash: str
107
- gql_manager: GraphQLSchemaManager
104
+ class InfrahubObjectReference:
105
+ reference: type[InfrahubObject]
106
+ reference_hash: str
108
107
 
109
108
 
110
- class GraphQLSchemaManager:
111
- _extra_types: dict[str, GraphQLTypes] = {}
112
- _branch_details_by_name: dict[str, BranchDetails] = {}
113
-
114
- @classmethod
115
- def clear_cache(cls) -> None:
116
- cls._branch_details_by_name = {}
117
-
118
- @classmethod
119
- def purge_inactive(cls, active_branches: list[str]) -> set[str]:
120
- """Return inactive branches that were purged"""
121
- inactive_branches: set[str] = set()
122
- for branch_name in list(cls._branch_details_by_name):
123
- if branch_name not in active_branches:
124
- inactive_branches.add(branch_name)
125
- del cls._branch_details_by_name[branch_name]
126
- return inactive_branches
127
-
128
- @classmethod
129
- def _cache_branch(
130
- cls, branch: Branch, schema_branch: SchemaBranch, schema_hash: str | None = None
131
- ) -> BranchDetails:
132
- if not schema_hash:
133
- if branch.schema_hash:
134
- schema_hash = branch.schema_hash.main
135
- else:
136
- schema_hash = schema_branch.get_hash()
137
- branch_details = BranchDetails(
138
- branch_name=branch.name,
139
- schema_changed_at=Timestamp(branch.schema_changed_at) if branch.schema_changed_at else Timestamp(),
140
- schema_hash=schema_hash,
141
- gql_manager=cls(schema=schema_branch),
142
- )
143
- cls._branch_details_by_name[branch.name] = branch_details
144
- return branch_details
145
-
146
- @classmethod
147
- def get_manager_for_branch(cls, branch: Branch, schema_branch: SchemaBranch) -> GraphQLSchemaManager:
148
- if branch.name not in cls._branch_details_by_name:
149
- branch_details = cls._cache_branch(branch=branch, schema_branch=schema_branch)
150
- return branch_details.gql_manager
151
- cached_branch_details = cls._branch_details_by_name[branch.name]
152
- # try to use the schema_changed_at time b/c it is faster than checking the hash
153
- if branch.schema_changed_at:
154
- changed_at_time = Timestamp(branch.schema_changed_at)
155
- if changed_at_time > cached_branch_details.schema_changed_at:
156
- cached_branch_details = cls._cache_branch(branch=branch, schema_branch=schema_branch)
157
- return cached_branch_details.gql_manager
158
- if branch.schema_hash:
159
- current_hash = branch.active_schema_hash.main
160
- else:
161
- current_hash = schema_branch.get_hash()
162
- if cached_branch_details.schema_hash != current_hash:
163
- cached_branch_details = cls._cache_branch(
164
- branch=branch, schema_branch=schema_branch, schema_hash=current_hash
165
- )
109
+ @dataclass
110
+ class InfrahubEdgedReference:
111
+ reference: type[InfrahubObject]
112
+ reference_hash: str
113
+
114
+
115
+ def get_attr_kind(node_schema: MainSchemaTypes, attr_schema: AttributeSchema) -> str:
116
+ if not config.SETTINGS.experimental_features.graphql_enums or not attr_schema.enum:
117
+ return attr_schema.kind
118
+ return get_enum_attribute_type_name(node_schema=node_schema, attr_schema=attr_schema)
166
119
 
167
- return cached_branch_details.gql_manager
168
120
 
121
+ class GraphQLSchemaManager:
169
122
  def __init__(self, schema: SchemaBranch) -> None:
170
123
  self.schema = schema
124
+ self.schema_hash = schema.get_hash()
171
125
 
172
126
  self._full_graphql_schema: GraphQLSchema | None = None
173
127
  self._graphql_types: dict[str, GraphQLTypes] = {}
@@ -275,9 +229,7 @@ class GraphQLSchemaManager:
275
229
  raise ValueError(f"Unable to find {name!r}")
276
230
 
277
231
  def get_all(self) -> dict[str, GraphQLTypes]:
278
- infrahub_types = self._graphql_types
279
- infrahub_types.update(self._extra_types)
280
- return infrahub_types
232
+ return self._graphql_types
281
233
 
282
234
  def set_type(self, name: str, graphql_type: GraphQLTypes) -> None:
283
235
  self._graphql_types[name] = graphql_type
@@ -342,7 +294,9 @@ class GraphQLSchemaManager:
342
294
  )
343
295
  ATTRIBUTE_TYPES[base_enum_name] = data_type_class
344
296
 
345
- def _get_related_input_type(self, relationship: RelationshipSchema) -> type[RelatedNodeInput]:
297
+ def _get_related_input_type(
298
+ self, relationship: RelationshipSchema
299
+ ) -> type[RelatedNodeInput | RelatedIPPrefixNodeInput | RelatedIPAddressNodeInput]:
346
300
  peer_schema = self.schema.get(name=relationship.peer, duplicate=False)
347
301
  if peer_schema.is_ip_prefix:
348
302
  return RelatedIPPrefixNodeInput
@@ -400,12 +354,15 @@ class GraphQLSchemaManager:
400
354
  # Generate all GraphQL ObjectType, Nested, Paginated & NestedPaginated and store them in the registry
401
355
  for node_schema in full_schema.values():
402
356
  if isinstance(node_schema, NodeSchema | ProfileSchema | TemplateSchema):
403
- node_type = self.generate_graphql_object(schema=node_schema, populate_cache=True)
357
+ node_object_type = self.generate_graphql_object(schema=node_schema, populate_cache=True)
404
358
  node_type_edged = self.generate_graphql_edged_object(
405
- schema=node_schema, node=node_type, populate_cache=True
359
+ schema=node_schema, node=node_object_type, populate_cache=True
406
360
  )
407
361
  nested_node_type_edged = self.generate_graphql_edged_object(
408
- schema=node_schema, node=node_type, relation_property=relationship_property, populate_cache=True
362
+ schema=node_schema,
363
+ node=node_object_type,
364
+ relation_property=relationship_property,
365
+ populate_cache=True,
409
366
  )
410
367
 
411
368
  self.generate_graphql_paginated_object(schema=node_schema, edge=node_type_edged, populate_cache=True)
@@ -533,6 +490,8 @@ class GraphQLSchemaManager:
533
490
  base_class = InfrahubIPPrefixMutation
534
491
  elif isinstance(node_schema, NodeSchema) and node_schema.is_ip_address:
535
492
  base_class = InfrahubIPAddressMutation
493
+ elif isinstance(node_schema, ProfileSchema):
494
+ base_class = InfrahubProfileMutation
536
495
  else:
537
496
  base_class = mutation_map.get(node_schema.kind, InfrahubMutation)
538
497
 
@@ -557,13 +516,15 @@ class GraphQLSchemaManager:
557
516
 
558
517
  return type("MutationMixin", (object,), class_attrs)
559
518
 
560
- def generate_graphql_object(self, schema: MainSchemaTypes, populate_cache: bool = False) -> type[InfrahubObject]:
519
+ def generate_graphql_object(self, schema: MainSchemaTypes, populate_cache: bool = False) -> InfrahubObjectReference:
561
520
  """Generate a GraphQL object Type from a Infrahub NodeSchema."""
562
521
 
563
522
  interfaces: set[type[InfrahubObject]] = set()
523
+ md5hash = hashlib.md5(usedforsecurity=False)
524
+ md5hash.update(f"{schema.kind}{schema.get_hash()}".encode())
564
525
 
565
526
  if isinstance(schema, NodeSchema | ProfileSchema | TemplateSchema) and schema.inherit_from:
566
- for generic_name in schema.inherit_from:
527
+ for generic_name in sorted(schema.inherit_from):
567
528
  generic = self.get_type(name=generic_name)
568
529
  interfaces.add(generic)
569
530
 
@@ -597,21 +558,26 @@ class GraphQLSchemaManager:
597
558
  req = "" if attr.optional else " (required)"
598
559
  main_attrs[attr.name] = graphene.Field(attr_type, description=f"{attr.description}{req}")
599
560
 
561
+ object_hash = md5hash.hexdigest()
562
+
600
563
  graphql_object = type(schema.kind, (InfrahubObject,), main_attrs)
564
+ registry.set_object_type(reference=graphql_object, reference_hash=object_hash, schema_hash=self.schema_hash)
601
565
 
602
566
  if populate_cache:
603
567
  self.set_type(name=schema.kind, graphql_type=graphql_object)
604
568
 
605
- return graphql_object
569
+ return InfrahubObjectReference(reference=graphql_object, reference_hash=object_hash)
606
570
 
607
- def generate_interface_object(
608
- self, schema: GenericSchema, populate_cache: bool = False
609
- ) -> type[graphene.Interface]:
571
+ def generate_interface_object(self, schema: GenericSchema, populate_cache: bool = False) -> InterfaceReference:
610
572
  meta_attrs = {
611
573
  "name": schema.kind,
612
574
  "description": schema.description,
613
575
  }
614
576
 
577
+ md5hash = hashlib.md5(usedforsecurity=False)
578
+ md5hash.update(f"interface-{schema.kind}{schema.get_hash()}".encode())
579
+ interface_hash = md5hash.hexdigest()
580
+
615
581
  main_attrs = {
616
582
  "id": graphene.Field(graphene.String, required=False, description="Unique identifier"),
617
583
  "hfid": graphene.Field(
@@ -620,7 +586,6 @@ class GraphQLSchemaManager:
620
586
  description="Human friendly identifier",
621
587
  ),
622
588
  "display_label": graphene.String(required=False),
623
- "Meta": type("Meta", (object,), meta_attrs),
624
589
  }
625
590
 
626
591
  for attr in schema.attributes:
@@ -628,12 +593,18 @@ class GraphQLSchemaManager:
628
593
  attr_type = self.get_type(name=get_attribute_type(kind=attr_kind).get_graphql_type_name())
629
594
  main_attrs[attr.name] = graphene.Field(attr_type, description=attr.description)
630
595
 
631
- interface_object = type(schema.kind, (InfrahubInterface,), main_attrs)
596
+ interface_object = registry.get_interface_type(reference_hash=interface_hash, schema_hash=self.schema_hash)
597
+ if not interface_object:
598
+ main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
599
+ interface_object = type(schema.kind, (InfrahubInterface,), main_attrs)
600
+
601
+ interface_reference = InterfaceReference(reference=interface_object, reference_hash=interface_hash)
602
+ registry.set_interface_type(reference=interface_reference, schema_hash=self.schema_hash)
632
603
 
633
604
  if populate_cache:
634
605
  self.set_type(name=schema.kind, graphql_type=interface_object)
635
606
 
636
- return interface_object
607
+ return interface_reference
637
608
 
638
609
  def define_relationship_property(self, data_source: type[InfrahubObject], data_owner: type[InfrahubObject]) -> None:
639
610
  type_name = "RelationshipProperty"
@@ -721,7 +692,18 @@ class GraphQLSchemaManager:
721
692
  elif rel.cardinality == RelationshipCardinality.MANY:
722
693
  attrs[rel.name] = graphene.InputField(graphene.List(input_type), description=rel.description)
723
694
 
724
- return type(f"{schema.kind}CreateInput", (graphene.InputObjectType,), attrs)
695
+ input_name = f"{schema.kind}CreateInput"
696
+ md5hash = hashlib.md5(usedforsecurity=False)
697
+ md5hash.update(f"{input_name}{schema.get_hash()}".encode())
698
+ input_hash = md5hash.hexdigest()
699
+ mutation_input_type = registry.get_input_type(reference_hash=input_hash, schema_hash=self.schema_hash)
700
+ if not mutation_input_type:
701
+ mutation_input_type = type(input_name, (graphene.InputObjectType,), attrs)
702
+ registry.set_input_type(
703
+ reference=mutation_input_type, reference_hash=input_hash, schema_hash=self.schema_hash
704
+ )
705
+
706
+ return mutation_input_type
725
707
 
726
708
  def generate_graphql_mutation_update_input(self, schema: MainSchemaTypes) -> type[graphene.InputObjectType]:
727
709
  """Generate an InputObjectType Object from a Infrahub NodeSchema
@@ -734,7 +716,7 @@ class GraphQLSchemaManager:
734
716
  slug = InputField(StringAttributeUpdate, required=False)
735
717
  description = InputField(StringAttributeUpdate, required=False)
736
718
  """
737
- attrs: dict[str, graphene.String | graphene.InputField] = {
719
+ attrs: dict[str, graphene.String | graphene.InputField | graphene.List] = {
738
720
  "id": graphene.String(required=False),
739
721
  "hfid": graphene.List(of_type=graphene.String, required=False),
740
722
  }
@@ -760,7 +742,18 @@ class GraphQLSchemaManager:
760
742
  graphene.List(input_type), required=False, description=rel.description
761
743
  )
762
744
 
763
- return type(f"{schema.kind}UpdateInput", (graphene.InputObjectType,), attrs)
745
+ input_name = f"{schema.kind}UpdateInput"
746
+ md5hash = hashlib.md5(usedforsecurity=False)
747
+ md5hash.update(f"{input_name}{schema.get_hash()}".encode())
748
+ input_hash = md5hash.hexdigest()
749
+ mutation_input_type = registry.get_input_type(reference_hash=input_hash, schema_hash=self.schema_hash)
750
+ if not mutation_input_type:
751
+ mutation_input_type = type(input_name, (graphene.InputObjectType,), attrs)
752
+ registry.set_input_type(
753
+ reference=mutation_input_type, reference_hash=input_hash, schema_hash=self.schema_hash
754
+ )
755
+
756
+ return mutation_input_type
764
757
 
765
758
  def generate_graphql_mutation_upsert_input(
766
759
  self, schema: NodeSchema | ProfileSchema | TemplateSchema
@@ -775,7 +768,7 @@ class GraphQLSchemaManager:
775
768
  slug = InputField(StringAttributeUpdate, required=True)
776
769
  description = InputField(StringAttributeUpdate, required=False)
777
770
  """
778
- attrs: dict[str, graphene.String | graphene.InputField] = {
771
+ attrs: dict[str, graphene.String | graphene.InputField | graphene.List] = {
779
772
  "id": graphene.String(required=False),
780
773
  "hfid": graphene.List(of_type=graphene.String, required=False),
781
774
  }
@@ -806,8 +799,18 @@ class GraphQLSchemaManager:
806
799
  attrs[rel.name] = graphene.InputField(
807
800
  graphene.List(input_type), required=required, description=rel.description
808
801
  )
802
+ input_name = f"{schema.kind}UpsertInput"
803
+ md5hash = hashlib.md5(usedforsecurity=False)
804
+ md5hash.update(f"{input_name}{schema.get_hash()}".encode())
805
+ input_hash = md5hash.hexdigest()
806
+ mutation_input_type = registry.get_input_type(reference_hash=input_hash, schema_hash=self.schema_hash)
807
+ if not mutation_input_type:
808
+ mutation_input_type = type(input_name, (graphene.InputObjectType,), attrs)
809
+ registry.set_input_type(
810
+ reference=mutation_input_type, reference_hash=input_hash, schema_hash=self.schema_hash
811
+ )
809
812
 
810
- return type(f"{schema.kind}UpsertInput", (graphene.InputObjectType,), attrs)
813
+ return mutation_input_type
811
814
 
812
815
  def generate_graphql_mutation_create(
813
816
  self,
@@ -819,17 +822,27 @@ class GraphQLSchemaManager:
819
822
  """Generate a GraphQL Mutation to CREATE an object based on the specified NodeSchema."""
820
823
  name = f"{schema.kind}{mutation_type}"
821
824
 
822
- object_type = self.generate_graphql_object(schema=schema)
825
+ md5hash = hashlib.md5(usedforsecurity=False)
826
+ md5hash.update(f"{name}{schema.get_hash()}".encode())
827
+ mutation_hash = md5hash.hexdigest()
823
828
 
824
- main_attrs: dict[str, Any] = {"ok": graphene.Boolean(), "object": graphene.Field(object_type)}
829
+ mutation_object = registry.get_mutation_type(reference_hash=mutation_hash, schema_hash=self.schema_hash)
830
+ if not mutation_object:
831
+ object_type = self.generate_graphql_object(schema=schema)
825
832
 
826
- meta_attrs: dict[str, Any] = {"schema": schema, "name": name, "description": schema.description}
827
- main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
833
+ main_attrs: dict[str, Any] = {"ok": graphene.Boolean(), "object": graphene.Field(object_type.reference)}
828
834
 
829
- args_attrs = {"data": input_type(required=True), "context": ContextInput(required=False)}
830
- main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
835
+ meta_attrs: dict[str, Any] = {"schema": schema, "name": name, "description": schema.description}
836
+ main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
831
837
 
832
- return type(name, (base_class,), main_attrs)
838
+ args_attrs = {"data": input_type(required=True), "context": ContextInput(required=False)}
839
+ main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
840
+ mutation_object = type(name, (base_class,), main_attrs)
841
+ registry.set_mutation_type(
842
+ reference=mutation_object, reference_hash=mutation_hash, schema_hash=self.schema_hash
843
+ )
844
+
845
+ return mutation_object
833
846
 
834
847
  def generate_graphql_mutation_update(
835
848
  self,
@@ -840,34 +853,50 @@ class GraphQLSchemaManager:
840
853
  """Generate a GraphQL Mutation to UPDATE an object based on the specified NodeSchema."""
841
854
  name = f"{schema.kind}Update"
842
855
 
843
- object_type = self.generate_graphql_object(schema=schema)
856
+ md5hash = hashlib.md5(usedforsecurity=False)
857
+ md5hash.update(f"{name}{schema.get_hash()}".encode())
858
+ mutation_hash = md5hash.hexdigest()
859
+
860
+ mutation_object = registry.get_mutation_type(reference_hash=mutation_hash, schema_hash=self.schema_hash)
861
+ if not mutation_object:
862
+ object_type = self.generate_graphql_object(schema=schema)
863
+
864
+ main_attrs: dict[str, Any] = {"ok": graphene.Boolean(), "object": graphene.Field(object_type.reference)}
844
865
 
845
- main_attrs: dict[str, Any] = {"ok": graphene.Boolean(), "object": graphene.Field(object_type)}
866
+ meta_attrs: dict[str, Any] = {"schema": schema, "name": name, "description": schema.description}
867
+ main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
846
868
 
847
- meta_attrs: dict[str, Any] = {"schema": schema, "name": name, "description": schema.description}
848
- main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
869
+ args_attrs = {"data": input_type(required=True), "context": ContextInput(required=False)}
870
+ main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
849
871
 
850
- args_attrs = {"data": input_type(required=True), "context": ContextInput(required=False)}
851
- main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
872
+ mutation_object = type(name, (base_class,), main_attrs)
873
+ registry.set_mutation_type(
874
+ reference=mutation_object, reference_hash=mutation_hash, schema_hash=self.schema_hash
875
+ )
852
876
 
853
- return type(name, (base_class,), main_attrs)
877
+ return mutation_object
854
878
 
855
- @staticmethod
856
879
  def generate_graphql_mutation_delete(
857
- schema: NodeSchema | ProfileSchema | TemplateSchema, base_class: type[InfrahubMutation] = InfrahubMutation
880
+ self, schema: NodeSchema | ProfileSchema | TemplateSchema, base_class: type[InfrahubMutation] = InfrahubMutation
858
881
  ) -> type[InfrahubMutation]:
859
882
  """Generate a GraphQL Mutation to DELETE an object based on the specified NodeSchema."""
860
883
  name = f"{schema.kind}Delete"
884
+ md5hash = hashlib.md5(usedforsecurity=False)
885
+ md5hash.update(f"{name}{schema.get_hash()}".encode())
886
+ mutation_hash = md5hash.hexdigest()
887
+ mutation_object = registry.get_mutation_type(reference_hash=mutation_hash, schema_hash=self.schema_hash)
888
+ if not mutation_object:
889
+ main_attrs: dict[str, Any] = {"ok": graphene.Boolean()}
890
+ meta_attrs = {"schema": schema, "name": name, "description": schema.description}
891
+ main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
892
+ args_attrs: dict[str, Any] = {"data": DeleteInput(required=True), "context": ContextInput(required=False)}
893
+ main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
894
+ mutation_object = type(name, (base_class,), main_attrs)
895
+ registry.set_mutation_type(
896
+ reference=mutation_object, reference_hash=mutation_hash, schema_hash=self.schema_hash
897
+ )
861
898
 
862
- main_attrs: dict[str, Any] = {"ok": graphene.Boolean()}
863
-
864
- meta_attrs = {"schema": schema, "name": name, "description": schema.description}
865
- main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
866
-
867
- args_attrs: dict[str, Any] = {"data": DeleteInput(required=True), "context": ContextInput(required=False)}
868
- main_attrs["Arguments"] = type("Arguments", (object,), args_attrs)
869
-
870
- return type(name, (base_class,), main_attrs)
899
+ return mutation_object
871
900
 
872
901
  def generate_filters(
873
902
  self, schema: MainSchemaTypes, top_level: bool = False, include_properties: bool = True
@@ -940,16 +969,20 @@ class GraphQLSchemaManager:
940
969
  def generate_graphql_edged_object(
941
970
  self,
942
971
  schema: MainSchemaTypes,
943
- node: type[InfrahubObject],
972
+ node: InterfaceReference | InfrahubObjectReference,
944
973
  relation_property: type[InfrahubObject] | None = None,
945
974
  populate_cache: bool = False,
946
- ) -> type[InfrahubObject]:
975
+ ) -> InfrahubEdgedReference:
947
976
  """Generate a edged GraphQL object Type from a Infrahub NodeSchema for pagination."""
948
977
 
949
978
  object_name = f"Edged{schema.kind}"
950
979
  if relation_property:
951
980
  object_name = f"NestedEdged{schema.kind}"
952
981
 
982
+ md5hash = hashlib.md5(usedforsecurity=False)
983
+ md5hash.update(f"{object_name}{schema.get_hash()}{node.reference_hash}".encode())
984
+ edge_hash = md5hash.hexdigest()
985
+
953
986
  meta_attrs: dict[str, Any] = {
954
987
  "schema": schema,
955
988
  "name": object_name,
@@ -958,22 +991,27 @@ class GraphQLSchemaManager:
958
991
  }
959
992
 
960
993
  main_attrs: dict[str, Any] = {
961
- "node": graphene.Field(node, required=False),
994
+ "node": graphene.Field(node.reference, required=False),
962
995
  "Meta": type("Meta", (object,), meta_attrs),
963
996
  }
964
997
 
965
998
  if relation_property:
966
999
  main_attrs["properties"] = graphene.Field(relation_property, required=False)
967
1000
 
968
- graphql_edged_object = type(object_name, (InfrahubObject,), main_attrs)
1001
+ graphql_edged_object = registry.get_edge_type(reference_hash=edge_hash, schema_hash=self.schema_hash)
1002
+ if not graphql_edged_object:
1003
+ graphql_edged_object = type(object_name, (InfrahubObject,), main_attrs)
1004
+ registry.set_edge_type(
1005
+ reference=graphql_edged_object, reference_hash=edge_hash, schema_hash=self.schema_hash
1006
+ )
969
1007
 
970
1008
  if populate_cache:
971
1009
  self.set_type(name=object_name, graphql_type=graphql_edged_object)
972
1010
 
973
- return graphql_edged_object
1011
+ return InfrahubEdgedReference(reference=graphql_edged_object, reference_hash=edge_hash)
974
1012
 
975
1013
  def generate_graphql_paginated_object(
976
- self, schema: MainSchemaTypes, edge: type[InfrahubObject], nested: bool = False, populate_cache: bool = False
1014
+ self, schema: MainSchemaTypes, edge: InfrahubEdgedReference, nested: bool = False, populate_cache: bool = False
977
1015
  ) -> type[InfrahubObject]:
978
1016
  """Generate a paginated GraphQL object Type from a Infrahub NodeSchema."""
979
1017
 
@@ -981,6 +1019,10 @@ class GraphQLSchemaManager:
981
1019
  if nested:
982
1020
  object_name = f"NestedPaginated{schema.kind}"
983
1021
 
1022
+ md5hash = hashlib.md5(usedforsecurity=False)
1023
+ md5hash.update(f"{object_name}{schema.get_hash()}{edge.reference_hash}".encode())
1024
+ paginated_hash = md5hash.hexdigest()
1025
+
984
1026
  meta_attrs: dict[str, Any] = {
985
1027
  "schema": schema,
986
1028
  "name": object_name,
@@ -991,14 +1033,21 @@ class GraphQLSchemaManager:
991
1033
 
992
1034
  main_attrs: dict[str, Any] = {
993
1035
  "count": graphene.Int(required=True),
994
- "edges": graphene.List(of_type=graphene.NonNull(edge), required=True),
1036
+ "edges": graphene.List(of_type=graphene.NonNull(edge.reference), required=True),
995
1037
  "permissions": graphene.Field(
996
1038
  PaginatedObjectPermission, required=True, resolver=parent_field_name_resolver
997
1039
  ),
998
- "Meta": type("Meta", (object,), meta_attrs),
999
1040
  }
1000
1041
 
1001
- graphql_paginated_object = type(object_name, (InfrahubObject,), main_attrs)
1042
+ graphql_paginated_object = registry.get_paginated_type(
1043
+ reference_hash=paginated_hash, schema_hash=self.schema_hash
1044
+ )
1045
+ if not graphql_paginated_object:
1046
+ main_attrs["Meta"] = type("Meta", (object,), meta_attrs)
1047
+ graphql_paginated_object = type(object_name, (InfrahubObject,), main_attrs)
1048
+ registry.set_paginated_type(
1049
+ reference=graphql_paginated_object, reference_hash=paginated_hash, schema_hash=self.schema_hash
1050
+ )
1002
1051
 
1003
1052
  if populate_cache:
1004
1053
  self.set_type(name=object_name, graphql_type=graphql_paginated_object)
@@ -1028,27 +1077,53 @@ class GraphQLSchemaManager:
1028
1077
  main_attrs["properties"] = graphene.Field(relation_property, required=False)
1029
1078
 
1030
1079
  object_name = f"NestedEdged{schema.kind}"
1031
- nested_interface_object = type(object_name, (InfrahubObject,), main_attrs)
1080
+ md5hash = hashlib.md5(usedforsecurity=False)
1081
+ md5hash.update(f"{object_name}{schema.get_hash()}".encode())
1082
+ paginated_hash = md5hash.hexdigest()
1083
+ nested_interface_object = registry.get_paginated_type(
1084
+ reference_hash=paginated_hash, schema_hash=self.schema_hash
1085
+ )
1086
+ if not nested_interface_object:
1087
+ nested_interface_object = type(object_name, (InfrahubObject,), main_attrs)
1088
+ registry.set_paginated_type(
1089
+ reference=nested_interface_object, reference_hash=paginated_hash, schema_hash=self.schema_hash
1090
+ )
1032
1091
 
1033
1092
  if populate_cache:
1034
1093
  self.set_type(name=object_name, graphql_type=nested_interface_object)
1035
1094
 
1036
1095
  return nested_interface_object
1037
1096
 
1038
- @staticmethod
1039
1097
  def generate_paginated_interface_object(
1040
- schema: GenericSchema, base_interface: type[graphene.ObjectType]
1098
+ self, schema: GenericSchema, base_interface: type[graphene.ObjectType]
1041
1099
  ) -> type[InfrahubObject]:
1042
- meta_attrs: dict[str, Any] = {
1043
- "name": f"NestedPaginated{schema.kind}",
1044
- "schema": schema,
1045
- "description": schema.description,
1046
- }
1100
+ object_name = f"NestedPaginated{schema.kind}"
1101
+ md5hash = hashlib.md5(usedforsecurity=False)
1102
+ md5hash.update(f"{object_name}{schema.get_hash()}".encode())
1103
+ paginated_hash = md5hash.hexdigest()
1104
+
1105
+ nested_interface_object = registry.get_paginated_type(
1106
+ reference_hash=paginated_hash, schema_hash=self.schema_hash
1107
+ )
1108
+ if not nested_interface_object:
1109
+ meta_attrs: dict[str, Any] = {
1110
+ "name": object_name,
1111
+ "schema": schema,
1112
+ "description": schema.description,
1113
+ }
1114
+
1115
+ main_attrs: dict[str, Any] = {
1116
+ "count": graphene.Int(required=True),
1117
+ "edges": graphene.List(of_type=graphene.NonNull(base_interface)),
1118
+ "Meta": type("Meta", (object,), meta_attrs),
1119
+ }
1120
+
1121
+ nested_interface_object = type(object_name, (InfrahubObject,), main_attrs)
1122
+ registry.set_paginated_type(
1123
+ reference=nested_interface_object, reference_hash=paginated_hash, schema_hash=self.schema_hash
1124
+ )
1125
+
1126
+ return nested_interface_object
1047
1127
 
1048
- main_attrs: dict[str, Any] = {
1049
- "count": graphene.Int(required=True),
1050
- "edges": graphene.List(of_type=graphene.NonNull(base_interface)),
1051
- "Meta": type("Meta", (object,), meta_attrs),
1052
- }
1053
1128
 
1054
- return type(f"NestedPaginated{schema.kind}", (InfrahubObject,), main_attrs)
1129
+ registry._register_manager(manager=GraphQLSchemaManager)
@@ -0,0 +1,12 @@
1
+ from infrahub.core.branch.needs_rebase_status import check_need_rebase_status
2
+
3
+ ALLOWED_MUTATIONS_ON_NEED_REBASE_BRANCH = ["BranchRebase", "BranchDelete", "BranchCreate", "ProposedChangeCreate"]
4
+
5
+
6
+ def raise_on_mutation_on_branch_needing_rebase(next, root, info, **kwargs): # type: ignore # noqa
7
+ if info.operation.operation.value == "mutation":
8
+ mutation_name = info.operation.selection_set.selections[0].name.value
9
+ if mutation_name not in ALLOWED_MUTATIONS_ON_NEED_REBASE_BRANCH:
10
+ check_need_rebase_status(branch=info.context.branch)
11
+
12
+ return next(root, info, **kwargs)
@@ -7,8 +7,10 @@ from opentelemetry import trace
7
7
  from typing_extensions import Self
8
8
 
9
9
  from infrahub.branch.merge_mutation_checker import verify_branch_merge_mutation_allowed
10
+ from infrahub.core import registry
10
11
  from infrahub.core.branch import Branch
11
12
  from infrahub.database import retry_db_transaction
13
+ from infrahub.exceptions import BranchNotFoundError, ValidationError
12
14
  from infrahub.graphql.context import apply_external_context
13
15
  from infrahub.graphql.field_extractor import extract_graphql_fields
14
16
  from infrahub.graphql.types.context import ContextInput
@@ -66,12 +68,21 @@ class BranchCreate(Mutation):
66
68
  background_execution: bool = False,
67
69
  wait_until_completion: bool = True,
68
70
  ) -> Self:
71
+ if data.origin_branch and data.origin_branch != registry.default_branch:
72
+ raise ValueError(f"origin_branch must be '{registry.default_branch}'")
73
+
69
74
  graphql_context: GraphqlContext = info.context
70
75
  task: dict | None = None
71
76
 
72
77
  model = BranchCreateModel(**data)
73
78
  await apply_external_context(graphql_context=graphql_context, context_input=context)
74
79
 
80
+ try:
81
+ await Branch.get_by_name(db=graphql_context.db, name=model.name)
82
+ raise ValidationError(f"The branch {model.name} already exists")
83
+ except BranchNotFoundError:
84
+ pass
85
+
75
86
  if background_execution or not wait_until_completion:
76
87
  workflow = await graphql_context.active_service.workflow.submit_workflow(
77
88
  workflow=BRANCH_CREATE, context=graphql_context.get_context(), parameters={"model": model}