From bc8d0cc616ee01bc866690d19d07dc8d0a6629b7 Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Tue, 7 Jan 2025 17:55:19 +0700 Subject: [PATCH 1/4] Setup endpoint and test case --- lib/uplink/nodes/router.ex | 28 ++++++++++++++++++++++++++++ test/uplink/nodes/router_test.exs | 20 ++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 lib/uplink/nodes/router.ex create mode 100644 test/uplink/nodes/router_test.exs diff --git a/lib/uplink/nodes/router.ex b/lib/uplink/nodes/router.ex new file mode 100644 index 0000000..b3edc38 --- /dev/null +++ b/lib/uplink/nodes/router.ex @@ -0,0 +1,28 @@ +defmodule Uplink.Nodes.Router do + use Plug.Router + use Uplink.Web + + alias Uplink.Secret + alias Uplink.Clients.LXD + + plug :match + + plug Plug.Parsers, + parsers: [:urlencoded, :json], + body_reader: {Uplink.Web.CacheBodyReader, :read_body, []}, + json_decoder: Jason + + plug Secret.VerificationPlug + + plug :dispatch + + get "/" do + nodes = + LXD.list_cluster_members() + |> Enum.map(fn member -> + LXD.get_node(member.server_name) + end) + + json(conn, :ok, %{data: nodes}) + end +end diff --git a/test/uplink/nodes/router_test.exs b/test/uplink/nodes/router_test.exs new file mode 100644 index 0000000..7303322 --- /dev/null +++ b/test/uplink/nodes/router_test.exs @@ -0,0 +1,20 @@ +defmodule Uplink.Nodes.RouterTest do + use ExUnit.Case + use Plug.Test + + setup do + bypass = Bypass.open() + + Cache.put(:self, %{ + "credential" => %{ + "endpoint" => "http://localhost:#{bypass.port}" + } + }) + + response = File.read!("test/fixtures/lxd/cluster/members/list.json") + + Cache.delete(:cluster_members) + + {:ok, bypass: bypass, response: response} + end +end From e74b97410535a117992634d04bfc93ed3de3bc49 Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Tue, 7 Jan 2025 18:31:55 +0700 Subject: [PATCH 2/4] Setup nodes router --- lib/uplink/clients/lxd/node.ex | 2 + lib/uplink/nodes/router.ex | 2 +- lib/uplink/router.ex | 2 + test/fixtures/lxd/resources/show.json | 263 ++++++++++++++++++++++++++ test/uplink/nodes/router_test.exs | 64 ++++++- 5 files changed, 330 insertions(+), 3 deletions(-) create mode 100644 test/fixtures/lxd/resources/show.json diff --git a/lib/uplink/clients/lxd/node.ex b/lib/uplink/clients/lxd/node.ex index df12dcf..d1e840d 100644 --- a/lib/uplink/clients/lxd/node.ex +++ b/lib/uplink/clients/lxd/node.ex @@ -2,6 +2,8 @@ defmodule Uplink.Clients.LXD.Node do use Ecto.Schema import Ecto.Changeset + @derive Jason.Encoder + @primary_key false embedded_schema do field :name, :string diff --git a/lib/uplink/nodes/router.ex b/lib/uplink/nodes/router.ex index b3edc38..e3c49db 100644 --- a/lib/uplink/nodes/router.ex +++ b/lib/uplink/nodes/router.ex @@ -16,7 +16,7 @@ defmodule Uplink.Nodes.Router do plug :dispatch - get "/" do + post "/" do nodes = LXD.list_cluster_members() |> Enum.map(fn member -> diff --git a/lib/uplink/router.ex b/lib/uplink/router.ex index 208dc4d..6bb9408 100644 --- a/lib/uplink/router.ex +++ b/lib/uplink/router.ex @@ -6,6 +6,7 @@ defmodule Uplink.Router do alias Uplink.Installations alias Uplink.Cache alias Uplink.Monitors + alias Uplink.Nodes alias Uplink.Packages.{ Instance, @@ -27,6 +28,7 @@ defmodule Uplink.Router do forward "/components", to: Components.Router forward "/cache", to: Cache.Router forward "/monitors", to: Monitors.Router + forward "/nodes", to: Nodes.Router match _ do send_resp(conn, 404, "not found") diff --git a/test/fixtures/lxd/resources/show.json b/test/fixtures/lxd/resources/show.json new file mode 100644 index 0000000..2287136 --- /dev/null +++ b/test/fixtures/lxd/resources/show.json @@ -0,0 +1,263 @@ +{ + "cpu": { + "architecture": "x86_64", + "sockets": [ + { + "cache": [ + { + "level": 1, + "size": 32768, + "type": "Data" + }, + { + "level": 1, + "size": 65536, + "type": "Instruction" + }, + { + "level": 2, + "size": 524288, + "type": "Unified" + }, + { + "level": 3, + "size": 8388608, + "type": "Unified" + } + ], + "cores": [ + { + "core": 0, + "die": 0, + "threads": [ + { + "id": 0, + "isolated": false, + "numa_node": 0, + "online": true, + "thread": 0 + }, + { + "id": 1, + "isolated": false, + "numa_node": 0, + "online": true, + "thread": 1 + } + ] + } + ], + "name": "AMD EPYC 7571", + "socket": 0, + "vendor": "AuthenticAMD" + } + ], + "total": 2 + }, + "gpu": { + "cards": [ + { + "numa_node": 0, + "pci_address": "0000:00:03.0", + "product_id": "1111", + "vendor": "Amazon.com, Inc.", + "vendor_id": "1d0f" + } + ], + "total": 1 + }, + "memory": { + "hugepages_size": 2097152, + "hugepages_total": 0, + "hugepages_used": 0, + "nodes": [ + { + "hugepages_total": 0, + "hugepages_used": 0, + "numa_node": 0, + "total": 4294967296, + "used": 3809120256 + } + ], + "total": 4294967296, + "used": 2107842560 + }, + "network": { + "cards": [ + { + "driver": "ena", + "driver_version": "6.5.0-1023-aws", + "numa_node": 0, + "pci_address": "0000:00:05.0", + "ports": [ + { + "address": "06:b1:cc:28:99:2d", + "auto_negotiation": false, + "id": "ens5", + "link_detected": true, + "port": 0, + "protocol": "ethernet" + } + ], + "product": "Elastic Network Adapter (ENA)", + "product_id": "ec20", + "vendor": "Amazon.com, Inc.", + "vendor_id": "1d0f" + } + ], + "total": 1 + }, + "pci": { + "devices": [ + { + "driver": "", + "driver_version": "", + "iommu_group": 0, + "numa_node": 0, + "pci_address": "0000:00:00.0", + "product": "440FX - 82441FX PMC [Natoma]", + "product_id": "1237", + "vendor": "Intel Corporation", + "vendor_id": "8086", + "vpd": {} + }, + { + "driver": "", + "driver_version": "", + "iommu_group": 0, + "numa_node": 0, + "pci_address": "0000:00:01.0", + "product": "82371SB PIIX3 ISA [Natoma/Triton II]", + "product_id": "7000", + "vendor": "Intel Corporation", + "vendor_id": "8086", + "vpd": {} + }, + { + "driver": "", + "driver_version": "", + "iommu_group": 0, + "numa_node": 0, + "pci_address": "0000:00:01.3", + "product": "82371AB/EB/MB PIIX4 ACPI", + "product_id": "7113", + "vendor": "Intel Corporation", + "vendor_id": "8086", + "vpd": {} + }, + { + "driver": "", + "driver_version": "", + "iommu_group": 0, + "numa_node": 0, + "pci_address": "0000:00:03.0", + "product": "", + "product_id": "1111", + "vendor": "Amazon.com, Inc.", + "vendor_id": "1d0f", + "vpd": {} + }, + { + "driver": "nvme", + "driver_version": "6.5.0-1023-aws", + "iommu_group": 0, + "numa_node": 0, + "pci_address": "0000:00:04.0", + "product": "", + "product_id": "8061", + "vendor": "Amazon.com, Inc.", + "vendor_id": "1d0f", + "vpd": {} + }, + { + "driver": "ena", + "driver_version": "6.5.0-1023-aws", + "iommu_group": 0, + "numa_node": 0, + "pci_address": "0000:00:05.0", + "product": "Elastic Network Adapter (ENA)", + "product_id": "ec20", + "vendor": "Amazon.com, Inc.", + "vendor_id": "1d0f", + "vpd": {} + } + ], + "total": 6 + }, + "storage": { + "disks": [ + { + "block_size": 512, + "device": "259:0", + "device_id": "nvme-nvme.1d0f-766f6c3038346564376336353263323933646366-416d617a6f6e20456c617374696320426c6f636b2053746f7265-00000001", + "device_path": "pci-0000:00:04.0-nvme-1", + "firmware_version": "1.0", + "id": "nvme0n1", + "model": "Amazon Elastic Block Store", + "numa_node": 0, + "partitions": [ + { + "device": "259:1", + "id": "nvme0n1p1", + "partition": 1, + "read_only": false, + "size": 42833264128 + }, + { + "device": "259:2", + "id": "nvme0n1p14", + "partition": 14, + "read_only": false, + "size": 4194304 + }, + { + "device": "259:3", + "id": "nvme0n1p15", + "partition": 15, + "read_only": false, + "size": 111149056 + } + ], + "read_only": false, + "removable": false, + "rpm": 0, + "serial": "vol084ed7c652c293dcf", + "size": 42949672960, + "type": "nvme", + "wwn": "nvme.1d0f-766f6c3038346564376336353263323933646366-416d617a6f6e20456c617374696320426c6f636b2053746f7265-00000001" + } + ], + "total": 4 + }, + "system": { + "chassis": { + "serial": "", + "type": "Other", + "vendor": "Amazon EC2", + "version": "" + }, + "family": "", + "firmware": { + "date": "10/16/2017", + "vendor": "Amazon EC2", + "version": "1.0" + }, + "motherboard": { + "product": "", + "serial": "", + "vendor": "Amazon EC2", + "version": "" + }, + "product": "t3a.medium", + "serial": "ec20b7a9-497a-9279-cba6-7417800214a3", + "sku": "", + "type": "virtual-machine", + "uuid": "ec20b7a9-497a-9279-cba6-7417800214a3", + "vendor": "Amazon EC2", + "version": "" + }, + "usb": { + "devices": [], + "total": 0 + } +} diff --git a/test/uplink/nodes/router_test.exs b/test/uplink/nodes/router_test.exs index 7303322..5b1f570 100644 --- a/test/uplink/nodes/router_test.exs +++ b/test/uplink/nodes/router_test.exs @@ -2,6 +2,19 @@ defmodule Uplink.Nodes.RouterTest do use ExUnit.Case use Plug.Test + alias Uplink.Cache + alias Uplink.Nodes.Router + + @opts Router.init([]) + + @valid_body Jason.encode!(%{ + "actor" => %{ + "provider" => "instellar", + "identifier" => "zacksiri", + "id" => "1" + } + }) + setup do bypass = Bypass.open() @@ -11,10 +24,57 @@ defmodule Uplink.Nodes.RouterTest do } }) - response = File.read!("test/fixtures/lxd/cluster/members/list.json") + members_response = File.read!("test/fixtures/lxd/cluster/members/list.json") + + resource_response = File.read!("test/fixtures/lxd/resources/show.json") Cache.delete(:cluster_members) - {:ok, bypass: bypass, response: response} + {:ok, + bypass: bypass, + members_response: members_response, + resource_response: resource_response} + end + + describe "list nodes" do + setup do + signature = + :crypto.mac(:hmac, :sha256, Uplink.Secret.get(), @valid_body) + |> Base.encode16() + |> String.downcase() + + {:ok, signature: signature} + end + + test "can successfully fetch list of nodes", %{ + bypass: bypass, + signature: signature, + members_response: members_response, + resource_response: resource_response + } do + Bypass.expect_once(bypass, "GET", "/1.0/cluster/members", fn conn -> + assert %{"recursion" => "1"} = conn.query_params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, members_response) + end) + + Bypass.expect_once(bypass, "GET", "/1.0/resources", fn conn -> + assert %{"target" => "ubuntu-s-1vcpu-1gb-sgp1-01"} = conn.params + + conn + |> Plug.Conn.put_resp_header("content-type", "application/json") + |> Plug.Conn.resp(200, resource_response) + end) + + conn = + conn(:post, "/", @valid_body) + |> put_req_header("x-uplink-signature-256", "sha256=#{signature}") + |> put_req_header("content-type", "application/json") + |> Router.call(@opts) + + assert conn.status == 200 + end end end From 201802ff65747f05ed180dbf23d38773d93890a7 Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Tue, 7 Jan 2025 20:39:13 +0700 Subject: [PATCH 3/4] Add json structure assertion for nodes endpoint --- lib/uplink/nodes/router.ex | 2 +- test/uplink/nodes/router_test.exs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/lib/uplink/nodes/router.ex b/lib/uplink/nodes/router.ex index e3c49db..115b182 100644 --- a/lib/uplink/nodes/router.ex +++ b/lib/uplink/nodes/router.ex @@ -23,6 +23,6 @@ defmodule Uplink.Nodes.Router do LXD.get_node(member.server_name) end) - json(conn, :ok, %{data: nodes}) + json(conn, :ok, nodes) end end diff --git a/test/uplink/nodes/router_test.exs b/test/uplink/nodes/router_test.exs index 5b1f570..5ae5cc4 100644 --- a/test/uplink/nodes/router_test.exs +++ b/test/uplink/nodes/router_test.exs @@ -74,6 +74,17 @@ defmodule Uplink.Nodes.RouterTest do |> put_req_header("content-type", "application/json") |> Router.call(@opts) + assert %{"data" => nodes} = Jason.decode!(conn.resp_body) + + [node] = nodes + + %{ + "cpu_cores_count" => _, + "name" => _, + "total_memory" => _, + "total_storage" => _ + } = node + assert conn.status == 200 end end From 8ed6a39ed6ba9914eee3aea190220a9fe34f67c8 Mon Sep 17 00:00:00 2001 From: Zack Siri Date: Tue, 7 Jan 2025 20:39:32 +0700 Subject: [PATCH 4/4] Add assertion keyword --- test/uplink/nodes/router_test.exs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/uplink/nodes/router_test.exs b/test/uplink/nodes/router_test.exs index 5ae5cc4..6ba0336 100644 --- a/test/uplink/nodes/router_test.exs +++ b/test/uplink/nodes/router_test.exs @@ -78,12 +78,12 @@ defmodule Uplink.Nodes.RouterTest do [node] = nodes - %{ - "cpu_cores_count" => _, - "name" => _, - "total_memory" => _, - "total_storage" => _ - } = node + assert %{ + "cpu_cores_count" => _, + "name" => _, + "total_memory" => _, + "total_storage" => _ + } = node assert conn.status == 200 end