• 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/database_controller.ex
1
defmodule Kompost.Kompo.Postgres.Controller.DatabaseController do
2
  use Bonny.ControllerV2
3

4
  require Logger
5

6
  alias Kompost.Kompo.Postgres.Database
7
  alias Kompost.Kompo.Postgres.Database.Params
8
  alias Kompost.Kompo.Postgres.Instance
9
  alias Kompost.Kompo.Postgres.Privileges
10
  alias Kompost.Kompo.Postgres.User
11

12
  alias Kompost.Tools.NamespaceAccess
13
  alias Kompost.Tools.Password
14

15
  import YamlElixir.Sigil
16

17
  step Bonny.Pluggable.SkipObservedGenerations
18

19
  step Kompost.Pluggable.InitConditions, conditions: ["Connection", "AppUser", "InspectorUser"]
20

21
  step Bonny.Pluggable.Finalizer,
22
    id: "kompost.chuge.li/delete-resources",
23
    impl: &__MODULE__.delete_resources/1,
24
    add_to_resource: &__MODULE__.add_finalizer?/1,
25
    log_level: :debug
26

27
  step :handle_event
28

29
  @impl true
30
  def rbac_rules() do
×
31
    [to_rbac_rule({"", "secrets", ["*"]})]
32
  end
33

34
  @spec handle_event(Bonny.Axn.t(), Keyword.t()) :: Bonny.Axn.t()
35
  def handle_event(%Bonny.Axn{action: action} = axn, _opts)
36
      when action in [:add, :modify, :reconcile] do
37
    resource = axn.resource
×
38
    namespace = resource["metadata"]["namespace"]
×
39
    db_name = Database.name(resource)
×
40
    db_params = Params.new!(resource["spec"]["params"])
×
41
    instance = resource |> instance_id() |> Instance.lookup()
×
42

43
    with {:instance, [{conn, conn_args, allowed_namespaces}]} <- {:instance, instance},
×
44
         axn <-
×
45
           set_condition(
46
             axn,
47
             "Connection",
48
             true,
49
             "Connected to the referenced PostgreSQL instance."
50
           ),
51
         {:can_access, axn, true} <-
×
52
           {:can_access, axn, NamespaceAccess.can_access?(namespace, allowed_namespaces)},
53
         {:database, axn, :ok} <- {:database, axn, Database.apply(db_name, db_params, conn)},
×
54
         axn <-
×
55
           set_condition(
56
             axn,
57
             "Database",
58
             true,
59
             ~s(The database "#{db_name}" was created on the PostgreSQL instance)
×
60
           ),
61
         axn <- Bonny.Axn.update_status(axn, &Map.put(&1, "sql_db_name", db_name)),
×
62
         {:app_user, {:ok, axn}} <-
×
63
           {:app_user, apply_user("app", :read_write, axn, conn, conn_args, db_name)},
64
         axn <-
×
65
           set_condition(
66
             axn,
67
             "AppUser",
68
             true,
69
             "Application user was created successfully."
70
           ),
71
         {:inspector_user, {:ok, axn}} <-
×
72
           {:inspector_user, apply_user("inspector", :read_only, axn, conn, conn_args, db_name)},
73
         axn <-
×
74
           set_condition(
75
             axn,
76
             "InspectorUser",
77
             true,
78
             "Inspector user was created successfully."
79
           ) do
80
      success_event(axn)
×
81
    else
82
      {:instance, []} ->
83
        message = "The referenced PostgreSQL instance was not found."
×
84
        Logger.warning("#{axn.action} failed. #{message}")
×
85

86
        axn
87
        |> failure_event(message: message)
88
        |> set_condition("Connection", false, message)
×
89

90
      {:database, axn, {:error, message}} ->
91
        Logger.warning("#{axn.action} failed. #{message}")
×
92

93
        axn
94
        |> failure_event(message: message)
95
        |> set_condition("Database", false, message)
×
96

