@hearthkit/cli 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.
- package/package.json +45 -0
- package/src/cli-contract.ts +423 -0
- package/src/cli-failure-results.ts +175 -0
- package/src/cli-output-streams.ts +14 -0
- package/src/cli-runtime-context.ts +8 -0
- package/src/default-backup-file-path.ts +28 -0
- package/src/derive-hearthkit-project-name.ts +27 -0
- package/src/derive-local-storage-bucket-name.test.ts +89 -0
- package/src/derive-local-storage-bucket-name.ts +24 -0
- package/src/docker-compose-commands.ts +100 -0
- package/src/format-doctor-report.ts +25 -0
- package/src/generate-local-infra-compose.test.ts +215 -0
- package/src/generate-local-infra-compose.ts +154 -0
- package/src/hearthkit-bin-entry.js +19 -0
- package/src/hearthkit-bin-execution.test.ts +136 -0
- package/src/hearthkit-bin.ts +8 -0
- package/src/index.ts +107 -0
- package/src/load-payments-catalog-module.ts +60 -0
- package/src/parse-cli-invocation.ts +365 -0
- package/src/read-environment-variable-value.ts +14 -0
- package/src/read-project-infra-manifest.ts +99 -0
- package/src/report-cli-outcome.ts +139 -0
- package/src/resolve-admin-database-url.ts +72 -0
- package/src/resolve-local-infra-compose-file.ts +80 -0
- package/src/run-child-process-command.ts +90 -0
- package/src/run-db-lifecycle-command.ts +103 -0
- package/src/run-dev-command.ts +104 -0
- package/src/run-dev-infra-command.ts +115 -0
- package/src/run-doctor-checks.ts +339 -0
- package/src/run-hearthkit-cli-db-commands.test.ts +330 -0
- package/src/run-hearthkit-cli-dev-infra-bucket.test.ts +254 -0
- package/src/run-hearthkit-cli-dev-infra.test.ts +616 -0
- package/src/run-hearthkit-cli-doctor.test.ts +75 -0
- package/src/run-hearthkit-cli-payments-sync.test.ts +180 -0
- package/src/run-hearthkit-cli.ts +62 -0
- package/src/run-payments-sync-command.ts +113 -0
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
import { afterAll, describe, expect, it } from 'vitest'
|
|
2
|
+
import { expectCliSuccess, runHearthkitCliGate } from '../test-fixtures/cli-run-expectations.ts'
|
|
3
|
+
import {
|
|
4
|
+
gateContainerExists,
|
|
5
|
+
gateContainerIsRunning,
|
|
6
|
+
removeGateComposeNetworks,
|
|
7
|
+
removeGateContainer,
|
|
8
|
+
} from '../test-fixtures/docker-gate-containers.ts'
|
|
9
|
+
import {
|
|
10
|
+
remapGeneratedMinioHostPorts,
|
|
11
|
+
removeGateComposeProject,
|
|
12
|
+
reserveFreeHostPort,
|
|
13
|
+
} from '../test-fixtures/gate-compose-project-runs.ts'
|
|
14
|
+
import {
|
|
15
|
+
createGateDirectory,
|
|
16
|
+
gateEnvironment,
|
|
17
|
+
removeGateDirectory,
|
|
18
|
+
uniqueGateProjectName,
|
|
19
|
+
writeGateComposeFile,
|
|
20
|
+
writeGateProjectManifest,
|
|
21
|
+
} from '../test-fixtures/gate-project-directories.ts'
|
|
22
|
+
import { loadHearthkitCliBucketExports } from '../test-fixtures/hearthkit-cli-bucket-exports.ts'
|
|
23
|
+
import { loadHearthkitCliEntry } from '../test-fixtures/hearthkit-cli-entry.ts'
|
|
24
|
+
import {
|
|
25
|
+
getMinioObjectWithSignedUrl,
|
|
26
|
+
putMinioObjectWithSignedUrl,
|
|
27
|
+
waitForMinioS3Endpoint,
|
|
28
|
+
} from '../test-fixtures/minio-s3-gate-requests.ts'
|
|
29
|
+
import {
|
|
30
|
+
generateLocalInfraComposeOptionsSchema,
|
|
31
|
+
hearthkitProjectNameSchema,
|
|
32
|
+
} from './cli-contract.ts'
|
|
33
|
+
|
|
34
|
+
/** Pulling images, waiting for a MinIO healthcheck and tearing the stack down again is far past the file-wide timeout. */
|
|
35
|
+
const dockerGateTimeoutMilliseconds = 300_000
|
|
36
|
+
|
|
37
|
+
const directoriesToRemove: string[] = []
|
|
38
|
+
const composeFilePathsToRemove: string[] = []
|
|
39
|
+
const containersToRemove: string[] = []
|
|
40
|
+
const composeProjectNamesToClean: string[] = []
|
|
41
|
+
|
|
42
|
+
/** Everything one gate needs to drive and probe its own throwaway MinIO stack. */
|
|
43
|
+
type BucketGateStack = {
|
|
44
|
+
directoryPath: string
|
|
45
|
+
composeFilePath: string
|
|
46
|
+
hearthkitProjectName: string
|
|
47
|
+
minioContainerName: string
|
|
48
|
+
bucketInitContainerName: string
|
|
49
|
+
s3HostPort: number
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Writes the real generated compose file into a throwaway project directory and hands back the
|
|
54
|
+
* names and the port a gate needs to observe it. Two things are deliberate. The file is written
|
|
55
|
+
* before the CLI runs, because resolveLocalInfraComposeFile never overwrites an existing
|
|
56
|
+
* docker-compose.yml, so `dev infra up` runs exactly these generated bytes. And the published minio
|
|
57
|
+
* ports are moved onto reserved free ports, because the contract fixes them at 9000 and 9001, which
|
|
58
|
+
* the repo's own MinIO and a developer's running stack both hold.
|
|
59
|
+
*/
|
|
60
|
+
async function prepareBucketGateStack(purpose: string): Promise<BucketGateStack> {
|
|
61
|
+
const { generateLocalInfraCompose } = await loadHearthkitCliEntry()
|
|
62
|
+
const { localStorageBucketInitServiceName } = await loadHearthkitCliBucketExports()
|
|
63
|
+
|
|
64
|
+
const directoryPath = await createGateDirectory(purpose)
|
|
65
|
+
directoriesToRemove.push(directoryPath)
|
|
66
|
+
const hearthkitProjectName = uniqueGateProjectName(purpose)
|
|
67
|
+
composeProjectNamesToClean.push(hearthkitProjectName)
|
|
68
|
+
|
|
69
|
+
const generatedComposeFileContent = generateLocalInfraCompose(
|
|
70
|
+
generateLocalInfraComposeOptionsSchema.parse({
|
|
71
|
+
hearthkitProjectName,
|
|
72
|
+
infraServices: ['minio'],
|
|
73
|
+
}),
|
|
74
|
+
)
|
|
75
|
+
const [s3HostPort, consoleHostPort] = await Promise.all([
|
|
76
|
+
reserveFreeHostPort(),
|
|
77
|
+
reserveFreeHostPort(),
|
|
78
|
+
])
|
|
79
|
+
if (s3HostPort === consoleHostPort) {
|
|
80
|
+
throw new Error('gate reserved the same host port twice for one MinIO stack')
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const composeFilePath = await writeGateComposeFile(
|
|
84
|
+
directoryPath,
|
|
85
|
+
remapGeneratedMinioHostPorts({
|
|
86
|
+
composeFileContent: generatedComposeFileContent,
|
|
87
|
+
s3HostPort,
|
|
88
|
+
consoleHostPort,
|
|
89
|
+
}),
|
|
90
|
+
)
|
|
91
|
+
composeFilePathsToRemove.push(composeFilePath)
|
|
92
|
+
// What a project that installed @hearthkit/storage really carries. The compose file is already
|
|
93
|
+
// there, so the CLI never reads it; it is written so the directory is the situation being gated.
|
|
94
|
+
await writeGateProjectManifest({
|
|
95
|
+
directoryPath,
|
|
96
|
+
manifestName: hearthkitProjectName,
|
|
97
|
+
hearthkitDependencies: ['@hearthkit/storage'],
|
|
98
|
+
})
|
|
99
|
+
|
|
100
|
+
const minioContainerName = `${hearthkitProjectName}-minio`
|
|
101
|
+
const bucketInitContainerName = `${hearthkitProjectName}-${localStorageBucketInitServiceName}`
|
|
102
|
+
containersToRemove.push(minioContainerName, bucketInitContainerName)
|
|
103
|
+
|
|
104
|
+
return {
|
|
105
|
+
directoryPath,
|
|
106
|
+
composeFilePath,
|
|
107
|
+
hearthkitProjectName,
|
|
108
|
+
minioContainerName,
|
|
109
|
+
bucketInitContainerName,
|
|
110
|
+
s3HostPort,
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
afterAll(async () => {
|
|
115
|
+
for (const composeFilePath of composeFilePathsToRemove) {
|
|
116
|
+
await removeGateComposeProject(composeFilePath)
|
|
117
|
+
}
|
|
118
|
+
for (const containerName of containersToRemove) {
|
|
119
|
+
await removeGateContainer(containerName)
|
|
120
|
+
}
|
|
121
|
+
await removeGateComposeNetworks(composeProjectNamesToClean)
|
|
122
|
+
for (const directoryPath of directoriesToRemove) {
|
|
123
|
+
await removeGateDirectory(directoryPath)
|
|
124
|
+
}
|
|
125
|
+
})
|
|
126
|
+
|
|
127
|
+
describe('hearthkit dev infra with a local storage bucket', () => {
|
|
128
|
+
it(
|
|
129
|
+
'starts the generated minio and bucket init containers with dev infra up succeeding on a fresh run and again on a repeat run',
|
|
130
|
+
async () => {
|
|
131
|
+
const stack = await prepareBucketGateStack('bucket-up')
|
|
132
|
+
|
|
133
|
+
try {
|
|
134
|
+
const freshRun = await runHearthkitCliGate({
|
|
135
|
+
argv: ['dev', 'infra', 'up'],
|
|
136
|
+
cwd: stack.directoryPath,
|
|
137
|
+
env: gateEnvironment(),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
const freshSuccess = expectCliSuccess(freshRun, 'dev-infra-up-succeeded', 0)
|
|
141
|
+
// The init container is not a LocalInfraServiceName, so compose starting it never widens
|
|
142
|
+
// the reported list.
|
|
143
|
+
expect(freshSuccess.startedInfraServices).toEqual(['minio'])
|
|
144
|
+
expect(await gateContainerIsRunning(stack.minioContainerName)).toBe(true)
|
|
145
|
+
// The container that created the bucket is still running, and it has to be. dev infra up
|
|
146
|
+
// passes --wait unconditionally, and docker compose up --wait exits 1 when any service it
|
|
147
|
+
// started has exited, whatever the exit code — so an init container that did its work and
|
|
148
|
+
// exited 0 would have made the run above report infra-compose-failed instead.
|
|
149
|
+
expect(await gateContainerIsRunning(stack.bucketInitContainerName)).toBe(true)
|
|
150
|
+
|
|
151
|
+
const repeatRun = await runHearthkitCliGate({
|
|
152
|
+
argv: ['dev', 'infra', 'up'],
|
|
153
|
+
cwd: stack.directoryPath,
|
|
154
|
+
env: gateEnvironment(),
|
|
155
|
+
})
|
|
156
|
+
|
|
157
|
+
// mc mb --ignore-existing is what makes the second run a no-op rather than a failure on a
|
|
158
|
+
// bucket that is already there.
|
|
159
|
+
const repeatSuccess = expectCliSuccess(repeatRun, 'dev-infra-up-succeeded', 0)
|
|
160
|
+
expect(repeatSuccess.startedInfraServices).toEqual(['minio'])
|
|
161
|
+
expect(await gateContainerIsRunning(stack.minioContainerName)).toBe(true)
|
|
162
|
+
expect(await gateContainerIsRunning(stack.bucketInitContainerName)).toBe(true)
|
|
163
|
+
} finally {
|
|
164
|
+
await removeGateComposeProject(stack.composeFilePath)
|
|
165
|
+
}
|
|
166
|
+
},
|
|
167
|
+
dockerGateTimeoutMilliseconds,
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
it(
|
|
171
|
+
'leaves a bucket in MinIO that a real signed S3 request can upload to and read back',
|
|
172
|
+
async () => {
|
|
173
|
+
const stack = await prepareBucketGateStack('bucket-s3')
|
|
174
|
+
const { deriveLocalStorageBucketName } = await loadHearthkitCliBucketExports()
|
|
175
|
+
const localStorageBucketName = deriveLocalStorageBucketName(
|
|
176
|
+
hearthkitProjectNameSchema.parse(stack.hearthkitProjectName),
|
|
177
|
+
)
|
|
178
|
+
|
|
179
|
+
try {
|
|
180
|
+
expectCliSuccess(
|
|
181
|
+
await runHearthkitCliGate({
|
|
182
|
+
argv: ['dev', 'infra', 'up'],
|
|
183
|
+
cwd: stack.directoryPath,
|
|
184
|
+
env: gateEnvironment(),
|
|
185
|
+
}),
|
|
186
|
+
'dev-infra-up-succeeded',
|
|
187
|
+
0,
|
|
188
|
+
)
|
|
189
|
+
await waitForMinioS3Endpoint({ s3HostPort: stack.s3HostPort })
|
|
190
|
+
|
|
191
|
+
const objectKey = 'hearthkit-gate-upload.txt'
|
|
192
|
+
const objectBody = `hearthkit gate body for ${stack.hearthkitProjectName}`
|
|
193
|
+
const upload = await putMinioObjectWithSignedUrl({
|
|
194
|
+
s3HostPort: stack.s3HostPort,
|
|
195
|
+
bucketName: localStorageBucketName,
|
|
196
|
+
objectKey,
|
|
197
|
+
objectBody,
|
|
198
|
+
})
|
|
199
|
+
|
|
200
|
+
// A signed request is the first thing that can see a missing bucket: signing never contacts
|
|
201
|
+
// the server, so a MinIO with no bucket answers the upload with 404 NoSuchBucket. Checking
|
|
202
|
+
// the bucket exists is not the same claim as checking it is usable over the S3 API.
|
|
203
|
+
expect(upload.status).toBe(200)
|
|
204
|
+
|
|
205
|
+
const download = await getMinioObjectWithSignedUrl({
|
|
206
|
+
s3HostPort: stack.s3HostPort,
|
|
207
|
+
bucketName: localStorageBucketName,
|
|
208
|
+
objectKey,
|
|
209
|
+
})
|
|
210
|
+
expect(download.status).toBe(200)
|
|
211
|
+
expect(download.body).toBe(objectBody)
|
|
212
|
+
} finally {
|
|
213
|
+
await removeGateComposeProject(stack.composeFilePath)
|
|
214
|
+
}
|
|
215
|
+
},
|
|
216
|
+
dockerGateTimeoutMilliseconds,
|
|
217
|
+
)
|
|
218
|
+
|
|
219
|
+
it(
|
|
220
|
+
'removes the still-running bucket init container along with minio when dev infra down runs',
|
|
221
|
+
async () => {
|
|
222
|
+
const stack = await prepareBucketGateStack('bucket-down')
|
|
223
|
+
|
|
224
|
+
try {
|
|
225
|
+
expectCliSuccess(
|
|
226
|
+
await runHearthkitCliGate({
|
|
227
|
+
argv: ['dev', 'infra', 'up'],
|
|
228
|
+
cwd: stack.directoryPath,
|
|
229
|
+
env: gateEnvironment(),
|
|
230
|
+
}),
|
|
231
|
+
'dev-infra-up-succeeded',
|
|
232
|
+
0,
|
|
233
|
+
)
|
|
234
|
+
expect(await gateContainerIsRunning(stack.minioContainerName)).toBe(true)
|
|
235
|
+
expect(await gateContainerIsRunning(stack.bucketInitContainerName)).toBe(true)
|
|
236
|
+
|
|
237
|
+
const downRun = await runHearthkitCliGate({
|
|
238
|
+
argv: ['dev', 'infra', 'down'],
|
|
239
|
+
cwd: stack.directoryPath,
|
|
240
|
+
env: gateEnvironment(),
|
|
241
|
+
})
|
|
242
|
+
|
|
243
|
+
// dev infra down is unchanged by the amendment: docker compose down removes every container
|
|
244
|
+
// the file defines, the idle init container included.
|
|
245
|
+
expectCliSuccess(downRun, 'dev-infra-down-succeeded', 0)
|
|
246
|
+
expect(await gateContainerExists(stack.minioContainerName)).toBe(false)
|
|
247
|
+
expect(await gateContainerExists(stack.bucketInitContainerName)).toBe(false)
|
|
248
|
+
} finally {
|
|
249
|
+
await removeGateComposeProject(stack.composeFilePath)
|
|
250
|
+
}
|
|
251
|
+
},
|
|
252
|
+
dockerGateTimeoutMilliseconds,
|
|
253
|
+
)
|
|
254
|
+
})
|