Skip to content

Commit

Permalink
Merge pull request #240 from rbino/base-images
Browse files Browse the repository at this point in the history
Add Base Images
  • Loading branch information
szakhlypa authored Feb 23, 2023
2 parents 72a69ac + 02a4947 commit eab5551
Show file tree
Hide file tree
Showing 27 changed files with 1,758 additions and 27 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
- Allow creating and managing groups based on selectors.
- Add support for device's `network_interfaces` ([#231](https://github.com/edgehog-device-manager/edgehog/pull/231), [#232](https://github.com/edgehog-device-manager/edgehog/pull/232)).
- Add support for base image collections ([#229](https://github.com/edgehog-device-manager/edgehog/pull/229), [#230](https://github.com/edgehog-device-manager/edgehog/pull/230)).
- Add support for base images ([#240](https://github.com/edgehog-device-manager/edgehog/pull/240))

### Changed
- Handle Device part numbers for nonexistent system models
Expand Down
3 changes: 2 additions & 1 deletion backend/config/test.exs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#
# This file is part of Edgehog.
#
# Copyright 2021 SECO Mind Srl
# Copyright 2021-2023 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.
Expand Down Expand Up @@ -71,6 +71,7 @@ config :edgehog,

# Storage mocks for tests
config :edgehog, :assets_system_model_picture_module, Edgehog.Assets.SystemModelPictureMock
config :edgehog, :base_images_storage_module, Edgehog.BaseImages.StorageMock
config :edgehog, :os_management_ephemeral_image_module, Edgehog.OSManagement.EphemeralImageMock

# Enable s3 storage since we're using mocks for it
Expand Down
172 changes: 172 additions & 0 deletions backend/lib/edgehog/base_images.ex
Original file line number Diff line number Diff line change
Expand Up @@ -26,14 +26,24 @@ defmodule Edgehog.BaseImages do
import Ecto.Query, warn: false
alias Edgehog.Repo

alias Ecto.Multi
alias Edgehog.Devices
alias Edgehog.BaseImages.BaseImage
alias Edgehog.BaseImages.BaseImageCollection
alias Edgehog.BaseImages.BucketStorage

@storage_module Application.compile_env(
:edgehog,
:base_images_storage_module,
BucketStorage
)

@doc """
Preloads the default associations for a Base Image Collection (or a list of base image collections)
"""
def preload_defaults_for_base_image_collection(collection_or_collections) do
Repo.preload(collection_or_collections,
base_images: [],
system_model: [:hardware_type, :part_numbers]
)
end
Expand Down Expand Up @@ -148,4 +158,166 @@ defmodule Edgehog.BaseImages do
def change_base_image_collection(%BaseImageCollection{} = base_image_collection, attrs \\ %{}) do
BaseImageCollection.changeset(base_image_collection, attrs)
end

@doc """
Preloads the default associations for a Base Image (or a list of base images)
"""
def preload_defaults_for_base_image(image_or_images) do
Repo.preload(image_or_images,
base_image_collection: [
system_model: [:hardware_type, :part_numbers]
]
)
end

@doc """
Returns the list of base_images.
## Examples
iex> list_base_images()
[%BaseImage{}, ...]
"""
def list_base_images do
Repo.all(BaseImage)
|> preload_defaults_for_base_image()
end

@doc """
Returns the list of base_images in a specific `%BaseImageCollection{}`.
## Examples
iex> list_base_images_for_collection(base_image_collection)
[%BaseImage{}, ...]
"""
def list_base_images_for_collection(%BaseImageCollection{} = base_image_collection) do
Ecto.assoc(base_image_collection, :base_images)
|> Repo.all()
|> preload_defaults_for_base_image()
end

@doc """
Fetches a single base_image.
Returns `{:error, :not_found}` if the Base image does not exist.
## Examples
iex> fetch_base_image(123)
{:ok, %BaseImage{}}
iex> fetch_base_image(456)
{:error, :not_found}
"""
def fetch_base_image(id) do
case Repo.get(BaseImage, id) do
nil -> {:error, :not_found}
%BaseImage{} = base_image -> {:ok, preload_defaults_for_base_image(base_image)}
end
end

@doc """
Creates a base_image.
## Examples
iex> create_base_image(%BaseImageCollection{}, %{field: value})
{:ok, %BaseImage{}}
iex> create_base_image(%BaseImageCollection{}, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def create_base_image(%BaseImageCollection{} = base_image_collection, attrs \\ %{}) do
Multi.new()
|> Multi.insert(:no_file_base_image, fn _changes ->
%BaseImage{base_image_collection_id: base_image_collection.id}
|> BaseImage.create_changeset(attrs)
end)
|> Multi.run(:image_upload, fn _repo, %{no_file_base_image: base_image} ->
file = %Plug.Upload{} = attrs.file
# If version is nil, the changeset will fail below
@storage_module.store(base_image, file)
end)
|> Multi.update(:base_image, fn changes ->
%{no_file_base_image: base_image, image_upload: url} = changes
Ecto.Changeset.change(base_image, url: url)
end)
|> Repo.transaction()
|> case do
{:ok, %{base_image: base_image}} ->
{:ok, preload_defaults_for_base_image(base_image)}

{:error, _failed_operation, failed_value, _changes_so_far} ->
{:error, failed_value}
end
end

@doc """
Updates a base_image.
## Examples
iex> update_base_image(base_image, %{field: new_value})
{:ok, %BaseImage{}}
iex> update_base_image(base_image, %{field: bad_value})
{:error, %Ecto.Changeset{}}
"""
def update_base_image(%BaseImage{} = base_image, attrs) do
changeset = BaseImage.update_changeset(base_image, attrs)

with {:ok, base_image} <- Repo.update(changeset) do
{:ok, preload_defaults_for_base_image(base_image)}
end
end

@doc """
Deletes a base_image.
## Examples
iex> delete_base_image(base_image)
{:ok, %BaseImage{}}
iex> delete_base_image(base_image)
{:error, %Ecto.Changeset{}}
"""
def delete_base_image(%BaseImage{} = base_image) do
Multi.new()
|> Multi.delete(:base_image, base_image)
|> Multi.run(:image_deletion, fn _repo, %{base_image: base_image} ->
# If version is nil, the changeset will fail below
with :ok <- @storage_module.delete(base_image) do
{:ok, nil}
end
end)
|> Repo.transaction()
|> case do
{:ok, %{base_image: base_image}} ->
{:ok, preload_defaults_for_base_image(base_image)}

{:error, _failed_operation, failed_value, _changes_so_far} ->
{:error, failed_value}
end
end

@doc """
Returns an `%Ecto.Changeset{}` for tracking base_image changes.
## Examples
iex> change_base_image(base_image)
%Ecto.Changeset{data: %BaseImage{}}
"""
def change_base_image(%BaseImage{} = base_image, attrs \\ %{}) do
BaseImage.update_changeset(base_image, attrs)
end
end
83 changes: 83 additions & 0 deletions backend/lib/edgehog/base_images/base_image.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#
# This file is part of Edgehog.
#
# Copyright 2023 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.BaseImages.BaseImage do
use Ecto.Schema
use I18nHelpers.Ecto.TranslatableFields
import Ecto.Changeset
import Edgehog.Localization.Validation

alias Edgehog.BaseImages.BaseImageCollection

@type t :: Ecto.Schema.t()

schema "base_images" do
field :tenant_id, :integer, autogenerate: {Edgehog.Repo, :get_tenant_id, []}
translatable_field :description
translatable_field :release_display_name
field :starting_version_requirement, :string
field :url, :string
field :version, :string
translatable_belongs_to :base_image_collection, BaseImageCollection

timestamps()
end

@doc false
def create_changeset(base_image, attrs) do
base_image
|> cast(attrs, [:version, :release_display_name, :description, :starting_version_requirement])
|> validate_required([:version])
|> unique_constraint([:version, :base_image_collection_id, :tenant_id])
|> validate_change(:version, &validate_version/2)
|> validate_change(:starting_version_requirement, &validate_version_requirement/2)
|> validate_change(:description, &validate_locale/2)
|> validate_change(:release_display_name, &validate_locale/2)
end

@doc false
def update_changeset(base_image, attrs) do
base_image
|> cast(attrs, [:release_display_name, :description, :starting_version_requirement])
|> validate_change(:starting_version_requirement, &validate_version_requirement/2)
|> validate_change(:description, &validate_locale/2)
|> validate_change(:release_display_name, &validate_locale/2)
end

defp validate_version(field, value) do
case Version.parse(value) do
{:ok, _version} ->
[]

:error ->
[{field, "is not a valid version"}]
end
end

defp validate_version_requirement(field, value) do
case Version.parse_requirement(value) do
{:ok, _version} ->
[]

:error ->
[{field, "is not a valid version requirement"}]
end
end
end
2 changes: 2 additions & 0 deletions backend/lib/edgehog/base_images/base_image_collection.ex
Original file line number Diff line number Diff line change
Expand Up @@ -23,13 +23,15 @@ defmodule Edgehog.BaseImages.BaseImageCollection do
use I18nHelpers.Ecto.TranslatableFields
import Ecto.Changeset

alias Edgehog.BaseImages.BaseImage
alias Edgehog.Devices

schema "base_image_collections" do
field :tenant_id, :integer, autogenerate: {Edgehog.Repo, :get_tenant_id, []}
field :handle, :string
field :name, :string
translatable_belongs_to :system_model, Devices.SystemModel
translatable_has_many :base_images, BaseImage

timestamps()
end
Expand Down
41 changes: 41 additions & 0 deletions backend/lib/edgehog/base_images/bucket_storage.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
#
# This file is part of Edgehog.
#
# Copyright 2023 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.BaseImages.BucketStorage do
@behaviour Edgehog.BaseImages.Storage

alias Edgehog.BaseImages.BaseImage
alias Edgehog.BaseImages.Uploaders

@impl true
def store(%BaseImage{} = scope, %Plug.Upload{} = upload) do
with {:ok, file_name} <- Uploaders.BaseImage.store({upload, scope}) do
# TODO: investigate URL signing instead of public access
file_url = Uploaders.BaseImage.url({file_name, scope})
{:ok, file_url}
end
end

@impl true
def delete(%BaseImage{} = scope) do
%BaseImage{url: url} = scope
Uploaders.BaseImage.delete({url, scope})
end
end
31 changes: 31 additions & 0 deletions backend/lib/edgehog/base_images/storage.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
#
# This file is part of Edgehog.
#
# Copyright 2023 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.BaseImages.Storage do
alias Edgehog.BaseImages.BaseImage

@type upload :: %Plug.Upload{}

@callback store(scope :: BaseImage.t(), file :: upload()) ::
{:ok, file_url :: String.t()} | {:error, reason :: any}

@callback delete(base_image :: BaseImage.t()) ::
:ok | {:error, reason :: any}
end
Loading

0 comments on commit eab5551

Please sign in to comment.