From 5809500bec305b8f1720bc9155e3f97f4d271b0a Mon Sep 17 00:00:00 2001 From: Rogerio Pontual Date: Thu, 16 Jan 2025 16:05:25 +0100 Subject: [PATCH] Add steps API based on pre-existing run API --- .../controllers/api/run_controller.ex | 52 ------- lib/lightning_web/controllers/api/run_json.ex | 41 ------ .../controllers/api/step_controller.ex | 39 ++++++ .../controllers/api/step_json.ex | 35 +++++ lib/lightning_web/router.ex | 3 +- mix.lock | 4 +- .../controllers/api/run_controller_test.exs | 130 ------------------ .../controllers/api/step_controller_test.exs | 93 +++++++++++++ 8 files changed, 170 insertions(+), 227 deletions(-) delete mode 100644 lib/lightning_web/controllers/api/run_controller.ex delete mode 100644 lib/lightning_web/controllers/api/run_json.ex create mode 100644 lib/lightning_web/controllers/api/step_controller.ex create mode 100644 lib/lightning_web/controllers/api/step_json.ex delete mode 100644 test/lightning_web/controllers/api/run_controller_test.exs create mode 100644 test/lightning_web/controllers/api/step_controller_test.exs diff --git a/lib/lightning_web/controllers/api/run_controller.ex b/lib/lightning_web/controllers/api/run_controller.ex deleted file mode 100644 index 5d02699de6..0000000000 --- a/lib/lightning_web/controllers/api/run_controller.ex +++ /dev/null @@ -1,52 +0,0 @@ -# defmodule LightningWeb.API.RunController do -# use LightningWeb, :controller - -# alias Lightning.Invocation -# alias Lightning.Policies.Permissions -# alias Lightning.Policies.ProjectUsers -# alias Lightning.Projects.Project -# alias Lightning.Repo - -# action_fallback LightningWeb.FallbackController - -# def index(conn, %{"project_id" => project_id} = params) do -# pagination_attrs = Map.take(params, ["page_size", "page"]) - -# with project = %Project{} <- -# Lightning.Projects.get_project(project_id) || {:error, :not_found}, -# :ok <- -# ProjectUsers -# |> Permissions.can( -# :access_project, -# conn.assigns.current_user, -# project -# ) do -# page = Invocation.list_runs_for_project(project, pagination_attrs) - -# render(conn, "index.json", %{page: page, conn: conn}) -# end -# end - -# def index(conn, params) do -# pagination_attrs = Map.take(params, ["page_size", "page"]) - -# page = -# Invocation.Query.runs_for(conn.assigns.current_user) -# |> Lightning.Repo.paginate(pagination_attrs) - -# render(conn, "index.json", %{page: page, conn: conn}) -# end - -# def show(conn, %{"id" => id}) do -# with run <- Invocation.get_run_with_job!(id), -# :ok <- -# ProjectUsers -# |> Permissions.can( -# :access_project, -# conn.assigns.current_user, -# Repo.preload(run.job, :project).project -# ) do -# render(conn, "show.json", %{run: run, conn: conn}) -# end -# end -# end diff --git a/lib/lightning_web/controllers/api/run_json.ex b/lib/lightning_web/controllers/api/run_json.ex deleted file mode 100644 index c69311d28b..0000000000 --- a/lib/lightning_web/controllers/api/run_json.ex +++ /dev/null @@ -1,41 +0,0 @@ -# defmodule LightningWeb.API.RunJSON do -# @moduledoc false - -# import LightningWeb.API.Helpers - -# alias LightningWeb.Router.Helpers, as: Routes - -# @fields ~w(started_at finished_at log)a - -# def render("index.json", %{page: page, conn: conn}) do -# %{ -# data: Enum.map(page.entries, &resource(conn, &1)), -# included: [], -# links: -# %{ -# self: url_for(conn) -# } -# |> Map.merge(pagination_links(conn, page)) -# } -# end - -# def render("show.json", %{run: run, conn: conn}) do -# %{ -# data: resource(conn, run), -# included: [], -# links: %{ -# self: url_for(conn) -# } -# } -# end - -# defp resource(conn, run) do -# %{ -# type: "runs", -# relationships: %{}, -# links: %{self: Routes.api_run_url(conn, :show, run)}, -# id: run.id, -# attributes: Map.take(run, @fields) -# } -# end -# end diff --git a/lib/lightning_web/controllers/api/step_controller.ex b/lib/lightning_web/controllers/api/step_controller.ex new file mode 100644 index 0000000000..b79554f632 --- /dev/null +++ b/lib/lightning_web/controllers/api/step_controller.ex @@ -0,0 +1,39 @@ +defmodule LightningWeb.API.StepController do + use LightningWeb, :controller + + alias Lightning.Invocation + alias Lightning.Policies.Permissions + alias Lightning.Policies.ProjectUsers + alias Lightning.Projects + + action_fallback LightningWeb.FallbackController + + def index(conn, %{"project_id" => project_id} = params) do + with :ok <- authorize_read(conn, project_id) do + pagination_attrs = Map.take(params, ["page_size", "page"]) + + page = + project_id + |> Projects.get_project!() + |> Invocation.list_steps_for_project(pagination_attrs) + + render(conn, "index.json", %{page: page, conn: conn}) + end + end + + def show(conn, %{"project_id" => project_id, "id" => id}) do + with :ok <- authorize_read(conn, project_id) do + step = Invocation.get_step_with_job!(id) + render(conn, "show.json", %{step: step, conn: conn}) + end + end + + defp authorize_read(conn, project_id) do + Permissions.can( + ProjectUsers, + :access_project, + conn.assigns.current_resource, + %{project_id: project_id} + ) + end +end diff --git a/lib/lightning_web/controllers/api/step_json.ex b/lib/lightning_web/controllers/api/step_json.ex new file mode 100644 index 0000000000..ddf307908d --- /dev/null +++ b/lib/lightning_web/controllers/api/step_json.ex @@ -0,0 +1,35 @@ +defmodule LightningWeb.API.StepJSON do + @moduledoc false + + # import LightningWeb.API.Helpers + + def render("index.json", %{page: page}) do + page.entries + |> Enum.map(&process_instance/1) + # |> Enum.concat(pagination_links(conn, page)) + end + + def render("show.json", %{step: step}) do + process_instance(step) + end + + defp process_instance(step) do + %{ + id: step.id, + processRef: "#{step.job.name}:1:#{step.job.id}", + initTime: step.started_at, + state: step_state(step), + lastChangeTime: step.updated_at + } + end + + defp step_state(step) do + case {step.started_at, step.finished_at, step.exit_reason} do + {nil, nil, _reason} -> "Ready" + {_started_at, nil, _reason} -> "Active" + {_started_at, _finished_at, "sucess"} -> "Completed" + {_started_at, _finished_at, failed} when failed in ["cancel", "kill"] -> "Terminated" + {_started_at, _finished_at, _reason} -> "Failed" + end + end +end diff --git a/lib/lightning_web/router.ex b/lib/lightning_web/router.ex index 50252e9a72..4c52b753e4 100644 --- a/lib/lightning_web/router.ex +++ b/lib/lightning_web/router.ex @@ -83,11 +83,10 @@ defmodule LightningWeb.Router do resources "/projects", API.ProjectController, only: [:index, :show] do resources "/jobs", API.JobController, only: [:index, :show] resources "/workflows", API.WorkflowsController, except: [:delete] - # resources "/runs", API.RunController, only: [:index, :show] + resources "/steps", API.StepController, only: [:index, :show] end resources "/jobs", API.JobController, only: [:index, :show] - # resources "/runs", API.RunController, only: [:index, :show] end ## Collections diff --git a/mix.lock b/mix.lock index 3d697c4c03..f3cd2c84e6 100644 --- a/mix.lock +++ b/mix.lock @@ -26,13 +26,13 @@ "crontab": {:hex, :crontab, "1.1.14", "233fcfdc2c74510cabdbcb800626babef414e7cb13cea11ddf62e10e16e2bf76", [:mix], [{:ecto, "~> 1.0 or ~> 2.0 or ~> 3.0", [hex: :ecto, repo: "hexpm", optional: true]}], "hexpm", "4e3b9950bc22ae8d0395ffb5f4b127a140005cba95745abf5ff9ee7e8203c6fa"}, "ctx": {:hex, :ctx, "0.6.0", "8ff88b70e6400c4df90142e7f130625b82086077a45364a78d208ed3ed53c7fe", [:rebar3], [], "hexpm", "a14ed2d1b67723dbebbe423b28d7615eb0bdcba6ff28f2d1f1b0a7e1d4aa5fc2"}, "db_connection": {:hex, :db_connection, "2.7.0", "b99faa9291bb09892c7da373bb82cba59aefa9b36300f6145c5f201c7adf48ec", [:mix], [{:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "dcf08f31b2701f857dfc787fbad78223d61a32204f217f15e881dd93e4bdd3ff"}, - "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, + "decimal": {:hex, :decimal, "2.3.0", "3ad6255aa77b4a3c4f818171b12d237500e63525c2fd056699967a3e7ea20f62", [:mix], [], "hexpm", "a4d66355cb29cb47c3cf30e71329e58361cfcb37c34235ef3bf1d7bf3773aeac"}, "deep_merge": {:hex, :deep_merge, "1.0.0", "b4aa1a0d1acac393bdf38b2291af38cb1d4a52806cf7a4906f718e1feb5ee961", [:mix], [], "hexpm", "ce708e5f094b9cd4e8f2be4f00d2f4250c4095be93f8cd6d018c753894885430"}, "dialyxir": {:hex, :dialyxir, "1.4.5", "ca1571ac18e0f88d4ab245f0b60fa31ff1b12cbae2b11bd25d207f865e8ae78a", [:mix], [{:erlex, ">= 0.2.7", [hex: :erlex, repo: "hexpm", optional: false]}], "hexpm", "b0fb08bb8107c750db5c0b324fa2df5ceaa0f9307690ee3c1f6ba5b9eb5d35c3"}, "dotenvy": {:hex, :dotenvy, "0.8.0", "777486ad485668317c56afc53a7cbcd74f43e4e34588ba8e95a73e15a360050e", [:mix], [], "hexpm", "1f535066282388cbd109743d337ac46ff0708195780d4b5778bb83491ab1b654"}, "earmark": {:hex, :earmark, "1.4.47", "7e7596b84fe4ebeb8751e14cbaeaf4d7a0237708f2ce43630cfd9065551f94ca", [:mix], [], "hexpm", "3e96bebea2c2d95f3b346a7ff22285bc68a99fbabdad9b655aa9c6be06c698f8"}, "earmark_parser": {:hex, :earmark_parser, "1.4.41", "ab34711c9dc6212dda44fcd20ecb87ac3f3fce6f0ca2f28d4a00e4154f8cd599", [:mix], [], "hexpm", "a81a04c7e34b6617c2792e291b5a2e57ab316365c2644ddc553bb9ed863ebefa"}, - "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, + "ecto": {:hex, :ecto, "3.12.5", "4a312960ce612e17337e7cefcf9be45b95a3be6b36b6f94dfb3d8c361d631866", [:mix], [{:decimal, "~> 2.0", [hex: :decimal, repo: "hexpm", optional: false]}, {:jason, "~> 1.0", [hex: :jason, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "6eb18e80bef8bb57e17f5a7f068a1719fbda384d40fc37acb8eb8aeca493b6ea"}, "ecto_enum": {:hex, :ecto_enum, "1.4.0", "d14b00e04b974afc69c251632d1e49594d899067ee2b376277efd8233027aec8", [:mix], [{:ecto, ">= 3.0.0", [hex: :ecto, repo: "hexpm", optional: false]}, {:ecto_sql, "> 3.0.0", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:mariaex, ">= 0.0.0", [hex: :mariaex, repo: "hexpm", optional: true]}, {:postgrex, ">= 0.0.0", [hex: :postgrex, repo: "hexpm", optional: true]}], "hexpm", "8fb55c087181c2b15eee406519dc22578fa60dd82c088be376d0010172764ee4"}, "ecto_psql_extras": {:hex, :ecto_psql_extras, "0.8.2", "79350a53246ac5ec27326d208496aebceb77fa82a91744f66a9154560f0759d3", [:mix], [{:ecto_sql, "~> 3.7", [hex: :ecto_sql, repo: "hexpm", optional: false]}, {:postgrex, "> 0.16.0 and < 0.20.0", [hex: :postgrex, repo: "hexpm", optional: false]}, {:table_rex, "~> 3.1.1 or ~> 4.0.0", [hex: :table_rex, repo: "hexpm", optional: false]}], "hexpm", "6149c1c4a5ba6602a76cb09ee7a269eb60dab9694a1dbbb797f032555212de75"}, "ecto_sql": {:hex, :ecto_sql, "3.12.1", "c0d0d60e85d9ff4631f12bafa454bc392ce8b9ec83531a412c12a0d415a3a4d0", [:mix], [{:db_connection, "~> 2.4.1 or ~> 2.5", [hex: :db_connection, repo: "hexpm", optional: false]}, {:ecto, "~> 3.12", [hex: :ecto, repo: "hexpm", optional: false]}, {:myxql, "~> 0.7", [hex: :myxql, repo: "hexpm", optional: true]}, {:postgrex, "~> 0.19 or ~> 1.0", [hex: :postgrex, repo: "hexpm", optional: true]}, {:tds, "~> 2.1.1 or ~> 2.2", [hex: :tds, repo: "hexpm", optional: true]}, {:telemetry, "~> 0.4.0 or ~> 1.0", [hex: :telemetry, repo: "hexpm", optional: false]}], "hexpm", "aff5b958a899762c5f09028c847569f7dfb9cc9d63bdb8133bff8a5546de6bf5"}, diff --git a/test/lightning_web/controllers/api/run_controller_test.exs b/test/lightning_web/controllers/api/run_controller_test.exs deleted file mode 100644 index 1252d09df1..0000000000 --- a/test/lightning_web/controllers/api/run_controller_test.exs +++ /dev/null @@ -1,130 +0,0 @@ -# defmodule LightningWeb.API.RunControllerTest do -# use LightningWeb.ConnCase, async: true - -# import Lightning.InvocationFixtures -# import Lightning.WorkflowsFixtures -# import Lightning.JobsFixtures -# import Lightning.ProjectsFixtures - -# setup %{conn: conn} do -# {:ok, conn: put_req_header(conn, "accept", "application/json")} -# end - -# test "without a token", %{conn: conn} do -# conn = get(conn, ~p"/api/projects/#{Ecto.UUID.generate()}/runs") - -# assert %{"error" => "Unauthorized"} == json_response(conn, 401) -# end - -# describe "with invalid token" do -# test "gets a 401", %{conn: conn} do -# token = "Oooops" -# conn = conn |> Plug.Conn.put_req_header("authorization", "Bearer #{token}") -# conn = get(conn, ~p"/api/projects/#{Ecto.UUID.generate()}/runs") -# assert json_response(conn, 401) == %{"error" => "Unauthorized"} -# end -# end - -# describe "index" do -# setup [:assign_bearer_for_api, :create_project_for_current_user, :create_run] - -# defp pluck_id(data) do -# Map.get(data, "id") || Map.get(data, :id) -# end - -# test "lists all runs for a project I belong to", %{ -# conn: conn, -# project: project -# } do -# other_project = project_fixture() -# runs = Enum.map(0..10, fn _ -> run_fixture(project_id: project.id) end) - -# other_runs = -# Enum.map(0..2, fn _ -> run_fixture(project_id: other_project.id) end) - -# conn = get(conn, ~p"/api/projects/#{project.id}/runs?#{%{page_size: 2}}") - -# response = json_response(conn, 200) - -# all_run_ids = MapSet.new(runs |> Enum.map(&pluck_id/1)) -# returned_run_ids = MapSet.new(response["data"] |> Enum.map(&pluck_id/1)) - -# assert MapSet.subset?(returned_run_ids, all_run_ids) - -# other_run_ids = MapSet.new(other_runs |> Enum.map(&pluck_id/1)) - -# refute MapSet.subset?(other_run_ids, all_run_ids) -# end - -# test "responds with a 401 when I don't have access", %{conn: conn} do -# other_project = project_fixture() - -# conn = get(conn, ~p"/api/projects/#{other_project.id}/runs") - -# response = json_response(conn, 401) - -# assert response == %{"error" => "Unauthorized"} -# end - -# test "lists all runs for the current user", %{ -# conn: conn, -# project: project, -# run: run -# } do -# runs = [create_run(%{project: project}).run, run] -# other_run = run_fixture() - -# conn = -# get( -# conn, -# Routes.api_run_path(conn, :index, %{ -# "page_size" => 3 -# }) -# ) - -# response = json_response(conn, 200) - -# runs -# |> Enum.each(fn r -> -# assert response["data"] |> Enum.any?(fn d -> d["id"] == r.id end) -# end) - -# refute response["data"] |> Enum.any?(fn d -> d["id"] == other_run.id end) -# end -# end - -# describe "show" do -# setup [:assign_bearer_for_api, :create_project_for_current_user, :create_run] - -# test "shows the run", %{conn: conn, run: run} do -# conn = -# get( -# conn, -# Routes.api_run_path(conn, :show, run, %{ -# "fields" => %{"runs" => "exit_code,finished_at"} -# }) -# ) - -# response = json_response(conn, 200) -# step_id = run.id - -# assert %{ -# "attributes" => %{ -# "finished_at" => nil -# }, -# "id" => ^step_id, -# "links" => %{ -# "self" => _ -# }, -# "relationships" => %{}, -# "type" => "runs" -# } = response["data"] -# end -# end - -# # TODO: see if we can't use run fixture -# defp create_run(%{project: project}) do -# job = job_fixture(workflow_id: workflow_fixture(project_id: project.id).id) -# %{run: run_fixture(job_id: job.id)} -# end -# end diff --git a/test/lightning_web/controllers/api/step_controller_test.exs b/test/lightning_web/controllers/api/step_controller_test.exs new file mode 100644 index 0000000000..c66c8521ae --- /dev/null +++ b/test/lightning_web/controllers/api/step_controller_test.exs @@ -0,0 +1,93 @@ +defmodule LightningWeb.API.StepControllerTest do + use LightningWeb.ConnCase, async: true + + import Lightning.Factories + import Lightning.ProjectsFixtures + + setup %{conn: conn} do + {:ok, conn: put_req_header(conn, "accept", "application/json")} + end + + test "without a token", %{conn: conn} do + conn = get(conn, ~p"/api/projects/#{Ecto.UUID.generate()}/steps") + + assert %{"error" => "Unauthorized"} == json_response(conn, 401) + end + + describe "with invalid token" do + test "gets a 401", %{conn: conn} do + token = "Oooops" + conn = conn |> Plug.Conn.put_req_header("authorization", "Bearer #{token}") + conn = get(conn, ~p"/api/projects/#{Ecto.UUID.generate()}/steps") + assert json_response(conn, 401) == %{"error" => "Unauthorized"} + end + end + + describe "index" do + setup [:assign_bearer_for_api, :create_project_for_current_user] + + test "lists a page of project steps for the current user", %{ + conn: conn, + project: project, + } do + %{jobs: [job]} = insert(:simple_workflow, project: project) + + steps = insert_list(6, :step, job: job) + + conn = get(conn, ~p"/api/projects/#{project.id}/steps/", page_size: 5) + + assert response_steps = json_response(conn, 200) + + steps_ids = Enum.map(steps, & &1.id) + + assert Enum.all?(response_steps, & &1["id"] in steps_ids) + end + + test "lists all project steps for the current user", %{ + conn: conn, + project: project, + } do + %{jobs: [job]} = insert(:simple_workflow, project: project) + + steps = insert_list(6, :step, job: job) + + assert response_steps = get(conn, ~p"/api/projects/#{project.id}/steps/") |> json_response(200) + + assert MapSet.equal?(MapSet.new(response_steps, & &1["id"]), MapSet.new(steps, & &1.id)) + end + + test "returns 401 on unrelated projects", %{conn: conn} do + other_project = project_fixture() + + conn = get(conn, ~p"/api/projects/#{other_project.id}/steps") + + response = json_response(conn, 401) + + assert response == %{"error" => "Unauthorized"} + end + + end + + describe "show" do + setup [:assign_bearer_for_api, :create_project_for_current_user] + + test "shows the run", %{conn: conn, project: project} do + %{jobs: [job]} = insert(:simple_workflow, project: project) + %{id: step_id, updated_at: updated_at} = insert(:step, job: job) + + updated_at = DateTime.to_iso8601(updated_at) + processRef = "#{job.name}:1:#{job.id}" + + assert %{ + "id" => ^step_id, + "initTime" => nil, + "lastChangeTime" => ^updated_at, + "processRef" => ^processRef, + "state" => "Ready" + } = + conn + |> get(~p"/api/projects/#{project.id}/steps/#{step_id}") + |> json_response(200) + end + end +end