Packages
k8s
2.0.0-rc.2
2.8.0
2.7.0
2.6.2
2.6.1
2.6.0
2.5.0
2.4.2
2.4.1
2.4.0
2.3.0
2.2.0
2.1.1
2.1.0
2.0.3
2.0.2
2.0.1
2.0.0
2.0.0-rc.6
2.0.0-rc.5
2.0.0-rc.4
2.0.0-rc.3
2.0.0-rc.2
2.0.0-rc.1
2.0.0-rc.0
1.2.1
1.2.0
1.1.10
1.1.9
1.1.8
1.1.7
1.1.6
1.1.5
1.1.4
1.1.3
1.1.2
1.1.1
1.1.0
1.0.0
1.0.0-rc1
0.5.2
0.5.1
0.5.0
0.5.0-rc.2
0.5.0-rc.1
0.4.0
0.3.2
0.3.1
0.3.0
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
retired
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.3
Kubernetes API Client for Elixir
Current section
Files
Jump to
Current section
Files
lib/k8s/client/runner/stream/watch.ex
defmodule K8s.Client.Runner.Stream.Watch do
@moduledoc """
Watches a `K8s.Client.list/3` operation and returns an Elixir [`Stream`](https://hexdocs.pm/elixir/Stream.html) of events.
"""
alias K8s.Client.Runner.Base
require Logger
@log_prefix "#{__MODULE__} - " |> String.replace_leading("Elixir.", "")
@type t :: %__MODULE__{
conn: K8s.Conn.t(),
operation: K8s.Operation.t(),
resource_version: binary(),
http_opts: keyword(),
remainder: binary()
}
defstruct [:conn, :operation, :resource_version, :http_opts, remainder: ""]
@doc """
Watches resources and returns an Elixir Stream of events emmitted by kubernetes.
### Example
iex> {:ok,conn} = K8s.Conn.from_file("~/.kube/config", [context: "docker-desktop"])
...> op = K8s.Client.list("v1", "Namespace")
...> K8s.Client.Runner.Watch.Stream.stream(conn, op, []) |> Stream.map(&IO.inspect/1) |> Stream.run()
"""
@spec stream(K8s.Conn.t(), K8s.Operation.t(), keyword()) ::
{:ok, Enumerable.t()} | K8s.Client.Provider.error_t()
def stream(conn, operation, http_opts) do
http_opts =
http_opts
|> Keyword.put_new(:params, [])
|> put_in([:params, :allowWatchBookmarks], true)
|> put_in([:params, :watch], 1)
do_resource(conn, struct!(operation, verb: :list), http_opts, nil)
end
@spec do_resource(K8s.Conn.t(), K8s.Operation.t(), keyword(), nil | binary()) ::
{:ok, Enumerable.t()} | K8s.Client.Provider.error_t()
defp do_resource(conn, operation, http_opts, nil) do
with {:ok, resource_version} <- get_resource_version(conn, operation) do
do_resource(conn, operation, http_opts, resource_version)
end
end
defp do_resource(conn, operation, http_opts, resource_version) do
with http_opts <- put_in(http_opts, [:params, :resourceVersion], resource_version),
{:ok, stream} <- Base.stream(conn, operation, http_opts) do
stream =
stream
|> K8s.Client.HTTPStream.transform_to_lines()
|> K8s.Client.HTTPStream.decode_json_objects()
|> Stream.reject(&(&1 == {:status, 200}))
|> Stream.filter(&(&1 == :done || elem(&1, 0) in [:status, :object, :error]))
|> Stream.transform(
%__MODULE__{
conn: conn,
operation: operation,
http_opts: http_opts,
resource_version: resource_version
},
&reduce/2
)
{:ok, stream}
end
end
@spec reduce(K8s.Client.Provider.http_chunk_t(), t() | :halt) ::
{:halt, nil} | {Enumerable.t(), t()}
defp reduce(_, :halt), do: {:halt, nil}
defp reduce(:done, state) do
{:ok, stream} =
do_resource(state.conn, state.operation, state.http_opts, state.resource_version)
{stream, state}
end
defp reduce({:status, 410}, state) do
Logger.warn(
@log_prefix <> "410 Gone received from watcher - resetting the resource version",
library: :k8s
)
{:ok, stream} = do_resource(state.conn, state.operation, state.http_opts, nil)
{stream, :halt}
end
defp reduce({:status, status}, _state) do
Logger.warn(
@log_prefix <> "Erronous async status #{status} received from watcher - aborting the watch",
library: :k8s
)
{:halt, nil}
end
defp reduce({:object, object}, state) do
process_object(object, state)
end
@spec process_object(map(), t()) :: {Enumerable.t(), t()}
defp process_object(
%{"type" => "ERROR", "object" => %{"message" => message, "code" => 410} = object},
state
) do
Logger.debug(
@log_prefix <> "#{message} - resetting the resource version",
library: :k8s,
object: object
)
new_state = struct!(state, resource_version: nil)
{[], new_state}
end
defp process_object(%{"object" => %{"message" => message} = object}, state) do
Logger.error(
@log_prefix <>
"Erronous event received from watcher: #{message} - resetting the resource version",
library: :k8s,
object: object
)
new_state = struct!(state, resource_version: nil)
{[], new_state}
end
defp process_object(%{"type" => "BOOKMARK", "object" => object}, state) do
Logger.debug(
@log_prefix <> "Bookmark received",
library: :k8s,
object: object
)
{[], struct!(state, resource_version: object["metadata"]["resourceVersion"])}
end
defp process_object(
%{"object" => %{"metadata" => %{"resourceVersion" => resource_version}}},
state
)
when resource_version == state.resource_version do
{[], state}
end
defp process_object(%{"object" => object} = new_event, state) do
new_state = struct!(state, resource_version: object["metadata"]["resourceVersion"])
{[new_event], new_state}
end
@spec get_resource_version(K8s.Conn.t(), K8s.Operation.t()) :: {:ok, binary} | Base.error_t()
defp get_resource_version(%K8s.Conn{} = conn, %K8s.Operation{} = operation) do
with {:ok, payload} <- Base.run(conn, operation) do
rv = parse_resource_version(payload)
{:ok, rv}
end
end
@resource_version_json_path ~w(metadata resourceVersion)
@spec parse_resource_version(any) :: binary
defp parse_resource_version(%{} = payload),
do: get_in(payload, @resource_version_json_path) || "0"
defp parse_resource_version(_), do: "0"
end