97
      {:can_access, axn, false} ->
98
        message =
×
99
          ~s(The referenced PostgresClusterInstance cannot be accesed. Check the annotation "kompost.chuge.li/allowed_namespaces" on the PostgresClusterInstance.)
100

101
        Logger.warning("#{axn.action} failed. #{message}")
×
102

103
        axn
104
        |> failure_event(message: message)
105
        |> set_condition("ClusterInstanceAccess", false, message)
×
106

107
      {:app_user, {:error, axn, message}} ->
108
        Logger.warning("#{axn.action} failed. #{message}")
×
109

110
        axn
111
        |> failure_event(message: message)
112
        |> set_condition("AppUser", false, message)
×
113

114
      {:inspector_user, {:error, axn, message}} ->
115
        Logger.warning("#{axn.action} failed. #{message}")
×
116

117
        axn
118
        |> failure_event(message: message)
119
        |> set_condition("InspectorUser", false, message)
×
120
    end
121
  end
122

123
  # See `delete_resources/1`
124
  def handle_event(%Bonny.Axn{action: :delete} = axn, _opts) do
125
    success_event(axn)
×
126
  end
127

128
  @doc """
129
  Finalizer preventing the deletion of the database resource until underlying
130
  resources on the postgres instance are cleaned up.
131
  """
132
  @spec delete_resources(Bonny.Axn.t()) :: {:ok, Bonny.Axn.t()} | {:error, Bonny.Axn.t()}
133
  def delete_resources(axn) do
134
    resource = axn.resource
×
135
    db_name = Database.name(resource)
×
136
    users = resource["status"]["users"]
×
137
    instance = resource |> instance_id() |> Instance.lookup()
×
138

139
    with {:instance, [{conn, _conn_args, _allowed_namespaces}]} <- {:instance, instance},
×
140
         {:users, axn, :ok} <- {:users, axn, drop_users(users, db_name, conn)},
×
141
         {:database, axn, :ok} <- {:database, axn, Database.drop(db_name, conn)} do
×
142
      {:ok, axn}
143
    else
144
      {:instance, []} ->
145
        Logger.warning(
×
146
          "The referenced PostgreSQL instance was not found. But we consider the Database removed."
147
        )
148

149
        {:ok, axn}
150

151
      {:users, axn, {:error, message}} ->
152
        Logger.warning("Failed to finalize. #{message}")
×
153
        failure_event(axn, message: message)
×
154
        {:error, axn}
155

156
      {:database, axn, {:error, message}} ->
157
        Logger.warning("Failed to finalize. #{message}")
×
158
        failure_event(axn, message: message)
×
159
        {:error, axn}
160
    end
161
  end
162

163
  @spec apply_user_secret(
164
          axn :: Bonny.Axn.t(),
165
          secret_name :: binary(),
166
          user_env :: map(),
167
          conn_args :: Keyword.t()
168
        ) :: {:ok, Bonny.Axn.t()}
169
  defp apply_user_secret(axn, secret_name, user_env, conn_args) do
170
    data =
×
171
      Map.merge(user_env, %{
172
        DB_HOST: Keyword.fetch!(conn_args, :hostname),
173
        DB_PORT: "#{Keyword.fetch!(conn_args, :port)}",
×
174
        DB_SSL: "#{conn_args[:ssl]}",
×
175
        DB_SSL_VERIFY: "#{conn_args[:ssl_opts][:verify] == :verify_peer}"
×
176
      })
177

178
    user_secret =
×
179
      ~y"""
180
      apiVersion: v1
181
      kind: Secret
182
      metadata:
183
          namespace: #{K8s.Resource.FieldAccessors.namespace(axn.resource)}
×
184
          name: #{secret_name}
×
185
      """
186
      |> Map.put("stringData", data)
187

188
    username = user_env[:DB_USER]
×
189
    status_entry = %{"username" => username, "secret" => secret_name}
×
190

191
    axn =
