From ec306c60e98171a72c64ea4f2dc9d6876424717b Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Wed, 17 Jul 2024 12:11:06 +0700 Subject: [PATCH 1/8] Add parsing of package_size to install metadata --- lib/uplink/packages/metadata.ex | 39 +++++++++++++++++++ .../packages/deployment/router_test.exs | 14 +++++++ test/uplink/packages/install/manager_test.exs | 12 ++++++ 3 files changed, 65 insertions(+) diff --git a/lib/uplink/packages/metadata.ex b/lib/uplink/packages/metadata.ex index ecb1dc14..8fd105e8 100644 --- a/lib/uplink/packages/metadata.ex +++ b/lib/uplink/packages/metadata.ex @@ -16,6 +16,20 @@ defmodule Uplink.Packages.Metadata do embeds_many :ports, __MODULE__.Port + embeds_one :package_size, Size, primary_key: false do + field :slug, :string + + embeds_one :allocation, Allocation, primary_key: false do + field :cpu, :integer + field :cpu_allowance, :string, default: "100%" + field :cpu_priority, :integer, default: 10 + field :memory, :integer + field :memory_unit, :string, default: "GiB" + field :memory_enforce, Ecto.Enum, values: [:hard, :soft], default: :hard + field :memory_swap, :boolean, default: false + end + end + embeds_one :channel, Channel, primary_key: false do field :slug, :string @@ -57,6 +71,7 @@ defmodule Uplink.Packages.Metadata do |> cast_embed(:main_port) |> cast_embed(:ports) |> cast_embed(:variables, with: &variable_changeset/2) + |> cast_embed(:package_size, with: &package_size_changeset/2) end defp organization_changeset(organization, params) do @@ -71,6 +86,30 @@ defmodule Uplink.Packages.Metadata do |> validate_required([:key, :value]) end + defp package_size_changeset(package_size, params) do + package_size + |> cast(params, [:slug]) + |> cast_embed(:allocation, with: &allocation_changeset/2) + end + + defp allocation_changeset(allocation, params) do + allocation + |> cast(params, [ + :cpu, + :cpu_allowance, + :cpu_priority, + :memory, + :memory_unit, + :memory_enforce, + :memory_swap + ]) + |> validate_inclusion(:memory_unit, ["GiB", "MiB", "GB", "MB"]) + |> validate_number(:cpu_priority, + greater_than_or_equal_to: 0, + less_than_or_equal_to: 10 + ) + end + defp package_changeset(package, params) do package |> cast(params, [:slug]) diff --git a/test/uplink/packages/deployment/router_test.exs b/test/uplink/packages/deployment/router_test.exs index e896a247..eb590897 100644 --- a/test/uplink/packages/deployment/router_test.exs +++ b/test/uplink/packages/deployment/router_test.exs @@ -32,6 +32,18 @@ defmodule Uplink.Packages.Deployment.RouterTest do "slug" => "uplink-web", "service_port" => 4000, "exposed_port" => 49152, + "package_size" => %{ + "slug" => "medium", + "allocation" => %{ + "cpu" => 1, + "cpu_allowance" => "100%", + "cpu_priority" => 10, + "memory" => 1, + "memory_unit" => "GiB", + "memory_swap" => false, + "memory_enforce" => "hard" + } + }, "channel" => %{ "slug" => "develop", "package" => %{ @@ -213,6 +225,8 @@ defmodule Uplink.Packages.Deployment.RouterTest do install = Uplink.Repo.get(Uplink.Packages.Install, install_id) assert install.current_state == "validating" + + assert install.metadata_snapshot.package_size != nil end end diff --git a/test/uplink/packages/install/manager_test.exs b/test/uplink/packages/install/manager_test.exs index ea5e64dc..127881e1 100644 --- a/test/uplink/packages/install/manager_test.exs +++ b/test/uplink/packages/install/manager_test.exs @@ -26,6 +26,18 @@ defmodule Uplink.Packages.Install.ManagerTest do "source" => 49153, "target" => 4000 }, + "package_size" => %{ + "slug" => "medium", + "allocation" => %{ + "cpu" => 1, + "cpu_allowance" => "100%", + "cpu_priority" => 10, + "memory" => 1, + "memory_unit" => "GiB", + "memory_swap" => false, + "memory_enforce" => "hard" + } + }, "channel" => %{ "slug" => "develop", "package" => %{ From dfe6023a5509186ca6ed5a0ce7356b56c7a31f17 Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Wed, 17 Jul 2024 14:00:29 +0700 Subject: [PATCH 2/8] Handle case when package_size is nil for metadata --- lib/uplink/packages.ex | 3 ++ lib/uplink/packages/instance/bootstrap.ex | 26 +++++++---- lib/uplink/packages/metadata/manager.ex | 55 +++++++++++++++++++++++ 3 files changed, 76 insertions(+), 8 deletions(-) diff --git a/lib/uplink/packages.ex b/lib/uplink/packages.ex index 6f7b251c..599a3395 100644 --- a/lib/uplink/packages.ex +++ b/lib/uplink/packages.ex @@ -76,6 +76,9 @@ defmodule Uplink.Packages do defdelegate get_or_create_project_name(client, metadata), to: Metadata.Manager + defdelegate get_or_create_size_profile(cient, metadata), + to: Metadata.Manager + defdelegate profile_name(metadata), to: Metadata.Manager diff --git a/lib/uplink/packages/instance/bootstrap.ex b/lib/uplink/packages/instance/bootstrap.ex index a07de665..90db9f4e 100644 --- a/lib/uplink/packages/instance/bootstrap.ex +++ b/lib/uplink/packages/instance/bootstrap.ex @@ -83,17 +83,31 @@ defmodule Uplink.Packages.Instance.Bootstrap do |> Enum.find(fn member -> member.server_name == node end) do + client = LXD.client() + profile_name = Packages.profile_name(metadata) + + size_profile_name = + Packages.get_or_create_size_profile(client, metadata) + + lxd_project_name = Packages.get_or_create_project_name(client, metadata) + image_server = get_image_server() + profiles = [profile_name, "default"] + + profiles = + if size_profile_name do + [size_profile_name | profiles] + else + profiles + end + lxd_instance = Map.merge(@default_params, %{ "name" => instance_name, "architecture" => architecture, - "profiles" => [ - profile_name, - "default" - ], + "profiles" => profiles, "source" => %{ "type" => "image", "mode" => "pull", @@ -103,10 +117,6 @@ defmodule Uplink.Packages.Instance.Bootstrap do } }) - client = LXD.client() - - lxd_project_name = Packages.get_or_create_project_name(client, metadata) - client |> Formation.lxd_create(node, lxd_instance, project: lxd_project_name) |> case do diff --git a/lib/uplink/packages/metadata/manager.ex b/lib/uplink/packages/metadata/manager.ex index 7a0cc218..e5d2d8b0 100644 --- a/lib/uplink/packages/metadata/manager.ex +++ b/lib/uplink/packages/metadata/manager.ex @@ -32,6 +32,25 @@ defmodule Uplink.Packages.Metadata.Manager do end end + def get_or_create_size_profile(_client, %Metadata{package_size: nil}), do: nil + + def get_or_create_size_profile( + client, + %Metadata{package_size: %Metadata.Size{}} = metadata + ) do + size_profile = size_profile_name(metadata) + + client + |> Lexdee.get_profile(size_profile) + |> case do + {:ok, %{body: %{"name" => name}}} -> + name + + {:error, %{"error_code" => 404}} -> + create_size_profile(client, metadata) + end + end + defp create_project(client, %Metadata{} = metadata) do project = project_name(metadata) @@ -58,10 +77,46 @@ defmodule Uplink.Packages.Metadata.Manager do end end + defp create_size_profile( + client, + %Metadata{package_size: package_size} = metadata + ) do + profile_name = size_profile_name(metadata) + + params = %{ + "name" => profile_name, + "config" => %{ + "limits.cpu" => package_size.allocation.cpu, + "limits.cpu.allowance" => package_size.allocation.cpu_allowance, + "limits.cpu.priority" => package_size.allocation.cpu_priority, + "limits.memory" => + "#{package_size.allocation.memory}#{package_size.allocation.memory_unit}", + "limits.memory.swap" => package_size.allocation.memory_swap, + "limits.memory.enforce" => "#{package_size.allocation.memory_enforce}" + }, + "description" => + "Size profile for #{metadata.channel.package.organization.slug}/#{metadata.channel.package.slug}" + } + + client + |> Lexdee.create_profile(params) + |> case do + {:ok, %{body: nil}} -> + profile_name + + {:error, %{"error" => _message}} -> + nil + end + end + defp project_name(%Metadata{channel: channel}) do "#{channel.package.organization.slug}.#{channel.package.slug}" end + defp size_profile_name(%Metadata{channel: channel, package_size: package_size}) do + "size.#{channel.package.organization.slug}.#{channel.package.slug}.#{package_size.slug}" + end + def public_key_name(%Metadata{channel: channel}) do Enum.join([channel.package.organization.slug, channel.package.slug], "-") end From 27b496d249700ad9cb7cd700537ba670994191d9 Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Wed, 17 Jul 2024 14:56:42 +0700 Subject: [PATCH 3/8] Add test case for size profile assignment on bootstrap --- test/fixtures/lxd/profiles/not_found.json | 9 + test/scenarios/deployment.ex | 114 +++++++ .../packages/instance/bootstrap_test.exs | 306 ++++++++++++++++++ 3 files changed, 429 insertions(+) create mode 100644 test/fixtures/lxd/profiles/not_found.json diff --git a/test/fixtures/lxd/profiles/not_found.json b/test/fixtures/lxd/profiles/not_found.json new file mode 100644 index 00000000..a014cd3d --- /dev/null +++ b/test/fixtures/lxd/profiles/not_found.json @@ -0,0 +1,9 @@ +{ + "type": "error", + "status": "", + "status_code": 0, + "operation": "", + "error_code": 404, + "error": "Profile not found", + "metadata": null +} \ No newline at end of file diff --git a/test/scenarios/deployment.ex b/test/scenarios/deployment.ex index 988696db..cf908ec4 100644 --- a/test/scenarios/deployment.ex +++ b/test/scenarios/deployment.ex @@ -62,6 +62,70 @@ defmodule Uplink.Scenarios.Deployment do } } + @deployment_params_with_package_size %{ + "hash" => "some-hash-with-package-size", + "archive_url" => "http://localhost/archives/packages.zip", + "stack" => "alpine/3.14", + "channel" => "develop", + "metadata" => %{ + "id" => 1, + "slug" => "uplink-web", + "package_size" => %{ + "slug" => "medium", + "allocation" => %{ + "cpu" => 1, + "cpu_allowance" => "100%", + "cpu_priority" => 10, + "memory" => 1, + "memory_unit" => "GiB", + "memory_swap" => false, + "memory_enforce" => "hard" + } + }, + "main_port" => %{ + "slug" => "web", + "source" => 49152, + "target" => 4000, + "routing" => %{ + "router_id" => 1, + "paths" => ["/configure*"] + } + }, + "ports" => [ + %{ + "slug" => "grpc", + "source" => 49153, + "target" => 6000 + } + ], + "hosts" => ["something.com"], + "variables" => [ + %{"key" => "SOMETHING", "value" => "blah"} + ], + "channel" => %{ + "slug" => "develop", + "package" => %{ + "slug" => "something-1640927800", + "credential" => %{ + "public_key" => "public_key" + }, + "organization" => %{ + "slug" => "upmaru" + } + } + }, + "instances" => [ + %{ + "id" => 1, + "slug" => "something-1", + "node" => %{ + "slug" => "some-node" + } + } + ] + } + } + def setup_endpoints(_context) do :ok = Ecto.Adapters.SQL.Sandbox.checkout(Uplink.Repo) @@ -157,4 +221,54 @@ defmodule Uplink.Scenarios.Deployment do deployment: deployment, install: executing_install} end + + def setup_base_with_package_size(_context) do + {:ok, actor} = + Members.get_or_create_actor(%{ + "identifier" => "zacksiri", + "provider" => "instellar", + "id" => "1" + }) + + metadata = Map.get(@deployment_params_with_package_size, "metadata") + + {:ok, metadata} = Packages.parse_metadata(metadata) + + app = + metadata + |> Metadata.app_slug() + |> Packages.get_or_create_app() + + {:ok, deployment} = + Packages.get_or_create_deployment( + app, + @deployment_params_with_package_size + ) + + {:ok, install} = + Packages.create_install(deployment, %{ + "installation_id" => 1, + "deployment" => @deployment_params_with_package_size + }) + + signature = Secret.Signature.compute_signature(deployment.hash) + + Cache.put( + {:deployment, signature, install.instellar_installation_id}, + metadata + ) + + {:ok, %{resource: validating_install}} = + Packages.transition_install_with(install, actor, "validate") + + {:ok, %{resource: executing_install}} = + Packages.transition_install_with(validating_install, actor, "execute") + + {:ok, + actor: actor, + metadata: metadata, + app: app, + deployment: deployment, + install: executing_install} + end end diff --git a/test/uplink/packages/instance/bootstrap_test.exs b/test/uplink/packages/instance/bootstrap_test.exs index 3e8cd90e..2febddce 100644 --- a/test/uplink/packages/instance/bootstrap_test.exs +++ b/test/uplink/packages/instance/bootstrap_test.exs @@ -835,4 +835,310 @@ defmodule Uplink.Packages.Instance.BootstrapTest do assert_enqueued(worker: Uplink.Packages.Instance.Cleanup, args: args) end end + + describe "bootstrap instance metadata with package_size" do + setup [:setup_base_with_package_size] + + setup %{metadata: metadata} do + size_profile = + "size.#{metadata.channel.package.organization.slug}.#{metadata.channel.package.slug}.#{metadata.package_size.slug}" + + {:ok, size_profile: size_profile} + end + + test "when project and size does not exist", %{ + bypass: bypass, + install: install, + actor: actor, + public_key_name: public_key_name, + create_instance: create_instance, + start_instance: start_instance, + exec_instance: exec_instance, + wait_for_operation: wait_for_operation, + wait_with_log: wait_with_log, + project: project_name, + size_profile: size_profile + } do + instance_slug = "test-02" + + project_not_found = + File.read!("test/fixtures/lxd/projects/not_found.json") + + Bypass.expect_once( + bypass, + "GET", + "/1.0/projects/#{project_name}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(404, project_not_found) + end + ) + + create_project = File.read!("test/fixtures/lxd/projects/create.json") + + Bypass.expect_once( + bypass, + "POST", + "/1.0/projects", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, create_project) + end + ) + + size_profile_not_found = + File.read!("test/fixtures/lxd/profiles/not_found.json") + + Bypass.expect_once( + bypass, + "GET", + "/1.0/profiles/#{size_profile}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(404, size_profile_not_found) + end + ) + + create_size_profile = File.read!("test/fixtures/lxd/profiles/create.json") + + Bypass.expect_once( + bypass, + "POST", + "/1.0/profiles", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, create_size_profile) + end + ) + + Bypass.expect_once(bypass, "POST", "/1.0/instances", fn conn -> + assert %{ + "target" => "ubuntu-s-1vcpu-1gb-sgp1-01", + "project" => project + } = conn.query_params + + {:ok, body, conn} = Plug.Conn.read_body(conn) + + assert %{"source" => source, "profiles" => profiles} = + Jason.decode!(body) + + assert size_profile in profiles + + assert %{"server" => server} = source + + assert server == "https://localhost/spaces/test" + + assert project == project_name + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, create_instance) + end) + + create_instance_params = Jason.decode!(create_instance) + create_instance_operation_id = create_instance_params["metadata"]["id"] + + Bypass.expect_once( + bypass, + "GET", + "/1.0/operations/#{create_instance_operation_id}/wait", + fn conn -> + assert %{"timeout" => "180"} = conn.query_params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, wait_for_operation) + end + ) + + Bypass.expect_once( + bypass, + "PUT", + "/1.0/instances/#{instance_slug}/state", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, start_instance) + end + ) + + start_instance_params = Jason.decode!(start_instance) + start_instance_operation_id = start_instance_params["metadata"]["id"] + + Bypass.expect_once( + bypass, + "GET", + "/1.0/operations/#{start_instance_operation_id}/wait", + fn conn -> + assert %{"timeout" => "180"} = conn.query_params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, wait_for_operation) + end + ) + + hostname = System.get_env("HOSTNAME") + + distribution_port = + Application.get_env(:uplink, Uplink.Internal) + |> Keyword.get(:port) + + Bypass.expect( + bypass, + "POST", + "/1.0/instances/#{instance_slug}/exec", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + assert {:ok, body, conn} = Plug.Conn.read_body(conn) + assert {:ok, %{"command" => command}} = Jason.decode(body) + + assert command in [ + [ + "/bin/sh", + "-c", + "echo 'public_key' > /etc/apk/keys/#{public_key_name}.rsa.pub\n" + ], + ["/bin/sh", "-c", "cat /etc/apk/repositories\n"], + [ + "/bin/sh", + "-c", + "echo -e 'http://#{hostname}:#{distribution_port}/distribution/develop/upmaru/something-1640927800' >> /etc/apk/repositories\n" + ] + ] + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, exec_instance) + end + ) + + setup_public_key_params = Jason.decode!(exec_instance) + setup_public_key_operation_id = setup_public_key_params["metadata"]["id"] + + Bypass.expect( + bypass, + "GET", + "/1.0/operations/#{setup_public_key_operation_id}/wait", + fn conn -> + assert %{"timeout" => _timeout} = conn.query_params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, wait_with_log) + end + ) + + Bypass.expect( + bypass, + "GET", + "/1.0/instances/#{instance_slug}/logs/stdout.log", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + conn + |> Plug.Conn.resp( + 200, + "http://#{hostname}:#{distribution_port}/distribution/develop/upmaru/something-1640927800" + ) + end + ) + + Bypass.expect( + bypass, + "GET", + "/1.0/instances/#{instance_slug}/logs/stderr.log", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + conn + |> Plug.Conn.resp(200, "") + end + ) + + Bypass.expect_once( + bypass, + "PUT", + "/1.0/instances/#{instance_slug}/state", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + assert {:ok, body, conn} = Plug.Conn.read_body(conn) + assert {:ok, body} = Jason.decode(body) + + assert body["action"] == "start" + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, start_instance) + end + ) + + start_instance_key_params = Jason.decode!(start_instance) + start_instance_operation_id = start_instance_key_params["metadata"]["id"] + + Bypass.expect( + bypass, + "GET", + "/1.0/operations/#{start_instance_operation_id}/wait", + fn conn -> + %{"timeout" => "180"} = conn.query_params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, wait_for_operation) + end + ) + + Bypass.expect_once( + bypass, + "POST", + "/uplink/installations/#{install.instellar_installation_id}/instances/#{instance_slug}/events", + fn conn -> + assert {:ok, body, conn} = Plug.Conn.read_body(conn) + assert {:ok, body} = Jason.decode(body) + + assert %{"event" => %{"name" => "boot" = event_name}} = body + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp( + 201, + Jason.encode!(%{ + "data" => %{"attributes" => %{"id" => 1, "name" => event_name}} + }) + ) + end + ) + + assert {:ok, job} = + perform_job(Bootstrap, %{ + instance: %{ + slug: instance_slug, + node: %{slug: "ubuntu-s-1vcpu-1gb-sgp1-01"} + }, + install_id: install.id, + actor_id: actor.id + }) + + assert_enqueued(worker: Uplink.Packages.Instance.Install, args: job.args) + end + end end From 82da8062964ac41c762f404a5dd19eac72f635b7 Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Wed, 17 Jul 2024 17:08:10 +0700 Subject: [PATCH 4/8] Setup failing test for upgrade to call for size profile --- lib/uplink/packages/metadata/manager.ex | 35 ++- .../uplink/packages/instance/upgrade_test.exs | 247 ++++++++++++++++++ 2 files changed, 273 insertions(+), 9 deletions(-) diff --git a/lib/uplink/packages/metadata/manager.ex b/lib/uplink/packages/metadata/manager.ex index e5d2d8b0..0ec809e0 100644 --- a/lib/uplink/packages/metadata/manager.ex +++ b/lib/uplink/packages/metadata/manager.ex @@ -83,17 +83,34 @@ defmodule Uplink.Packages.Metadata.Manager do ) do profile_name = size_profile_name(metadata) + config = %{ + "limits.cpu.allowance" => package_size.allocation.cpu_allowance, + "limits.cpu.priority" => package_size.allocation.cpu_priority, + "limits.memory.swap" => package_size.allocation.memory_swap, + "limits.memory.enforce" => "#{package_size.allocation.memory_enforce}" + } + + config = + if package_size.allocation.cpu do + Map.put(config, "limits.cpu", package_size.allocation.cpu) + else + config + end + + config = + if package_size.allocation.memory do + Map.put( + config, + "limits.memory", + "#{package_size.allocation.memory}#{package_size.allocation.memory_unit}" + ) + else + config + end + params = %{ "name" => profile_name, - "config" => %{ - "limits.cpu" => package_size.allocation.cpu, - "limits.cpu.allowance" => package_size.allocation.cpu_allowance, - "limits.cpu.priority" => package_size.allocation.cpu_priority, - "limits.memory" => - "#{package_size.allocation.memory}#{package_size.allocation.memory_unit}", - "limits.memory.swap" => package_size.allocation.memory_swap, - "limits.memory.enforce" => "#{package_size.allocation.memory_enforce}" - }, + "config" => config, "description" => "Size profile for #{metadata.channel.package.organization.slug}/#{metadata.channel.package.slug}" } diff --git a/test/uplink/packages/instance/upgrade_test.exs b/test/uplink/packages/instance/upgrade_test.exs index d1b86579..f8b2d14e 100644 --- a/test/uplink/packages/instance/upgrade_test.exs +++ b/test/uplink/packages/instance/upgrade_test.exs @@ -732,4 +732,251 @@ defmodule Uplink.Packages.Instance.UpgradeTest do assert worker == "Uplink.Packages.Instance.Restart" end end + + @deployment_with_package_size %{ + "hash" => "some-hash-2-with-package-size", + "archive_url" => "http://localhost/archives/packages.zip", + "stack" => "alpine/3.14", + "channel" => "develop", + "metadata" => %{ + "id" => 1, + "slug" => "uplink-web", + "service_port" => 4000, + "exposed_port" => 49152, + "package_size" => %{ + "slug" => "medium", + "allocation" => %{ + "cpu" => 1, + "cpu_allowance" => "100%", + "cpu_priority" => 10, + "memory" => 1, + "memory_unit" => "GiB", + "memory_swap" => false, + "memory_enforce" => "hard" + } + }, + "variables" => [ + %{"key" => "SOMETHING", "value" => "blah"} + ], + "channel" => %{ + "slug" => "develop", + "package" => %{ + "slug" => "something-1640927800", + "credential" => %{ + "public_key" => "public_key" + }, + "organization" => %{ + "slug" => "upmaru" + } + } + }, + "instances" => [ + %{ + "id" => 1, + "slug" => "something-1", + "node" => %{ + "slug" => "some-node" + } + } + ] + } + } + + describe "upgrade instance with size profile" do + setup %{app: app, metadata: metadata} do + {:ok, first_deployment} = + Packages.get_or_create_deployment(app, @first_deployment) + + {:ok, second_deployment} = + Packages.get_or_create_deployment(app, @deployment_with_package_size) + + {:ok, first_install} = + Packages.create_install(first_deployment, %{ + "installation_id" => 1, + "deployment" => @first_deployment + }) + + first_install + |> Ecto.Changeset.cast(%{current_state: "completed"}, [:current_state]) + |> Repo.update() + + {:ok, second_install} = + Packages.create_install(second_deployment, %{ + "installation_id" => 1, + "deployment" => @deployment_with_package_size + }) + + second_install + |> Ecto.Changeset.cast(%{current_state: "completed"}, [:current_state]) + |> Repo.update() + + project = + "#{metadata.channel.package.organization.slug}.#{metadata.channel.package.slug}" + + size_profile = + "size.#{metadata.channel.package.organization.slug}.#{metadata.channel.package.slug}.#{second_install.metadata_snapshot.package_size.slug}" + + {:ok, project: project, size_profile: size_profile} + end + + test "perform", %{ + bypass: bypass, + actor: actor, + install: install, + exec_instance: exec_instance, + wait_with_log: wait_with_log, + metadata: metadata, + project: project_name, + size_profile: size_profile + } do + instance_slug = "some-instance-01" + + project_found = File.read!("test/fixtures/lxd/projects/show.json") + + Bypass.expect_once( + bypass, + "GET", + "/1.0/projects/#{project_name}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, project_found) + end + ) + + size_profile_not_found = + File.read!("test/fixtures/lxd/profiles/not_found.json") + + Bypass.expect_once( + bypass, + "GET", + "/1.0/profiles/#{size_profile}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(404, size_profile_not_found) + end + ) + + create_size_profile = File.read!("test/fixtures/lxd/profiles/create.json") + + Bypass.expect_once( + bypass, + "POST", + "/1.0/profiles", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, create_size_profile) + end + ) + + Bypass.expect_once( + bypass, + "POST", + "/1.0/instances/#{instance_slug}/exec", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + assert {:ok, body, conn} = Plug.Conn.read_body(conn) + assert {:ok, %{"command" => command}} = Jason.decode(body) + + assert command == [ + "/bin/sh", + "-c", + "apk update && apk add --upgrade #{metadata.channel.package.slug}\n" + ] + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, exec_instance) + end + ) + + exec_instance_params = Jason.decode!(exec_instance) + exec_instance_operation_id = exec_instance_params["metadata"]["id"] + + Bypass.expect_once( + bypass, + "GET", + "/1.0/operations/#{exec_instance_operation_id}/wait", + fn conn -> + assert %{"timeout" => "180"} = conn.query_params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, wait_with_log) + end + ) + + complete_message = "upgrade complete" + + Bypass.expect_once( + bypass, + "GET", + "/1.0/instances/#{instance_slug}/logs/stdout.log", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + conn + |> Plug.Conn.resp( + 200, + complete_message + ) + end + ) + + Bypass.expect_once( + bypass, + "GET", + "/1.0/instances/#{instance_slug}/logs/stderr.log", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + conn + |> Plug.Conn.resp(200, "") + end + ) + + Bypass.expect( + bypass, + "POST", + "/uplink/installations/#{install.instellar_installation_id}/instances/#{instance_slug}/events", + fn conn -> + assert {:ok, body, conn} = Plug.Conn.read_body(conn) + assert {:ok, body} = Jason.decode(body) + + assert %{"event" => %{"name" => event_name}} = body + assert event_name in ["upgrade", "complete"] + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp( + 201, + Jason.encode!(%{ + "data" => %{"attributes" => %{"id" => 1, "name" => event_name}} + }) + ) + end + ) + + assert {:ok, %Oban.Job{worker: worker}} = + perform_job(Upgrade, %{ + instance: %{ + slug: instance_slug, + node: %{slug: "some-node-01"} + }, + install_id: install.id, + actor_id: actor.id + }) + + assert worker == "Uplink.Packages.Instance.Finalize" + end + end end From 1cc57e9576f54b3c3ed9477ead78cd552737324d Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Wed, 17 Jul 2024 17:38:54 +0700 Subject: [PATCH 5/8] Update upgrade worker to update instance --- lib/uplink/packages/instance/upgrade.ex | 31 +++++++++++++++++++++++++ mix.exs | 2 +- mix.lock | 16 ++++++------- 3 files changed, 40 insertions(+), 9 deletions(-) diff --git a/lib/uplink/packages/instance/upgrade.ex b/lib/uplink/packages/instance/upgrade.ex index fad2a2ce..6b41b8ed 100644 --- a/lib/uplink/packages/instance/upgrade.ex +++ b/lib/uplink/packages/instance/upgrade.ex @@ -129,6 +129,7 @@ defmodule Uplink.Packages.Instance.Upgrade do Map.put(@transition_parameters, "node", node["slug"]) LXD.client() + |> handle_update_config(formation_instance, metadata) |> Formation.lxd_upgrade_alpine_package(formation_instance) |> case do {:ok, upgrade_package_output} -> @@ -220,4 +221,34 @@ defmodule Uplink.Packages.Instance.Upgrade do |> Instance.Cleanup.new() |> Oban.insert() end + + defp handle_update_config(client, instance, metadata) do + profile_name = Packages.profile_name(metadata) + size_profile_name = Packages.get_or_create_size_profile(client, metadata) + + profiles = [profile_name, "default"] + + profiles = + if size_profile_name do + [size_profile_name | profiles] + else + profiles + end + + params = %{ + "profiles" => profiles + } + + client + |> Lexdee.update_instance(instance.slug, params, + query: [project: instance.project] + ) + |> case do + {:ok, _message} -> + client + + {:error, error} -> + raise "Failed to update instance config: #{inspect(error)}" + end + end end diff --git a/mix.exs b/mix.exs index 3fb5faaa..5cbef02f 100644 --- a/mix.exs +++ b/mix.exs @@ -66,7 +66,7 @@ defmodule Uplink.MixProject do # Infrastructure {:formation, "~> 0.15"}, - {:lexdee, "~> 2.3"}, + {:lexdee, "~> 2.4"}, {:plug_cowboy, "~> 2.0"}, {:reverse_proxy_plug, "~> 2.1"}, {:mint_web_socket, "~> 1.0.2"}, diff --git a/mix.lock b/mix.lock index f9762947..9a3c25ac 100644 --- a/mix.lock +++ b/mix.lock @@ -2,7 +2,7 @@ "aws": {:hex, :aws, "0.13.3", "d2e932c2588e2b15fca04f345dfced6e07b81d6534e65783de23190c57891df7", [:mix], [{:aws_signature, "~> 0.3", [hex: :aws_signature, repo: "hexpm", optional: false]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:hackney, "~> 1.16", [hex: :hackney, repo: "hexpm", optional: true]}, {:jason, "~> 1.2", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "4ad46a204370d3ef8eab102776a00070ec6fcc4414db7f6b39f3fc0d99564e18"}, "aws_signature": {:hex, :aws_signature, "0.3.2", "adf33bc4af00b2089b7708bf20e3246f09c639a905a619b3689f0a0a22c3ef8f", [:rebar3], [], "hexpm", "b0daf61feb4250a8ab0adea60db3e336af732ff71dd3fb22e45ae3dcbd071e44"}, "bypass": {:hex, :bypass, "2.1.0", "909782781bf8e20ee86a9cabde36b259d44af8b9f38756173e8f5e2e1fabb9b1", [:mix], [{:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.0", [hex: :plug_cowboy, repo: "hexpm", optional: false]}, {:ranch, "~> 1.3", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "d9b5df8fa5b7a6efa08384e9bbecfe4ce61c77d28a4282f79e02f1ef78d96b80"}, - "castore": {:hex, :castore, "0.1.22", "4127549e411bedd012ca3a308dede574f43819fe9394254ca55ab4895abfa1a2", [:mix], [], "hexpm", "c17576df47eb5aa1ee40cc4134316a99f5cad3e215d5c77b8dd3cfef12a22cac"}, + "castore": {:hex, :castore, "1.0.8", "dedcf20ea746694647f883590b82d9e96014057aff1d44d03ec90f36a5c0dc6e", [:mix], [], "hexpm", "0b2b66d2ee742cb1d9cb8c8be3b43c3a70ee8651f37b75a8b982e036752983f1"}, "certifi": {:hex, :certifi, "2.12.0", "2d1cca2ec95f59643862af91f001478c9863c2ac9cb6e2f89780bfd8de987329", [:rebar3], [], "hexpm", "ee68d85df22e554040cdb4be100f33873ac6051387baf6a8f6ce82272340ff1c"}, "cowboy": {:hex, :cowboy, "2.10.0", "ff9ffeff91dae4ae270dd975642997afe2a1179d94b1887863e43f681a203e26", [:make, :rebar3], [{:cowlib, "2.12.1", [hex: :cowlib, repo: "hexpm", optional: false]}, {:ranch, "1.8.0", [hex: :ranch, repo: "hexpm", optional: false]}], "hexpm", "3afdccb7183cc6f143cb14d3cf51fa00e53db9ec80cdcd525482f5e99bc41d6b"}, "cowboy_telemetry": {:hex, :cowboy_telemetry, "0.4.0", "f239f68b588efa7707abce16a84d0d2acf3a0f50571f8bb7f56a15865aae820c", [:rebar3], [{:cowboy, "~> 2.7", [hex: :cowboy, repo: "hexpm", optional: false]}, {:telemetry, "~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "7d98bac1ee4565d31b62d59f8823dfd8356a169e7fcbb83831b8a5397404c9de"}, @@ -18,17 +18,17 @@ "finch": {:hex, :finch, "0.18.0", "944ac7d34d0bd2ac8998f79f7a811b21d87d911e77a786bc5810adb75632ada4", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.3", [hex: :mint, repo: "hexpm", optional: false]}, {:nimble_options, "~> 0.4 or ~> 1.0", [hex: :nimble_options, repo: "hexpm", optional: false]}, {:nimble_pool, "~> 0.2.6 or ~> 1.0", [hex: :nimble_pool, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "69f5045b042e531e53edc2574f15e25e735b522c37e2ddb766e15b979e03aa65"}, "formation": {:hex, :formation, "0.15.2", "0ff349edbc11b7b9f4bb385f12d37f278907b796a3c19ff8cd9d105c17d1eb3f", [:mix], [{:aws, "~> 0.13.0", [hex: :aws, repo: "hexpm", optional: false]}, {:ecto, "~> 3.10", [hex: :ecto, repo: "hexpm", optional: false]}, {:finch, "~> 0.18.0", [hex: :finch, repo: "hexpm", optional: false]}, {:lexdee, "~> 2.3", [hex: :lexdee, repo: "hexpm", optional: false]}, {:mustache, "~> 0.5.0", [hex: :mustache, repo: "hexpm", optional: false]}, {:postgrex, "~> 0.17.1", [hex: :postgrex, repo: "hexpm", optional: false]}, {:tesla, "~> 1.7.0", [hex: :tesla, repo: "hexpm", optional: false]}], "hexpm", "c233e3ffc307665fab34ddb75aa2201c1daa7d5d4519fa12b6f9a5ccc5be44a9"}, "hackney": {:hex, :hackney, "1.20.1", "8d97aec62ddddd757d128bfd1df6c5861093419f8f7a4223823537bad5d064e2", [:rebar3], [{:certifi, "~> 2.12.0", [hex: :certifi, repo: "hexpm", optional: false]}, {:idna, "~> 6.1.0", [hex: :idna, repo: "hexpm", optional: false]}, {:metrics, "~> 1.0.0", [hex: :metrics, repo: "hexpm", optional: false]}, {:mimerl, "~> 1.1", [hex: :mimerl, repo: "hexpm", optional: false]}, {:parse_trans, "3.4.1", [hex: :parse_trans, repo: "hexpm", optional: false]}, {:ssl_verify_fun, "~> 1.1.0", [hex: :ssl_verify_fun, repo: "hexpm", optional: false]}, {:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "fe9094e5f1a2a2c0a7d10918fee36bfec0ec2a979994cff8cfe8058cd9af38e3"}, - "hpax": {:hex, :hpax, "0.2.0", "5a58219adcb75977b2edce5eb22051de9362f08236220c9e859a47111c194ff5", [:mix], [], "hexpm", "bea06558cdae85bed075e6c036993d43cd54d447f76d8190a8db0dc5893fa2f1"}, + "hpax": {:hex, :hpax, "1.0.0", "28dcf54509fe2152a3d040e4e3df5b265dcb6cb532029ecbacf4ce52caea3fd2", [:mix], [], "hexpm", "7f1314731d711e2ca5fdc7fd361296593fc2542570b3105595bb0bc6d0fad601"}, "httpoison": {:hex, :httpoison, "2.2.1", "87b7ed6d95db0389f7df02779644171d7319d319178f6680438167d7b69b1f3d", [:mix], [{:hackney, "~> 1.17", [hex: :hackney, repo: "hexpm", optional: false]}], "hexpm", "51364e6d2f429d80e14fe4b5f8e39719cacd03eb3f9a9286e61e216feac2d2df"}, "idna": {:hex, :idna, "6.1.1", "8a63070e9f7d0c62eb9d9fcb360a7de382448200fbbd1b106cc96d3d8099df8d", [:rebar3], [{:unicode_util_compat, "~> 0.7.0", [hex: :unicode_util_compat, repo: "hexpm", optional: false]}], "hexpm", "92376eb7894412ed19ac475e4a86f7b413c1b9fbb5bd16dccd57934157944cea"}, - "jason": {:hex, :jason, "1.4.1", "af1504e35f629ddcdd6addb3513c3853991f694921b1b9368b0bd32beb9f1b63", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "fbb01ecdfd565b56261302f7e1fcc27c4fb8f32d56eab74db621fc154604a7a1"}, - "lexdee": {:hex, :lexdee, "2.3.8", "3ad9ea51907b3ec98756e85b990fe8d3177768a649e879c33d96c7b8ea03a5cc", [:mix], [{:castore, "~> 0.1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: false]}, {:mint_web_socket, "~> 1.0.2", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:tesla, "~> 1.7.0", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.8.1", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm", "b8a886ae75f4ccb4ed53fb510fbd54d45bf5dab0e22e2ae2d5e5f46d3b46b03b"}, + "jason": {:hex, :jason, "1.4.3", "d3f984eeb96fe53b85d20e0b049f03e57d075b5acda3ac8d465c969a2536c17b", [:mix], [{:decimal, "~> 1.0 or ~> 2.0", [hex: :decimal, repo: "hexpm", optional: true]}], "hexpm", "9a90e868927f7c777689baa16d86f4d0e086d968db5c05d917ccff6d443e58a3"}, + "lexdee": {:hex, :lexdee, "2.4.2", "05a74d4b90116167b75a08bea2ff9ae563d89f382dee9432fdae9524746240ce", [:mix], [{:castore, "~> 1.0", [hex: :castore, repo: "hexpm", optional: false]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: false]}, {:mint, "~> 1.5", [hex: :mint, repo: "hexpm", optional: false]}, {:mint_web_socket, "~> 1.0.2", [hex: :mint_web_socket, repo: "hexpm", optional: false]}, {:tesla, "~> 1.7.0", [hex: :tesla, repo: "hexpm", optional: false]}, {:x509, "~> 0.8.1", [hex: :x509, repo: "hexpm", optional: false]}], "hexpm", "5a7dd7d1bfbc9fe14bf60c31aa214dd855e924f524c085f45e680031e4f7859c"}, "libcluster": {:hex, :libcluster, "3.3.3", "a4f17721a19004cfc4467268e17cff8b1f951befe428975dd4f6f7b84d927fe0", [:mix], [{:jason, "~> 1.1", [hex: :jason, repo: "hexpm", optional: false]}], "hexpm", "7c0a2275a0bb83c07acd17dab3c3bfb4897b145106750eeccc62d302e3bdfee5"}, "metrics": {:hex, :metrics, "1.0.1", "25f094dea2cda98213cecc3aeff09e940299d950904393b2a29d191c346a8486", [:rebar3], [], "hexpm", "69b09adddc4f74a40716ae54d140f93beb0fb8978d8636eaded0c31b6f099f16"}, - "mime": {:hex, :mime, "2.0.5", "dc34c8efd439abe6ae0343edbb8556f4d63f178594894720607772a041b04b02", [:mix], [], "hexpm", "da0d64a365c45bc9935cc5c8a7fc5e49a0e0f9932a761c55d6c52b142780a05c"}, + "mime": {:hex, :mime, "2.0.6", "8f18486773d9b15f95f4f4f1e39b710045fa1de891fada4516559967276e4dc2", [:mix], [], "hexpm", "c9945363a6b26d747389aac3643f8e0e09d30499a138ad64fe8fd1d13d9b153e"}, "mimerl": {:hex, :mimerl, "1.3.0", "d0cd9fc04b9061f82490f6581e0128379830e78535e017f7780f37fea7545726", [:rebar3], [], "hexpm", "a1e15a50d1887217de95f0b9b0793e32853f7c258a5cd227650889b38839fe9d"}, - "mint": {:hex, :mint, "1.6.0", "88a4f91cd690508a04ff1c3e28952f322528934be541844d54e0ceb765f01d5e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "3c5ae85d90a5aca0a49c0d8b67360bbe407f3b54f1030a111047ff988e8fefaa"}, - "mint_web_socket": {:hex, :mint_web_socket, "1.0.3", "aab42fff792a74649916236d0b01f560a0b3f03ca5dea693c230d1c44736b50e", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "ca3810ca44cc8532e3dce499cc17f958596695d226bb578b2fbb88c09b5954b0"}, + "mint": {:hex, :mint, "1.6.2", "af6d97a4051eee4f05b5500671d47c3a67dac7386045d87a904126fd4bbcea2e", [:mix], [{:castore, "~> 0.1.0 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:hpax, "~> 0.1.1 or ~> 0.2.0 or ~> 1.0", [hex: :hpax, repo: "hexpm", optional: false]}], "hexpm", "5ee441dffc1892f1ae59127f74afe8fd82fda6587794278d924e4d90ea3d63f9"}, + "mint_web_socket": {:hex, :mint_web_socket, "1.0.4", "0b539116dbb3d3f861cdf5e15e269a933cb501c113a14db7001a3157d96ffafd", [:mix], [{:mint, ">= 1.4.1 and < 2.0.0-0", [hex: :mint, repo: "hexpm", optional: false]}], "hexpm", "027d4c5529c45a4ba0ce27a01c0f35f284a5468519c045ca15f43decb360a991"}, "mox": {:hex, :mox, "1.1.0", "0f5e399649ce9ab7602f72e718305c0f9cdc351190f72844599545e4996af73c", [:mix], [], "hexpm", "d44474c50be02d5b72131070281a5d3895c0e7a95c780e90bc0cfe712f633a13"}, "mustache": {:hex, :mustache, "0.5.1", "1bfee8a68a8e72d47616541a93a632ae1684c1abc17c9eb5f7bfecb78d7496e5", [:mix], [], "hexpm", "524c9bbb6080a52d7b6806436b4e269e0224c785a228faf3293ef30a75016bfa"}, "nebulex": {:hex, :nebulex, "2.5.1", "8ffbde30643e76d6cec712281ca68ab05f73170de9e758a39bc7e4e6987f608f", [:mix], [{:decorator, "~> 1.4", [hex: :decorator, repo: "hexpm", optional: true]}, {:shards, "~> 1.1", [hex: :shards, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "8d0d3800d98c68ee19b229b7fe35fac0192ab5963a573612cf74a388e083bccf"}, @@ -49,5 +49,5 @@ "telemetry": {:hex, :telemetry, "1.2.1", "68fdfe8d8f05a8428483a97d7aab2f268aaff24b49e0f599faa091f1d4e7f61c", [:rebar3], [], "hexpm", "dad9ce9d8effc621708f99eac538ef1cbe05d6a874dd741de2e689c47feafed5"}, "tesla": {:hex, :tesla, "1.7.0", "a62dda2f80d4f8a925eb7b8c5b78c461e0eb996672719fe1a63b26321a5f8b4e", [:mix], [{:castore, "~> 0.1 or ~> 1.0", [hex: :castore, repo: "hexpm", optional: true]}, {:exjsx, ">= 3.0.0", [hex: :exjsx, repo: "hexpm", optional: true]}, {:finch, "~> 0.13", [hex: :finch, repo: "hexpm", optional: true]}, {:fuse, "~> 2.4", [hex: :fuse, repo: "hexpm", optional: true]}, {:gun, "~> 1.3", [hex: :gun, repo: "hexpm", optional: true]}, {:hackney, "~> 1.6", [hex: :hackney, repo: "hexpm", optional: true]}, {:ibrowse, "4.4.0", [hex: :ibrowse, repo: "hexpm", optional: true]}, {:jason, ">= 1.0.0", [hex: :jason, repo: "hexpm", optional: true]}, {:mime, "~> 1.0 or ~> 2.0", [hex: :mime, repo: "hexpm", optional: false]}, {:mint, "~> 1.0", [hex: :mint, repo: "hexpm", optional: true]}, {:msgpax, "~> 2.3", [hex: :msgpax, repo: "hexpm", optional: true]}, {:poison, ">= 1.0.0", [hex: :poison, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: true]}], "hexpm", "2e64f01ebfdb026209b47bc651a0e65203fcff4ae79c11efb73c4852b00dc313"}, "unicode_util_compat": {:hex, :unicode_util_compat, "0.7.0", "bc84380c9ab48177092f43ac89e4dfa2c6d62b40b8bd132b1059ecc7232f9a78", [:rebar3], [], "hexpm", "25eee6d67df61960cf6a794239566599b09e17e668d3700247bc498638152521"}, - "x509": {:hex, :x509, "0.8.8", "aaf5e58b19a36a8e2c5c5cff0ad30f64eef5d9225f0fd98fb07912ee23f7aba3", [:mix], [], "hexpm", "ccc3bff61406e5bb6a63f06d549f3dba3a1bbb456d84517efaaa210d8a33750f"}, + "x509": {:hex, :x509, "0.8.9", "03c47e507171507d3d3028d802f48dd575206af2ef00f764a900789dfbe17476", [:mix], [], "hexpm", "ea3fb16a870a199cb2c45908a2c3e89cc934f0434173dc0c828136f878f11661"}, } From 77718bbfa884e14e16cdddeb2e9bb1d2e01b718d Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Wed, 17 Jul 2024 17:48:04 +0700 Subject: [PATCH 6/8] Add ability to skip update instance if package size is nil --- lib/uplink/packages/instance/upgrade.ex | 3 +++ test/fixtures/lxd/instances/update.json | 5 +++++ test/uplink/packages/instance/upgrade_test.exs | 15 +++++++++++++++ 3 files changed, 23 insertions(+) create mode 100644 test/fixtures/lxd/instances/update.json diff --git a/lib/uplink/packages/instance/upgrade.ex b/lib/uplink/packages/instance/upgrade.ex index 6b41b8ed..8adf40e5 100644 --- a/lib/uplink/packages/instance/upgrade.ex +++ b/lib/uplink/packages/instance/upgrade.ex @@ -222,6 +222,9 @@ defmodule Uplink.Packages.Instance.Upgrade do |> Oban.insert() end + defp handle_update_config(client, _instance, %Metadata{package_size: nil}), + do: client + defp handle_update_config(client, instance, metadata) do profile_name = Packages.profile_name(metadata) size_profile_name = Packages.get_or_create_size_profile(client, metadata) diff --git a/test/fixtures/lxd/instances/update.json b/test/fixtures/lxd/instances/update.json new file mode 100644 index 00000000..3be5f682 --- /dev/null +++ b/test/fixtures/lxd/instances/update.json @@ -0,0 +1,5 @@ +{ + "status": "Success", + "status_code": 200, + "type": "sync" +} \ No newline at end of file diff --git a/test/uplink/packages/instance/upgrade_test.exs b/test/uplink/packages/instance/upgrade_test.exs index f8b2d14e..1fb82e8f 100644 --- a/test/uplink/packages/instance/upgrade_test.exs +++ b/test/uplink/packages/instance/upgrade_test.exs @@ -783,6 +783,8 @@ defmodule Uplink.Packages.Instance.UpgradeTest do } describe "upgrade instance with size profile" do + setup [:setup_base_with_package_size] + setup %{app: app, metadata: metadata} do {:ok, first_deployment} = Packages.get_or_create_deployment(app, @first_deployment) @@ -871,6 +873,19 @@ defmodule Uplink.Packages.Instance.UpgradeTest do end ) + update_instance = File.read!("test/fixtures/lxd/instances/update.json") + + Bypass.expect_once( + bypass, + "PATCH", + "/1.0/instances/#{instance_slug}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, update_instance) + end + ) + Bypass.expect_once( bypass, "POST", From 21a466189600cb7b925d6e9a7baddfb84af73b06 Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Thu, 18 Jul 2024 09:54:37 +0700 Subject: [PATCH 7/8] User upserting for size profile --- lib/uplink/packages.ex | 2 +- lib/uplink/packages/instance/bootstrap.ex | 2 +- lib/uplink/packages/instance/upgrade.ex | 2 +- lib/uplink/packages/metadata/manager.ex | 57 ++-- .../lxd/profiles/show_size_profile.json | 18 ++ .../packages/instance/bootstrap_test.exs | 301 ++++++++++++++++++ 6 files changed, 360 insertions(+), 22 deletions(-) create mode 100644 test/fixtures/lxd/profiles/show_size_profile.json diff --git a/lib/uplink/packages.ex b/lib/uplink/packages.ex index 599a3395..2b232052 100644 --- a/lib/uplink/packages.ex +++ b/lib/uplink/packages.ex @@ -76,7 +76,7 @@ defmodule Uplink.Packages do defdelegate get_or_create_project_name(client, metadata), to: Metadata.Manager - defdelegate get_or_create_size_profile(cient, metadata), + defdelegate get_or_upsert_size_profile(cient, metadata), to: Metadata.Manager defdelegate profile_name(metadata), diff --git a/lib/uplink/packages/instance/bootstrap.ex b/lib/uplink/packages/instance/bootstrap.ex index 90db9f4e..3e07fc1a 100644 --- a/lib/uplink/packages/instance/bootstrap.ex +++ b/lib/uplink/packages/instance/bootstrap.ex @@ -88,7 +88,7 @@ defmodule Uplink.Packages.Instance.Bootstrap do profile_name = Packages.profile_name(metadata) size_profile_name = - Packages.get_or_create_size_profile(client, metadata) + Packages.get_or_upsert_size_profile(client, metadata) lxd_project_name = Packages.get_or_create_project_name(client, metadata) diff --git a/lib/uplink/packages/instance/upgrade.ex b/lib/uplink/packages/instance/upgrade.ex index 8adf40e5..2d833641 100644 --- a/lib/uplink/packages/instance/upgrade.ex +++ b/lib/uplink/packages/instance/upgrade.ex @@ -227,7 +227,7 @@ defmodule Uplink.Packages.Instance.Upgrade do defp handle_update_config(client, instance, metadata) do profile_name = Packages.profile_name(metadata) - size_profile_name = Packages.get_or_create_size_profile(client, metadata) + size_profile_name = Packages.get_or_upsert_size_profile(client, metadata) profiles = [profile_name, "default"] diff --git a/lib/uplink/packages/metadata/manager.ex b/lib/uplink/packages/metadata/manager.ex index 0ec809e0..915451a3 100644 --- a/lib/uplink/packages/metadata/manager.ex +++ b/lib/uplink/packages/metadata/manager.ex @@ -32,9 +32,9 @@ defmodule Uplink.Packages.Metadata.Manager do end end - def get_or_create_size_profile(_client, %Metadata{package_size: nil}), do: nil + def get_or_upsert_size_profile(_client, %Metadata{package_size: nil}), do: nil - def get_or_create_size_profile( + def get_or_upsert_size_profile( client, %Metadata{package_size: %Metadata.Size{}} = metadata ) do @@ -43,8 +43,8 @@ defmodule Uplink.Packages.Metadata.Manager do client |> Lexdee.get_profile(size_profile) |> case do - {:ok, %{body: %{"name" => name}}} -> - name + {:ok, %{body: %{"name" => _name}}} -> + update_size_profile(client, metadata) {:error, %{"error_code" => 404}} -> create_size_profile(client, metadata) @@ -77,10 +77,39 @@ defmodule Uplink.Packages.Metadata.Manager do end end - defp create_size_profile( - client, - %Metadata{package_size: package_size} = metadata - ) do + defp create_size_profile(client, %Metadata{} = metadata) do + profile_params = build_size_config(metadata) + profile_name = profile_params["name"] + + client + |> Lexdee.create_profile(profile_params) + |> case do + {:ok, %{body: nil}} -> + profile_name + + {:error, %{"error" => _message}} -> + nil + end + end + + defp update_size_profile(client, %Metadata{} = metadata) do + profile_params = build_size_config(metadata) + profile_name = profile_params["name"] + + profile_params = Map.delete(profile_params, "name") + + client + |> Lexdee.update_profile(profile_name, profile_params) + |> case do + {:ok, %{body: _body}} -> + profile_name + + {:error, %{"error" => _message}} -> + nil + end + end + + defp build_size_config(%Metadata{package_size: package_size} = metadata) do profile_name = size_profile_name(metadata) config = %{ @@ -108,22 +137,12 @@ defmodule Uplink.Packages.Metadata.Manager do config end - params = %{ + %{ "name" => profile_name, "config" => config, "description" => "Size profile for #{metadata.channel.package.organization.slug}/#{metadata.channel.package.slug}" } - - client - |> Lexdee.create_profile(params) - |> case do - {:ok, %{body: nil}} -> - profile_name - - {:error, %{"error" => _message}} -> - nil - end end defp project_name(%Metadata{channel: channel}) do diff --git a/test/fixtures/lxd/profiles/show_size_profile.json b/test/fixtures/lxd/profiles/show_size_profile.json new file mode 100644 index 00000000..cad2e6be --- /dev/null +++ b/test/fixtures/lxd/profiles/show_size_profile.json @@ -0,0 +1,18 @@ +{ + "type": "sync", + "status": "Success", + "status_code": 200, + "operation": "", + "error_code": 0, + "error": "", + "metadata": { + "name": "example.medium", + "description": "", + "config": { + "limits.cpu": "1", + "limits.memory": "1GiB" + }, + "devices": {}, + "used_by": [] + } +} \ No newline at end of file diff --git a/test/uplink/packages/instance/bootstrap_test.exs b/test/uplink/packages/instance/bootstrap_test.exs index 2febddce..55dde109 100644 --- a/test/uplink/packages/instance/bootstrap_test.exs +++ b/test/uplink/packages/instance/bootstrap_test.exs @@ -1140,5 +1140,306 @@ defmodule Uplink.Packages.Instance.BootstrapTest do assert_enqueued(worker: Uplink.Packages.Instance.Install, args: job.args) end + + test "when size already exists", %{ + bypass: bypass, + install: install, + actor: actor, + public_key_name: public_key_name, + create_instance: create_instance, + start_instance: start_instance, + exec_instance: exec_instance, + wait_for_operation: wait_for_operation, + wait_with_log: wait_with_log, + project: project_name, + size_profile: size_profile + } do + instance_slug = "test-02" + + project_not_found = + File.read!("test/fixtures/lxd/projects/not_found.json") + + Bypass.expect_once( + bypass, + "GET", + "/1.0/projects/#{project_name}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(404, project_not_found) + end + ) + + create_project = File.read!("test/fixtures/lxd/projects/create.json") + + Bypass.expect_once( + bypass, + "POST", + "/1.0/projects", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, create_project) + end + ) + + size_profile_response = + File.read!("test/fixtures/lxd/profiles/show_size_profile.json") + + Bypass.expect_once( + bypass, + "GET", + "/1.0/profiles/#{size_profile}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, size_profile_response) + end + ) + + update_size_profile = File.read!("test/fixtures/lxd/profiles/update.json") + + Bypass.expect_once( + bypass, + "PATCH", + "/1.0/profiles/#{size_profile}", + fn conn -> + {:ok, body, _} = Plug.Conn.read_body(conn) + + params = Jason.decode!(body) + + assert is_nil(params["name"]) + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, update_size_profile) + end + ) + + Bypass.expect_once(bypass, "POST", "/1.0/instances", fn conn -> + assert %{ + "target" => "ubuntu-s-1vcpu-1gb-sgp1-01", + "project" => project + } = conn.query_params + + {:ok, body, conn} = Plug.Conn.read_body(conn) + + assert %{"source" => source, "profiles" => profiles} = + Jason.decode!(body) + + assert size_profile in profiles + + assert %{"server" => server} = source + + assert server == "https://localhost/spaces/test" + + assert project == project_name + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, create_instance) + end) + + create_instance_params = Jason.decode!(create_instance) + create_instance_operation_id = create_instance_params["metadata"]["id"] + + Bypass.expect_once( + bypass, + "GET", + "/1.0/operations/#{create_instance_operation_id}/wait", + fn conn -> + assert %{"timeout" => "180"} = conn.query_params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, wait_for_operation) + end + ) + + Bypass.expect_once( + bypass, + "PUT", + "/1.0/instances/#{instance_slug}/state", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, start_instance) + end + ) + + start_instance_params = Jason.decode!(start_instance) + start_instance_operation_id = start_instance_params["metadata"]["id"] + + Bypass.expect_once( + bypass, + "GET", + "/1.0/operations/#{start_instance_operation_id}/wait", + fn conn -> + assert %{"timeout" => "180"} = conn.query_params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, wait_for_operation) + end + ) + + hostname = System.get_env("HOSTNAME") + + distribution_port = + Application.get_env(:uplink, Uplink.Internal) + |> Keyword.get(:port) + + Bypass.expect( + bypass, + "POST", + "/1.0/instances/#{instance_slug}/exec", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + assert {:ok, body, conn} = Plug.Conn.read_body(conn) + assert {:ok, %{"command" => command}} = Jason.decode(body) + + assert command in [ + [ + "/bin/sh", + "-c", + "echo 'public_key' > /etc/apk/keys/#{public_key_name}.rsa.pub\n" + ], + ["/bin/sh", "-c", "cat /etc/apk/repositories\n"], + [ + "/bin/sh", + "-c", + "echo -e 'http://#{hostname}:#{distribution_port}/distribution/develop/upmaru/something-1640927800' >> /etc/apk/repositories\n" + ] + ] + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, exec_instance) + end + ) + + setup_public_key_params = Jason.decode!(exec_instance) + setup_public_key_operation_id = setup_public_key_params["metadata"]["id"] + + Bypass.expect( + bypass, + "GET", + "/1.0/operations/#{setup_public_key_operation_id}/wait", + fn conn -> + assert %{"timeout" => _timeout} = conn.query_params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, wait_with_log) + end + ) + + Bypass.expect( + bypass, + "GET", + "/1.0/instances/#{instance_slug}/logs/stdout.log", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + conn + |> Plug.Conn.resp( + 200, + "http://#{hostname}:#{distribution_port}/distribution/develop/upmaru/something-1640927800" + ) + end + ) + + Bypass.expect( + bypass, + "GET", + "/1.0/instances/#{instance_slug}/logs/stderr.log", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + conn + |> Plug.Conn.resp(200, "") + end + ) + + Bypass.expect_once( + bypass, + "PUT", + "/1.0/instances/#{instance_slug}/state", + fn conn -> + assert %{"project" => project} = conn.query_params + + assert project == project_name + + assert {:ok, body, conn} = Plug.Conn.read_body(conn) + assert {:ok, body} = Jason.decode(body) + + assert body["action"] == "start" + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, start_instance) + end + ) + + start_instance_key_params = Jason.decode!(start_instance) + start_instance_operation_id = start_instance_key_params["metadata"]["id"] + + Bypass.expect( + bypass, + "GET", + "/1.0/operations/#{start_instance_operation_id}/wait", + fn conn -> + %{"timeout" => "180"} = conn.query_params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, wait_for_operation) + end + ) + + Bypass.expect_once( + bypass, + "POST", + "/uplink/installations/#{install.instellar_installation_id}/instances/#{instance_slug}/events", + fn conn -> + assert {:ok, body, conn} = Plug.Conn.read_body(conn) + assert {:ok, body} = Jason.decode(body) + + assert %{"event" => %{"name" => "boot" = event_name}} = body + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp( + 201, + Jason.encode!(%{ + "data" => %{"attributes" => %{"id" => 1, "name" => event_name}} + }) + ) + end + ) + + assert {:ok, job} = + perform_job(Bootstrap, %{ + instance: %{ + slug: instance_slug, + node: %{slug: "ubuntu-s-1vcpu-1gb-sgp1-01"} + }, + install_id: install.id, + actor_id: actor.id + }) + + assert_enqueued(worker: Uplink.Packages.Instance.Install, args: job.args) + end end end From 14e40ea9dadc80fa27bd728f0cbcc171aa6e96d3 Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Thu, 18 Jul 2024 11:11:46 +0700 Subject: [PATCH 8/8] Add test for validate worker for handling size profile --- lib/uplink/packages.ex | 5 +- lib/uplink/packages/install/validate.ex | 24 +- lib/uplink/packages/instance/bootstrap.ex | 3 +- lib/uplink/packages/instance/upgrade.ex | 2 +- lib/uplink/packages/metadata/manager.ex | 37 +++- .../uplink/packages/install/validate_test.exs | 207 ++++++++++++++++++ .../packages/instance/bootstrap_test.exs | 36 +-- .../uplink/packages/instance/upgrade_test.exs | 25 +-- 8 files changed, 269 insertions(+), 70 deletions(-) diff --git a/lib/uplink/packages.ex b/lib/uplink/packages.ex index 2b232052..8fc156ef 100644 --- a/lib/uplink/packages.ex +++ b/lib/uplink/packages.ex @@ -76,7 +76,10 @@ defmodule Uplink.Packages do defdelegate get_or_create_project_name(client, metadata), to: Metadata.Manager - defdelegate get_or_upsert_size_profile(cient, metadata), + defdelegate get_size_profile(metadata), + to: Metadata.Manager + + defdelegate upsert_size_profile(metadata), to: Metadata.Manager defdelegate profile_name(metadata), diff --git a/lib/uplink/packages/install/validate.ex b/lib/uplink/packages/install/validate.ex index 776abab3..0c15657e 100644 --- a/lib/uplink/packages/install/validate.ex +++ b/lib/uplink/packages/install/validate.ex @@ -11,9 +11,8 @@ defmodule Uplink.Packages.Install.Validate do alias Members.Actor - alias Packages.{ - Install - } + alias Packages.Install + alias Packages.Metadata alias Clients.LXD @@ -41,9 +40,28 @@ defmodule Uplink.Packages.Install.Validate do install |> Packages.build_install_state(actor) + |> ensure_size_profile_exists() |> ensure_profile_exists() end + defp ensure_size_profile_exists( + %{metadata: %Metadata{package_size: nil}} = state + ), + do: state + + defp ensure_size_profile_exists( + %{metadata: %Metadata{package_size: %Metadata.Size{}} = metadata} = + state + ) do + case Packages.upsert_size_profile(metadata) do + {:ok, _} -> + state + + {:error, error} -> + raise "Error: #{inspect(error)} occured when attempting to update or create size profile" + end + end + defp ensure_profile_exists(%{ install: install, metadata: metadata, diff --git a/lib/uplink/packages/instance/bootstrap.ex b/lib/uplink/packages/instance/bootstrap.ex index 3e07fc1a..b3a86b33 100644 --- a/lib/uplink/packages/instance/bootstrap.ex +++ b/lib/uplink/packages/instance/bootstrap.ex @@ -87,8 +87,7 @@ defmodule Uplink.Packages.Instance.Bootstrap do profile_name = Packages.profile_name(metadata) - size_profile_name = - Packages.get_or_upsert_size_profile(client, metadata) + size_profile_name = Packages.get_size_profile(metadata) lxd_project_name = Packages.get_or_create_project_name(client, metadata) diff --git a/lib/uplink/packages/instance/upgrade.ex b/lib/uplink/packages/instance/upgrade.ex index 2d833641..5aa155e8 100644 --- a/lib/uplink/packages/instance/upgrade.ex +++ b/lib/uplink/packages/instance/upgrade.ex @@ -227,7 +227,7 @@ defmodule Uplink.Packages.Instance.Upgrade do defp handle_update_config(client, instance, metadata) do profile_name = Packages.profile_name(metadata) - size_profile_name = Packages.get_or_upsert_size_profile(client, metadata) + size_profile_name = Packages.get_size_profile(metadata) profiles = [profile_name, "default"] diff --git a/lib/uplink/packages/metadata/manager.ex b/lib/uplink/packages/metadata/manager.ex index 915451a3..81cf96eb 100644 --- a/lib/uplink/packages/metadata/manager.ex +++ b/lib/uplink/packages/metadata/manager.ex @@ -1,5 +1,6 @@ defmodule Uplink.Packages.Metadata.Manager do alias Uplink.Packages.Metadata + alias Uplink.Clients.LXD defdelegate parse(params), to: Metadata @@ -32,14 +33,27 @@ defmodule Uplink.Packages.Metadata.Manager do end end - def get_or_upsert_size_profile(_client, %Metadata{package_size: nil}), do: nil + def get_size_profile(%Metadata{package_size: nil}), do: nil - def get_or_upsert_size_profile( - client, - %Metadata{package_size: %Metadata.Size{}} = metadata - ) do + def get_size_profile(%Metadata{} = metadata) do size_profile = size_profile_name(metadata) + LXD.client() + |> Lexdee.get_profile(size_profile) + |> case do + {:ok, %{body: %{"name" => _name}}} -> + size_profile + + {:error, %{"error_code" => 404}} -> + nil + end + end + + def upsert_size_profile(%Metadata{package_size: %Metadata.Size{}} = metadata) do + size_profile = size_profile_name(metadata) + + client = LXD.client() + client |> Lexdee.get_profile(size_profile) |> case do @@ -79,16 +93,15 @@ defmodule Uplink.Packages.Metadata.Manager do defp create_size_profile(client, %Metadata{} = metadata) do profile_params = build_size_config(metadata) - profile_name = profile_params["name"] client |> Lexdee.create_profile(profile_params) |> case do {:ok, %{body: nil}} -> - profile_name + {:ok, :size_profile_created} - {:error, %{"error" => _message}} -> - nil + {:error, %{"error" => message}} -> + {:error, message} end end @@ -102,10 +115,10 @@ defmodule Uplink.Packages.Metadata.Manager do |> Lexdee.update_profile(profile_name, profile_params) |> case do {:ok, %{body: _body}} -> - profile_name + {:ok, :size_profile_updated} - {:error, %{"error" => _message}} -> - nil + {:error, %{"error" => message}} -> + {:error, message} end end diff --git a/test/uplink/packages/install/validate_test.exs b/test/uplink/packages/install/validate_test.exs index fb9605ed..630a56a6 100644 --- a/test/uplink/packages/install/validate_test.exs +++ b/test/uplink/packages/install/validate_test.exs @@ -195,4 +195,211 @@ defmodule Uplink.Packages.Install.ValidateTest do assert install.current_state == "executing" end end + + @deployment_params_with_package_size %{ + "hash" => "some-hash-with-package-size", + "archive_url" => "http://localhost/archives/packages.zip", + "stack" => "alpine/3.14", + "channel" => "develop", + "metadata" => %{ + "id" => 1, + "slug" => "uplink-web", + "service_port" => 4000, + "exposed_port" => 49152, + "package_size" => %{ + "slug" => "medium", + "allocation" => %{ + "cpu" => 1, + "cpu_allowance" => "100%", + "cpu_priority" => 10, + "memory" => 1, + "memory_unit" => "GiB", + "memory_swap" => false, + "memory_enforce" => "hard" + } + }, + "channel" => %{ + "slug" => "develop", + "package" => %{ + "slug" => "something-1640927800", + "credential" => %{ + "public_key" => "public_key" + }, + "organization" => %{ + "slug" => "upmaru" + } + } + }, + "instances" => [ + %{ + "id" => 1, + "slug" => "something-1", + "node" => %{ + "slug" => "some-node" + } + } + ] + } + } + + describe "with package size" do + setup %{actor: actor} do + create_profile = File.read!("test/fixtures/lxd/profiles/create.json") + + metadata = Map.get(@deployment_params_with_package_size, "metadata") + + {:ok, metadata} = Packages.parse_metadata(metadata) + + app = + metadata + |> Metadata.app_slug() + |> Packages.get_or_create_app() + + {:ok, deployment} = + Packages.get_or_create_deployment( + app, + @deployment_params_with_package_size + ) + + {:ok, install} = + Packages.create_install(deployment, %{ + "installation_id" => 1, + "deployment" => @deployment_params_with_package_size + }) + + signature = compute_signature(deployment.hash) + + Cache.put( + {:deployment, signature, install.instellar_installation_id}, + metadata + ) + + {:ok, %{resource: validating_install}} = + Packages.transition_install_with(install, actor, "validate") + + size_profile = + "size.#{metadata.channel.package.organization.slug}.#{metadata.channel.package.slug}.#{validating_install.metadata_snapshot.package_size.slug}" + + {:ok, + install: validating_install, + deployment: deployment, + metadata: metadata, + create_profile: create_profile, + size_profile: size_profile} + end + + test "can create app profile and size profile", %{ + bypass: bypass, + install: install, + actor: actor, + list_profiles: list_profiles, + size_profile: size_profile + } do + size_profile_not_found = + File.read!("test/fixtures/lxd/profiles/not_found.json") + + Bypass.expect_once( + bypass, + "GET", + "/1.0/profiles/#{size_profile}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(404, size_profile_not_found) + end + ) + + create_profile = File.read!("test/fixtures/lxd/profiles/create.json") + + Bypass.expect( + bypass, + "POST", + "/1.0/profiles", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, create_profile) + end + ) + + Bypass.expect_once(bypass, "GET", "/1.0/profiles", fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, list_profiles) + end) + + assert {:ok, %{resource: install}} = + perform_job(Validate, %{ + install_id: install.id, + actor_id: actor.id + }) + + assert install.current_state == "executing" + end + + test "can update app profile and size profile", %{ + bypass: bypass, + install: install, + metadata: metadata, + actor: actor, + size_profile: size_profile + } do + list_profiles = + File.read!("test/fixtures/lxd/profiles/list_profile_exists.json") + + update_profile = File.read!("test/fixtures/lxd/profiles/update.json") + + profile_name = Packages.profile_name(metadata) + + Bypass.expect_once(bypass, "GET", "/1.0/profiles", fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, list_profiles) + end) + + size_profile_response = + File.read!("test/fixtures/lxd/profiles/show_size_profile.json") + + Bypass.expect_once( + bypass, + "GET", + "/1.0/profiles/#{size_profile}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, size_profile_response) + end + ) + + Bypass.expect_once( + bypass, + "PATCH", + "/1.0/profiles/#{profile_name}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, update_profile) + end + ) + + Bypass.expect_once( + bypass, + "PATCH", + "/1.0/profiles/#{size_profile}", + fn conn -> + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, update_profile) + end + ) + + assert {:ok, %{resource: install}} = + perform_job(Validate, %{ + install_id: install.id, + actor_id: actor.id + }) + + assert install.current_state == "executing" + end + end end diff --git a/test/uplink/packages/instance/bootstrap_test.exs b/test/uplink/packages/instance/bootstrap_test.exs index 55dde109..69ba45be 100644 --- a/test/uplink/packages/instance/bootstrap_test.exs +++ b/test/uplink/packages/instance/bootstrap_test.exs @@ -902,19 +902,6 @@ defmodule Uplink.Packages.Instance.BootstrapTest do end ) - create_size_profile = File.read!("test/fixtures/lxd/profiles/create.json") - - Bypass.expect_once( - bypass, - "POST", - "/1.0/profiles", - fn conn -> - conn - |> Plug.Conn.put_resp_header("content-type", "application/json") - |> Plug.Conn.resp(200, create_size_profile) - end - ) - Bypass.expect_once(bypass, "POST", "/1.0/instances", fn conn -> assert %{ "target" => "ubuntu-s-1vcpu-1gb-sgp1-01", @@ -923,11 +910,9 @@ defmodule Uplink.Packages.Instance.BootstrapTest do {:ok, body, conn} = Plug.Conn.read_body(conn) - assert %{"source" => source, "profiles" => profiles} = + assert %{"source" => source, "profiles" => _profiles} = Jason.decode!(body) - assert size_profile in profiles - assert %{"server" => server} = source assert server == "https://localhost/spaces/test" @@ -1197,25 +1182,6 @@ defmodule Uplink.Packages.Instance.BootstrapTest do end ) - update_size_profile = File.read!("test/fixtures/lxd/profiles/update.json") - - Bypass.expect_once( - bypass, - "PATCH", - "/1.0/profiles/#{size_profile}", - fn conn -> - {:ok, body, _} = Plug.Conn.read_body(conn) - - params = Jason.decode!(body) - - assert is_nil(params["name"]) - - conn - |> Plug.Conn.put_resp_header("content-type", "application/json") - |> Plug.Conn.resp(200, update_size_profile) - end - ) - Bypass.expect_once(bypass, "POST", "/1.0/instances", fn conn -> assert %{ "target" => "ubuntu-s-1vcpu-1gb-sgp1-01", diff --git a/test/uplink/packages/instance/upgrade_test.exs b/test/uplink/packages/instance/upgrade_test.exs index 1fb82e8f..41ebae9c 100644 --- a/test/uplink/packages/instance/upgrade_test.exs +++ b/test/uplink/packages/instance/upgrade_test.exs @@ -846,8 +846,8 @@ defmodule Uplink.Packages.Instance.UpgradeTest do end ) - size_profile_not_found = - File.read!("test/fixtures/lxd/profiles/not_found.json") + size_profile_response = + File.read!("test/fixtures/lxd/profiles/show_size_profile.json") Bypass.expect_once( bypass, @@ -856,20 +856,7 @@ defmodule Uplink.Packages.Instance.UpgradeTest do fn conn -> conn |> Plug.Conn.put_resp_header("content-type", "application/json") - |> Plug.Conn.resp(404, size_profile_not_found) - end - ) - - create_size_profile = File.read!("test/fixtures/lxd/profiles/create.json") - - Bypass.expect_once( - bypass, - "POST", - "/1.0/profiles", - fn conn -> - conn - |> Plug.Conn.put_resp_header("content-type", "application/json") - |> Plug.Conn.resp(200, create_size_profile) + |> Plug.Conn.resp(200, size_profile_response) end ) @@ -880,6 +867,12 @@ defmodule Uplink.Packages.Instance.UpgradeTest do "PATCH", "/1.0/instances/#{instance_slug}", fn conn -> + {:ok, body, conn} = Plug.Conn.read_body(conn) + + assert %{"profiles" => profiles} = Jason.decode!(body) + + assert size_profile in profiles + conn |> Plug.Conn.put_resp_header("content-type", "application/json") |> Plug.Conn.resp(200, update_instance)