From 1d726304dbe27a2a83f3b8cf4232bc8c59d18f42 Mon Sep 17 00:00:00 2001 From: Matt Beanland Date: Wed, 20 May 2026 04:52:06 +0930 Subject: [PATCH] resource lifecycle_status --- AGENTS.md | 1 + lib/diffo/provider.ex | 1 + lib/diffo/provider/assigner/assigner.ex | 17 ++++++++++++++--- lib/diffo/provider/components/base_instance.ex | 16 ++++++++++++++++ lib/diffo/provider/components/instance/util.ex | 5 ++++- test/provider/extension/assigner_test.exs | 12 ++++++++---- test/support/servo.ex | 1 + 7 files changed, 45 insertions(+), 8 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index d99c454..0c48a21 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -299,6 +299,7 @@ not. Add any useful hypotheses as a follow-up comment on the issue, then leave i - Using the removed `AssignableValue` TypedStruct — it no longer exists; use `pools do`. - Calling `Assigner.assign/4` when a `pools do` declaration exists — prefer `Assigner.assign/3` which looks up the thing automatically. - Forgetting to call `Pool.update_pools/3` in `:define` actions when the resource has `pools do` — pool bounds (`first`, `last`, `algorithm`) are set here. +- Calling `Assigner.assign/3` on an instance that is not in the correct lifecycle state — the assigner enforces: resource instances must have `resource_state: :operating`; service instances must have `service_state: :active` or `:inactive`. Lifecycle state transitions are an internal domain concern managed by the provider; assignment actions are external-facing. Future: consumer reads may filter out non-`:operating` resources entirely. - Using `characteristic :pool_name, Diffo.Provider.AssignedToRelationship` — `AssignedToRelationship` no longer exists; use `pools do / pool :name, :thing / end` instead. - Querying `Diffo.Provider.Relationship` for assignment records — assignments are stored as `Diffo.Provider.DefinedSimpleRelationship`; access them via `instance.assignments`. - Filtering `instance.forward_relationships` for `type == :assignedTo` — those records no longer exist there; use `instance.assignments` directly. diff --git a/lib/diffo/provider.ex b/lib/diffo/provider.ex index 898749b..6bf31ea 100644 --- a/lib/diffo/provider.ex +++ b/lib/diffo/provider.ex @@ -47,6 +47,7 @@ defmodule Diffo.Provider do define :suspend_service, action: :suspend define :terminate_service, action: :terminate define :status_service, action: :status + define :lifecycle_resource, action: :lifecycle define :respecify_instance, action: :specify define :relate_instance_features, action: :relate_features define :unrelate_instance_features, action: :unrelate_features diff --git a/lib/diffo/provider/assigner/assigner.ex b/lib/diffo/provider/assigner/assigner.ex index 358b7a7..63fe270 100644 --- a/lib/diffo/provider/assigner/assigner.ex +++ b/lib/diffo/provider/assigner/assigner.ex @@ -19,9 +19,11 @@ defmodule Diffo.Provider.Assigner do """ def assign(result, changeset, pool_name) when is_struct(result) and is_struct(changeset, Ash.Changeset) and is_atom(pool_name) do - case result.__struct__.pool(pool_name) do - nil -> {:error, "pool #{pool_name} not declared on #{result.__struct__}"} - pool -> assign(result, changeset, pool_name, pool.thing) + with :ok <- check_lifecycle(result) do + case result.__struct__.pool(pool_name) do + nil -> {:error, "pool #{pool_name} not declared on #{result.__struct__}"} + pool -> assign(result, changeset, pool_name, pool.thing) + end end end @@ -58,6 +60,15 @@ defmodule Diffo.Provider.Assigner do end end + defp check_lifecycle(%{type: :resource, resource_state: state}) when state != :operating, + do: {:error, "cannot assign: resource lifecycle state is #{inspect(state)}, must be :operating"} + + defp check_lifecycle(%{type: :service, service_state: state}) + when state not in [:active, :inactive], + do: {:error, "cannot assign: service state is #{inspect(state)}, must be :active or :inactive"} + + defp check_lifecycle(_), do: :ok + defp create_assignment(result, pool, thing, value, assignee_id) when is_struct(result) and is_atom(pool) and is_atom(thing) and is_integer(value) and is_bitstring(assignee_id) do diff --git a/lib/diffo/provider/components/base_instance.ex b/lib/diffo/provider/components/base_instance.ex index 1ee5655..b58f8bc 100644 --- a/lib/diffo/provider/components/base_instance.ex +++ b/lib/diffo/provider/components/base_instance.ex @@ -261,6 +261,7 @@ defmodule Diffo.Provider.BaseInstance do :endOperatingDate, :state, :operatingStatus, + :lifecycleState, :administrativeState, :operationalState, :resourceStatus, @@ -366,6 +367,14 @@ defmodule Diffo.Provider.BaseInstance do constraints one_of: Diffo.Provider.Service.service_operating_statuses() end + attribute :resource_state, :atom do + description "the TMF lifecycleState for resource instances: planning, installing, operating, or retiring" + allow_nil? true + public? true + default nil + constraints one_of: [:planning, :installing, :operating, :retiring] + end + create_timestamp :created_at update_timestamp :updated_at @@ -605,6 +614,13 @@ defmodule Diffo.Provider.BaseInstance do accept [:service_operating_status] end + update :lifecycle do + description "sets the TMF lifecycleState for a resource instance" + require_atomic? false + validate attribute_equals(:type, :resource) + accept [:resource_state] + end + update :relate_features do description "relates features to the instance" argument :features, {:array, :uuid} diff --git a/lib/diffo/provider/components/instance/util.ex b/lib/diffo/provider/components/instance/util.ex index 9460c6a..b555255 100644 --- a/lib/diffo/provider/components/instance/util.ex +++ b/lib/diffo/provider/components/instance/util.ex @@ -64,7 +64,10 @@ defmodule Diffo.Provider.Instance.Util do |> Diffo.Util.set(:operatingStatus, record.service_operating_status) :resource -> - result + case record.resource_state do + nil -> result + state -> Diffo.Util.set(result, :lifecycleState, state) + end # |> Diffo.Util.ensure_not_nil(:administrativeState, record.resource_administrative_state) # |> Diffo.Util.ensure_not_nil(:operationalState, record.resource_operational_state) # |> Diffo.Util.ensure_not_nil(:resourceStatus, record.resource_status) diff --git a/test/provider/extension/assigner_test.exs b/test/provider/extension/assigner_test.exs index 4c9e652..a0b2f0f 100644 --- a/test/provider/extension/assigner_test.exs +++ b/test/provider/extension/assigner_test.exs @@ -98,6 +98,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do ] {:ok, card} = Servo.define_card(card, %{characteristic_value_updates: updates}) + {:ok, card} = Servo.lifecycle_card(card, %{resource_state: :operating}) {:ok, card} = Servo.assign_port(card, %{ @@ -109,7 +110,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]}]}) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]}]}) end test "auto assign two ports to same resource" do @@ -123,6 +124,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do ] {:ok, card} = Servo.define_card(card, %{characteristic_value_updates: updates}) + {:ok, card} = Servo.lifecycle_card(card, %{resource_state: :operating}) {:ok, card} = Servo.assign_port(card, %{ @@ -139,7 +141,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]},{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":2}]}]}) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":1}]},{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":2}]}]}) end test "specific assignment rejects duplicate request" do @@ -153,6 +155,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do ] {:ok, card} = Servo.define_card(card, %{characteristic_value_updates: updates}) + {:ok, card} = Servo.lifecycle_card(card, %{resource_state: :operating}) {:ok, card} = Servo.assign_port(card, %{ @@ -169,7 +172,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":5}]}]}) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\",\"resourceRelationship\":[{\"type\":\"assignedTo\",\"resource\":{\"id\":\"#{assignee.id}\",\"href\":\"resourceInventoryManagement/v4/resource/#{assignee.id}\"},\"resourceRelationshipCharacteristic\":[{\"name\":\"port\",\"value\":5}]}]}) end test "unassign an auto-assigned port from a resource" do @@ -183,6 +186,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do ] {:ok, card} = Servo.define_card(card, %{characteristic_value_updates: updates}) + {:ok, card} = Servo.lifecycle_card(card, %{resource_state: :operating}) {:ok, card} = Servo.assign_port(card, %{ @@ -207,7 +211,7 @@ defmodule Diffo.Provider.Extension.AssignerTest do encoding = Jason.encode!(card) |> Diffo.Util.summarise_dates() assert encoding == - ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"}}) + ~s({\"id\":\"#{card.id}",\"href\":\"resourceInventoryManagement/v4/resource/#{card.id}",\"category\":\"Network Resource\",\"description\":\"A Card Resource Instance\",\"resourceSpecification\":{\"id\":\"cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"href\":\"resourceCatalogManagement/v4/resourceSpecification/cd29956f-6c68-44cc-bf54-705eb8d2f754\",\"name\":\"card\",\"version\":\"v1.0.0\"},\"lifecycleState\":\"operating\"}) end end end diff --git a/test/support/servo.ex b/test/support/servo.ex index 95a222f..d055fff 100644 --- a/test/support/servo.ex +++ b/test/support/servo.ex @@ -41,6 +41,7 @@ defmodule Diffo.Test.Servo do define :define_card, action: :define define :relate_card, action: :relate define :assign_port, action: :assign_port + define :lifecycle_card, action: :lifecycle end resource Broadband do