Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add further patch mechanisms #229

Merged
merged 4 commits into from
Feb 25, 2023
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

<!-- ### Added | Changed | Deprecated | Removed | Fixed | Security -->

### Added

- Added further PATCH mechanisms - [#229](https://github.com/coryodaniel/k8s/pull/229)

<!--------------------- Don't add new entries after this line --------------------->

## [2.0.3] - 2023-02-17
Expand Down
36 changes: 21 additions & 15 deletions lib/k8s/client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,20 @@ defmodule K8s.Client do
...> K8s.Client.apply(deployment, field_manager: "my-operator", force: true)
%K8s.Operation{
method: :patch,
verb: :apply,
verb: :patch,
api_version: "apps/v1",
name: "Deployment",
path_params: [namespace: "test", name: "nginx"],
data: K8s.Resource.from_file!("test/support/manifests/nginx-deployment.yaml"),
query_params: [fieldManager: "my-operator", force: true]
query_params: [fieldManager: "my-operator", force: true],
header_params: ["Content-Type": "application/apply-patch+yaml"],
}
"""
@spec apply(map(), keyword()) :: Operation.t()
def apply(resource, mgmt_params \\ []) do
field_manager = Keyword.get(mgmt_params, :field_manager, @mgmt_param_defaults[:field_manager])
force = Keyword.get(mgmt_params, :force, @mgmt_param_defaults[:force])
Operation.build(:apply, resource, field_manager: field_manager, force: force)
Operation.build(:patch, resource, field_manager: field_manager, force: force, patch_type: :apply)
end

@doc """
Expand All @@ -111,12 +112,13 @@ defmodule K8s.Client do
...> K8s.Client.apply("v1", "pods/status", [namespace: "default", name: "nginx"], pod_with_status_subresource, field_manager: "my-operator", force: true)
%K8s.Operation{
method: :patch,
verb: :apply,
verb: :patch,
api_version: "v1",
name: "pods/status",
path_params: [namespace: "default", name: "nginx"],
data: K8s.Resource.from_file!("test/support/manifests/nginx-pod.yaml") |> Map.put("status", %{"message" => "some message"}),
query_params: [fieldManager: "my-operator", force: true]
query_params: [fieldManager: "my-operator", force: true],
header_params: ["Content-Type": "application/apply-patch+yaml"]
}
"""
@spec apply(binary, Operation.name_t(), Keyword.t(), map(), keyword()) :: Operation.t()
Expand All @@ -130,9 +132,10 @@ defmodule K8s.Client do
field_manager = Keyword.get(mgmt_params, :field_manager, @mgmt_param_defaults[:field_manager])
force = Keyword.get(mgmt_params, :force, @mgmt_param_defaults[:force])

Operation.build(:apply, api_version, kind, path_params, subresource,
Operation.build(:patch, api_version, kind, path_params, subresource,
field_manager: field_manager,
force: force
force: force,
patch_type: :apply
)
end

Expand Down Expand Up @@ -436,37 +439,40 @@ defmodule K8s.Client do
api_version: "apps/v1",
name: "Deployment",
path_params: [namespace: "test", name: "nginx"],
data: K8s.Resource.from_file!("test/support/manifests/nginx-deployment.yaml")
data: K8s.Resource.from_file!("test/support/manifests/nginx-deployment.yaml"),
header_params: ["Content-Type": "application/merge-patch+json"]
}
"""
@spec patch(map()) :: Operation.t()
def patch(%{} = resource), do: Operation.build(:patch, resource)
def patch(%{} = resource), do: Operation.build(:patch, resource, patch_type: :merge)

@doc """
Returns a `PATCH` operation to patch the given subresource given a resource's details and a subresource map.
"""
@spec patch(binary, Operation.name_t(), Keyword.t(), map()) :: Operation.t()
def patch(api_version, kind, path_params, subresource),
do: Operation.build(:patch, api_version, kind, path_params, subresource)
@spec patch(binary, Operation.name_t(), Keyword.t(), map(), patch_type :: Operation.patch_type()) :: Operation.t()
def patch(api_version, kind, path_params, subresource, patch_type \\ :merge),
do: Operation.build(:patch, api_version, kind, path_params, subresource, patch_type: patch_type)

@doc """
Returns a `PATCH` operation to patch the given subresource given a resource map and a subresource map.
"""
@spec patch(map(), map()) :: Operation.t()
@spec patch(map(), map(), patch_type :: Operation.patch_type()) :: Operation.t()
def patch(
%{
"apiVersion" => api_version,
"kind" => kind,
"metadata" => %{"namespace" => ns, "name" => name}
},
%{"kind" => subkind} = subresource
%{"kind" => subkind} = subresource,
patch_type \\ :merge
) do
Operation.build(
:patch,
api_version,
{kind, subkind},
[namespace: ns, name: name],
subresource
subresource,
patch_type: patch_type
)
end

Expand Down
8 changes: 1 addition & 7 deletions lib/k8s/client/runner/base.ex
Original file line number Diff line number Diff line change
Expand Up @@ -199,13 +199,7 @@ defmodule K8s.Client.Runner.Base do
defp new_request(%Conn{} = conn, url, %Operation{} = operation, body, http_opts) do
req = %Request{conn: conn, method: operation.method, body: body}

headers =
case operation.verb do
:patch -> ["Content-Type": "application/merge-patch+json"]
:apply -> ["Content-Type": "application/apply-patch+yaml"]
_ -> ["Content-Type": "application/json"]
end

headers = operation.header_params
operation_query_params = build_query_params(operation)
http_opts_params = Keyword.get(http_opts, :params, [])
merged_params = Keyword.merge(operation_query_params, http_opts_params)
Expand Down
53 changes: 43 additions & 10 deletions lib/k8s/operation.ex
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ defmodule K8s.Operation do

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

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

@allow_http_body [:put, :patch, :post]
@selector :labelSelector
Expand All @@ -20,14 +23,22 @@ defmodule K8s.Operation do
apply: :patch
}

