Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions lib/bob/artifacts.ex
Original file line number Diff line number Diff line change
Expand Up @@ -222,6 +222,13 @@ defmodule Bob.Artifacts do
|> Repo.aggregate(:count, :id)
end

def artifact_page(filters, limit, offset) do
%{
results: search_artifacts(filters, limit, offset),
total: count_artifacts(filters)
}
end

def search_docker_tags(filters \\ %{}, limit \\ 100, offset \\ 0) do
filters
|> docker_tag_search_query()
Expand All @@ -237,6 +244,30 @@ defmodule Bob.Artifacts do
|> Repo.aggregate(:count, :id)
end

@doc """
Returns a page of matching Docker tags together with the total match count.

The page and the count are separate queries, so the total is kept coherent
with the page if tags change between them, and the count is skipped entirely
when the first page comes back empty.
"""
def docker_tag_page(filters, limit, offset) do
results = search_docker_tags(filters, limit, offset)

total =
if offset == 0 and results == [] do
0
else
filters
|> count_docker_tags()
# The page and count are separate queries, so keep the pager coherent if
# tags change between them.
|> max(offset + length(results))
end

%{results: results, total: total}
end

def distinct_kinds(), do: distinct_values(Artifact, :kind)
def distinct_arches(), do: distinct_values(Artifact, :arch)
def distinct_oses(), do: distinct_values(Artifact, :os)
Expand Down
23 changes: 23 additions & 0 deletions lib/bob/artifacts/artifact_search.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
defmodule Bob.Artifacts.ArtifactSearch do
@page 100
@filter_keys ~w(query kind arch os)a

def page_size(), do: @page
def filter_keys(), do: @filter_keys

def parse_filters(params) do
%{
query: params["query"] || "",
kind: params["kind"] || "",
arch: params["arch"] || "",
os: params["os"] || ""
}
end

def parse_offset(params) do
case Integer.parse(params["offset"] || "") do
{offset, ""} when offset > 0 -> offset
_ -> 0
end
end
end
30 changes: 30 additions & 0 deletions lib/bob/artifacts/docker_tag_search.ex
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,40 @@ defmodule Bob.Artifacts.DockerTagSearch do
@elixir_tag_regex ~r/^(.+)-erlang-(.+)-(alpine|ubuntu|debian)-(.+)$/
@version_regex ~r/^\d+(?:\.\d+)*(?:-[0-9A-Za-z][0-9A-Za-z.-]*)?$/

@page 100
@filter_keys ~w(repo tag arch elixir_version erlang_version os os_version)a

def repos(), do: Enum.sort(@erlang_repos ++ @elixir_repos)
def arches(), do: @archs
def oses(), do: @oses

def page_size(), do: @page
def filter_keys(), do: @filter_keys

def parse_filters(params) do
tag = params["tag"] || ""

%{
repo: params["repo"] || "",
tag: tag,
arch: params["arch"] || "",
elixir_version: structured_param(params, "elixir_version", tag),
erlang_version: structured_param(params, "erlang_version", tag),
os: structured_param(params, "os", tag),
os_version: structured_param(params, "os_version", tag)
}
end

def parse_offset(params) do
case Integer.parse(params["offset"] || "") do
{offset, ""} when offset > 0 -> offset
_ -> 0
end
end

defp structured_param(_params, _key, tag) when tag != "", do: ""
defp structured_param(params, key, _tag), do: params[key] || ""

def metadata(repo, tag) when repo in @erlang_repos do
case Regex.run(@erlang_tag_regex, tag, capture: :all_but_first) do
[erlang_version, os, os_version] ->
Expand Down
53 changes: 53 additions & 0 deletions lib/bob_web/controllers/public_api_controller.ex
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
defmodule BobWeb.PublicApiController do
@moduledoc """
Public API endpoints.

Provide search endpoints similar to the artifact and docker search LiveViews.
"""

use BobWeb, :controller

