Skip to content

Commit d5dbe18

Browse files
fix(deploy): fix certificates not getting written and add tests
Closes #127 Co-Authored-By: Pisut Sritrakulchai <pisut.sritrakulchai@nordicsemi.no>
1 parent 6cfe346 commit d5dbe18

10 files changed

+296
-51
lines changed

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,5 @@ npm-debug.log
88
cdk.out/
99
dist/
1010
.envrc
11-
certificates/
11+
/certificates/
1212
cdk.context.json

cdk/backend.ts

Lines changed: 14 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,11 @@ import { packLayer } from './helpers/lambdas/packLayer.js'
1818
import { packBackendLambdas } from './packBackendLambdas.js'
1919
import { ECR_NAME, STACK_NAME } from './stacks/stackConfig.js'
2020
import { mkdir } from 'node:fs/promises'
21-
import { Scope, getSettingsOptional, putSettings } from '../util/settings.js'
22-
import { readFilesFromMap } from './helpers/readFilesFromMap.js'
23-
import { writeFilesFromMap } from './helpers/writeFilesFromMap.js'
21+
import { Scope } from '../util/settings.js'
2422
import { mqttBridgeCertificateLocation } from '../bridge/mqttBridgeCertificateLocation.js'
2523
import { caLocation } from '../bridge/caLocation.js'
24+
import { restoreCertificateFromSSM } from './helpers/certificates/restoreCertificateFromSSM.js'
25+
import { storeCertificateInSSM } from './helpers/certificates/storeCertificateInSSM.js'
2626

