Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Base Images #240

Merged
merged 13 commits into from
Feb 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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: [],
rbino marked this conversation as resolved.
Show resolved Hide resolved
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