Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .devcontainer/onCreateCommand.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@ poetry install --no-interaction --no-ansi

git submodule update --init

invoke demo.pull
poetry run invoke demo.pull
2 changes: 1 addition & 1 deletion .devcontainer/postCreateCommand.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

git pull
git submodule update
invoke demo.start --wait
poetry run invoke demo.start --wait
8 changes: 4 additions & 4 deletions .devcontainer/updateContentCommand.sh
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
#!/bin/bash

export WEB_CONCURRENCY=2
invoke demo.start
poetry run invoke demo.start
sleep 120
docker logs infrahub-server-1
invoke demo.load-infra-schema
poetry run invoke demo.load-infra-schema
docker logs infrahub-server-1
sleep 90
docker logs infrahub-server-1
invoke demo.load-infra-data
invoke demo.stop
poetry run invoke demo.load-infra-data
poetry run invoke demo.stop
16 changes: 16 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,22 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang

<!-- towncrier release notes start -->

## [Infrahub - v1.4.12](https://github.com/opsmill/infrahub/tree/infrahub-v1.4.12) - 2025-10-23

### Added

- - Schema Visualizer now displays `on_delete` settings for relationships
- Fixed display of common_parent settings in relationships.

([#7431](https://github.com/opsmill/infrahub/issues/7431))

### Fixed

- Loosen requirements for upsert mutations in the GraphQL schema so that required fields can be supplied by a template. ([#7398](https://github.com/opsmill/infrahub/issues/7398))
- Fix a bug that could cause duplicated attributes to be created when updating a generic schema with a new attribute. Includes a migration to fix any existing duplicated attributes created by this bug. ([#7407](https://github.com/opsmill/infrahub/issues/7407))
- Fix bug in logic to create an object from a template that would prevent existing objects in relationships of sub-templates from being correctly linked to the created object. ([#7430](https://github.com/opsmill/infrahub/issues/7430))
- The artifact count has been removed from the Proposed Changes list view.

## [Infrahub - v1.4.11](https://github.com/opsmill/infrahub/tree/infrahub-v1.4.11) - 2025-10-17

### Added
Expand Down
21 changes: 16 additions & 5 deletions backend/infrahub/core/node/create.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,14 +57,25 @@ async def extract_peer_data(

for rel in template_peer.get_schema().relationship_names:
rel_manager: RelationshipManager = getattr(template_peer, rel)
if (
rel_manager.schema.kind not in [RelationshipKind.COMPONENT, RelationshipKind.PARENT]
or rel_manager.schema.name not in obj_peer_schema.relationship_names
):

if rel_manager.schema.name not in obj_peer_schema.relationship_names:
continue

if list(await rel_manager.get_peers(db=db)) == [current_template.id]:
peers_map = await rel_manager.get_peers(db=db)
if rel_manager.schema.kind in [RelationshipKind.COMPONENT, RelationshipKind.PARENT] and list(
peers_map.keys()
) == [current_template.id]:
obj_peer_data[rel] = {"id": parent_obj.id}
continue

rel_peer_ids = []
for peer_id, peer_object in peers_map.items():
# deeper templates are handled in the next level of recursion
if peer_object.get_schema().is_template_schema:
continue
rel_peer_ids.append({"id": peer_id})

obj_peer_data[rel] = rel_peer_ids

return obj_peer_data

Expand Down
12 changes: 3 additions & 9 deletions backend/infrahub/graphql/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,25 +787,19 @@ class StatusUpsertInput(InputObjectType):
attr_kind = get_attr_kind(schema, attr)
attr_type = get_attribute_type(kind=attr_kind).get_graphql_update()

# A Field is not required if explicitly indicated or if a default value has been provided
required = not attr.optional if not attr.default_value else False

attrs[attr.name] = graphene.InputField(attr_type, required=required, description=attr.description)
attrs[attr.name] = graphene.InputField(attr_type, description=attr.description)

for rel in schema.relationships:
if rel.internal_peer or rel.read_only:
continue

input_type = self._get_related_input_type(relationship=rel)

required = not rel.optional
if rel.cardinality == RelationshipCardinality.ONE:
attrs[rel.name] = graphene.InputField(input_type, required=required, description=rel.description)
attrs[rel.name] = graphene.InputField(input_type, description=rel.description)

elif rel.cardinality == RelationshipCardinality.MANY:
attrs[rel.name] = graphene.InputField(
graphene.List(input_type), required=required, description=rel.description
)
attrs[rel.name] = graphene.InputField(graphene.List(input_type), description=rel.description)

return type(f"{schema.kind}UpsertInput", (graphene.InputObjectType,), attrs)

Expand Down
1 change: 1 addition & 0 deletions backend/tests/helpers/schema/tshirt.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
default_filter="name__value",
display_labels=["name__value"],
uniqueness_constraints=[["name__value"]],
generate_template=True,
attributes=[
AttributeSchema(name="name", kind="Text"),
AttributeSchema(
Expand Down
98 changes: 96 additions & 2 deletions backend/tests/unit/graphql/test_mutation_create.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
from infrahub import config
from infrahub.core import registry
from infrahub.core.branch.models import Branch
from infrahub.core.constants import InfrahubKind, SchemaPathType
from infrahub.core.constants import InfrahubKind, RelationshipKind, SchemaPathType
from infrahub.core.initialization import create_branch
from infrahub.core.manager import NodeManager
from infrahub.core.migrations.schema.node_kind_update import NodeKindUpdateMigration
Expand All @@ -17,7 +17,7 @@
from infrahub.graphql.initialization import prepare_graphql_params
from tests.constants import TestKind
from tests.helpers.graphql import graphql
from tests.helpers.schema import DEVICE_SCHEMA
from tests.helpers.schema import CAR_SCHEMA, DEVICE_SCHEMA


async def test_create_simple_object(db: InfrahubDatabase, default_branch, car_person_schema):
Expand Down Expand Up @@ -1462,6 +1462,100 @@ async def test_create_with_object_template(
assert sfp.part_number.source_id is None


async def test_create_with_object_template_and_real_object(
db: InfrahubDatabase, default_branch: Branch, register_core_models_schema: SchemaBranch, branch: Branch
):
"""
Test that relationships on sub-templates will correctly link the created sub-object to an existing object on non-component relationships
"""
updated_car_schema = CAR_SCHEMA.duplicate()
manufacturer_schema = updated_car_schema.get(name=TestKind.MANUFACTURER)
manufacturer_schema.generate_template = True
cars_rel = manufacturer_schema.get_relationship(name="cars")
cars_rel.kind = RelationshipKind.COMPONENT
person_schema = updated_car_schema.get(name=TestKind.PERSON)
person_schema.generate_template = True
car_schema = updated_car_schema.get(name=TestKind.CAR)
car_schema.generate_template = True
manufacturer_rel = car_schema.get_relationship(name="manufacturer")
manufacturer_rel.kind = RelationshipKind.PARENT
registry.schema.register_schema(schema=updated_car_schema, branch=branch.name)

manufacturer_object = await Node.init(schema=TestKind.MANUFACTURER, db=db, branch=branch)
await manufacturer_object.new(db=db, name="Hark Motors")
await manufacturer_object.save(db=db)

person_object = await Node.init(schema=TestKind.PERSON, db=db, branch=branch)
await person_object.new(db=db, name="John", height=180)
await person_object.save(db=db)

car_object = await Node.init(schema=TestKind.CAR, db=db, branch=branch)
await car_object.new(db=db, name="Accord", manufacturer=manufacturer_object, owner=person_object, color="blurple")
await car_object.save(db=db)

manufacturer_template: Node = await Node.init(schema=f"Template{TestKind.MANUFACTURER}", db=db, branch=branch)
await manufacturer_template.new(db=db, template_name="m_template", customers=[person_object])
await manufacturer_template.save(db=db)

car_template_with_person_object = await Node.init(schema=f"Template{TestKind.CAR}", db=db, branch=branch)
await car_template_with_person_object.new(
db=db,
template_name="c_template",
name="Civic",
color="blurple",
manufacturer=manufacturer_template,
owner=person_object,
)
await car_template_with_person_object.save(db=db)

create_manufacturer_with_template_query = """
mutation CreateManufacturerWithTemplate($manufacturer_name: String!, $template_id: String!) {
TestingManufacturerCreate(data: {
name: {value: $manufacturer_name}
object_template: {id: $template_id}
}) {
ok
object {
id
}
}
}
"""
gql_params = await prepare_graphql_params(db=db, branch=branch)
result = await graphql(
schema=gql_params.schema,
source=create_manufacturer_with_template_query,
context_value=gql_params.context,
variable_values={"manufacturer_name": "Fresh Motors", "template_id": manufacturer_template.id},
)
assert not result.errors
new_manufacturer = await NodeManager.get_one(
db=db,
kind=TestKind.MANUFACTURER,
branch=branch,
id=result.data[f"{TestKind.MANUFACTURER}Create"]["object"]["id"],
)
assert new_manufacturer
assert new_manufacturer.name.value == "Fresh Motors"
customers_peers = await new_manufacturer.customers.get_peers(db=db)
assert len(customers_peers) == 1
customers_by_name = {person.name.value: person for person in customers_peers.values()}
# check non-template person
non_template_person = customers_by_name["John"]
assert non_template_person.id == person_object.id

cars_peers = await new_manufacturer.cars.get_peers(db=db)
assert len(cars_peers) == 1
cars_by_name = {car.name.value: car for car in cars_peers.values()}
# check car template with person object
car_template_with_person_object = cars_by_name["Civic"]
assert car_template_with_person_object.color.value == "blurple"
car_manufacturer = await car_template_with_person_object.manufacturer.get_peer(db=db)
assert car_manufacturer.id == new_manufacturer.id
car_owner = await car_template_with_person_object.owner.get_peer(db=db)
assert car_owner.id == person_object.id


async def test_create_without_object_template(
db: InfrahubDatabase, default_branch: Branch, register_core_models_schema: SchemaBranch, branch: Branch
):
Expand Down
81 changes: 80 additions & 1 deletion backend/tests/unit/graphql/test_mutation_upsert.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from tests.adapters.event import MemoryInfrahubEvent
from tests.constants import TestKind
from tests.helpers.graphql import graphql
from tests.helpers.schema import TICKET
from tests.helpers.schema import COLOR, TICKET, TSHIRT
from tests.node_creation import create_and_save


Expand Down Expand Up @@ -673,3 +673,82 @@ async def test_upsert_node_on_branch_with_hfid_on_default(db: InfrahubDatabase,
in result.errors[0].message
)
assert f"Please rebase this branch to access {person.id} / TestPerson" in result.errors[0].message


async def test_upsert_with_required_relationship_from_template(
db: InfrahubDatabase, default_branch: Branch, register_core_models_schema: None
) -> None:
"""Validate that we can use a template to populate required relationships in upsert mutations.

Steps:
- Create a color node and a Tshirt template node.
- Try to upsert a Tshirt without specifying color or template (should fail).
- Upsert a Tshirt specifying the template (should succeed and apply the color from the template).
"""
registry.schema.register_schema(schema=SchemaRoot(nodes=[TSHIRT, COLOR]), branch=default_branch.name)

# Create a color node
color_node = await Node.init(db=db, schema="TestingColor", branch=default_branch)
await color_node.new(db=db, name="Red", description="Bright Red Color")
await color_node.save(db=db)

# Create a Tshirt template node with the color relationship set
template_node = await Node.init(db=db, schema="TemplateTestingTShirt", branch=default_branch)
await template_node.new(db=db, template_name="Basic Red Tshirt", color=color_node)
await template_node.save(db=db)

# Try to upsert a TShirt without specifying color or template (should fail)
query_missing_required = """
mutation {
TestingTShirtUpsert(data: {name: {value: "My Shirt"} }) {
ok
object {
id
name { value }
color { node { id name { value } } }
}
}
}
"""
gql_params = await prepare_graphql_params(db=db, include_subscription=False, branch=default_branch)
result_missing = await graphql(
schema=gql_params.schema,
source=query_missing_required,
context_value=gql_params.context,
root_value=None,
variable_values={},
)
assert result_missing.errors
assert "color is mandatory for TestingTShirt at color" in str(result_missing.errors)

# Upsert a Tshirt specifying the template (should succeed and apply the color from the template)
query_with_template = """
mutation UpsertTShirt($template_id: String!) {
TestingTShirtUpsert(data: {
name: {value: "My Tshirt"},
object_template: {id: $template_id}
}) {
ok
object {
id
name { value }
color { node { id name { value } } }
}
}
}
"""

result_with_template = await graphql(
schema=gql_params.schema,
source=query_with_template,
context_value=gql_params.context,
root_value=None,
variable_values={"template_id": template_node.id},
)
assert result_with_template.errors is None
assert result_with_template.data
assert result_with_template.data["TestingTShirtUpsert"]["ok"] is True
tshirt_obj = result_with_template.data["TestingTShirtUpsert"]["object"]
assert tshirt_obj["name"]["value"] == "My Tshirt"
assert tshirt_obj["color"]["node"]["id"] == color_node.id
assert tshirt_obj["color"]["node"]["name"]["value"] == "Red"
1 change: 0 additions & 1 deletion changelog/+artifact.fixed.md

This file was deleted.

1 change: 1 addition & 0 deletions changelog/+checks.fixed.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Resolved a problem that caused generator checks to fail when retrying requests
1 change: 0 additions & 1 deletion changelog/7407.fixed.md

This file was deleted.

2 changes: 0 additions & 2 deletions changelog/7431.added.md

This file was deleted.

6 changes: 3 additions & 3 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -199,7 +199,7 @@ services:
- 6362:6362

task-manager:
image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.4.11}"
image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.4.12}"
command: uvicorn --host 0.0.0.0 --port 4200 --factory infrahub.prefect_server.app:create_infrahub_prefect
restart: unless-stopped
depends_on:
Expand Down Expand Up @@ -232,7 +232,7 @@ services:
retries: 5

infrahub-server:
image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.4.11}"
image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.4.12}"
restart: unless-stopped
command: >
gunicorn --config backend/infrahub/serve/gunicorn_config.py
Expand Down Expand Up @@ -278,7 +278,7 @@ services:
deploy:
mode: replicated
replicas: 2
image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.4.11}"
image: "${INFRAHUB_DOCKER_IMAGE:-registry.opsmill.io/opsmill/infrahub}:${VERSION:-1.4.12}"
command: prefect worker start --type infrahubasync --pool infrahub-worker --with-healthcheck
restart: unless-stopped
depends_on:
Expand Down
7 changes: 7 additions & 0 deletions docs/docs/guides/change-approval-workflow.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,12 @@ This guide walks you through implementing a change approval workflow in Infrahub
Some features in this guide require the Enterprise Edition of Infrahub. If you are using the Community Edition, the enforcement mechanisms of the change approval workflow will not be available, though you can still implement a process-based approach.
:::

:::success Change Management Workflow Blog Post

Want to see how branches can be used in a change management workflow? Read our blog post on [Infrahub’s Change Management Workflow Is Built for Infrastructure Data](https://opsmill.com/blog/infrastructure-change-management-workflow/).

:::

## What you'll build

By the end of this guide, you'll have a complete governance system for infrastructure changes that includes:
Expand Down Expand Up @@ -296,3 +302,4 @@ Users with `Super Administrator` permission can:
- [Managing users and permissions](../topics/permissions-roles)
- [Working with branches](../topics/version-control)
- [Configuring Git repositories](../guides/repository)
- [Change Management Workflow Blog Post](https://opsmill.com/blog/infrastructure-change-management-workflow/)
Loading