@platformatic/watt-extra 0.1.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.
Files changed (95) hide show
  1. package/README.md +87 -0
  2. package/app.js +124 -0
  3. package/cli.js +141 -0
  4. package/clients/compliance/compliance-types.d.ts +887 -0
  5. package/clients/compliance/compliance.mjs +1049 -0
  6. package/clients/compliance/compliance.openapi.json +6127 -0
  7. package/clients/control-plane/control-plane-types.d.ts +2696 -0
  8. package/clients/control-plane/control-plane.mjs +3051 -0
  9. package/clients/control-plane/control-plane.openapi.json +13693 -0
  10. package/clients/cron/cron-types.d.ts +1479 -0
  11. package/clients/cron/cron.mjs +872 -0
  12. package/clients/cron/cron.openapi.json +9330 -0
  13. package/compliance/index.js +21 -0
  14. package/compliance/rules/dependencies.js +76 -0
  15. package/compliance/rules/utils.js +12 -0
  16. package/eslint.config.js +11 -0
  17. package/help/start.txt +12 -0
  18. package/help/watt-extra.txt +12 -0
  19. package/index.js +45 -0
  20. package/lib/banner.js +22 -0
  21. package/lib/errors.js +34 -0
  22. package/lib/utils.js +34 -0
  23. package/lib/wattpro.js +580 -0
  24. package/package.json +50 -0
  25. package/plugins/alerts.js +115 -0
  26. package/plugins/auth.js +89 -0
  27. package/plugins/compliancy.js +70 -0
  28. package/plugins/env.js +58 -0
  29. package/plugins/flamegraphs.js +100 -0
  30. package/plugins/init.js +70 -0
  31. package/plugins/metadata.js +84 -0
  32. package/plugins/scheduler.js +48 -0
  33. package/plugins/update.js +128 -0
  34. package/renovate.json +6 -0
  35. package/test/alerts.test.js +607 -0
  36. package/test/auth.test.js +128 -0
  37. package/test/auto-cache.test.js +401 -0
  38. package/test/cli.test.js +75 -0
  39. package/test/compliancy.test.js +87 -0
  40. package/test/fixtures/runtime-domains/alpha/package.json +5 -0
  41. package/test/fixtures/runtime-domains/alpha/platformatic.json +6 -0
  42. package/test/fixtures/runtime-domains/alpha/plugin.js +16 -0
  43. package/test/fixtures/runtime-domains/beta/package.json +5 -0
  44. package/test/fixtures/runtime-domains/beta/platformatic.json +6 -0
  45. package/test/fixtures/runtime-domains/beta/plugin.js +7 -0
  46. package/test/fixtures/runtime-domains/composer/package.json +5 -0
  47. package/test/fixtures/runtime-domains/composer/platformatic.json +19 -0
  48. package/test/fixtures/runtime-domains/package.json +1 -0
  49. package/test/fixtures/runtime-domains/platformatic.json +27 -0
  50. package/test/fixtures/runtime-health/package.json +20 -0
  51. package/test/fixtures/runtime-health/platformatic.json +16 -0
  52. package/test/fixtures/runtime-health/services/service-1/package.json +17 -0
  53. package/test/fixtures/runtime-health/services/service-1/platformatic.json +16 -0
  54. package/test/fixtures/runtime-health/services/service-1/plugins/example.js +6 -0
  55. package/test/fixtures/runtime-health/services/service-1/routes/root.cjs +8 -0
  56. package/test/fixtures/runtime-health/services/service-2/package.json +17 -0
  57. package/test/fixtures/runtime-health/services/service-2/platformatic.json +16 -0
  58. package/test/fixtures/runtime-health/services/service-2/plugins/example.js +6 -0
  59. package/test/fixtures/runtime-health/services/service-2/routes/root.cjs +8 -0
  60. package/test/fixtures/runtime-next/package.json +5 -0
  61. package/test/fixtures/runtime-next/platformatic.json +9 -0
  62. package/test/fixtures/runtime-next/web/next/next.config.js +2 -0
  63. package/test/fixtures/runtime-next/web/next/package.json +7 -0
  64. package/test/fixtures/runtime-next/web/next/platformatic.json +9 -0
  65. package/test/fixtures/runtime-next/web/next/src/app/direct/route.js +3 -0
  66. package/test/fixtures/runtime-next/web/next/src/app/layout.jsx +7 -0
  67. package/test/fixtures/runtime-next/web/next/src/app/page.jsx +3 -0
  68. package/test/fixtures/runtime-scheduler/main/package.json +5 -0
  69. package/test/fixtures/runtime-scheduler/main/platformatic.json +9 -0
  70. package/test/fixtures/runtime-scheduler/main/routes/root.cjs +11 -0
  71. package/test/fixtures/runtime-scheduler/package.json +1 -0
  72. package/test/fixtures/runtime-scheduler/platformatic.json +27 -0
  73. package/test/fixtures/runtime-service/main/package.json +5 -0
  74. package/test/fixtures/runtime-service/main/platformatic.json +12 -0
  75. package/test/fixtures/runtime-service/main/routes/root.cjs +11 -0
  76. package/test/fixtures/runtime-service/package.json +1 -0
  77. package/test/fixtures/runtime-service/platformatic.json +19 -0
  78. package/test/fixtures/service-1/package.json +7 -0
  79. package/test/fixtures/service-1/platformatic.json +18 -0
  80. package/test/fixtures/service-1/routes/root.cjs +48 -0
  81. package/test/fixtures/service-2/platformatic.json +21 -0
  82. package/test/fixtures/service-2/routes/root.cjs +5 -0
  83. package/test/fixtures/service-3/package.json +5 -0
  84. package/test/fixtures/service-3/platformatic.json +21 -0
  85. package/test/fixtures/service-3/routes/root.cjs +8 -0
  86. package/test/health.test.js +44 -0
  87. package/test/helper.js +274 -0
  88. package/test/init.test.js +243 -0
  89. package/test/patch-config.test.js +434 -0
  90. package/test/scheduler.test.js +71 -0
  91. package/test/send-to-icc-retry.test.js +138 -0
  92. package/test/shared-context.test.js +82 -0
  93. package/test/spawn.test.js +110 -0
  94. package/test/trigger-flamegraphs.test.js +226 -0
  95. package/test/update.test.js +519 -0
