K8s.Operation
s are Kubernetes REST operations. They encapsulate all the details of an HTTP request except the server to perform them against.
Many more client examples exist in the K8s.Client
docs.
resource = %{
"apiVersion" => "apps/v1",
"kind" => "Deployment",
"metadata" => %{
"labels" => %{"app" => "nginx"},
"name" => "nginx-deployment",
"namespace" => "default"
},
"spec" => %{
"replicas" => 3,
"selector" => %{"matchLabels" => %{"app" => "nginx"}},
"template" => %{
"metadata" => %{"labels" => %{"app" => "nginx"}},
"spec" => %{
"containers" => [
%{
"image" => "nginx:1.7.9",
"name" => "nginx",
"ports" => [%{"containerPort" => 80}]
}
]
}
}
}
}
operation = K8s.Client.create(resource)
{:ok, conn} = K8s.Conn.from_file("path/to/kubeconfig.yaml")
{:ok, response} = K8s.Client.run(conn, operation)
K8s.Resource
provides YAML resource parsing and interpolation support as well as a few helper functions for accessing common Kubernetes resource fields.
Given the YAML file priv/deployment.yaml
:
apiVersion: apps/v1
kind: Deployment
metadata:
name: <%= name %>-deployment
namespace: <%= namespace %>
labels:
app: <%= name %>
spec:
replicas: 3
selector:
matchLabels:
app: <%= name %>
template:
metadata:
labels:
app: <%= name %>
spec:
containers:
- name: <%= name %>
image: <%= image %>
ports:
- containerPort: 80
opts = [namespace: "default", name: "nginx", image: "nginx:nginx:1.7.9"]
resource = K8s.Resource.from_file!("priv/deployment.yaml", opts)
operation = K8s.Client.create(resource)
{:ok, conn} = K8s.Conn.from_file("path/to/kubeconfig.yaml")
{:ok, deployment} = K8s.Client.run(conn, operation)
In a given namespace:
operation = K8s.Client.list("apps/v1", "Deployment", namespace: "prod")
{:ok, conn} = K8s.Conn.from_file("path/to/kubeconfig.yaml")
{:ok, deployments} = K8s.Client.run(conn, operation)
Across all namespaces:
operation = K8s.Client.list("apps/v1", "Deployment", namespace: :all)
{:ok, conn} = K8s.Conn.from_file("path/to/kubeconfig.yaml")
{:ok, deployments} = K8s.Client.run(conn, operation)
Note: K8s.Client.list
will return a map
. The list of resources will be under "items"
.
K8s.Selector
supports programatically building Kubernetes labelSelector
s.
{:ok, conn} = K8s.Conn.from_file("path/to/kubeconfig.yaml")
operation =
K8s.Client.list("apps/v1", :deployments)
|> K8s.Selector.label({"app", "nginx"})
|> K8s.Selector.label_in({"environment", ["qa", "prod"]})
K8s.Client.run(conn, operation)
{:ok, conn} = K8s.Conn.from_file("path/to/kubeconfig.yaml")
operation = K8s.Client.get("apps/v1", :deployment, [namespace: "default", name: "nginx-deployment"])
{:ok, deployment} = K8s.Client.run(conn, operation)
Watch operations use the Kubernetes Watch API to stream added
, modified
, and deleted
as they occur.
To get a stream of events:
operation = K8s.Client.watch("apps/v1", :deployment, namespace: :all)
{:ok, conn} = K8s.Conn.from_file("path/to/kubeconfig.yaml")
{:ok, event_stream} = K8s.Client.stream(conn, operation)
The wait runner permits read operations to be made and block until a certain state is met in Kubernetes.
This follow example will wait 60 seconds for the field status.succeeded
to equal 1
.
operation = K8s.Client.get("batch/v1", :job, namespace: "default", name: "database-migrator")
wait_opts = [find: ["status", "succeeded"], eval: 1, timeout: 60]
{:ok, conn} = K8s.Conn.from_file("path/to/kubeconfig.yaml")
{:ok, job} = K8s.Client.wait_until(conn, operation, wait_opts)
:find
and :eval
also accept functions to apply to check success.
An async runner is provided for running operations in parallel. All operations are fired async and their results are returned. Processing does not halt if an error occurs for one operation.
operation1 = K8s.Client.get("v1", "Pod", namespace: "default", name: "pod-1")
operation2 = K8s.Client.get("v1", "Pod", namespace: "default", name: "pod-2")
{:ok, conn} = K8s.Conn.from_file("path/to/kubeconfig.yaml")
results = K8s.Client.async(conn, [operation1, operation2])
results
will be a list of :ok
and :error
tuples.
A stream runner is provided to automatically handle pagination in K8s.Client.list/3
operations.
operation = K8s.Client.list("v1", "Pod", namespace: :all)
{:ok, conn} = K8s.Conn.from_file("path/to/kubeconfig.yaml")
conn
|> K8s.Client.stream(operation)
|> Stream.filter(&my_filter_function?/1)
|> Stream.map(&my_map_function?/1)
|> Enum.into([])
Use the :connect
operation to connect to the pods/exec
subresource and
execute commands. A :connect
operation is created with K8s.Client.connect/N
.
When connecting to pods/exec
, be sure to pass the command you want to run in
the options.
If you want to run a command that terminates and wait for it, pass the :connect
operation to K8s.Client.run/N
.
{:ok, conn} = K8s.Conn.from_file("~/.kube/config")
op = K8s.Client.connect(
"v1",
"pods/exec",
[namespace: "default", name: "nginx-8f458dc5b-zwmkb"],
command: ["/bin/sh", "-c", "nginx -t"]
)
{:ok, response} = K8s.Client.run(conn, op)
If you send a command that does not terminate (e.g. /bin/sh
) or one that takes
long to terminate, you can use K8s.Client.stream_to/3
which opens a connection
in a separate process and sends messages to your pid. The function returns a
callback that you can use to send messages to the pod.
The following is a very simple example. In your code you'd rather use a
GenServer
or so to track the state.
defmodule TestPodExec do
require Logger
def recv_loop() do
receive do
{:open, true} ->
Logger.debug("Connection established.")
recv_loop()
{:stdout, msg} ->
Logger.info(~s(Message received on stdout: "#{msg}"))
recv_loop()
{:close, reason} ->
Logger.debug("Connection closed. Reason: #{inspect(reason)}")
end
end
def run() do
{:ok, pid} =
Task.start_link(fn ->
recv_loop()
end)
{:ok, conn} = K8s.Conn.from_file("~/.kube/config")
op = K8s.Client.connect(
"v1",
"pods/exec",
[namespace: "default", name: "nginx-8f458dc5b-zwmkb"],
command: ["/bin/sh"],
tty: true
)
{:ok, send_to_pod} = K8s.Client.stream_to(conn, op, pid)
Process.sleep(1000)
send_to_pod.({:stdin, ~s(echo "hello world"\n)})
Process.sleep(1000)
send_to_pod.(:close)
Process.sleep(1000)
end
end
TestPodExec.run()
What you should see:
- A DEBUG log stmt saying
Connection established.
- A couple of stdout messages. It is not deterministic whether the pod sends
them in one chunk or in multiple:
- Shell prompt (e.g.
#
or$
) - Unless we opt out of it (
stty -echo
), the pod sends back what we "type":echo "hello world"\n
- The actual echo:
hello world
- The next shell prompt
- Shell prompt (e.g.
- A DEBUG log stmt saying
Connection closed.
If we concatenate all the stdout messages you see it reads what you would see on a shell:
$ echo "hello world"
hello world
$
command
- required for running commandscontainer
- if a pod runs multiple containers, you have to specify the container to run the command in.stdin
- enable stdin (defaults totrue
)stdout
- enable stdout (defaults totrue
)stderr
- enable stderr (defaults totrue
)tty
- stdin is a TTY (defaults tofalse
)
{:ok, conn} = K8s.Conn.from_file("~/.kube/config")
op = K8s.Client.connect(
"v1",
"pods/exec",
[namespace: "default", name: "nginx-8f458dc5b-zwmkb"],
command: ["/bin/sh", "-c", "nginx -t"],
container: "main",
tty: true
)
{:ok, response} = K8s.Client.run(conn, op)
Use the :connect
operation to connect to the pods/log
subresource. A
:connect
operation is created with K8s.Client.connect/N
.
Refer to the Kubernetes documentation for documentation on these options.
container
follow
- Use withK8s.Client.stream/N
orK8s.Client.stream_to/N
.insecureSkipTLSVerifyBackend
limitBytes
pretty
previous
sinceSeconds
tailLines
timestamps
{:ok, conn} = K8s.Conn.from_file("~/.kube/config")
{:ok, stream} = K8s.Client.connect(
"v1",
"pods/log",
[namespace: "default", name: "nginx-8f458dc5b-zwmkb"],
command: ["/bin/sh", "-c", "nginx -t"],
container: "main",
follow: true
)
|> K8s.Client.put_conn(conn)
|> K8s.Client.stream()