Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
Expand Up @@ -554,6 +554,9 @@ defmodule DeployexWeb.Components.ConfigChangesModal do
end

defp render_certificate_change(%{status: %{status: :modified}} = assigns) do
assigns =
assign(assigns, :field_changes, Map.get(assigns.status, :changes, %{}))

~H"""
<div class="bg-warning/10 border-l-4 border-warning rounded-lg p-3">
<div class="flex items-center gap-2">
Expand All @@ -565,6 +568,16 @@ defmodule DeployexWeb.Components.ConfigChangesModal do
<div class="text-sm text-base-content/70 ml-4 mt-1">
{Enum.join(@status.config.domains, ", ")}
</div>
<div :if={map_size(@field_changes) > 0} class="mt-2 space-y-1">
<%= for {field, %{old: old, new: new}} <- @field_changes do %>
<div class="flex gap-2 items-center text-xs">
<span class="text-base-content/50 font-mono">{field}</span>
<span class="text-error line-through">{format_cert_value(old)}</span>
<span class="text-base-content/50">→</span>
<span class="text-success">{format_cert_value(new)}</span>
</div>
<% end %>
</div>
</div>
"""
end
Expand Down Expand Up @@ -611,6 +624,27 @@ defmodule DeployexWeb.Components.ConfigChangesModal do

defp format_value(_, value), do: to_string(value)

defp format_cert_value(nil), do: "none"

defp format_cert_value([]), do: "[]"

defp format_cert_value(list) when is_list(list) do
Enum.map_join(list, ", ", &format_cert_value/1)
end

defp format_cert_value(%_{} = struct) do
struct
|> Map.from_struct()
|> Enum.reject(fn {_k, v} -> is_nil(v) end)
|> Enum.map_join(", ", fn {k, v} -> "#{k}: #{format_cert_value(v)}" end)
end

defp format_cert_value(value) when is_atom(value) do
value |> to_string() |> String.replace_prefix("Elixir.", "")
end

defp format_cert_value(value), do: to_string(value)

defp format_timestamp(timestamp) do
Calendar.strftime(timestamp, "%Y-%m-%d %H:%M:%S UTC")
end
Expand Down
30 changes: 29 additions & 1 deletion apps/sentinel/lib/sentinel/config/watcher.ex
Original file line number Diff line number Diff line change
Expand Up @@ -622,14 +622,42 @@ defmodule Sentinel.Config.Watcher do
Map.put(acc, type, %{status: :removed, config: old_cert})

normalize(old_cert) != normalize(new_cert) ->
Map.put(acc, type, %{status: :modified, config: new_cert})
changes = diff_certificate_fields(old_cert, new_cert)
Map.put(acc, type, %{status: :modified, config: new_cert, changes: changes})

true ->
acc
end
end)
end

defp diff_certificate_fields(old_cert, new_cert) do
[
:domains,
:renew_before_days,
:certificate_check_interval_ms,
:dns_propagation_timeout_ms,
:dns_check_interval_ms,
:dns_provider,
:dns_options,
:acme_provider,
:acme_options,
:importer,
:importer_options,
:storage_options
]
|> Enum.reduce(%{}, fn field, acc ->
old_val = Map.get(old_cert, field)
new_val = Map.get(new_cert, field)

if normalize(old_val) != normalize(new_val) do
Map.put(acc, field, %{old: old_val, new: new_val})
else
acc
end
end)
end

defp normalize(value) when is_struct(value) do
value
|> Map.from_struct()
Expand Down
120 changes: 120 additions & 0 deletions apps/sentinel/test/config/watcher_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -1680,6 +1680,126 @@ defmodule Sentinel.Config.WatcherTest do
end
end

@tag :capture_log
test "detects field-level changes for modified certificate" do
test_pid = self()
ref = make_ref()

old_applications = [
Map.merge(@default_application, %{certificates: [@default_certificate]})
]

new_applications = [
Map.merge(@default_application, %{
certificates: [
Map.merge(@default_certificate, %{domains: ["example.com", "www.example.com"]})
]
})
]

with_mocks([
{Upgradable, [],
[
from_app_env: fn ->
Map.merge(@default_upgradable, %{applications: old_applications})
end,
from_yaml: fn _config ->
Map.merge(@default_upgradable, %{applications: new_applications})
end
]},
{Yaml, [],
[
load: fn %Yaml{config_checksum: "current_checksum"} ->
Process.send_after(test_pid, {:handle_ref_event, ref}, 100)
{:ok, %Yaml{}}
end
]}
]) do
log =
capture_log(fn ->
{:ok, _pid} =
Watcher.start_link(name: :test_cert_field_diff, check_interval_ms: 10)

assert_receive {:handle_ref_event, ^ref}, 1_000
end)

{:ok, changes} = Watcher.get_pending_changes(:test_cert_field_diff)

cert_detail = changes.summary.applications.details["my_new_app"].changes.certificates
field_changes = cert_detail.details[:acme].changes

assert Map.has_key?(field_changes, :domains)
assert field_changes.domains.old == ["example.com"]
assert field_changes.domains.new == ["example.com", "www.example.com"]

refute Map.has_key?(field_changes, :acme_provider)
refute Map.has_key?(field_changes, :dns_provider)

assert log =~ "Detected 1 change(s) in upgradable fields: [:applications]"
end
end

@tag :capture_log
test "detects field-level changes for modified acme_options" do
test_pid = self()
ref = make_ref()

old_applications = [
Map.merge(@default_application, %{certificates: [@default_certificate]})
]

new_acme_options = %Foundation.Yaml.Certificate.AcmeOptions{
contact_email: "new@example.com",
url: "https://acme-v02.api.letsencrypt.org/directory",
key_size: 4096,
propagation_timeout_ms: 120_000,
check_interval_ms: 2000
}

new_applications = [
Map.merge(@default_application, %{
certificates: [Map.merge(@default_certificate, %{acme_options: new_acme_options})]
})
]

with_mocks([
{Upgradable, [],
[
from_app_env: fn ->
Map.merge(@default_upgradable, %{applications: old_applications})
end,
from_yaml: fn _config ->
Map.merge(@default_upgradable, %{applications: new_applications})
end
]},
{Yaml, [],
[
load: fn %Yaml{config_checksum: "current_checksum"} ->
Process.send_after(test_pid, {:handle_ref_event, ref}, 100)
{:ok, %Yaml{}}
end
]}
]) do
log =
capture_log(fn ->
{:ok, _pid} =
Watcher.start_link(name: :test_cert_field_acme, check_interval_ms: 10)

assert_receive {:handle_ref_event, ^ref}, 1_000
end)

{:ok, changes} = Watcher.get_pending_changes(:test_cert_field_acme)

cert_detail = changes.summary.applications.details["my_new_app"].changes.certificates
field_changes = cert_detail.details[:acme].changes

assert Map.has_key?(field_changes, :acme_options)
refute Map.has_key?(field_changes, :domains)

assert log =~ "Detected 1 change(s) in upgradable fields: [:applications]"
end
end

@tag :capture_log
test "detects modified certificate acme_options" do
test_pid = self()
Expand Down
Loading