@patch_type_header_map %{
merge: ["Content-Type": "application/merge-patch+json"],
strategic_merge: ["Content-Type": "application/strategic-merge-patch+json"],
json_merge: ["Content-Type": "application/json-patch+json"],
apply: ["Content-Type": "application/apply-patch+yaml"]
}

defstruct method: nil,
verb: nil,
api_version: nil,
name: nil,
data: nil,
conn: nil,
path_params: [],
query_params: []
query_params: [],
header_params: ["Content-Type": "application/json"]

@typedoc "`K8s.Operation` name. May be an atom, string, or tuple of `{resource, subresource}`."
@type name_t :: binary() | atom() | {binary(), binary()}
Expand All @@ -40,6 +51,7 @@ defmodule K8s.Operation do
* `verb` - Kubernetes [REST API verb](https://kubernetes.io/docs/reference/access-authn-authz/authorization/#determine-the-request-verb) (`deletecollection`, `update`, `create`, `watch`, etc)
* `path_params` - Parameters to interpolate into the Kubernetes REST URL
* `query_params` - Query parameters. Merged w/ params provided to any `K8s.Client.Runner`. `K8s.Client.Runner` options win.
* `header_params` - Header parameters.

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

Expand All @@ -54,7 +66,8 @@ defmodule K8s.Operation do
api_version: "v1", # api version of the "Scale" kind
name: "deployments/scale",
data: %{"apiVersion" => "v1", "kind" => "Scale"}, # `data` is of kind "Scale"
path_params: [name: "nginx", namespace: "default"]
path_params: [name: "nginx", namespace: "default"],
header_params: ["Content-Type": "application/json"]
}
```

Expand All @@ -67,7 +80,8 @@ defmodule K8s.Operation do
api_version: "apps/v1", # api version of the "Deployment" kind
name: "deployments/status",
data: %{"apiVersion" => "apps/v1", "kind" => "Deployment"}, # `data` is of kind "Deployment"
path_params: [name: "nginx", namespace: "default"]
path_params: [name: "nginx", namespace: "default"],
header_params: ["Content-Type": "application/json"]
}
```
"""
Expand All @@ -79,7 +93,8 @@ defmodule K8s.Operation do
data: map() | nil,
conn: K8s.Conn.t() | nil,
path_params: keyword(),
query_params: keyword()
query_params: keyword(),
header_params: keyword()
}

@doc """
Expand Down Expand Up @@ -165,6 +180,7 @@ defmodule K8s.Operation do
@spec build(atom, binary, name_t(), keyword(), map() | nil, keyword()) :: __MODULE__.t()
def build(verb, api_version, name_or_kind, path_params, data \\ nil, opts \\ []) do
http_method = @verb_map[verb] || verb
patch_type = Keyword.get(opts, :patch_type, :not_set)

http_body =
case http_method do
Expand All @@ -173,31 +189,48 @@ defmodule K8s.Operation do
end

query_params =
case verb do
:apply ->
cond do
verb === :apply || (verb === :patch && patch_type === :apply) ->
[
fieldManager: Keyword.get(opts, :field_manager, "elixir"),
force: Keyword.get(opts, :force, true)
]

:connect ->
verb === :connect ->
[stdin: true, stdout: true, stderr: true, tty: false]
|> Keyword.merge(
Keyword.take(opts, [:stdin, :stdout, :stderr, :tty, :command, :container])
)

_ ->
true ->
[]
end

header_params =
case {verb, patch_type} do
{:patch, merge_patch_types} when merge_patch_types in [:merge, :not_set] ->
@patch_type_header_map[:merge]
{:patch, :strategic_merge} ->
@patch_type_header_map[:strategic_merge]
{:patch, :json_merge} ->
@patch_type_header_map[:json_merge]
{:patch, :apply} ->
@patch_type_header_map[:apply]
{:apply, apply_patch_types} when apply_patch_types in [:apply, :not_set] ->
@patch_type_header_map[:apply]
_ -> ["Content-Type": "application/json"]
end
|> Keyword.merge(Keyword.get(opts, :header_params, []))

%__MODULE__{
method: http_method,
verb: verb,
data: http_body,
api_version: api_version,
name: name_or_kind,
path_params: path_params,
query_params: query_params
query_params: query_params,
header_params: header_params
}
end

Expand Down