From 24b3a9ac3fb4d2f55589e2daef8648a11a83943e Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Fri, 31 Jan 2025 19:02:24 +0100 Subject: [PATCH 01/13] feat: Customize app scaling --- apps/lenra/lib/lenra/apps.ex | 73 +++++++- apps/lenra/lib/lenra/apps/app.ex | 1 - apps/lenra/lib/lenra/apps/environment.ex | 4 +- .../lenra/apps/environments_scale_options.ex | 39 ++++ .../20250131103046_env_scale_option.exs | 15 ++ apps/lenra/test/lenra/app_adapter_test.exs | 171 ++++++++++++++++++ .../lenra/test/lenra/apps/deployment_test.exs | 8 +- apps/lenra/test/support/faas_stub_helper.ex | 6 + apps/lenra_web/lib/lenra_web/app_adapter.ex | 23 ++- config/config.exs | 9 +- libs/application_runner/lib/adapter.ex | 5 + .../lib/channels/app_socket.ex | 15 +- .../lib/environment/metadata.ex | 8 +- .../lib/monitor/environment_monitor.ex | 7 +- .../lib/services/application_services.ex | 10 +- .../test/support/app_adapter.ex | 5 + 16 files changed, 373 insertions(+), 26 deletions(-) create mode 100644 apps/lenra/lib/lenra/apps/environments_scale_options.ex create mode 100644 apps/lenra/priv/repo/migrations/20250131103046_env_scale_option.exs create mode 100644 apps/lenra/test/lenra/app_adapter_test.exs diff --git a/apps/lenra/lib/lenra/apps.ex b/apps/lenra/lib/lenra/apps.ex index 05b88d30..2a4d8e56 100644 --- a/apps/lenra/lib/lenra/apps.ex +++ b/apps/lenra/lib/lenra/apps.ex @@ -19,6 +19,7 @@ defmodule Lenra.Apps do """ import Ecto.Query + alias Lenra.Apps.EnvironmentScaleOptions alias ApplicationRunner.ApplicationServices alias Lenra.Repo @@ -405,7 +406,10 @@ defmodule Lenra.Apps do end) |> Repo.transaction() - ApplicationServices.stop_app("#{OpenfaasServices.get_function_name(service_name, build_number)}") + if Application.fetch_env!(:application_runner, :scale_to_zero) do + ApplicationServices.stop_app("#{OpenfaasServices.get_function_name(service_name, build_number)}") + end + transaction # Function not found in openfaas, 2 retry (10s), @@ -787,4 +791,71 @@ defmodule Lenra.Apps do def get_image(image_id) do Repo.get(Image, image_id) end + + ############### + # Environment Scale Options # + ############### + + def effective_env_scale_options(env) when is_map(env) do + env_scale_options_for_subscription(env, Subscriptions.get_subscription_by_app_id(env.application_id)) + end + + defp env_scale_options_for_subscription(_env, nil) do + %{ + scale_min: Application.fetch_env!(:lenra, :scale_free_min), + scale_max: Application.fetch_env!(:lenra, :scale_free_max) + } + end + + defp env_scale_options_for_subscription(env, %Subscriptions.Subscription{}) do + env = + env + |> Repo.preload(:scale_options) + + scale_min = Application.fetch_env!(:lenra, :scale_paid_min) + scale_max = Application.fetch_env!(:lenra, :scale_paid_max) + + scale_options = env.scale_options || %{} + + scale_min = + (scale_options + |> Map.get(:min) || scale_min) + |> max(scale_min) + |> min(scale_max) + + scale_max = + (scale_options + |> Map.get(:max) || scale_max) + |> max(1) + |> max(scale_min) + |> min(scale_max) + + %{ + scale_min: scale_min, + scale_max: scale_max + } + end + + def get_env_scale_options(env_id) do + Repo.get_by(EnvironmentScaleOptions, environment_id: env_id) + end + + def fetch_env_scale_options(env_id) do + Repo.fetch_by(EnvironmentScaleOptions, environment_id: env_id) + end + + def create_env_scale_options(env_id, params) do + Ecto.Multi.new() + |> Ecto.Multi.insert( + :inserted_env_scale_options, + EnvironmentScaleOptions.new(env_id, params) + ) + |> Repo.transaction() + end + + def update_env_scale_options(env_scale_options, params) do + Ecto.Multi.new() + |> Ecto.Multi.update(:updated_env_scale_options, EnvironmentScaleOptions.changeset(env_scale_options, params)) + |> Repo.transaction() + end end diff --git a/apps/lenra/lib/lenra/apps/app.ex b/apps/lenra/lib/lenra/apps/app.ex index cf86836d..2ae13351 100644 --- a/apps/lenra/lib/lenra/apps/app.ex +++ b/apps/lenra/lib/lenra/apps/app.ex @@ -7,7 +7,6 @@ defmodule Lenra.Apps.App do import Ecto.Changeset alias Lenra.Accounts.User - alias Lenra.Apps.{Build, Environment, MainEnv} @type t :: %__MODULE__{} diff --git a/apps/lenra/lib/lenra/apps/environment.ex b/apps/lenra/lib/lenra/apps/environment.ex index 3563ebb0..209cc9f5 100644 --- a/apps/lenra/lib/lenra/apps/environment.ex +++ b/apps/lenra/lib/lenra/apps/environment.ex @@ -7,8 +7,7 @@ defmodule Lenra.Apps.Environment do import Ecto.Changeset alias Lenra.Accounts.User - alias Lenra.Apps.{App, Deployment} - alias Lenra.Apps.UserEnvironmentAccess + alias Lenra.Apps.{App, Deployment, EnvironmentScaleOptions, UserEnvironmentAccess} @type t :: %__MODULE__{} @@ -30,6 +29,7 @@ defmodule Lenra.Apps.Environment do belongs_to(:creator, User) belongs_to(:deployment, Deployment) many_to_many(:shared_with, User, join_through: UserEnvironmentAccess) + has_one(:scale_options, EnvironmentScaleOptions, foreign_key: :environment_id) timestamps() end diff --git a/apps/lenra/lib/lenra/apps/environments_scale_options.ex b/apps/lenra/lib/lenra/apps/environments_scale_options.ex new file mode 100644 index 00000000..84ae2d1e --- /dev/null +++ b/apps/lenra/lib/lenra/apps/environments_scale_options.ex @@ -0,0 +1,39 @@ +defmodule Lenra.Apps.EnvironmentScaleOptions do + @moduledoc """ + The environment scale options. + """ + + use Lenra.Schema + import Ecto.Changeset + + alias Lenra.Apps.Environment + + @type t :: %__MODULE__{} + + @derive {Jason.Encoder, + only: [ + :environment_id, + :min, + :max + ]} + schema "environments_scale_options" do + field(:min, :integer) + field(:max, :integer) + belongs_to(:environment, Environment) + + timestamps() + end + + def changeset(scale_options, params \\ %{}) do + scale_options + |> cast(params, [:min, :max]) + |> validate_required([:environment_id]) + |> foreign_key_constraint(:environment_id) + |> unique_constraint([:environment_id], name: :environment_id_unique_index) + end + + def new(env_id, params) do + %__MODULE__{environment_id: env_id} + |> __MODULE__.changeset(params) + end +end diff --git a/apps/lenra/priv/repo/migrations/20250131103046_env_scale_option.exs b/apps/lenra/priv/repo/migrations/20250131103046_env_scale_option.exs new file mode 100644 index 00000000..df3dadb0 --- /dev/null +++ b/apps/lenra/priv/repo/migrations/20250131103046_env_scale_option.exs @@ -0,0 +1,15 @@ +defmodule Lenra.Repo.Migrations.EnvScaleOption do + use Ecto.Migration + + def change do + create table(:environments_scale_options) do + add(:environment_id, references(:environments, on_delete: :delete_all), null: false) + add(:min, :integer) + add(:max, :integer) + + timestamps() + end + + create(unique_index(:environments_scale_options, [:environment_id])) + end +end diff --git a/apps/lenra/test/lenra/app_adapter_test.exs b/apps/lenra/test/lenra/app_adapter_test.exs new file mode 100644 index 00000000..f9182cbd --- /dev/null +++ b/apps/lenra/test/lenra/app_adapter_test.exs @@ -0,0 +1,171 @@ +defmodule LenraWeb.AppAdapterTest do + @moduledoc """ + Test the app adapter + """ + use Lenra.RepoCase, async: true + + alias Lenra.{Apps, Repo} + alias Lenra.Errors.BusinessError + alias Lenra.FaasStub + alias Lenra.GitlabStubHelper + alias Lenra.Subscriptions.Subscription + alias LenraWeb.AppAdapter + + setup do + GitlabStubHelper.create_gitlab_stub() + {:ok, %{inserted_user: user}} = UserTestHelper.register_john_doe() + {:ok, %{inserted_application: app, inserted_env: env}} = create_and_return_application(user, "test") + + {:ok, app: app, env: env} + end + + defp create_and_return_application(user, name) do + Apps.create_app(user.id, %{ + name: name, + color: "FFFFFF", + icon: "60189" + }) + end + + describe "get_function_name/1" do + test "returns function name when app is built", %{app: app, env: env} do + {:ok, %{inserted_build: build}} = + Apps.create_build(app.creator_id, app.id, %{ + commit_hash: "abcdef" + }) + |> Repo.transaction() + + function_name = FaasStub.get_function_name(app.service_name, build.build_number) + + {:ok, %{inserted_deployment: deployment}} = Apps.create_deployment(env.id, build.id, app.creator_id) + + {:ok, _} = + Ecto.Multi.new() + |> Ecto.Multi.update( + :updated_deployment, + Ecto.Changeset.change(deployment, status: :success) + ) + |> Ecto.Multi.run(:updated_env, fn _repo, %{updated_deployment: updated_deployment} -> + env + |> Ecto.Changeset.change(deployment_id: updated_deployment.id) + |> Repo.update() + end) + |> Repo.transaction() + + assert AppAdapter.get_function_name(app.service_name) == function_name + end + + test "returns error tuple when app is not built", %{app: app} do + assert AppAdapter.get_function_name(app.service_name) == BusinessError.application_not_built_tuple() + end + end + + describe "get_env_id/1" do + test "returns environment id", %{app: app, env: env} do + assert AppAdapter.get_env_id(app.service_name) == env.id + end + end + + describe "get_scale_options/1" do + test "returns scale options without subscription nor scale options", %{app: app} do + assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 0, scale_max: 1} + end + + test "returns scale options without subscription with scale options", %{app: app, env: env} do + Apps.create_env_scale_options(env.id, %{ + min: 2, + max: 5 + }) + + assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 0, scale_max: 1} + end + + test "returns scale options with subscription without scale options", %{app: app} do + subscription = + Subscription.new(%{ + application_id: app.id, + start_date: DateTime.utc_now(), + end_date: DateTime.utc_now() |> DateTime.add(1000, :second), + plan: "month" + }) + + Repo.insert(subscription) + + assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 0, scale_max: 5} + end + + test "returns scale options with subscription with min scale options", %{app: app, env: env} do + {:ok, _} = Apps.create_env_scale_options(env.id, %{ + min: 2 + }) + + subscription = + Subscription.new(%{ + application_id: app.id, + start_date: DateTime.utc_now(), + end_date: DateTime.utc_now() |> DateTime.add(1000, :second), + plan: "month" + }) + + Repo.insert(subscription) + + assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 2, scale_max: 5} + end + + test "returns scale options with subscription with max scale options", %{app: app, env: env} do + {:ok, _} = Apps.create_env_scale_options(env.id, %{ + max: 5 + }) + + subscription = + Subscription.new(%{ + application_id: app.id, + start_date: DateTime.utc_now(), + end_date: DateTime.utc_now() |> DateTime.add(1000, :second), + plan: "month" + }) + + Repo.insert(subscription) + + assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 0, scale_max: 5} + end + + test "returns scale options with subscription with min and max scale options", %{app: app, env: env} do + {:ok, _} = Apps.create_env_scale_options(env.id, %{ + min: 2, + max: 5 + }) + + subscription = + Subscription.new(%{ + application_id: app.id, + start_date: DateTime.utc_now(), + end_date: DateTime.utc_now() |> DateTime.add(1000, :second), + plan: "month" + }) + + Repo.insert(subscription) + + assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 2, scale_max: 5} + end + + test "returns scale options with subscription with reversed min and max scale options", %{app: app, env: env} do + {:ok, _} = Apps.create_env_scale_options(env.id, %{ + min: 2, + max: 1 + }) + + subscription = + Subscription.new(%{ + application_id: app.id, + start_date: DateTime.utc_now(), + end_date: DateTime.utc_now() |> DateTime.add(1000, :second), + plan: "month" + }) + + Repo.insert(subscription) + + assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 2, scale_max: 2} + end + end +end diff --git a/apps/lenra/test/lenra/apps/deployment_test.exs b/apps/lenra/test/lenra/apps/deployment_test.exs index f11f2d16..0aa96f0a 100644 --- a/apps/lenra/test/lenra/apps/deployment_test.exs +++ b/apps/lenra/test/lenra/apps/deployment_test.exs @@ -36,12 +36,6 @@ defmodule Lenra.Apps.DeploymentTest do app end - def get_function_name(service_name, build_number) do - lenra_env = Application.fetch_env!(:lenra, :lenra_env) - - String.downcase("#{lenra_env}-#{service_name}-#{build_number}") - end - describe "create" do test "deployment successfully", %{app: app} do bypass = FaasStub.create_faas_stub() @@ -53,7 +47,7 @@ defmodule Lenra.Apps.DeploymentTest do FaasStub.expect_get_function_once( bypass, %{"ok" => "200"}, - get_function_name(app.service_name, build.build_number) + FaasStub.get_function_name(app.service_name, build.build_number) ) Apps.create_deployment(env.id, build.id, app.creator_id) diff --git a/apps/lenra/test/support/faas_stub_helper.ex b/apps/lenra/test/support/faas_stub_helper.ex index 76d922f2..290bf58b 100644 --- a/apps/lenra/test/support/faas_stub_helper.ex +++ b/apps/lenra/test/support/faas_stub_helper.ex @@ -118,4 +118,10 @@ defmodule Lenra.FaasStub do def push(app_name, call_result) do Agent.update(__MODULE__, &Map.put(&1, app_name, Map.get(&1, app_name, []) ++ [call_result])) end + + def get_function_name(service_name, build_number) do + lenra_env = Application.fetch_env!(:lenra, :lenra_env) + + String.downcase("#{lenra_env}-#{service_name}-#{build_number}") + end end diff --git a/apps/lenra_web/lib/lenra_web/app_adapter.ex b/apps/lenra_web/lib/lenra_web/app_adapter.ex index 9da9d841..76a66a7b 100644 --- a/apps/lenra_web/lib/lenra_web/app_adapter.ex +++ b/apps/lenra_web/lib/lenra_web/app_adapter.ex @@ -9,6 +9,7 @@ defmodule LenraWeb.AppAdapter do alias Lenra.{Apps, Repo} alias Lenra.Apps.{App, Environment, MainEnv} alias Lenra.Errors.BusinessError + alias Lenra.Subscriptions require Logger @@ -35,7 +36,13 @@ defmodule LenraWeb.AppAdapter do case get_app(app_name, environment: [deployment: [:build]]) do %App{} = application -> - build = application.main_env.environment.deployment.build + build = + get_in(application, [ + Access.key(:main_env), + Access.key(:environment), + Access.key(:deployment), + Access.key(:build) + ]) if build do String.downcase("#{lenra_env}-#{app_name}-#{build.build_number}") @@ -54,9 +61,19 @@ defmodule LenraWeb.AppAdapter do application = App |> Repo.get_by(service_name: app_name) - |> Repo.preload(:environments) + |> Repo.preload(:main_env) - List.first(application.environments).id + application.main_env.environment_id + end + + @impl ApplicationRunner.Adapter + def get_scale_options(app_name) do + application = + App + |> Repo.get_by(service_name: app_name) + |> Repo.preload(main_env: [:environment]) + + Apps.effective_env_scale_options(application.main_env.environment) end @impl ApplicationRunner.Adapter diff --git a/config/config.exs b/config/config.exs index 7b534d31..939216ab 100644 --- a/config/config.exs +++ b/config/config.exs @@ -122,7 +122,14 @@ config :application_runner, ApplicationRunner.Scheduler, storage: ApplicationRun # additional_env_modules: {LenraWeb.ApplicationRunnerAdapter, :additional_env_modules} config :lenra, - kubernetes_build_namespace: System.get_env("KUBERNETES_BUILD_NAMESPACE", "lenra-build") +kubernetes_build_namespace: System.get_env("KUBERNETES_BUILD_NAMESPACE", "lenra-build") + +# Scaling configuration +config :lenra, + scale_free_min: 0, + scale_free_max: 1, + scale_paid_min: 0, + scale_paid_max: 5 config :argon2_elixir, t_cost: 8, diff --git a/libs/application_runner/lib/adapter.ex b/libs/application_runner/lib/adapter.ex index 2bb8eea4..f04f298b 100644 --- a/libs/application_runner/lib/adapter.ex +++ b/libs/application_runner/lib/adapter.ex @@ -18,5 +18,10 @@ defmodule ApplicationRunner.Adapter do """ @callback get_env_id(String.t()) :: number() + @doc """ + Override this function to return the scale options from the app_name to the server/devtools needs + """ + @callback get_scale_options(String.t()) :: %{ scale_min: number(), scale_max: number() } + @callback resource_from_params(map()) :: {:ok, number, any(), map()} | {:error, any()} end diff --git a/libs/application_runner/lib/channels/app_socket.ex b/libs/application_runner/lib/channels/app_socket.ex index 1f100c56..75e307e9 100644 --- a/libs/application_runner/lib/channels/app_socket.ex +++ b/libs/application_runner/lib/channels/app_socket.ex @@ -114,7 +114,8 @@ defmodule ApplicationRunner.AppSocket do with function_name when is_bitstring(function_name) <- adapter_mod.get_function_name(app_name), - env_id <- adapter_mod.get_env_id(app_name) do + env_id <- adapter_mod.get_env_id(app_name), + scale_options <- adapter_mod.get_scale_options(app_name) do # prepare the assigns to the session/environment session_metadata = %Session.Metadata{ env_id: env_id, @@ -125,10 +126,14 @@ defmodule ApplicationRunner.AppSocket do context: context } - env_metadata = %Environment.Metadata{ - env_id: env_id, - function_name: function_name - } + env_metadata = + Map.merge( + %Environment.Metadata{ + env_id: env_id, + function_name: function_name + }, + scale_options + ) {:ok, env_metadata, session_metadata} else diff --git a/libs/application_runner/lib/environment/metadata.ex b/libs/application_runner/lib/environment/metadata.ex index ceddecb1..b84e41a8 100644 --- a/libs/application_runner/lib/environment/metadata.ex +++ b/libs/application_runner/lib/environment/metadata.ex @@ -5,11 +5,15 @@ defmodule ApplicationRunner.Environment.Metadata do @enforce_keys [:env_id, :function_name] defstruct [ :env_id, - :function_name + :function_name, + :scale_min, + :scale_max ] @type t :: %__MODULE__{ env_id: term(), - function_name: term() + function_name: term(), + scale_min: non_neg_integer(), + scale_max: non_neg_integer() } end diff --git a/libs/application_runner/lib/monitor/environment_monitor.ex b/libs/application_runner/lib/monitor/environment_monitor.ex index 9b7cf457..efdbc5d4 100644 --- a/libs/application_runner/lib/monitor/environment_monitor.ex +++ b/libs/application_runner/lib/monitor/environment_monitor.ex @@ -37,11 +37,12 @@ defmodule ApplicationRunner.Monitor.EnvironmentMonitor do base_url = Application.fetch_env!(:application_runner, :faas_url) auth = Application.fetch_env!(:application_runner, :faas_auth) - # env_id = Map.get(metadata, :env_id) - function_name = Map.get(metadata, :function_name) Logger.debug("#{__MODULE__} handle down #{inspect(pid)} with metadata #{inspect(metadata)}") - if Application.fetch_env!(:application_runner, :scale_to_zero) do + min_scale = Map.get(metadata, :min_scale, 0) + + if Application.fetch_env!(:application_runner, :scale_to_zero) && min_scale == 0 do + function_name = Map.get(metadata, :function_name) ApplicationServices.stop_app(function_name) end diff --git a/libs/application_runner/lib/services/application_services.ex b/libs/application_runner/lib/services/application_services.ex index 79d5fcdc..4eaad8c6 100644 --- a/libs/application_runner/lib/services/application_services.ex +++ b/libs/application_runner/lib/services/application_services.ex @@ -259,7 +259,15 @@ defmodule ApplicationRunner.ApplicationServices do end @doc """ - Set the maximum scale of an OpenFaaS application. + Set the scale values of an OpenFaaS application. + """ + @spec set_app_scale(String.t(), integer(), integer()) :: :ok | {:error, struct} | {:ok, any} + def set_app_scale(function_name, min_scale, max_scale) do + set_app_labels(function_name, %{@min_scale_label => to_string(min_scale), @max_scale_label => to_string(max_scale)}) + end + + @doc """ + Set the scale factor of an OpenFaaS application. """ @spec set_app_scale_factor(String.t(), integer()) :: :ok | {:error, struct} | {:ok, any} def set_app_scale_factor(function_name, scale_factor) when scale_factor in [0, 100] do diff --git a/libs/application_runner/test/support/app_adapter.ex b/libs/application_runner/test/support/app_adapter.ex index b8cccf53..628fed51 100644 --- a/libs/application_runner/test/support/app_adapter.ex +++ b/libs/application_runner/test/support/app_adapter.ex @@ -28,6 +28,11 @@ defmodule ApplicationRunner.FakeAppAdapter do 1 end + @impl ApplicationRunner.Adapter + def get_scale_options(_app_name) do + %{scale_min: 0, scale_max: 1} + end + @impl ApplicationRunner.Adapter def resource_from_params(%{"connect_result" => connect_result}) do connect_result From cb36dbc045be765f70baa933b276eeb9537aedbf Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Mon, 3 Feb 2025 10:01:16 +0100 Subject: [PATCH 02/13] refactor: min and max instead of scale_min and scale_max --- apps/lenra/lib/lenra/apps.ex | 39 ++++++------- .../lib/lenra/services/openfaas_services.ex | 4 +- apps/lenra/lib/lenra/subscriptions.ex | 1 + apps/lenra/test/lenra/app_adapter_test.exs | 49 ++++++++-------- config/config.exs | 2 +- libs/application_runner/lib/adapter.ex | 2 +- .../lib/channels/app_socket.ex | 14 ++--- .../lib/environment/dynamic_supervisor.ex | 9 +-- .../lib/services/application_services.ex | 56 +++++++++++-------- .../test/support/app_adapter.ex | 2 +- 10 files changed, 91 insertions(+), 87 deletions(-) diff --git a/apps/lenra/lib/lenra/apps.ex b/apps/lenra/lib/lenra/apps.ex index 2a4d8e56..d75c6376 100644 --- a/apps/lenra/lib/lenra/apps.ex +++ b/apps/lenra/lib/lenra/apps.ex @@ -19,14 +19,11 @@ defmodule Lenra.Apps do """ import Ecto.Query - alias Lenra.Apps.EnvironmentScaleOptions alias ApplicationRunner.ApplicationServices - + alias ApplicationRunner.MongoStorage.MongoUserLink alias Lenra.Repo alias Lenra.Subscriptions - alias Lenra.{Accounts, EmailWorker, GitlabApiServices, OpenfaasServices} - alias Lenra.Kubernetes.ApiServices alias Lenra.Apps.{ @@ -34,6 +31,7 @@ defmodule Lenra.Apps do Build, Deployment, Environment, + EnvironmentScaleOptions, Image, Logo, MainEnv, @@ -42,8 +40,6 @@ defmodule Lenra.Apps do UserEnvironmentRole } - alias ApplicationRunner.MongoStorage.MongoUserLink - alias Lenra.Errors.{BusinessError, TechnicalError} require Logger @@ -343,8 +339,7 @@ defmodule Lenra.Apps do {:ok, _status} <- OpenfaasServices.deploy_app( loaded_build.application.service_name, - build.build_number, - Subscriptions.get_max_replicas(loaded_build.application.id) + build.build_number ) do update_deployment(deployment, status: :waitingForAppReady) @@ -406,9 +401,9 @@ defmodule Lenra.Apps do end) |> Repo.transaction() - if Application.fetch_env!(:application_runner, :scale_to_zero) do - ApplicationServices.stop_app("#{OpenfaasServices.get_function_name(service_name, build_number)}") - end + service_name + |> OpenfaasServices.get_function_name(build_number) + |> ApplicationServices.set_app_scale_options(effective_env_scale_options(env)) transaction @@ -802,8 +797,8 @@ defmodule Lenra.Apps do defp env_scale_options_for_subscription(_env, nil) do %{ - scale_min: Application.fetch_env!(:lenra, :scale_free_min), - scale_max: Application.fetch_env!(:lenra, :scale_free_max) + min: Application.fetch_env!(:lenra, :scale_free_min), + max: Application.fetch_env!(:lenra, :scale_free_max) } end @@ -812,27 +807,27 @@ defmodule Lenra.Apps do env |> Repo.preload(:scale_options) - scale_min = Application.fetch_env!(:lenra, :scale_paid_min) - scale_max = Application.fetch_env!(:lenra, :scale_paid_max) + default_scale_min = Application.fetch_env!(:lenra, :scale_paid_min) + default_scale_max = Application.fetch_env!(:lenra, :scale_paid_max) scale_options = env.scale_options || %{} scale_min = (scale_options - |> Map.get(:min) || scale_min) - |> max(scale_min) - |> min(scale_max) + |> Map.get(:min) || default_scale_min) + |> max(default_scale_min) + |> min(default_scale_max) scale_max = (scale_options - |> Map.get(:max) || scale_max) + |> Map.get(:max) || default_scale_max) |> max(1) |> max(scale_min) - |> min(scale_max) + |> min(default_scale_max) %{ - scale_min: scale_min, - scale_max: scale_max + min: scale_min, + max: scale_max } end diff --git a/apps/lenra/lib/lenra/services/openfaas_services.ex b/apps/lenra/lib/lenra/services/openfaas_services.ex index 531b4541..ad033d5b 100644 --- a/apps/lenra/lib/lenra/services/openfaas_services.ex +++ b/apps/lenra/lib/lenra/services/openfaas_services.ex @@ -28,11 +28,11 @@ defmodule Lenra.OpenfaasServices do String.downcase("#{lenra_env}-#{service_name}-#{build_number}") end - def deploy_app(service_name, build_number, replicas) do + def deploy_app(service_name, build_number, scale_options \\ %{}) when is_map(scale_options) do ApplicationServices.deploy_app( get_function_name(service_name, build_number), Apps.image_name(service_name, build_number), - replicas + scale_options ) end diff --git a/apps/lenra/lib/lenra/subscriptions.ex b/apps/lenra/lib/lenra/subscriptions.ex index f0f84183..5caac780 100644 --- a/apps/lenra/lib/lenra/subscriptions.ex +++ b/apps/lenra/lib/lenra/subscriptions.ex @@ -30,6 +30,7 @@ defmodule Lenra.Subscriptions do end end + @deprecated "Use Lenra.Apps.effective_env_scale_options/1" def get_max_replicas(application_id) do if get_subscription_by_app_id(application_id) != nil do 5 diff --git a/apps/lenra/test/lenra/app_adapter_test.exs b/apps/lenra/test/lenra/app_adapter_test.exs index f9182cbd..dee6b857 100644 --- a/apps/lenra/test/lenra/app_adapter_test.exs +++ b/apps/lenra/test/lenra/app_adapter_test.exs @@ -30,7 +30,8 @@ defmodule LenraWeb.AppAdapterTest do describe "get_function_name/1" do test "returns function name when app is built", %{app: app, env: env} do {:ok, %{inserted_build: build}} = - Apps.create_build(app.creator_id, app.id, %{ + app.creator_id + |> Apps.create_build(app.id, %{ commit_hash: "abcdef" }) |> Repo.transaction() @@ -68,7 +69,7 @@ defmodule LenraWeb.AppAdapterTest do describe "get_scale_options/1" do test "returns scale options without subscription nor scale options", %{app: app} do - assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 0, scale_max: 1} + assert AppAdapter.get_scale_options(app.service_name) == %{min: 0, max: 1} end test "returns scale options without subscription with scale options", %{app: app, env: env} do @@ -77,7 +78,7 @@ defmodule LenraWeb.AppAdapterTest do max: 5 }) - assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 0, scale_max: 1} + assert AppAdapter.get_scale_options(app.service_name) == %{min: 0, max: 1} end test "returns scale options with subscription without scale options", %{app: app} do @@ -91,13 +92,14 @@ defmodule LenraWeb.AppAdapterTest do Repo.insert(subscription) - assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 0, scale_max: 5} + assert AppAdapter.get_scale_options(app.service_name) == %{min: 0, max: 5} end test "returns scale options with subscription with min scale options", %{app: app, env: env} do - {:ok, _} = Apps.create_env_scale_options(env.id, %{ - min: 2 - }) + {:ok, _} = + Apps.create_env_scale_options(env.id, %{ + min: 2 + }) subscription = Subscription.new(%{ @@ -109,13 +111,14 @@ defmodule LenraWeb.AppAdapterTest do Repo.insert(subscription) - assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 2, scale_max: 5} + assert AppAdapter.get_scale_options(app.service_name) == %{min: 2, max: 5} end test "returns scale options with subscription with max scale options", %{app: app, env: env} do - {:ok, _} = Apps.create_env_scale_options(env.id, %{ - max: 5 - }) + {:ok, _} = + Apps.create_env_scale_options(env.id, %{ + max: 5 + }) subscription = Subscription.new(%{ @@ -127,14 +130,15 @@ defmodule LenraWeb.AppAdapterTest do Repo.insert(subscription) - assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 0, scale_max: 5} + assert AppAdapter.get_scale_options(app.service_name) == %{min: 0, max: 5} end test "returns scale options with subscription with min and max scale options", %{app: app, env: env} do - {:ok, _} = Apps.create_env_scale_options(env.id, %{ - min: 2, - max: 5 - }) + {:ok, _} = + Apps.create_env_scale_options(env.id, %{ + min: 2, + max: 5 + }) subscription = Subscription.new(%{ @@ -146,14 +150,15 @@ defmodule LenraWeb.AppAdapterTest do Repo.insert(subscription) - assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 2, scale_max: 5} + assert AppAdapter.get_scale_options(app.service_name) == %{min: 2, max: 5} end test "returns scale options with subscription with reversed min and max scale options", %{app: app, env: env} do - {:ok, _} = Apps.create_env_scale_options(env.id, %{ - min: 2, - max: 1 - }) + {:ok, _} = + Apps.create_env_scale_options(env.id, %{ + min: 2, + max: 1 + }) subscription = Subscription.new(%{ @@ -165,7 +170,7 @@ defmodule LenraWeb.AppAdapterTest do Repo.insert(subscription) - assert AppAdapter.get_scale_options(app.service_name) == %{scale_min: 2, scale_max: 2} + assert AppAdapter.get_scale_options(app.service_name) == %{min: 2, max: 2} end end end diff --git a/config/config.exs b/config/config.exs index 939216ab..b4b7f57f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -122,7 +122,7 @@ config :application_runner, ApplicationRunner.Scheduler, storage: ApplicationRun # additional_env_modules: {LenraWeb.ApplicationRunnerAdapter, :additional_env_modules} config :lenra, -kubernetes_build_namespace: System.get_env("KUBERNETES_BUILD_NAMESPACE", "lenra-build") + kubernetes_build_namespace: System.get_env("KUBERNETES_BUILD_NAMESPACE", "lenra-build") # Scaling configuration config :lenra, diff --git a/libs/application_runner/lib/adapter.ex b/libs/application_runner/lib/adapter.ex index f04f298b..0678cee4 100644 --- a/libs/application_runner/lib/adapter.ex +++ b/libs/application_runner/lib/adapter.ex @@ -21,7 +21,7 @@ defmodule ApplicationRunner.Adapter do @doc """ Override this function to return the scale options from the app_name to the server/devtools needs """ - @callback get_scale_options(String.t()) :: %{ scale_min: number(), scale_max: number() } + @callback get_scale_options(String.t()) :: %{min: number(), max: number()} @callback resource_from_params(map()) :: {:ok, number, any(), map()} | {:error, any()} end diff --git a/libs/application_runner/lib/channels/app_socket.ex b/libs/application_runner/lib/channels/app_socket.ex index 75e307e9..784d1934 100644 --- a/libs/application_runner/lib/channels/app_socket.ex +++ b/libs/application_runner/lib/channels/app_socket.ex @@ -126,14 +126,12 @@ defmodule ApplicationRunner.AppSocket do context: context } - env_metadata = - Map.merge( - %Environment.Metadata{ - env_id: env_id, - function_name: function_name - }, - scale_options - ) + env_metadata = %Environment.Metadata{ + env_id: env_id, + function_name: function_name, + scale_min: scale_options.min, + scale_max: scale_options.max + } {:ok, env_metadata, session_metadata} else diff --git a/libs/application_runner/lib/environment/dynamic_supervisor.ex b/libs/application_runner/lib/environment/dynamic_supervisor.ex index 8f250fcf..431aa5c2 100644 --- a/libs/application_runner/lib/environment/dynamic_supervisor.ex +++ b/libs/application_runner/lib/environment/dynamic_supervisor.ex @@ -33,14 +33,7 @@ defmodule ApplicationRunner.Environment.DynamicSupervisor do defp start_env(%Environment.Metadata{} = env_metadata) do Logger.debug("#{__MODULE__} Start Environment Supervisor with env_metadata: #{inspect(env_metadata)}") - start_result = - if Application.fetch_env!(:application_runner, :scale_to_zero) do - ApplicationServices.start_app(env_metadata.function_name) - else - {:ok, nil} - end - - with {:ok, _status} <- start_result, + with {:ok, _status} <- ApplicationServices.start_app(env_metadata.function_name), {:ok, pid} <- DynamicSupervisor.start_child( __MODULE__, diff --git a/libs/application_runner/lib/services/application_services.ex b/libs/application_runner/lib/services/application_services.ex index 4eaad8c6..ebe19240 100644 --- a/libs/application_runner/lib/services/application_services.ex +++ b/libs/application_runner/lib/services/application_services.ex @@ -14,9 +14,9 @@ defmodule ApplicationRunner.ApplicationServices do @min_scale_label "com.openfaas.scale.min" @max_scale_label "com.openfaas.scale.max" @scale_factor_label "com.openfaas.scale.factor" - @min_scale_default "1" - @max_scale_default "5" - @scale_factor_default "10" + @min_scale_default 1 + @max_scale_default 5 + @scale_factor_default 10 defp get_http_context do base_url = Application.fetch_env!(:application_runner, :faas_url) @@ -191,8 +191,8 @@ defmodule ApplicationRunner.ApplicationServices do @doc """ Deploy an application to OpenFaaS. """ - @spec deploy_app(String.t(), String.t(), integer()) :: :ok | {:error, struct} | {:ok, any} - def deploy_app(function_name, image_name, replicas) do + @spec deploy_app(String.t(), String.t()) :: :ok | {:error, struct} | {:ok, any} + def deploy_app(function_name, image_name, scale_options \\ %{}) when is_map(scale_options) do Logger.info("Deploy Openfaas application #{function_name} with image #{image_name}") {base_url, headers} = get_http_context() @@ -205,9 +205,9 @@ defmodule ApplicationRunner.ApplicationServices do function_name, image_name, %{ - @min_scale_label => @min_scale_default, - @max_scale_label => to_string(replicas), - @scale_factor_label => @scale_factor_default + @min_scale_label => to_string(Map.get(scale_options, :min, @min_scale_default)), + @max_scale_label => to_string(Map.get(scale_options, :max, @max_scale_default)), + @scale_factor_label => to_string(Map.get(scale_options, :factor, @scale_factor_default)) } ) ) @@ -227,19 +227,19 @@ defmodule ApplicationRunner.ApplicationServices do @doc """ Start an OpenFaaS application. """ - @spec start_app(String.t()) :: :ok | {:error, struct} | {:ok, any} - def start_app(function_name) do + @spec start_app(String.t(), integer()) :: :ok | {:error, struct} | {:ok, any} + def start_app(function_name, scale_min \\ 0) do Logger.info("Start Openfaas application #{function_name}") - set_app_min_scale(function_name, 1) + set_app_min_scale(function_name, max(scale_min, 1)) end @doc """ Stop an OpenFaaS application. """ - @spec stop_app(String.t()) :: :ok | {:error, struct} | {:ok, any} - def stop_app(function_name) do + @spec stop_app(String.t(), integer()) :: :ok | {:error, struct} | {:ok, any} + def stop_app(function_name, scale_min \\ 0) do Logger.info("Stop Openfaas application #{function_name}") - set_app_min_scale(function_name, 0) + set_app_min_scale(function_name, max(scale_min, 0)) end @doc """ @@ -258,14 +258,6 @@ defmodule ApplicationRunner.ApplicationServices do set_app_labels(function_name, %{@max_scale_label => to_string(max_scale)}) end - @doc """ - Set the scale values of an OpenFaaS application. - """ - @spec set_app_scale(String.t(), integer(), integer()) :: :ok | {:error, struct} | {:ok, any} - def set_app_scale(function_name, min_scale, max_scale) do - set_app_labels(function_name, %{@min_scale_label => to_string(min_scale), @max_scale_label => to_string(max_scale)}) - end - @doc """ Set the scale factor of an OpenFaaS application. """ @@ -274,6 +266,26 @@ defmodule ApplicationRunner.ApplicationServices do set_app_labels(function_name, %{@scale_factor_label => to_string(scale_factor)}) end + @doc """ + Set the scale values of an OpenFaaS application. + """ + @spec set_app_scale_options(String.t(), map()) :: :ok | {:error, struct} | {:ok, any} + def set_app_scale_options(function_name, scale_options) do + labels = + scale_options + |> Enum.map(fn {key, value} -> + {case key do + :scale_min -> @min_scale_label + :scale_max -> @max_scale_label + :scale_factor -> @scale_factor_label + _ -> nil + end, to_string(value)} + end) + |> Enum.filter(fn {key, _} -> key != nil end) + + set_app_labels(function_name, labels) + end + @doc """ Get the status of an application from OpenFaaS. """ diff --git a/libs/application_runner/test/support/app_adapter.ex b/libs/application_runner/test/support/app_adapter.ex index 628fed51..eee7b6a1 100644 --- a/libs/application_runner/test/support/app_adapter.ex +++ b/libs/application_runner/test/support/app_adapter.ex @@ -30,7 +30,7 @@ defmodule ApplicationRunner.FakeAppAdapter do @impl ApplicationRunner.Adapter def get_scale_options(_app_name) do - %{scale_min: 0, scale_max: 1} + %{min: 0, max: 1} end @impl ApplicationRunner.Adapter From 984593d751104466f0a2f59eddbc5def1e3d8673 Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Mon, 3 Feb 2025 11:19:08 +0100 Subject: [PATCH 03/13] feat: stop_app with min_scale --- .../lib/environment/dynamic_supervisor.ex | 4 +- .../lib/monitor/environment_monitor.ex | 9 +-- .../lib/services/application_services.ex | 4 +- .../environment/dynamic_supervisor_test.exs | 68 ++++++++++++++++++- .../services/application_services_test.exs | 32 +++++++-- 5 files changed, 101 insertions(+), 16 deletions(-) diff --git a/libs/application_runner/lib/environment/dynamic_supervisor.ex b/libs/application_runner/lib/environment/dynamic_supervisor.ex index 431aa5c2..47bea4e7 100644 --- a/libs/application_runner/lib/environment/dynamic_supervisor.ex +++ b/libs/application_runner/lib/environment/dynamic_supervisor.ex @@ -30,10 +30,10 @@ defmodule ApplicationRunner.Environment.DynamicSupervisor do @spec start_env(term()) :: {:error, {:already_started, pid()}} | {:ok, pid()} | {:error, term()} - defp start_env(%Environment.Metadata{} = env_metadata) do + defp start_env(%Environment.Metadata{scale_min: scale_min} = env_metadata) do Logger.debug("#{__MODULE__} Start Environment Supervisor with env_metadata: #{inspect(env_metadata)}") - with {:ok, _status} <- ApplicationServices.start_app(env_metadata.function_name), + with {:ok, _status} <- ApplicationServices.start_app(env_metadata.function_name, scale_min), {:ok, pid} <- DynamicSupervisor.start_child( __MODULE__, diff --git a/libs/application_runner/lib/monitor/environment_monitor.ex b/libs/application_runner/lib/monitor/environment_monitor.ex index efdbc5d4..e64342dd 100644 --- a/libs/application_runner/lib/monitor/environment_monitor.ex +++ b/libs/application_runner/lib/monitor/environment_monitor.ex @@ -39,12 +39,9 @@ defmodule ApplicationRunner.Monitor.EnvironmentMonitor do Logger.debug("#{__MODULE__} handle down #{inspect(pid)} with metadata #{inspect(metadata)}") - min_scale = Map.get(metadata, :min_scale, 0) - - if Application.fetch_env!(:application_runner, :scale_to_zero) && min_scale == 0 do - function_name = Map.get(metadata, :function_name) - ApplicationServices.stop_app(function_name) - end + metadata + |> Map.get(:function_name) + |> ApplicationServices.stop_app(Map.get(metadata, :scale_min, 0)) {:noreply, new_state} end diff --git a/libs/application_runner/lib/services/application_services.ex b/libs/application_runner/lib/services/application_services.ex index ebe19240..925622aa 100644 --- a/libs/application_runner/lib/services/application_services.ex +++ b/libs/application_runner/lib/services/application_services.ex @@ -403,12 +403,12 @@ defmodule ApplicationRunner.ApplicationServices do TechnicalError.error_404_tuple(body) 500 -> - formated_error = + formatted_error = body |> Errors.format_error_with_stacktrace() |> TechnicalError.error_500() - Telemetry.event(:alert, %{}, formated_error) + Telemetry.event(:alert, %{}, formatted_error) TechnicalError.error_500_tuple(body) 504 -> diff --git a/libs/application_runner/test/environment/dynamic_supervisor_test.exs b/libs/application_runner/test/environment/dynamic_supervisor_test.exs index ecdad408..c62db171 100644 --- a/libs/application_runner/test/environment/dynamic_supervisor_test.exs +++ b/libs/application_runner/test/environment/dynamic_supervisor_test.exs @@ -27,7 +27,7 @@ defmodule ApplicationRunner.Environment.DynamixSupervisorTest do Plug.Conn.resp(conn, 200, Jason.encode!(%{name: @function_name})) end - test "should scall to one on environment start and to zero on environment exit" do + test "should scale to one on environment start and to zero on environment exit" do {:ok, %{id: env_id}} = Repo.insert(Contract.Environment.new()) bypass = Bypass.open(port: 1234) @@ -37,7 +37,9 @@ defmodule ApplicationRunner.Environment.DynamixSupervisorTest do env_metadata = %Environment.Metadata{ env_id: env_id, - function_name: @function_name + function_name: @function_name, + scale_min: 0, + scale_max: 1 } on_exit(fn -> @@ -86,4 +88,66 @@ defmodule ApplicationRunner.Environment.DynamixSupervisorTest do assert_receive(:lookup, 500) end + + test "should scale to 1 on environment start and stay at 1 on environment exit" do + {:ok, %{id: env_id}} = Repo.insert(Contract.Environment.new()) + + bypass = Bypass.open(port: 1234) + Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", &handle_app_info_resp/1) + Bypass.stub(bypass, "PUT", "/system/functions", &handle_resp/1) + Bypass.stub(bypass, "POST", "/function/#{@function_name}", &handle_resp/1) + + env_metadata = %Environment.Metadata{ + env_id: env_id, + function_name: @function_name, + scale_min: 1, + scale_max: 5 + } + + on_exit(fn -> + Swarm.unregister_name(Environment.Supervisor.get_name(env_id)) + end) + + # Check scale up + Bypass.expect_once( + bypass, + "PUT", + "/system/functions", + fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + app = Jason.decode!(body) + + assert "1" = app["labels"]["com.openfaas.scale.min"] + + conn + |> send_resp(200, "ok") + end + ) + + {:ok, _pid} = DynamicSupervisor.ensure_env_started(env_metadata) + + my_pid = self() + + # Check scale down + Bypass.expect_once( + bypass, + "PUT", + "/system/functions", + fn conn -> + send(my_pid, :lookup) + + {:ok, body, conn} = Plug.Conn.read_body(conn) + app = Jason.decode!(body) + + assert "1" = app["labels"]["com.openfaas.scale.min"] + + conn + |> send_resp(200, "ok") + end + ) + + DynamicSupervisor.stop_env(env_id) + + assert_receive(:lookup, 500) + end end diff --git a/libs/application_runner/test/services/application_services_test.exs b/libs/application_runner/test/services/application_services_test.exs index c5c8067d..f00ee788 100644 --- a/libs/application_runner/test/services/application_services_test.exs +++ b/libs/application_runner/test/services/application_services_test.exs @@ -46,7 +46,7 @@ defmodule ApplicationRunner.ApplicationServicesTest do ApplicationServices.start_app(@function_name) end - test "stop app" do + test "start app with min scale" do bypass = Bypass.open(port: 1234) Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", app_info_handler()) Bypass.stub(bypass, "PUT", "/system/functions", &handle_resp/1) @@ -60,14 +60,38 @@ defmodule ApplicationRunner.ApplicationServicesTest do {:ok, body, conn} = Plug.Conn.read_body(conn) app = Jason.decode!(body) - assert "1" = app["labels"]["com.openfaas.scale.min"] + assert "2" = app["labels"]["com.openfaas.scale.min"] conn |> send_resp(200, "ok") end ) - ApplicationServices.start_app(@function_name) + ApplicationServices.start_app(@function_name, 2) + end + + test "stop app with min scale" do + bypass = Bypass.open(port: 1234) + Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", app_info_handler()) + Bypass.stub(bypass, "PUT", "/system/functions", &handle_resp/1) + + # Check scale up + Bypass.expect_once( + bypass, + "PUT", + "/system/functions", + fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + app = Jason.decode!(body) + + assert "2" = app["labels"]["com.openfaas.scale.min"] + + conn + |> send_resp(200, "ok") + end + ) + + ApplicationServices.stop_app(@function_name, 2) end @tag telemetry_listen: [:application_runner, :alert, :event] @@ -100,7 +124,7 @@ defmodule ApplicationRunner.ApplicationServicesTest do end @tag telemetry_listen: [:application_runner, :alert, :event] - test "failing app info while stoping app" do + test "failing app info while stopping app" do bypass = Bypass.open(port: 1234) Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", &handle_error_resp/1) From 15439c654cc2c9f93f4ca5087c78a5008fbec82a Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Mon, 3 Feb 2025 14:01:04 +0100 Subject: [PATCH 04/13] fix: Credo errors --- libs/application_runner/lib/crons.ex | 40 ++++++++++++------- .../lib/services/application_services.ex | 2 +- 2 files changed, 27 insertions(+), 15 deletions(-) diff --git a/libs/application_runner/lib/crons.ex b/libs/application_runner/lib/crons.ex index 4d558573..a83065eb 100644 --- a/libs/application_runner/lib/crons.ex +++ b/libs/application_runner/lib/crons.ex @@ -2,7 +2,6 @@ defmodule ApplicationRunner.Crons do @moduledoc """ ApplicationRunner.Crons delegates methods to the corresponding service. """ - import Ecto.Query, only: [from: 2, from: 1] alias ApplicationRunner.Crons.Cron @@ -10,19 +9,32 @@ defmodule ApplicationRunner.Crons do alias ApplicationRunner.{AppSocket, Environment, EventHandler, Repo} alias Crontab.CronExpression.{Composer, Parser} - def run_env_cron( - listener, - props, - event, - env_id, - function_name - ) do - with {:ok, _pid} <- - Environment.ensure_env_started(%Environment.Metadata{ - env_id: env_id, - function_name: function_name - }) do - EventHandler.send_env_event(env_id, listener, props, event) + defmacro __using__(opts) do + adapter_mod = Keyword.fetch!(opts, :adapter) + + quote do + @adapter_mod unquote(adapter_mod) + + def run_env_cron( + listener, + props, + event, + env_id, + function_name + ) do + scale_options = @adapter_mod.get_scale_options(app_name) + + with {:ok, _pid} <- + Environment.ensure_env_started(%Environment.Metadata{ + env_id: env_id, + function_name: function_name, + # TODO: Get real scale_min and scale_max from the environment. + scale_min: scale_options.min, + scale_max: scale_options.max + }) do + EventHandler.send_env_event(env_id, listener, props, event) + end + end end end diff --git a/libs/application_runner/lib/services/application_services.ex b/libs/application_runner/lib/services/application_services.ex index 925622aa..b0aa2097 100644 --- a/libs/application_runner/lib/services/application_services.ex +++ b/libs/application_runner/lib/services/application_services.ex @@ -269,7 +269,6 @@ defmodule ApplicationRunner.ApplicationServices do @doc """ Set the scale values of an OpenFaaS application. """ - @spec set_app_scale_options(String.t(), map()) :: :ok | {:error, struct} | {:ok, any} def set_app_scale_options(function_name, scale_options) do labels = scale_options @@ -282,6 +281,7 @@ defmodule ApplicationRunner.ApplicationServices do end, to_string(value)} end) |> Enum.filter(fn {key, _} -> key != nil end) + |> Map.new() set_app_labels(function_name, labels) end From 30608d3d81d96992dee2daf6adcaed3ed1af76be Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Fri, 7 Feb 2025 20:27:36 +0100 Subject: [PATCH 05/13] feat: better management --- apps/lenra/lib/lenra/apps.ex | 36 +++++++++-- .../lib/lenra/services/openfaas_services.ex | 4 -- apps/lenra/test/lenra/app_adapter_test.exs | 20 +++--- .../lenra/test/lenra/apps/deployment_test.exs | 12 +++- apps/lenra/test/support/faas_stub_helper.ex | 4 ++ apps/lenra_web/lib/lenra_web/app_adapter.ex | 11 ++-- libs/application_runner/lib/adapter.ex | 4 +- .../lib/channels/app_socket.ex | 2 +- libs/application_runner/lib/crons.ex | 3 +- .../lib/environment/dynamic_supervisor.ex | 24 +++++-- .../lib/monitor/environment_monitor.ex | 24 +++++++ .../lib/services/application_services.ex | 6 +- .../environment/dynamic_supervisor_test.exs | 64 +++++++++++++++++++ .../test/environment/view_dyn_sup_test.exs | 14 ++-- .../test/support/app_adapter.ex | 2 +- 15 files changed, 184 insertions(+), 46 deletions(-) diff --git a/apps/lenra/lib/lenra/apps.ex b/apps/lenra/lib/lenra/apps.ex index d75c6376..6505692f 100644 --- a/apps/lenra/lib/lenra/apps.ex +++ b/apps/lenra/lib/lenra/apps.ex @@ -848,9 +848,37 @@ defmodule Lenra.Apps do |> Repo.transaction() end - def update_env_scale_options(env_scale_options, params) do - Ecto.Multi.new() - |> Ecto.Multi.update(:updated_env_scale_options, EnvironmentScaleOptions.changeset(env_scale_options, params)) - |> Repo.transaction() + def set_env_scale_options(env_id, params) do + with {:ok, scale_opt} <- + Repo.insert( + EnvironmentScaleOptions.new(env_id, params), + on_conflict: [set: env_scale_opt_to_list(params)] + ), + %{ + environment: + %Environment{ + application: %App{service_name: service_name}, + deployment: %Deployment{build: %Build{build_number: build_number}} + } = env + } <- Repo.preload(scale_opt, environment: [:application, deployment: [:build]]), + function_name <- OpenfaasServices.get_function_name(service_name, build_number), + effective_scale_opts <- effective_env_scale_options(env), + {:ok} <- Environment.DynamicSupervisor.update_env_scale_options(env_id, effective_scale_opts), + {:ok} <- ApplicationServices.set_app_scale_options(function_name, effective_scale_opts) do + {:ok, scale_opt} + end + end + + defp env_scale_opt_to_list(params) do + [] + |> add_present(params, :min) + |> add_present(params, :max) + end + + defp add_present(list, map, key) do + case Map.has_key?(map, key) do + nil -> list + value -> [{key, value} | list] + end end end diff --git a/apps/lenra/lib/lenra/services/openfaas_services.ex b/apps/lenra/lib/lenra/services/openfaas_services.ex index ad033d5b..e9aea9b3 100644 --- a/apps/lenra/lib/lenra/services/openfaas_services.ex +++ b/apps/lenra/lib/lenra/services/openfaas_services.ex @@ -10,10 +10,6 @@ defmodule Lenra.OpenfaasServices do require Logger - @min_scale_label "com.openfaas.scale.min" - @max_scale_label "com.openfaas.scale.max" - @min_scale_default "1" - defp get_http_context do base_url = Application.fetch_env!(:lenra, :faas_url) auth = Application.fetch_env!(:lenra, :faas_auth) diff --git a/apps/lenra/test/lenra/app_adapter_test.exs b/apps/lenra/test/lenra/app_adapter_test.exs index dee6b857..a7e9a156 100644 --- a/apps/lenra/test/lenra/app_adapter_test.exs +++ b/apps/lenra/test/lenra/app_adapter_test.exs @@ -68,20 +68,20 @@ defmodule LenraWeb.AppAdapterTest do end describe "get_scale_options/1" do - test "returns scale options without subscription nor scale options", %{app: app} do - assert AppAdapter.get_scale_options(app.service_name) == %{min: 0, max: 1} + test "returns scale options without subscription nor scale options", %{env: env} do + assert AppAdapter.get_scale_options(env.id) == %{min: 0, max: 1} end - test "returns scale options without subscription with scale options", %{app: app, env: env} do + test "returns scale options without subscription with scale options", %{env: env} do Apps.create_env_scale_options(env.id, %{ min: 2, max: 5 }) - assert AppAdapter.get_scale_options(app.service_name) == %{min: 0, max: 1} + assert AppAdapter.get_scale_options(env.id) == %{min: 0, max: 1} end - test "returns scale options with subscription without scale options", %{app: app} do + test "returns scale options with subscription without scale options", %{app: app, env: env} do subscription = Subscription.new(%{ application_id: app.id, @@ -92,7 +92,7 @@ defmodule LenraWeb.AppAdapterTest do Repo.insert(subscription) - assert AppAdapter.get_scale_options(app.service_name) == %{min: 0, max: 5} + assert AppAdapter.get_scale_options(env.id) == %{min: 0, max: 5} end test "returns scale options with subscription with min scale options", %{app: app, env: env} do @@ -111,7 +111,7 @@ defmodule LenraWeb.AppAdapterTest do Repo.insert(subscription) - assert AppAdapter.get_scale_options(app.service_name) == %{min: 2, max: 5} + assert AppAdapter.get_scale_options(env.id) == %{min: 2, max: 5} end test "returns scale options with subscription with max scale options", %{app: app, env: env} do @@ -130,7 +130,7 @@ defmodule LenraWeb.AppAdapterTest do Repo.insert(subscription) - assert AppAdapter.get_scale_options(app.service_name) == %{min: 0, max: 5} + assert AppAdapter.get_scale_options(env.id) == %{min: 0, max: 5} end test "returns scale options with subscription with min and max scale options", %{app: app, env: env} do @@ -150,7 +150,7 @@ defmodule LenraWeb.AppAdapterTest do Repo.insert(subscription) - assert AppAdapter.get_scale_options(app.service_name) == %{min: 2, max: 5} + assert AppAdapter.get_scale_options(env.id) == %{min: 2, max: 5} end test "returns scale options with subscription with reversed min and max scale options", %{app: app, env: env} do @@ -170,7 +170,7 @@ defmodule LenraWeb.AppAdapterTest do Repo.insert(subscription) - assert AppAdapter.get_scale_options(app.service_name) == %{min: 2, max: 2} + assert AppAdapter.get_scale_options(env.id) == %{min: 2, max: 2} end end end diff --git a/apps/lenra/test/lenra/apps/deployment_test.exs b/apps/lenra/test/lenra/apps/deployment_test.exs index 0aa96f0a..b27503ec 100644 --- a/apps/lenra/test/lenra/apps/deployment_test.exs +++ b/apps/lenra/test/lenra/apps/deployment_test.exs @@ -44,10 +44,20 @@ defmodule Lenra.Apps.DeploymentTest do env = Enum.at(Repo.all(Environment), 0) build = Enum.at(Repo.all(Build), 0) + function_name = FaasStub.get_function_name(app.service_name, build.build_number) + + FaasStub.expect_get_function_once( + bypass, + %{"ok" => "200"}, + function_name + ) + # Not found since spawn in another process + FaasStub.expect_update_function_once(bypass, %{"ok" => "200"}) + FaasStub.expect_get_function_once( bypass, %{"ok" => "200"}, - FaasStub.get_function_name(app.service_name, build.build_number) + function_name ) Apps.create_deployment(env.id, build.id, app.creator_id) diff --git a/apps/lenra/test/support/faas_stub_helper.ex b/apps/lenra/test/support/faas_stub_helper.ex index 290bf58b..0d28b776 100644 --- a/apps/lenra/test/support/faas_stub_helper.ex +++ b/apps/lenra/test/support/faas_stub_helper.ex @@ -34,6 +34,10 @@ defmodule Lenra.FaasStub do expect_once("/system/function/#{service_name}", "GET", bypass, result) end + def expect_update_function_once(bypass, result) do + expect_once("/system/functions", "PUT", bypass, result) + end + def expect_deploy_app_once(bypass, result) do expect_once("/system/functions", "POST", bypass, result) end diff --git a/apps/lenra_web/lib/lenra_web/app_adapter.ex b/apps/lenra_web/lib/lenra_web/app_adapter.ex index 76a66a7b..934fa071 100644 --- a/apps/lenra_web/lib/lenra_web/app_adapter.ex +++ b/apps/lenra_web/lib/lenra_web/app_adapter.ex @@ -67,13 +67,12 @@ defmodule LenraWeb.AppAdapter do end @impl ApplicationRunner.Adapter - def get_scale_options(app_name) do - application = - App - |> Repo.get_by(service_name: app_name) - |> Repo.preload(main_env: [:environment]) + def get_scale_options(env_id) do + environment = + Environment + |> Repo.get(env_id) - Apps.effective_env_scale_options(application.main_env.environment) + Apps.effective_env_scale_options(environment) end @impl ApplicationRunner.Adapter diff --git a/libs/application_runner/lib/adapter.ex b/libs/application_runner/lib/adapter.ex index 0678cee4..47b65465 100644 --- a/libs/application_runner/lib/adapter.ex +++ b/libs/application_runner/lib/adapter.ex @@ -19,9 +19,9 @@ defmodule ApplicationRunner.Adapter do @callback get_env_id(String.t()) :: number() @doc """ - Override this function to return the scale options from the app_name to the server/devtools needs + Override this function to return the scale options from the env id to the server/devtools needs """ - @callback get_scale_options(String.t()) :: %{min: number(), max: number()} + @callback get_scale_options(integer()) :: %{min: number(), max: number()} @callback resource_from_params(map()) :: {:ok, number, any(), map()} | {:error, any()} end diff --git a/libs/application_runner/lib/channels/app_socket.ex b/libs/application_runner/lib/channels/app_socket.ex index 784d1934..e93984f3 100644 --- a/libs/application_runner/lib/channels/app_socket.ex +++ b/libs/application_runner/lib/channels/app_socket.ex @@ -115,7 +115,7 @@ defmodule ApplicationRunner.AppSocket do with function_name when is_bitstring(function_name) <- adapter_mod.get_function_name(app_name), env_id <- adapter_mod.get_env_id(app_name), - scale_options <- adapter_mod.get_scale_options(app_name) do + scale_options <- adapter_mod.get_scale_options(env_id) do # prepare the assigns to the session/environment session_metadata = %Session.Metadata{ env_id: env_id, diff --git a/libs/application_runner/lib/crons.ex b/libs/application_runner/lib/crons.ex index a83065eb..82b6d446 100644 --- a/libs/application_runner/lib/crons.ex +++ b/libs/application_runner/lib/crons.ex @@ -22,13 +22,12 @@ defmodule ApplicationRunner.Crons do env_id, function_name ) do - scale_options = @adapter_mod.get_scale_options(app_name) + scale_options = @adapter_mod.get_scale_options(env_id) with {:ok, _pid} <- Environment.ensure_env_started(%Environment.Metadata{ env_id: env_id, function_name: function_name, - # TODO: Get real scale_min and scale_max from the environment. scale_min: scale_options.min, scale_max: scale_options.max }) do diff --git a/libs/application_runner/lib/environment/dynamic_supervisor.ex b/libs/application_runner/lib/environment/dynamic_supervisor.ex index 47bea4e7..f5ac7298 100644 --- a/libs/application_runner/lib/environment/dynamic_supervisor.ex +++ b/libs/application_runner/lib/environment/dynamic_supervisor.ex @@ -85,20 +85,25 @@ defmodule ApplicationRunner.Environment.DynamicSupervisor do @spec stop_env(number()) :: :ok | {:error, LC.BusinessError.t()} def stop_env(env_id) do Logger.debug("Stopping environment for env_id: #{env_id}") - name = Environment.Supervisor.get_name(env_id) - case Swarm.whereis_name(name) do + case get_env_pid(env_id) do :undefined -> - Logger.error("Failed to find supervision tree for name: #{inspect(name)}") + Logger.error("Failed to find supervision tree for env_id: #{inspect(env_id)}") BusinessError.env_not_started_tuple() pid -> - Logger.info("Stopping environment supervision tree for name: #{inspect(name)}") + Logger.info("Stopping environment supervision tree for env_id: #{inspect(env_id)}") DynamicSupervisor.terminate_child(__MODULE__, pid) # Supervisor.stop(pid) end end + defp get_env_pid(env_id) do + env_id + |> Environment.Supervisor.get_name() + |> Swarm.whereis_name() + end + def session_stopped(env_id) do session_pid = Swarm.whereis_name(Session.DynamicSupervisor.get_name(env_id)) @@ -127,4 +132,15 @@ defmodule ApplicationRunner.Environment.DynamicSupervisor do stop_env(env_id) end end + + def update_env_scale_options(env_id, scale_opts) do + case get_env_pid(env_id) do + :undefined -> + :ok + + pid -> + Logger.info("Updating environment scale options in EnvironmentMonitor for env_id: #{inspect(env_id)}") + EnvironmentMonitor.update_scale_options(pid, scale_opts) + end + end end diff --git a/libs/application_runner/lib/monitor/environment_monitor.ex b/libs/application_runner/lib/monitor/environment_monitor.ex index e64342dd..eb7ce7f0 100644 --- a/libs/application_runner/lib/monitor/environment_monitor.ex +++ b/libs/application_runner/lib/monitor/environment_monitor.ex @@ -15,6 +15,15 @@ defmodule ApplicationRunner.Monitor.EnvironmentMonitor do Logger.error("#{__MODULE__} fail in monitor with metadata #{inspect(metadata)} and error: #{inspect(e)}") end + def update_scale_options(pid, scale_opts) do + GenServer.call(__MODULE__, {:update_scale_opts, pid, scale_opts}) + rescue + e -> + Logger.error( + "#{__MODULE__} fail in updating scale options with scale_opts #{inspect(scale_opts)} and error: #{inspect(e)}" + ) + end + def start_link(_opts) do Logger.debug("Start #{__MODULE__}") GenServer.start_link(__MODULE__, [], name: __MODULE__) @@ -32,6 +41,21 @@ defmodule ApplicationRunner.Monitor.EnvironmentMonitor do {:reply, :ok, Map.put(state, pid, {metadata})} end + def handle_call({:update_scale_opts, pid, scale_opts}, _from, state) do + {metadata} = Map.get(state, pid) + + Logger.debug( + "#{__MODULE__} update scale options #{inspect(pid)} with metadata #{inspect(metadata)} and scale_opts #{inspect(scale_opts)}" + ) + + metadata = + metadata + |> Map.put(:scale_min, scale_opts.min) + |> Map.put(:scale_max, scale_opts.max) + + {:reply, :ok, Map.put(state, pid, {metadata})} + end + def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do {{metadata}, new_state} = Map.pop(state, pid) base_url = Application.fetch_env!(:application_runner, :faas_url) diff --git a/libs/application_runner/lib/services/application_services.ex b/libs/application_runner/lib/services/application_services.ex index b0aa2097..8a770325 100644 --- a/libs/application_runner/lib/services/application_services.ex +++ b/libs/application_runner/lib/services/application_services.ex @@ -274,9 +274,9 @@ defmodule ApplicationRunner.ApplicationServices do scale_options |> Enum.map(fn {key, value} -> {case key do - :scale_min -> @min_scale_label - :scale_max -> @max_scale_label - :scale_factor -> @scale_factor_label + :min -> @min_scale_label + :max -> @max_scale_label + :factor -> @scale_factor_label _ -> nil end, to_string(value)} end) diff --git a/libs/application_runner/test/environment/dynamic_supervisor_test.exs b/libs/application_runner/test/environment/dynamic_supervisor_test.exs index c62db171..cafa6357 100644 --- a/libs/application_runner/test/environment/dynamic_supervisor_test.exs +++ b/libs/application_runner/test/environment/dynamic_supervisor_test.exs @@ -150,4 +150,68 @@ defmodule ApplicationRunner.Environment.DynamixSupervisorTest do assert_receive(:lookup, 500) end + + test "update env scale options while env open" do + {:ok, %{id: env_id}} = Repo.insert(Contract.Environment.new()) + + bypass = Bypass.open(port: 1234) + Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", &handle_app_info_resp/1) + Bypass.stub(bypass, "PUT", "/system/functions", &handle_resp/1) + Bypass.stub(bypass, "POST", "/function/#{@function_name}", &handle_resp/1) + + env_metadata = %Environment.Metadata{ + env_id: env_id, + function_name: @function_name, + scale_min: 0, + scale_max: 1 + } + + on_exit(fn -> + Swarm.unregister_name(Environment.Supervisor.get_name(env_id)) + end) + + # Check scale up + Bypass.expect_once( + bypass, + "PUT", + "/system/functions", + fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + app = Jason.decode!(body) + + assert "1" = app["labels"]["com.openfaas.scale.min"] + + conn + |> send_resp(200, "ok") + end + ) + + {:ok, _pid} = DynamicSupervisor.ensure_env_started(env_metadata) + + :ok = DynamicSupervisor.update_env_scale_options(env_id, %{min: 1, max: 5}) + + my_pid = self() + + # Check scale down + Bypass.expect_once( + bypass, + "PUT", + "/system/functions", + fn conn -> + send(my_pid, :lookup) + + {:ok, body, conn} = Plug.Conn.read_body(conn) + app = Jason.decode!(body) + + assert "1" = app["labels"]["com.openfaas.scale.min"] + + conn + |> send_resp(200, "ok") + end + ) + + DynamicSupervisor.stop_env(env_id) + + assert_receive(:lookup, 500) + end end diff --git a/libs/application_runner/test/environment/view_dyn_sup_test.exs b/libs/application_runner/test/environment/view_dyn_sup_test.exs index 7d829baa..7de56b58 100644 --- a/libs/application_runner/test/environment/view_dyn_sup_test.exs +++ b/libs/application_runner/test/environment/view_dyn_sup_test.exs @@ -24,8 +24,6 @@ defmodule ApplicationRunner.Environment.ViewDynSupTest do } } @view %{"_type" => "text", "value" => "test"} - - @function_name Ecto.UUID.generate() @session_id 1337 setup do @@ -36,16 +34,16 @@ defmodule ApplicationRunner.Environment.ViewDynSupTest do env_metadata = %Environment.Metadata{ env_id: env_id, - function_name: "env_#{env_id}" + function_name: "env_#{env_id}", + scale_min: 0, + scale_max: 1 } {:ok, _pid} = start_supervised({Environment.Supervisor, env_metadata}) - # TODO: This is causing the tests to fail because the app - # (or something in the test environment) is already closed by the time this line runs - # on_exit(fn -> - # Swarm.unregister_name(Environment.Supervisor.get_name(env_id)) - # end) + on_exit(fn -> + Swarm.unregister_name(Environment.Supervisor.get_name(env_id)) + end) {:ok, env_id: env_id} end diff --git a/libs/application_runner/test/support/app_adapter.ex b/libs/application_runner/test/support/app_adapter.ex index eee7b6a1..cb50c6aa 100644 --- a/libs/application_runner/test/support/app_adapter.ex +++ b/libs/application_runner/test/support/app_adapter.ex @@ -29,7 +29,7 @@ defmodule ApplicationRunner.FakeAppAdapter do end @impl ApplicationRunner.Adapter - def get_scale_options(_app_name) do + def get_scale_options(_env_id) do %{min: 0, max: 1} end From b931dbe1347d479a167f9c09524962628fb8557b Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Sun, 9 Feb 2025 16:28:32 +0100 Subject: [PATCH 06/13] test: Fix unit tests with async actions --- apps/lenra/lib/lenra/apps.ex | 27 ++- .../lenra/test/lenra/apps/deployment_test.exs | 21 +- .../lib/services/application_services.ex | 5 +- .../environment/dynamic_supervisor_test.exs | 203 ++++++------------ .../test/environment/query_server_test.exs | 2 +- .../services/application_services_test.exs | 9 + 6 files changed, 100 insertions(+), 167 deletions(-) diff --git a/apps/lenra/lib/lenra/apps.ex b/apps/lenra/lib/lenra/apps.ex index 6505692f..1305d8e3 100644 --- a/apps/lenra/lib/lenra/apps.ex +++ b/apps/lenra/lib/lenra/apps.ex @@ -388,24 +388,23 @@ defmodule Lenra.Apps do when retry <= 120 do case OpenfaasServices.is_deploy(service_name, build_number) do true -> - transaction = - Ecto.Multi.new() - |> Ecto.Multi.update( - :updated_deployment, - Ecto.Changeset.change(deployment, status: :success) - ) - |> Ecto.Multi.run(:updated_env, fn _repo, %{updated_deployment: updated_deployment} -> - env - |> Ecto.Changeset.change(deployment_id: updated_deployment.id) - |> Repo.update() - end) - |> Repo.transaction() + scale_opts = effective_env_scale_options(env) service_name |> OpenfaasServices.get_function_name(build_number) - |> ApplicationServices.set_app_scale_options(effective_env_scale_options(env)) + |> ApplicationServices.set_app_scale_options(scale_opts) - transaction + Ecto.Multi.new() + |> Ecto.Multi.update( + :updated_deployment, + Ecto.Changeset.change(deployment, status: :success) + ) + |> Ecto.Multi.run(:updated_env, fn _repo, %{updated_deployment: updated_deployment} -> + env + |> Ecto.Changeset.change(deployment_id: updated_deployment.id) + |> Repo.update() + end) + |> Repo.transaction() # Function not found in openfaas, 2 retry (10s), # To let openfaas deploy in case of overload, after 2 retry -> failure diff --git a/apps/lenra/test/lenra/apps/deployment_test.exs b/apps/lenra/test/lenra/apps/deployment_test.exs index b27503ec..e82d795d 100644 --- a/apps/lenra/test/lenra/apps/deployment_test.exs +++ b/apps/lenra/test/lenra/apps/deployment_test.exs @@ -2,7 +2,7 @@ defmodule Lenra.Apps.DeploymentTest do @moduledoc """ Test the deployment services """ - use Lenra.RepoCase, async: true + use Lenra.RepoCase, async: false alias Lenra.{ FaasStub, @@ -46,19 +46,12 @@ defmodule Lenra.Apps.DeploymentTest do function_name = FaasStub.get_function_name(app.service_name, build.build_number) - FaasStub.expect_get_function_once( - bypass, - %{"ok" => "200"}, - function_name - ) - # Not found since spawn in another process - FaasStub.expect_update_function_once(bypass, %{"ok" => "200"}) + Bypass.stub(bypass, "GET", "/system/function/#{function_name}", fn conn -> + conn + |> Plug.Conn.resp(200, Jason.encode!(%{"availableReplicas" => 1})) + end) - FaasStub.expect_get_function_once( - bypass, - %{"ok" => "200"}, - function_name - ) + FaasStub.expect_update_function_once(bypass, %{"ok" => "200"}) Apps.create_deployment(env.id, build.id, app.creator_id) @@ -66,6 +59,8 @@ defmodule Lenra.Apps.DeploymentTest do assert nil != Enum.at(Repo.all(Deployment), 0) assert nil != Repo.get_by(Deployment, environment_id: env.id, build_id: build.id) + # Wait for async deployment check (spawned process) + Process.sleep(5) end test "deployment but wrong environment", %{app: app} do diff --git a/libs/application_runner/lib/services/application_services.ex b/libs/application_runner/lib/services/application_services.ex index 8a770325..69f30242 100644 --- a/libs/application_runner/lib/services/application_services.ex +++ b/libs/application_runner/lib/services/application_services.ex @@ -337,7 +337,8 @@ defmodule ApplicationRunner.ApplicationServices do |> Finch.request(AppHttp, receive_timeout: 5000) |> response(:update_app) - _ -> + e -> + # {:error, TechnicalError.app_not_found()} TechnicalError.openfaas_not_reachable_tuple() end end @@ -370,7 +371,7 @@ defmodule ApplicationRunner.ApplicationServices do {:ok, Jason.decode!(body)} end - defp response({:error, %Mint.TransportError{reason: reason}}, _listener) do + defp response({:error, %Mint.TransportError{reason: reason}}, listener) do Telemetry.event( :alert, %{}, diff --git a/libs/application_runner/test/environment/dynamic_supervisor_test.exs b/libs/application_runner/test/environment/dynamic_supervisor_test.exs index cafa6357..e3995d74 100644 --- a/libs/application_runner/test/environment/dynamic_supervisor_test.exs +++ b/libs/application_runner/test/environment/dynamic_supervisor_test.exs @@ -7,6 +7,20 @@ defmodule ApplicationRunner.Environment.DynamixSupervisorTest do @function_name Ecto.UUID.generate() + setup do + {:ok, %{id: env_id}} = Repo.insert(Contract.Environment.new()) + + bypass = Bypass.open(port: 1234) + Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", &handle_app_info_resp/1) + Bypass.stub(bypass, "POST", "/function/#{@function_name}", &handle_resp/1) + + on_exit(fn -> + Swarm.unregister_name(Environment.Supervisor.get_name(env_id)) + end) + + {:ok, env_id: env_id, bypass: bypass} + end + defp handle_resp(conn) do {:ok, body, conn} = Plug.Conn.read_body(conn) @@ -27,76 +41,65 @@ defmodule ApplicationRunner.Environment.DynamixSupervisorTest do Plug.Conn.resp(conn, 200, Jason.encode!(%{name: @function_name})) end - test "should scale to one on environment start and to zero on environment exit" do - {:ok, %{id: env_id}} = Repo.insert(Contract.Environment.new()) + defp handle_min_scale_to_0(conn) do + {:ok, body, conn} = Plug.Conn.read_body(conn) + app = Jason.decode!(body) - bypass = Bypass.open(port: 1234) - Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", &handle_app_info_resp/1) - Bypass.stub(bypass, "PUT", "/system/functions", &handle_resp/1) - Bypass.stub(bypass, "POST", "/function/#{@function_name}", &handle_resp/1) + assert "0" = app["labels"]["com.openfaas.scale.min"] - env_metadata = %Environment.Metadata{ - env_id: env_id, - function_name: @function_name, - scale_min: 0, - scale_max: 1 - } + conn + |> send_resp(200, "ok") + end - on_exit(fn -> - Swarm.unregister_name(Environment.Supervisor.get_name(env_id)) - end) + defp handle_min_scale_to_1(conn) do + {:ok, body, conn} = Plug.Conn.read_body(conn) + app = Jason.decode!(body) - # Check scale up + assert "1" = app["labels"]["com.openfaas.scale.min"] + + conn + |> send_resp(200, "ok") + end + + defp check_scale_up(bypass) do Bypass.expect_once( bypass, "PUT", "/system/functions", - fn conn -> - {:ok, body, conn} = Plug.Conn.read_body(conn) - app = Jason.decode!(body) - - assert "1" = app["labels"]["com.openfaas.scale.min"] - - conn - |> send_resp(200, "ok") - end + &handle_min_scale_to_1/1 ) + end - {:ok, _pid} = DynamicSupervisor.ensure_env_started(env_metadata) - - my_pid = self() - - # Check scale down + defp check_scale_down(bypass) do Bypass.expect_once( bypass, "PUT", "/system/functions", - fn conn -> - send(my_pid, :lookup) - - {:ok, body, conn} = Plug.Conn.read_body(conn) - app = Jason.decode!(body) + &handle_min_scale_to_0/1 + ) + end - assert "0" = app["labels"]["com.openfaas.scale.min"] + test "should scale to one on environment start and to zero on environment exit", %{env_id: env_id, bypass: bypass} do + env_metadata = %Environment.Metadata{ + env_id: env_id, + function_name: @function_name, + scale_min: 0, + scale_max: 1 + } - conn - |> send_resp(200, "ok") - end - ) + check_scale_up(bypass) - DynamicSupervisor.stop_env(env_id) + {:ok, _pid} = DynamicSupervisor.ensure_env_started(env_metadata) - assert_receive(:lookup, 500) - end + check_scale_down(bypass) - test "should scale to 1 on environment start and stay at 1 on environment exit" do - {:ok, %{id: env_id}} = Repo.insert(Contract.Environment.new()) + :ok = DynamicSupervisor.stop_env(env_id) - bypass = Bypass.open(port: 1234) - Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", &handle_app_info_resp/1) - Bypass.stub(bypass, "PUT", "/system/functions", &handle_resp/1) - Bypass.stub(bypass, "POST", "/function/#{@function_name}", &handle_resp/1) + # Await for async environment stop + Process.sleep(5) + end + test "should scale to 1 on environment start and stay at 1 on environment exit", %{env_id: env_id, bypass: bypass} do env_metadata = %Environment.Metadata{ env_id: env_id, function_name: @function_name, @@ -104,61 +107,19 @@ defmodule ApplicationRunner.Environment.DynamixSupervisorTest do scale_max: 5 } - on_exit(fn -> - Swarm.unregister_name(Environment.Supervisor.get_name(env_id)) - end) - - # Check scale up - Bypass.expect_once( - bypass, - "PUT", - "/system/functions", - fn conn -> - {:ok, body, conn} = Plug.Conn.read_body(conn) - app = Jason.decode!(body) - - assert "1" = app["labels"]["com.openfaas.scale.min"] - - conn - |> send_resp(200, "ok") - end - ) + check_scale_up(bypass) {:ok, _pid} = DynamicSupervisor.ensure_env_started(env_metadata) - my_pid = self() - - # Check scale down - Bypass.expect_once( - bypass, - "PUT", - "/system/functions", - fn conn -> - send(my_pid, :lookup) - - {:ok, body, conn} = Plug.Conn.read_body(conn) - app = Jason.decode!(body) - - assert "1" = app["labels"]["com.openfaas.scale.min"] - - conn - |> send_resp(200, "ok") - end - ) + check_scale_up(bypass) - DynamicSupervisor.stop_env(env_id) + :ok = DynamicSupervisor.stop_env(env_id) - assert_receive(:lookup, 500) + # Await for async environment stop + Process.sleep(5) end - test "update env scale options while env open" do - {:ok, %{id: env_id}} = Repo.insert(Contract.Environment.new()) - - bypass = Bypass.open(port: 1234) - Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", &handle_app_info_resp/1) - Bypass.stub(bypass, "PUT", "/system/functions", &handle_resp/1) - Bypass.stub(bypass, "POST", "/function/#{@function_name}", &handle_resp/1) - + test "update env scale options while env open", %{env_id: env_id, bypass: bypass} do env_metadata = %Environment.Metadata{ env_id: env_id, function_name: @function_name, @@ -166,52 +127,20 @@ defmodule ApplicationRunner.Environment.DynamixSupervisorTest do scale_max: 1 } - on_exit(fn -> - Swarm.unregister_name(Environment.Supervisor.get_name(env_id)) - end) - - # Check scale up - Bypass.expect_once( - bypass, - "PUT", - "/system/functions", - fn conn -> - {:ok, body, conn} = Plug.Conn.read_body(conn) - app = Jason.decode!(body) - - assert "1" = app["labels"]["com.openfaas.scale.min"] - - conn - |> send_resp(200, "ok") - end - ) + check_scale_up(bypass) {:ok, _pid} = DynamicSupervisor.ensure_env_started(env_metadata) - :ok = DynamicSupervisor.update_env_scale_options(env_id, %{min: 1, max: 5}) - - my_pid = self() - - # Check scale down - Bypass.expect_once( - bypass, - "PUT", - "/system/functions", - fn conn -> - send(my_pid, :lookup) - - {:ok, body, conn} = Plug.Conn.read_body(conn) - app = Jason.decode!(body) + # Check scale update + check_scale_up(bypass) - assert "1" = app["labels"]["com.openfaas.scale.min"] + :ok = DynamicSupervisor.update_env_scale_options(env_id, %{min: 1, max: 5}) - conn - |> send_resp(200, "ok") - end - ) + check_scale_up(bypass) - DynamicSupervisor.stop_env(env_id) + :ok = DynamicSupervisor.stop_env(env_id) - assert_receive(:lookup, 500) + # Await for async environment stop + Process.sleep(5) end end diff --git a/libs/application_runner/test/environment/query_server_test.exs b/libs/application_runner/test/environment/query_server_test.exs index 1c2575f3..a7071ca3 100644 --- a/libs/application_runner/test/environment/query_server_test.exs +++ b/libs/application_runner/test/environment/query_server_test.exs @@ -1,5 +1,5 @@ defmodule ApplicationRunner.Environment.QueryServerTest do - use ExUnit.Case + use ExUnit.Case, async: false alias ApplicationRunner.Environment.{ MongoInstance, diff --git a/libs/application_runner/test/services/application_services_test.exs b/libs/application_runner/test/services/application_services_test.exs index f00ee788..3c439208 100644 --- a/libs/application_runner/test/services/application_services_test.exs +++ b/libs/application_runner/test/services/application_services_test.exs @@ -22,6 +22,15 @@ defmodule ApplicationRunner.ApplicationServicesTest do end end + test "get app status" do + bypass = Bypass.open(port: 1234) + Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", app_info_handler()) + + {:ok, app} = ApplicationServices.get_app_status(@function_name) + + assert %{"name" => @function_name} == app + end + test "start app" do bypass = Bypass.open(port: 1234) Bypass.stub(bypass, "GET", "/system/function/#{@function_name}", app_info_handler()) From 329908519e9499d513b9bce1cbbb29d8ff07c8cd Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Sun, 9 Feb 2025 16:59:28 +0100 Subject: [PATCH 07/13] style: Fix credo --- apps/lenra/lib/lenra/apps.ex | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/apps/lenra/lib/lenra/apps.ex b/apps/lenra/lib/lenra/apps.ex index 1305d8e3..972eee0f 100644 --- a/apps/lenra/lib/lenra/apps.ex +++ b/apps/lenra/lib/lenra/apps.ex @@ -862,8 +862,8 @@ defmodule Lenra.Apps do } <- Repo.preload(scale_opt, environment: [:application, deployment: [:build]]), function_name <- OpenfaasServices.get_function_name(service_name, build_number), effective_scale_opts <- effective_env_scale_options(env), - {:ok} <- Environment.DynamicSupervisor.update_env_scale_options(env_id, effective_scale_opts), - {:ok} <- ApplicationServices.set_app_scale_options(function_name, effective_scale_opts) do + :ok <- ApplicationRunner.Environment.DynamicSupervisor.update_env_scale_options(env_id, effective_scale_opts), + {:ok, _} <- ApplicationServices.set_app_scale_options(function_name, effective_scale_opts) do {:ok, scale_opt} end end @@ -874,10 +874,12 @@ defmodule Lenra.Apps do |> add_present(params, :max) end + @spec add_present(list :: list, map :: map, key :: atom) :: list defp add_present(list, map, key) do - case Map.has_key?(map, key) do - nil -> list - value -> [{key, value} | list] + if Map.has_key?(map, key) do + [{key, Map.fetch(map, key)} | list] + else + list end end end From 4f1fa6cfbe5e911c3334e552679c06881d446e27 Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Sun, 9 Feb 2025 17:04:44 +0100 Subject: [PATCH 08/13] style: Fix --- apps/lenra/lib/lenra/apps.ex | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/apps/lenra/lib/lenra/apps.ex b/apps/lenra/lib/lenra/apps.ex index 972eee0f..eb97251a 100644 --- a/apps/lenra/lib/lenra/apps.ex +++ b/apps/lenra/lib/lenra/apps.ex @@ -20,6 +20,7 @@ defmodule Lenra.Apps do import Ecto.Query alias ApplicationRunner.ApplicationServices + alias ApplicationRunner.Environment.DynamicSupervisor alias ApplicationRunner.MongoStorage.MongoUserLink alias Lenra.Repo alias Lenra.Subscriptions @@ -301,14 +302,6 @@ defmodule Lenra.Apps do end) end - defp update_build_after_pipeline(multi) do - multi - |> Ecto.Multi.update(:update_build_after_pipeline, fn - %{inserted_build: %Build{} = build, gitlab_pipeline: pipeline} -> - Build.changeset(build, %{"pipeline_id" => pipeline["id"]}) - end) - end - def update_build(build, params) do Ecto.Multi.new() |> Ecto.Multi.update(:updated_build, Build.update(build, params)) @@ -862,7 +855,7 @@ defmodule Lenra.Apps do } <- Repo.preload(scale_opt, environment: [:application, deployment: [:build]]), function_name <- OpenfaasServices.get_function_name(service_name, build_number), effective_scale_opts <- effective_env_scale_options(env), - :ok <- ApplicationRunner.Environment.DynamicSupervisor.update_env_scale_options(env_id, effective_scale_opts), + :ok <- DynamicSupervisor.update_env_scale_options(env_id, effective_scale_opts), {:ok, _} <- ApplicationServices.set_app_scale_options(function_name, effective_scale_opts) do {:ok, scale_opt} end From 4e976afea9779a74e380f80cebdae02fb3188c29 Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Sun, 9 Feb 2025 17:08:08 +0100 Subject: [PATCH 09/13] style: Better alias --- apps/lenra/lib/lenra/apps.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/lenra/lib/lenra/apps.ex b/apps/lenra/lib/lenra/apps.ex index eb97251a..cf21bc7f 100644 --- a/apps/lenra/lib/lenra/apps.ex +++ b/apps/lenra/lib/lenra/apps.ex @@ -20,7 +20,7 @@ defmodule Lenra.Apps do import Ecto.Query alias ApplicationRunner.ApplicationServices - alias ApplicationRunner.Environment.DynamicSupervisor + alias ApplicationRunner.Environment.DynamicSupervisor, as: EnvDynamicSupervisor alias ApplicationRunner.MongoStorage.MongoUserLink alias Lenra.Repo alias Lenra.Subscriptions @@ -855,7 +855,7 @@ defmodule Lenra.Apps do } <- Repo.preload(scale_opt, environment: [:application, deployment: [:build]]), function_name <- OpenfaasServices.get_function_name(service_name, build_number), effective_scale_opts <- effective_env_scale_options(env), - :ok <- DynamicSupervisor.update_env_scale_options(env_id, effective_scale_opts), + :ok <- EnvDynamicSupervisor.update_env_scale_options(env_id, effective_scale_opts), {:ok, _} <- ApplicationServices.set_app_scale_options(function_name, effective_scale_opts) do {:ok, scale_opt} end From b8160e2dccc7399e1a672a2dc1435c924a7a89f6 Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Sun, 9 Feb 2025 17:20:05 +0100 Subject: [PATCH 10/13] style: Not working... --- libs/application_runner/lib/environment.ex | 2 ++ libs/application_runner/lib/session.ex | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/libs/application_runner/lib/environment.ex b/libs/application_runner/lib/environment.ex index d4b83bd4..afa49567 100644 --- a/libs/application_runner/lib/environment.ex +++ b/libs/application_runner/lib/environment.ex @@ -8,4 +8,6 @@ defmodule ApplicationRunner.Environment do defdelegate get_manifest(env_id), to: Environment.ManifestHandler defdelegate ensure_env_started(env_metadata), to: Environment.DynamicSupervisor + + defdelegate update_env_scale_options(env_id, scale_opts), to: Environment.DynamicSupervisor end diff --git a/libs/application_runner/lib/session.ex b/libs/application_runner/lib/session.ex index c7cc4f51..e2adb042 100644 --- a/libs/application_runner/lib/session.ex +++ b/libs/application_runner/lib/session.ex @@ -1,6 +1,6 @@ defmodule ApplicationRunner.Session do @moduledoc """ - ApplicationRunner.Session manage all lenra session fonctionnality + ApplicationRunner.Session manage all lenra session functionality """ alias ApplicationRunner.Session From 33cfa4ef850be5b897e03681ded676c6c5e23414 Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Sun, 9 Feb 2025 17:22:53 +0100 Subject: [PATCH 11/13] style: No as in alias --- apps/lenra/lib/lenra/apps.ex | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/apps/lenra/lib/lenra/apps.ex b/apps/lenra/lib/lenra/apps.ex index cf21bc7f..eb97251a 100644 --- a/apps/lenra/lib/lenra/apps.ex +++ b/apps/lenra/lib/lenra/apps.ex @@ -20,7 +20,7 @@ defmodule Lenra.Apps do import Ecto.Query alias ApplicationRunner.ApplicationServices - alias ApplicationRunner.Environment.DynamicSupervisor, as: EnvDynamicSupervisor + alias ApplicationRunner.Environment.DynamicSupervisor alias ApplicationRunner.MongoStorage.MongoUserLink alias Lenra.Repo alias Lenra.Subscriptions @@ -855,7 +855,7 @@ defmodule Lenra.Apps do } <- Repo.preload(scale_opt, environment: [:application, deployment: [:build]]), function_name <- OpenfaasServices.get_function_name(service_name, build_number), effective_scale_opts <- effective_env_scale_options(env), - :ok <- EnvDynamicSupervisor.update_env_scale_options(env_id, effective_scale_opts), + :ok <- DynamicSupervisor.update_env_scale_options(env_id, effective_scale_opts), {:ok, _} <- ApplicationServices.set_app_scale_options(function_name, effective_scale_opts) do {:ok, scale_opt} end From 08c174fdd8ac246cc98999cf19c75fea84a8abdc Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Sun, 9 Feb 2025 17:36:17 +0100 Subject: [PATCH 12/13] style: Fix --- libs/application_runner/lib/crons.ex | 2 +- .../application_runner/lib/environment/dynamic_supervisor.ex | 1 + libs/application_runner/lib/monitor/environment_monitor.ex | 2 -- libs/application_runner/lib/services/application_services.ex | 5 ++--- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/libs/application_runner/lib/crons.ex b/libs/application_runner/lib/crons.ex index 82b6d446..deff0757 100644 --- a/libs/application_runner/lib/crons.ex +++ b/libs/application_runner/lib/crons.ex @@ -6,7 +6,7 @@ defmodule ApplicationRunner.Crons do alias ApplicationRunner.Crons.Cron alias ApplicationRunner.Errors.{BusinessError, TechnicalError} - alias ApplicationRunner.{AppSocket, Environment, EventHandler, Repo} + alias ApplicationRunner.{Environment, EventHandler, Repo} alias Crontab.CronExpression.{Composer, Parser} defmacro __using__(opts) do diff --git a/libs/application_runner/lib/environment/dynamic_supervisor.ex b/libs/application_runner/lib/environment/dynamic_supervisor.ex index f5ac7298..d9f2f1fb 100644 --- a/libs/application_runner/lib/environment/dynamic_supervisor.ex +++ b/libs/application_runner/lib/environment/dynamic_supervisor.ex @@ -133,6 +133,7 @@ defmodule ApplicationRunner.Environment.DynamicSupervisor do end end + @spec update_env_scale_options(integer(), map()) :: :ok def update_env_scale_options(env_id, scale_opts) do case get_env_pid(env_id) do :undefined -> diff --git a/libs/application_runner/lib/monitor/environment_monitor.ex b/libs/application_runner/lib/monitor/environment_monitor.ex index eb7ce7f0..3980e43a 100644 --- a/libs/application_runner/lib/monitor/environment_monitor.ex +++ b/libs/application_runner/lib/monitor/environment_monitor.ex @@ -58,8 +58,6 @@ defmodule ApplicationRunner.Monitor.EnvironmentMonitor do def handle_info({:DOWN, _ref, :process, pid, _reason}, state) do {{metadata}, new_state} = Map.pop(state, pid) - base_url = Application.fetch_env!(:application_runner, :faas_url) - auth = Application.fetch_env!(:application_runner, :faas_auth) Logger.debug("#{__MODULE__} handle down #{inspect(pid)} with metadata #{inspect(metadata)}") diff --git a/libs/application_runner/lib/services/application_services.ex b/libs/application_runner/lib/services/application_services.ex index 69f30242..8a770325 100644 --- a/libs/application_runner/lib/services/application_services.ex +++ b/libs/application_runner/lib/services/application_services.ex @@ -337,8 +337,7 @@ defmodule ApplicationRunner.ApplicationServices do |> Finch.request(AppHttp, receive_timeout: 5000) |> response(:update_app) - e -> - # {:error, TechnicalError.app_not_found()} + _ -> TechnicalError.openfaas_not_reachable_tuple() end end @@ -371,7 +370,7 @@ defmodule ApplicationRunner.ApplicationServices do {:ok, Jason.decode!(body)} end - defp response({:error, %Mint.TransportError{reason: reason}}, listener) do + defp response({:error, %Mint.TransportError{reason: reason}}, _listener) do Telemetry.event( :alert, %{}, From 2bb540a6d612a307be7e13ec3a02d6c9fdd655b8 Mon Sep 17 00:00:00 2001 From: Thomas DA ROCHA Date: Sun, 9 Feb 2025 18:00:58 +0100 Subject: [PATCH 13/13] build: Update deps --- libs/application_runner/lib/guardian/app_guardian.ex | 10 ++-------- mix.lock | 2 -- 2 files changed, 2 insertions(+), 10 deletions(-) diff --git a/libs/application_runner/lib/guardian/app_guardian.ex b/libs/application_runner/lib/guardian/app_guardian.ex index 594b023d..16484b3a 100644 --- a/libs/application_runner/lib/guardian/app_guardian.ex +++ b/libs/application_runner/lib/guardian/app_guardian.ex @@ -6,14 +6,8 @@ defmodule ApplicationRunner.Guardian.AppGuardian do use Guardian, otp_app: :application_runner alias ApplicationRunner.Environment.TokenAgent - - alias ApplicationRunner.{ - Environment, - MongoStorage, - Session - } - - alias ApplicationRunner.Errors.{BusinessError, TechnicalError} + alias ApplicationRunner.Errors.BusinessError + alias ApplicationRunner.MongoStorage require Logger diff --git a/mix.lock b/mix.lock index c4068262..67b17e0e 100644 --- a/mix.lock +++ b/mix.lock @@ -1,5 +1,4 @@ %{ - "application_runner": {:git, "https://github.com/lenra-io/application-runner.git", "d657ea5cf484ba3d78f4121c678249550da6c8b5", [ref: "v1.0.0-beta.130", submodules: true]}, "argon2_elixir": {:hex, :argon2_elixir, "3.2.1", "f47740bf9f2a39ffef79ba48eb25dea2ee37bcc7eadf91d49615591d1a6fce1a", [:make, :mix], [{:comeonin, "~> 5.3", [hex: :comeonin, repo: "hexpm", optional: false]}, {:elixir_make, "~> 0.6", [hex: :elixir_make, repo: "hexpm", optional: false]}], "hexpm", "a813b78217394530b5fcf4c8070feee43df03ffef938d044019169c766315690"}, "bamboo": {:hex, :bamboo, "2.2.0", "f10a406d2b7f5123eb1f02edfa043c259db04b47ab956041f279eaac776ef5ce", [:mix], [{:hackney, ">= 1.15.2", [hex: :hackney, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.4", [hex: :mime, repo: "hexpm", optional: false]}, {:plug, "~> 1.0", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "8c3b14ba7d2f40cb4be04128ed1e2aff06d91d9413d38bafb4afccffa3ade4fc"}, "bamboo_smtp": {:hex, :bamboo_smtp, "4.2.2", "e9f57a2300df9cb496c48751bd7668a86a2b89aa2e79ccaa34e0c46a5f64c3ae", [:mix], [{:bamboo, "~> 2.2.0", [hex: :bamboo, repo: "hexpm", optional: false]}, {:gen_smtp, "~> 1.2.0", [hex: :gen_smtp, repo: "hexpm", optional: false]}], "hexpm", "28cac2ec8adaae02aed663bf68163992891a3b44cfd7ada0bebe3e09bed7207f"}, @@ -49,7 +48,6 @@ "jason": {:hex, :jason, "1.4.4", "b9226785a9aa77b6857ca22832cffa5d5011a667207eb2a0ad56adb5db443b8a", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "c5eb0cab91f094599f94d55bc63409236a8ec69a21a67814529e8d5f6cc90b3b"}, "jose": {:hex, :jose, "1.11.6", "613fda82552128aa6fb804682e3a616f4bc15565a048dabd05b1ebd5827ed965", [:mix, :rebar3], [], "hexpm", "6275cb75504f9c1e60eeacb771adfeee4905a9e182103aa59b53fed651ff9738"}, "json_diff": {:hex, :json_diff, "0.1.3", "c80d5ca5416e785867e765e906e9a91b7efc35bfd505af276654d108f4995736", [:mix], [], "hexpm", "a5332e8293e7e9f384d34ea44645d7961334db73739165178fd4a7728d06f7d1"}, - "lenra_common": {:hex, :lenra_common, "2.9.0", "78c941b4878d10fed9ec3bbf44437221c662d5556a441a9a832175069414796d", [:mix], [{:jason, "~> 1.4", [hex: :jason, repo: "hexpm", optional: false]}, {:phoenix, "~> 1.6.15", [hex: :phoenix, repo: "hexpm", optional: false]}], "hexpm", "ce907706adc0fbb72b373c1f1f868205d1ed8ffa7e9d7b4c0db9c25320b3dcc0"}, "libcluster": {:hex, :libcluster, "3.3.3", "a4f17721a19004cfc4467268e17cff8b1f951befe428975dd4f6f7b84d927fe0", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7c0a2275a0bb83c07acd17dab3c3bfb4897b145106750eeccc62d302e3bdfee5"}, "libring": {:hex, :libring, "1.6.0", "d5dca4bcb1765f862ab59f175b403e356dec493f565670e0bacc4b35e109ce0d", [:mix], [], "hexpm", "5e91ece396af4bce99953d49ee0b02f698cd38326d93cd068361038167484319"}, "makeup": {:hex, :makeup, "1.1.0", "6b67c8bc2882a6b6a445859952a602afc1a41c2e08379ca057c0f525366fc3ca", [:mix], [{:nimble_parsec, "~> 1.2.2 or ~> 1.3", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "0a45ed501f4a8897f580eabf99a2e5234ea3e75a4373c8a52824f6e873be57a6"},