• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

coryodaniel / k8s / 807e93631268e5fd52ca29e3e4755088cf11bf27-PR-262

pending completion
807e93631268e5fd52ca29e3e4755088cf11bf27-PR-262

Pull #262

github

mruoss
add possibility to wait for delete
Pull Request #262: add possibility to wait for delete

6 of 6 new or added lines in 1 file covered. (100.0%)

732 of 1009 relevant lines covered (72.55%)

44.92 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

76.47
/lib/k8s/operation.ex
1
defmodule K8s.Operation do
2
  @moduledoc "Encapsulates Kubernetes REST API operations."
3

4
  alias K8s.{Operation, Selector}
5
  alias K8s.Operation.Error
6
  @derive {Jason.Encoder, except: [:path_params, :header_params]}
7

8
  @typedoc "Acceptable patch types"
9
  @type patch_type :: :strategic_merge | :merge | :json_merge | :apply
10

11
  @allow_http_body [:put, :patch, :post]
12
  @selector :labelSelector
13
  @verb_map %{
14
    list_all_namespaces: :get,
15
    watch_all_namespaces: :get,
16
    list: :get,
17
    watch: :get,
18
    deletecollection: :delete,
19
    create: :post,
20
    connect: :post,
21
    update: :put,
22
    patch: :patch,
23
    apply: :patch
24
  }
25

26
  @patch_type_content_types %{
27
    merge: "application/merge-patch+json",
28
    strategic_merge: "application/strategic-merge-patch+json",
29
    json_merge: "application/json-patch+json",
30
    apply: "application/apply-patch+yaml"
31
  }
32

33
  @exec_default_params [stdin: true, stdout: true, stderr: true, tty: false]
34
  @exec_allowed_connect_params [:stdin, :stdout, :stderr, :tty, :command, :container]
35

36
  @log_allowed_connect_params [
37
    :container,
38
    :follow,
39
    :insecureSkipTLSVerifyBackend,
40
    :limitBytes,
41
    :pretty,
42
    :previous,
43
    :sinceSeconds,
44
    :tailLines,
45
    :timestamps
46
  ]
47

48
  defstruct method: nil,
49
            verb: nil,
50
            api_version: nil,
51
            name: nil,
52
            data: nil,
53
            conn: nil,
54
            path_params: [],
55
            query_params: [],
56
            header_params: ["Content-Type": "application/json"]
57

58
  @typedoc "`K8s.Operation` name. May be an atom, string, or tuple of `{resource, subresource}`."
59
  @type name_t :: binary() | atom() | {binary(), binary()}
60

61
  @typedoc """
62
  * `api_version` - API `groupVersion`, AKA `apiVersion`
63
  * `name` - The name of the REST operation (Kubernets kind/resource/subresource). This is *not* _always_ the same as the `kind` key in the `data` field. e.g: `deployments` when POSTing, GETting a deployment.
64
  * `data` - HTTP request body to submit when applicable. (POST, PUT, PATCH, etc)
65
  * `method` - HTTP Method
66
  * `verb` - Kubernetes [REST API verb](https://kubernetes.io/docs/reference/access-authn-authz/authorization/#determine-the-request-verb) (`deletecollection`, `update`, `create`, `watch`, etc)
67
  * `path_params` - Parameters to interpolate into the Kubernetes REST URL
68
  * `query_params` - Query parameters. Merged w/ params provided to any `K8s.Client.Runner`. `K8s.Client.Runner` options win.
69
  * `header_params` - Header parameters.
70

71
  `name` would be `deployments` in the case of a deployment, but may be `deployments/status` or `deployments/scale` for Status and Scale subresources.
72

73
  ## `name` and `data` field examples
74

75
  The following example would `update` the *nginx* deployment's `Scale`. Note the `deployments/scale` operation will have a `Scale` *data* payload:
76

77
  ```elixir
78
  %K8s.Operation{
79
    method: :put,
80
    verb: :update,
81
    api_version: "v1", # api version of the "Scale" kind
82
    name: "deployments/scale",
83
    data: %{"apiVersion" => "v1", "kind" => "Scale"}, # `data` is of kind "Scale"
84
    path_params: [name: "nginx", namespace: "default"],
85
    header_params: ["Content-Type": "application/json"]
86
  }
87
  ```
88

89
  The following example would `update` the *nginx* deployment's `Status`. Note the `deployments/status` operation will have a `Deployment` *data* payload:
90

91
  ```elixir
92
  %K8s.Operation{
93
    method: :put,
94
    verb: :update,
95
    api_version: "apps/v1", # api version of the "Deployment" kind
96
    name: "deployments/status",
97
    data: %{"apiVersion" => "apps/v1", "kind" => "Deployment"}, # `data` is of kind "Deployment"
98
    path_params: [name: "nginx", namespace: "default"],
99
    header_params: ["Content-Type": "application/json"]
100
  }
101
  ```
102
  """
