Skip to content

Commit 436231f

Browse files
feat: add full proxy configuration (Phase 8)
Implement three sub-phases of proxy configuration features: 8A - Service policy fields: CORS, IP access control, compression, path rewrite, and redirect route type. Adds global compression and access control to project config. Extends KDL generator with new block types using a shared build_nested_map_block helper. 8B - Upstream groups with load balancing: new upstream_groups and upstream_targets tables, schema, CRUD context functions, KDL generation, REST API controller, and LiveView UI. Supports 6 algorithms (round_robin, least_conn, ip_hash, consistent_hash, weighted, random), health checks, circuit breakers, sticky sessions. 8C - TLS certificate management: certificate storage with AES-256-GCM encrypted private keys, X.509 PEM parsing for metadata extraction (issuer, validity, fingerprint, SAN domains), Oban expiry worker, REST API, LiveView UI, and KDL TLS block generation. 521 tests passing (36 new certificate + upstream group tests).
1 parent fb9c180 commit 436231f

41 files changed

Lines changed: 4660 additions & 68 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

lib/sentinel_cp/application.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ defmodule SentinelCp.Application do
2828
# Start periodic workers
2929
SentinelCp.Rollouts.SchedulerWorker.ensure_started()
3030
SentinelCp.Nodes.DriftWorker.ensure_started()
31+
SentinelCp.Services.CertificateExpiryWorker.ensure_started()
3132

3233
result
3334
end

lib/sentinel_cp/services.ex

Lines changed: 161 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ defmodule SentinelCp.Services do
88

99
import Ecto.Query, warn: false
1010
alias SentinelCp.Repo
11-
alias SentinelCp.Services.{Service, ProjectConfig}
11+
alias SentinelCp.Services.{Service, ProjectConfig, UpstreamGroup, UpstreamTarget, Certificate}
1212

1313
## Services
1414

@@ -116,4 +116,164 @@ defmodule SentinelCp.Services do
116116
|> ProjectConfig.changeset(attrs)
117117
|> Repo.update()
118118
end
119+
120+
## Upstream Groups
121+
122+
@doc """
123+
Lists upstream groups for a project, preloading targets.
124+
"""
125+
def list_upstream_groups(project_id) do
126+
from(g in UpstreamGroup,
127+
where: g.project_id == ^project_id,
128+
order_by: [asc: g.name],
129+
preload: [:targets]
130+
)
131+
|> Repo.all()
132+
end
133+
134+
@doc """
135+
Gets a single upstream group by ID, preloading targets.
136+
"""
137+
def get_upstream_group(id) do
138+
UpstreamGroup
139+
|> Repo.get(id)
140+
|> Repo.preload(:targets)
141+
end
142+
143+
@doc """
144+
Gets a single upstream group by ID, raises if not found.
145+
"""
146+
def get_upstream_group!(id) do
147+
UpstreamGroup
148+
|> Repo.get!(id)
149+
|> Repo.preload(:targets)
150+
end
151+
152+
@doc """
153+
Creates an upstream group.
154+
"""
155+
def create_upstream_group(attrs) do
156+
%UpstreamGroup{}
157+
|> UpstreamGroup.create_changeset(attrs)
158+
|> Repo.insert()
159+
end
160+
161+
@doc """
162+
Updates an upstream group.
163+
"""
164+
def update_upstream_group(%UpstreamGroup{} = group, attrs) do
165+
group
166+
|> UpstreamGroup.update_changeset(attrs)
167+
|> Repo.update()
168+
end
169+
170+
@doc """
171+
Deletes an upstream group.
172+
"""
173+
def delete_upstream_group(%UpstreamGroup{} = group) do
174+
Repo.delete(group)
175+
end
176+
177+
@doc """
178+
Adds a target to an upstream group.
179+
"""
180+
def add_upstream_target(attrs) do
181+
%UpstreamTarget{}
182+
|> UpstreamTarget.changeset(attrs)
183+
|> Repo.insert()
184+
end
185+
186+
@doc """
187+
Updates an upstream target.
188+
"""
189+
def update_upstream_target(%UpstreamTarget{} = target, attrs) do
190+
target
191+
|> UpstreamTarget.changeset(attrs)
192+
|> Repo.update()
193+
end
194+
195+
@doc """
196+
Gets an upstream target by ID.
197+
"""
198+
def get_upstream_target(id), do: Repo.get(UpstreamTarget, id)
199+
200+
@doc """
201+
Removes an upstream target.
202+
"""
203+
def remove_upstream_target(%UpstreamTarget{} = target) do
204+
Repo.delete(target)
205+
end
206+
207+
## Certificates
208+
209+
@doc """
210+
Lists certificates for a project, ordered by domain.
211+
"""
212+
def list_certificates(project_id) do
213+
from(c in Certificate,
214+
where: c.project_id == ^project_id,
215+
order_by: [asc: c.domain]
216+
)
217+
|> Repo.all()
218+
end
219+
220+
@doc """
221+
Gets a single certificate by ID.
222+
"""
223+
def get_certificate(id), do: Repo.get(Certificate, id)
224+
225+
@doc """
226+
Gets a single certificate by ID, raises if not found.
227+
"""
228+
def get_certificate!(id), do: Repo.get!(Certificate, id)
229+
230+
@doc """
231+
Creates a certificate. Expects `key_pem` (plaintext) in attrs — it will be encrypted.
232+
"""
233+
def create_certificate(attrs) do
234+
%Certificate{}
235+
|> Certificate.create_changeset(attrs)
236+
|> Repo.insert()
237+
end
238+
239+
@doc """
240+
Updates a certificate (name, auto_renew, acme_config).
241+
"""
242+
def update_certificate(%Certificate{} = cert, attrs) do
243+
cert
244+
|> Certificate.update_changeset(attrs)
245+
|> Repo.update()
246+
end
247+
248+
@doc """
249+
Renews a certificate with new PEM data.
250+
"""
251+
def renew_certificate(%Certificate{} = cert, attrs) do
252+
cert
253+
|> Certificate.renew_changeset(attrs)
254+
|> Repo.update()
255+
end
256+
257+
@doc """
258+
Deletes a certificate.
259+
"""
260+
def delete_certificate(%Certificate{} = cert) do
261+
Repo.delete(cert)
262+
end
263+
264+
@doc """
265+
Lists certificates expiring within the given number of days.
266+
"""
267+
def list_expiring_certificates(days_ahead \\ 30) do
268+
now = DateTime.utc_now()
269+
threshold = DateTime.add(now, days_ahead * 86_400, :second)
270+
271+
from(c in Certificate,
272+
where: c.status in ["active", "expiring_soon"],
273+
where: c.not_after > ^now,
274+
where: c.not_after <= ^threshold,
275+
order_by: [asc: c.not_after]
276+
)
277+
|> Repo.all()
278+
end
119279
end

0 commit comments

Comments
 (0)