Skip to content

Commit

Permalink
feat(containers): application upgrade interface
Browse files Browse the repository at this point in the history
expose a way to upgrade an application to a newer release .

- does not allow downgrading
- the releases must be of the same application

Signed-off-by: Francesco Noacco <[email protected]>
  • Loading branch information
noaccOS committed Nov 7, 2024
1 parent e0de00a commit f084e76
Show file tree
Hide file tree
Showing 11 changed files with 300 additions and 0 deletions.
1 change: 1 addition & 0 deletions backend/config/test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,7 @@ config :edgehog,
Edgehog.Astarte.Device.CreateNetworkRequestMock

config :edgehog, :astarte_deployment_command_module, Edgehog.Astarte.Device.DeploymentCommandMock
config :edgehog, :astarte_deployment_update_module, Edgehog.Astarte.Device.DeploymentUpdateMock
config :edgehog, :astarte_device_status_module, Edgehog.Astarte.Device.DeviceStatusMock
config :edgehog, :astarte_forwarder_session_module, Edgehog.Astarte.Device.ForwarderSessionMock
config :edgehog, :astarte_geolocation_module, Edgehog.Astarte.Device.GeolocationMock
Expand Down
42 changes: 42 additions & 0 deletions backend/lib/edgehog/astarte/device/deployment_update.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
#
# 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.Astarte.Device.DeploymentUpdate do
@moduledoc false

@behaviour Edgehog.Astarte.Device.DeploymentUpdate.Behaviour

alias Astarte.Client.AppEngine
alias Edgehog.Error.AstarteAPIError

@interface "io.edgehog.devicemanager.apps.DeploymentUpdate"

@impl Edgehog.Astarte.Device.DeploymentUpdate.Behaviour
def update(client, device_id, data) do
api_call =
AppEngine.Devices.send_datastream(client, device_id, @interface, "/deployment", data)

with {:error, api_error} <- api_call do
reason = AstarteAPIError.exception(status: api_error.status, response: api_error.response)

{:error, reason}
end
end
end
32 changes: 32 additions & 0 deletions backend/lib/edgehog/astarte/device/deployment_update/behaviour.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
#
# 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.Astarte.Device.DeploymentUpdate.Behaviour do
@moduledoc false

alias Astarte.Client.AppEngine
alias Edgehog.Astarte.Device.DeploymentUpdate.RequestData

@callback update(
client :: AppEngine.t(),
device_id :: String.t(),
data :: RequestData.t()
) :: :ok | {:error, term()}
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
#
# 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.Astarte.Device.DeploymentUpdate.RequestData do
@moduledoc false

defstruct [:from, :to]

@type t :: %__MODULE__{
from: String.t(),
to: String.t()
}
end
48 changes: 48 additions & 0 deletions backend/lib/edgehog/containers/validations/is_upgrade.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#
# 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.Validations.IsUpgrade do
@moduledoc false

use Ash.Resource.Validation

@impl Ash.Resource.Validation
def validate(changeset, opts, _context) do
from = Ash.Changeset.get_argument(changeset, opts[:from])
to = Ash.Changeset.get_argument(changeset, opts[:to])

with {:ok, from} <- Ash.load(from, [:release], reuse_values?: true),
{:ok, to} <- Ash.load(to, [:release], reuse_values?: true),
{:ok, from_version} <- parse_version(from),
{:ok, to_version} <- parse_version(to) do
if Version.compare(to_version, from_version) == :gt do
:ok
else
{:error, field: opts[:to], message: "must be a newer release than from"}
end
end
end

defp parse_version(deployment) do
with :error <- deployment.release.version |> get_in() |> to_string() |> Version.parse() do
{:error, :invalid_release}
end
end
end
43 changes: 43 additions & 0 deletions backend/lib/edgehog/containers/validations/same_application.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
#
# 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.Validations.SameApplication do
@moduledoc false

use Ash.Resource.Validation

@impl Ash.Resource.Validation
def validate(changeset, opts, _context) do
deployment_a = Ash.Changeset.get_argument(changeset, opts[:deployment_a])
deployment_b = Ash.Changeset.get_argument(changeset, opts[:deployment_b])

