diff --git a/assets/js/lib/network/index.js b/assets/js/lib/network/index.js index 5ef3d62e51..b76e43e8cc 100644 --- a/assets/js/lib/network/index.js +++ b/assets/js/lib/network/index.js @@ -68,36 +68,36 @@ function handleResponseStatus(response) { return response; } -export const post = function post(url, data) { +export const post = function post(url, data, config = null) { return networkClient - .post(url, data) + .post(url, data, config) .then(handleResponseStatus) .catch((error) => { handleError(error); }); }; -export const del = function del(url) { +export const del = function del(url, config = null) { return networkClient - .delete(url) + .delete(url, config) .then(handleResponseStatus) .catch((error) => { handleError(error); }); }; -export const put = function put(url, data) { +export const put = function put(url, data, config = null) { return networkClient - .put(url, data) + .put(url, data, config) .then(handleResponseStatus) .catch((error) => { handleError(error); }); }; -export const get = function get(url) { +export const get = function get(url, config = null) { return networkClient - .get(url) + .get(url, config) .then(handleResponseStatus) .catch((error) => { handleError(error); diff --git a/assets/js/lib/network/network.test.js b/assets/js/lib/network/network.test.js index 9b833dcad8..b1b11677b8 100644 --- a/assets/js/lib/network/network.test.js +++ b/assets/js/lib/network/network.test.js @@ -24,6 +24,22 @@ describe('networkClient', () => { clearCredentialsFromStore(); }); + it('should use default baseURL', async () => { + axiosMock.onGet('/api/v1/test').reply(200, { ok: 'ok' }); + + const response = await networkClient.get('/test'); + + expect(response.data).toEqual({ ok: 'ok' }); + }); + + it('should apply the specific config in each request', async () => { + axiosMock.onGet('/base/test').reply(200, { ok: 'ok' }); + + const response = await networkClient.get('/test', { baseURL: '/base' }); + + expect(response.data).toEqual({ ok: 'ok' }); + }); + it('should attach the access token from the store when a request is made', async () => { storeAccessToken('test-access'); diff --git a/assets/js/state/sagas/index.js b/assets/js/state/sagas/index.js index 52340d9ffc..9fac291e86 100644 --- a/assets/js/state/sagas/index.js +++ b/assets/js/state/sagas/index.js @@ -119,7 +119,9 @@ function* initialDataFetch() { yield put(stopHostsLoading()); yield put(startClustersLoading()); - const { data: clusters } = yield call(get, '/clusters'); + const { data: clusters } = yield call(get, '/clusters', { + baseURL: '/api/v2', + }); yield put(setClusters(clusters)); yield put(stopClustersLoading()); diff --git a/lib/trento/application/projectors/cluster_projector.ex b/lib/trento/application/projectors/cluster_projector.ex index 9c273c5e77..fae23adeb5 100644 --- a/lib/trento/application/projectors/cluster_projector.ex +++ b/lib/trento/application/projectors/cluster_projector.ex @@ -8,7 +8,7 @@ defmodule Trento.ClusterProjector do repo: Trento.Repo, name: "cluster_projector" - alias TrentoWeb.V1.ClusterView + alias TrentoWeb.V2.ClusterView alias Trento.Domain.Events.{ ChecksSelected, diff --git a/lib/trento_web/controllers/v2/cluster_controller.ex b/lib/trento_web/controllers/v2/cluster_controller.ex new file mode 100644 index 0000000000..a30d64d457 --- /dev/null +++ b/lib/trento_web/controllers/v2/cluster_controller.ex @@ -0,0 +1,27 @@ +defmodule TrentoWeb.V2.ClusterController do + use TrentoWeb, :controller + use OpenApiSpex.ControllerSpecs + + alias Trento.Clusters + + alias TrentoWeb.OpenApi.V2.Schema + + plug OpenApiSpex.Plug.CastAndValidate, json_render_error_v2: true + action_fallback TrentoWeb.FallbackController + + operation :list, + summary: "List Pacemaker Clusters", + tags: ["Target Infrastructure"], + description: "List all the discovered Pacemaker Clusters on the target infrastructure", + responses: [ + ok: + {"A collection of the discovered Pacemaker Clusters", "application/json", + Schema.Cluster.PacemakerClustersCollection} + ] + + def list(conn, _) do + clusters = Clusters.get_all_clusters() + + render(conn, "clusters.json", clusters: clusters) + end +end diff --git a/lib/trento_web/openapi/v1/schema/cluster.ex b/lib/trento_web/openapi/v1/schema/cluster.ex index 91aa30366d..45d9a17993 100644 --- a/lib/trento_web/openapi/v1/schema/cluster.ex +++ b/lib/trento_web/openapi/v1/schema/cluster.ex @@ -2,7 +2,6 @@ defmodule TrentoWeb.OpenApi.V1.Schema.Cluster do @moduledoc false require OpenApiSpex - require Trento.Domain.Enums.ClusterType, as: ClusterType alias OpenApiSpex.Schema @@ -37,13 +36,12 @@ defmodule TrentoWeb.OpenApi.V1.Schema.Cluster do site: %Schema{type: :string}, hana_status: %Schema{type: :string}, attributes: %Schema{ - title: "ClusterNodeAttributes", - type: :array, - items: %Schema{type: :string} + type: :object, + description: "Node attributes", + additionalProperties: %Schema{type: :string} }, virtual_ip: %Schema{type: :string}, resources: %Schema{ - title: "ClustrNodeResources", description: "A list of Cluster resources", type: :array, items: ClusterResource @@ -57,7 +55,7 @@ defmodule TrentoWeb.OpenApi.V1.Schema.Cluster do OpenApiSpex.schema(%{ title: "SbdDevice", - description: "Ad Sbd Device", + description: "SBD Device", type: :object, properties: %{ device: %Schema{type: :string}, @@ -83,22 +81,20 @@ defmodule TrentoWeb.OpenApi.V1.Schema.Cluster do sr_health_state: %Schema{type: :string, description: "SR health state"}, fencing_type: %Schema{type: :string, description: "Fencing Type"}, stopped_resources: %Schema{ - title: "ClusterResource", description: "A list of the stopped resources on this HANA Cluster", type: :array, items: ClusterResource }, nodes: %Schema{ - title: "HanaClusterNodes", type: :array, items: HanaClusterNode }, sbd_devices: %Schema{ - title: "SbdDevice", type: :array, items: SbdDevice } - } + }, + required: [:nodes] }) end @@ -106,7 +102,7 @@ defmodule TrentoWeb.OpenApi.V1.Schema.Cluster do @moduledoc false OpenApiSpex.schema(%{ - title: "PacemakerClusterDetail", + title: "PacemakerClusterDetails", description: "Details of the detected PacemakerCluster", nullable: true, oneOf: [ @@ -135,11 +131,11 @@ defmodule TrentoWeb.OpenApi.V1.Schema.Cluster do type: %Schema{ type: :string, description: "Detected type of the cluster", - enum: ClusterType.values() + enum: [:hana_scale_up, :hana_scale_out, :unknown] }, selected_checks: %Schema{ title: "SelectedChecks", - description: "A list ids of the checks selected for execution on this cluster", + description: "A list of check ids selected for an execution on this cluster", type: :array, items: %Schema{type: :string} }, diff --git a/lib/trento_web/openapi/v2/api_spec.ex b/lib/trento_web/openapi/v2/api_spec.ex new file mode 100644 index 0000000000..fd7908b28f --- /dev/null +++ b/lib/trento_web/openapi/v2/api_spec.ex @@ -0,0 +1,8 @@ +defmodule TrentoWeb.OpenApi.V2.ApiSpec do + @moduledoc """ + OpenApi specification entry point for V2 version + """ + + use TrentoWeb.OpenApi.ApiSpec, + api_version: "v2" +end diff --git a/lib/trento_web/openapi/v2/schema/cluster.ex b/lib/trento_web/openapi/v2/schema/cluster.ex new file mode 100644 index 0000000000..81f8fbc26f --- /dev/null +++ b/lib/trento_web/openapi/v2/schema/cluster.ex @@ -0,0 +1,176 @@ +defmodule TrentoWeb.OpenApi.V2.Schema.Cluster do + @moduledoc false + + require OpenApiSpex + require Trento.Domain.Enums.ClusterType, as: ClusterType + + alias OpenApiSpex.Schema + + alias TrentoWeb.OpenApi.V1.Schema.{Cluster, Provider, ResourceHealth, Tags} + + defmodule AscsErsClusterNode do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "AscsErsClusterNode", + description: "ASCS/ERS Cluster Node", + type: :object, + properties: %{ + attributes: %Schema{ + type: :object, + description: "Node attributes", + additionalProperties: %Schema{type: :string} + }, + filesystems: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: "List of filesystems managed in this node" + }, + name: %Schema{ + type: :string, + description: "Node name" + }, + resources: %Schema{ + type: :array, + items: Cluster.ClusterResource, + description: "A list of Cluster resources" + }, + roles: %Schema{ + type: :array, + items: %Schema{type: :string, enum: ["ascs", "ers"]}, + description: "List of roles managed in this node" + }, + virtual_ips: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: "List of virtual IPs managed in this node" + } + } + }) + end + + defmodule AscsErsClusterSAPSystem do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "AscsErsClusterSAPSystem", + description: "SAP system managed by a ASCS/ERS cluster", + type: :object, + properties: %{ + distributed: %Schema{ + type: :boolean, + description: "ASCS and ERS instances are distributed and running in different nodes" + }, + filesystem_resource_based: %Schema{ + type: :boolean, + description: + "ASCS and ERS filesystems are handled by the cluster with the Filesystem resource agent" + }, + nodes: %Schema{ + type: :array, + items: AscsErsClusterNode, + description: "List of ASCS/ERS nodes for this SAP system" + } + } + }) + end + + defmodule AscsErsClusterDetails do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "AscsErsClusterDetails", + description: "Details of a ASCS/ERS Pacemaker Cluster", + type: :object, + properties: %{ + fencing_type: %Schema{ + type: :string, + description: "Fencing type" + }, + sap_systems: %Schema{ + type: :array, + items: AscsErsClusterSAPSystem, + description: "List of managed SAP systems in a single or multi SID cluster" + }, + sbd_devices: %Schema{ + type: :array, + items: Cluster.SbdDevice, + description: "List of SBD devices used in the cluster" + }, + stopped_resources: %Schema{ + type: :array, + items: Cluster.ClusterResource, + description: "List of the stopped resources on this HANA Cluster" + } + }, + required: [:sap_systems] + }) + end + + defmodule Details do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "PacemakerClusterDetails", + description: "Details of the detected PacemakerCluster", + nullable: true, + oneOf: [ + AscsErsClusterDetails, + Cluster.HanaClusterDetails + ] + }) + end + + defmodule PacemakerCluster do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "PacemakerCluster", + description: "A discovered Pacemaker Cluster on the target infrastructure", + type: :object, + properties: %{ + id: %Schema{type: :string, description: "Cluster ID", format: :uuid}, + name: %Schema{type: :string, description: "Cluster name"}, + sid: %Schema{type: :string, description: "SID"}, + additional_sids: %Schema{ + type: :array, + items: %Schema{type: :string}, + description: "Additionally discovered SIDs, such as ASCS/ERS cluster SIDs" + }, + provider: Provider.SupportedProviders, + type: %Schema{ + type: :string, + description: "Detected type of the cluster", + enum: ClusterType.values() + }, + selected_checks: %Schema{ + title: "SelectedChecks", + description: "A list of check ids selected for an execution on this cluster", + type: :array, + items: %Schema{type: :string} + }, + health: ResourceHealth, + resources_number: %Schema{type: :integer, description: "Resource number", nullable: true}, + hosts_number: %Schema{type: :integer, description: "Hosts number", nullable: true}, + cib_last_written: %Schema{ + type: :string, + description: "CIB last written date", + nullable: true + }, + details: Details, + tags: Tags + } + }) + end + + defmodule PacemakerClustersCollection do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "PacemakerClustersCollection", + description: "A list of the discovered Pacemaker Clusters", + type: :array, + items: PacemakerCluster + }) + end +end diff --git a/lib/trento_web/router.ex b/lib/trento_web/router.ex index 444bff222a..9084bb4d43 100644 --- a/lib/trento_web/router.ex +++ b/lib/trento_web/router.ex @@ -3,7 +3,7 @@ defmodule TrentoWeb.Router do use Pow.Phoenix.Router # From newest to oldest - @available_api_versions ["v1"] + @available_api_versions ["v2", "v1"] pipeline :browser do plug :accepts, ["html"] @@ -24,6 +24,11 @@ defmodule TrentoWeb.Router do plug OpenApiSpex.Plug.PutApiSpec, module: TrentoWeb.OpenApi.V1.ApiSpec end + pipeline :api_v2 do + plug :api + plug OpenApiSpex.Plug.PutApiSpec, module: TrentoWeb.OpenApi.V2.ApiSpec + end + pipeline :protected_api do plug Unplug, if: {Unplug.Predicates.AppConfigEquals, {:trento, :jwt_authentication_enabled, true}}, @@ -44,7 +49,8 @@ defmodule TrentoWeb.Router do get "/api/doc", OpenApiSpex.Plug.SwaggerUI, path: "/api/v1/openapi", urls: [ - %{url: "/api/v1/openapi", name: "Version 1"} + %{url: "/api/v1/openapi", name: "Version 1"}, + %{url: "/api/v2/openapi", name: "Version 2"} ] end @@ -119,6 +125,12 @@ defmodule TrentoWeb.Router do get "/hosts/:id/exporters_status", PrometheusController, :exporters_status end + + scope "/v2", TrentoWeb.V2 do + pipe_through [:api_v2] + + get "/clusters", ClusterController, :list + end end scope "/api" do @@ -149,6 +161,11 @@ defmodule TrentoWeb.Router do pipe_through :api_v1 get "/openapi", OpenApiSpex.Plug.RenderSpec, [] end + + scope "/v2" do + pipe_through :api_v2 + get "/openapi", OpenApiSpex.Plug.RenderSpec, [] + end end scope "/api" do diff --git a/lib/trento_web/views/v1/cluster_view.ex b/lib/trento_web/views/v1/cluster_view.ex index ced5875afe..856c0d4890 100644 --- a/lib/trento_web/views/v1/cluster_view.ex +++ b/lib/trento_web/views/v1/cluster_view.ex @@ -9,6 +9,7 @@ defmodule TrentoWeb.V1.ClusterView do cluster |> Map.from_struct() |> Map.delete(:__meta__) + |> adapt_v1() end def render("cluster_registered.json", %{cluster: cluster}) do @@ -22,30 +23,12 @@ defmodule TrentoWeb.V1.ClusterView do |> Map.put(:id, data.cluster_id) end - def render("settings.json", %{settings: settings}) do - render_many(settings, __MODULE__, "setting.json", as: :setting) - end + defp adapt_v1(%{type: type} = cluster) when type in [:hana_scale_up, :hana_scale_out, :unknown], + do: cluster - def render("setting.json", %{ - setting: %{ - host_id: host_id, - hostname: hostname, - user: user, - provider_data: provider_data - } - }) do - %{ - host_id: host_id, - hostname: hostname, - user: user, - default_user: determine_default_connection_user(provider_data) - } + defp adapt_v1(cluster) do + cluster + |> Map.replace(:type, :unknown) + |> Map.replace(:details, nil) end - - defp determine_default_connection_user(%{ - "admin_username" => admin_username - }), - do: admin_username - - defp determine_default_connection_user(_), do: "root" end diff --git a/lib/trento_web/views/v2/cluster_view.ex b/lib/trento_web/views/v2/cluster_view.ex new file mode 100644 index 0000000000..c4361982d4 --- /dev/null +++ b/lib/trento_web/views/v2/cluster_view.ex @@ -0,0 +1,24 @@ +defmodule TrentoWeb.V2.ClusterView do + use TrentoWeb, :view + + def render("clusters.json", %{clusters: clusters}) do + render_many(clusters, __MODULE__, "cluster.json") + end + + def render("cluster.json", %{cluster: cluster}) do + cluster + |> Map.from_struct() + |> Map.delete(:__meta__) + end + + def render("cluster_registered.json", %{cluster: cluster}) do + Map.delete(render("cluster.json", %{cluster: cluster}), :tags) + end + + def render("cluster_details_updated.json", %{data: data}) do + data + |> Map.from_struct() + |> Map.delete(:cluster_id) + |> Map.put(:id, data.cluster_id) + end +end diff --git a/test/support/factory.ex b/test/support/factory.ex index c43f238152..112cf8f9d8 100644 --- a/test/support/factory.ex +++ b/test/support/factory.ex @@ -8,6 +8,9 @@ defmodule Trento.Factory do require Trento.Domain.Enums.Health, as: Health alias Trento.Domain.{ + AscsErsClusterDetails, + AscsErsClusterNode, + AscsErsClusterSapSystem, ClusterResource, HanaClusterDetails, HanaClusterNode, @@ -304,6 +307,54 @@ defmodule Trento.Factory do } end + def ascs_ers_cluster_node_factory do + %AscsErsClusterNode{ + name: Faker.Pokemon.name(), + roles: [Enum.random(["ascs", "ers"])], + virtual_ips: [Faker.Internet.ip_v4_address()], + filesystems: [Faker.File.file_name()], + attributes: %{ + Faker.Pokemon.name() => Faker.Pokemon.name() + }, + resources: build_list(5, :cluster_resource) + } + end + + def ascs_ers_cluster_sap_system_factory do + %AscsErsClusterSapSystem{ + sid: sequence(:sid, &"PR#{&1}"), + filesystem_resource_based: Enum.random([false, true]), + distributed: Enum.random([false, true]), + nodes: build_list(2, :ascs_ers_cluster_node) + } + end + + def sbd_device_factory do + %SbdDevice{ + device: Faker.File.file_name(), + status: Enum.random(["healthy", "unhealthy"]) + } + end + + def cluster_resource_factory do + %ClusterResource{ + id: Faker.UUID.v4(), + type: Faker.StarWars.planet(), + role: Faker.Beer.hop(), + status: Faker.Pokemon.name(), + fail_count: Enum.random(0..100) + } + end + + def ascs_ers_cluster_details_factory do + %AscsErsClusterDetails{ + fencing_type: Faker.Beer.hop(), + sap_systems: build_list(2, :ascs_ers_cluster_sap_system), + sbd_devices: build_list(2, :sbd_device), + stopped_resources: build_list(2, :cluster_resource) + } + end + def database_factory do %DatabaseReadModel{ id: Faker.UUID.v4(), diff --git a/test/trento_web/controllers/v2/cluster_controller_test.exs b/test/trento_web/controllers/v2/cluster_controller_test.exs new file mode 100644 index 0000000000..99d7fead9e --- /dev/null +++ b/test/trento_web/controllers/v2/cluster_controller_test.exs @@ -0,0 +1,24 @@ +defmodule TrentoWeb.V2.ClusterControllerTest do + use TrentoWeb.ConnCase, async: true + + import OpenApiSpex.TestAssertions + import Mox + import Trento.Factory + + alias TrentoWeb.OpenApi.V2.ApiSpec + + setup [:set_mox_from_context, :verify_on_exit!] + + describe "list" do + test "should be compliant with ASCS/ERS clusters schema", %{conn: conn} do + insert(:cluster, details: build(:ascs_ers_cluster_details)) + + api_spec = ApiSpec.spec() + + conn + |> get("/api/v2/clusters") + |> json_response(200) + |> assert_schema("PacemakerClustersCollection", api_spec) + end + end +end diff --git a/test/trento_web/views/v1/cluster_view_test.exs b/test/trento_web/views/v1/cluster_view_test.exs new file mode 100644 index 0000000000..9241a31951 --- /dev/null +++ b/test/trento_web/views/v1/cluster_view_test.exs @@ -0,0 +1,15 @@ +defmodule TrentoWeb.V1.ClusterViewTest do + use TrentoWeb.ConnCase, async: true + + import Phoenix.View + import Trento.Factory + + alias TrentoWeb.V1.ClusterView + + test "should adapt the cluster view to V1 version" do + cluster = build(:cluster, type: :ascs_ers, details: build(:ascs_ers_cluster_details)) + + assert %{type: :unknown, details: nil} = + render(ClusterView, "cluster.json", %{cluster: cluster}) + end +end