This repository has been archived on 2024-06-24. You can view files and clone it, but cannot push or open issues or pull requests.
balena-device/lib/balena_device/v2.ex
2019-10-20 07:24:11 +08:00

508 lines
14 KiB
Elixir

defmodule BalenaDevice.V2 do
alias BalenaDevice.HTTP
import BalenaDevice.Utils
alias HTTPoison.Response
@moduledoc """
This module contains the version two API endpoints as described in [the API
documentation](https://www.balena.io/docs/reference/supervisor/supervisor-api).
"""
# Options which can be passed to `journald_logs/1`.
@type journald_options :: [journald_option]
# A valid option for `journald_log/1`.
@type journald_option ::
{:all, boolean}
| {:follow, boolean}
| {:count, non_neg_integer}
| {:unit, String.t()}
| {:format, String.t()}
@doc """
Returns a map of applications and their state.
"""
@spec applications_state :: {:ok, [map]} | {:error, term}
def applications_state do
case HTTP.get("/v2/applications/state") do
{:ok, %Response{body: body, status_code: 200}} ->
state =
body
|> Jason.decode!()
|> underscore_map_keys()
{:ok, state}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Use this endpoint to get the state of a single application, given the app_id.
"""
@spec application_state(BalenaDevice.app_id()) :: {:ok, map} | {:error, term}
def application_state(app_id) when is_app_id(app_id) do
case HTTP.get("/v2/applications/#{app_id}/state") do
{:ok, %Response{status_code: 200, body: body}} ->
state =
body
|> Jason.decode!()
|> underscore_map_keys()
{:ok, state}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
This will return a list of images, containers, the overall download progress
and the status of the state engine.
"""
@spec status :: {:ok, map} | {:error, term}
def status do
case HTTP.get("/v2/state/status") do
{:ok, %Response{body: body, status_code: 200}} ->
status =
body
|> Jason.decode!()
|> underscore_map_keys()
{:ok, status}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Restart an application's service based on it's service name or image id.
"""
@spec restart_service(
BalenaDevice.app_id(),
[{:service_name, String.t()}] | [{:image_id, String.t()}]
) ::
:ok | {:error, term}
def restart_service(app_id, service_name: service_name) when is_app_id(app_id) do
case HTTP.post(
"/v2/applications/#{app_id}/restart-service",
Jason.encode!(%{"serviceName" => service_name})
) do
{:ok, %Response{status_code: 200}} ->
:ok
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
def restart_service(app_id, image_id: image_id) when is_app_id(app_id) do
case HTTP.post(
"/v2/applications/#{app_id}/restart-service",
Jason.encode!(%{"imageId" => image_id})
) do
{:ok, %Response{status_code: 200}} ->
:ok
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
def restart_service(_app_id, _),
do: {:error, "Must specify either a service name or an image id"}
@doc """
Stop an application's service based on it's service name or image id.
"""
@spec stop_service(
BalenaDevice.app_id(),
[{:service_name, String.t()}] | [{:image_id, String.t()}]
) ::
:ok | {:error, term}
def stop_service(app_id, service_name: service_name) when is_app_id(app_id) do
case HTTP.post(
"/v2/applications/#{app_id}/stop-service",
Jason.encode!(%{"serviceName" => service_name})
) do
{:ok, %Response{status_code: 200}} ->
:ok
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
def stop_service(app_id, image_id: image_id) when is_app_id(app_id) do
case HTTP.post(
"/v2/applications/#{app_id}/stop-service",
Jason.encode!(%{"imageId" => image_id})
) do
{:ok, %Response{status_code: 200}} ->
:ok
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
def stop_service(_app_id, _),
do: {:error, "Must specify either a service name or an image id"}
@doc """
Start an application's service based on it's service name or image id.
"""
@spec start_service(
BalenaDevice.app_id(),
[{:service_name, String.t()}] | [{:image_id, String.t()}]
) ::
:ok | {:error, term}
def start_service(app_id, service_name: service_name) when is_app_id(app_id) do
case HTTP.post(
"/v2/applications/#{app_id}/start-service",
Jason.encode!(%{"serviceName" => service_name})
) do
{:ok, %Response{status_code: 200}} ->
:ok
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
def start_service(app_id, image_id: image_id) when is_app_id(app_id) do
case HTTP.post(
"/v2/applications/#{app_id}/start-service",
Jason.encode!(%{"imageId" => image_id})
) do
{:ok, %Response{status_code: 200}} ->
:ok
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
def start_service(_app_id, _),
do: {:error, "Must specify either a service name or an image id"}
@doc """
Restart all services in an application.
"""
@spec restart_services(BalenaDevice.app_id()) :: :ok | {:error, term}
def restart_services(app_id) when is_app_id(app_id) do
case HTTP.post("/v2/applications/#{app_id}/restart", "") do
{:ok, %Response{status_code: 200}} ->
:ok
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Use this endpoint to purge all user data for a given application id.
"""
@spec purge(BalenaDevice.app_id()) :: :ok | {:error, term}
def purge(app_id) when is_app_id(app_id) do
case HTTP.post("/v2/applications/#{app_id}/purge", "") do
{:ok, %Response{status_code: 200, body: "OK"}} ->
:ok
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
This endpoint returns the supervisor version currently running the device api.
"""
@spec supervisor_version() :: {:ok, String.t()} | {:error, term}
def supervisor_version do
case HTTP.get("/v2/version") do
{:ok, %Response{status_code: 200, body: body}} ->
version =
body
|> Jason.decode!()
|> Map.get("version")
{:ok, version}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Use this endpoint to match a service name to a container ID.
"""
@spec container_ids() :: {:ok, map} | {:error, term}
def container_ids() do
case HTTP.get("/v2/containerId") do
{:ok, %Response{status_code: 200, body: body}} ->
services =
body
|> Jason.decode!()
|> Map.get("services")
{:ok, services}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Use this endpoint to match a service name to a container ID.
"""
@spec container_id(String.t()) :: {:ok, String.t()} | {:error, term}
def container_id(service_name) when is_binary(service_name) do
case HTTP.get("/v2/containerId?serviceName=#{service_name}") do
{:ok, %Response{status_code: 200, body: body}} ->
container_id =
body
|> Jason.decode!()
|> Map.get("containerId")
{:ok, container_id}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Get the current target state. Note that if a local mode target state has not
been set then the apps section of the response will always be empty.
Only works on devices which are running in local mode.
"""
@spec local_target_state :: {:ok, map} | {:error, term}
def local_target_state do
case HTTP.get("/v2/local/target-state") do
{:ok, %Response{status_code: 200, body: body}} ->
state =
body
|> Jason.decode!()
|> Map.get("state")
{:ok, state}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Set the current target state.
"""
@spec local_target_state(map) :: :ok | {:error, term}
def local_target_state(config) do
case HTTP.post("/v2/local/target-state", Jason.encode!(config)) do
{:ok, %Response{status_code: 200}} ->
:ok
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Get the architecture and device type of the device.
"""
@spec local_device_info :: {:ok, map} | {:error, term}
def local_device_info do
case HTTP.get("/v2/local/device-info") do
{:ok, %Response{status_code: 200, body: body}} ->
info =
body
|> Jason.decode!()
|> Map.get("info")
{:ok, info}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Stream local mode application logs from the device.
This function starts a process which will send logs to the
creating processes mailbox as they arrive.
"""
@spec local_log_stream() :: {:ok, pid} | {:error, term}
def local_log_stream,
do: BalenaDevice.LocalLogStream.start_link(receiver: self())
@doc """
Get the last returned device name from the balena API. Note that this differs
from the `BALENA_DEVICE_NAME_AT_INIT` environment variable provided to
containers, as this will not change throughout the runtime of the container,
but the endpoint will always return the latest known device name.
"""
@spec device_name() :: {:ok, String.t()} | {:error, term}
def device_name do
case HTTP.get("/v2/device/name") do
{:ok, %Response{status_code: 200, body: body}} ->
name =
body
|> Jason.decode!()
|> Map.get("deviceName")
{:ok, name}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Retrieve any device tags from the balena API. Note that this endpoint will not
work when the device does not have an available connection to the balena API.
"""
@spec device_tags() :: {:ok, [map]} | {:error, term}
def device_tags do
case HTTP.get("/v2/device/tags") do
{:ok, %Response{status_code: 200, body: body}} ->
tags =
body
|> Jason.decode!()
|> Map.get("tags")
{:ok, tags}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
Cleanup volumes with no references
Added in supervisor version v10.0.0
Starting with balena-supervisor v10.0.0, volumes which have no references are
no longer automatically removed as part of the standard update flow. To
cleanup up any orphaned volumes, use this supervisor endpoint.
"""
@spec cleanup_volumes() :: :ok | {:error, term}
def cleanup_volumes do
case HTTP.get("/v2/cleanup-volumess") do
{:ok, %Response{status_code: 200}} ->
:ok
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
@doc """
journald logs
Added in supervisor version v10.2.0
Retrieve a stream to the journald logs on device. This is equivalent to
running `journalctl --no-pager`. Options supported are:
* `all: boolean` Show all fields in full, equivalent to `journalctl --all`.
* `follow: boolean` Continuously stream logs as they are generated, equivalent
to `journalctl --follow`.
* `count: integer` Show the most recent count events, equivalent to
`journalctl --line=<count>`.
* `unit: String.t`
Show journal logs from unit only, equivalent to `journalctl --unit=<unit>`.
* `format: String.t` Added in supervisor version v10.3.0 The format which will
be streamed from journalctl, formats are described here:
https://www.freedesktop.org/software/systemd/man/journalctl.html#-o
"""
@spec journald_logs(journald_options) :: {:ok, String.t()} | {:ok, pid} | {:error, term}
def journald_logs(options \\ []) when is_list(options) do
options
|> Keyword.take(~w[all follow count unit format]a)
|> Enum.into(%{})
|> do_journald_logs()
end
defp do_journald_logs(%{follow: true} = options) do
body = options |> Jason.encode!()
BalenaDevice.JournaldLogStream.start_link(receiver: self(), body: body)
end
defp do_journald_logs(options) do
body = options |> Jason.encode!()
case HTTP.post("/v2/journal-logs", body) do
{:ok, %Response{status_code: 200, body: body}} ->
{:ok, body}
{:ok, %Response{body: body, status_code: status}} ->
{:error, "Status #{status}: #{inspect(body)}"}
{:error, reason} ->
{:error, reason}
end
end
end