alias Bob.Artifacts
alias Bob.Artifacts.{ArtifactSearch, DockerTagSearch}

def artifacts(conn, params) do
offset = ArtifactSearch.parse_offset(params)
filters = ArtifactSearch.parse_filters(params)
page = Artifacts.artifact_page(filters, ArtifactSearch.page_size(), offset)

json(conn, envelope(page, :artifacts, offset, ArtifactSearch.page_size(), &artifact/1))
end

def docker_tags(conn, params) do
offset = DockerTagSearch.parse_offset(params)
filters = DockerTagSearch.parse_filters(params)
page = Artifacts.docker_tag_page(filters, DockerTagSearch.page_size(), offset)

json(conn, envelope(page, :tags, offset, DockerTagSearch.page_size(), &docker_tag/1))
end

defp envelope(%{results: results, total: total}, key, offset, page_size, row) do
%{key => Enum.map(results, row), total: total, offset: offset, page_size: page_size}
end

defp artifact(artifact) do
%{
kind: artifact.kind,
arch: artifact.arch,
os: artifact.os,
name: artifact.name,
ref: artifact.ref,
sha256: artifact.sha256,
built_at: artifact.built_at && DateTime.to_iso8601(artifact.built_at)
}
end

defp docker_tag(tag) do
%{
repo: tag.repo,
tag: tag.tag,
archs: tag.archs,
built_at: tag.built_at && DateTime.to_iso8601(tag.built_at)
}
end
end
40 changes: 10 additions & 30 deletions lib/bob_web/live/artifacts_live.ex
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
defmodule BobWeb.ArtifactsLive do
use BobWeb, :live_view

@page 100
alias Bob.Artifacts.ArtifactSearch

@page ArtifactSearch.page_size()
@filter_keys ArtifactSearch.filter_keys()

@impl true
def mount(_params, _session, socket) do
Expand Down Expand Up @@ -35,42 +38,24 @@ defmodule BobWeb.ArtifactsLive do

def handle_event("page", %{"dir" => dir}, socket) do
offset = max(socket.assigns.offset + step(dir), 0)

filters = [
query: socket.assigns.query,
kind: socket.assigns.kind,
arch: socket.assigns.arch,
os: socket.assigns.os
]

filters = Enum.map(@filter_keys, &{&1, socket.assigns[&1]})
{:noreply, push_patch(socket, to: ~p"/artifacts?#{query(filters, offset)}")}
end

defp step("next"), do: @page
defp step("prev"), do: -@page

defp filter_assigns(params) do
[
query: params["query"] || "",
kind: params["kind"] || "",
arch: params["arch"] || "",
os: params["os"] || "",
offset: parse_offset(params)
]
filters = ArtifactSearch.parse_filters(params)

Enum.map(@filter_keys, &{&1, filters[&1]}) ++ [offset: ArtifactSearch.parse_offset(params)]
end

defp query(filters, offset) do
filters = Enum.reject(filters, fn {_key, value} -> value in [nil, ""] end)
if offset > 0, do: filters ++ [offset: offset], else: filters
end

defp parse_offset(params) do
case Integer.parse(params["offset"] || "") do
{offset, ""} when offset > 0 -> offset
_ -> 0
end
end