103
  @type t :: %__MODULE__{
104
          method: atom(),
105
          verb: atom(),
106
          api_version: binary(),
107
          name: name_t(),
108
          data: map() | nil,
109
          conn: K8s.Conn.t() | nil,
110
          path_params: keyword(),
111
          query_params: keyword(),
112
          header_params: keyword()
113
        }
114

115
  @doc """
116
  Builds an `Operation` given a verb and a k8s resource.
117

118
  ## Examples
119

120
      iex> deploy = %{"apiVersion" => "apps/v1", "kind" => "Deployment", "metadata" => %{"namespace" => "default", "name" => "nginx"}}
121
      ...> K8s.Operation.build(:update, deploy)
122
      %K8s.Operation{
123
        method: :put,
124
        verb: :update,
125
        data: %{"apiVersion" => "apps/v1", "kind" => "Deployment", "metadata" => %{"namespace" => "default", "name" => "nginx"}},
126
        path_params: [namespace: "default", name: "nginx"],
127
        api_version: "apps/v1",
128
        name: "Deployment"
129
      }
130
  """
131
  @spec build(atom(), map(), keyword()) :: __MODULE__.t()
132
  def build(verb, resource, opts \\ [])
10✔
133

134
  def build(
135
        verb,
136
        %{
137
          "apiVersion" => v,
138
          "kind" => k,
139
          "metadata" => %{"name" => name, "namespace" => ns}
140
        } = resource,
141
        opts
142
      ) do
143
    build(verb, v, k, [namespace: ns, name: name], resource, opts)
13✔
144
  end
145

146
  def build(
147
        verb,
148
        %{"apiVersion" => v, "kind" => k, "metadata" => %{"name" => name}} = resource,
149
        opts
150
      ) do
151
    build(verb, v, k, [name: name], resource, opts)
×
152
  end
153

154
  def build(
155
        verb,
156
        %{"apiVersion" => v, "kind" => k, "metadata" => %{"namespace" => ns}} = resource,
157
        opts
158
      ) do
159
    build(verb, v, k, [namespace: ns], resource, opts)
×
160
  end
161

162
  def build(verb, %{"apiVersion" => v, "kind" => k} = resource, opts) do
163
    build(verb, v, k, [], resource, opts)
×
164
  end
165

166
  @doc """
167
  Builds an `Operation` given an verb and a k8s resource info.
168

169
  *Note:* The `name` here may be a `Kind` and not a REST resource name in the event that the operation was built using a map.
170
  Use `K8s.Discovery.ResourceFinder.resource_name_for_kind/3` to get the correct REST resource name, given a `kind`.
171

172
  ## Examples
173
    Building a GET deployment operation:
174
      iex> K8s.Operation.build(:get, "apps/v1", :deployment, [namespace: "default", name: "nginx"])
175
      %K8s.Operation{
176
        method: :get,
177
        verb: :get,
178
        data: nil,
179
        path_params: [namespace: "default", name: "nginx"],
180
        api_version: "apps/v1",
181
        name: :deployment
182
      }
183

184
  Building a GET deployments/status operation:
185
      iex> K8s.Operation.build(:get, "apps/v1", "deployments/status", [namespace: "default", name: "nginx"])
186
      %K8s.Operation{
187
        method: :get,
188
        verb: :get,
189
        data: nil,
190
        path_params: [namespace: "default", name: "nginx"],
191
        api_version: "apps/v1",
192
        name: "deployments/status"
193
      }
194
  """
195
  @spec build(atom, binary, name_t(), keyword(), map() | nil, keyword()) :: __MODULE__.t()
196
  def build(verb, api_version, name_or_kind, path_params, data \\ nil, opts \\ [])
40✔
197

198
  def build(:apply, api_version, name_or_kind, path_params, data, opts) do
199
    # This is considered deprecated and is only left for BC.
200
    build(
×
201
      :patch,
202
      api_version,
203
      name_or_kind,
204
      path_params,
205
      data,
206
      Keyword.put(opts, :patch_type, :apply)
207
    )
208
  end
209

210
  # credo:disable-for-next-line Credo.Check.Refactor.CyclomaticComplexity
211
  def build(verb, api_version, name_or_kind, path_params, data, opts) do
212
    http_method = @verb_map[verb] || verb
70✔
213
    patch_type = Keyword.get(opts, :patch_type, :not_set)
70✔
214

215
    http_body =
70✔
216
      case http_method do
217
        method when method in @allow_http_body -> data
22✔
218
        _ -> nil
48✔
219
      end
220

221
    query_params =
70✔
222
      cond do
223
        verb === :patch and patch_type === :apply ->
70✔
224
          [
225
            fieldManager: Keyword.get(opts, :field_manager, "elixir"),
226
            force: Keyword.get(opts, :force, true)
227
          ]
228

229
        verb === :connect and name_or_kind === "pods/exec" ->
67✔
230
          @exec_default_params
231
          |> Keyword.merge(opts)
232
          |> Keyword.take(@exec_allowed_connect_params)
3✔
233

234
        verb === :connect and name_or_kind === "pods/log" ->
64✔
235
          Keyword.take(opts, @log_allowed_connect_params)
×
236

237
        true ->
64✔
238
          []
