diff --git a/backend/lib/edgehog/containers/container/calculations/container_ready.ex b/backend/lib/edgehog/containers/container/calculations/container_ready.ex new file mode 100644 index 000000000..c9a7a19f3 --- /dev/null +++ b/backend/lib/edgehog/containers/container/calculations/container_ready.ex @@ -0,0 +1,72 @@ +# +# This file is part of Edgehog. +# +# Copyright 2025 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Containers.Container.Calculations.ContainerReady do + @moduledoc false + + use Ash.Resource.Calculation + + alias Ash.Resource.Calculation + alias Edgehog.Containers + + @impl Calculation + def load(_query, _opts, _context) do + [container: [:image, :networks, :volumes], device: []] + end + + @impl Calculation + def calculate(records, _opts, context) do + %{tenant: tenant} = context + Enum.map(records, &compute_ready(&1, tenant)) + end + + defp compute_ready(deployment, tenant) do + container = deployment.container + device = deployment.device + + image = + Containers.fetch_image_deployment!(container.image.id, device.id, + tenant: tenant, + load: [:ready?] + ) + + networks = + Enum.map( + container.networks, + &Containers.fetch_network_deployment!(&1.id, device.id, tenant: tenant, load: [:ready?]) + ) + + volumes = + Enum.map( + container.volumes, + &Containers.fetch_volume_deployment!(&1.id, device.id, tenant: tenant, load: [:ready?]) + ) + + resources = [image | networks ++ volumes] + + Enum.reduce_while(resources, true, fn resource, _ -> + if resource.ready? do + {:cont, true} + else + {:halt, false} + end + end) + end +end diff --git a/backend/lib/edgehog/containers/container/changes/deploy_container_on_device.ex b/backend/lib/edgehog/containers/container/changes/deploy_container_on_device.ex new file mode 100644 index 000000000..b8b75c088 --- /dev/null +++ b/backend/lib/edgehog/containers/container/changes/deploy_container_on_device.ex @@ -0,0 +1,97 @@ +# +# This file is part of Edgehog. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Containers.Container.Changes.DeployContainerOnDevice do + @moduledoc false + use Ash.Resource.Change + + alias Ash.Error.Query.NotFound + alias Edgehog.Containers + alias Edgehog.Devices + + @impl Ash.Resource.Change + def change(changeset, _opts, context) do + %{tenant: tenant} = context + + Ash.Changeset.after_action(changeset, fn _changeset, deployment -> + with {:ok, deployment} <- + Ash.load(deployment, device: [], container: [:image, :networks, :volumes]), + {:ok, _image_deployment} <- deploy_image(deployment, tenant), + :ok <- deploy_networks(deployment, tenant), + :ok <- deploy_volumes(deployment, tenant), + {:ok, _device} <- + Devices.send_create_container_request(deployment.device, deployment.container, tenant: tenant) do + Containers.container_deployment_sent(deployment, tenant: tenant) + end + end) + end + + def deploy_image(deployment, tenant) do + image = deployment.container.image + device = deployment.device + Containers.deploy_image(image.id, device.id, tenant: tenant) + end + + def deploy_networks(deployment, tenant) do + device = deployment.device + + networks = + deployment.container.networks + |> Enum.uniq_by(& &1.id) + |> Enum.reject(&deployed?(&1, device, tenant)) + + Enum.reduce_while(networks, :ok, fn network, _acc -> + case Containers.deploy_network(network.id, device.id, tenant: tenant) do + {:ok, _network_deployment} -> {:cont, :ok} + error -> {:halt, error} + end + end) + end + + def deploy_volumes(deployment, tenant) do + device = deployment.device + + volumes = + deployment.container.volumes + |> Enum.uniq_by(& &1.id) + |> Enum.reject(&deployed?(&1, device, tenant)) + + Enum.reduce_while(volumes, :ok, fn volume, _acc -> + case Containers.deploy_volume(volume.id, device.id, tenant: tenant) do + {:ok, _volume_deployment} -> {:cont, :ok} + error -> {:halt, error} + end + end) + end + + defp deployed?(%Containers.Network{} = network, device, tenant) do + case Containers.network_is_deployed?(network.id, device.id, tenant: tenant) do + {:ok, _network} -> true + {:error, %NotFound{}} -> false + end + end + + defp deployed?(%Containers.Volume{} = volume, device, tenant) do + case Containers.volume_is_deployed?(volume.id, device.id, tenant: tenant) do + {:ok, _volume} -> true + {:error, %NotFound{}} -> false + end + end +end diff --git a/backend/lib/edgehog/containers/container.ex b/backend/lib/edgehog/containers/container/container.ex similarity index 95% rename from backend/lib/edgehog/containers/container.ex rename to backend/lib/edgehog/containers/container/container.ex index 0ab70dbcc..87f09dbb6 100644 --- a/backend/lib/edgehog/containers/container.ex +++ b/backend/lib/edgehog/containers/container/container.ex @@ -156,6 +156,11 @@ defmodule Edgehog.Containers.Container do through Edgehog.Containers.ContainerNetwork public? true end + + many_to_many :devices, Edgehog.Devices.Device do + through Edgehog.Containers.Container.Deployment + join_relationship :container_deployments + end end calculations do diff --git a/backend/lib/edgehog/containers/container/deployment.ex b/backend/lib/edgehog/containers/container/deployment.ex new file mode 100644 index 000000000..2872ab38c --- /dev/null +++ b/backend/lib/edgehog/containers/container/deployment.ex @@ -0,0 +1,121 @@ +# +# This file is part of Edgehog. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Containers.Container.Deployment do + @moduledoc false + use Edgehog.MultitenantResource, + domain: Edgehog.Containers, + extensions: [AshGraphql.Resource] + + alias Edgehog.Containers.Container.Calculations + alias Edgehog.Containers.Container.Changes + + graphql do + type :container_deployment + end + + actions do + defaults [:read, :destroy] + + create :deploy do + description """ + Deploys an image on a device, the status according to device triggers. + """ + + accept [:container_id] + + argument :device_id, :id do + allow_nil? false + end + + change set_attribute(:state, :created) + change manage_relationship(:device_id, :device, type: :append) + change Changes.DeployContainerOnDevice + end + + update :sent do + change set_attribute(:state, :sent) + end + + update :received do + change set_attribute(:state, :received) + end + + update :created do + change set_attribute(:state, :device_created) + end + + update :stopped do + change set_attribute(:state, :stopped) + end + + update :running do + change set_attribute(:state, :running) + end + + update :errored do + argument :message, :string do + allow_nil? false + end + + change set_attribute(:last_message, arg(:message)) + change set_attribute(:state, :error) + end + end + + attributes do + uuid_primary_key :id + + attribute :last_message, :string + + attribute :state, :atom, + constraints: [ + one_of: [:created, :sent, :received, :device_created, :stopped, :running, :error] + ] + + timestamps() + end + + relationships do + belongs_to :container, Edgehog.Containers.Container do + attribute_type :uuid + public? true + end + + belongs_to :device, Edgehog.Devices.Device + end + + calculations do + calculate :ready?, :boolean, Calculations.ContainerReady + end + + identities do + identity :container_instance, [:container_id, :device_id] + end + + postgres do + table "container_deployments" + + references do + reference :container, on_delete: :delete + reference :device, on_delete: :delete + end + end +end diff --git a/backend/lib/edgehog/containers/container/status.ex b/backend/lib/edgehog/containers/container/status.ex new file mode 100644 index 000000000..128c86b32 --- /dev/null +++ b/backend/lib/edgehog/containers/container/status.ex @@ -0,0 +1,26 @@ +# +# This file is part of Edgehog. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Containers.Container.Status do + @moduledoc false + use Ash.Type.Enum, values: [:received, :created, :running, :stopped] + + def graphql_type(_), do: :application_container_status +end diff --git a/backend/lib/edgehog/containers/containers.ex b/backend/lib/edgehog/containers/containers.ex index 66bccc603..703d9fb78 100644 --- a/backend/lib/edgehog/containers/containers.ex +++ b/backend/lib/edgehog/containers/containers.ex @@ -26,11 +26,14 @@ defmodule Edgehog.Containers do ] alias Edgehog.Containers.Application - alias Edgehog.Containers.Deployment - alias Edgehog.Containers.DeploymentReadyAction - alias Edgehog.Containers.DeploymentReadyAction.Upgrade + alias Edgehog.Containers.Container + alias Edgehog.Containers.ContainerNetwork + alias Edgehog.Containers.Image alias Edgehog.Containers.ImageCredentials + alias Edgehog.Containers.Network alias Edgehog.Containers.Release + alias Edgehog.Containers.ReleaseContainers + alias Edgehog.Containers.Volume graphql do root_level_errors? true @@ -73,75 +76,118 @@ defmodule Edgehog.Containers do destroy ImageCredentials, :delete_image_credentials, :destroy - create Deployment, :deploy_release, :deploy do + create Release.Deployment, :deploy_release, :deploy do description "Deploy the application on a device" relay_id_translations input: [release_id: :release, device_id: :device] end - update Deployment, :start_deployment, :start - update Deployment, :stop_deployment, :stop - update Deployment, :delete_deployment, :delete + update Release.Deployment, :start_deployment, :start + update Release.Deployment, :stop_deployment, :stop + update Release.Deployment, :delete_deployment, :delete - update Deployment, :upgrade_deployment, :upgrade_release do + update Release.Deployment, :upgrade_deployment, :upgrade_release do relay_id_translations input: [target: :release] end end end resources do - resource Edgehog.Containers.Application + resource Application - resource Edgehog.Containers.Container do + resource Container do define :fetch_container, action: :read, get_by: [:id] define :containers_with_image, action: :filter_by_image, args: [:image_id] end - resource Edgehog.Containers.Deployment do + resource Container.Deployment do + define :deploy_container, action: :deploy, args: [:container_id, :device_id] + define :fetch_container_deployment, action: :read, get_by_identity: :container_instance + define :container_deployment_sent, action: :sent + define :container_deployment_received, action: :received + define :container_deployment_created, action: :created + define :container_deployment_stopped, action: :stopped + define :container_deployment_running, action: :running + define :container_deployment_errored, action: :errored, args: [:message] + end + + resource Release do + define :fetch_release, action: :read, get_by: [:id] + end + + resource Release.Deployment do define :deploy, action: :deploy, args: [:release_id, :device_id] - define :send_deploy_request, action: :send_deploy_request, args: [:deployment] define :fetch_deployment, action: :read, get_by: [:id] - define :deployment_set_status, action: :set_status, args: [:status, :message] define :delete_deployment, action: :destroy - define :deployment_update_status, action: :update_status define :deployments_with_release, action: :filter_by_release, args: [:release_id] define :run_ready_actions, action: :run_ready_actions + + define :release_deployment_sent, action: :sent + define :release_deployment_started, action: :started + define :release_deployment_stopped, action: :stopped + define :release_deployment_error, action: :error, args: [:message] + + define :release_deployment_starting, action: :starting + define :release_deployment_stopping, action: :stopping end - resource Edgehog.Containers.Image do - define :fetch_image, action: :read, get_by: [:id] + resource Release.Deployment.ReadyAction do + define :run_ready_action, action: :run end - resource Edgehog.Containers.ImageCredentials + resource Release.Deployment.ReadyAction.Upgrade - resource Edgehog.Containers.Release do - define :fetch_release, action: :read, get_by: [:id] + resource Image do + define :fetch_image, action: :read, get_by: [:id] end - resource Edgehog.Containers.ReleaseContainers do + resource Image.Deployment do + define :deploy_image, action: :deploy, args: [:image_id, :device_id] + define :fetch_image_deployment, action: :read, get_by_identity: :image_instance + define :image_deployment_sent, action: :sent + define :image_deployment_unpulled, action: :unpulled + define :image_deployment_pulled, action: :pulled + define :image_deployment_errored, action: :errored, args: [:message] + end + + resource ImageCredentials + + resource ReleaseContainers do define :releases_with_container, action: :releases_by_container, args: [:container_id] end - resource Edgehog.Containers.Network - resource Edgehog.Containers.Volume + resource Volume - resource DeploymentReadyAction - resource Upgrade + resource Volume.Deployment do + define :deploy_volume, action: :deploy, args: [:volume_id, :device_id] + define :fetch_volume_deployment, action: :read, get_by_identity: :volume_instance + define :volume_deployment_sent, action: :sent + define :volume_deployment_available, action: :available + define :volume_deployment_unavailable, action: :unavailable + define :volume_deployment_errored, action: :errored, args: [:message] + define :volume_is_deployed?, action: :read, get_by_identity: :volume_instance + end + + resource Network - resource Edgehog.Containers.ContainerNetwork do + resource Network.Deployment do + define :deploy_network, action: :deploy, args: [:network_id, :device_id] + define :fetch_network_deployment, action: :read, get_by_identity: :network_instance + define :network_deployment_sent, action: :sent + define :network_deployment_available, action: :available + define :network_deployment_unavailable, action: :unavailable + define :network_deployment_errored, action: :errored, args: [:message] + define :network_is_deployed?, action: :read, get_by_identity: :network_instance + end + + resource ContainerNetwork do define :containers_with_network, action: :containers_by_network, args: [:network_id] end resource Edgehog.Containers.ContainerVolume - - resource DeploymentReadyAction do - define :run_ready_action, action: :run - end - - resource Upgrade end end diff --git a/backend/lib/edgehog/containers/deployment/changes/check_deployments.ex b/backend/lib/edgehog/containers/deployment/changes/check_deployments.ex deleted file mode 100644 index 66fbaf58a..000000000 --- a/backend/lib/edgehog/containers/deployment/changes/check_deployments.ex +++ /dev/null @@ -1,52 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2024 SECO Mind Srl -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -defmodule Edgehog.Containers.Deployment.Changes.CheckDeployments do - @moduledoc false - use Ash.Resource.Change - - alias Edgehog.Containers - - @impl Ash.Resource.Change - def change(changeset, _opts, _context) do - deployment = changeset.data - - with {:ok, :created_containers} <- Ash.Changeset.fetch_argument_or_change(changeset, :status), - {:ok, deployment} <- Ash.load(deployment, device: :available_deployments) do - available_deployment = - Enum.find(deployment.device.available_deployments, &(&1.id == deployment.id)) - - if available_deployment do - changeset - |> Ash.Changeset.change_attribute(:status, available_deployment.status) - |> Ash.Changeset.after_transaction(fn _changeset, transaction_result -> - with {:ok, deployment} <- transaction_result do - Containers.run_ready_actions(deployment) - end - end) - else - changeset - end - else - _ -> - changeset - end - end -end diff --git a/backend/lib/edgehog/containers/deployment/changes/check_networks.ex b/backend/lib/edgehog/containers/deployment/changes/check_networks.ex deleted file mode 100644 index ec42c2b31..000000000 --- a/backend/lib/edgehog/containers/deployment/changes/check_networks.ex +++ /dev/null @@ -1,51 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2024 SECO Mind Srl -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -defmodule Edgehog.Containers.Deployment.Changes.CheckNetworks do - @moduledoc false - use Ash.Resource.Change - - @impl Ash.Resource.Change - def change(changeset, _opts, _context) do - deployment = changeset.data - - with {:ok, :created_images} <- Ash.Changeset.fetch_argument_or_change(changeset, :status), - {:ok, deployment} <- - Ash.load(deployment, device: :available_networks, release: [containers: [:networks]]) do - available_network_ids = - Enum.map(deployment.device.available_networks, & &1.id) - - missing_networks = - deployment.release.containers - |> Enum.flat_map(& &1.networks) - |> Enum.map(& &1.id) - |> Enum.uniq() - |> Enum.reject(&(&1 in available_network_ids)) - - if missing_networks == [] do - Ash.Changeset.change_attribute(changeset, :status, :created_networks) - else - changeset - end - else - _ -> changeset - end - end -end diff --git a/backend/lib/edgehog/containers/deployment/changes/check_images.ex b/backend/lib/edgehog/containers/image/changes/deploy_image_on_device.ex similarity index 50% rename from backend/lib/edgehog/containers/deployment/changes/check_images.ex rename to backend/lib/edgehog/containers/image/changes/deploy_image_on_device.ex index 0761c6aec..d6ce3925b 100644 --- a/backend/lib/edgehog/containers/deployment/changes/check_images.ex +++ b/backend/lib/edgehog/containers/image/changes/deploy_image_on_device.ex @@ -18,33 +18,23 @@ # SPDX-License-Identifier: Apache-2.0 # -defmodule Edgehog.Containers.Deployment.Changes.CheckImages do +defmodule Edgehog.Containers.Image.Changes.DeployImageOnDevice do @moduledoc false use Ash.Resource.Change - @impl Ash.Resource.Change - def change(changeset, _opts, _context) do - deployment = changeset.data - - with :sent <- deployment.status, - {:ok, deployment} <- - Ash.load(deployment, device: :available_images, release: [containers: [:image]]) do - available_images_ids = - Enum.map(deployment.device.available_images, & &1.id) + alias Edgehog.Containers + alias Edgehog.Devices - missing_images = - deployment.release.containers - |> Enum.map(& &1.image.id) - |> Enum.reject(&(&1 in available_images_ids)) + @impl Ash.Resource.Change + def change(changeset, _opts, context) do + %{tenant: tenant} = context - if missing_images == [] do - Ash.Changeset.change_attribute(changeset, :status, :created_images) - else - changeset + Ash.Changeset.after_action(changeset, fn _changeset, deployment -> + with {:ok, deployment} <- Ash.load(deployment, [:device, :image]), + {:ok, _device} <- + Devices.send_create_image_request(deployment.device, deployment.image, tenant: tenant) do + Containers.image_deployment_sent(deployment, tenant: tenant) end - else - _ -> - changeset - end + end) end end diff --git a/backend/lib/edgehog/containers/image/deployment.ex b/backend/lib/edgehog/containers/image/deployment.ex new file mode 100644 index 000000000..7bbfeecf8 --- /dev/null +++ b/backend/lib/edgehog/containers/image/deployment.ex @@ -0,0 +1,112 @@ +# +# This file is part of Edgehog. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Containers.Image.Deployment do + @moduledoc false + use Edgehog.MultitenantResource, + domain: Edgehog.Containers, + extensions: [AshGraphql.Resource] + + alias Edgehog.Containers.Image.Changes + + graphql do + type :image_deployment + end + + actions do + defaults [:read, :destroy] + + create :deploy do + description """ + Deploys an image on a device, the status according to device triggers. + """ + + accept [:image_id] + + argument :device_id, :id do + allow_nil? false + end + + change set_attribute(:state, :created) + change manage_relationship(:device_id, :device, type: :append) + change Changes.DeployImageOnDevice + end + + update :sent do + change set_attribute(:state, :sent) + end + + update :unpulled do + change set_attribute(:state, :unpulled) + end + + update :pulled do + change set_attribute(:state, :pulled) + end + + update :errored do + argument :message, :string do + allow_nil? false + end + + change set_attribute(:last_message, arg(:message)) + change set_attribute(:state, :error) + end + end + + attributes do + uuid_primary_key :id + + attribute :last_message, :string + + attribute :state, :atom, + constraints: [ + one_of: [:created, :sent, :pulled, :unpulled, :error] + ] + + timestamps() + end + + relationships do + belongs_to :image, Edgehog.Containers.Image do + attribute_type :uuid + public? true + end + + belongs_to :device, Edgehog.Devices.Device + end + + calculations do + calculate :ready?, :boolean, expr(state not in [:created, :sent, :error]) + end + + identities do + identity :image_instance, [:image_id, :device_id] + end + + postgres do + table "image_deployments" + + references do + reference :image, on_delete: :delete + reference :device, on_delete: :delete + end + end +end diff --git a/backend/lib/edgehog/containers/image.ex b/backend/lib/edgehog/containers/image/image.ex similarity index 90% rename from backend/lib/edgehog/containers/image.ex rename to backend/lib/edgehog/containers/image/image.ex index 31c9623d7..5b260f333 100644 --- a/backend/lib/edgehog/containers/image.ex +++ b/backend/lib/edgehog/containers/image/image.ex @@ -51,6 +51,11 @@ defmodule Edgehog.Containers.Image do attribute_type :uuid public? true end + + many_to_many :devices, Edgehog.Devices.Device do + through Edgehog.Containers.Image.Deployment + join_relationship :image_deployments + end end identities do diff --git a/backend/lib/edgehog/containers/image_credentials.ex b/backend/lib/edgehog/containers/image_credentials/image_credentials.ex similarity index 100% rename from backend/lib/edgehog/containers/image_credentials.ex rename to backend/lib/edgehog/containers/image_credentials/image_credentials.ex diff --git a/backend/lib/edgehog/containers/manual_actions/send_deploy_request.ex b/backend/lib/edgehog/containers/manual_actions/send_deploy_request.ex deleted file mode 100644 index 916040204..000000000 --- a/backend/lib/edgehog/containers/manual_actions/send_deploy_request.ex +++ /dev/null @@ -1,97 +0,0 @@ -# -# This file is part of Edgehog. -# -# Copyright 2024 - 2025 SECO Mind Srl -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -defmodule Edgehog.Containers.ManualActions.SendDeployRequest do - @moduledoc false - - use Ash.Resource.Actions.Implementation - - alias Edgehog.Containers - alias Edgehog.Devices - - @impl Ash.Resource.Actions.Implementation - def run(input, _opts, context) do - deployment = input.arguments.deployment - %{tenant: tenant} = context - - with {:ok, deployment} <- - Ash.load(deployment, device: [], release: [containers: [:image, :networks, :volumes]]) do - device = deployment.device - - release = deployment.release - containers = release.containers - images = containers |> Enum.map(& &1.image) |> Enum.uniq() - - networks = - containers - |> Enum.flat_map(& &1.networks) - |> Enum.uniq_by(& &1.id) - - volumes = - containers - |> Enum.flat_map(& &1.volumes) - |> Enum.uniq_by(& &1.id) - - with :ok <- send_create_image_requests(device, images), - :ok <- send_create_volume_requests(device, volumes), - :ok <- send_create_container_requests(device, containers), - :ok <- send_create_network_requests(device, networks), - {:ok, _device} <- Devices.send_create_deployment_request(device, deployment) do - Containers.deployment_set_status(deployment, :sent, nil, tenant: tenant) - end - end - end - - defp send_create_network_requests(device, networks) do - Enum.reduce_while(networks, :ok, fn network, _acc -> - case Devices.send_create_network_request(device, network) do - {:ok, _device} -> {:cont, :ok} - {:error, reason} -> {:halt, {:error, reason}} - end - end) - end - - defp send_create_image_requests(device, images) do - Enum.reduce_while(images, :ok, fn image, _acc -> - case Devices.send_create_image_request(device, image) do - {:ok, _device} -> {:cont, :ok} - {:error, reason} -> {:halt, {:error, reason}} - end - end) - end - - defp send_create_volume_requests(device, volumes) do - Enum.reduce_while(volumes, :ok, fn volume, _acc -> - case Devices.send_create_volume_request(device, volume) do - {:ok, _device} -> {:cont, :ok} - {:error, reason} -> {:halt, {:error, reason}} - end - end) - end - - defp send_create_container_requests(device, containers) do - Enum.reduce_while(containers, :ok, fn container, _acc -> - case Devices.send_create_container_request(device, container) do - {:ok, _device} -> {:cont, :ok} - {:error, reason} -> {:halt, {:error, reason}} - end - end) - end -end diff --git a/backend/lib/edgehog/containers/manual_actions/send_deployment_upgrade.ex b/backend/lib/edgehog/containers/manual_actions/send_deployment_upgrade.ex index 591917e42..7e45aa1ff 100644 --- a/backend/lib/edgehog/containers/manual_actions/send_deployment_upgrade.ex +++ b/backend/lib/edgehog/containers/manual_actions/send_deployment_upgrade.ex @@ -20,10 +20,9 @@ defmodule Edgehog.Containers.ManualActions.SendDeploymentUpgrade do @moduledoc false - use Ash.Resource.ManualUpdate - alias Edgehog.Containers.DeploymentReadyAction + alias Edgehog.Containers.Release @impl Ash.Resource.ManualUpdate def update(changeset, _opts, _context) do @@ -33,7 +32,7 @@ defmodule Edgehog.Containers.ManualActions.SendDeploymentUpgrade do target_id = Ash.Changeset.get_argument(changeset, :target) with {:ok, action} <- - DeploymentReadyAction + Release.Deployment.ReadyAction |> Ash.Changeset.for_create( :create_deployment, %{ diff --git a/backend/lib/edgehog/containers/deployment/changes/check_containers.ex b/backend/lib/edgehog/containers/network/changes/deploy_network_on_device.ex similarity index 50% rename from backend/lib/edgehog/containers/deployment/changes/check_containers.ex rename to backend/lib/edgehog/containers/network/changes/deploy_network_on_device.ex index 2139500e8..36a7ebd11 100644 --- a/backend/lib/edgehog/containers/deployment/changes/check_containers.ex +++ b/backend/lib/edgehog/containers/network/changes/deploy_network_on_device.ex @@ -18,29 +18,23 @@ # SPDX-License-Identifier: Apache-2.0 # -defmodule Edgehog.Containers.Deployment.Changes.CheckContainers do +defmodule Edgehog.Containers.Network.Changes.DeployNetworkOnDevice do @moduledoc false use Ash.Resource.Change - @impl Ash.Resource.Change - def change(changeset, _opts, _context) do - deployment = changeset.data - - with {:ok, :created_networks} <- Ash.Changeset.fetch_argument_or_change(changeset, :status), - {:ok, deployment} <- - Ash.load(deployment, device: :available_containers, release: [:containers]) do - available_container_ids = Enum.map(deployment.device.available_containers, & &1.id) + alias Edgehog.Containers + alias Edgehog.Devices - missing_containers = - Enum.reject(deployment.release.containers, &(&1.id in available_container_ids)) + @impl Ash.Resource.Change + def change(changeset, _opts, context) do + %{tenant: tenant} = context - if missing_containers == [] do - Ash.Changeset.change_attribute(changeset, :status, :created_containers) - else - changeset + Ash.Changeset.after_action(changeset, fn _changeset, deployment -> + with {:ok, deployment} <- Ash.load(deployment, [:device, :network]), + {:ok, _device} <- + Devices.send_create_network_request(deployment.device, deployment.network) do + Containers.network_deployment_sent(deployment, tenant: tenant) end - else - _ -> changeset - end + end) end end diff --git a/backend/lib/edgehog/containers/network/deployment.ex b/backend/lib/edgehog/containers/network/deployment.ex new file mode 100644 index 000000000..c2331c448 --- /dev/null +++ b/backend/lib/edgehog/containers/network/deployment.ex @@ -0,0 +1,114 @@ +# +# This file is part of Edgehog. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Containers.Network.Deployment do + @moduledoc false + use Edgehog.MultitenantResource, + domain: Edgehog.Containers, + extensions: [AshGraphql.Resource] + + alias Edgehog.Containers.Network + alias Edgehog.Containers.Network.Changes + alias Edgehog.Devices.Device + + graphql do + type :network_deployment + end + + actions do + defaults [:read, :destroy] + + create :deploy do + description """ + Deploys an image on a device, the status according to device triggers. + """ + + accept [:network_id] + + argument :device_id, :id do + allow_nil? false + end + + change set_attribute(:state, :created) + change manage_relationship(:device_id, :device, type: :append) + change Changes.DeployNetworkOnDevice + end + + update :sent do + change set_attribute(:state, :sent) + end + + update :available do + change set_attribute(:state, :available) + end + + update :unavailable do + change set_attribute(:state, :unavailable) + end + + update :errored do + argument :message, :string do + allow_nil? false + end + + change set_attribute(:last_message, arg(:message)) + change set_attribute(:state, :error) + end + end + + attributes do + uuid_primary_key :id + + attribute :last_message, :string + + attribute :state, :atom, + constraints: [ + one_of: [:created, :sent, :available, :unavailable, :error] + ] + + timestamps() + end + + relationships do + belongs_to :network, Network do + attribute_type :uuid + public? true + end + + belongs_to :device, Device + end + + calculations do + calculate :ready?, :boolean, expr(state not in [:created, :sent, :error]) + end + + identities do + identity :network_instance, [:network_id, :device_id] + end + + postgres do + table "network_deployments" + + references do + reference :network, on_delete: :delete + reference :device, on_delete: :delete + end + end +end diff --git a/backend/lib/edgehog/containers/network.ex b/backend/lib/edgehog/containers/network/network.ex similarity index 91% rename from backend/lib/edgehog/containers/network.ex rename to backend/lib/edgehog/containers/network/network.ex index ea58356c2..c4f8e7772 100644 --- a/backend/lib/edgehog/containers/network.ex +++ b/backend/lib/edgehog/containers/network/network.ex @@ -68,6 +68,11 @@ defmodule Edgehog.Containers.Network do many_to_many :containers, Edgehog.Containers.Container do through Edgehog.Containers.ContainerNetwork end + + many_to_many :devices, Edgehog.Devices.Device do + through Edgehog.Containers.Network.Deployment + join_relationship :network_deployments + end end calculations do diff --git a/backend/lib/edgehog/containers/release/deployment/calculations/release_ready.ex b/backend/lib/edgehog/containers/release/deployment/calculations/release_ready.ex new file mode 100644 index 000000000..7afa2c95f --- /dev/null +++ b/backend/lib/edgehog/containers/release/deployment/calculations/release_ready.ex @@ -0,0 +1,58 @@ +# +# This file is part of Edgehog. +# +# Copyright 2025 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Containers.Release.Deployment.Calculations.ReleaseReady do + @moduledoc false + + use Ash.Resource.Calculation + + alias Ash.Resource.Calculation + alias Edgehog.Containers + + # @impl Calculation + # def load(_query, opts, _context) do + # [release: [containers: [:ready?]]] + # end + + @impl Calculation + def calculate(records, _opts, context) do + %{tenant: tenant} = context + Enum.map(records, &ready?(&1, tenant)) + end + + defp ready?(deployment, tenant) do + with {:ok, deployment} <- + Ash.load(deployment, [release: [:containers], device: []], tenant: tenant) do + containers = deployment.release.containers + device = deployment.device + + containers + |> Enum.map(&load_container_deployment(&1, device, tenant)) + |> Enum.all?(fn deployment -> deployment.ready? end) + end + end + + defp load_container_deployment(container, device, tenant) do + Containers.fetch_container_deployment!(container.id, device.id, + tenant: tenant, + load: [:ready?] + ) + end +end diff --git a/backend/lib/edgehog/containers/release/deployment/changes/create_deployment_on_device.ex b/backend/lib/edgehog/containers/release/deployment/changes/create_deployment_on_device.ex new file mode 100644 index 000000000..35929cdcd --- /dev/null +++ b/backend/lib/edgehog/containers/release/deployment/changes/create_deployment_on_device.ex @@ -0,0 +1,53 @@ +# +# This file is part of Edgehog. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Containers.Release.Deployment.Changes.CreateDeploymentOnDevice do + @moduledoc false + use Ash.Resource.Change + + alias Edgehog.Containers + alias Edgehog.Devices + + @impl Ash.Resource.Change + def change(changeset, _opts, context) do + %{tenant: tenant} = context + + Ash.Changeset.after_action(changeset, fn _changeset, deployment -> + with {:ok, deployment} <- Ash.load(deployment, [:device, release: [:containers]]), + :ok <- deploy_containers(deployment, tenant), + {:ok, _device} <- + Devices.send_create_deployment_request(deployment.device, deployment) do + Containers.release_deployment_sent(deployment, tenant: tenant) + end + end) + end + + def deploy_containers(deployment, tenant) do + containers = deployment.release.containers + device = deployment.device + + Enum.reduce_while(containers, :ok, fn container, _acc -> + case Containers.deploy_container(container.id, device.id, tenant: tenant) do + {:ok, _container_deployment} -> {:cont, :ok} + error -> {:halt, error} + end + end) + end +end diff --git a/backend/lib/edgehog/containers/release/deployment/changes/run_ready_actions.ex b/backend/lib/edgehog/containers/release/deployment/changes/run_ready_actions.ex new file mode 100644 index 000000000..04fd7a73d --- /dev/null +++ b/backend/lib/edgehog/containers/release/deployment/changes/run_ready_actions.ex @@ -0,0 +1,43 @@ +# +# This file is part of Edgehog. +# +# Copyright 2025 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Containers.Release.Deployment.Changes.RunReadyActions do + @moduledoc """ + Change to add an after transaction hook to run the ready actions when a deployment reaches a ready state. + """ + use Ash.Resource.Change + + alias Edgehog.Containers + + @impl Ash.Resource.Change + def change(changeset, _opts, context) do + %{tenant: tenant} = context + + if changeset.data.state == :sent do + Ash.Changeset.after_transaction(changeset, fn _changeset, result -> + with {:ok, deployment} <- result do + Containers.run_ready_actions(deployment, tenant: tenant) + end + end) + else + changeset + end + end +end diff --git a/backend/lib/edgehog/containers/deployment.ex b/backend/lib/edgehog/containers/release/deployment/deployment.ex similarity index 69% rename from backend/lib/edgehog/containers/deployment.ex rename to backend/lib/edgehog/containers/release/deployment/deployment.ex index d0e4e1000..740da3a57 100644 --- a/backend/lib/edgehog/containers/deployment.ex +++ b/backend/lib/edgehog/containers/release/deployment/deployment.ex @@ -18,25 +18,27 @@ # SPDX-License-Identifier: Apache-2.0 # -defmodule Edgehog.Containers.Deployment do +defmodule Edgehog.Containers.Release.Deployment do @moduledoc false use Edgehog.MultitenantResource, domain: Edgehog.Containers, extensions: [AshGraphql.Resource] - alias Edgehog.Containers.Deployment.Changes alias Edgehog.Containers.ManualActions alias Edgehog.Containers.Release - alias Edgehog.Containers.Types.DeploymentStatus + alias Edgehog.Containers.Release.Deployment.Calculations + alias Edgehog.Containers.Release.Deployment.Changes + alias Edgehog.Containers.Release.Deployment.ReadyAction alias Edgehog.Containers.Validations.IsUpgrade alias Edgehog.Containers.Validations.SameApplication + alias Edgehog.Devices.Device graphql do - type :deployment + type :release_deployment end actions do - defaults [:read, :destroy, create: [:device_id, :release_id, :status, :message]] + defaults [:read, :destroy, create: [:device_id, :release_id, :last_message, :state]] create :deploy do description """ @@ -50,8 +52,8 @@ defmodule Edgehog.Containers.Deployment do allow_nil? false end + change set_attribute(:state, :created) change manage_relationship(:device_id, :device, type: :append) - change Changes.CreateDeploymentOnDevice end @@ -68,6 +70,7 @@ defmodule Edgehog.Containers.Deployment do Sends a :stop command to the release on the device. """ + change set_attribute(:state, :stopping) manual {ManualActions.SendDeploymentCommand, command: :stop} end @@ -79,21 +82,44 @@ defmodule Edgehog.Containers.Deployment do manual {ManualActions.SendDeploymentCommand, command: :delete} end - update :run_ready_actions do - description """ - Executes deployment callbacks - """ + update :sent do + change set_attribute(:state, :sent) + end - manual ManualActions.RunReadyActions + update :started do + change set_attribute(:state, :started) + end + + update :stopped do + change set_attribute(:state, :stopped) + require_atomic? false + + change Changes.RunReadyActions + end + + update :starting do + change set_attribute(:state, :starting) + end + + update :stopping do + change set_attribute(:state, :stopping) end - action :send_deploy_request do - argument :deployment, :struct do - constraints instance_of: __MODULE__ + update :error do + argument :message, :string do allow_nil? false end - run ManualActions.SendDeployRequest + change set_attribute(:last_message, arg(:message)) + change set_attribute(:state, :error) + end + + update :run_ready_actions do + description """ + Executes deployment callbacks + """ + + manual ManualActions.RunReadyActions end update :upgrade_release do @@ -107,19 +133,6 @@ defmodule Edgehog.Containers.Deployment do manual ManualActions.SendDeploymentUpgrade end - update :set_status do - accept [:status, :message] - end - - update :update_status do - change Changes.CheckImages - change Changes.CheckNetworks - change Changes.CheckContainers - change Changes.CheckDeployments - - require_atomic? false - end - read :filter_by_release do argument :release_id, :uuid @@ -130,21 +143,18 @@ defmodule Edgehog.Containers.Deployment do attributes do uuid_primary_key :id - attribute :status, DeploymentStatus do - allow_nil? false - default :created - public? true - end + attribute :last_message, :string - attribute :message, :string do - public? true - end + attribute :state, :atom, + constraints: [ + one_of: [:created, :sent, :starting, :started, :stopping, :stopped, :deleting, :error] + ] timestamps() end relationships do - belongs_to :device, Edgehog.Devices.Device do + belongs_to :device, Device do public? true end @@ -153,20 +163,25 @@ defmodule Edgehog.Containers.Deployment do public? true end - has_many :ready_actions, Edgehog.Containers.DeploymentReadyAction do + has_many :ready_actions, ReadyAction do public? true end end - identities do - identity :release_instance, [:device_id, :release_id] + calculations do + calculate :ready?, :boolean, Calculations.ReleaseReady end postgres do - table "application_deployments" + table "release_deployments" references do reference :device, on_delete: :delete + reference :release, on_delete: :delete end end + + identities do + identity :release_instance, [:device_id, :release_id] + end end diff --git a/backend/lib/edgehog/containers/deployment_ready_action.ex b/backend/lib/edgehog/containers/release/deployment/ready_action/ready_action.ex similarity index 88% rename from backend/lib/edgehog/containers/deployment_ready_action.ex rename to backend/lib/edgehog/containers/release/deployment/ready_action/ready_action.ex index 1b5553e07..888d6d78b 100644 --- a/backend/lib/edgehog/containers/deployment_ready_action.ex +++ b/backend/lib/edgehog/containers/release/deployment/ready_action/ready_action.ex @@ -18,7 +18,7 @@ # SPDX-License-Identifier: Apache-2.0 # -defmodule Edgehog.Containers.DeploymentReadyAction do +defmodule Edgehog.Containers.Release.Deployment.ReadyAction do @moduledoc false use Edgehog.MultitenantResource, domain: Edgehog.Containers @@ -60,12 +60,12 @@ defmodule Edgehog.Containers.DeploymentReadyAction do end relationships do - belongs_to :deployment, Edgehog.Containers.Deployment do + belongs_to :deployment, Edgehog.Containers.Release.Deployment do allow_nil? false attribute_type :uuid end - has_one :upgrade_deployment, Edgehog.Containers.DeploymentReadyAction.Upgrade + has_one :upgrade_deployment, Edgehog.Containers.Release.Deployment.ReadyAction.Upgrade end postgres do diff --git a/backend/lib/edgehog/containers/deployment_ready_action/upgrade.ex b/backend/lib/edgehog/containers/release/deployment/ready_action/upgrade.ex similarity index 76% rename from backend/lib/edgehog/containers/deployment_ready_action/upgrade.ex rename to backend/lib/edgehog/containers/release/deployment/ready_action/upgrade.ex index ffb2b16be..f7a86d97b 100644 --- a/backend/lib/edgehog/containers/deployment_ready_action/upgrade.ex +++ b/backend/lib/edgehog/containers/release/deployment/ready_action/upgrade.ex @@ -18,7 +18,7 @@ # SPDX-License-Identifier: Apache-2.0 # -defmodule Edgehog.Containers.DeploymentReadyAction.Upgrade do +defmodule Edgehog.Containers.Release.Deployment.ReadyAction.Upgrade do @moduledoc false use Edgehog.MultitenantResource, domain: Edgehog.Containers @@ -32,22 +32,22 @@ defmodule Edgehog.Containers.DeploymentReadyAction.Upgrade do end relationships do - belongs_to :upgrade_target, Edgehog.Containers.Deployment do + belongs_to :upgrade_target, Edgehog.Containers.Release.Deployment do allow_nil? false attribute_type :uuid end - belongs_to :deployment_ready_action, Edgehog.Containers.DeploymentReadyAction do + belongs_to :ready_action, Edgehog.Containers.Release.Deployment.ReadyAction do allow_nil? false attribute_type :uuid end end postgres do - table "deployment_ready_action_upgrades" + table "ready_action_upgrades" references do - reference :deployment_ready_action, on_delete: :delete + reference :ready_action, on_delete: :delete end end end diff --git a/backend/lib/edgehog/containers/release.ex b/backend/lib/edgehog/containers/release/release.ex similarity index 95% rename from backend/lib/edgehog/containers/release.ex rename to backend/lib/edgehog/containers/release/release.ex index aa5b12201..8176c585c 100644 --- a/backend/lib/edgehog/containers/release.ex +++ b/backend/lib/edgehog/containers/release/release.ex @@ -77,8 +77,8 @@ defmodule Edgehog.Containers.Release do end many_to_many :devices, Edgehog.Devices.Device do - through Edgehog.Containers.Deployment - join_relationship :deployments + through Edgehog.Containers.Release.Deployment + join_relationship :release_deployments end many_to_many :containers, Edgehog.Containers.Container do diff --git a/backend/lib/edgehog/containers/deployment/changes/create_deployment_on_device.ex b/backend/lib/edgehog/containers/volume/changes/deploy_volume_on_device.ex similarity index 66% rename from backend/lib/edgehog/containers/deployment/changes/create_deployment_on_device.ex rename to backend/lib/edgehog/containers/volume/changes/deploy_volume_on_device.ex index cd2fafb27..c08985de3 100644 --- a/backend/lib/edgehog/containers/deployment/changes/create_deployment_on_device.ex +++ b/backend/lib/edgehog/containers/volume/changes/deploy_volume_on_device.ex @@ -18,20 +18,22 @@ # SPDX-License-Identifier: Apache-2.0 # -defmodule Edgehog.Containers.Deployment.Changes.CreateDeploymentOnDevice do +defmodule Edgehog.Containers.Volume.Changes.DeployVolumeOnDevice do @moduledoc false use Ash.Resource.Change alias Edgehog.Containers + alias Edgehog.Devices @impl Ash.Resource.Change - def change(changeset, _opts, _context) do - # After the transaction has been executed, i.e. all checks have passed - # and the deployment is in the data layer, start the deployment with - # the result. + def change(changeset, _opts, context) do + %{tenant: tenant} = context + Ash.Changeset.after_action(changeset, fn _changeset, deployment -> - with :ok <- Containers.send_deploy_request(deployment) do - {:ok, deployment} + with {:ok, deployment} <- Ash.load(deployment, [:device, :volume]), + {:ok, _device} <- + Devices.send_create_volume_request(deployment.device, deployment.volume) do + Containers.volume_deployment_sent(deployment, tenant: tenant) end end) end diff --git a/backend/lib/edgehog/containers/volume/deployment.ex b/backend/lib/edgehog/containers/volume/deployment.ex new file mode 100644 index 000000000..17c1b9cd8 --- /dev/null +++ b/backend/lib/edgehog/containers/volume/deployment.ex @@ -0,0 +1,107 @@ +# +# This file is part of Edgehog. +# +# Copyright 2024 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Containers.Volume.Deployment do + @moduledoc false + use Edgehog.MultitenantResource, + domain: Edgehog.Containers, + extensions: [AshGraphql.Resource] + + alias Edgehog.Containers.Volume.Changes + + graphql do + type :volume_deployment + end + + actions do + defaults [:read, :destroy, :create] + + create :deploy do + description """ + Deploys an image on a device, the status according to device triggers. + """ + + accept [:volume_id] + + argument :device_id, :id do + allow_nil? false + end + + change set_attribute(:state, :created) + change manage_relationship(:device_id, :device, type: :append) + change Changes.DeployVolumeOnDevice + end + + update :sent do + change set_attribute(:state, :sent) + end + + update :available do + change set_attribute(:state, :available) + end + + update :unavailable do + change set_attribute(:state, :unavailable) + end + + update :errored do + argument :message, :string do + allow_nil? false + end + + change set_attribute(:last_message, arg(:message)) + change set_attribute(:state, :error) + end + end + + attributes do + uuid_primary_key :id + + attribute :last_message, :string + + attribute :state, :atom, + constraints: [ + one_of: [:created, :sent, :available, :unavailable, :error] + ] + + timestamps() + end + + relationships do + belongs_to :volume, Edgehog.Containers.Volume do + attribute_type :uuid + public? true + end + + belongs_to :device, Edgehog.Devices.Device + end + + calculations do + calculate :ready?, :boolean, expr(state not in [:created, :sent, :error]) + end + + identities do + identity :volume_instance, [:volume_id, :device_id] + end + + postgres do + table "application_volume_deployments" + end +end diff --git a/backend/lib/edgehog/containers/volume.ex b/backend/lib/edgehog/containers/volume/volume.ex similarity index 88% rename from backend/lib/edgehog/containers/volume.ex rename to backend/lib/edgehog/containers/volume/volume.ex index 495c4fc93..33c48b2f6 100644 --- a/backend/lib/edgehog/containers/volume.ex +++ b/backend/lib/edgehog/containers/volume/volume.ex @@ -50,6 +50,13 @@ defmodule Edgehog.Containers.Volume do timestamps() end + relationships do + many_to_many :devices, Edgehog.Devices.Device do + through Edgehog.Containers.Volume.Deployment + join_relationship :volume_deployments + end + end + calculations do calculate :options_encoding, {:array, :string}, OptionsCalculation end diff --git a/backend/lib/edgehog/devices/device/device.ex b/backend/lib/edgehog/devices/device/device.ex index d660cbd36..354cf90d1 100644 --- a/backend/lib/edgehog/devices/device/device.ex +++ b/backend/lib/edgehog/devices/device/device.ex @@ -27,9 +27,9 @@ defmodule Edgehog.Devices.Device do ] alias Edgehog.Changes.NormalizeTagName - alias Edgehog.Containers.Deployment alias Edgehog.Containers.Image alias Edgehog.Containers.Release + alias Edgehog.Containers.Release.Deployment alias Edgehog.Containers.Volume alias Edgehog.Devices.Device.BatterySlot alias Edgehog.Devices.Device.Calculations @@ -254,7 +254,7 @@ defmodule Edgehog.Devices.Device do manual ManualActions.SetLedBehavior end - update :send_create_image do + update :send_create_image_request do description "Sends a create image request to the device." argument :image, :struct do @@ -401,12 +401,12 @@ defmodule Edgehog.Devices.Device do writable? false end - has_many :application_deployments, Edgehog.Containers.Deployment do + has_many :application_deployments, Edgehog.Containers.Release.Deployment do public? true end many_to_many :application_releases, Edgehog.Containers.Release do - through Edgehog.Containers.Deployment + through Edgehog.Containers.Release.Deployment join_relationship :application_deployments end end diff --git a/backend/lib/edgehog/devices/device/manual_actions/send_application_command.ex b/backend/lib/edgehog/devices/device/manual_actions/send_application_command.ex index 2501de559..5acffb354 100644 --- a/backend/lib/edgehog/devices/device/manual_actions/send_application_command.ex +++ b/backend/lib/edgehog/devices/device/manual_actions/send_application_command.ex @@ -23,7 +23,7 @@ defmodule Edgehog.Devices.Device.ManualActions.SendApplicationCommand do use Ash.Resource.ManualUpdate alias Edgehog.Astarte.Device.DeploymentCommand.RequestData - alias Edgehog.Containers.Deployment + alias Edgehog.Containers.Release.Deployment @deployment_command Application.compile_env( :edgehog, diff --git a/backend/lib/edgehog/devices/devices.ex b/backend/lib/edgehog/devices/devices.ex index c81fb7416..b9f48a867 100644 --- a/backend/lib/edgehog/devices/devices.ex +++ b/backend/lib/edgehog/devices/devices.ex @@ -85,9 +85,10 @@ defmodule Edgehog.Devices do resources do resource Device do define :fetch_device, action: :read, get_by: [:id] + define :fetch_device_by_identity, action: :read, get_by_identity: :unique_realm_device_id define :send_create_image_request, - action: :send_create_image, + action: :send_create_image_request, args: [:image] define :send_create_container_request, @@ -98,14 +99,14 @@ defmodule Edgehog.Devices do action: :send_create_network_request, args: [:network] - define :send_create_volume_request, - action: :send_create_volume_request, - args: [:volume] - define :send_create_deployment_request, action: :send_create_deployment_request, args: [:deployment] + define :send_create_volume_request, + action: :send_create_volume_request, + args: [:volume] + define :send_release_command, action: :send_release_command, args: [:release, :command] diff --git a/backend/lib/edgehog/pubsub.ex b/backend/lib/edgehog/pubsub.ex index 7c65ae9e2..b729e2637 100644 --- a/backend/lib/edgehog/pubsub.ex +++ b/backend/lib/edgehog/pubsub.ex @@ -25,7 +25,7 @@ defmodule Edgehog.PubSub do alias Edgehog.OSManagement.OTAOperation - @type event :: :ota_operation_created | :ota_operation_updated + @type event :: :ota_operation_created | :ota_operation_updated | :release_deployment_available @doc """ Publish an event to the PubSub. Raises if any of the publish fails. @@ -51,6 +51,10 @@ defmodule Edgehog.PubSub do broadcast_many!(topics, payload) end + def publish!(event, payload) do + broadcast_many!([topic_for_subject(event)], payload) + end + defp broadcast_many!(topics, payload) do Enum.each(topics, fn topic -> Phoenix.PubSub.broadcast!(Edgehog.PubSub, topic, payload) @@ -73,4 +77,5 @@ defmodule Edgehog.PubSub do defp topic_for_subject(%OTAOperation{id: id}), do: "ota_operations:#{id}" defp topic_for_subject({:ota_operation, id}), do: "ota_operations:#{id}" defp topic_for_subject(:ota_operations), do: "ota_operations:*" + defp topic_for_subject(subject) when is_atom(subject), do: Atom.to_string(subject) end diff --git a/backend/lib/edgehog/triggers/handler/manual_actions/handle_trigger.ex b/backend/lib/edgehog/triggers/handler/manual_actions/handle_trigger.ex index 25ac57090..d597ae6df 100644 --- a/backend/lib/edgehog/triggers/handler/manual_actions/handle_trigger.ex +++ b/backend/lib/edgehog/triggers/handler/manual_actions/handle_trigger.ex @@ -25,6 +25,7 @@ defmodule Edgehog.Triggers.Handler.ManualActions.HandleTrigger do alias Edgehog.Astarte alias Edgehog.Astarte.Realm alias Edgehog.Containers + alias Edgehog.Devices alias Edgehog.Devices.Device alias Edgehog.OSManagement alias Edgehog.Triggers.DeviceConnected @@ -41,15 +42,6 @@ defmodule Edgehog.Triggers.Handler.ManualActions.HandleTrigger do @ota_response "io.edgehog.devicemanager.OTAResponse" @system_info "io.edgehog.devicemanager.SystemInfo" - @initial_statuses [ - :created, - :sent, - :created_images, - :created_networks, - :created_containers, - :created_deployment - ] - @impl Ash.Resource.Actions.Implementation def run(input, _opts, _context) do realm_name = input.arguments.realm_name @@ -118,71 +110,58 @@ defmodule Edgehog.Triggers.Handler.ManualActions.HandleTrigger do |> Ash.create(tenant: tenant) end - defp handle_event(%IncomingData{interface: @available_images} = event, tenant, _realm_id, _device_id, _timestamp) do + defp handle_event(%IncomingData{interface: @available_images} = event, tenant, realm_id, device_id, _timestamp) do + device = Devices.fetch_device_by_identity!(device_id, realm_id, tenant: tenant) + case String.split(event.path, "/") do ["", image_id, "pulled"] -> - containers = Containers.containers_with_image!(image_id, tenant: tenant) - - releases = - containers - |> Enum.flat_map(&Containers.releases_with_container!(&1.id, tenant: tenant, load: :release)) - |> Enum.map(& &1.release_id) - |> Enum.uniq() - - deployments = - releases - |> Enum.flat_map(&Containers.deployments_with_release!(&1, tenant: tenant)) - |> Enum.uniq_by(& &1.id) - - {:ok, Enum.map(deployments, &Containers.deployment_update_status!/1)} + with {:ok, deployment} <- + Containers.fetch_image_deployment(image_id, device.id, tenant: tenant) do + if event.value do + Containers.image_deployment_pulled(deployment, tenant: tenant) + else + Containers.image_deployment_unpulled(deployment, tenant: tenant) + end + end _ -> {:error, :invalid_event_path} end end - defp handle_event(%IncomingData{interface: @available_networks} = event, tenant, _realm_id, _device_id, _timestamp) do + defp handle_event(%IncomingData{interface: @available_networks} = event, tenant, realm_id, device_id, _timestamp) do + device = Devices.fetch_device_by_identity!(device_id, realm_id, tenant: tenant) + case String.split(event.path, "/") do ["", network_id, "created"] -> - containers = - network_id - |> Containers.containers_with_network!(tenant: tenant, load: :container) - |> Enum.map(& &1.container_id) - |> Enum.uniq() - - releases = - containers - |> Enum.flat_map(&Containers.releases_with_container!(&1, tenant: tenant, load: :release)) - |> Enum.map(& &1.release_id) - |> Enum.uniq() - - deployments = - releases - |> Enum.flat_map(&Containers.deployments_with_release!(&1, tenant: tenant)) - |> Enum.uniq_by(& &1.id) - - {:ok, Enum.map(deployments, &Containers.deployment_update_status!/1)} + with {:ok, deployment} <- + Containers.fetch_network_deployment(network_id, device.id, tenant: tenant) do + if event.value do + Containers.network_deployment_available(deployment, tenant: tenant) + else + Containers.network_deployment_unavailable(deployment, tenant: tenant) + end + end _ -> {:error, :invalid_event_path} end end - defp handle_event(%IncomingData{interface: @available_containers} = event, tenant, _realm_id, _device_id, _timestamp) do + defp handle_event(%IncomingData{interface: @available_containers} = event, tenant, realm_id, device_id, _timestamp) do + device = Devices.fetch_device_by_identity!(device_id, realm_id, tenant: tenant) + case String.split(event.path, "/") do ["", container_id, "status"] -> - releases = - container_id - |> Containers.releases_with_container!(tenant: tenant, load: :release) - |> Enum.map(& &1.release_id) - |> Enum.uniq() - - deployments = - releases - |> Enum.flat_map(&Containers.deployments_with_release!(&1, tenant: tenant)) - |> Enum.uniq_by(& &1.id) - - {:ok, Enum.map(deployments, &Containers.deployment_update_status!/1)} + with {:ok, deployment} <- + Containers.fetch_container_deployment(container_id, device.id, tenant: tenant) do + case event.value do + "Received" -> Containers.container_deployment_received(deployment, tenant: tenant) + "Created" -> Containers.container_deployment_created(deployment, tenant: tenant) + "Stopped" -> Containers.container_deployment_stopped(deployment, tenant: tenant) + "Running" -> Containers.container_deployment_running(deployment, tenant: tenant) + end + end _ -> {:error, :invalid_event_path} @@ -198,7 +177,7 @@ defmodule Edgehog.Triggers.Handler.ManualActions.HandleTrigger do } = event.value with {:ok, deployment} <- Containers.fetch_deployment(deployment_id, tenant: tenant) do - case {deployment.status, status} do + case {deployment.state, status} do {:started, "Starting"} -> # Skip Starting if already Started {:ok, deployment} @@ -208,13 +187,13 @@ defmodule Edgehog.Triggers.Handler.ManualActions.HandleTrigger do {:ok, deployment} {_, "Error"} -> - # Errors have precedence - Containers.deployment_set_status(deployment, status, message, tenant: tenant) + Containers.release_deployment_error(deployment, message, tenant: tenant) + + {_, "Starting"} -> + Containers.release_deployment_starting(deployment, tenant: tenant) - _ -> - if deployment.status in @initial_statuses, - do: Containers.deployment_update_status(deployment, tenant: tenant), - else: Containers.deployment_set_status(deployment, status, message, tenant: tenant) + {_, "Stopping"} -> + Containers.release_deployment_stopping(deployment, tenant: tenant) end end end @@ -222,18 +201,19 @@ defmodule Edgehog.Triggers.Handler.ManualActions.HandleTrigger do defp handle_event(%IncomingData{interface: @available_deployments} = event, tenant, _realm_id, _device_id, _timestamp) do case String.split(event.path, "/") do ["", deployment_id, "status"] -> - status = event.value - with {:ok, deployment} <- Containers.fetch_deployment(deployment_id, tenant: tenant) do - cond do - status == nil -> - Containers.delete_deployment(deployment) + case event.value do + "Started" -> + Containers.release_deployment_started(deployment, tenant: tenant) + + "Stopped" -> + Containers.release_deployment_stopped(deployment, tenant: tenant) - deployment.status in @initial_statuses -> - Containers.deployment_update_status(deployment, tenant: tenant) + nil -> + Containers.delete_deployment(deployment, tenant: tenant) - true -> - Containers.deployment_set_status(deployment, status, deployment.message, tenant: tenant) + _ -> + {:error, :unsupported_event_value} end end diff --git a/backend/mix.exs b/backend/mix.exs index 465b26c24..22fa0da21 100644 --- a/backend/mix.exs +++ b/backend/mix.exs @@ -118,7 +118,8 @@ defmodule Edgehog.MixProject do {:picosat_elixir, "~> 0.2"}, {:styler, "~> 1.0.0-rc.1", only: [:dev, :test], runtime: false}, {:open_api_spex, "~> 3.16"}, - {:ymlr, "~> 5.1"} + {:ymlr, "~> 5.1"}, + {:ash_state_machine, "~> 0.2.7"} ] end diff --git a/backend/mix.lock b/backend/mix.lock index adad5dc03..9f54c0b84 100644 --- a/backend/mix.lock +++ b/backend/mix.lock @@ -7,6 +7,7 @@ "ash_json_api": {:hex, :ash_json_api, "1.4.7", "ced9c146e6e7a4ab2e9891efb133bcaaf031ac7b05541a41d549262af2988ee7", [:mix], [{:ash, "~> 3.3", [hex: :ash, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.14 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}, {:json_xema, "~> 0.4", [hex: :json_xema, repo: "hexpm", optional: false]}, {:open_api_spex, "~> 3.16", [hex: :open_api_spex, repo: "hexpm", optional: true]}, {:plug, "~> 1.11", [hex: :plug, repo: "hexpm", optional: false]}, {:spark, ">= 2.2.10 and < 3.0.0-0", [hex: :spark, repo: "hexpm", optional: false]}], "hexpm", "a00f8657e2492d87d7e7e851302ee320af21f12fc104a49cace972c804df8f4a"}, "ash_postgres": {:hex, :ash_postgres, "2.3.0", "85b786019f75d12f63af00a0a7cf15d885820b1279561e2a33fbb59df1aa4af5", [:mix], [{:ash, ">= 3.4.2 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ash_sql, ">= 0.2.30 and < 1.0.0-0", [hex: :ash_sql, repo: "hexpm", optional: false]}, {:ecto, ">= 3.12.1 and < 4.0.0-0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.12", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:igniter, ">= 0.3.6 and < 1.0.0-0", [hex: :igniter, repo: "hexpm", optional: false]}, {:inflex, "~> 2.1", [hex: :inflex, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: false]}, {:owl, "~> 0.11", [hex: :owl, repo: "hexpm", optional: false]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: false]}], "hexpm", "e7f7ada1978239bfbf60ed3d4af76c34d427278aff0aa51552ac2196c7460394"}, "ash_sql": {:hex, :ash_sql, "0.2.32", "de99255becfb9daa7991c18c870e9f276bb372acda7eda3e05c3e2ff2ca8922e", [:mix], [{:ash, ">= 3.1.7 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}, {:ecto, "~> 3.9", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "~> 3.9", [hex: :ecto_sql, repo: "hexpm", optional: false]}], "hexpm", "43773bcd33d21319c11804d76fe11f1a1b7c8faba7aaedeab6f55fde3d2405db"}, + "ash_state_machine": {:hex, :ash_state_machine, "0.2.7", "76247e89d4eaa6ed1db8dea35285d817fd3066afa6023b1d792bdd278b0c29d8", [:mix], [{:ash, ">= 3.1.4 and < 4.0.0-0", [hex: :ash, repo: "hexpm", optional: false]}], "hexpm", "c0cd8af5fb18d3bfb9005cc1c2448fe2036d3da45abbb308d28d2cbcfa3f16e9"}, "astarte_client": {:git, "https://github.com/astarte-platform/astarte-client-elixir.git", "e7e66963eb6d977f73309f95b0b9d04506dff358", []}, "bunt": {:hex, :bunt, "1.0.0", "081c2c665f086849e6d57900292b3a161727ab40431219529f13c4ddcf3e7a44", [:mix], [], "hexpm", "dc5f86aa08a5f6fa6b8096f0735c4e76d54ae5c9fa2c143e5a1fc7c1cd9bb6b5"}, "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, diff --git a/backend/priv/repo/migrations/20250203092534_deployment_refactor.exs b/backend/priv/repo/migrations/20250203092534_deployment_refactor.exs new file mode 100644 index 000000000..6f7d0747e --- /dev/null +++ b/backend/priv/repo/migrations/20250203092534_deployment_refactor.exs @@ -0,0 +1,464 @@ +# +# This file is part of Edgehog. +# +# Copyright 2025 SECO Mind Srl +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +defmodule Edgehog.Repo.Migrations.DeploymentRefactor do + @moduledoc """ + Updates resources based on their most recent snapshots. + + This file was autogenerated with `mix ash_postgres.generate_migrations` + """ + + use Ecto.Migration + + def up do + drop constraint(:deployment_ready_actions, "deployment_ready_actions_deployment_id_fkey") + + create table(:release_deployments, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + end + + alter table(:deployment_ready_actions) do + modify :deployment_id, + references(:release_deployments, + column: :id, + name: "deployment_ready_actions_deployment_id_fkey", + type: :uuid, + prefix: "public", + on_delete: :delete_all + ) + end + + alter table(:release_deployments) do + add :last_message, :text + add :state, :text + + add :inserted_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :updated_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :tenant_id, + references(:tenants, + column: :tenant_id, + name: "release_deployments_tenant_id_fkey", + type: :bigint, + prefix: "public", + on_delete: :delete_all + ), + null: false + + add :device_id, + references(:devices, + column: :id, + name: "release_deployments_device_id_fkey", + type: :bigint, + prefix: "public", + on_delete: :delete_all + ) + + add :release_id, + references(:application_releases, + column: :id, + name: "release_deployments_release_id_fkey", + type: :uuid, + prefix: "public", + on_delete: :delete_all + ) + end + + create index(:release_deployments, [:tenant_id]) + + create index(:release_deployments, [:id, :tenant_id], unique: true) + + create unique_index(:release_deployments, [:tenant_id, :device_id, :release_id], + name: "release_deployments_release_instance_index" + ) + + create table(:image_deployments, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + add :last_message, :text + add :state, :text + + add :inserted_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :updated_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :tenant_id, + references(:tenants, + column: :tenant_id, + name: "image_deployments_tenant_id_fkey", + type: :bigint, + prefix: "public", + on_delete: :delete_all + ), + null: false + + add :image_id, + references(:images, + column: :id, + name: "image_deployments_image_id_fkey", + type: :uuid, + prefix: "public", + on_delete: :delete_all + ) + + add :device_id, + references(:devices, + column: :id, + name: "image_deployments_device_id_fkey", + type: :bigint, + prefix: "public", + on_delete: :delete_all + ) + end + + create index(:image_deployments, [:tenant_id]) + + create index(:image_deployments, [:id, :tenant_id], unique: true) + + create unique_index(:image_deployments, [:tenant_id, :image_id, :device_id], + name: "image_deployments_image_instance_index" + ) + + create table(:container_deployments, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + add :last_message, :text + add :state, :text + + add :inserted_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :updated_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :tenant_id, + references(:tenants, + column: :tenant_id, + name: "container_deployments_tenant_id_fkey", + type: :bigint, + prefix: "public", + on_delete: :delete_all + ), + null: false + + add :container_id, + references(:containers, + column: :id, + name: "container_deployments_container_id_fkey", + type: :uuid, + prefix: "public", + on_delete: :delete_all + ) + + add :device_id, + references(:devices, + column: :id, + name: "container_deployments_device_id_fkey", + type: :bigint, + prefix: "public", + on_delete: :delete_all + ) + end + + create index(:container_deployments, [:tenant_id]) + + create index(:container_deployments, [:id, :tenant_id], unique: true) + + create unique_index(:container_deployments, [:tenant_id, :container_id, :device_id], + name: "container_deployments_container_instance_index" + ) + + create table(:ready_action_upgrades, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + + add :tenant_id, + references(:tenants, + column: :tenant_id, + name: "ready_action_upgrades_tenant_id_fkey", + type: :bigint, + prefix: "public", + on_delete: :delete_all + ), + null: false + + add :upgrade_target_id, + references(:release_deployments, + column: :id, + name: "ready_action_upgrades_upgrade_target_id_fkey", + type: :uuid, + prefix: "public" + ), + null: false + + add :ready_action_id, + references(:deployment_ready_actions, + column: :id, + name: "ready_action_upgrades_ready_action_id_fkey", + type: :uuid, + prefix: "public", + on_delete: :delete_all + ), + null: false + end + + create index(:ready_action_upgrades, [:tenant_id]) + + create index(:ready_action_upgrades, [:id, :tenant_id], unique: true) + + create table(:application_volume_deployments, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + add :last_message, :text + add :state, :text + + add :inserted_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :updated_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :tenant_id, + references(:tenants, + column: :tenant_id, + name: "application_volume_deployments_tenant_id_fkey", + type: :bigint, + prefix: "public", + on_delete: :delete_all + ), + null: false + + add :volume_id, + references(:volumes, + column: :id, + name: "application_volume_deployments_volume_id_fkey", + type: :uuid, + prefix: "public" + ) + + add :device_id, + references(:devices, + column: :id, + name: "application_volume_deployments_device_id_fkey", + type: :bigint, + prefix: "public" + ) + end + + create index(:application_volume_deployments, [:tenant_id]) + + create index(:application_volume_deployments, [:id, :tenant_id], unique: true) + + create unique_index(:application_volume_deployments, [:tenant_id, :volume_id, :device_id], + name: "application_volume_deployments_volume_instance_index" + ) + + create table(:network_deployments, primary_key: false) do + add :id, :uuid, null: false, default: fragment("gen_random_uuid()"), primary_key: true + add :last_message, :text + add :state, :text + + add :inserted_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :updated_at, :utc_datetime_usec, + null: false, + default: fragment("(now() AT TIME ZONE 'utc')") + + add :tenant_id, + references(:tenants, + column: :tenant_id, + name: "network_deployments_tenant_id_fkey", + type: :bigint, + prefix: "public", + on_delete: :delete_all + ), + null: false + + add :network_id, + references(:networks, + column: :id, + name: "network_deployments_network_id_fkey", + type: :uuid, + prefix: "public", + on_delete: :delete_all + ) + + add :device_id, + references(:devices, + column: :id, + name: "network_deployments_device_id_fkey", + type: :bigint, + prefix: "public", + on_delete: :delete_all + ) + end + + create index(:network_deployments, [:tenant_id]) + + create index(:network_deployments, [:id, :tenant_id], unique: true) + + create unique_index(:network_deployments, [:tenant_id, :network_id, :device_id], + name: "network_deployments_network_instance_index" + ) + + execute( + "ALTER TABLE deployment_ready_actions alter CONSTRAINT deployment_ready_actions_deployment_id_fkey NOT DEFERRABLE" + ) + end + + def down do + drop_if_exists unique_index(:network_deployments, [:tenant_id, :network_id, :device_id], + name: "network_deployments_network_instance_index" + ) + + drop constraint(:network_deployments, "network_deployments_tenant_id_fkey") + + drop constraint(:network_deployments, "network_deployments_network_id_fkey") + + drop constraint(:network_deployments, "network_deployments_device_id_fkey") + + drop_if_exists index(:network_deployments, [:id, :tenant_id]) + + drop_if_exists index(:network_deployments, [:tenant_id]) + + drop table(:network_deployments) + + drop_if_exists unique_index( + :application_volume_deployments, + [:tenant_id, :volume_id, :device_id], + name: "application_volume_deployments_volume_instance_index" + ) + + drop constraint( + :application_volume_deployments, + "application_volume_deployments_tenant_id_fkey" + ) + + drop constraint( + :application_volume_deployments, + "application_volume_deployments_volume_id_fkey" + ) + + drop constraint( + :application_volume_deployments, + "application_volume_deployments_device_id_fkey" + ) + + drop_if_exists index(:application_volume_deployments, [:id, :tenant_id]) + + drop_if_exists index(:application_volume_deployments, [:tenant_id]) + + drop table(:application_volume_deployments) + + drop constraint(:ready_action_upgrades, "ready_action_upgrades_tenant_id_fkey") + + drop constraint(:ready_action_upgrades, "ready_action_upgrades_upgrade_target_id_fkey") + + drop constraint(:ready_action_upgrades, "ready_action_upgrades_ready_action_id_fkey") + + drop_if_exists index(:ready_action_upgrades, [:id, :tenant_id]) + + drop_if_exists index(:ready_action_upgrades, [:tenant_id]) + + drop table(:ready_action_upgrades) + + drop_if_exists unique_index(:container_deployments, [:tenant_id, :container_id, :device_id], + name: "container_deployments_container_instance_index" + ) + + drop constraint(:container_deployments, "container_deployments_tenant_id_fkey") + + drop constraint(:container_deployments, "container_deployments_container_id_fkey") + + drop constraint(:container_deployments, "container_deployments_device_id_fkey") + + drop_if_exists index(:container_deployments, [:id, :tenant_id]) + + drop_if_exists index(:container_deployments, [:tenant_id]) + + drop table(:container_deployments) + + drop_if_exists unique_index(:image_deployments, [:tenant_id, :image_id, :device_id], + name: "image_deployments_image_instance_index" + ) + + drop constraint(:image_deployments, "image_deployments_tenant_id_fkey") + + drop constraint(:image_deployments, "image_deployments_image_id_fkey") + + drop constraint(:image_deployments, "image_deployments_device_id_fkey") + + drop_if_exists index(:image_deployments, [:id, :tenant_id]) + + drop_if_exists index(:image_deployments, [:tenant_id]) + + drop table(:image_deployments) + + drop_if_exists unique_index(:release_deployments, [:tenant_id, :device_id, :release_id], + name: "release_deployments_release_instance_index" + ) + + drop constraint(:release_deployments, "release_deployments_tenant_id_fkey") + + drop constraint(:release_deployments, "release_deployments_device_id_fkey") + + drop constraint(:release_deployments, "release_deployments_release_id_fkey") + + drop_if_exists index(:release_deployments, [:id, :tenant_id]) + + drop_if_exists index(:release_deployments, [:tenant_id]) + + alter table(:release_deployments) do + remove :release_id + remove :device_id + remove :tenant_id + remove :updated_at + remove :inserted_at + remove :state + remove :last_message + end + + drop constraint(:deployment_ready_actions, "deployment_ready_actions_deployment_id_fkey") + + alter table(:deployment_ready_actions) do + modify :deployment_id, + references(:application_deployments, + column: :id, + name: "deployment_ready_actions_deployment_id_fkey", + type: :uuid, + prefix: "public", + on_delete: :delete_all + ) + end + + drop table(:release_deployments) + end +end diff --git a/backend/priv/repo/seeds.exs b/backend/priv/repo/seeds.exs index afa66b274..ee8c59bfb 100644 --- a/backend/priv/repo/seeds.exs +++ b/backend/priv/repo/seeds.exs @@ -22,11 +22,11 @@ alias Edgehog.Astarte alias Edgehog.Containers.Application alias Edgehog.Containers.Container alias Edgehog.Containers.ContainerNetwork -alias Edgehog.Containers.Deployment alias Edgehog.Containers.Image alias Edgehog.Containers.ImageCredentials alias Edgehog.Containers.Network alias Edgehog.Containers.Release +alias Edgehog.Containers.Release.Deployment alias Edgehog.Containers.ReleaseContainers alias Edgehog.Devices.Device alias Edgehog.Tenants diff --git a/backend/priv/resource_snapshots/repo/application_volume_deployments/20250203092534.json b/backend/priv/resource_snapshots/repo/application_volume_deployments/20250203092534.json new file mode 100644 index 000000000..15214718c --- /dev/null +++ b/backend/priv/resource_snapshots/repo/application_volume_deployments/20250203092534.json @@ -0,0 +1,225 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "last_message", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "tenant_id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "application_volume_deployments_tenant_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "tenants" + }, + "size": null, + "source": "tenant_id", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "application_volume_deployments_volume_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "volumes" + }, + "size": null, + "source": "volume_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "application_volume_deployments_device_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "devices" + }, + "size": null, + "source": "device_id", + "type": "bigint" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [ + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "id", + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "id" + }, + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": true, + "using": null, + "where": null + }, + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + } + ], + "custom_statements": [], + "has_create_action": true, + "hash": "85249D650D48911EFA29C59290578619ED965F725884478131BE63DA84971F93", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "application_volume_deployments_volume_instance_index", + "keys": [ + { + "type": "atom", + "value": "volume_id" + }, + { + "type": "atom", + "value": "device_id" + } + ], + "name": "volume_instance", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "repo": "Elixir.Edgehog.Repo", + "schema": null, + "table": "application_volume_deployments" +} \ No newline at end of file diff --git a/backend/priv/resource_snapshots/repo/container_deployments/20250203092534.json b/backend/priv/resource_snapshots/repo/container_deployments/20250203092534.json new file mode 100644 index 000000000..3f767341c --- /dev/null +++ b/backend/priv/resource_snapshots/repo/container_deployments/20250203092534.json @@ -0,0 +1,225 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "last_message", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "tenant_id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "container_deployments_tenant_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "tenants" + }, + "size": null, + "source": "tenant_id", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "container_deployments_container_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "containers" + }, + "size": null, + "source": "container_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "container_deployments_device_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "devices" + }, + "size": null, + "source": "device_id", + "type": "bigint" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [ + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "id", + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "id" + }, + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": true, + "using": null, + "where": null + }, + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + } + ], + "custom_statements": [], + "has_create_action": true, + "hash": "EE15CF5A467BCE6B39DFD7AD3701FA70A18E68018A422BE5E48977B0E641397E", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "container_deployments_container_instance_index", + "keys": [ + { + "type": "atom", + "value": "container_id" + }, + { + "type": "atom", + "value": "device_id" + } + ], + "name": "container_instance", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "repo": "Elixir.Edgehog.Repo", + "schema": null, + "table": "container_deployments" +} \ No newline at end of file diff --git a/backend/priv/resource_snapshots/repo/deployment_ready_actions/20250203092534.json b/backend/priv/resource_snapshots/repo/deployment_ready_actions/20250203092534.json new file mode 100644 index 000000000..9ad17dc1d --- /dev/null +++ b/backend/priv/resource_snapshots/repo/deployment_ready_actions/20250203092534.json @@ -0,0 +1,147 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "action_type", + "type": "text" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "tenant_id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "deployment_ready_actions_tenant_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "tenants" + }, + "size": null, + "source": "tenant_id", + "type": "bigint" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "deployment_ready_actions_deployment_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "release_deployments" + }, + "size": null, + "source": "deployment_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [ + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "id", + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "id" + }, + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": true, + "using": null, + "where": null + }, + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + } + ], + "custom_statements": [], + "has_create_action": true, + "hash": "61454401B39CA3FFCFAD93374AB3515350A502323BD0365476642E64F1D33BA2", + "identities": [], + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "repo": "Elixir.Edgehog.Repo", + "schema": null, + "table": "deployment_ready_actions" +} \ No newline at end of file diff --git a/backend/priv/resource_snapshots/repo/image_deployments/20250203092534.json b/backend/priv/resource_snapshots/repo/image_deployments/20250203092534.json new file mode 100644 index 000000000..1f77ad6a0 --- /dev/null +++ b/backend/priv/resource_snapshots/repo/image_deployments/20250203092534.json @@ -0,0 +1,225 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "last_message", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "tenant_id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "image_deployments_tenant_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "tenants" + }, + "size": null, + "source": "tenant_id", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "image_deployments_image_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "images" + }, + "size": null, + "source": "image_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "image_deployments_device_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "devices" + }, + "size": null, + "source": "device_id", + "type": "bigint" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [ + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "id", + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "id" + }, + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": true, + "using": null, + "where": null + }, + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + } + ], + "custom_statements": [], + "has_create_action": true, + "hash": "9D9A675DD222160A8A4F30E5EE556402B393B45E4B36C2235ACEF944741EB20D", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "image_deployments_image_instance_index", + "keys": [ + { + "type": "atom", + "value": "image_id" + }, + { + "type": "atom", + "value": "device_id" + } + ], + "name": "image_instance", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "repo": "Elixir.Edgehog.Repo", + "schema": null, + "table": "image_deployments" +} \ No newline at end of file diff --git a/backend/priv/resource_snapshots/repo/network_deployments/20250203092534.json b/backend/priv/resource_snapshots/repo/network_deployments/20250203092534.json new file mode 100644 index 000000000..8a1703b80 --- /dev/null +++ b/backend/priv/resource_snapshots/repo/network_deployments/20250203092534.json @@ -0,0 +1,225 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "last_message", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "tenant_id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "network_deployments_tenant_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "tenants" + }, + "size": null, + "source": "tenant_id", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "network_deployments_network_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "networks" + }, + "size": null, + "source": "network_id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "network_deployments_device_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "devices" + }, + "size": null, + "source": "device_id", + "type": "bigint" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [ + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "id", + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "id" + }, + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": true, + "using": null, + "where": null + }, + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + } + ], + "custom_statements": [], + "has_create_action": true, + "hash": "224B03E138EE4580DFD410B8BC58157B34AFAA5296CEBB239A6B60BC76A87321", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "network_deployments_network_instance_index", + "keys": [ + { + "type": "atom", + "value": "network_id" + }, + { + "type": "atom", + "value": "device_id" + } + ], + "name": "network_instance", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "repo": "Elixir.Edgehog.Repo", + "schema": null, + "table": "network_deployments" +} \ No newline at end of file diff --git a/backend/priv/resource_snapshots/repo/ready_action_upgrades/20250203092534.json b/backend/priv/resource_snapshots/repo/ready_action_upgrades/20250203092534.json new file mode 100644 index 000000000..ad9b67290 --- /dev/null +++ b/backend/priv/resource_snapshots/repo/ready_action_upgrades/20250203092534.json @@ -0,0 +1,166 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "tenant_id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "ready_action_upgrades_tenant_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "tenants" + }, + "size": null, + "source": "tenant_id", + "type": "bigint" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "ready_action_upgrades_upgrade_target_id_fkey", + "on_delete": null, + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "release_deployments" + }, + "size": null, + "source": "upgrade_target_id", + "type": "uuid" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "ready_action_upgrades_ready_action_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "deployment_ready_actions" + }, + "size": null, + "source": "ready_action_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [ + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "id", + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "id" + }, + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": true, + "using": null, + "where": null + }, + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + } + ], + "custom_statements": [], + "has_create_action": true, + "hash": "670EE0622CB41ECBD80189410B4D26F5E21E36CF4BD84B0152D17EC6D0786B83", + "identities": [], + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "repo": "Elixir.Edgehog.Repo", + "schema": null, + "table": "ready_action_upgrades" +} \ No newline at end of file diff --git a/backend/priv/resource_snapshots/repo/release_deployments/20250203092534.json b/backend/priv/resource_snapshots/repo/release_deployments/20250203092534.json new file mode 100644 index 000000000..20e8c16f0 --- /dev/null +++ b/backend/priv/resource_snapshots/repo/release_deployments/20250203092534.json @@ -0,0 +1,225 @@ +{ + "attributes": [ + { + "allow_nil?": false, + "default": "fragment(\"gen_random_uuid()\")", + "generated?": false, + "primary_key?": true, + "references": null, + "size": null, + "source": "id", + "type": "uuid" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "last_message", + "type": "text" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "state", + "type": "text" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "inserted_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "fragment(\"(now() AT TIME ZONE 'utc')\")", + "generated?": false, + "primary_key?": false, + "references": null, + "size": null, + "source": "updated_at", + "type": "utc_datetime_usec" + }, + { + "allow_nil?": false, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "tenant_id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": null, + "global": null, + "strategy": null + }, + "name": "release_deployments_tenant_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "tenants" + }, + "size": null, + "source": "tenant_id", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "release_deployments_device_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "devices" + }, + "size": null, + "source": "device_id", + "type": "bigint" + }, + { + "allow_nil?": true, + "default": "nil", + "generated?": false, + "primary_key?": false, + "references": { + "deferrable": false, + "destination_attribute": "id", + "destination_attribute_default": null, + "destination_attribute_generated": null, + "index?": false, + "match_type": null, + "match_with": null, + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "name": "release_deployments_release_id_fkey", + "on_delete": "delete", + "on_update": null, + "primary_key?": true, + "schema": "public", + "table": "application_releases" + }, + "size": null, + "source": "release_id", + "type": "uuid" + } + ], + "base_filter": null, + "check_constraints": [], + "custom_indexes": [ + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "id", + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "id" + }, + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": true, + "using": null, + "where": null + }, + { + "all_tenants?": true, + "concurrently": false, + "error_fields": [ + "tenant_id" + ], + "fields": [ + { + "type": "atom", + "value": "tenant_id" + } + ], + "include": null, + "message": null, + "name": null, + "nulls_distinct": true, + "prefix": null, + "table": null, + "unique": false, + "using": null, + "where": null + } + ], + "custom_statements": [], + "has_create_action": true, + "hash": "CA3FF2EDE813DFEA1F7DB62221F5031E5CA2169EBD2689A66A17010616038952", + "identities": [ + { + "all_tenants?": false, + "base_filter": null, + "index_name": "release_deployments_release_instance_index", + "keys": [ + { + "type": "atom", + "value": "device_id" + }, + { + "type": "atom", + "value": "release_id" + } + ], + "name": "release_instance", + "nils_distinct?": true, + "where": null + } + ], + "multitenancy": { + "attribute": "tenant_id", + "global": false, + "strategy": "attribute" + }, + "repo": "Elixir.Edgehog.Repo", + "schema": null, + "table": "release_deployments" +} \ No newline at end of file diff --git a/backend/test/edgehog_web/controllers/astarte_trigger_controller_test.exs b/backend/test/edgehog_web/controllers/astarte_trigger_controller_test.exs index e75d96458..c602031f4 100644 --- a/backend/test/edgehog_web/controllers/astarte_trigger_controller_test.exs +++ b/backend/test/edgehog_web/controllers/astarte_trigger_controller_test.exs @@ -26,8 +26,14 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do import Edgehog.DevicesFixtures import Edgehog.OSManagementFixtures + alias Edgehog.Astarte.Device.CreateContainerRequestMock + alias Edgehog.Astarte.Device.CreateDeploymentRequestMock + alias Edgehog.Astarte.Device.CreateImageRequestMock + alias Edgehog.Astarte.Device.CreateNetworkRequestMock + alias Edgehog.Astarte.Device.CreateVolumeRequestMock alias Edgehog.Astarte.Device.DeviceStatusMock - alias Edgehog.Containers.Deployment + alias Edgehog.Containers + alias Edgehog.Containers.Release.Deployment alias Edgehog.Devices.Device alias Edgehog.OSManagement @@ -402,7 +408,7 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do end end - describe "process_event for deployment updates" do + describe "deployment process" do setup %{tenant: tenant} do cluster = cluster_fixture() realm = realm_fixture(cluster_id: cluster.id, tenant: tenant) @@ -411,7 +417,7 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do {:ok, cluster: cluster, realm: realm, device: device} end - test "updates the deployment status", context do + test "works fine when all the resources are available", context do %{ conn: conn, realm: realm, @@ -419,18 +425,67 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do tenant: tenant } = context - deployment = deployment_fixture(tenant: tenant, device_id: device.id) + # One container for the release + containters = 1 + + release = + [tenant: tenant, containers: containters] + |> release_fixture() + |> Ash.load!([:containers]) + [container] = Enum.map(release.containers, &Ash.load!(&1, [:image, :networks])) + + image = container.image + [network] = container.networks + + expect(CreateImageRequestMock, :send_create_image_request, fn _, _, _ -> :ok end) + + expect(CreateVolumeRequestMock, :send_create_volume_request, fn _, _, _ -> :ok end) + + expect(CreateContainerRequestMock, :send_create_container_request, fn _, _, _ -> + :ok + end) + + expect(CreateNetworkRequestMock, :send_create_network_request, fn _, _, _ -> + :ok + end) + + expect(CreateDeploymentRequestMock, :send_create_deployment_request, fn _, _, _ -> + :ok + end) + + deployment = + release.id + |> Containers.deploy!(device.id, tenant: tenant) + |> Ash.load!(:ready?, tenant: tenant) + + container_deployment = + Containers.fetch_container_deployment!(container.id, device.id, + tenant: tenant, + load: [:ready?] + ) + + image_deployment = + Containers.fetch_image_deployment!(image.id, device.id, tenant: tenant, load: [:ready?]) + + network_deployment = + Containers.fetch_network_deployment(network.id, device.id, + tenant: tenant, + load: [:ready?] + ) + + # Deployment is initially not ready + refute deployment.ready? + refute image_deployment.ready? + + # Send available image from the device deployment_event = %{ device_id: device.device_id, event: %{ type: "incoming_data", - interface: "io.edgehog.devicemanager.apps.DeploymentEvent", - path: "/" <> deployment.id, - value: %{ - "status" => "Error", - "message" => "error message" - } + interface: "io.edgehog.devicemanager.apps.AvailableImages", + path: "/" <> image.id <> "/pulled", + value: false }, timestamp: DateTime.to_iso8601(DateTime.utc_now()) } @@ -442,22 +497,77 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do |> post(path, deployment_event) |> response(200) - # Deployment must be reloaded from the db - deployment = Ash.get!(Edgehog.Containers.Deployment, deployment.id, tenant: tenant) + deployment = Ash.load!(deployment, :ready?, tenant: tenant, reuse_values?: false) + refute deployment.ready? + + # Image deployment is ready once available image is sent + image_deployment = + Ash.load!(image_deployment, :ready?, tenant: tenant, reuse_values?: false) + + assert image_deployment.ready? + + container_deployment = + Ash.load!(container_deployment, :ready?, tenant: tenant, reuse_values?: false) - assert deployment.status == :error - assert deployment.message == "error message" + refute container_deployment.ready? deployment_event = %{ device_id: device.device_id, event: %{ type: "incoming_data", - interface: "io.edgehog.devicemanager.apps.DeploymentEvent", - path: "/" <> deployment.id, - value: %{ - "status" => "Starting", - "message" => "" - } + interface: "io.edgehog.devicemanager.apps.AvailableContainers", + path: "/" <> container.id <> "/status", + value: "Received" + }, + timestamp: DateTime.to_iso8601(DateTime.utc_now()) + } + + path = Routes.astarte_trigger_path(conn, :process_event, tenant.slug) + + conn + |> put_req_header("astarte-realm", realm.name) + |> post(path, deployment_event) + |> response(200) + + deployment = Ash.load!(deployment, :ready?, tenant: tenant, reuse_values?: false) + refute deployment.ready? + + container_deployment = + Ash.load!(container_deployment, :ready?, tenant: tenant, reuse_values?: false) + + refute container_deployment.ready? + + deployment_event = %{ + device_id: device.device_id, + event: %{ + type: "incoming_data", + interface: "io.edgehog.devicemanager.apps.AvailableDeployments", + path: "/" <> deployment.id <> "/status", + value: "Stopped" + }, + timestamp: DateTime.to_iso8601(DateTime.utc_now()) + } + + path = Routes.astarte_trigger_path(conn, :process_event, tenant.slug) + + conn + |> put_req_header("astarte-realm", realm.name) + |> post(path, deployment_event) + |> response(200) + + deployment = Ash.load!(deployment, :ready?, tenant: tenant, reuse_values?: false) + refute deployment.ready? + + network_deployment = Ash.load!(network_deployment, :ready?, tenant: tenant) + refute network_deployment.ready? + + deployment_event = %{ + device_id: device.device_id, + event: %{ + type: "incoming_data", + interface: "io.edgehog.devicemanager.apps.AvailableNetworks", + path: "/" <> network.id <> "/created", + value: true }, timestamp: DateTime.to_iso8601(DateTime.utc_now()) } @@ -469,13 +579,24 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do |> post(path, deployment_event) |> response(200) - deployment = Ash.get!(Edgehog.Containers.Deployment, deployment.id, tenant: tenant) + network_deployment = Ash.load!(network_deployment, :ready?, tenant: tenant) + assert network_deployment.ready? - assert deployment.status == :starting - assert deployment.message == nil + deployment = Ash.load!(deployment, :ready?, tenant: tenant, reuse_values?: false) + assert deployment.ready? end + end - test "Starting status does not update a Started deployment", context do + describe "process_event for deployment updates" do + setup %{tenant: tenant} do + cluster = cluster_fixture() + realm = realm_fixture(cluster_id: cluster.id, tenant: tenant) + device = device_fixture(realm_id: realm.id, tenant: tenant) + + {:ok, cluster: cluster, realm: realm, device: device} + end + + test "updates the deployment status", context do %{ conn: conn, realm: realm, @@ -483,7 +604,7 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do tenant: tenant } = context - deployment = deployment_fixture(tenant: tenant, device_id: device.id, status: :started) + deployment = deployment_fixture(tenant: tenant, device_id: device.id) deployment_event = %{ device_id: device.device_id, @@ -492,8 +613,8 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do interface: "io.edgehog.devicemanager.apps.DeploymentEvent", path: "/" <> deployment.id, value: %{ - "status" => "Starting", - "message" => nil + "status" => "Error", + "message" => "error message" } }, timestamp: DateTime.to_iso8601(DateTime.utc_now()) @@ -507,21 +628,10 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do |> response(200) # Deployment must be reloaded from the db - deployment = Ash.get!(Edgehog.Containers.Deployment, deployment.id, tenant: tenant) + deployment = Ash.get!(Edgehog.Containers.Release.Deployment, deployment.id, tenant: tenant) - assert deployment.status == :started - assert deployment.message == nil - end - - test "Stopping status does not update a Stopped deployment", context do - %{ - conn: conn, - realm: realm, - device: device, - tenant: tenant - } = context - - deployment = deployment_fixture(tenant: tenant, device_id: device.id, status: :stopped) + assert deployment.state == :error + assert deployment.last_message == "error message" deployment_event = %{ device_id: device.device_id, @@ -530,8 +640,8 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do interface: "io.edgehog.devicemanager.apps.DeploymentEvent", path: "/" <> deployment.id, value: %{ - "status" => "Stopping", - "message" => nil + "status" => "Starting", + "message" => "" } }, timestamp: DateTime.to_iso8601(DateTime.utc_now()) @@ -544,44 +654,36 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do |> post(path, deployment_event) |> response(200) - # Deployment must be reloaded from the db - deployment = Ash.get!(Edgehog.Containers.Deployment, deployment.id, tenant: tenant) + deployment = Ash.get!(Edgehog.Containers.Release.Deployment, deployment.id, tenant: tenant) - assert deployment.status == :stopped - assert deployment.message == nil + assert deployment.state == :starting + assert deployment.last_message != nil end - test "AvailableImages triggers update deployment status", context do - %{conn: conn, realm: realm, device: device, tenant: tenant} = context - - release = - [containers: 1, tenant: tenant] - |> release_fixture() - |> Ash.load!(containers: [:image, :networks]) + test "Starting status does not update a Started deployment", context do + %{ + conn: conn, + realm: realm, + device: device, + tenant: tenant + } = context - [container] = release.containers - - deployment = - deployment_fixture( - tenant: tenant, - device_id: device.id, - release_id: release.id, - status: :sent - ) + deployment = deployment_fixture(tenant: tenant, device_id: device.id, state: :started) deployment_event = %{ device_id: device.device_id, event: %{ type: "incoming_data", - interface: "io.edgehog.devicemanager.apps.AvailableImages", - path: "/" <> container.image.id <> "/pulled", - value: true + interface: "io.edgehog.devicemanager.apps.DeploymentEvent", + path: "/" <> deployment.id, + value: %{ + "status" => "Starting", + "message" => nil + } }, timestamp: DateTime.to_iso8601(DateTime.utc_now()) } - set_resource_expectations([deployment]) - path = Routes.astarte_trigger_path(conn, :process_event, tenant.slug) conn @@ -589,39 +691,37 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do |> post(path, deployment_event) |> response(200) - deployment = Ash.get!(Deployment, deployment.id, tenant: tenant) - assert deployment.status == :stopped - end - - test "AvailableContainers triggers update deployment status", context do - %{conn: conn, realm: realm, device: device, tenant: tenant} = context + # Deployment must be reloaded from the db + deployment = Ash.get!(Edgehog.Containers.Release.Deployment, deployment.id, tenant: tenant) - release = - [containers: 1, tenant: tenant] - |> release_fixture() - |> Ash.load!(containers: [:image, :networks]) + assert deployment.state == :started + assert deployment.last_message == nil + end - [container] = release.containers + test "Stopping status does not update a Stopped deployment", context do + %{ + conn: conn, + realm: realm, + device: device, + tenant: tenant + } = context - deployment = - [tenant: tenant, device_id: device.id, release_id: release.id] - |> deployment_fixture() - |> Ash.Changeset.for_update(:set_status, %{status: :sent}, tenant: tenant) - |> Ash.update!() + deployment = deployment_fixture(tenant: tenant, device_id: device.id, state: :stopped) deployment_event = %{ device_id: device.device_id, event: %{ type: "incoming_data", - interface: "io.edgehog.devicemanager.apps.AvailableContainers", - path: "/" <> container.id <> "/status", - value: "Created" + interface: "io.edgehog.devicemanager.apps.DeploymentEvent", + path: "/" <> deployment.id, + value: %{ + "status" => "Stopping", + "message" => nil + } }, timestamp: DateTime.to_iso8601(DateTime.utc_now()) } - set_resource_expectations([deployment]) - path = Routes.astarte_trigger_path(conn, :process_event, tenant.slug) conn @@ -629,21 +729,25 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do |> post(path, deployment_event) |> response(200) - deployment = Ash.get!(Deployment, deployment.id, tenant: tenant) - assert deployment.status == :stopped + # Deployment must be reloaded from the db + deployment = Ash.get!(Edgehog.Containers.Release.Deployment, deployment.id, tenant: tenant) + + assert deployment.state == :stopped + assert deployment.last_message == nil end test "AvailableDeployments triggers update deployment status", context do %{conn: conn, realm: realm, device: device, tenant: tenant} = context release = - release_fixture(containers: 1, tenant: tenant) + [containers: 1, tenant: tenant] + |> release_fixture() + |> Ash.load!(containers: [:image]) deployment = [tenant: tenant, device_id: device.id, release_id: release.id] |> deployment_fixture() - |> Ash.Changeset.for_update(:set_status, %{status: :sent}, tenant: tenant) - |> Ash.update!() + |> Edgehog.Containers.release_deployment_sent!(tenant: tenant) deployment_event = %{ device_id: device.device_id, @@ -656,8 +760,6 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do timestamp: DateTime.to_iso8601(DateTime.utc_now()) } - set_resource_expectations([deployment]) - path = Routes.astarte_trigger_path(conn, :process_event, tenant.slug) conn @@ -666,7 +768,8 @@ defmodule EdgehogWeb.Controllers.AstarteTriggerControllerTest do |> response(200) deployment = Ash.get!(Deployment, deployment.id, tenant: tenant) - assert deployment.status == :stopped + assert deployment.state == :stopped + assert deployment.last_message == nil end test "unset AvailableDeployments deletes an existing deployment", context do diff --git a/backend/test/edgehog_web/schema/mutation/send_deployment_upgrade_test.exs b/backend/test/edgehog_web/schema/mutation/send_deployment_upgrade_test.exs index f0607dfcf..214d6db7e 100644 --- a/backend/test/edgehog_web/schema/mutation/send_deployment_upgrade_test.exs +++ b/backend/test/edgehog_web/schema/mutation/send_deployment_upgrade_test.exs @@ -25,9 +25,8 @@ defmodule EdgehogWeb.Schema.Mutation.SendDeploymentUpgradeTest do import Edgehog.ContainersFixtures alias Edgehog.Astarte.Device.CreateDeploymentRequestMock - alias Edgehog.Astarte.Device.DeploymentUpdateMock alias Edgehog.Containers - alias Edgehog.Containers.Deployment + alias Edgehog.Containers.Release describe "sendDeploymentUpgrade" do setup %{tenant: tenant} do @@ -64,17 +63,20 @@ defmodule EdgehogWeb.Schema.Mutation.SendDeploymentUpgradeTest do {:ok, %{id: deployment_id}} = AshGraphql.Resource.decode_relay_id(result["id"]) - assert Edgehog.Containers.DeploymentReadyAction + assert Release.Deployment.ReadyAction |> Ash.read_first!(tenant: tenant) |> Map.fetch!(:deployment_id) == deployment_id end test "sends the deployment upgrade once the new deployment reaches :ready state", args do - %{deployment_0_0_1: deployment_0_0_1, release_0_0_2: release_0_0_2, tenant: tenant} = - args + %{ + deployment_0_0_1: deployment_0_0_1, + release_0_0_2: release_0_0_2, + tenant: tenant + # conn: conn + } = args expect(CreateDeploymentRequestMock, :send_create_deployment_request, fn _, _, _ -> :ok end) - expect(DeploymentUpdateMock, :update, fn _, _, _ -> :ok end) result = [tenant: tenant, deployment: deployment_0_0_1, target: release_0_0_2] @@ -83,10 +85,20 @@ defmodule EdgehogWeb.Schema.Mutation.SendDeploymentUpgradeTest do {:ok, %{id: deployment_id}} = AshGraphql.Resource.decode_relay_id(result["id"]) - deployment = Ash.get!(Deployment, deployment_id, tenant: tenant) - set_resource_expectations([deployment_0_0_1, deployment]) + deployment = Ash.get!(Release.Deployment, deployment_id, tenant: tenant) + + expect(Edgehog.Astarte.Device.DeploymentUpdateMock, :update, 1, fn _, _, update_data -> + %{from: from, to: to} = update_data + assert from == deployment_0_0_1.id + assert to == deployment.id + + :ok + end) + + assert :sent = deployment.state - Containers.deployment_update_status!(deployment) + # Setting a container stopped for the first time should trigger the ready actions + Containers.release_deployment_stopped(deployment, tenant: tenant) end test "fails if the deployments do not belong to the same application", args do diff --git a/backend/test/edgehog_web/schema/mutation/update_deployment_start_test.exs b/backend/test/edgehog_web/schema/mutation/update_deployment_start_test.exs index 8db816e15..10ed8d9e0 100644 --- a/backend/test/edgehog_web/schema/mutation/update_deployment_start_test.exs +++ b/backend/test/edgehog_web/schema/mutation/update_deployment_start_test.exs @@ -28,7 +28,7 @@ defmodule EdgehogWeb.Schema.Mutation.UpdateDeploymentStartTest do describe "startDeployment mutation tests" do test "start on an existing deployment", %{tenant: tenant} do - deployment = deployment_fixture(tenant: tenant) + deployment = deployment_fixture(tenant: tenant, state: :stopped) expect(DeploymentCommandMock, :send_deployment_command, 1, fn _, _, _ -> :ok end) diff --git a/backend/test/support/fixtures/containers_fixtures.ex b/backend/test/support/fixtures/containers_fixtures.ex index bbb15d913..1abcec078 100644 --- a/backend/test/support/fixtures/containers_fixtures.ex +++ b/backend/test/support/fixtures/containers_fixtures.ex @@ -23,21 +23,13 @@ defmodule Edgehog.ContainersFixtures do This module defines test helpers for creating entities via the `Edgehog.Containers` context. """ - alias Edgehog.Astarte.Device.AvailableContainers.ContainerStatus - alias Edgehog.Astarte.Device.AvailableContainersMock - alias Edgehog.Astarte.Device.AvailableDeployments.DeploymentStatus - alias Edgehog.Astarte.Device.AvailableDeploymentsMock - alias Edgehog.Astarte.Device.AvailableImages.ImageStatus - alias Edgehog.Astarte.Device.AvailableImagesMock - alias Edgehog.Astarte.Device.AvailableNetworks.NetworkStatus - alias Edgehog.Astarte.Device.AvailableNetworksMock alias Edgehog.AstarteFixtures alias Edgehog.Containers.Application alias Edgehog.Containers.Container - alias Edgehog.Containers.Deployment alias Edgehog.Containers.Image alias Edgehog.Containers.Network alias Edgehog.Containers.Release + alias Edgehog.Containers.Release.Deployment @doc """ Generate a unique application name. @@ -218,45 +210,4 @@ defmodule Edgehog.ContainersFixtures do Ash.create!(Deployment, params, tenant: tenant) end - - def set_resource_expectations(deployments, new_deployments \\ 1) do - deployments = - Enum.map(deployments, &Ash.load!(&1, release: [containers: [:image, :networks]])) - - containers = - deployments - |> Enum.map(& &1.release) - |> Enum.flat_map(& &1.containers) - |> Enum.uniq_by(& &1.id) - - available_containers = Enum.map(containers, &%ContainerStatus{id: &1.id, status: "Created"}) - - available_images = - containers - |> Enum.map(&%ImageStatus{id: &1.image_id, pulled: false}) - |> Enum.uniq() - - available_networks = - containers - |> Enum.flat_map(& &1.networks) - |> Enum.map(&%NetworkStatus{id: &1.id, created: false}) - |> Enum.uniq() - - available_deployments = - Enum.map(deployments, &%DeploymentStatus{id: &1.id, status: :stopped}) - - Mox.expect(AvailableImagesMock, :get, new_deployments, fn _, _ -> {:ok, available_images} end) - - Mox.expect(AvailableNetworksMock, :get, new_deployments, fn _, _ -> - {:ok, available_networks} - end) - - Mox.expect(AvailableContainersMock, :get, new_deployments, fn _, _ -> - {:ok, available_containers} - end) - - Mox.expect(AvailableDeploymentsMock, :get, new_deployments, fn _, _ -> - {:ok, available_deployments} - end) - end end