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

mruoss / kompost / d3d3b53dc1f761432ab8cd958b74b2b23b4427ad-PR-27

pending completion
d3d3b53dc1f761432ab8cd958b74b2b23b4427ad-PR-27

Pull #27

github

mruoss
fix ssl connection and add ca
Pull Request #27: SSL support

22 of 22 new or added lines in 3 files covered. (100.0%)

74 of 532 relevant lines covered (13.91%)

4.56 hits per line

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

0.0
/lib/kompost/kompo/postgres/controller/instance_controller.ex
1
defmodule Kompost.Kompo.Postgres.Controller.InstanceController do
2
  use Bonny.ControllerV2
3

4
  require Logger
5

6
  alias Kompost.Kompo.Postgres.Instance
7

8
  alias Kompost.Tools.NamespaceAccess
9

10
  step Bonny.Pluggable.SkipObservedGenerations
11

12
  step Bonny.Pluggable.Finalizer,
13
    id: "kompost.chuge.li/databases",
14
    impl: &__MODULE__.check_for_depending_databases/1,
15
    add_to_resource: true,
16
    log_level: :debug
17

18
  step Kompost.Pluggable.InitConditions, conditions: ["Credentials", "Connected", "Privileged"]
19
  step :handle_event
20

21
  @impl true
22
  def rbac_rules() do
×
23
    [to_rbac_rule({"", "secrets", ["get", "list"]})]
24
  end
25

26
  @spec handle_event(Bonny.Axn.t(), Keyword.t()) :: Bonny.Axn.t()
27
  def handle_event(%Bonny.Axn{action: action} = axn, _opts)
28
      when action in [:add, :modify, :reconcile] do
29
    id = instance_id(axn.resource)
×
30
    allowed_ns = allowed_namespaces(axn.resource)
×
31

32
    with {:cred, {:ok, connection_args}} <- {:cred, get_connection_args(axn.resource, axn.conn)},
×
33
         axn <- set_condition(axn, "Credentials", true),
×
34
         {:conn, axn, {:ok, conn}} <-
×
35
           {:conn, axn, Instance.connect(id, connection_args, allowed_ns)},
36
         axn <- set_condition(axn, "Connected", true, "Connection to database was established"),
×
37
         {:privileges, axn, :ok} <- {:privileges, axn, Instance.check_privileges(conn)},
×
38
         axn <-
×
39
           set_condition(axn, "Privileged", true, "The conneted user has the required privileges") do
40
      success_event(axn)
×
41
    else
42
      {:cred, {:error, error}} ->
43
        Logger.warning("Error when trying to fetch password.", error: error)
×
44

45
        axn
46
        |> failure_event(message: Exception.message(error))
47
        |> set_condition("Credentials", false, Exception.message(error))
×
48

49
      {:conn, axn, {:error, error}} ->
50
        message = if is_exception(error), do: Exception.message(error), else: inspect(error)
×
51
        Logger.warning("#{axn.action} failed. #{message}")
×
52

53
        axn
54
        |> failure_event(message: message)
55
        |> set_condition("Connected", false, message)
×
56

57
      {:privileges, axn, {:error, message}} ->
58
        Logger.warning("#{axn.action} failed. #{message}")
×
59

60
        axn
61
        |> failure_event(message: message)
62
        |> set_condition("Privileged", false, message)
×
63
    end
64
  end
65

66
  def handle_event(%Bonny.Axn{action: :delete} = axn, _opts) do
67
    :ok =
×
68
      axn.resource
×
69
      |> instance_id()
70
      |> Instance.disconnect()
71

72
    success_event(axn)
×
73
  end
74

75
  @spec get_connection_args(map(), K8s.Conn.t()) ::
76
          {:ok, [Postgrex.start_option()]} | {:error, K8s.Client.Runner.Base.error_t()}
77
  defp get_connection_args(%{"spec" => %{"plainPassword" => _} = spec}, _conn) do
78
    cacerts =
×
79
      case spec["ssl"]["ca"] do
80
        nil ->
81
          :public_key.cacerts_get()
×
82

83
        cacert ->
84
          cacert
85
          |> :public_key.pem_decode()
86
          |> Enum.map(fn {_, der, _} -> der end)
×
87
      end
88

89
    {:ok,
90
     [
91
       hostname: spec["hostname"],
92
       port: spec["port"],
93
       username: spec["username"],
94
       password: spec["plainPassword"],
95
       database: "postgres",
96
       ssl: spec["ssl"]["enabled"] || false,
×
97
       ssl_opts: [
98
         verify: (spec["ssl"]["verify"] || "verify_none") |> String.to_atom(),
×
99
         cacerts: cacerts,
100
         server_name_indication: String.to_charlist(spec["hostname"])
101
       ]
102
     ]}
103
  end
104

105
  defp get_connection_args(instance, conn) do
106
    %{
107
      "metadata" => %{"namespace" => namespace},
108
      "spec" => %{"passwordSecretRef" => %{"name" => secret_name, "key" => key} = secret_ref}
×
109
    } = instance
110

×
111
    with {:ok, secret} <-
112
           K8s.Client.get("v1", "Secret",
113
             name: secret_name,
114
             namespace: secret_ref["namespace"] || namespace
×
115
           )
×
116
           |> K8s.Client.put_conn(conn)
117
           |> K8s.Client.run() do
118
      password = Base.decode64!(secret["data"][key])
119
      instance = put_in(instance, ~w(spec plainPassword), password)
×
120
      get_connection_args(instance, conn)
×
121
    end
×
122
  end
123

124
  @doc """
125
  A finalizer preventing the deletion of an instance if there are database
126
  resources in the cluster which still depend on it.
127
  """
128
  @spec check_for_depending_databases(Bonny.Axn.t()) ::
129
          {:ok, Bonny.Axn.t()} | {:error, Bonny.Axn.t()}
130
  def check_for_depending_databases(%Bonny.Axn{resource: resource, conn: conn} = axn) do
131
    {:ok, result} =
132
      K8s.Client.list("kompost.chuge.li/v1alpha1", "PostgresDatabase", namespace: :all)
×
133
      |> K8s.Client.put_conn(conn)
134
      |> K8s.Client.run()
135

136
    if Enum.any?(
137
         result["items"],
×
138
         &(is_nil(&1["metadata"]["deletionTimestamp"]) &&
139
             &1["spec"]["instanceRef"]["name"] == resource["metadata"]["name"] and
×
140
             &1["spec"]["instanceRef"]["namespace"] == resource["metadata"]["namespace"])
×
141
       ) do
×
142
      {:error, axn}
143
    else
144
      {:ok, axn}
145
    end
146
  end
147

148
  @spec instance_id(resource :: map()) :: Instance.id()
149
  defp instance_id(%{"kind" => "PostgresInstance", "metadata" => metadata}),
150
    do: {metadata["namespace"], metadata["name"]}
×
151

152
  defp instance_id(%{"kind" => "PostgresClusterInstance", "metadata" => metadata}),
153
    do: {:cluster, metadata["name"]}
×
154

155
  @spec allowed_namespaces(map()) :: NamespaceAccess.allowed_namespaces()
156
  defp(allowed_namespaces(%{"kind" => "PostgresInstance"} = resource)) do
157
    [~r/^#{resource["metadata"]["namespace"]}$/]
×
158
  end
×
159

160
  defp allowed_namespaces(%{"kind" => "PostgresClusterInstance"} = resource) do
161
    NamespaceAccess.allowed_namespaces!(resource)
162
  end
×
163
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