forked from getarcaneapp/arcane
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathhuma.go
More file actions
422 lines (378 loc) · 13.6 KB
/
huma.go
File metadata and controls
422 lines (378 loc) · 13.6 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
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
package huma
import (
"reflect"
"strings"
"github.com/danielgtaylor/huma/v2"
"github.com/danielgtaylor/huma/v2/adapters/humagin"
"github.com/getarcaneapp/arcane/backend/internal/config"
"github.com/getarcaneapp/arcane/backend/internal/huma/handlers"
"github.com/getarcaneapp/arcane/backend/internal/huma/middleware"
"github.com/getarcaneapp/arcane/backend/internal/services"
"github.com/gin-gonic/gin"
)
const (
arcaneTypesPrefix = "github.com/getarcaneapp/arcane/types/"
dockerSDKPrefix = "github.com/moby/moby"
)
var dockerSchemaPrefixes = map[string]string{
"types": "DockerTypes",
"registry": "DockerRegistry",
"system": "DockerSystem",
"container": "DockerContainer",
"network": "DockerNetwork",
"volume": "DockerVolume",
"swarm": "DockerSwarm",
"mount": "DockerMount",
"filters": "DockerFilters",
"blkiodev": "DockerBlkiodev",
"strslice": "DockerStrslice",
"events": "DockerEvents",
"image": "DockerImage",
}
// customSchemaNamer creates unique schema names using package prefix for types
// from github.com/getarcaneapp/arcane/types to avoid conflicts between packages that have
// types with the same name (e.g., image.Summary vs env.Summary).
func customSchemaNamer(t reflect.Type, hint string) string {
name := huma.DefaultSchemaNamer(t, hint)
typeStr := t.String()
pkgPath := packagePathForType(t)
shortPkg := shortPackageFromTypeString(typeStr)
if pkgName, ok := arcanePackageName(pkgPath); ok {
name = pkgName + name
} else if dockerPrefix, ok := dockerSchemaPrefix(pkgPath, shortPkg); ok {
name = dockerPrefix + name
}
if innerPkg, ok := genericInnerPackageName(pkgPath, typeStr); ok {
return strings.Replace(name, "UsageCounts", innerPkg+"UsageCounts", 1)
}
return name
}
func packagePathForType(t reflect.Type) string {
for t.Kind() == reflect.Pointer {
t = t.Elem()
}
return t.PkgPath()
}
func shortPackageFromTypeString(typeStr string) string {
before, _, ok := strings.Cut(typeStr, ".")
if !ok {
return ""
}
return before
}
func arcanePackageName(pkgPath string) (string, bool) {
if !strings.HasPrefix(pkgPath, arcaneTypesPrefix) {
return "", false
}
parts := strings.Split(pkgPath, "/")
if len(parts) == 0 {
return "", false
}
pkg := parts[len(parts)-1]
if pkg == "" {
return "", false
}
return capitalizeFirst(pkg), true
}
func dockerSchemaPrefix(pkgPath, shortPkg string) (string, bool) {
if strings.Contains(pkgPath, dockerSDKPrefix) {
parts := strings.Split(pkgPath, "/")
last := parts[len(parts)-1]
if prefix, ok := dockerSchemaPrefixes[last]; ok {
return prefix, true
}
}
prefix, ok := dockerSchemaPrefixes[shortPkg]
if !ok {
return "", false
}
return prefix, true
}
func genericInnerPackageName(pkgPath, typeName string) (string, bool) {
if !strings.HasPrefix(pkgPath, arcaneTypesPrefix+"base") {
return "", false
}
if !strings.Contains(typeName, "[") || !strings.Contains(typeName, arcaneTypesPrefix) {
return "", false
}
_, after, ok := strings.Cut(typeName, arcaneTypesPrefix)
if !ok {
return "", false
}
before, _, ok := strings.Cut(after, ".")
if !ok || before == "" {
return "", false
}
return capitalizeFirst(before), true
}
func capitalizeFirst(s string) string {
if s == "" {
return s
}
return strings.ToUpper(s[:1]) + s[1:]
}
// Services holds all service dependencies needed by Huma handlers.
type Services struct {
User *services.UserService
Auth *services.AuthService
Oidc *services.OidcService
ApiKey *services.ApiKeyService
AppImages *services.ApplicationImagesService
Font *services.FontService
Project *services.ProjectService
Event *services.EventService
Version *services.VersionService
Environment *services.EnvironmentService
Settings *services.SettingsService
JobSchedule *services.JobService
SettingsSearch *services.SettingsSearchService
ContainerRegistry *services.ContainerRegistryService
Template *services.TemplateService
Docker *services.DockerClientService
Image *services.ImageService
ImageUpdate *services.ImageUpdateService
Build *services.BuildService
BuildWorkspace *services.BuildWorkspaceService
Volume *services.VolumeService
Container *services.ContainerService
Network *services.NetworkService
Port *services.PortService
Swarm *services.SwarmService
Notification *services.NotificationService
Apprise *services.AppriseService //nolint:staticcheck // Apprise still functional, deprecated in favor of Shoutrrr
Updater *services.UpdaterService
CustomizeSearch *services.CustomizeSearchService
System *services.SystemService
SystemUpgrade *services.SystemUpgradeService
GitRepository *services.GitRepositoryService
GitOpsSync *services.GitOpsSyncService
Webhook *services.WebhookService
Vulnerability *services.VulnerabilityService
Dashboard *services.DashboardService
Config *config.Config
}
// SetupAPI creates and configures the Huma API alongside the existing Gin router.
func SetupAPI(router *gin.Engine, apiGroup *gin.RouterGroup, cfg *config.Config, svc *Services) huma.API {
humaConfig := huma.DefaultConfig("Arcane API", config.Version)
humaConfig.Info.Description = "Modern Docker Management, Designed for Everyone"
// Disable default docs path - we'll use Scalar instead
humaConfig.DocsPath = ""
// Configure servers for OpenAPI spec
if cfg.AppUrl != "" {
humaConfig.Servers = []*huma.Server{
{URL: cfg.AppUrl + "/api"},
}
} else {
humaConfig.Servers = []*huma.Server{
{URL: "/api"},
}
}
// Configure security schemes
humaConfig.Components.SecuritySchemes = map[string]*huma.SecurityScheme{
"BearerAuth": {
Type: "http",
Scheme: "bearer",
BearerFormat: "JWT",
Description: "JWT Bearer token authentication",
},
"ApiKeyAuth": {
Type: "apiKey",
In: "header",
Name: "X-API-Key",
Description: "API Key authentication",
},
}
humaConfig.Security = []map[string][]string{
{"BearerAuth": {}},
{"ApiKeyAuth": {}},
}
// Use custom schema namer to avoid conflicts between types with same name
// from different packages (e.g., image.Summary vs env.Summary)
humaConfig.Components.Schemas = huma.NewMapRegistry("#/components/schemas/", customSchemaNamer)
// Create Huma API wrapping the Gin router group
api := humagin.NewWithGroup(router, apiGroup, humaConfig)
// Add authentication middleware
api.UseMiddleware(middleware.NewAuthBridge(api, svc.Auth, svc.ApiKey, svc.Environment, cfg))
// Register all Huma handlers
registerHandlers(api, svc)
// Register Scalar API docs endpoint with dark mode
registerScalarDocs(apiGroup)
return api
}
// scalarDocsHTML returns the HTML template for Scalar API documentation.
const scalarDocsHTML = `<!doctype html>
<html>
<head>
<title>Arcane API Reference</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
</head>
<body>
<script
id="api-reference"
data-url="/api/openapi.json"
data-configuration='{
"theme": "purple",
"darkMode": true,
"layout": "modern",
"hiddenClients": ["unirest"],
"defaultHttpClient": { "targetKey": "shell", "clientKey": "curl" }
}'></script>
<script src="https://cdn.jsdelivr.net/npm/@scalar/api-reference"></script>
</body>
</html>`
// registerScalarDocs adds the Scalar API documentation endpoint.
func registerScalarDocs(apiGroup *gin.RouterGroup) {
apiGroup.GET("/docs", func(c *gin.Context) {
c.Header("Content-Type", "text/html")
c.String(200, scalarDocsHTML)
})
}
// SetupAPIForSpec creates a Huma API instance for OpenAPI spec generation only.
// No services are required - this is purely for schema generation.
func SetupAPIForSpec() huma.API {
gin.SetMode(gin.ReleaseMode)
router := gin.New()
apiGroup := router.Group("/api")
humaConfig := huma.DefaultConfig("Arcane API", config.Version)
humaConfig.Info.Description = "Modern Docker Management, Designed for Everyone"
humaConfig.Servers = []*huma.Server{
{URL: "/api"},
}
humaConfig.Components.SecuritySchemes = map[string]*huma.SecurityScheme{
"BearerAuth": {
Type: "http",
Scheme: "bearer",
BearerFormat: "JWT",
Description: "JWT Bearer token authentication",
},
"ApiKeyAuth": {
Type: "apiKey",
In: "header",
Name: "X-API-Key",
Description: "API Key authentication",
},
}
humaConfig.Security = []map[string][]string{
{"BearerAuth": {}},
{"ApiKeyAuth": {}},
}
// Use custom schema namer to avoid conflicts between types with same name
humaConfig.Components.Schemas = huma.NewMapRegistry("#/components/schemas/", customSchemaNamer)
api := humagin.NewWithGroup(router, apiGroup, humaConfig)
// Register handlers with nil services (just for schema)
registerHandlers(api, nil)
return api
}
// registerHandlers registers all Huma-based API handlers.
// Add new handlers here as they are migrated from Gin.
func registerHandlers(api huma.API, svc *Services) {
var userSvc *services.UserService
var authSvc *services.AuthService
var oidcSvc *services.OidcService
var apiKeySvc *services.ApiKeyService
var appImagesSvc *services.ApplicationImagesService
var fontSvc *services.FontService
var projectSvc *services.ProjectService
var eventSvc *services.EventService
var versionSvc *services.VersionService
var environmentSvc *services.EnvironmentService
var settingsSvc *services.SettingsService
var jobScheduleSvc *services.JobService
var settingsSearchSvc *services.SettingsSearchService
var containerRegistrySvc *services.ContainerRegistryService
var templateSvc *services.TemplateService
var dockerSvc *services.DockerClientService
var imageSvc *services.ImageService
var imageUpdateSvc *services.ImageUpdateService
var buildSvc *services.BuildService
var buildWorkspaceSvc *services.BuildWorkspaceService
var volumeSvc *services.VolumeService
var containerSvc *services.ContainerService
var networkSvc *services.NetworkService
var portSvc *services.PortService
var swarmSvc *services.SwarmService
var notificationSvc *services.NotificationService
var appriseSvc *services.AppriseService //nolint:staticcheck // Apprise still functional, deprecated in favor of Shoutrrr
var updaterSvc *services.UpdaterService
var customizeSearchSvc *services.CustomizeSearchService
var systemSvc *services.SystemService
var systemUpgradeSvc *services.SystemUpgradeService
var gitRepositorySvc *services.GitRepositoryService
var gitOpsSyncSvc *services.GitOpsSyncService
var webhookSvc *services.WebhookService
var vulnerabilitySvc *services.VulnerabilityService
var dashboardSvc *services.DashboardService
var cfg *config.Config
if svc != nil {
userSvc = svc.User
authSvc = svc.Auth
oidcSvc = svc.Oidc
apiKeySvc = svc.ApiKey
appImagesSvc = svc.AppImages
fontSvc = svc.Font
projectSvc = svc.Project
eventSvc = svc.Event
versionSvc = svc.Version
environmentSvc = svc.Environment
settingsSvc = svc.Settings
jobScheduleSvc = svc.JobSchedule
settingsSearchSvc = svc.SettingsSearch
containerRegistrySvc = svc.ContainerRegistry
templateSvc = svc.Template
dockerSvc = svc.Docker
imageSvc = svc.Image
imageUpdateSvc = svc.ImageUpdate
buildSvc = svc.Build
buildWorkspaceSvc = svc.BuildWorkspace
volumeSvc = svc.Volume
containerSvc = svc.Container
networkSvc = svc.Network
portSvc = svc.Port
swarmSvc = svc.Swarm
notificationSvc = svc.Notification
appriseSvc = svc.Apprise
updaterSvc = svc.Updater
customizeSearchSvc = svc.CustomizeSearch
systemSvc = svc.System
systemUpgradeSvc = svc.SystemUpgrade
gitRepositorySvc = svc.GitRepository
gitOpsSyncSvc = svc.GitOpsSync
webhookSvc = svc.Webhook
vulnerabilitySvc = svc.Vulnerability
dashboardSvc = svc.Dashboard
cfg = svc.Config
}
handlers.RegisterHealth(api)
handlers.RegisterAuth(api, userSvc, authSvc, oidcSvc)
handlers.RegisterApiKeys(api, apiKeySvc)
handlers.RegisterAppImages(api, appImagesSvc)
handlers.RegisterFonts(api, fontSvc)
handlers.RegisterProjects(api, projectSvc)
handlers.RegisterUsers(api, userSvc)
handlers.RegisterVersion(api, versionSvc)
handlers.RegisterEvents(api, eventSvc, apiKeySvc)
handlers.RegisterOidc(api, authSvc, oidcSvc, cfg)
handlers.RegisterEnvironments(api, environmentSvc, settingsSvc, apiKeySvc, eventSvc, cfg)
handlers.RegisterContainerRegistries(api, containerRegistrySvc, environmentSvc)
handlers.RegisterTemplates(api, templateSvc, environmentSvc)
handlers.RegisterImages(api, dockerSvc, imageSvc, imageUpdateSvc, settingsSvc, buildSvc)
handlers.RegisterBuildWorkspaces(api, buildWorkspaceSvc)
handlers.RegisterImageUpdates(api, imageUpdateSvc, imageSvc)
handlers.RegisterSettings(api, settingsSvc, settingsSearchSvc, environmentSvc, cfg)
handlers.RegisterJobSchedules(api, jobScheduleSvc, environmentSvc)
handlers.RegisterVolumes(api, dockerSvc, volumeSvc)
handlers.RegisterContainers(api, containerSvc, dockerSvc, settingsSvc)
handlers.RegisterPorts(api, portSvc)
handlers.RegisterNetworks(api, networkSvc, dockerSvc)
handlers.RegisterSwarm(api, swarmSvc, environmentSvc, eventSvc, cfg)
handlers.RegisterNotifications(api, notificationSvc, appriseSvc, cfg)
handlers.RegisterUpdater(api, updaterSvc)
handlers.RegisterCustomize(api, customizeSearchSvc)
handlers.RegisterSystem(api, dockerSvc, systemSvc, systemUpgradeSvc, cfg)
handlers.RegisterGitRepositories(api, gitRepositorySvc)
handlers.RegisterGitOpsSyncs(api, gitOpsSyncSvc)
handlers.RegisterWebhooks(api, webhookSvc)
handlers.RegisterVulnerability(api, vulnerabilitySvc)
handlers.RegisterDashboard(api, dashboardSvc)
}