@gokiteam/goki-dev 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +478 -0
- package/bin/goki-dev.js +452 -0
- package/bin/mcp-server.js +16 -0
- package/bin/secrets-cli.js +302 -0
- package/cli/ComposeOverrideGenerator.js +226 -0
- package/cli/ComposeParser.js +73 -0
- package/cli/ConfigGenerator.js +304 -0
- package/cli/ConfigManager.js +46 -0
- package/cli/DatabaseManager.js +94 -0
- package/cli/DevToolsChecker.js +21 -0
- package/cli/DevToolsDir.js +66 -0
- package/cli/DevToolsManager.js +451 -0
- package/cli/DockerManager.js +138 -0
- package/cli/FunctionManager.js +95 -0
- package/cli/HttpProxyRewriter.js +91 -0
- package/cli/Logger.js +10 -0
- package/cli/McpConfigManager.js +123 -0
- package/cli/NgrokManager.js +431 -0
- package/cli/ProjectCLI.js +2322 -0
- package/cli/PubSubManager.js +129 -0
- package/cli/SnapshotManager.js +88 -0
- package/cli/UiFormatter.js +292 -0
- package/cli/WebhookUrlRewriter.js +32 -0
- package/cli/secrets/BiometricAuth.js +125 -0
- package/cli/secrets/SecretInjector.js +47 -0
- package/cli/secrets/SecretsConfig.js +141 -0
- package/cli/secrets/SecretsDoctor.js +384 -0
- package/cli/secrets/SecretsManager.js +255 -0
- package/client/dist/client.d.ts +332 -0
- package/client/dist/client.js +507 -0
- package/client/dist/helpers.d.ts +62 -0
- package/client/dist/helpers.js +122 -0
- package/client/dist/index.d.ts +59 -0
- package/client/dist/index.js +78 -0
- package/client/dist/package.json +1 -0
- package/client/dist/types.d.ts +280 -0
- package/client/dist/types.js +7 -0
- package/config.development +46 -0
- package/config.test +18 -0
- package/guidelines/CodingStyleGuideline.md +148 -0
- package/guidelines/CommentingGuideline.md +10 -0
- package/guidelines/HttpApiImplementationGuideline.md +137 -0
- package/guidelines/NamingGuideline.md +182 -0
- package/package.json +138 -0
- package/patterns/api/[collectionName]/Controllers.md +62 -0
- package/patterns/api/[collectionName]/Logic.md +154 -0
- package/patterns/api/[collectionName]/Permissions.md +81 -0
- package/patterns/api/[collectionName]/Router.md +83 -0
- package/patterns/api/[collectionName]/Schemas.md +197 -0
- package/patterns/configs/Patterns.md +7 -0
- package/patterns/enums/Patterns.md +24 -0
- package/patterns/errorHandling/Patterns.md +185 -0
- package/patterns/testing/Patterns.md +232 -0
- package/src/Server.js +238 -0
- package/src/api/dashboard/Controllers.js +9 -0
- package/src/api/dashboard/Logic.js +76 -0
- package/src/api/dashboard/Router.js +11 -0
- package/src/api/dashboard/Schemas.js +47 -0
- package/src/api/data/Controllers.js +26 -0
- package/src/api/data/Logic.js +188 -0
- package/src/api/data/Router.js +16 -0
- package/src/api/docker/Controllers.js +33 -0
- package/src/api/docker/Logic.js +268 -0
- package/src/api/docker/Router.js +15 -0
- package/src/api/docker/Schemas.js +80 -0
- package/src/api/docs/Controllers.js +15 -0
- package/src/api/docs/Logic.js +85 -0
- package/src/api/docs/Router.js +12 -0
- package/src/api/export/Controllers.js +30 -0
- package/src/api/export/Logic.js +143 -0
- package/src/api/export/Router.js +18 -0
- package/src/api/export/Schemas.js +104 -0
- package/src/api/firestore/Controllers.js +152 -0
- package/src/api/firestore/Logic.js +474 -0
- package/src/api/firestore/Router.js +23 -0
- package/src/api/functions/Controllers.js +261 -0
- package/src/api/functions/Logic.js +710 -0
- package/src/api/functions/Router.js +50 -0
- package/src/api/functions/Schemas.js +193 -0
- package/src/api/gateway/Controllers.js +72 -0
- package/src/api/gateway/Logic.js +74 -0
- package/src/api/gateway/Router.js +10 -0
- package/src/api/gateway/Schemas.js +19 -0
- package/src/api/health/Controllers.js +14 -0
- package/src/api/health/Logic.js +24 -0
- package/src/api/health/Router.js +12 -0
- package/src/api/httpTraffic/Controllers.js +29 -0
- package/src/api/httpTraffic/Logic.js +33 -0
- package/src/api/httpTraffic/Router.js +9 -0
- package/src/api/httpTraffic/Schemas.js +23 -0
- package/src/api/logging/Controllers.js +80 -0
- package/src/api/logging/Logic.js +461 -0
- package/src/api/logging/Router.js +24 -0
- package/src/api/logging/Schemas.js +43 -0
- package/src/api/mqtt/Controllers.js +17 -0
- package/src/api/mqtt/Logic.js +66 -0
- package/src/api/mqtt/Router.js +12 -0
- package/src/api/postgres/Controllers.js +97 -0
- package/src/api/postgres/Logic.js +221 -0
- package/src/api/postgres/Router.js +21 -0
- package/src/api/pubsub/Controllers.js +236 -0
- package/src/api/pubsub/Logic.js +732 -0
- package/src/api/pubsub/Router.js +41 -0
- package/src/api/pubsub/Schemas.js +355 -0
- package/src/api/redis/Controllers.js +63 -0
- package/src/api/redis/Logic.js +239 -0
- package/src/api/redis/Router.js +21 -0
- package/src/api/scheduler/Controllers.js +27 -0
- package/src/api/scheduler/Logic.js +49 -0
- package/src/api/scheduler/Router.js +16 -0
- package/src/api/services/Controllers.js +26 -0
- package/src/api/services/Logic.js +205 -0
- package/src/api/services/Router.js +14 -0
- package/src/api/services/Schemas.js +66 -0
- package/src/api/snapshots/Controllers.js +37 -0
- package/src/api/snapshots/Logic.js +797 -0
- package/src/api/snapshots/Router.js +15 -0
- package/src/api/snapshots/Schemas.js +23 -0
- package/src/api/webhooks/Controllers.js +49 -0
- package/src/api/webhooks/Logic.js +137 -0
- package/src/api/webhooks/Router.js +12 -0
- package/src/api/webhooks/Schemas.js +31 -0
- package/src/configs/Application.js +147 -0
- package/src/configs/Default.js +13 -0
- package/src/consumers/BlackboxLogsConsumer.js +235 -0
- package/src/consumers/DockerLogsConsumer.js +687 -0
- package/src/db/Tables.js +66 -0
- package/src/db/schemas/firestore.js +18 -0
- package/src/db/schemas/functions.js +65 -0
- package/src/db/schemas/httpTraffic.js +43 -0
- package/src/db/schemas/logging.js +74 -0
- package/src/db/schemas/migrations.js +64 -0
- package/src/db/schemas/mqtt.js +56 -0
- package/src/db/schemas/pubsub.js +90 -0
- package/src/db/schemas/pubsubRegistry.js +22 -0
- package/src/db/schemas/webhooks.js +28 -0
- package/src/emulation/awsiot/Controllers.js +91 -0
- package/src/emulation/awsiot/Logic.js +70 -0
- package/src/emulation/awsiot/Router.js +19 -0
- package/src/emulation/awsiot/Server.js +100 -0
- package/src/emulation/firestore/Server.js +136 -0
- package/src/emulation/logging/Controllers.js +212 -0
- package/src/emulation/logging/Logic.js +416 -0
- package/src/emulation/logging/Router.js +36 -0
- package/src/emulation/logging/Schemas.js +82 -0
- package/src/emulation/logging/Server.js +108 -0
- package/src/emulation/pubsub/Controllers.js +279 -0
- package/src/emulation/pubsub/DefaultTopics.js +162 -0
- package/src/emulation/pubsub/Logic.js +427 -0
- package/src/emulation/pubsub/README.md +309 -0
- package/src/emulation/pubsub/Router.js +33 -0
- package/src/emulation/pubsub/Server.js +104 -0
- package/src/emulation/pubsub/ShadowPoller.js +276 -0
- package/src/emulation/pubsub/ShadowSubscriptionManager.js +199 -0
- package/src/enums/ContainerNames.js +106 -0
- package/src/enums/ErrorReason.js +28 -0
- package/src/enums/FunctionStatuses.js +15 -0
- package/src/enums/FunctionTriggerTypes.js +15 -0
- package/src/enums/GatewayState.js +7 -0
- package/src/enums/ServiceNames.js +68 -0
- package/src/jobs/DatabaseMaintenance.js +184 -0
- package/src/jobs/MessageHistoryCleanup.js +152 -0
- package/src/mcp/ApiClient.js +25 -0
- package/src/mcp/Server.js +52 -0
- package/src/mcp/prompts/debugging.js +104 -0
- package/src/mcp/resources/platform.js +118 -0
- package/src/mcp/tools/data.js +84 -0
- package/src/mcp/tools/docker.js +166 -0
- package/src/mcp/tools/firestore.js +162 -0
- package/src/mcp/tools/functions.js +380 -0
- package/src/mcp/tools/httpTraffic.js +69 -0
- package/src/mcp/tools/logging.js +174 -0
- package/src/mcp/tools/mqtt.js +37 -0
- package/src/mcp/tools/postgres.js +130 -0
- package/src/mcp/tools/pubsub.js +316 -0
- package/src/mcp/tools/redis.js +146 -0
- package/src/mcp/tools/services.js +169 -0
- package/src/mcp/tools/snapshots.js +88 -0
- package/src/mcp/tools/webhooks.js +115 -0
- package/src/middleware/DevProxy.js +67 -0
- package/src/middleware/ErrorCatcher.js +35 -0
- package/src/middleware/HttpProxy.js +215 -0
- package/src/middleware/Reply.js +24 -0
- package/src/middleware/TraceId.js +9 -0
- package/src/middleware/WebhookProxy.js +234 -0
- package/src/protocols/mqtt/Broker.js +92 -0
- package/src/protocols/mqtt/Handlers.js +175 -0
- package/src/protocols/mqtt/PubSubBridge.js +162 -0
- package/src/protocols/mqtt/Server.js +116 -0
- package/src/runtime/FunctionRunner.js +179 -0
- package/src/services/AppGatewayService.js +582 -0
- package/src/singletons/FirestoreBroadcaster.js +367 -0
- package/src/singletons/FunctionTriggerDispatcher.js +456 -0
- package/src/singletons/FunctionsService.js +418 -0
- package/src/singletons/HttpProxy.js +224 -0
- package/src/singletons/LogBroadcaster.js +159 -0
- package/src/singletons/Logger.js +49 -0
- package/src/singletons/MemoryJsonStore.js +175 -0
- package/src/singletons/MessageBroadcaster.js +190 -0
- package/src/singletons/PostgresBroadcaster.js +367 -0
- package/src/singletons/PostgresClient.js +180 -0
- package/src/singletons/RedisClient.js +184 -0
- package/src/singletons/SqliteStore.js +480 -0
- package/src/singletons/TickService.js +151 -0
- package/src/singletons/WebhookProxy.js +223 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import { SqliteStore } from '../../singletons/SqliteStore.js'
|
|
2
|
+
import { Application } from '../../configs/Application.js'
|
|
3
|
+
import { RedisClient } from '../../singletons/RedisClient.js'
|
|
4
|
+
import {
|
|
5
|
+
PUBSUB_TOPICS,
|
|
6
|
+
PUBSUB_SUBSCRIPTIONS,
|
|
7
|
+
PUBSUB_MESSAGES,
|
|
8
|
+
PUBSUB_MESSAGE_HISTORY,
|
|
9
|
+
LOGGING_ENTRIES,
|
|
10
|
+
MQTT_CLIENTS,
|
|
11
|
+
MQTT_MESSAGES,
|
|
12
|
+
FIRESTORE_METADATA
|
|
13
|
+
} from '../../db/Tables.js'
|
|
14
|
+
|
|
15
|
+
const FIRESTORE_API = `http://localhost:${Application.ports.firestore}`
|
|
16
|
+
const PROJECT_ID = Application.firestore.projectId
|
|
17
|
+
const DATABASE = '(default)'
|
|
18
|
+
|
|
19
|
+
async function clearFirestoreEmulator () {
|
|
20
|
+
try {
|
|
21
|
+
// Use Firebase emulator's clear endpoint to delete all data
|
|
22
|
+
const clearUrl = `${FIRESTORE_API}/emulator/v1/projects/${PROJECT_ID}/databases/${DATABASE}/documents`
|
|
23
|
+
await fetch(clearUrl, { method: 'DELETE' })
|
|
24
|
+
} catch {
|
|
25
|
+
// Ignore errors when clearing Firestore (emulator might not be running)
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const Logic = {
|
|
30
|
+
async export (params) {
|
|
31
|
+
const { traceId } = params
|
|
32
|
+
const topics = SqliteStore.list(PUBSUB_TOPICS)
|
|
33
|
+
const subscriptions = SqliteStore.list(PUBSUB_SUBSCRIPTIONS)
|
|
34
|
+
const messages = SqliteStore.list(PUBSUB_MESSAGES)
|
|
35
|
+
const logEntries = SqliteStore.list(LOGGING_ENTRIES)
|
|
36
|
+
const mqttClients = SqliteStore.list(MQTT_CLIENTS)
|
|
37
|
+
const mqttMessages = SqliteStore.list(MQTT_MESSAGES)
|
|
38
|
+
return {
|
|
39
|
+
data: {
|
|
40
|
+
pubsub: {
|
|
41
|
+
topics: topics.data,
|
|
42
|
+
subscriptions: subscriptions.data,
|
|
43
|
+
messages: messages.data
|
|
44
|
+
},
|
|
45
|
+
logging: {
|
|
46
|
+
entries: logEntries.data
|
|
47
|
+
},
|
|
48
|
+
mqtt: {
|
|
49
|
+
clients: mqttClients.data,
|
|
50
|
+
messages: mqttMessages.data
|
|
51
|
+
},
|
|
52
|
+
exportedAt: new Date().toISOString()
|
|
53
|
+
},
|
|
54
|
+
traceId
|
|
55
|
+
}
|
|
56
|
+
},
|
|
57
|
+
async import (params) {
|
|
58
|
+
const { data, traceId } = params
|
|
59
|
+
let imported = {
|
|
60
|
+
topics: 0,
|
|
61
|
+
subscriptions: 0,
|
|
62
|
+
messages: 0,
|
|
63
|
+
logEntries: 0,
|
|
64
|
+
mqttClients: 0,
|
|
65
|
+
mqttMessages: 0
|
|
66
|
+
}
|
|
67
|
+
if (data.pubsub) {
|
|
68
|
+
if (data.pubsub.topics) {
|
|
69
|
+
for (const topic of data.pubsub.topics) {
|
|
70
|
+
SqliteStore.create(PUBSUB_TOPICS, topic)
|
|
71
|
+
imported.topics++
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
if (data.pubsub.subscriptions) {
|
|
75
|
+
for (const subscription of data.pubsub.subscriptions) {
|
|
76
|
+
SqliteStore.create(PUBSUB_SUBSCRIPTIONS, subscription)
|
|
77
|
+
imported.subscriptions++
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (data.pubsub.messages) {
|
|
81
|
+
for (const message of data.pubsub.messages) {
|
|
82
|
+
SqliteStore.create(PUBSUB_MESSAGES, message)
|
|
83
|
+
imported.messages++
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
if (data.logging && data.logging.entries) {
|
|
88
|
+
for (const entry of data.logging.entries) {
|
|
89
|
+
SqliteStore.create(LOGGING_ENTRIES, entry)
|
|
90
|
+
imported.logEntries++
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (data.mqtt) {
|
|
94
|
+
if (data.mqtt.clients) {
|
|
95
|
+
for (const client of data.mqtt.clients) {
|
|
96
|
+
SqliteStore.create(MQTT_CLIENTS, client)
|
|
97
|
+
imported.mqttClients++
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (data.mqtt.messages) {
|
|
101
|
+
for (const message of data.mqtt.messages) {
|
|
102
|
+
SqliteStore.create(MQTT_MESSAGES, message)
|
|
103
|
+
imported.mqttMessages++
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
SqliteStore.flushAll()
|
|
108
|
+
return {
|
|
109
|
+
message: 'Data imported successfully',
|
|
110
|
+
imported,
|
|
111
|
+
traceId
|
|
112
|
+
}
|
|
113
|
+
},
|
|
114
|
+
async clear (params) {
|
|
115
|
+
const { traceId } = params
|
|
116
|
+
SqliteStore.clear(PUBSUB_TOPICS)
|
|
117
|
+
SqliteStore.clear(PUBSUB_SUBSCRIPTIONS)
|
|
118
|
+
SqliteStore.clear(PUBSUB_MESSAGES)
|
|
119
|
+
SqliteStore.clear(LOGGING_ENTRIES)
|
|
120
|
+
SqliteStore.clear(MQTT_CLIENTS)
|
|
121
|
+
SqliteStore.clear(MQTT_MESSAGES)
|
|
122
|
+
// Clear Firestore emulator data
|
|
123
|
+
await clearFirestoreEmulator()
|
|
124
|
+
SqliteStore.clear(FIRESTORE_METADATA)
|
|
125
|
+
return {
|
|
126
|
+
message: 'All data cleared successfully',
|
|
127
|
+
traceId
|
|
128
|
+
}
|
|
129
|
+
},
|
|
130
|
+
|
|
131
|
+
async clearServices (params) {
|
|
132
|
+
const { services, keepSystemData = true, traceId } = params
|
|
133
|
+
const cleared = {}
|
|
134
|
+
try {
|
|
135
|
+
for (const service of services) {
|
|
136
|
+
if (service === 'pubsub') {
|
|
137
|
+
if (keepSystemData) {
|
|
138
|
+
const systemTopics = ['systemOneMinuteTick']
|
|
139
|
+
const allMessages = SqliteStore.list(PUBSUB_MESSAGE_HISTORY).data
|
|
140
|
+
const toDelete = allMessages.filter(msg => !systemTopics.includes(msg.topic))
|
|
141
|
+
for (const msg of toDelete) {
|
|
142
|
+
SqliteStore.delete(PUBSUB_MESSAGE_HISTORY, msg.id)
|
|
143
|
+
}
|
|
144
|
+
cleared.pubsub = toDelete.length
|
|
145
|
+
} else {
|
|
146
|
+
SqliteStore.clear(PUBSUB_MESSAGE_HISTORY)
|
|
147
|
+
cleared.pubsub = 'all'
|
|
148
|
+
}
|
|
149
|
+
} else if (service === 'logging') {
|
|
150
|
+
if (keepSystemData) {
|
|
151
|
+
const systemServices = ['dev-tools']
|
|
152
|
+
const allLogs = SqliteStore.list(LOGGING_ENTRIES).data
|
|
153
|
+
const toDelete = allLogs.filter(log => !systemServices.includes(log.service_name))
|
|
154
|
+
for (const log of toDelete) {
|
|
155
|
+
SqliteStore.delete(LOGGING_ENTRIES, log.id)
|
|
156
|
+
}
|
|
157
|
+
cleared.logging = toDelete.length
|
|
158
|
+
} else {
|
|
159
|
+
SqliteStore.clear(LOGGING_ENTRIES)
|
|
160
|
+
SqliteStore.clear('logging-logs')
|
|
161
|
+
cleared.logging = 'all'
|
|
162
|
+
}
|
|
163
|
+
} else if (service === 'redis') {
|
|
164
|
+
await RedisClient.connect()
|
|
165
|
+
await RedisClient.deleteAll()
|
|
166
|
+
cleared.redis = 'all'
|
|
167
|
+
} else if (service === 'postgres') {
|
|
168
|
+
cleared.postgres = 'not implemented'
|
|
169
|
+
} else if (service === 'firestore') {
|
|
170
|
+
cleared.firestore = 'not implemented'
|
|
171
|
+
} else if (service === 'mqtt') {
|
|
172
|
+
cleared.mqtt = 'not applicable'
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
return {
|
|
176
|
+
cleared,
|
|
177
|
+
traceId
|
|
178
|
+
}
|
|
179
|
+
} catch (error) {
|
|
180
|
+
return {
|
|
181
|
+
status: 'error',
|
|
182
|
+
message: error.message,
|
|
183
|
+
cleared,
|
|
184
|
+
traceId
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import KoaRouter from 'koa-router'
|
|
2
|
+
import { Controllers } from './Controllers.js'
|
|
3
|
+
|
|
4
|
+
const Router = new KoaRouter()
|
|
5
|
+
const v1 = new KoaRouter({ prefix: '/v1/data' })
|
|
6
|
+
|
|
7
|
+
v1.post('/export', Controllers.export)
|
|
8
|
+
v1.post('/import', Controllers.import)
|
|
9
|
+
v1.post('/clear', Controllers.clear)
|
|
10
|
+
|
|
11
|
+
// Testing Helpers
|
|
12
|
+
v1.post('/clear-services', Controllers.clearServices)
|
|
13
|
+
|
|
14
|
+
Router.use(v1.routes())
|
|
15
|
+
|
|
16
|
+
export { Router }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Logic } from './Logic.js'
|
|
2
|
+
|
|
3
|
+
export const Controllers = {
|
|
4
|
+
async list (ctx) {
|
|
5
|
+
const { traceId } = ctx.state
|
|
6
|
+
const result = await Logic.listContainers({ traceId })
|
|
7
|
+
ctx.reply(result)
|
|
8
|
+
},
|
|
9
|
+
async start (ctx) {
|
|
10
|
+
const { traceId } = ctx.state
|
|
11
|
+
const { containerName } = ctx.request.body
|
|
12
|
+
const result = await Logic.startContainer({ containerName, traceId })
|
|
13
|
+
ctx.reply(result)
|
|
14
|
+
},
|
|
15
|
+
async stop (ctx) {
|
|
16
|
+
const { traceId } = ctx.state
|
|
17
|
+
const { containerName } = ctx.request.body
|
|
18
|
+
const result = await Logic.stopContainer({ containerName, traceId })
|
|
19
|
+
ctx.reply(result)
|
|
20
|
+
},
|
|
21
|
+
async restart (ctx) {
|
|
22
|
+
const { traceId } = ctx.state
|
|
23
|
+
const { containerName } = ctx.request.body
|
|
24
|
+
const result = await Logic.restartContainer({ containerName, traceId })
|
|
25
|
+
ctx.reply(result)
|
|
26
|
+
},
|
|
27
|
+
async logs (ctx) {
|
|
28
|
+
const { traceId } = ctx.state
|
|
29
|
+
const { containerName, lines } = ctx.request.body
|
|
30
|
+
const result = await Logic.getContainerLogs({ containerName, lines, traceId })
|
|
31
|
+
ctx.reply(result)
|
|
32
|
+
}
|
|
33
|
+
}
|
|
@@ -0,0 +1,268 @@
|
|
|
1
|
+
import http from 'http'
|
|
2
|
+
import { Logger } from '../../singletons/Logger.js'
|
|
3
|
+
import { ContainerNames, ContainerDisplayNames, ContainerDescriptions, ContainerPorts } from '../../enums/ContainerNames.js'
|
|
4
|
+
|
|
5
|
+
const DOCKER_SOCKET = '/var/run/docker.sock'
|
|
6
|
+
const DOCKER_API_VERSION = 'v1.44'
|
|
7
|
+
|
|
8
|
+
// Map docker container names to our service identifiers
|
|
9
|
+
const CONTAINER_NAME_MAP = {
|
|
10
|
+
'goki-postgres': ContainerNames.POSTGRES,
|
|
11
|
+
'goki-redis': ContainerNames.REDIS,
|
|
12
|
+
'goki-redis-logs': ContainerNames.REDIS_LOGS,
|
|
13
|
+
'goki-pubsub-emulator': ContainerNames.PUBSUB,
|
|
14
|
+
'goki-firestore-emulator': ContainerNames.FIRESTORE,
|
|
15
|
+
'goki-elasticsearch': ContainerNames.ELASTICSEARCH,
|
|
16
|
+
'goki-kibana': ContainerNames.KIBANA,
|
|
17
|
+
'goki-dev-tools-backend': ContainerNames.DEV_TOOLS_BACKEND,
|
|
18
|
+
'goki-dev-tools-frontend': ContainerNames.DEV_TOOLS_FRONTEND,
|
|
19
|
+
'device-native-app': ContainerNames.DEVICE_NATIVE,
|
|
20
|
+
'device-simulator-app': ContainerNames.DEVICE_SIMULATOR,
|
|
21
|
+
'integration-apaleo-app': ContainerNames.INTEGRATION_APALEO,
|
|
22
|
+
'core-pms-app': ContainerNames.CORE_PMS,
|
|
23
|
+
'core-key-app': ContainerNames.CORE_KEY,
|
|
24
|
+
'integration-mews-app': ContainerNames.INTEGRATION_MEWS,
|
|
25
|
+
'integration-opera-app': ContainerNames.INTEGRATION_OPERA
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
// Reverse map for looking up docker names
|
|
29
|
+
const SERVICE_TO_CONTAINER = Object.fromEntries(
|
|
30
|
+
Object.entries(CONTAINER_NAME_MAP).map(([k, v]) => [v, k])
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
const dockerRequest = (method, path, body) => {
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
const options = {
|
|
36
|
+
socketPath: DOCKER_SOCKET,
|
|
37
|
+
path: `/${DOCKER_API_VERSION}${path}`,
|
|
38
|
+
method,
|
|
39
|
+
headers: { 'Content-Type': 'application/json' }
|
|
40
|
+
}
|
|
41
|
+
const req = http.request(options, (res) => {
|
|
42
|
+
let data = ''
|
|
43
|
+
res.on('data', (chunk) => { data += chunk })
|
|
44
|
+
res.on('end', () => {
|
|
45
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
46
|
+
resolve(data ? JSON.parse(data) : null)
|
|
47
|
+
} else if (res.statusCode === 304) {
|
|
48
|
+
resolve(null)
|
|
49
|
+
} else {
|
|
50
|
+
const error = new Error(data ? JSON.parse(data).message : `Docker API returned ${res.statusCode}`)
|
|
51
|
+
error.statusCode = res.statusCode
|
|
52
|
+
reject(error)
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
})
|
|
56
|
+
req.on('error', reject)
|
|
57
|
+
if (body) req.write(JSON.stringify(body))
|
|
58
|
+
req.end()
|
|
59
|
+
})
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const dockerRequestRaw = (method, path) => {
|
|
63
|
+
return new Promise((resolve, reject) => {
|
|
64
|
+
const options = {
|
|
65
|
+
socketPath: DOCKER_SOCKET,
|
|
66
|
+
path: `/${DOCKER_API_VERSION}${path}`,
|
|
67
|
+
method
|
|
68
|
+
}
|
|
69
|
+
const req = http.request(options, (res) => {
|
|
70
|
+
const chunks = []
|
|
71
|
+
res.on('data', (chunk) => chunks.push(chunk))
|
|
72
|
+
res.on('end', () => {
|
|
73
|
+
const data = Buffer.concat(chunks).toString('utf8')
|
|
74
|
+
if (res.statusCode >= 200 && res.statusCode < 300) {
|
|
75
|
+
resolve(data)
|
|
76
|
+
} else {
|
|
77
|
+
reject(new Error(data || `Docker API returned ${res.statusCode}`))
|
|
78
|
+
}
|
|
79
|
+
})
|
|
80
|
+
})
|
|
81
|
+
req.on('error', reject)
|
|
82
|
+
req.end()
|
|
83
|
+
})
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const parseContainerStatus = (state) => {
|
|
87
|
+
if (!state) return 'unknown'
|
|
88
|
+
const lower = state.toLowerCase()
|
|
89
|
+
if (lower === 'running') return 'running'
|
|
90
|
+
if (lower === 'exited') return 'stopped'
|
|
91
|
+
if (lower === 'created') return 'created'
|
|
92
|
+
if (lower === 'restarting') return 'restarting'
|
|
93
|
+
if (lower === 'paused') return 'paused'
|
|
94
|
+
return 'unknown'
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const parseUptime = (status) => {
|
|
98
|
+
if (!status) return null
|
|
99
|
+
const match = status.match(/Up\s+(.+?)(?:\s+\(|$)/i)
|
|
100
|
+
if (!match) return null
|
|
101
|
+
return match[1].trim()
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const parsePublicPort = (ports) => {
|
|
105
|
+
if (!ports || !ports.length) return null
|
|
106
|
+
const publicPort = ports.find(p => p.PublicPort)
|
|
107
|
+
return publicPort ? publicPort.PublicPort : null
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const generateDisplayName = (containerName) => {
|
|
111
|
+
return containerName
|
|
112
|
+
.replace(/-app$/, '')
|
|
113
|
+
.replace(/^goki-/, '')
|
|
114
|
+
.split('-')
|
|
115
|
+
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
|
|
116
|
+
.join(' ')
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export const Logic = {
|
|
120
|
+
async listContainers (params) {
|
|
121
|
+
const { traceId } = params
|
|
122
|
+
try {
|
|
123
|
+
const filters = JSON.stringify({ network: ['goki-network'] })
|
|
124
|
+
const rawContainers = await dockerRequest('GET', `/containers/json?all=true&filters=${encodeURIComponent(filters)}`)
|
|
125
|
+
const containers = rawContainers.map(c => {
|
|
126
|
+
const name = c.Names[0].replace(/^\//, '')
|
|
127
|
+
const serviceId = CONTAINER_NAME_MAP[name] || name
|
|
128
|
+
const isKnown = !!CONTAINER_NAME_MAP[name]
|
|
129
|
+
const isSelf = serviceId === ContainerNames.DEV_TOOLS_BACKEND
|
|
130
|
+
return {
|
|
131
|
+
name: serviceId,
|
|
132
|
+
containerName: name,
|
|
133
|
+
displayName: ContainerDisplayNames[serviceId] || generateDisplayName(name),
|
|
134
|
+
description: ContainerDescriptions[serviceId] || '',
|
|
135
|
+
status: parseContainerStatus(c.State),
|
|
136
|
+
statusText: c.Status,
|
|
137
|
+
uptime: parseUptime(c.Status),
|
|
138
|
+
ports: ContainerPorts[serviceId] || parsePublicPort(c.Ports),
|
|
139
|
+
image: c.Image,
|
|
140
|
+
category: isKnown ? 'infrastructure' : 'microservice',
|
|
141
|
+
capabilities: {
|
|
142
|
+
canStart: !isSelf,
|
|
143
|
+
canStop: !isSelf,
|
|
144
|
+
canRestart: !isSelf
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
})
|
|
148
|
+
containers.sort((a, b) => a.displayName.localeCompare(b.displayName))
|
|
149
|
+
return {
|
|
150
|
+
data: containers,
|
|
151
|
+
traceId
|
|
152
|
+
}
|
|
153
|
+
} catch (error) {
|
|
154
|
+
Logger.log({
|
|
155
|
+
level: 'error',
|
|
156
|
+
message: 'Failed to list Docker containers',
|
|
157
|
+
data: { error: error.message, traceId }
|
|
158
|
+
})
|
|
159
|
+
return {
|
|
160
|
+
data: [],
|
|
161
|
+
message: 'Failed to list containers: ' + error.message,
|
|
162
|
+
traceId
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
async startContainer (params) {
|
|
167
|
+
const { containerName, traceId } = params
|
|
168
|
+
const dockerName = SERVICE_TO_CONTAINER[containerName] || containerName
|
|
169
|
+
try {
|
|
170
|
+
await dockerRequest('POST', `/containers/${dockerName}/start`)
|
|
171
|
+
Logger.log({
|
|
172
|
+
level: 'info',
|
|
173
|
+
message: 'Container started',
|
|
174
|
+
data: { containerName: dockerName, traceId }
|
|
175
|
+
})
|
|
176
|
+
return {
|
|
177
|
+
message: `${ContainerDisplayNames[containerName] || containerName} started successfully`,
|
|
178
|
+
traceId
|
|
179
|
+
}
|
|
180
|
+
} catch (error) {
|
|
181
|
+
Logger.log({
|
|
182
|
+
level: 'error',
|
|
183
|
+
message: 'Failed to start container',
|
|
184
|
+
data: { containerName: dockerName, error: error.message, traceId }
|
|
185
|
+
})
|
|
186
|
+
return {
|
|
187
|
+
message: `Failed to start ${containerName}: ${error.message}`,
|
|
188
|
+
traceId
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
},
|
|
192
|
+
async stopContainer (params) {
|
|
193
|
+
const { containerName, traceId } = params
|
|
194
|
+
const dockerName = SERVICE_TO_CONTAINER[containerName] || containerName
|
|
195
|
+
try {
|
|
196
|
+
await dockerRequest('POST', `/containers/${dockerName}/stop`)
|
|
197
|
+
Logger.log({
|
|
198
|
+
level: 'info',
|
|
199
|
+
message: 'Container stopped',
|
|
200
|
+
data: { containerName: dockerName, traceId }
|
|
201
|
+
})
|
|
202
|
+
return {
|
|
203
|
+
message: `${ContainerDisplayNames[containerName] || containerName} stopped successfully`,
|
|
204
|
+
traceId
|
|
205
|
+
}
|
|
206
|
+
} catch (error) {
|
|
207
|
+
Logger.log({
|
|
208
|
+
level: 'error',
|
|
209
|
+
message: 'Failed to stop container',
|
|
210
|
+
data: { containerName: dockerName, error: error.message, traceId }
|
|
211
|
+
})
|
|
212
|
+
return {
|
|
213
|
+
message: `Failed to stop ${containerName}: ${error.message}`,
|
|
214
|
+
traceId
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
async restartContainer (params) {
|
|
219
|
+
const { containerName, traceId } = params
|
|
220
|
+
const dockerName = SERVICE_TO_CONTAINER[containerName] || containerName
|
|
221
|
+
try {
|
|
222
|
+
await dockerRequest('POST', `/containers/${dockerName}/restart`)
|
|
223
|
+
Logger.log({
|
|
224
|
+
level: 'info',
|
|
225
|
+
message: 'Container restarted',
|
|
226
|
+
data: { containerName: dockerName, traceId }
|
|
227
|
+
})
|
|
228
|
+
return {
|
|
229
|
+
message: `${ContainerDisplayNames[containerName] || containerName} restarted successfully`,
|
|
230
|
+
traceId
|
|
231
|
+
}
|
|
232
|
+
} catch (error) {
|
|
233
|
+
Logger.log({
|
|
234
|
+
level: 'error',
|
|
235
|
+
message: 'Failed to restart container',
|
|
236
|
+
data: { containerName: dockerName, error: error.message, traceId }
|
|
237
|
+
})
|
|
238
|
+
return {
|
|
239
|
+
message: `Failed to restart ${containerName}: ${error.message}`,
|
|
240
|
+
traceId
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
},
|
|
244
|
+
async getContainerLogs (params) {
|
|
245
|
+
const { containerName, lines = 100, traceId } = params
|
|
246
|
+
const dockerName = SERVICE_TO_CONTAINER[containerName] || containerName
|
|
247
|
+
try {
|
|
248
|
+
const raw = await dockerRequestRaw('GET', `/containers/${dockerName}/logs?stdout=true&stderr=true&tail=${lines}`)
|
|
249
|
+
// Docker log stream has 8-byte header per frame, strip them
|
|
250
|
+
const cleaned = raw.replace(/[\x00-\x08]/g, '').replace(/\r/g, '')
|
|
251
|
+
return {
|
|
252
|
+
data: cleaned,
|
|
253
|
+
traceId
|
|
254
|
+
}
|
|
255
|
+
} catch (error) {
|
|
256
|
+
Logger.log({
|
|
257
|
+
level: 'error',
|
|
258
|
+
message: 'Failed to get container logs',
|
|
259
|
+
data: { containerName: dockerName, error: error.message, traceId }
|
|
260
|
+
})
|
|
261
|
+
return {
|
|
262
|
+
data: '',
|
|
263
|
+
message: `Failed to get logs: ${error.message}`,
|
|
264
|
+
traceId
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import KoaRouter from 'koa-router'
|
|
2
|
+
import { Controllers } from './Controllers.js'
|
|
3
|
+
|
|
4
|
+
const Router = new KoaRouter()
|
|
5
|
+
const v1 = new KoaRouter({ prefix: '/v1/docker' })
|
|
6
|
+
|
|
7
|
+
v1.post('/containers/list', Controllers.list)
|
|
8
|
+
v1.post('/containers/start', Controllers.start)
|
|
9
|
+
v1.post('/containers/stop', Controllers.stop)
|
|
10
|
+
v1.post('/containers/restart', Controllers.restart)
|
|
11
|
+
v1.post('/containers/logs', Controllers.logs)
|
|
12
|
+
|
|
13
|
+
Router.use(v1.routes())
|
|
14
|
+
|
|
15
|
+
export { Router }
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Joi } from '@gokiteam/koa'
|
|
2
|
+
|
|
3
|
+
export const Schemas = {
|
|
4
|
+
list: {
|
|
5
|
+
request: {
|
|
6
|
+
body: {}
|
|
7
|
+
},
|
|
8
|
+
responses: {
|
|
9
|
+
success: {
|
|
10
|
+
data: Joi.array().items(
|
|
11
|
+
Joi.object({
|
|
12
|
+
name: Joi.string().required(),
|
|
13
|
+
containerName: Joi.string().required(),
|
|
14
|
+
displayName: Joi.string().required(),
|
|
15
|
+
description: Joi.string().allow('').required(),
|
|
16
|
+
status: Joi.string().valid('running', 'stopped', 'created', 'restarting', 'paused', 'unknown').required(),
|
|
17
|
+
statusText: Joi.string().required(),
|
|
18
|
+
uptime: Joi.string().allow(null).required(),
|
|
19
|
+
ports: Joi.number().integer().allow(null).required(),
|
|
20
|
+
image: Joi.string().required(),
|
|
21
|
+
capabilities: Joi.object({
|
|
22
|
+
canStart: Joi.boolean().required(),
|
|
23
|
+
canStop: Joi.boolean().required(),
|
|
24
|
+
canRestart: Joi.boolean().required()
|
|
25
|
+
}).required()
|
|
26
|
+
})
|
|
27
|
+
).required()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
},
|
|
31
|
+
start: {
|
|
32
|
+
request: {
|
|
33
|
+
body: {
|
|
34
|
+
containerName: Joi.string().required()
|
|
35
|
+
}
|
|
36
|
+
},
|
|
37
|
+
responses: {
|
|
38
|
+
success: {
|
|
39
|
+
message: Joi.string().required()
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
stop: {
|
|
44
|
+
request: {
|
|
45
|
+
body: {
|
|
46
|
+
containerName: Joi.string().required()
|
|
47
|
+
}
|
|
48
|
+
},
|
|
49
|
+
responses: {
|
|
50
|
+
success: {
|
|
51
|
+
message: Joi.string().required()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
restart: {
|
|
56
|
+
request: {
|
|
57
|
+
body: {
|
|
58
|
+
containerName: Joi.string().required()
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
responses: {
|
|
62
|
+
success: {
|
|
63
|
+
message: Joi.string().required()
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
},
|
|
67
|
+
logs: {
|
|
68
|
+
request: {
|
|
69
|
+
body: {
|
|
70
|
+
containerName: Joi.string().required(),
|
|
71
|
+
lines: Joi.number().integer().min(1).max(1000).default(100)
|
|
72
|
+
}
|
|
73
|
+
},
|
|
74
|
+
responses: {
|
|
75
|
+
success: {
|
|
76
|
+
data: Joi.string().required()
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { Logic } from './Logic.js'
|
|
2
|
+
|
|
3
|
+
export const Controllers = {
|
|
4
|
+
async resolve (ctx) {
|
|
5
|
+
const docPath = ctx.params[0] || ''
|
|
6
|
+
const result = Logic.resolve(docPath)
|
|
7
|
+
if (!result) {
|
|
8
|
+
ctx.status = 404
|
|
9
|
+
ctx.body = 'Not found'
|
|
10
|
+
return
|
|
11
|
+
}
|
|
12
|
+
ctx.type = result.contentType
|
|
13
|
+
ctx.body = result.content
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fs from 'fs'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { fileURLToPath } from 'url'
|
|
4
|
+
|
|
5
|
+
const __filename = fileURLToPath(import.meta.url)
|
|
6
|
+
const __dirname = path.dirname(__filename)
|
|
7
|
+
const projectRoot = path.resolve(__dirname, '../../../')
|
|
8
|
+
const docsRoot = path.join(projectRoot, 'docs')
|
|
9
|
+
|
|
10
|
+
const ALLOWED_EXTENSIONS = ['.md', '.js', '.ts', '.json', '.txt']
|
|
11
|
+
|
|
12
|
+
const CONTENT_TYPES = {
|
|
13
|
+
'.md': 'text/markdown; charset=utf-8',
|
|
14
|
+
'.js': 'text/javascript; charset=utf-8',
|
|
15
|
+
'.ts': 'text/typescript; charset=utf-8',
|
|
16
|
+
'.json': 'application/json; charset=utf-8',
|
|
17
|
+
'.txt': 'text/plain; charset=utf-8'
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const isAllowedFile = (filename) => {
|
|
21
|
+
const ext = path.extname(filename).toLowerCase()
|
|
22
|
+
return ALLOWED_EXTENSIONS.includes(ext)
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const getContentType = (filename) => {
|
|
26
|
+
const ext = path.extname(filename).toLowerCase()
|
|
27
|
+
return CONTENT_TYPES[ext] || 'text/plain; charset=utf-8'
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const resolveSafe = (requestedPath) => {
|
|
31
|
+
const resolved = path.resolve(docsRoot, requestedPath)
|
|
32
|
+
if (!resolved.startsWith(docsRoot)) return null
|
|
33
|
+
if (resolved.includes('node_modules')) return null
|
|
34
|
+
return resolved
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const toApiPath = (fsPath) => {
|
|
38
|
+
const relative = path.relative(docsRoot, fsPath)
|
|
39
|
+
return `/v1/docs/${relative}`.replace(/\/+/g, '/')
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const generateDirectoryListing = (absDir, dirName) => {
|
|
43
|
+
const entries = fs.readdirSync(absDir, { withFileTypes: true })
|
|
44
|
+
const dirs = []
|
|
45
|
+
const files = []
|
|
46
|
+
for (const entry of entries) {
|
|
47
|
+
if (entry.name.startsWith('.')) continue
|
|
48
|
+
if (entry.name === 'node_modules') continue
|
|
49
|
+
if (entry.name === 'index.md') continue
|
|
50
|
+
const fullPath = path.join(absDir, entry.name)
|
|
51
|
+
if (entry.isDirectory()) {
|
|
52
|
+
dirs.push(`- [${entry.name}/](${toApiPath(fullPath)}/)`)
|
|
53
|
+
} else if (isAllowedFile(entry.name)) {
|
|
54
|
+
files.push(`- [${entry.name}](${toApiPath(fullPath)})`)
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
dirs.sort()
|
|
58
|
+
files.sort()
|
|
59
|
+
const title = dirName || 'docs'
|
|
60
|
+
const lines = [`# ${title}\n`]
|
|
61
|
+
if (dirs.length) lines.push('## Directories\n', ...dirs, '')
|
|
62
|
+
if (files.length) lines.push('## Files\n', ...files, '')
|
|
63
|
+
return lines.join('\n')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export const Logic = {
|
|
67
|
+
resolve (docPath) {
|
|
68
|
+
const cleaned = (docPath || '').replace(/^\/+/, '').replace(/\/+$/, '')
|
|
69
|
+
const absPath = cleaned ? resolveSafe(cleaned) : docsRoot
|
|
70
|
+
if (!absPath || !fs.existsSync(absPath)) return null
|
|
71
|
+
const stat = fs.statSync(absPath)
|
|
72
|
+
if (stat.isDirectory()) {
|
|
73
|
+
const indexPath = path.join(absPath, 'index.md')
|
|
74
|
+
const content = fs.existsSync(indexPath)
|
|
75
|
+
? fs.readFileSync(indexPath, 'utf-8')
|
|
76
|
+
: generateDirectoryListing(absPath, cleaned.split('/').pop())
|
|
77
|
+
return { contentType: 'text/markdown; charset=utf-8', content }
|
|
78
|
+
}
|
|
79
|
+
if (stat.isFile()) {
|
|
80
|
+
if (!isAllowedFile(absPath)) return null
|
|
81
|
+
return { contentType: getContentType(absPath), content: fs.readFileSync(absPath, 'utf-8') }
|
|
82
|
+
}
|
|
83
|
+
return null
|
|
84
|
+
}
|
|
85
|
+
}
|