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

mruoss / kompost / 915118b46ab9f0492c28b638ace168363acff731-PR-32

pending completion
915118b46ab9f0492c28b638ace168363acff731-PR-32

Pull #32

github

mruoss
rename allowed-namespace annotation and add doc
Pull Request #32: rename allowed-namespace annotation and add doc

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

328 of 539 relevant lines covered (60.85%)

10.12 hits per line

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

84.42
/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
8✔
38
    namespace = resource["metadata"]["namespace"]
8✔
39
    db_name = Database.name(resource)
8✔
40
    db_params = Params.new!(resource["spec"]["params"])
8✔
41
    instance = resource |> instance_id() |> Instance.lookup()
8✔
42

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

86
        axn
87
        |> failure_event(message: message)
88
        |> set_condition("Connection", false, message)
1✔
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 =
1✔
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}")
1✔
102

103
        axn
104
        |> failure_event(message: message)
105
        |> set_condition("ClusterInstanceAccess", false, message)
1✔
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)
5✔
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
7✔
135
    db_name = Database.name(resource)
7✔
136
    users = resource["status"]["users"]
7✔
137
    instance = resource |> instance_id() |> Instance.lookup()
7✔
138

139
    with {:instance, [{conn, _conn_args, _allowed_namespaces}]} <- {:instance, instance},
7✔
140
         {:users, axn, :ok} <- {:users, axn, drop_users(users, db_name, conn)},
7✔
141
         {:database, axn, :ok} <- {:database, axn, Database.drop(db_name, conn)} do
7✔
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}")
3✔
158
        failure_event(axn, message: message)
3✔
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 =
12✔
171
      Map.merge(user_env, %{
172
        DB_HOST: Keyword.fetch!(conn_args, :hostname),
173
        DB_PORT: "#{Keyword.fetch!(conn_args, :port)}",
12✔
174
        DB_SSL: "#{conn_args[:ssl]}",
12✔
175
        DB_SSL_VERIFY: "#{conn_args[:ssl_opts][:verify] == :verify_peer}"
12✔
176
      })
177

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

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

191
    axn =
12✔
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]))
12✔
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)
12✔
211
    resource_namespace = K8s.Resource.FieldAccessors.namespace(axn.resource)
12✔
212
    secret_name = Slugger.slugify_downcase("psql-#{resource_name}-#{username}", ?-)
12✔
213
    password = get_user_password(axn.conn, resource_namespace, secret_name)
12✔
214
    %{"session_authorization" => superuser} = Postgrex.parameters(conn)
12✔
215

216
    with {:ok, user_env} <- User.apply(username, conn, db_name, password),
12✔
217
         :ok <- Privileges.grant(user_env[:DB_USER], superuser, conn),
12✔
218
         :ok <- Privileges.grant(user_env[:DB_USER], user_access, db_name, conn) do
12✔
219
      apply_user_secret(axn, secret_name, user_env, conn_args)
12✔
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)
7✔
230

231
    users
232
    |> List.wrap()
233
    |> Enum.uniq()
234
    |> Enum.find_value(:ok, fn
7✔
235
      %{"username" => username} ->
236
        with :ok <- Privileges.revoke(username, :all, db_name, conn),
12✔
237
             :ok <- User.drop(username, superuser, conn) do
12✔
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)
12✔
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()
12✔
250
    end
251
  end
252

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

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

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

269
  defp instance_id(%{"spec" => %{"clusterInstanceRef" => %{}}} = resource) do
8✔
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