2727
const repoUrl = new URL(pJSON.repository.url)
2828
const repository = {
@@ -80,44 +80,14 @@ const certificates = [
8080
] as [Scope, Record<string, string>, logFn][]
8181

8282
// Restore message bridge certificates from SSM
83-
const restoredCertificates = await Promise.all<boolean>(
84-
certificates.map(async ([scope, certsMap, debug]) => {
85-
debug(`Getting settings`, scope)
86-
const settings = await getSettingsOptional<Record<string, string>, null>({
87-
ssm,
88-
stackName: STACK_NAME,
83+
const restoredCertificates = await Promise.all(
84+
certificates.map(async ([scope, certsMap, debug]) =>
85+
restoreCertificateFromSSM({ ssm, stackName: STACK_NAME })(
8986
scope,
90-
})(null)
91-
if (settings === null) {
92-
debug(`No certificate in settings.`)
93-
return false
94-
}
95-
96-
debug(`Restoring`)
97-
const locations: Record<string, string> = Object.entries(settings).reduce(
98-
(locations, [k, v]) => {
99-
const path = certsMap[k]
100-
debug(`Unrecognized path:`, k)
101-
if (path === undefined) return locations
102-
return {
103-
...locations,
104-
[path]: v,
105-
}
106-
},
107-
{},
108-
)
109-
// Make sure all required locations exist
110-
111-
for (const k of Object.keys(certsMap)) {
112-
if (locations[k] === undefined) {
113-
debug(`Restored certificate settings are missing key`, k)
114-
return false
115-
}
116-
}
117-
for (const k of Object.keys(locations)) debug(`Restoring:`, k)
118-
await writeFilesFromMap(settings)
119-
return true
120-
}),
87+
certsMap,
88+
debug,
89+
),
90+
),
12191
)
12292

12393
// Pick up existing, or create new certificates
@@ -139,16 +109,10 @@ await Promise.all(
139109
debug(`Certificate was restored. Nothing to store.`)
140110
return
141111
}
142-
for (const k of Object.keys(certsMap)) debug(`Storing:`, k)
143-
Object.entries(readFilesFromMap(certsMap)).map(async ([k, v]) =>
144-
putSettings({
145-
ssm,
146-
stackName: STACK_NAME,
147-
scope,
148-
})({
149-
property: k,
150-
value: v,
151-
}),
112+
await storeCertificateInSSM({ ssm, stackName: STACK_NAME })(
113+
scope,
114+
certsMap,
115+
debug,
152116
)
153117
}),
154118
)
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
import {
2+
ParameterType,
3+
type GetParametersByPathCommandOutput,
4+
type SSMClient,
5+
} from '@aws-sdk/client-ssm'
6+
import { restoreCertificateFromSSM } from './restoreCertificateFromSSM.js'
7+
import { Scope } from '../../../util/settings.js'
8+
import { caLocation } from '../../../bridge/caLocation.js'
9+
import path from 'node:path'
10+
import os from 'node:os'
11+
import fs from 'node:fs/promises'
12+
import { readFilesFromMap } from './readFilesFromMap.js'
13+
14+
describe('restoreCertificateFromSSM()', () => {
15+
it('should query SSM for stored certificates, but not restored if value is not present', async () => {
16+
const result: GetParametersByPathCommandOutput = {
17+
Parameters: undefined,
18+
$metadata: {},
19+
}
20+
const send = jest.fn(async () => Promise.resolve(result))
21+
const ssm: SSMClient = {
22+
send,
23+
} as any
24+
25+
const tempDir = await fs.mkdtemp(
26+
path.join(os.tmpdir(), 'restoreCertificateFromSSM-'),
27+
)
28+
29+
const res = await restoreCertificateFromSSM({
30+
ssm,
31+
stackName: 'hello-nrfcloud',
32+
})(
33+
Scope.NRFCLOUD_BRIDGE_CERTIFICATE_MQTT, // 'nRFCloudBridgeCertificate/MQTT'
34+
caLocation({
35+
certsDir: tempDir,
36+
}),
37+
)
38+
39+
expect(res).toEqual(false)
40+
41+
expect(send).toHaveBeenLastCalledWith(
42+
expect.objectContaining({
43+
input: {
44+
Path: `/hello-nrfcloud/nRFCloudBridgeCertificate/MQTT`,
45+
Recursive: true,
46+
},
47+
}),
48+
)
49+
})
50+
51+
it('should not restore if value is incomplete', async () => {
52+
const result: GetParametersByPathCommandOutput = {
53+
Parameters: [
54+
{
55+
Type: ParameterType.STRING,
56+
Name: 'invalidName',
57+
Value: 'invalidValue',
58+
},
59+
],
60+
$metadata: {},
61+
}
62+
const send = jest.fn(async () => Promise.resolve(result))
63+
const ssm: SSMClient = {
64+
send,
65+
} as any
66+
67+
const tempDir = await fs.mkdtemp(
68+
path.join(os.tmpdir(), 'restoreCertificateFromSSM-'),
69+
)
70+
71+
const res = await restoreCertificateFromSSM({
72+
ssm,
73+
stackName: 'hello-nrfcloud',
74+
})(
75+
Scope.NRFCLOUD_BRIDGE_CERTIFICATE_MQTT, // 'nRFCloudBridgeCertificate/MQTT'
76+
caLocation({
77+
certsDir: tempDir,
78+
}),
79+
)
80+
81+
expect(res).toEqual(false)
82+
83+
expect(send).toHaveBeenLastCalledWith(
84+
expect.objectContaining({
85+
input: {
86+
Path: `/hello-nrfcloud/nRFCloudBridgeCertificate/MQTT`,
87+
Recursive: true,
88+
},
89+
}),
90+
)
91+
})
92+
93+
it('should restore if value in SSM is complete', async () => {
94+
const result: GetParametersByPathCommandOutput = {
95+
Parameters: ['key', 'cert', 'verificationCert'].map((Name) => ({
96+
Type: ParameterType.STRING,
97+
Name,
98+
Value: `Content of ${Name}`,
99+
})),
100+
$metadata: {},
101+
}
102+
const send = jest.fn(async () => Promise.resolve(result))
103+
const ssm: SSMClient = {
104+
send,
105+
} as any
106+
107+
const tempDir = await fs.mkdtemp(
108+
path.join(os.tmpdir(), 'restoreCertificateFromSSM-'),
109+
)
110+
111+
const certsMap = caLocation({
112+
certsDir: tempDir,
113+
})
114+
const res = await restoreCertificateFromSSM({
115+
ssm,
116+
stackName: 'hello-nrfcloud',
117+
})(
118+
Scope.NRFCLOUD_BRIDGE_CERTIFICATE_MQTT, // 'nRFCloudBridgeCertificate/MQTT'
119+
certsMap,
120+
)
121+
122+
expect(res).toEqual(true)
123+
124+
expect(send).toHaveBeenLastCalledWith(
125+
expect.objectContaining({
126+
input: {
127+
Path: `/hello-nrfcloud/nRFCloudBridgeCertificate/MQTT`,
128+
Recursive: true,
129+
},
130+
}),
131+
)
132+
133+
expect(await readFilesFromMap(certsMap)).toMatchObject({
134+
key: `Content of key`,
135+
cert: `Content of cert`,
136+
verificationCert: `Content of verificationCert`,
137+
})
138+
})
139+
})
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
import { SSMClient } from '@aws-sdk/client-ssm'
2+
import { type logFn } from '../../../cli/log.js'
3+
import { Scope, getSettingsOptional } from '../../../util/settings.js'
4+
import { writeFilesFromMap } from './writeFilesFromMap.js'
5+
6+
export const restoreCertificateFromSSM =
7+
({ ssm, stackName }: { ssm: SSMClient; stackName: string }) =>
8+
async (
9+
scope: Scope,
10+
certificateLocations: Record<string, string>,
11+
debug?: logFn,
12+
): Promise<boolean> => {
13+
debug?.(`Getting settings`, scope)
14+
const settings = await getSettingsOptional<Record<string, string>, null>({
15+
ssm,
16+
stackName,
17+
scope,
18+
})(null)
19+
if (settings === null) {
20+
debug?.(`No certificate stored in settings.`)
21+
return false
22+
}
23+
24+
// Make sure all required locations exist
25+
for (const k of Object.keys(certificateLocations)) {
26+
if (settings[k] === undefined) {
27+
debug?.(`Restored certificate settings are missing key`, k)
28+
return false
29+
}
30+
}
31+
32+
const locations: Record<string, string> = Object.entries(settings).reduce(
33+
(locations, [k, v]) => {
34+
const path = certificateLocations[k]
35+
if (path === undefined) {
36+
debug?.(`Unrecognized path:`, k)
37+
return locations
38+
}
39+
return {
40+
...locations,
41+
[path]: v,
42+
}
43+
},
44+
{},
45+
)
46+
47+
for (const path of Object.keys(locations)) debug?.(`Restoring`, path)
48+
await writeFilesFromMap(locations)
49+
return true
50+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { ParameterType, type SSMClient } from '@aws-sdk/client-ssm'
2+
import { Scope } from '../../../util/settings.js'
3+
import { caLocation } from '../../../bridge/caLocation.js'
4+
import path from 'node:path'
5+
import os from 'node:os'
6+
import fs from 'node:fs/promises'
7+
import { writeFilesFromMap } from './writeFilesFromMap.js'
8+
import { storeCertificateInSSM } from './storeCertificateInSSM.js'
9+
10+
describe('storeCertificateInSSM()', () => {
11+
it('should store a certificate map in SSM', async () => {
12+
const send = jest.fn(async () => Promise.resolve())
13+
const ssm: SSMClient = {
14+
send,
15+
} as any
16+
const certsDir = await fs.mkdtemp(
17+
path.join(os.tmpdir(), 'restoreCertificateFromSSM-'),
18+
)
19+
const certsMap = caLocation({
20+
certsDir,
21+
})
22+
await writeFilesFromMap({
23+
[path.join(certsDir, 'CA.key')]: 'Contents of CA.key',
24+
[path.join(certsDir, 'CA.cert')]: 'Contents of CA.cert',
25+
[path.join(certsDir, 'CA.verification.cert')]:
26+
'Contents of CA.verification.cert',
27+
})
28+
29+
await storeCertificateInSSM({ ssm, stackName: 'hello-nrfcloud' })(
30+
Scope.NRFCLOUD_BRIDGE_CERTIFICATE_MQTT, // 'nRFCloudBridgeCertificate/MQTT',
31+
certsMap,
32+
)
33+
34+
expect(send).toHaveBeenCalledWith(
35+
expect.objectContaining({
36+
input: {
37+
Name: `/hello-nrfcloud/nRFCloudBridgeCertificate/MQTT/cert`,
38+
Type: ParameterType.STRING,
39+
Value: 'Contents of CA.cert',
40+
Overwrite: true,
41+
},
42+
}),
43+
)
44+
45+
expect(send).toHaveBeenCalledWith(
46+
expect.objectContaining({
47+
input: {
48+
Name: `/hello-nrfcloud/nRFCloudBridgeCertificate/MQTT/key`,
49+
Type: ParameterType.STRING,
50+
Value: 'Contents of CA.key',
51+
Overwrite: true,
52+
},
53+
}),
54+
)
55+
56+
expect(send).toHaveBeenCalledWith(
57+
expect.objectContaining({
58+
input: {
59+
Name: `/hello-nrfcloud/nRFCloudBridgeCertificate/MQTT/verificationCert`,
60+
Type: ParameterType.STRING,
61+
Value: 'Contents of CA.verification.cert',
62+
Overwrite: true,
63+
},
64+
}),
65+
)
66+
})
67+
})
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { SSMClient } from '@aws-sdk/client-ssm'
2+
import { type logFn } from '../../../cli/log.js'
3+
import { Scope, putSettings } from '../../../util/settings.js'
4+
import { readFilesFromMap } from './readFilesFromMap.js'
5+
6+
export const storeCertificateInSSM =
7+
({ ssm, stackName }: { ssm: SSMClient; stackName: string }) =>
8+
async (
9+
scope: Scope,
10+
certsMap: Record<string, string>,
11+
debug?: logFn,
12+
): Promise<void> => {
13+
const certContents = await readFilesFromMap(certsMap)
14+
for (const [k, content] of Object.entries(certContents)) {
15+
debug?.(`Storing certificate in settings:`, k)
16+
await putSettings({
17+
ssm,
18+
stackName,
19+
scope,
20+
})({
21+
property: k,
22+
value: content,
23+
})
24+
}
25+
}

0 commit comments

Comments
 (0)