Skip to content
Merged
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
291 changes: 222 additions & 69 deletions documentation/dsls/DSL-Diffo.Provider.Instance.Extension.md

Large diffs are not rendered by default.

91 changes: 45 additions & 46 deletions documentation/how_to/use_diffo_provider_extension.livemd
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,12 @@ end

Diffo also has an inbuilt Spark DSL extension [Diffo.Provider.Instance.Extension](https://hexdocs.pm/diffo/Diffo.Provider.Instance.Extension.html) which provides DSL and functions for use in building and operating domain specific services and resources.

Currently it has DSL to allow you to declare specification, features, characteristics, and party roles. It can be used for services or resources.
The extension has two top-level sections:

**`structure do`** — describes the static shape of the Instance kind: its TMF Specification, Characteristics, Features, and Party roles. All declarations are baked into the module at compile time and introspectable at runtime via generated functions (`specification/0`, `characteristics/0`, `features/0`, `parties/0`) and `Diffo.Provider.Instance.Info`.

**`behaviour do`** — declares which Ash actions should be wired for instance lifecycle management. Declaring `create :name` injects `:specified_by`, `:features`, and `:characteristics` arguments onto that action, and the `BuildBefore`/`BuildAfter` changes registered on `BaseInstance` automatically handle specification upsert, feature and characteristic creation, party validation, and graph relationship wiring for every create action. You write the action body for your domain-specific accepts and arguments; the structural wiring is handled for you.

Feature and Instance Characteristics can have payloads defined by [Ash.TypedStruct](https://hexdocs.pm/ash/Ash.TypedStruct.html). TypedStruct are DSL specified types which are effectively lightweight embedded resources. We've extended both [AshJason](https://hexdocs.pm/ash_jason/) and [AshOutstanding](https://hexdocs.pm/ash_outstanding/) to support Ash.TypedStruct.

For partial resource allocation and assignment we've created Diffo.Provider.Assigner. It is used by the host resource, which declares a characteristic with an Diffo.Provider.AssignableValue TypedStruct. Allocation is managed within the Provider domain using this characteristic. Assignment to Services or Resources is via 'reverse' type: "assignedTo" relationships enriched by relationship characteristics.
Expand Down Expand Up @@ -172,7 +177,6 @@ defmodule Diffo.Compute.Cluster do
alias Diffo.Provider.BaseInstance
alias Diffo.Provider.Instance.Relationship
alias Diffo.Provider.Instance.Characteristic
alias Diffo.Provider.Instance.ActionHelper
alias Diffo.Compute
alias Diffo.Compute.ClusterValue
alias Diffo.Compute.Tenant
Expand All @@ -187,42 +191,40 @@ defmodule Diffo.Compute.Cluster do
plural_name :Clusters
end

specification do
id "4bcfc4c9-e776-4878-a658-e8d81857bed7"
name "cluster"
type :resourceSpecification
description "A Cluster Resource Instance"
category "Network Resource"
end
structure do
specification do
id "4bcfc4c9-e776-4878-a658-e8d81857bed7"
name "cluster"
type :resourceSpecification
description "A Cluster Resource Instance"
category "Network Resource"
end

characteristics do
characteristic :cluster, ClusterValue
end

characteristics do
characteristic :cluster, ClusterValue
parties do
party :operator, Tenant
party :manager, Engineer
end
end

parties do
party :operator, Tenant
party :manager, Engineer
behaviour do
actions do
create :build
end
end

actions do
create :build do
description "creates a new Cluster resource instance for build"
accept [:id, :name, :type, :which]
argument :specified_by, :uuid, public?: false
argument :relationships, {:array, :struct}
argument :features, {:array, :uuid}, public?: false
argument :characteristics, {:array, :uuid}, public?: false
argument :places, {:array, :struct}
argument :parties, {:array, :struct}

change set_attribute(:type, :resource)

change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end)

change after_action(fn changeset, result, _context ->
ActionHelper.build_after(changeset, result, Compute, :get_cluster_by_id)
end)

change load [:href]
upsert? false
end
Expand All @@ -232,7 +234,7 @@ defmodule Diffo.Compute.Cluster do
argument :characteristic_value_updates, {:array, :term}

change after_action(fn changeset, result, _context ->
with {:ok, _result} <- Characteristic.update_values(result, changeset),
with {:ok, result} <- Characteristic.update_values(result, changeset),
{:ok, cluster} <- Compute.get_cluster_by_id(result.id),
do: {:ok, cluster}
end)
Expand Down Expand Up @@ -305,7 +307,6 @@ defmodule Diffo.Compute.GPU do
alias Diffo.Provider.BaseInstance
alias Diffo.Provider.Instance.Relationship
alias Diffo.Provider.Instance.Characteristic
alias Diffo.Provider.Instance.ActionHelper
alias Diffo.Provider.Assigner
alias Diffo.Provider.Assignment
alias Diffo.Provider.AssignableValue
Expand All @@ -321,38 +322,36 @@ defmodule Diffo.Compute.GPU do
plural_name :gpus
end

specification do
id "ad50073f-17e0-45cb-b9b1-aa4296876156"
name "gpu"
type :resourceSpecification
description "A GPU Resource Instance"
category "Network Resource"
structure do
specification do
id "ad50073f-17e0-45cb-b9b1-aa4296876156"
name "gpu"
type :resourceSpecification
description "A GPU Resource Instance"
category "Network Resource"
end

characteristics do
characteristic :gpu, GPUValue
characteristic :cores, AssignableValue
end
end

characteristics do
characteristic :gpu, GPUValue
characteristic :cores, AssignableValue
behaviour do
actions do
create :build
end
end

actions do
create :build do
description "creates a new GPU resource instance for build"
accept [:id, :name, :type, :which]
argument :specified_by, :uuid, public?: false
argument :relationships, {:array, :struct}
argument :features, {:array, :uuid}, public?: false
argument :characteristics, {:array, :uuid}, public?: false
argument :places, {:array, :struct}
argument :parties, {:array, :struct}

change set_attribute(:type, :resource)

change before_action(fn changeset, _context -> ActionHelper.build_before(changeset) end)

change after_action(fn changeset, result, _context ->
ActionHelper.build_after(changeset, result, Compute, :get_gpu_by_id)
end)

change load [:href]
upsert? false
end
Expand Down Expand Up @@ -685,7 +684,7 @@ What happens when I request a specific assignment from an instance to which the

In this tutorial you've used Diffo's Provider Instance Extension to define a Compute domain with a composite Cluster resource comprised of assigned GPU cores, and the Provider Party Extension to define Tenant and Engineer party kinds that operate and manage those resources.

The BaseParty fragment follows the same pattern as BaseInstance — domain-specific resources use it as a fragment and finish their actions with a domain-scoped reload to pick up extended fields.
`BaseParty` follows the same pattern as `BaseInstance` — domain-specific party resources use it as a fragment and write their own `build` action for domain-specific attributes. No manual wiring is needed.

A `BasePlace` extension for domain-specific Place kinds (such as a DataCentre with its own attributes) follows the same pattern and will be added in a future release.

Expand Down
116 changes: 89 additions & 27 deletions lib/diffo/provider/components/base_instance.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,27 +4,30 @@

defmodule Diffo.Provider.BaseInstance do
@moduledoc """
Ash Resource Fragment which is a the point of extension for your TMF Service or Resource Instance
Ash Resource Fragment which is the point of extension for your TMF Service or Resource Instance.

`BaseInstance` is the foundation for domain-specific Service and Resource kinds.
Include it as a fragment on an `Ash.Resource` to get common Instance attributes,
Neo4j graph wiring, state machine, and the `Diffo.Provider.Instance.Extension` DSL.

## Instance Extension DSL

The `Diffo.Provider.Instance.Extension` DSL provides compile-time declaration blocks
for describing the shape of a domain-specific Service or Resource.
The DSL has two top-level sections: `structure do` describes what the instance kind is;
`behaviour do` wires it to Ash actions.

`specification do` — declares the TMF Specification for this Instance kind.
### structure

`features do` — declares the Features this Instance kind may have, each optionally
carrying a typed characteristic payload.
`specification do` — declares the TMF Specification for this Instance kind (id, name, type,
major_version, description, category).

`characteristics do` — declares the top-level Characteristics of this Instance kind,
each backed by an `Ash.TypedStruct`.
`characteristics do` — declares the top-level Characteristics of this Instance kind, each
backed by an `Ash.TypedStruct`.

`features do` — declares the Features this Instance kind may have, each optionally carrying
its own typed characteristic payload.

`parties do` — declares the Party roles this Instance kind relates to. Role names are
domain-specific nouns describing what the party is to the instance. Two forms:
domain-specific nouns describing what the party means to the instance. Two forms:

parties do
party :provider, MyApp.Provider, calculate: :provider_calculation
Expand All @@ -33,12 +36,44 @@ defmodule Diffo.Provider.BaseInstance do
party :owner, MyApp.InfrastructureCo, reference: true
end

- `party` — singular (at most one party in this role)
- `parties` — plural (unbounded, or bounded with `constraints:`)
- `party` — singular (at most one party in this role per instance)
- `parties` — plural (unbounded, or bounded with `constraints: [min: n, max: m]`)
- `reference: true` — no direct `PartyRef` edge; party is reachable by graph traversal
- `calculate:` — names an Ash calculation on this resource that produces the party at build time

All declarations are introspectable via `Diffo.Provider.Instance.Extension.Info`.
All declarations are introspectable at runtime via `Diffo.Provider.Instance.Info` and at
compile time via `Diffo.Provider.Instance.Extension.Info`.

### behaviour

`behaviour do actions do create :name end end` — marks a named create action for build
wiring. This injects `:specified_by`, `:features`, and `:characteristics` arguments onto
that action so Ash accepts the values that `build_before/1` sets automatically.

You still write the action body yourself for domain-specific accepts, arguments, and changes.
The build arguments are not public and do not need to appear in `accept`.

## Generated functions

Every resource using `BaseInstance` with a `specification do` gets the following functions
generated at compile time:

- `specification/0` — the specification keyword list baked at compile time
- `characteristics/0` — list of `Characteristic` structs
- `features/0` — list of `Feature` structs
- `parties/0` — list of `PartyDeclaration` structs
- `characteristic/1` — returns the named `Characteristic` or `nil`
- `feature/1` — returns the named `Feature` or `nil`
- `feature_characteristic/2` — returns the named characteristic within a feature, or `nil`
- `party/1` — returns the `PartyDeclaration` for the given role, or `nil`
- `build_before/1` — called automatically before every create action; upserts the
specification and creates features, characteristics, and parties, setting their ids
as action arguments
- `build_after/2` — called automatically after every create action; relates the created
TMF entities to the new instance node

Resources without a `specification do id` get trivial passthroughs for `build_before/1`
and `build_after/2`.

## Usage

Expand All @@ -50,28 +85,50 @@ defmodule Diffo.Provider.BaseInstance do
plural_name :clusters
end

specification do
id "4bcfc4c9-e776-4878-a658-e8d81857bed7"
name "cluster"
type :resourceSpecification
structure do
specification do
id "4bcfc4c9-e776-4878-a658-e8d81857bed7"
name "cluster"
type :resourceSpecification
end

parties do
party :operator, MyApp.Organization
parties :installer, MyApp.Engineer
end
end

behaviour do
actions do
create :build
end
end

parties do
party :operator, MyApp.Organization
parties :installer, MyApp.Engineer
actions do
create :build do
description "creates a new Cluster resource instance"
accept [:id, :name, :type, :which]
argument :relationships, {:array, :struct}
argument :parties, {:array, :struct}

change set_attribute(:type, :resource)
change load [:href]
upsert? false
end
end
end

## Action pattern
## Rolling your own actions

Domain-specific Instance resources should finish their `build` action with a reload via
their own domain's `get_xxx_by_id` to pick up extended fields:
The `behaviour do actions do create :name end end` declaration is optional. Omitting it
means the `:specified_by`, `:features`, and `:characteristics` arguments are not declared
on that action — but `build_before/1` and `build_after/2` are still called for every
create via the global `BuildBefore` and `BuildAfter` changes registered on `BaseInstance`.

create :build do
change after_action(fn changeset, result, _context ->
ActionHelper.build_after(changeset, result, MyApp.Domain, :get_cluster_by_id)
end)
end
If you have a create action that should NOT trigger the full build wiring (e.g. a
lightweight admin create), you can override `build_before/1` or `build_after/2` on your
resource, or use Ash's `skip_unknown_inputs` to absorb the injected arguments without
declaring them.
"""
use Spark.Dsl.Fragment,
of: Ash.Resource,
Expand Down Expand Up @@ -357,6 +414,11 @@ defmodule Diffo.Provider.BaseInstance do
end
end

changes do
change Diffo.Provider.Instance.Extension.Changes.BuildBefore, on: [:create]
change Diffo.Provider.Instance.Extension.Changes.BuildAfter, on: [:create]
end

actions do
defaults [:destroy]

Expand Down
2 changes: 1 addition & 1 deletion lib/diffo/provider/components/entity_ref.ex
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

defmodule Diffo.Provider.EntityRef do
@moduledoc """
EntityRef - Ash Resource for a TMF Entity Reference
Ash Resource for a TMF Entity Reference
"""
use Ash.Resource,
otp_app: :diffo,
Expand Down
Loading
Loading