@@ -0,0 +1,82 @@
1
+ import assert from 'node:assert'
2
+ import { test } from 'node:test'
3
+ import { join, dirname } from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
5
+
6
+ import { randomUUID } from 'node:crypto'
7
+ import { setTimeout as sleep } from 'node:timers/promises'
8
+ import { request } from 'undici'
9
+ import {
10
+ startICC,
11
+ installDeps,
12
+ setUpEnvironment,
13
+ createJwtToken
14
+ } from './helper.js'
15
+ import { start } from '../index.js'
16
+
17
+ const __filename = fileURLToPath(import.meta.url)
18
+ const __dirname = dirname(__filename)
19
+
20
+ test('should propagete a jwt token via runtime shared context', async (t) => {
21
+ const applicationName = 'test-app'
22
+ const applicationId = randomUUID()
23
+ const applicationPath = join(__dirname, 'fixtures', 'runtime-domains')
24
+
25
+ await installDeps(t, applicationPath, ['@platformatic/composer'])
26
+
27
+ const expiresIn = 10
28
+ const jwt = createJwtToken(expiresIn)
29
+
30
+ setUpEnvironment({
31
+ PLT_APP_NAME: applicationName,
32
+ PLT_APP_DIR: applicationPath,
33
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
34
+ PLT_TEST_TOKEN: jwt,
35
+ PLT_JWT_EXPIRATION_OFFSET_SEC: 1
36
+ })
37
+
38
+ const icc = await startICC(t, {
39
+ applicationId,
40
+ applicationName
41
+ })
42
+
43
+ const app = await start()
44
+
45
+ let newToken = null
46
+ setTimeout(() => {
47
+ newToken = createJwtToken(expiresIn)
48
+ process.env.PLT_TEST_TOKEN = newToken
49
+ }, expiresIn * 1000)
50
+
51
+ t.after(async () => {
52
+ await app.close()
53
+ await icc.close()
54
+ })
55
+
56
+ {
57
+ const { statusCode, body } = await request('http://127.0.0.1:3042/1/shared-context')
58
+ assert.strictEqual(statusCode, 200)
59
+
60
+ const sharedContext = await body.json()
61
+
62
+ const { iccAuthHeaders } = sharedContext
63
+ const { authorization } = iccAuthHeaders
64
+ assert.strictEqual(authorization, `Bearer ${jwt}`)
65
+ }
66
+
67
+ // Wait for the token to expire
68
+ await sleep(expiresIn * 1000 + 1000)
69
+
70
+ {
71
+ const { statusCode, body } = await request('http://127.0.0.1:3042/1/shared-context')
72
+ assert.strictEqual(statusCode, 200)
73
+
74
+ const sharedContext = await body.json()
75
+
76
+ const { iccAuthHeaders } = sharedContext
77
+ const { authorization } = iccAuthHeaders
78
+ assert.strictEqual(authorization, `Bearer ${newToken}`)
79
+ }
80
+
81
+ assert.notStrictEqual(jwt, newToken)
82
+ })
@@ -0,0 +1,110 @@
1
+ import assert from 'node:assert'
2
+ import { test } from 'node:test'
3
+ import { hostname } from 'node:os'
4
+ import { randomUUID } from 'node:crypto'
5
+ import { request } from 'undici'
6
+ import { join, dirname } from 'node:path'
7
+ import { fileURLToPath } from 'node:url'
8
+ import { createRequire } from 'node:module'
9
+ import { setTimeout as sleep } from 'node:timers/promises'
10
+ import { setUpEnvironment, startICC } from './helper.js'
11
+ import { start } from '../index.js'
12
+
13
+ const __filename = fileURLToPath(import.meta.url)
14
+ const __dirname = dirname(__filename)
15
+
16
+ const require = createRequire(import.meta.url)
17
+ const platformaticVersion = require('@platformatic/runtime/package.json').version
18
+
19
+ test('should spawn a service app sending the state', async (t) => {
20
+ const applicationName = 'test-app'
21
+ const applicationId = randomUUID()
22
+ const applicationPath = join(__dirname, 'fixtures', 'service-1')
23
+
24
+ const applicationStates = []
25
+
26
+ const icc = await startICC(t, {
27
+ applicationId,
28
+ applicationName,
29
+ saveApplicationInstanceState: (state) => {
30
+ applicationStates.push(state)
31
+ },
32
+ })
33
+
34
+ process.env.PLT_TEST_APP_1_URL = 'http://test-app-1:3042'
35
+ t.after(() => {
36
+ delete process.env.PLT_TEST_APP_1_URL
37
+ })
38
+
39
+ setUpEnvironment({
40
+ PLT_APP_NAME: applicationName,
41
+ PLT_APP_DIR: applicationPath,
42
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
43
+ })
44
+
45
+ const app = await start()
46
+
47
+ t.after(async () => {
48
+ await app.close()
49
+ await icc.close()
50
+ })
51
+
52
+ {
53
+ const { statusCode, body } = await request('http://127.0.0.1:3042/example')
54
+ assert.strictEqual(statusCode, 200)
55
+
56
+ const data = await body.json()
57
+ assert.deepStrictEqual(data, { hello: 'world' })
58
+ }
59
+
60
+ assert.strictEqual(applicationStates.length, 1)
61
+ const [state] = applicationStates
62
+ assert.strictEqual(state.instanceId, hostname())
63
+ assert.deepStrictEqual(state.state.applications.length, 1)
64
+ assert.strictEqual(
65
+ state.state.metadata.platformaticVersion,
66
+ platformaticVersion
67
+ )
68
+ })
69
+
70
+ test('should not fail on worker error', async (t) => {
71
+ const applicationName = 'test-app'
72
+ const applicationId = randomUUID()
73
+ const applicationPath = join(__dirname, 'fixtures', 'service-3')
74
+
75
+ const icc = await startICC(t, {
76
+ applicationId,
77
+ })
78
+
79
+ setUpEnvironment({
80
+ PLT_APP_NAME: applicationName,
81
+ PLT_APP_DIR: applicationPath,
82
+ PLT_ICC_URL: 'http://127.0.0.1:3000',
83
+ })
84
+
85
+ const app = await start()
86
+
87
+ t.after(async () => {
88
+ await app.close()
89
+ await icc.close()
90
+ })
91
+
92
+ {
93
+ const { statusCode, body } = await request('http://127.0.0.1:3042/example')
94
+ assert.strictEqual(statusCode, 200)
95
+
96
+ const data = await body.json()
97
+ assert.deepStrictEqual(data, { hello: 'world' })
98
+ }
99
+
100
+ // Await for runtime to crash and restart
101
+ await sleep(4000)
102
+
103
+ {
104
+ const { statusCode, body } = await request('http://127.0.0.1:3042/example')
105
+ assert.strictEqual(statusCode, 200)
106
+
107
+ const data = await body.json()
108
+ assert.deepStrictEqual(data, { hello: 'world' })
109
+ }
110
+ })
@@ -0,0 +1,226 @@
1
+ import { test } from 'node:test'
2
+ import { equal } from 'node:assert'
3
+ import { once } from 'node:events'
4
+ import { setTimeout as sleep } from 'node:timers/promises'
5
+ import { WebSocketServer } from 'ws'
6
+ import { setUpEnvironment } from './helper.js'
7
+ import updatePlugin from '../plugins/update.js'
8
+ import flamegraphsPlugin from '../plugins/flamegraphs.js'
9
+
10
+ function setupMockIccServer (wss, receivedMessages, validateAuth = false) {
11
+ let ws = null
12
+
13
+ const waitForClientSubscription = once(wss, 'connection').then(
14
+ ([socket, req]) => {
15
+ ws = socket
16
+
17
+ if (validateAuth) {
18
+ equal(req.headers.authorization, 'Bearer test-token')
19
+ }
20
+
21
+ return new Promise((resolve) => {
22
+ socket.on('message', (data) => {
23
+ const message = JSON.parse(data.toString())
24
+ receivedMessages.push(message)
25
+
26
+ if (message.command === 'subscribe' && message.topic === '/config') {
27
+ socket.send(JSON.stringify({ command: 'ack' }))
28
+ resolve()
29
+ }
30
+ })
31
+ })
32
+ }
33
+ )
34
+
35
+ return { waitForClientSubscription, getWs: () => ws }
36
+ }
37
+
38
+ function createMockApp (port, includeScalerUrl = true) {
39
+ const mockWattPro = {
40
+ runtime: {
41
+ getApplications: () => ({
42
+ applications: [{ id: 'service-1' }, { id: 'service-2' }],
43
+ }),
44
+ },
45
+ }
46
+
47
+ const app = {
48
+ log: {
49
+ info: () => {},
50
+ error: () => {},
51
+ warn: () => {},
52
+ debug: () => {},
53
+ },
54
+ instanceConfig: {
55
+ applicationId: 'test-application-id',
56
+ },
57
+ instanceId: 'test-pod-123',
58
+ getAuthorizationHeader: async () => {
59
+ return { Authorization: 'Bearer test-token' }
60
+ },
61
+ env: {
62
+ PLT_APP_NAME: 'test-app',
63
+ PLT_APP_DIR: '/path/to/app',
64
+ PLT_ICC_URL: `http://localhost:${port}`,
65
+ PLT_DISABLE_FLAMEGRAPHS: false,
66
+ PLT_FLAMEGRAPHS_INTERVAL_SEC: 1
67
+ },
68
+ wattpro: mockWattPro,
69
+ }
70
+
71
+ if (includeScalerUrl) {
72
+ app.instanceConfig.iccServices = {
73
+ scaler: {
74
+ url: `http://localhost:${port}/scaler`,
75
+ },
76
+ }
77
+ }
78
+
79
+ return app
80
+ }
81
+
82
+ const port = 14000
83
+
84
+ test('should handle trigger-flamegraph command and upload flamegraphs from services', async (t) => {
85
+ setUpEnvironment()
86
+
87
+ const receivedMessages = []
88
+ const getFlamegraphReqs = []
89
+ let uploadResolve
90
+ const allUploadsComplete = new Promise((resolve) => {
91
+ uploadResolve = resolve
92
+ })
93
+
94
+ const wss = new WebSocketServer({ port })
95
+ t.after(async () => wss.close())
96
+
97
+ const { waitForClientSubscription, getWs } = setupMockIccServer(
98
+ wss,
99
+ receivedMessages,
100
+ true
101
+ )
102
+
103
+ const app = createMockApp(port)
104
+
105
+ app.wattpro.runtime.sendCommandToApplication = async (
106
+ serviceId,
107
+ command
108
+ ) => {
109
+ if (command === 'getLastProfile') {
110
+ getFlamegraphReqs.push({ serviceId })
111
+ if (getFlamegraphReqs.length === 2) {
112
+ uploadResolve()
113
+ }
114
+ return { success: true }
115
+ }
116
+ return { success: false }
117
+ }
118
+
119
+ await updatePlugin(app)
120
+ await flamegraphsPlugin(app)
121
+
122
+ await app.connectToUpdates()
123
+ await app.setupFlamegraphs()
124
+
125
+ await waitForClientSubscription
126
+
127
+ const triggerFlamegraphMessage = {
128
+ command: 'trigger-flamegraph',
129
+ }
130
+
131
+ getWs().send(JSON.stringify(triggerFlamegraphMessage))
132
+
133
+ await allUploadsComplete
134
+
135
+ equal(getFlamegraphReqs.length, 2)
136
+
137
+ const service1Req = getFlamegraphReqs.find(
138
+ (f) => f.serviceId === 'service-1'
139
+ )
140
+ const service2Req = getFlamegraphReqs.find(
141
+ (f) => f.serviceId === 'service-2'
142
+ )
143
+
144
+ equal(service1Req.serviceId, 'service-1')
145
+ equal(service2Req.serviceId, 'service-2')
146
+
147
+ await app.closeUpdates()
148
+ })
149
+
150
+ test('should handle trigger-flamegraph when no runtime is available', async (t) => {
151
+ setUpEnvironment()
152
+
153
+ const receivedMessages = []
154
+
155
+ const wss = new WebSocketServer({ port: port + 1 })
156
+ t.after(async () => wss.close())
157
+
158
+ const { waitForClientSubscription, getWs } = setupMockIccServer(
159
+ wss,
160
+ receivedMessages,
161
+ false
162
+ )
163
+
164
+ const app = createMockApp(port + 1)
165
+ app.wattpro.runtime = null
166
+
167
+ await updatePlugin(app)
168
+ await app.connectToUpdates()
169
+ await waitForClientSubscription
170
+
171
+ const triggerFlamegraphMessage = {
172
+ command: 'trigger-flamegraph',
173
+ }
174
+
175
+ getWs().send(JSON.stringify(triggerFlamegraphMessage))
176
+
177
+ await sleep(100)
178
+
179
+ await app.closeUpdates()
180
+ })
181
+
182
+ test('should handle trigger-flamegraph when flamegraph upload fails', async (t) => {
183
+ setUpEnvironment()
184
+
185
+ const receivedMessages = []
186
+
187
+ const wss = new WebSocketServer({ port: port + 2 })
188
+ t.after(async () => wss.close())
189
+
190
+ const { waitForClientSubscription, getWs } = setupMockIccServer(
191
+ wss,
192
+ receivedMessages,
193
+ false
194
+ )
195
+
196
+ const app = createMockApp(port + 2)
197
+
198
+ app.wattpro.runtime.sendCommandToApplication = async (
199
+ serviceId,
200
+ command,
201
+ options
202
+ ) => {
203
+ if (command === 'sendFlamegraph' && options.url && options.headers) {
204
+ throw new Error('Flamegraph upload failed')
205
+ }
206
+ return { success: false }
207
+ }
208
+
209
+ await updatePlugin(app)
210
+ await flamegraphsPlugin(app)
211
+
212
+ await app.connectToUpdates()
213
+ await app.setupFlamegraphs()
214
+
215
+ await waitForClientSubscription
216
+
217
+ const triggerFlamegraphMessage = {
218
+ command: 'trigger-flamegraph',
219
+ }
220
+
221
+ getWs().send(JSON.stringify(triggerFlamegraphMessage))
222
+
223
+ await sleep(100)
224
+
225
+ await app.closeUpdates()
226
+ })