diff --git a/.formatter.exs b/.formatter.exs index b1f5c1f8a7..fda38b8adc 100644 --- a/.formatter.exs +++ b/.formatter.exs @@ -1,5 +1,5 @@ [ - import_deps: [:ecto, :phoenix, :commanded], + import_deps: [:ecto, :phoenix, :commanded, :open_api_spex], locals_without_parens: [ # mock assert_called: :*, diff --git a/lib/trento_web/controllers/host_controller.ex b/lib/trento_web/controllers/host_controller.ex index 68204c1686..4021245ea9 100644 --- a/lib/trento_web/controllers/host_controller.ex +++ b/lib/trento_web/controllers/host_controller.ex @@ -8,8 +8,20 @@ defmodule TrentoWeb.HostController do alias Trento.Support.StructHelper - @spec list(Plug.Conn.t(), map) :: Plug.Conn.t() + use OpenApiSpex.ControllerSpecs + + tags ["Landscape"] + + operation :list, + summary: "List hosts", + description: "List all the discovered hosts on the target infrastructure", + responses: [ + ok: + {"A collection of the discovered hosts", "application/json", + TrentoWeb.OpenApi.Schema.Host.HostsCollection} + ] + @spec list(Plug.Conn.t(), map) :: Plug.Conn.t() def list(conn, _) do # TODO: replace to_map with DTO approach hosts = Hosts.get_all_hosts() |> StructHelper.to_map() @@ -17,6 +29,8 @@ defmodule TrentoWeb.HostController do json(conn, hosts) end + operation :heartbeat, false + def heartbeat(conn, %{"id" => id}) do case Heartbeats.heartbeat(id) do {:ok, _} -> diff --git a/lib/trento_web/openapi/api_spec.ex b/lib/trento_web/openapi/api_spec.ex new file mode 100644 index 0000000000..b2e9fd5df5 --- /dev/null +++ b/lib/trento_web/openapi/api_spec.ex @@ -0,0 +1,34 @@ +defmodule TrentoWeb.OpenApi.ApiSpec do + @moduledoc """ + OpenApi specification entry point + """ + + alias OpenApiSpex.{Info, OpenApi, Paths, Server, Tag} + alias TrentoWeb.{Endpoint, Router} + @behaviour OpenApi + + @impl OpenApi + def spec do + %OpenApi{ + servers: [ + # Populate the Server info from a phoenix endpoint + Server.from_endpoint(Endpoint) + ], + info: %Info{ + title: "Trento", + description: to_string(Application.spec(:trento, :description)), + version: to_string(Application.spec(:trento, :vsn)) + }, + # Populate the paths from a phoenix router + paths: Paths.from_router(Router), + tags: [ + %Tag{ + name: "Landscape", + description: "Providing access to the discovered target infrastructure" + } + ] + } + # Discover request/response schemas from path specs + |> OpenApiSpex.resolve_schema_modules() + end +end diff --git a/lib/trento_web/openapi/schema/host.ex b/lib/trento_web/openapi/schema/host.ex new file mode 100644 index 0000000000..616cfe6b95 --- /dev/null +++ b/lib/trento_web/openapi/schema/host.ex @@ -0,0 +1,95 @@ +defmodule TrentoWeb.OpenApi.Schema.Host do + @moduledoc false + + require OpenApiSpex + alias OpenApiSpex.Schema + + defmodule IPv4 do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "IPv4", + type: :string, + format: :ipv4 + }) + end + + defmodule IPv6 do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "IPv6", + type: :string, + format: :ipv6 + }) + end + + defmodule HostItem do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "Host", + description: "A discovered host on the target infrastructure", + type: :object, + properties: %{ + id: %Schema{type: :integer, description: "Host ID"}, + hostname: %Schema{type: :string, description: "Host name"}, + ip_addresses: %Schema{ + type: :array, + description: "IP addresses", + items: %Schema{ + title: "IP address", + oneOf: [ + IPv4, + IPv6 + ] + } + }, + ssh_address: TrentoWeb.OpenApi.Schema.Host.IPv4, + agent_version: %Schema{ + type: :string, + description: "Version of the agent installed on the host" + }, + cluster_id: %Schema{ + type: :string, + description: "Identifier of the cluster this host is part of", + format: :uuid + }, + heartbeat: %Schema{ + type: :string, + description: "Host's last heartbeat status", + enum: [:critical, :passing, :unknown] + }, + provider: %Schema{ + type: :string, + description: "Detected Provider on which the host is running", + enum: [:azure, :aws, :gcp, :unknown] + }, + provider_data: TrentoWeb.OpenApi.Schema.Provider.ProviderData, + tags: %Schema{ + title: "Tags", + description: "A list of tags attached to a resource", + type: :array, + items: TrentoWeb.OpenApi.Schema.Tag + }, + sles_subscriptions: %Schema{ + title: "SlesSubscriptions", + description: "A list of the available SLES Subscriptions on a host", + type: :array, + items: TrentoWeb.OpenApi.Schema.SlesSubscription + } + } + }) + end + + defmodule HostsCollection do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "HostsCollection", + description: "A list of the discovered hosts", + type: :array, + items: HostItem + }) + end +end diff --git a/lib/trento_web/openapi/schema/provider.ex b/lib/trento_web/openapi/schema/provider.ex new file mode 100644 index 0000000000..8eadef009b --- /dev/null +++ b/lib/trento_web/openapi/schema/provider.ex @@ -0,0 +1,37 @@ +defmodule TrentoWeb.OpenApi.Schema.Provider do + @moduledoc false + + require OpenApiSpex + alias OpenApiSpex.Schema + + defmodule ProviderData do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "ProviderMetadata", + description: "Detected metadata for any provider", + oneOf: [ + TrentoWeb.OpenApi.Schema.Provider.AzureProviderData + ] + }) + end + + defmodule AzureProviderData do + @moduledoc false + + OpenApiSpex.schema(%{ + title: "AzureProviderData", + description: "Azure detected metadata", + type: :object, + properties: %{ + resource_group: %Schema{type: :string}, + location: %Schema{type: :string}, + vm_size: %Schema{type: :string}, + data_disk_number: %Schema{type: :integer}, + offer: %Schema{type: :string}, + sku: %Schema{type: :string}, + admin_username: %Schema{type: :string} + } + }) + end +end diff --git a/lib/trento_web/openapi/schema/sles_subscription.ex b/lib/trento_web/openapi/schema/sles_subscription.ex new file mode 100644 index 0000000000..18ab77d700 --- /dev/null +++ b/lib/trento_web/openapi/schema/sles_subscription.ex @@ -0,0 +1,23 @@ +defmodule TrentoWeb.OpenApi.Schema.SlesSubscription do + @moduledoc false + + require OpenApiSpex + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + title: "SlesSubscription", + description: "A discovered SLES Subscription on a host", + type: :object, + properties: %{ + host_id: %Schema{type: :string, format: :uuid}, + identifier: %Schema{type: :string}, + version: %Schema{type: :string}, + arch: %Schema{type: :string}, + status: %Schema{type: :string}, + subscription_status: %Schema{type: :string}, + type: %Schema{type: :string}, + starts_at: %Schema{type: :string}, + expires_at: %Schema{type: :string} + } + }) +end diff --git a/lib/trento_web/openapi/schema/tag.ex b/lib/trento_web/openapi/schema/tag.ex new file mode 100644 index 0000000000..3bcc6b7342 --- /dev/null +++ b/lib/trento_web/openapi/schema/tag.ex @@ -0,0 +1,18 @@ +defmodule TrentoWeb.OpenApi.Schema.Tag do + @moduledoc false + + require OpenApiSpex + alias OpenApiSpex.Schema + + OpenApiSpex.schema(%{ + title: "Tag", + description: "A tag attached to a resource", + type: :object, + properties: %{ + id: %Schema{type: :integer}, + resource_id: %Schema{type: :string, format: :uuid}, + resource_type: %Schema{type: :string, enum: [:host, :cluster, :sap_system, :database]}, + value: %Schema{type: :string} + } + }) +end diff --git a/lib/trento_web/router.ex b/lib/trento_web/router.ex index 637bcdfa81..db756741ed 100644 --- a/lib/trento_web/router.ex +++ b/lib/trento_web/router.ex @@ -13,6 +13,7 @@ defmodule TrentoWeb.Router do pipeline :api do plug :accepts, ["json"] + plug OpenApiSpex.Plug.PutApiSpec, module: TrentoWeb.OpenApi.ApiSpec end pipeline :protected do @@ -37,6 +38,8 @@ defmodule TrentoWeb.Router do scope "/" do pipe_through :browser + get "/api/doc", OpenApiSpex.Plug.SwaggerUI, path: "/api/openapi" + pow_session_routes() end @@ -116,6 +119,11 @@ defmodule TrentoWeb.Router do get "/prometheus/targets", PrometheusController, :targets end + scope "/api" do + pipe_through :api + get "/openapi", OpenApiSpex.Plug.RenderSpec, [] + end + # Other scopes may use custom stacks. # scope "/api", TrentoWeb do # pipe_through :api diff --git a/mix.exs b/mix.exs index ffc1d7b719..6cb5105da6 100644 --- a/mix.exs +++ b/mix.exs @@ -4,6 +4,7 @@ defmodule Trento.MixProject do def project do [ app: :trento, + description: "Easing your life in administering SAP applications", version: "1.0.0", elixir: "~> 1.13", elixirc_paths: elixirc_paths(Mix.env()), @@ -63,6 +64,7 @@ defmodule Trento.MixProject do {:jason, "~> 1.2"}, {:mock, "~> 0.3.0", only: :test}, {:mox, "~> 1.0", only: :test}, + {:open_api_spex, "~> 3.11"}, {:phoenix, "~> 1.6.2"}, {:phoenix_ecto, "~> 4.4"}, {:phoenix_html, "~> 3.0"}, diff --git a/mix.lock b/mix.lock index 4d23b33b6b..9f58d26e80 100644 --- a/mix.lock +++ b/mix.lock @@ -45,6 +45,7 @@ "mimerl": {:hex, :mimerl, "1.2.0", "67e2d3f571088d5cfd3e550c383094b47159f3eee8ffa08e64106cdf5e981be3", [:rebar3], [], "hexpm", "f278585650aa581986264638ebf698f8bb19df297f66ad91b18910dfc6e19323"}, "mock": {:hex, :mock, "0.3.7", "75b3bbf1466d7e486ea2052a73c6e062c6256fb429d6797999ab02fa32f29e03", [:mix], [{:meck, "~> 0.9.2", [hex: :meck, repo: "hexpm", optional: false]}], "hexpm", "4da49a4609e41fd99b7836945c26f373623ea968cfb6282742bcb94440cf7e5c"}, "mox": {:hex, :mox, "1.0.1", "b651bf0113265cda0ba3a827fcb691f848b683c373b77e7d7439910a8d754d6e", [:mix], [], "hexpm", "35bc0dea5499d18db4ef7fe4360067a59b06c74376eb6ab3bd67e6295b133469"}, + "open_api_spex": {:hex, :open_api_spex, "3.11.0", "e4ea9c00e2891c3195c2df511c49e280e7210272ba0077d8d9975258c70e43c0", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:plug, "~> 1.7", [hex: :plug, repo: "hexpm", optional: false]}, {:poison, "~> 3.1 or ~> 4.0", [hex: :poison, repo: "hexpm", optional: true]}], "hexpm", "640bd09f6bdd96cc0264213bfa229b0f8b3868df3384311de57e0f3fa22fc3af"}, "parse_trans": {:hex, :parse_trans, "3.3.1", "16328ab840cc09919bd10dab29e431da3af9e9e7e7e6f0089dd5a2d2820011d8", [:rebar3], [], "hexpm", "07cd9577885f56362d414e8c4c4e6bdf10d43a8767abb92d24cbe8b24c54888b"}, "phoenix": {:hex, :phoenix, "1.6.8", "9a34e5f4dd3ba959176c199fd5b2277b02e64005462428b71cf6ce9cb5e09cb4", [:mix], [{:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:phoenix_pubsub, "~> 2.0", [hex: :phoenix_pubsub, repo: "hexpm", optional: false]}, {:phoenix_view, "~> 1.0", [hex: :phoenix_view, repo: "hexpm", optional: false]}, {:plug, "~> 1.10", [hex: :plug, repo: "hexpm", optional: false]}, {:plug_cowboy, "~> 2.2", [hex: :plug_cowboy, repo: "hexpm", optional: true]}, {:plug_crypto, "~> 1.2", [hex: :plug_crypto, repo: "hexpm", optional: false]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "9f4d616aeb9c5e019bddfc1f9078b8c06f852ffa838e67f925559cc0993e9f71"}, "phoenix_ecto": {:hex, :phoenix_ecto, "4.4.0", "0672ed4e4808b3fbed494dded89958e22fb882de47a97634c0b13e7b0b5f7720", [:mix], [{:ecto, "~> 3.3", [hex: :ecto, repo: "hexpm", optional: false]}, {:phoenix_html, "~> 2.14.2 or ~> 3.0", [hex: :phoenix_html, repo: "hexpm", optional: true]}, {:plug, "~> 1.9", [hex: :plug, repo: "hexpm", optional: false]}], "hexpm", "09864e558ed31ee00bd48fcc1d4fc58ae9678c9e81649075431e69dbabb43cc1"},