with {:ok, deployment_a} <- Ash.load(deployment_a, [:release], reuse_values?: true),
{:ok, deployment_b} <- Ash.load(deployment_b, [:release], reuse_values?: true) do
application_a = get_in(deployment_a.release.application_id)
application_b = get_in(deployment_b.release.application_id)

if application_a == application_b do
:ok
else
{:error, field: opts[:release_b], message: "must belong to the same application as from"}
end
end
end
end
26 changes: 26 additions & 0 deletions backend/lib/edgehog/devices/device/device.ex
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,32 @@ defmodule Edgehog.Devices.Device do

manual Edgehog.Devices.Device.ManualActions.SendApplicationCommand
end

update :update_application do
description "Updates an application to a newer release."

argument :from, :struct do
constraints instance_of: Deployment
allow_nil? false

description """
The release to be upgraded. Should be currently installed.
This argument is needed because there might be multiple versions of a single application installed on the device.
"""
end

argument :to, :struct do
constraints instance_of: Deployment
allow_nil? false
description "The new release of the application"
end

validate {Edgehog.Containers.Validations.SameApplication, [deployment_a: :from, deployment_b: :to]}

validate {Edgehog.Containers.Validations.IsUpgrade, [from: :from, to: :to]}

manual Edgehog.Devices.Device.ManualActions.UpdateApplication
end
end

attributes do
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
#
# 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.Devices.Device.ManualActions.UpdateApplication do
@moduledoc false
use Ash.Resource.ManualUpdate

alias Edgehog.Astarte.Device.DeploymentUpdate.RequestData

@deployment_update Application.compile_env(
:edgehog,
:astarte_deployment_update_module,
Edgehog.Astarte.Device.DeploymentUpdate
)

@impl Ash.Resource.ManualUpdate
def update(changeset, _opts, _context) do
device = changeset.data

with {:ok, from} <- Ash.Changeset.fetch_argument(changeset, :from),
{:ok, to} <- Ash.Changeset.fetch_argument(changeset, :to),
{:ok, device} <- Ash.load(device, :appengine_client),
data = %RequestData{from: from.id, to: to.id},
:ok <- @deployment_update.update(device.appengine_client, device.device_id, data) do
{:ok, device}
end
end
end
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"interface_name": "io.edgehog.devicemanager.apps.DeploymentUpdate",
"version_major": 0,
"version_minor": 1,
"type": "datastream",
"ownership": "server",
"aggregation": "object",
"mappings": [
{
"endpoint": "/deployment/from",
"type": "string",
"database_retention_policy": "use_ttl",
"database_retention_ttl": 31556952,
"description": "Id of the deployment to update from"
},
{
"endpoint": "/deployment/to",
"type": "string",
"database_retention_policy": "use_ttl",
"database_retention_ttl": 31556952,
"description": "Id of the deployment to update to"
}
]
}
5 changes: 5 additions & 0 deletions backend/test/support/astarte_mock_case.ex
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ defmodule Edgehog.AstarteMockCase do
Edgehog.Astarte.Device.DeploymentCommand
)

Mox.stub_with(
Edgehog.Astarte.Device.DeploymentUpdateMock,
Edgehog.Astarte.Device.DeploymentUpdate
)

Mox.stub_with(
Edgehog.Astarte.Device.BaseImageMock,
Edgehog.Mocks.Astarte.Device.BaseImage
Expand Down
4 changes: 4 additions & 0 deletions backend/test/support/mocks.ex
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,10 @@ Mox.defmock(Edgehog.Astarte.Device.DeploymentCommandMock,
for: Edgehog.Astarte.Device.DeploymentCommand.Behaviour
)

Mox.defmock(Edgehog.Astarte.Device.DeploymentUpdateMock,
for: Edgehog.Astarte.Device.DeploymentUpdate.Behaviour
)

Mox.defmock(Edgehog.Astarte.Device.AvailableContainersMock,
for: Edgehog.Astarte.Device.AvailableContainers.Behaviour
)
Expand Down

0 comments on commit f084e76

Please sign in to comment.