defmodule K8s.Operation.Path do @moduledoc "Generates Kubernetes REST API Paths" alias K8s.Operation.Error @path_params [:namespace, :name, :path, :logpath] @doc false @spec path_params() :: list(atom) def path_params, do: @path_params @doc """ Generates the API path for a `K8s.Operation`. ## Examples Generate a path for a cluster scoped resource: iex> resource = K8s.Resource.build("apps/v1", "CertificateSigningRequest", "foo") ...> operation = %K8s.Operation{ ...> method: :put, ...> verb: :update, ...> data: resource, ...> path_params: [name: "foo"], ...> api_version: "apps/v1", ...> name: "certificatesigningrequests" ...> } ...> K8s.Operation.Path.build(operation) {:ok, "/apis/apps/v1/certificatesigningrequests/foo"} iex> resource = K8s.Resource.build("apps/v1", "CertificateSigningRequest", "foo") ...> operation = %K8s.Operation{ ...> method: :put, ...> verb: :update, ...> data: resource, ...> path_params: [name: "foo", namespace: nil], ...> api_version: "apps/v1", ...> name: "certificatesigningrequests" ...> } ...> K8s.Operation.Path.build(operation) {:ok, "/apis/apps/v1/certificatesigningrequests/foo"} Generate a path for a namespace scoped resource: iex> resource = K8s.Resource.build("v1", "Pod", "default", "foo") ...> operation = %K8s.Operation{ ...> method: :put, ...> verb: :update, ...> data: resource, ...> path_params: [namespace: "default", name: "foo"], ...> api_version: "v1", ...> name: "pods" ...> } ...> K8s.Operation.Path.build(operation) {:ok, "/api/v1/namespaces/default/pods/foo"} Generate a path for a namespace scoped resource on the collection: (ie create, list) iex> resource = K8s.Resource.build("v1", "Pod", "default", "foo") ...> operation = %K8s.Operation{ ...> method: :post, ...> verb: :create, ...> data: resource, ...> path_params: [namespace: "default", name: "foo"], ...> api_version: "v1", ...> name: "pods" ...> } ...> K8s.Operation.Path.build(operation) {:ok, "/api/v1/namespaces/default/pods"} Generating a listing path for a namespace: iex> operation = %K8s.Operation{ ...> method: :get, ...> verb: :list, ...> data: nil, ...> path_params: [namespace: "default"], ...> api_version: "v1", ...> name: "pods" ...> } ...> K8s.Operation.Path.build(operation) {:ok, "/api/v1/namespaces/default/pods"} Generating a listing path for a all namespaces: iex> operation = %K8s.Operation{ ...> method: :get, ...> verb: :list_all_namespaces, ...> data: nil, ...> path_params: [namespace: "default"], ...> api_version: "v1", ...> name: "pods" ...> } ...> K8s.Operation.Path.build(operation) {:ok, "/api/v1/pods"} Generating a listing path for a watching all namespaces: iex> operation = %K8s.Operation{ ...> method: :get, ...> verb: :watch_all_namespaces, ...> data: nil, ...> path_params: [namespace: "default"], ...> api_version: "v1", ...> name: "pods" ...> } ...> K8s.Operation.Path.build(operation) {:ok, "/api/v1/pods"} Generating a path for a subresource: iex> operation = %K8s.Operation{ ...> method: :get, ...> verb: :get, ...> data: nil, ...> path_params: [namespace: "default", name: "foo"], ...> api_version: "v1", ...> name: "pods/status" ...> } ...> K8s.Operation.Path.build(operation) {:ok, "/api/v1/namespaces/default/pods/foo/status"} Deleting a collection: iex> operation = %K8s.Operation{ ...> method: :delete, ...> verb: :deletecollection, ...> data: nil, ...> path_params: [namespace: "default"], ...> api_version: "v1", ...> name: "pods" ...> } ...> K8s.Operation.Path.build(operation) {:ok, "/api/v1/namespaces/default/pods"} """ @spec build(K8s.Operation.t()) :: {:ok, binary} | {:error, Error.t()} def build(%K8s.Operation{} = operation) do path_template = to_path(operation) required_params = K8s.Operation.Path.find_params(path_template) provided_params = Keyword.keys(operation.path_params) case required_params -- provided_params do [] -> {:ok, replace_path_vars(path_template, operation.path_params)} missing_params -> msg = "Missing required params: #{inspect(missing_params)}" {:error, %Error{message: msg}} end end @doc """ Replaces path variables with options. ## Examples iex> K8s.Operation.Path.replace_path_vars("/foo/{name}", name: "bar") "/foo/bar" """ @spec replace_path_vars(binary(), keyword()) :: binary() def replace_path_vars(path_template, opts) do Regex.replace(~r/\{(\w+?)\}/, path_template, fn _, var -> opts[String.to_existing_atom(var)] end) end @doc """ Find valid path params in a URL path. ## Examples iex> K8s.Operation.Path.find_params("/foo/{name}") [:name] iex> K8s.Operation.Path.find_params("/foo/{namespace}/bar/{name}") [:namespace, :name] iex> K8s.Operation.Path.find_params("/foo/bar") [] """ @spec find_params(binary()) :: list(atom()) def find_params(path_with_args) do ~r/{([a-z]+)}/ |> Regex.scan(path_with_args) |> Enum.map(fn match -> match |> List.last() |> String.to_existing_atom() end) end @spec to_path(K8s.Operation.t()) :: binary defp to_path(%K8s.Operation{path_params: params} = operation) do has_namespace = !is_nil(params[:namespace]) namespaced = has_namespace && params[:namespace] != :all prefix = case String.contains?(operation.api_version, "/") do true -> "/apis/#{operation.api_version}" false -> "/api/#{operation.api_version}" end suffix = operation.name |> String.split("/") |> resource_path_suffix(operation.verb) build_path(prefix, suffix, namespaced, operation.verb) end @spec resource_path_suffix(list(binary), atom) :: binary defp resource_path_suffix([name], verb), do: name_param(name, verb) defp resource_path_suffix([name, subresource], verb), do: name_with_subresource_param(name, subresource, verb) @spec name_param(binary, atom) :: binary defp name_param(resource_name, :create), do: resource_name defp name_param(resource_name, :list_all_namespaces), do: resource_name defp name_param(resource_name, :list), do: resource_name defp name_param(resource_name, :deletecollection), do: resource_name defp name_param(resource_name, :watch_all_namespaces), do: resource_name defp name_param(resource_name, _), do: "#{resource_name}/{name}" @spec name_with_subresource_param(binary, binary, atom) :: binary defp name_with_subresource_param(resource_name, subresource, _), do: "#{resource_name}/{name}/#{subresource}" @spec build_path(binary, binary, boolean, atom) :: binary defp build_path(prefix, suffix, true, :watch_all_namespaces), do: "#{prefix}/#{suffix}" defp build_path(prefix, suffix, true, :list_all_namespaces), do: "#{prefix}/#{suffix}" defp build_path(prefix, suffix, true, _), do: "#{prefix}/namespaces/{namespace}/#{suffix}" defp build_path(prefix, suffix, false, _), do: "#{prefix}/#{suffix}" end