diff --git a/apps/lenra/lib/lenra/apps.ex b/apps/lenra/lib/lenra/apps.ex index 05b88d30..eb97251a 100644 --- a/apps/lenra/lib/lenra/apps.ex +++ b/apps/lenra/lib/lenra/apps.ex @@ -20,12 +20,11 @@ defmodule Lenra.Apps do import Ecto.Query alias ApplicationRunner.ApplicationServices - + alias ApplicationRunner.Environment.DynamicSupervisor + alias ApplicationRunner.MongoStorage.MongoUserLink alias Lenra.Repo alias Lenra.Subscriptions - alias Lenra.{Accounts, EmailWorker, GitlabApiServices, OpenfaasServices} - alias Lenra.Kubernetes.ApiServices alias Lenra.Apps.{ @@ -33,6 +32,7 @@ defmodule Lenra.Apps do Build, Deployment, Environment, + EnvironmentScaleOptions, Image, Logo, MainEnv, @@ -41,8 +41,6 @@ defmodule Lenra.Apps do UserEnvironmentRole } - alias ApplicationRunner.MongoStorage.MongoUserLink - alias Lenra.Errors.{BusinessError, TechnicalError} require Logger @@ -304,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)) @@ -342,8 +332,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) @@ -392,21 +381,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(scale_opts) - ApplicationServices.stop_app("#{OpenfaasServices.get_function_name(service_name, build_number)}") - 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 @@ -787,4 +778,101 @@ 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 + %{ + min: Application.fetch_env!(:lenra, :scale_free_min), + 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) + + 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) || default_scale_min) + |> max(default_scale_min) + |> min(default_scale_max) + + scale_max = + (scale_options + |> Map.get(:max) || default_scale_max) + |> max(1) + |> max(scale_min) + |> min(default_scale_max) + + %{ + min: scale_min, + 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 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 <- 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 + + @spec add_present(list :: list, map :: map, key :: atom) :: list + defp add_present(list, map, key) do + if Map.has_key?(map, key) do + [{key, Map.fetch(map, key)} | list] + else + list + end + 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/lib/lenra/services/openfaas_services.ex b/apps/lenra/lib/lenra/services/openfaas_services.ex index 531b4541..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) @@ -28,11 +24,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/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..a7e9a156 --- /dev/null +++ b/apps/lenra/test/lenra/app_adapter_test.exs @@ -0,0 +1,176 @@ +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}} = + app.creator_id + |> Apps.create_build(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", %{env: env} do + assert AppAdapter.get_scale_options(env.id) == %{min: 0, max: 1} + end + + 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(env.id) == %{min: 0, max: 1} + end + + test "returns scale options with subscription without scale options", %{app: app, env: env} 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(env.id) == %{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 + }) + + 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(env.id) == %{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 + }) + + 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(env.id) == %{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 + }) + + 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(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 + {: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(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 f11f2d16..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, @@ -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() @@ -50,11 +44,14 @@ defmodule Lenra.Apps.DeploymentTest do env = Enum.at(Repo.all(Environment), 0) build = Enum.at(Repo.all(Build), 0) - FaasStub.expect_get_function_once( - bypass, - %{"ok" => "200"}, - get_function_name(app.service_name, build.build_number) - ) + function_name = FaasStub.get_function_name(app.service_name, build.build_number) + + Bypass.stub(bypass, "GET", "/system/function/#{function_name}", fn conn -> + conn + |> Plug.Conn.resp(200, Jason.encode!(%{"availableReplicas" => 1})) + end) + + FaasStub.expect_update_function_once(bypass, %{"ok" => "200"}) Apps.create_deployment(env.id, build.id, app.creator_id) @@ -62,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/apps/lenra/test/support/faas_stub_helper.ex b/apps/lenra/test/support/faas_stub_helper.ex index 76d922f2..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 @@ -118,4 +122,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..934fa071 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,18 @@ 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(env_id) do + environment = + Environment + |> Repo.get(env_id) + + Apps.effective_env_scale_options(environment) end @impl ApplicationRunner.Adapter diff --git a/config/config.exs b/config/config.exs index 7b534d31..b4b7f57f 100644 --- a/config/config.exs +++ b/config/config.exs @@ -124,6 +124,13 @@ config :application_runner, ApplicationRunner.Scheduler, storage: ApplicationRun config :lenra, 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, m_cost: 15, diff --git a/libs/application_runner/lib/adapter.ex b/libs/application_runner/lib/adapter.ex index 2bb8eea4..47b65465 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 env id to the server/devtools needs + """ + @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 1f100c56..e93984f3 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(env_id) do # prepare the assigns to the session/environment session_metadata = %Session.Metadata{ env_id: env_id, @@ -127,7 +128,9 @@ defmodule ApplicationRunner.AppSocket do env_metadata = %Environment.Metadata{ env_id: env_id, - function_name: function_name + function_name: function_name, + scale_min: scale_options.min, + scale_max: scale_options.max } {:ok, env_metadata, session_metadata} diff --git a/libs/application_runner/lib/crons.ex b/libs/application_runner/lib/crons.ex index 4d558573..deff0757 100644 --- a/libs/application_runner/lib/crons.ex +++ b/libs/application_runner/lib/crons.ex @@ -2,27 +2,38 @@ 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 alias ApplicationRunner.Errors.{BusinessError, TechnicalError} - alias ApplicationRunner.{AppSocket, Environment, EventHandler, Repo} + alias ApplicationRunner.{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(env_id) + + with {:ok, _pid} <- + Environment.ensure_env_started(%Environment.Metadata{ + env_id: env_id, + function_name: function_name, + 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/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/environment/dynamic_supervisor.ex b/libs/application_runner/lib/environment/dynamic_supervisor.ex index 8f250fcf..d9f2f1fb 100644 --- a/libs/application_runner/lib/environment/dynamic_supervisor.ex +++ b/libs/application_runner/lib/environment/dynamic_supervisor.ex @@ -30,17 +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)}") - 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, scale_min), {:ok, pid} <- DynamicSupervisor.start_child( __MODULE__, @@ -92,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)) @@ -134,4 +132,16 @@ defmodule ApplicationRunner.Environment.DynamicSupervisor do stop_env(env_id) 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 -> + :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/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/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/libs/application_runner/lib/monitor/environment_monitor.ex b/libs/application_runner/lib/monitor/environment_monitor.ex index 9b7cf457..3980e43a 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,18 +41,29 @@ 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) - 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 - 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 79d5fcdc..8a770325 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 """ @@ -259,13 +259,33 @@ defmodule ApplicationRunner.ApplicationServices do end @doc """ - Set the maximum scale of an OpenFaaS application. + 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 set_app_labels(function_name, %{@scale_factor_label => to_string(scale_factor)}) end + @doc """ + Set the scale values of an OpenFaaS application. + """ + def set_app_scale_options(function_name, scale_options) do + labels = + scale_options + |> Enum.map(fn {key, value} -> + {case key do + :min -> @min_scale_label + :max -> @max_scale_label + :factor -> @scale_factor_label + _ -> nil + end, to_string(value)} + end) + |> Enum.filter(fn {key, _} -> key != nil end) + |> Map.new() + + set_app_labels(function_name, labels) + end + @doc """ Get the status of an application from OpenFaaS. """ @@ -383,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/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 diff --git a/libs/application_runner/test/environment/dynamic_supervisor_test.exs b/libs/application_runner/test/environment/dynamic_supervisor_test.exs index ecdad408..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,63 +41,106 @@ 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 - {: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 - } + 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) + &handle_min_scale_to_0/1 + ) + end - {:ok, body, conn} = Plug.Conn.read_body(conn) - app = Jason.decode!(body) + 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 + } - assert "0" = app["labels"]["com.openfaas.scale.min"] + check_scale_up(bypass) - conn - |> send_resp(200, "ok") - end - ) + {:ok, _pid} = DynamicSupervisor.ensure_env_started(env_metadata) + + check_scale_down(bypass) + + :ok = DynamicSupervisor.stop_env(env_id) + + # 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, + scale_min: 1, + scale_max: 5 + } + + check_scale_up(bypass) + + {:ok, _pid} = DynamicSupervisor.ensure_env_started(env_metadata) + + check_scale_up(bypass) + + :ok = DynamicSupervisor.stop_env(env_id) + + # Await for async environment stop + Process.sleep(5) + end + + 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, + scale_min: 0, + scale_max: 1 + } + + check_scale_up(bypass) + + {:ok, _pid} = DynamicSupervisor.ensure_env_started(env_metadata) + + # Check scale update + check_scale_up(bypass) + + :ok = DynamicSupervisor.update_env_scale_options(env_id, %{min: 1, max: 5}) + + 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/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/services/application_services_test.exs b/libs/application_runner/test/services/application_services_test.exs index c5c8067d..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()) @@ -46,7 +55,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 +69,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 +133,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) diff --git a/libs/application_runner/test/support/app_adapter.ex b/libs/application_runner/test/support/app_adapter.ex index b8cccf53..cb50c6aa 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(_env_id) do + %{min: 0, max: 1} + end + @impl ApplicationRunner.Adapter def resource_from_params(%{"connect_result" => connect_result}) do connect_result 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"},