forked from edgehog-device-manager/edgehog
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Coordinate the provisioning of a Tenant with an associated realm and cluster. Add relevant tests. Signed-off-by: Riccardo Binetti <[email protected]>
- Loading branch information
Showing
4 changed files
with
372 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,110 @@ | ||
# | ||
# 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.Provisioning do | ||
alias Edgehog.Astarte | ||
alias Edgehog.Provisioning.AstarteConfig | ||
alias Edgehog.Provisioning.TenantConfig | ||
alias Edgehog.Repo | ||
alias Edgehog.Tenants | ||
|
||
def provision_tenant(attrs) do | ||
config_changeset = TenantConfig.changeset(%TenantConfig{}, attrs) | ||
|
||
with {:ok, tenant_config} <- Ecto.Changeset.apply_action(config_changeset, :create), | ||
{:error, error_changeset} <- provision_tenant_from_config(tenant_config) do | ||
{:error, remap_error_changeset(config_changeset, error_changeset)} | ||
end | ||
end | ||
|
||
defp provision_tenant_from_config(tenant_config) do | ||
%TenantConfig{astarte_config: astarte_config} = tenant_config | ||
|
||
Repo.transact(fn -> | ||
with {:ok, tenant} <- create_tenant(tenant_config), | ||
Repo.put_tenant_id(tenant.tenant_id), | ||
{:ok, cluster} <- create_cluster(astarte_config), | ||
{:ok, realm} <- create_realm(cluster, astarte_config) do | ||
# Build back the tenant config, to reflect what has actually been | ||
# saved in the database | ||
tenant_config = %TenantConfig{ | ||
name: tenant.name, | ||
slug: tenant.slug, | ||
public_key: tenant.public_key, | ||
astarte_config: %AstarteConfig{ | ||
base_api_url: cluster.base_api_url, | ||
realm_name: realm.name, | ||
realm_private_key: realm.private_key | ||
} | ||
} | ||
|
||
{:ok, tenant_config} | ||
end | ||
end) | ||
end | ||
|
||
defp create_tenant(tenant_config) do | ||
tenant_params = Map.take(tenant_config, [:name, :slug, :public_key]) | ||
Tenants.create_tenant(tenant_params) | ||
end | ||
|
||
defp create_cluster(astarte_config) do | ||
cluster_params = %{base_api_url: astarte_config.base_api_url} | ||
Astarte.upsert_cluster(cluster_params) | ||
end | ||
|
||
defp create_realm(cluster, astarte_config) do | ||
realm_params = %{ | ||
name: astarte_config.realm_name, | ||
private_key: astarte_config.realm_private_key | ||
} | ||
|
||
Astarte.create_realm(cluster, realm_params) | ||
end | ||
|
||
# Utils to remap error changesets from Cluster or Realm creation to a | ||
# TenantConfig error changest | ||
defp remap_error_changeset(config_changeset, error_changeset) do | ||
case error_changeset.data do | ||
%Tenants.Tenant{} -> | ||
# This has the same fields as TenantConfig, no need for remapping | ||
error_changeset | ||
|
||
%Astarte.Cluster{} -> | ||
field_mappings = %{base_api_url: :base_api_url} | ||
remap_astarte_config_errors(config_changeset, error_changeset, field_mappings) | ||
|
||
%Astarte.Realm{} -> | ||
field_mappings = %{name: :realm_name, private_key: :realm_private_key} | ||
remap_astarte_config_errors(config_changeset, error_changeset, field_mappings) | ||
end | ||
end | ||
|
||
defp remap_astarte_config_errors(config_changeset, error_changeset, field_mappings) do | ||
Enum.reduce(field_mappings, config_changeset, fn {source_field, dest_field}, acc -> | ||
if errors = error_changeset.errors[source_field] do | ||
put_in(acc.changes.astarte_config.errors[dest_field], errors) | ||
|> Map.put(:valid?, false) | ||
else | ||
acc | ||
end | ||
end) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,42 @@ | ||
# | ||
# 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.Provisioning.AstarteConfig do | ||
use Ecto.Schema | ||
import Ecto.Changeset | ||
import Edgehog.ChangesetValidation | ||
|
||
@primary_key false | ||
embedded_schema do | ||
field :base_api_url, :string | ||
field :realm_name, :string | ||
field :realm_private_key, :string | ||
end | ||
|
||
@doc false | ||
def changeset(astarte_config, attrs) do | ||
astarte_config | ||
|> cast(attrs, [:base_api_url, :realm_name, :realm_private_key]) | ||
|> validate_required([:base_api_url, :realm_name, :realm_private_key]) | ||
|> validate_realm_name(:realm_name) | ||
|> validate_pem_private_key(:realm_private_key) | ||
|> validate_url(:base_api_url) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,45 @@ | ||
# | ||
# 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.Provisioning.TenantConfig do | ||
use Ecto.Schema | ||
import Ecto.Changeset | ||
import Edgehog.ChangesetValidation | ||
|
||
alias Edgehog.Provisioning.AstarteConfig | ||
|
||
@primary_key false | ||
embedded_schema do | ||
field :name, :string | ||
field :slug, :string | ||
field :public_key, :string | ||
embeds_one :astarte_config, AstarteConfig | ||
end | ||
|
||
@doc false | ||
def changeset(tenant, attrs) do | ||
tenant | ||
|> cast(attrs, [:name, :slug, :public_key]) | ||
|> cast_embed(:astarte_config, required: true) | ||
|> validate_required([:name, :slug, :public_key]) | ||
|> validate_tenant_slug(:slug) | ||
|> validate_pem_public_key(:public_key) | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
# | ||
# 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.ProvisioningTest do | ||
use Edgehog.DataCase, async: true | ||
use Edgehog.AstarteMockCase | ||
|
||
import Edgehog.AstarteFixtures | ||
import Edgehog.TenantsFixtures | ||
|
||
alias Edgehog.Astarte | ||
alias Edgehog.Provisioning | ||
alias Edgehog.Provisioning.AstarteConfig | ||
alias Edgehog.Provisioning.TenantConfig | ||
alias Edgehog.Tenants | ||
|
||
@valid_pem_public_key X509.PrivateKey.new_ec(:secp256r1) | ||
|> X509.PublicKey.derive() | ||
|> X509.PublicKey.to_pem() | ||
|
||
@valid_pem_private_key X509.PrivateKey.new_ec(:secp256r1) |> X509.PrivateKey.to_pem() | ||
|
||
describe "provision_tenant/1" do | ||
test "with valid attrs creates the tenant, cluster and realm" do | ||
attrs = %{ | ||
name: "Test", | ||
slug: "test", | ||
public_key: @valid_pem_public_key, | ||
astarte_config: %{ | ||
base_api_url: "https://astarte.api.example", | ||
realm_name: "testrealm", | ||
realm_private_key: @valid_pem_private_key | ||
} | ||
} | ||
|
||
assert {:ok, %TenantConfig{} = tenant_config} = Provisioning.provision_tenant(attrs) | ||
|
||
%TenantConfig{ | ||
name: name, | ||
slug: slug, | ||
public_key: public_key | ||
} = tenant_config | ||
|
||
assert name == attrs.name | ||
assert slug == attrs.slug | ||
assert public_key == attrs.public_key | ||
|
||
assert {:ok, tenant} = Tenants.fetch_tenant_by_slug(tenant_config.slug) | ||
|
||
assert %Tenants.Tenant{ | ||
name: ^name, | ||
slug: ^slug, | ||
public_key: ^public_key | ||
} = tenant | ||
|
||
tenant_id = tenant.tenant_id | ||
|
||
%AstarteConfig{ | ||
base_api_url: base_api_url, | ||
realm_name: realm_name, | ||
realm_private_key: realm_private_key | ||
} = tenant_config.astarte_config | ||
|
||
assert base_api_url == attrs.astarte_config.base_api_url | ||
assert realm_name == attrs.astarte_config.realm_name | ||
assert realm_private_key == attrs.astarte_config.realm_private_key | ||
|
||
Astarte.Cluster | ||
|> Ecto.Query.where(base_api_url: ^base_api_url) | ||
|> Repo.exists?(skip_tenant_id: true) | ||
|
||
assert {:ok, %Astarte.Realm{tenant_id: ^tenant_id, private_key: ^realm_private_key}} = | ||
Astarte.fetch_realm_by_name(realm_name) | ||
end | ||
|
||
test "succeeds when providing the URL of an already existing cluster" do | ||
cluster = cluster_fixture() | ||
|
||
assert {:ok, _tenant_config} = | ||
provision_tenant(astarte_config: [base_api_url: cluster.base_api_url]) | ||
end | ||
|
||
test "fails with invalid tenant slug" do | ||
assert {:error, changeset} = provision_tenant(slug: "1-INVALID") | ||
assert errors_on(changeset)[:slug] != nil | ||
end | ||
|
||
test "fails with invalid tenant public key" do | ||
assert {:error, changeset} = provision_tenant(public_key: "invalid") | ||
assert errors_on(changeset)[:public_key] != nil | ||
end | ||
|
||
test "fails with invalid Astarte base API url" do | ||
assert {:error, changeset} = provision_tenant(astarte_config: [base_api_url: "invalid"]) | ||
assert errors_on(changeset)[:astarte_config][:base_api_url] != nil | ||
end | ||
|
||
test "fails with invalid Astarte realm name" do | ||
assert {:error, changeset} = provision_tenant(astarte_config: [realm_name: "INVALID"]) | ||
assert errors_on(changeset)[:astarte_config][:realm_name] != nil | ||
end | ||
|
||
test "fails with invalid Astarte realm private key" do | ||
assert {:error, changeset} = | ||
provision_tenant(astarte_config: [realm_private_key: "invalid"]) | ||
|
||
assert errors_on(changeset)[:astarte_config][:realm_private_key] != nil | ||
end | ||
|
||
test "fails when providing an already existing tenant slug" do | ||
tenant = tenant_fixture() | ||
|
||
assert {:error, changeset} = provision_tenant(slug: tenant.slug) | ||
assert "has already been taken" in errors_on(changeset)[:slug] | ||
end | ||
|
||
test "fails when providing an already existing tenant name" do | ||
tenant = tenant_fixture() | ||
|
||
assert {:error, changeset} = provision_tenant(name: tenant.name) | ||
assert "has already been taken" in errors_on(changeset)[:name] | ||
end | ||
|
||
test "fails when providing an already existing realm name" do | ||
cluster = cluster_fixture() | ||
realm = realm_fixture(cluster) | ||
|
||
assert {:error, changeset} = | ||
[astarte_config: [base_api_url: cluster.base_api_url, realm_name: realm.name]] | ||
|> provision_tenant() | ||
|
||
assert "has already been taken" in errors_on(changeset)[:astarte_config][:realm_name] | ||
end | ||
end | ||
|
||
defp provision_tenant(opts) do | ||
{astarte_config, opts} = Keyword.pop(opts, :astarte_config, []) | ||
|
||
astarte_config = | ||
astarte_config | ||
|> Enum.into(%{ | ||
base_api_url: unique_cluster_base_api_url(), | ||
realm_name: unique_realm_name(), | ||
realm_private_key: @valid_pem_private_key | ||
}) | ||
|
||
attrs = | ||
opts | ||
|> Enum.into(%{ | ||
name: unique_tenant_name(), | ||
slug: unique_tenant_slug(), | ||
public_key: @valid_pem_public_key, | ||
astarte_config: astarte_config | ||
}) | ||
|
||
Provisioning.provision_tenant(attrs) | ||
end | ||
end |