-
Notifications
You must be signed in to change notification settings - Fork 83
Expand file tree
/
Copy pathprovisioning_controller.ex
More file actions
217 lines (179 loc) · 6.31 KB
/
Copy pathprovisioning_controller.ex
File metadata and controls
217 lines (179 loc) · 6.31 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
defmodule LightningWeb.API.ProvisioningController do
@moduledoc """
API controller for project provisioning and deployment.
Handles creation, updates, and retrieval of project state for idempotent
deployments via the CLI. Supports both JSON and YAML formats.
## Endpoints
- `POST /api/provision` - Create or update a project
- `GET /api/provision/:id` - Get project state as JSON
- `GET /api/provision/:id.yaml` - Get project state as YAML
"""
use LightningWeb, :controller
alias Lightning.Policies.Permissions
alias Lightning.Policies.Provisioning
alias Lightning.Projects
alias Lightning.Projects.Project
alias Lightning.Projects.Provisioner
alias Lightning.Workflows
alias Lightning.WorkflowVersions
alias LightningWeb.API.Helpers
action_fallback(LightningWeb.FallbackController)
@doc """
Creates or updates a project based on a JSON payload.
Performs idempotent project provisioning by accepting UUIDs for existing
resources. If a project ID is provided and exists, the project will be
updated; otherwise, a new project is created.
## Parameters
- `conn` - The Plug connection struct with the current resource assigned
- `params` - Map containing project configuration (workflows, jobs, triggers, etc.)
## Returns
- `201 Created` with project JSON on success
- `422 Unprocessable Entity` with changeset errors on validation failure
- `403 Forbidden` if user lacks provisioning permissions
## Examples
# Create new project
POST /api/provision
{
"name": "My Project",
"workflows": [...]
}
# Update existing project
POST /api/provision
{
"id": "a1b2c3d4-...",
"name": "Updated Project",
"workflows": [...]
}
"""
@spec create(Plug.Conn.t(), map()) :: Plug.Conn.t()
def create(conn, params) do
with project <- get_or_build_project(params),
:ok <-
Permissions.can(
Provisioning,
:provision_project,
conn.assigns.current_resource,
project
) do
case Provisioner.import_document(
project,
conn.assigns.current_resource,
params
) do
{:ok, project} ->
conn
|> put_status(:created)
|> put_resp_header("location", ~p"/api/provision/#{project.id}")
|> render("create.json", project: project)
{:error, %Ecto.Changeset{} = changeset} ->
conn
|> put_status(:unprocessable_entity)
|> render("error.json", changeset: changeset)
{:error, error} ->
conn
|> put_status(:forbidden)
|> put_view(LightningWeb.ErrorView)
|> render(:"403",
error:
case error do
%Lightning.Extensions.Message{text: text} -> text
_ -> error
end
)
end
end
end
@doc """
Returns a project state as JSON with UUIDs for idempotent deployments.
Retrieves the complete project configuration including workflows, jobs,
triggers, edges, and credentials. UUIDs are included to enable updates
to existing projects via the CLI.
## Parameters
- `conn` - The Plug connection struct with the current resource assigned
- `params` - Map containing:
- `id` - Project UUID (required)
- `snapshots` - Whether to include workflow snapshots (optional)
## Returns
- `200 OK` with project JSON on success
- `404 Not Found` if project doesn't exist
- `403 Forbidden` if user lacks describe permissions
## Examples
# Get project state
GET /api/provision/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d
# Get project state with snapshots
GET /api/provision/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d?snapshots=true
"""
@spec show(Plug.Conn.t(), map()) :: Plug.Conn.t()
def show(conn, params) do
with :ok <- Helpers.validate_uuid(params["id"]),
project = %Project{} <-
Projects.get_project(params["id"]) || {:error, :not_found},
:ok <-
Permissions.can(
Provisioning,
:describe_project,
conn.assigns.current_resource,
project
),
:ok <- ensure_workflows_have_versions(project),
project <-
Provisioner.preload_dependencies(project, params["snapshots"]) do
conn
|> put_status(:ok)
|> render("create.json", project: project)
end
end
defp ensure_workflows_have_versions(project) do
workflows =
Workflows.list_project_workflows(project.id,
include: [:jobs, :edges, :triggers]
)
Enum.each(workflows, &WorkflowVersions.ensure_version_recorded/1)
end
@doc """
Returns a project state as YAML for CLI deployments.
Exports the complete project configuration in YAML format, equivalent to
the "Export to YAML" feature in the UI. Useful for version control and
CLI-based workflows.
## Parameters
- `conn` - The Plug connection struct with the current resource assigned
- `params` - Map containing:
- `id` - Project UUID (required)
- `snapshots` - Whether to include workflow snapshots (optional)
## Returns
- `200 OK` with YAML content on success
- `404 Not Found` if project doesn't exist
- `403 Forbidden` if user lacks describe permissions
## Examples
# Get project state as YAML
GET /api/provision/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d.yaml
# Get project state as YAML with snapshots
GET /api/provision/a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d.yaml?snapshots=true
"""
@spec show_yaml(Plug.Conn.t(), map()) :: Plug.Conn.t()
def show_yaml(conn, %{"id" => id} = params) do
with :ok <- Helpers.validate_uuid(id),
%Projects.Project{} = project <-
Projects.get_project(id) || {:error, :not_found},
:ok <-
Permissions.can(
Provisioning,
:describe_project,
conn.assigns.current_resource,
project
) do
{:ok, yaml} = Projects.export_project(:yaml, id, params["snapshots"])
conn
|> put_resp_content_type("text/yaml")
|> put_root_layout(false)
|> send_resp(200, yaml)
end
end
defp get_or_build_project(params) do
params
|> case do
%{"id" => id} -> Projects.get_project(id) || %Project{}
_ -> %Project{}
end
end
end