Skip to content

Commit 5dfa607

Browse files
feat: add OpenAPI 3.x spec import (Phase 11.1)
Upload OpenAPI 3.0/3.1 specs (JSON or YAML), preview extracted services and auth policies, select which to import, and bulk-create them. Specs are stored for re-import change detection via checksum comparison. - Parser: decode JSON/YAML, extract services (one per path), map security schemes to auth policy types (jwt, api_key, basic, mtls) - Context: OpenAPI spec CRUD, import_from_openapi/4 transaction with auth policy resolution via security refs - LiveView: multi-step workflow (upload → preview → done) with file upload, service selection, upstream URL override, diff banner - API: preview, import, list/show/delete spec endpoints - 50 new tests (30 parser + 20 context)
1 parent 4434113 commit 5dfa607

14 files changed

Lines changed: 1943 additions & 2 deletions

File tree

lib/sentinel_cp/services.ex

Lines changed: 117 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, ServiceTemplate, ProjectConfig, UpstreamGroup, UpstreamTarget, Certificate, AuthPolicy}
11+
alias SentinelCp.Services.{Service, ServiceTemplate, ProjectConfig, UpstreamGroup, UpstreamTarget, Certificate, AuthPolicy, OpenApiSpec}
1212

1313
## Services
1414

@@ -375,4 +375,120 @@ defmodule SentinelCp.Services do
375375
def delete_template(%ServiceTemplate{} = template) do
376376
Repo.delete(template)
377377
end
378+
379+
## OpenAPI Specs
380+
381+
@doc """
382+
Lists OpenAPI specs for a project, most recent first.
383+
"""
384+
def list_openapi_specs(project_id) do
385+
from(s in OpenApiSpec,
386+
where: s.project_id == ^project_id,
387+
order_by: [desc: s.inserted_at]
388+
)
389+
|> Repo.all()
390+
end
391+
392+
@doc """
393+
Gets a single OpenAPI spec by ID.
394+
"""
395+
def get_openapi_spec(id), do: Repo.get(OpenApiSpec, id)
396+
397+
@doc """
398+
Gets a single OpenAPI spec by ID, raises if not found.
399+
"""
400+
def get_openapi_spec!(id), do: Repo.get!(OpenApiSpec, id)
401+
402+
@doc """
403+
Creates an OpenAPI spec record.
404+
"""
405+
def create_openapi_spec(attrs) do
406+
%OpenApiSpec{}
407+
|> OpenApiSpec.changeset(attrs)
408+
|> Repo.insert()
409+
end
410+
411+
@doc """
412+
Deletes an OpenAPI spec.
413+
"""
414+
def delete_openapi_spec(%OpenApiSpec{} = spec) do
415+
Repo.delete(spec)
416+
end
417+
418+
@doc """
419+
Finds an OpenAPI spec by checksum within a project, for dedup on re-upload.
420+
"""
421+
def get_openapi_spec_by_checksum(project_id, checksum) do
422+
from(s in OpenApiSpec,
423+
where: s.project_id == ^project_id and s.checksum == ^checksum
424+
)
425+
|> Repo.one()
426+
end
427+
428+
@doc """
429+
Imports services (and optionally auth policies) from an OpenAPI spec.
430+
431+
Runs in a transaction: creates auth policies first, then services with
432+
spec linkage and resolved auth_policy_ids.
433+
"""
434+
def import_from_openapi(project_id, spec_id, selected_services, opts \\ []) do
435+
import_auth = Keyword.get(opts, :import_auth_policies, false)
436+
auth_policy_attrs = Keyword.get(opts, :auth_policy_attrs, [])
437+
438+
Repo.transaction(fn ->
439+
# Create auth policies if requested
440+
policy_map =
441+
if import_auth do
442+
auth_policy_attrs
443+
|> Enum.reduce(%{}, fn attrs, acc ->
444+
attrs_with_project = Map.put(attrs, :project_id, project_id)
445+
446+
case create_auth_policy(attrs_with_project) do
447+
{:ok, policy} -> Map.put(acc, attrs.name, policy.id)
448+
{:error, changeset} -> Repo.rollback({:auth_policy_error, changeset})
449+
end
450+
end)
451+
else
452+
%{}
453+
end
454+
455+
# Create services
456+
services =
457+
Enum.map(selected_services, fn svc_attrs ->
458+
auth_policy_id = resolve_auth_policy_id(svc_attrs, policy_map)
459+
460+
attrs =
461+
%{
462+
name: svc_attrs.name,
463+
route_path: svc_attrs.route_path,
464+
upstream_url: svc_attrs.upstream_url,
465+
description: svc_attrs[:description],
466+
openapi_spec_id: spec_id,
467+
openapi_path: svc_attrs.openapi_path,
468+
project_id: project_id
469+
}
470+
|> maybe_put(:auth_policy_id, auth_policy_id)
471+
472+
case create_service(attrs) do
473+
{:ok, service} -> service
474+
{:error, changeset} -> Repo.rollback({:service_error, changeset})
475+
end
476+
end)
477+
478+
%{
479+
services: services,
480+
services_count: length(services),
481+
auth_policies_count: map_size(policy_map)
482+
}
483+
end)
484+
end
485+
486+
defp resolve_auth_policy_id(%{security_refs: refs}, policy_map) when is_list(refs) do
487+
Enum.find_value(refs, fn ref -> Map.get(policy_map, ref) end)
488+
end
489+
490+
defp resolve_auth_policy_id(_, _), do: nil
491+
492+
defp maybe_put(map, _key, nil), do: map
493+
defp maybe_put(map, key, value), do: Map.put(map, key, value)
378494
end

0 commit comments

Comments
 (0)