×
192
      axn
193
      |> Bonny.Axn.register_descendant(user_secret)
194
      |> Bonny.Axn.update_status(fn status ->
195
        Map.update(status, "users", [status_entry], &Enum.uniq([status_entry | &1]))
×
196
      end)
197

198
    {:ok, axn}
199
  end
200

201
  @spec apply_user(
202
          username :: binary(),
203
          user_access :: Privileges.access(),
204
          Bonny.Axn.t(),
205
          Postgrex.conn(),
206
          conn_args :: Keyword.t(),
207
          db_name :: binary()
208
        ) :: {:ok, Bonny.Axn.t()} | {:error, binary()}
209
  defp apply_user(username, user_access, axn, conn, conn_args, db_name) do
210
    resource_name = K8s.Resource.FieldAccessors.name(axn.resource)
×
211
    resource_namespace = K8s.Resource.FieldAccessors.namespace(axn.resource)
×
212
    secret_name = Slugger.slugify_downcase("psql-#{resource_name}-#{username}", ?-)
×
213
    password = get_user_password(axn.conn, resource_namespace, secret_name)
×
214
    %{"session_authorization" => superuser} = Postgrex.parameters(conn)
×
215

216
    with {:ok, user_env} <- User.apply(username, conn, db_name, password),
×
217
         :ok <- Privileges.grant(user_env[:DB_USER], superuser, conn),
×
218
         :ok <- Privileges.grant(user_env[:DB_USER], user_access, db_name, conn) do
×
219
      apply_user_secret(axn, secret_name, user_env, conn_args)
×
220
    else
221
      {:error, error} ->
222
        {:error, axn, error}
×
223
    end
224
  end
225

226
  @spec drop_users(users :: list(map()), db_name :: binary(), Postgrex.conn()) ::
227
          :ok | {:error, binary()}
228
  defp drop_users(users, db_name, conn) do
229
    %{"session_authorization" => superuser} = Postgrex.parameters(conn)
×
230

231
    users
232
    |> List.wrap()
233
    |> Enum.uniq()
234
    |> Enum.find_value(:ok, fn
×
235
      %{"username" => username} ->
236
        with :ok <- Privileges.revoke(username, :all, db_name, conn),
×
237
             :ok <- User.drop(username, superuser, conn) do
×
238
          false
239
        end
240
    end)
241
  end
242

243
  @spec get_user_password(K8s.Conn.t(), namespace :: binary(), name :: binary()) :: binary()
244
  defp get_user_password(conn, namespace, name) do
245
    case K8s.Client.get("v1", "Secret", namespace: namespace, name: name)
×
246
         |> K8s.Client.put_conn(conn)
247
         |> K8s.Client.run() do
248
      {:ok, secret} -> secret["data"]["DB_PASS"] |> Base.decode64!()
×
249
      {:error, _} -> Password.random_string()
×
250
    end
251
  end
252

253
  @spec add_finalizer?(Bonny.Axn.t()) :: boolean()
254
  def add_finalizer?(%Bonny.Axn{resource: resource}) do
255
    conditions =
×
256
      resource
257
      |> get_in([Access.key("status", %{}), Access.key("conditions", [])])
258
      |> Map.new(&{&1["type"], &1})
×
259

260
    resource["metadata"]["annotations"]["kompost.chuge.li/deletion-policy"] != "abandon" and
×
261
      conditions["Connection"]["status"] == "True"
×
262
  end
263

264
  @spec instance_id(resource :: map()) :: Instance.id()
265
  defp instance_id(%{"spec" => %{"instanceRef" => %{}}} = resource) do
×
266
    {resource["metadata"]["namespace"], resource["spec"]["instanceRef"]["name"]}
267
  end
268

269
  defp instance_id(%{"spec" => %{"clusterInstanceRef" => %{}}} = resource) do
×
270
    {:cluster, resource["spec"]["clusterInstanceRef"]["name"]}
271
  end
272
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