defp load_options(socket) do
assign(socket,
kinds: Bob.Artifacts.distinct_kinds(),
Expand All @@ -84,14 +69,9 @@ defmodule BobWeb.ArtifactsLive do

filters = %{query: q, kind: kind, arch: arch, os: os}

results =
Bob.Artifacts.search_artifacts(filters, @page, offset)
%{results: results, total: total} = Bob.Artifacts.artifact_page(filters, @page, offset)

assign(socket,
results: results,
total: Bob.Artifacts.count_artifacts(filters),
loading: false
)
assign(socket, results: results, total: total, loading: false)
end

defp fmt(nil), do: "—"
Expand Down
43 changes: 7 additions & 36 deletions lib/bob_web/live/docker_tags_live.ex
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
defmodule BobWeb.DockerTagsLive do
use BobWeb, :live_view

@page 100
@filter_keys ~w(repo tag arch elixir_version erlang_version os os_version)a
alias Bob.Artifacts.DockerTagSearch

@page DockerTagSearch.page_size()
@filter_keys DockerTagSearch.filter_keys()

@impl true
def mount(_params, _session, socket) do
Expand Down Expand Up @@ -45,28 +47,9 @@ defmodule BobWeb.DockerTagsLive do
defp step("prev"), do: -@page

defp filter_assigns(params) do
tag = params["tag"] || ""

[
repo: params["repo"] || "",
tag: tag,
arch: params["arch"] || "",
elixir_version: structured_param(params, "elixir_version", tag),
erlang_version: structured_param(params, "erlang_version", tag),
os: structured_param(params, "os", tag),
os_version: structured_param(params, "os_version", tag),
offset: parse_offset(params)
]
end

defp structured_param(_params, _key, tag) when tag != "", do: ""
defp structured_param(params, key, _tag), do: params[key] || ""
filters = DockerTagSearch.parse_filters(params)

defp parse_offset(params) do
case Integer.parse(params["offset"] || "") do
{offset, ""} when offset > 0 -> offset
_ -> 0
end
Enum.map(@filter_keys, &{&1, filters[&1]}) ++ [offset: DockerTagSearch.parse_offset(params)]
end

defp query(filters, offset) do
Expand Down Expand Up @@ -96,19 +79,7 @@ defmodule BobWeb.DockerTagsLive do
os_version: os_version
}

results =
Bob.Artifacts.search_docker_tags(filters, @page, offset)

total =
if offset == 0 and results == [] do
0
else
filters
|> Bob.Artifacts.count_docker_tags()
# The page and count are separate queries, so keep the pager coherent if
# tags change between them.
|> max(offset + length(results))
end
%{results: results, total: total} = Bob.Artifacts.docker_tag_page(filters, @page, offset)

assign(socket, results: results, total: total, loading: false)
end
Expand Down
13 changes: 11 additions & 2 deletions lib/bob_web/plugs/secret.ex
Original file line number Diff line number Diff line change
@@ -1,10 +1,19 @@
defmodule BobWeb.Plugs.Secret do
import Plug.Conn

# The public, read-only search API needs no secret; every other /api route
# is agent-only. The check stays in the endpoint, ahead of Plug.Parsers, so
# unauthorized requests are rejected before their body is parsed.
@public_api [{"GET", ["api", "artifacts"]}, {"GET", ["api", "docker"]}]

def init(opts), do: opts

def call(%{path_info: ["api" | _]} = conn, _opts) do
authenticate(conn)
def call(%{path_info: ["api" | _] = path_info} = conn, _opts) do
if {conn.method, path_info} in @public_api do
conn
else
authenticate(conn)
end
end

def call(conn, opts) when is_list(opts) do
Expand Down
13 changes: 13 additions & 0 deletions lib/bob_web/router.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,11 @@ defmodule BobWeb.Router do
plug(:fetch_current_user)
end

pipeline :public_api do
plug(:accepts, ["json"])
end

# Authenticated in the endpoint by BobWeb.Plugs.Secret, before body parsing.
scope "/api", BobWeb do
post("/queue/start", QueueController, :start)
post("/queue/success", QueueController, :success)
Expand All @@ -23,6 +28,14 @@ defmodule BobWeb.Router do
post("/docker/add", ArtifactController, :add_docker)
end

# Public API routes, exempted from agent auth in BobWeb.Plugs.Secret.
scope "/api", BobWeb do
pipe_through(:public_api)

get("/artifacts", PublicApiController, :artifacts)
get("/docker", PublicApiController, :docker_tags)
end

scope "/", BobWeb do
pipe_through(:browser)

Expand Down
Loading
Loading