Skip to content

Commit ec91ae7

Browse files
authored
feat(api): add create aspect to maintainence (#2708)
# Goals It seems we need a more selective maintainence mode. # Implementation add create aspect to maintaince to more selectively disable will need a slightly sensitive deploy step
1 parent f79a38b commit ec91ae7

7 files changed

Lines changed: 98 additions & 27 deletions

File tree

.env.tpl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ PICKUP_BASIC_AUTH_TOKEN = dGVzdDp0ZXN0
3737
PICKUP_API_URL = http://127.0.0.1:9094
3838

3939
# Maintenance Mode
40-
MAINTENANCE_MODE = rw
40+
MAINTENANCE_MODE = rwc
4141

4242
# S3
4343
S3_ENDPOINT = http://127.0.0.1:9000

packages/api/src/config.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import {
22
modes as MaintenanceModes,
33
DEFAULT_MODE,
4+
NO_READ_OR_WRITE,
5+
READ_ONLY,
6+
READ_WRITE_ONLY,
47
} from './middleware/maintenance.js'
58

69
/**
@@ -193,6 +196,17 @@ function maintenanceModeFromString(s) {
193196
return m
194197
}
195198
}
199+
/** @type {Record<string, import('./middleware/maintenance.js').Mode>} */
200+
const legacyModeMappings = {
201+
'--': NO_READ_OR_WRITE,
202+
'r-': READ_ONLY,
203+
rw: READ_WRITE_ONLY,
204+
}
205+
for (const [key, value] of Object.entries(legacyModeMappings)) {
206+
if (s === key) {
207+
return value
208+
}
209+
}
196210
throw new Error(
197211
`invalid maintenance mode value "${s}". valid choices: ${MaintenanceModes}`
198212
)

packages/api/src/index.js

Lines changed: 15 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,8 @@ import { getServiceConfig } from './config.js'
3131
import {
3232
withMode,
3333
READ_ONLY as RO,
34-
READ_WRITE as RW,
34+
READ_WRITE_ONLY as RW,
35+
READ_WRITE_CREATE as RWC,
3536
} from './middleware/maintenance.js'
3637
import { getContext } from './utils/context.js'
3738
import { withAuth } from './middleware/auth.js'
@@ -96,7 +97,7 @@ r.add(
9697
r.add(
9798
'post',
9899
'/pins',
99-
withAuth(withMode(pinsAdd, RW), {
100+
withAuth(withMode(pinsAdd, RWC), {
100101
checkHasPsaAccess,
101102
checkHasAccountRestriction,
102103
}),
@@ -105,7 +106,7 @@ r.add(
105106
r.add(
106107
'post',
107108
'/pins/:requestid',
108-
withAuth(withMode(pinsReplace, RW), {
109+
withAuth(withMode(pinsReplace, RWC), {
109110
checkHasPsaAccess,
110111
checkHasAccountRestriction,
111112
}),
@@ -114,7 +115,7 @@ r.add(
114115
r.add(
115116
'delete',
116117
'/pins/:requestid',
117-
withAuth(withMode(pinsDelete, RW), {
118+
withAuth(withMode(pinsDelete, RWC), {
118119
checkHasDeleteRestriction,
119120
checkHasPsaAccess,
120121
}),
@@ -128,7 +129,7 @@ r.add('get', '/:cid', withAuth(withMode(nftGet, RO)), [postCors])
128129
r.add(
129130
'post',
130131
'/upload',
131-
withAuth(withMode(nftUpload, RW), {
132+
withAuth(withMode(nftUpload, RWC), {
132133
checkHasAccountRestriction,
133134
checkUcan,
134135
}),
@@ -137,24 +138,24 @@ r.add(
137138
r.add(
138139
'patch',
139140
'/upload/:cid',
140-
withAuth(withMode(nftUpdateUpload, RW), { checkHasAccountRestriction }),
141+
withAuth(withMode(nftUpdateUpload, RWC), { checkHasAccountRestriction }),
141142
[postCors]
142143
)
143144
r.add(
144145
'post',
145146
'/store',
146-
withAuth(withMode(nftStore, RW), { checkHasAccountRestriction }),
147+
withAuth(withMode(nftStore, RWC), { checkHasAccountRestriction }),
147148
[postCors]
148149
)
149150
r.add(
150151
'delete',
151152
'/:cid',
152-
withAuth(withMode(nftDelete, RW), { checkHasDeleteRestriction }),
153+
withAuth(withMode(nftDelete, RWC), { checkHasDeleteRestriction }),
153154
[postCors]
154155
)
155156

156157
// Temporary Metaplex upload route, mapped to metaplex user account.
157-
r.add('post', '/metaplex/upload', withMode(metaplexUpload, RW), [postCors])
158+
r.add('post', '/metaplex/upload', withMode(metaplexUpload, RWC), [postCors])
158159

159160
// User
160161
r.add(
@@ -206,7 +207,7 @@ r.add(
206207
r.add(
207208
'post',
208209
'/api/pins',
209-
withAuth(withMode(pinsAdd, RW), {
210+
withAuth(withMode(pinsAdd, RWC), {
210211
checkHasPsaAccess,
211212
checkHasAccountRestriction,
212213
}),
@@ -215,7 +216,7 @@ r.add(
215216
r.add(
216217
'post',
217218
'/api/pins/:requestid',
218-
withAuth(withMode(pinsReplace, RW), {
219+
withAuth(withMode(pinsReplace, RWC), {
219220
checkHasPsaAccess,
220221
checkHasAccountRestriction,
221222
}),
@@ -224,7 +225,7 @@ r.add(
224225
r.add(
225226
'delete',
226227
'/api/pins/:requestid',
227-
withAuth(withMode(pinsDelete, RW), {
228+
withAuth(withMode(pinsDelete, RWC), {
228229
checkHasDeleteRestriction,
229230
checkHasPsaAccess,
230231
}),
@@ -238,13 +239,13 @@ r.add('get', '/api/:cid', withAuth(withMode(nftGet, RO)), [postCors])
238239
r.add(
239240
'post',
240241
'/api/upload',
241-
withAuth(withMode(nftUpload, RW), { checkUcan, checkHasAccountRestriction }),
242+
withAuth(withMode(nftUpload, RWC), { checkUcan, checkHasAccountRestriction }),
242243
[postCors]
243244
)
244245
r.add(
245246
'delete',
246247
'/api/:cid',
247-
withAuth(withMode(nftDelete, RW), { checkHasDeleteRestriction }),
248+
withAuth(withMode(nftDelete, RWC), { checkHasDeleteRestriction }),
248249
[postCors]
249250
)
250251

packages/api/src/middleware/maintenance.js

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,32 +2,42 @@ import { ErrorMaintenance, HTTPError } from '../errors.js'
22
import { getServiceConfig } from '../config.js'
33

44
/**
5-
* @typedef {'rw' | 'r-' | '--'} Mode
5+
* @typedef {'rwc' | 'rw-' | 'r--' | '---'} Mode
66
* @typedef {import('../bindings').Handler} Handler
77
*/
88

9+
/**
10+
* Read and write and create
11+
*/
12+
export const READ_WRITE_CREATE = 'rwc'
13+
914
/**
1015
* Read and write.
1116
*/
12-
export const READ_WRITE = 'rw'
17+
export const READ_WRITE_ONLY = 'rw-'
1318

1419
/**
1520
* Read only mode.
1621
*/
17-
export const READ_ONLY = 'r-'
22+
export const READ_ONLY = 'r--'
1823

1924
/**
2025
* No reading or writing.
2126
*/
22-
export const NO_READ_OR_WRITE = '--'
27+
export const NO_READ_OR_WRITE = '---'
2328

2429
/** @type {readonly Mode[]} */
25-
export const modes = Object.freeze([NO_READ_OR_WRITE, READ_ONLY, READ_WRITE])
30+
export const modes = Object.freeze([
31+
NO_READ_OR_WRITE,
32+
READ_ONLY,
33+
READ_WRITE_ONLY,
34+
READ_WRITE_CREATE,
35+
])
2636

2737
/**
2838
* The default maintenance mode (normal operation).
2939
*/
30-
export const DEFAULT_MODE = READ_WRITE
40+
export const DEFAULT_MODE = READ_WRITE_CREATE
3141

3242
/** @type {() => Mode} */
3343
let getMaintenanceMode = () => getServiceConfig().MAINTENANCE_MODE

packages/api/test/config.spec.js

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ const BASE_CONFIG = {
2626
DATABASE_TOKEN:
2727
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYwMzk2ODgzNCwiZXhwIjoyNTUwNjUzNjM0LCJyb2xlIjoic2VydmljZV9yb2xlIn0.necIJaiP7X2T2QjGeV-FhpkizcNTX8HjDDBAxpgQTEI',
2828
DATABASE_CONNECTION: 'postgresql://postgres:postgres@localhost:5432/postgres',
29-
MAINTENANCE_MODE: 'rw',
29+
MAINTENANCE_MODE: 'rwc',
3030
S3_REGION: 'us-east-1',
3131
S3_ACCESS_KEY_ID: 'minioadmin',
3232
S3_SECRET_ACCESS_KEY: 'minioadmin',
@@ -145,7 +145,7 @@ test.serial(
145145
test.serial(
146146
'serviceConfigFromVariables sets MAINTENANCE_MODE if it contains a valid mode string',
147147
(t) => {
148-
const modes = ['--', 'r-', 'rw']
148+
const modes = ['---', 'r--', 'rw-', 'rwc']
149149
for (const m of modes) {
150150
t.is(
151151
serviceConfigFromVariables(
@@ -159,6 +159,23 @@ test.serial(
159159
}
160160
)
161161

162+
test.serial(
163+
'serviceConfigFromVariables sets MAINTENANCE_MODE if it contains a valid legacy mode string',
164+
(t) => {
165+
const modes = ['--', 'r-', 'rw']
166+
for (const m of modes) {
167+
t.is(
168+
serviceConfigFromVariables(
169+
override({
170+
MAINTENANCE_MODE: m,
171+
})
172+
).MAINTENANCE_MODE.toString(),
173+
m + '-'
174+
)
175+
}
176+
}
177+
)
178+
162179
test.serial(
163180
'serviceConfigFromVariables uses unaltered values for string config variables',
164181
(t) => {

packages/api/test/maintenance.spec.js

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import test from 'ava'
22
import {
33
READ_ONLY,
4-
READ_WRITE,
4+
READ_WRITE_ONLY,
55
NO_READ_OR_WRITE,
6+
READ_WRITE_CREATE,
67
} from '../src/middleware/maintenance.js'
78
import { createClientWithUser } from './scripts/helpers.js'
89

@@ -46,25 +47,33 @@ test('maintenance middleware should throw error when in maintenance mode', async
4647

4748
const expectedError = { message: /API undergoing maintenance/ }
4849

49-
await setMode(t, READ_WRITE)
50+
await setMode(t, READ_WRITE_CREATE)
5051
await t.notThrowsAsync(tryRead(t, token))
5152
await t.notThrowsAsync(tryWrite(t, token))
53+
await t.notThrowsAsync(tryCreate(t, token))
54+
55+
await setMode(t, READ_WRITE_ONLY)
56+
await t.notThrowsAsync(tryRead(t, token))
57+
await t.notThrowsAsync(tryWrite(t, token))
58+
await t.throwsAsync(tryCreate(t, token), expectedError)
5259

5360
await setMode(t, READ_ONLY)
5461
await t.notThrowsAsync(tryRead(t, token))
5562
await t.throwsAsync(tryWrite(t, token), expectedError)
63+
await t.throwsAsync(tryCreate(t, token), expectedError)
5664

5765
await setMode(t, NO_READ_OR_WRITE)
5866
await t.throwsAsync(tryRead(t, token), expectedError)
5967
await t.throwsAsync(tryWrite(t, token), expectedError)
68+
await t.throwsAsync(tryCreate(t, token), expectedError)
6069
})
6170

6271
/**
6372
*
6473
* @param {import('ava').ExecutionContext} t
6574
* @param {string} token
6675
*/
67-
async function tryWrite(t, token) {
76+
async function tryCreate(t, token) {
6877
const mf = getMiniflareContext(t)
6978
const res = await mf.dispatchFetch('http://miniflare.test/upload', {
7079
headers: { authorization: `Bearer ${token}` },
@@ -77,6 +86,26 @@ async function tryWrite(t, token) {
7786
}
7887
}
7988

89+
/**
90+
*
91+
* @param {import('ava').ExecutionContext} t
92+
* @param {string} token
93+
*/
94+
async function tryWrite(t, token) {
95+
const mf = getMiniflareContext(t)
96+
const res = await mf.dispatchFetch('http://miniflare.test/internal/tokens', {
97+
headers: {
98+
authorization: `Bearer ${token}`,
99+
},
100+
method: 'POST',
101+
body: JSON.stringify({ name: `new key ${new Date().toISOString()}` }),
102+
})
103+
if (!res.ok) {
104+
const { error } = await res.json()
105+
throw new Error(error.message)
106+
}
107+
}
108+
80109
/**
81110
*
82111
* @param {import('ava').ExecutionContext} t

packages/api/test/scripts/globals.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ globalThis.PICKUP_API_URL = 'http://127.0.0.1:9094'
1717
// will be used with we can active auth in cluster base64 of test:test
1818
globalThis.PICKUP_BASIC_AUTH_TOKEN = 'dGVzdDp0ZXN0'
1919

20-
globalThis.MAINTENANCE_MODE = 'rw'
20+
globalThis.MAINTENANCE_MODE = 'rwc'
2121

2222
globalThis.S3_ENDPOINT = 'http://127.0.0.1:9000'
2323
globalThis.S3_REGION = 'us-east-1'

0 commit comments

Comments
 (0)