239
      end
240

241
    content_type =
70✔
242
      if verb === :patch do
66✔
243
        Map.get(@patch_type_content_types, patch_type, "application/json")
4✔
244
      else
245
        "application/json"
246
      end
247

248
    header_params =
70✔
249
      opts
250
      |> Keyword.get(:header_params, [])
251
      |> Keyword.put(:"Content-Type", content_type)
252

253
    %__MODULE__{
70✔
254
      method: http_method,
255
      verb: verb,
256
      data: http_body,
257
      api_version: api_version,
258
      name: name_or_kind,
259
      path_params: path_params,
260
      query_params: query_params,
261
      header_params: header_params
262
    }
263
  end
264

265
  @doc "Converts a `K8s.Operation` into a URL path."
266
  @spec to_path(t()) ::
267
          {:ok, String.t()} | {:error, Error.t()}
268
  def to_path(%Operation{} = operation), do: Operation.Path.build(operation)
95✔
269

270
  @deprecated "Use put_selector/2"
271
  @spec put_label_selector(t(), Selector.t()) :: t()
272
  defdelegate put_label_selector(op, selector), to: __MODULE__, as: :put_selector
273

274
  @doc """
275
  Puts a `K8s.Selector` on the operation.
276

277
  ## Examples
278
      iex> operation = %K8s.Operation{}
279
      ...> selector = K8s.Selector.label({"component", "redis"})
280
      ...> K8s.Operation.put_selector(operation, selector)
281
      %K8s.Operation{
282
        query_params: [
283
          labelSelector: %K8s.Selector{
284
            match_expressions: [],
285
            match_labels: %{"component" => {"=", "redis"}}
286
          }
287
        ]
288
      }
289
  """
290
  @spec put_selector(t(), Selector.t()) :: t()
291
  def put_selector(%Operation{} = op, %Selector{} = selector),
292
    do: put_query_param(op, @selector, selector)
34✔
293

294
  @deprecated "Use get_selector/1"
295
  @spec get_label_selector(t()) :: K8s.Selector.t()
296
  defdelegate get_label_selector(operation), to: __MODULE__, as: :get_selector
297

298
  @doc """
299
  Gets a `K8s.Selector` on the operation.
300

301
  ## Examples
302
      iex> operation = %K8s.Operation{query_params: [labelSelector: K8s.Selector.label({"component", "redis"})]}
303
      ...> K8s.Operation.get_selector(operation)
304
      %K8s.Selector{
305
        match_expressions: [],
306
        match_labels: %{"component" => {"=", "redis"}}
307
      }
308
  """
309
  @spec get_selector(t()) :: K8s.Selector.t()
310
  def get_selector(%Operation{query_params: params}),
311
    do: Keyword.get(params, @selector, %K8s.Selector{})
127✔
312

313
  @doc """
314
  Add a query param to an operation
315

316
  ## Examples
317
    Using a `keyword` list of params:
318
      iex> operation = %K8s.Operation{}
319
      ...> K8s.Operation.put_query_param(operation, :foo, "bar")
320
      %K8s.Operation{query_params: [foo: "bar"]}
321
  """
322
  @spec put_query_param(t(), any(), String.t() | K8s.Selector.t()) :: t()
323
  def put_query_param(%Operation{query_params: params} = op, key, value) when is_list(params) do
324
    new_params = Keyword.put(params, key, value)
35✔
325
    %Operation{op | query_params: new_params}
35✔
326
  end
327

328
  # covers when query_params are a keyword list for operations like for Pod Connect
329
  def put_query_param(%Operation{query_params: params} = op, opts)
330
      when is_list(opts) and is_list(params) do
331
    new_params = params ++ opts
×
332
    %Operation{op | query_params: new_params}
×
333
  end
334

335
  @spec put_query_param(t(), list() | K8s.Selector.t()) :: t()
336
  def put_query_param(%Operation{query_params: _params} = op, opts) when is_list(opts) do
337
    %Operation{op | query_params: opts}
×
338
  end
339

340
  @doc """
341
  Get a query param of an operation
342

343
  ## Examples
344
    Using a `keyword` list of params:
345
      iex> operation = %K8s.Operation{query_params: [foo: "bar"]}
346
      ...> K8s.Operation.get_query_param(operation, :foo)
347
      "bar"
348
  """
349
  @spec get_query_param(t(), atom()) :: any()
350
  def get_query_param(%Operation{query_params: params}, key), do: Keyword.get(params, key)
1✔
351

352
  @doc """
353
  Set the connection object on the operation
354

355
  ## Examples
356
      iex> operation = %K8s.Operation{query_params: [foo: "bar"]}
357
      ...> conn = %K8s.Conn{}
358
      ...> operation = K8s.Operation.put_conn(operation, conn)
359
      ...> match?(%K8s.Operation{conn: %K8s.Conn{}}, operation)
360
      true
361
  """
362
  @spec put_conn(t(), K8s.Conn.t()) :: t()
363
  def put_conn(operation, conn), do: struct!(operation, conn: conn)
10✔
364
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc