Skip to content

Commit

Permalink
feat(api): add create aspect to maintainence (#2708)
Browse files Browse the repository at this point in the history
# 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
  • Loading branch information
hannahhoward authored Jul 2, 2024
1 parent f79a38b commit ec91ae7
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 27 deletions.
2 changes: 1 addition & 1 deletion .env.tpl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ PICKUP_BASIC_AUTH_TOKEN = dGVzdDp0ZXN0
PICKUP_API_URL = http://127.0.0.1:9094

# Maintenance Mode
MAINTENANCE_MODE = rw
MAINTENANCE_MODE = rwc

# S3
S3_ENDPOINT = http://127.0.0.1:9000
Expand Down
14 changes: 14 additions & 0 deletions packages/api/src/config.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import {
modes as MaintenanceModes,
DEFAULT_MODE,
NO_READ_OR_WRITE,
READ_ONLY,
READ_WRITE_ONLY,
} from './middleware/maintenance.js'

/**
Expand Down Expand Up @@ -193,6 +196,17 @@ function maintenanceModeFromString(s) {
return m
}
}
/** @type {Record<string, import('./middleware/maintenance.js').Mode>} */
const legacyModeMappings = {
'--': NO_READ_OR_WRITE,
'r-': READ_ONLY,
rw: READ_WRITE_ONLY,
}
for (const [key, value] of Object.entries(legacyModeMappings)) {
if (s === key) {
return value
}
}
throw new Error(
`invalid maintenance mode value "${s}". valid choices: ${MaintenanceModes}`
)
Expand Down
29 changes: 15 additions & 14 deletions packages/api/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@ import { getServiceConfig } from './config.js'
import {
withMode,
READ_ONLY as RO,
READ_WRITE as RW,
READ_WRITE_ONLY as RW,
READ_WRITE_CREATE as RWC,
} from './middleware/maintenance.js'
import { getContext } from './utils/context.js'
import { withAuth } from './middleware/auth.js'
Expand Down Expand Up @@ -96,7 +97,7 @@ r.add(
r.add(
'post',
'/pins',
withAuth(withMode(pinsAdd, RW), {
withAuth(withMode(pinsAdd, RWC), {
checkHasPsaAccess,
checkHasAccountRestriction,
}),
Expand All @@ -105,7 +106,7 @@ r.add(
r.add(
'post',
'/pins/:requestid',
withAuth(withMode(pinsReplace, RW), {
withAuth(withMode(pinsReplace, RWC), {
checkHasPsaAccess,
checkHasAccountRestriction,
}),
Expand All @@ -114,7 +115,7 @@ r.add(
r.add(
'delete',
'/pins/:requestid',
withAuth(withMode(pinsDelete, RW), {
withAuth(withMode(pinsDelete, RWC), {
checkHasDeleteRestriction,
checkHasPsaAccess,
}),
Expand All @@ -128,7 +129,7 @@ r.add('get', '/:cid', withAuth(withMode(nftGet, RO)), [postCors])
r.add(
'post',
'/upload',
withAuth(withMode(nftUpload, RW), {
withAuth(withMode(nftUpload, RWC), {
checkHasAccountRestriction,
checkUcan,
}),
Expand All @@ -137,24 +138,24 @@ r.add(
r.add(
'patch',
'/upload/:cid',
withAuth(withMode(nftUpdateUpload, RW), { checkHasAccountRestriction }),
withAuth(withMode(nftUpdateUpload, RWC), { checkHasAccountRestriction }),
[postCors]
)
r.add(
'post',
'/store',
withAuth(withMode(nftStore, RW), { checkHasAccountRestriction }),
withAuth(withMode(nftStore, RWC), { checkHasAccountRestriction }),
[postCors]
)
r.add(
'delete',
'/:cid',
withAuth(withMode(nftDelete, RW), { checkHasDeleteRestriction }),
withAuth(withMode(nftDelete, RWC), { checkHasDeleteRestriction }),
[postCors]
)

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

// User
r.add(
Expand Down Expand Up @@ -206,7 +207,7 @@ r.add(
r.add(
'post',
'/api/pins',
withAuth(withMode(pinsAdd, RW), {
withAuth(withMode(pinsAdd, RWC), {
checkHasPsaAccess,
checkHasAccountRestriction,
}),
Expand All @@ -215,7 +216,7 @@ r.add(
r.add(
'post',
'/api/pins/:requestid',
withAuth(withMode(pinsReplace, RW), {
withAuth(withMode(pinsReplace, RWC), {
checkHasPsaAccess,
checkHasAccountRestriction,
}),
Expand All @@ -224,7 +225,7 @@ r.add(
r.add(
'delete',
'/api/pins/:requestid',
withAuth(withMode(pinsDelete, RW), {
withAuth(withMode(pinsDelete, RWC), {
checkHasDeleteRestriction,
checkHasPsaAccess,
}),
Expand All @@ -238,13 +239,13 @@ r.add('get', '/api/:cid', withAuth(withMode(nftGet, RO)), [postCors])
r.add(
'post',
'/api/upload',
withAuth(withMode(nftUpload, RW), { checkUcan, checkHasAccountRestriction }),
withAuth(withMode(nftUpload, RWC), { checkUcan, checkHasAccountRestriction }),
[postCors]
)
r.add(
'delete',
'/api/:cid',
withAuth(withMode(nftDelete, RW), { checkHasDeleteRestriction }),
withAuth(withMode(nftDelete, RWC), { checkHasDeleteRestriction }),
[postCors]
)

Expand Down
22 changes: 16 additions & 6 deletions packages/api/src/middleware/maintenance.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,42 @@ import { ErrorMaintenance, HTTPError } from '../errors.js'
import { getServiceConfig } from '../config.js'

/**
* @typedef {'rw' | 'r-' | '--'} Mode
* @typedef {'rwc' | 'rw-' | 'r--' | '---'} Mode
* @typedef {import('../bindings').Handler} Handler
*/

/**
* Read and write and create
*/
export const READ_WRITE_CREATE = 'rwc'

/**
* Read and write.
*/
export const READ_WRITE = 'rw'
export const READ_WRITE_ONLY = 'rw-'

/**
* Read only mode.
*/
export const READ_ONLY = 'r-'
export const READ_ONLY = 'r--'

/**
* No reading or writing.
*/
export const NO_READ_OR_WRITE = '--'
export const NO_READ_OR_WRITE = '---'

/** @type {readonly Mode[]} */
export const modes = Object.freeze([NO_READ_OR_WRITE, READ_ONLY, READ_WRITE])
export const modes = Object.freeze([
NO_READ_OR_WRITE,
READ_ONLY,
READ_WRITE_ONLY,
READ_WRITE_CREATE,
])

/**
* The default maintenance mode (normal operation).
*/
export const DEFAULT_MODE = READ_WRITE
export const DEFAULT_MODE = READ_WRITE_CREATE

/** @type {() => Mode} */
let getMaintenanceMode = () => getServiceConfig().MAINTENANCE_MODE
Expand Down
21 changes: 19 additions & 2 deletions packages/api/test/config.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ const BASE_CONFIG = {
DATABASE_TOKEN:
'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJzdXBhYmFzZSIsImlhdCI6MTYwMzk2ODgzNCwiZXhwIjoyNTUwNjUzNjM0LCJyb2xlIjoic2VydmljZV9yb2xlIn0.necIJaiP7X2T2QjGeV-FhpkizcNTX8HjDDBAxpgQTEI',
DATABASE_CONNECTION: 'postgresql://postgres:postgres@localhost:5432/postgres',
MAINTENANCE_MODE: 'rw',
MAINTENANCE_MODE: 'rwc',
S3_REGION: 'us-east-1',
S3_ACCESS_KEY_ID: 'minioadmin',
S3_SECRET_ACCESS_KEY: 'minioadmin',
Expand Down Expand Up @@ -145,7 +145,7 @@ test.serial(
test.serial(
'serviceConfigFromVariables sets MAINTENANCE_MODE if it contains a valid mode string',
(t) => {
const modes = ['--', 'r-', 'rw']
const modes = ['---', 'r--', 'rw-', 'rwc']
for (const m of modes) {
t.is(
serviceConfigFromVariables(
Expand All @@ -159,6 +159,23 @@ test.serial(
}
)

test.serial(
'serviceConfigFromVariables sets MAINTENANCE_MODE if it contains a valid legacy mode string',
(t) => {
const modes = ['--', 'r-', 'rw']
for (const m of modes) {
t.is(
serviceConfigFromVariables(
override({
MAINTENANCE_MODE: m,
})
).MAINTENANCE_MODE.toString(),
m + '-'
)
}
}
)

test.serial(
'serviceConfigFromVariables uses unaltered values for string config variables',
(t) => {
Expand Down
35 changes: 32 additions & 3 deletions packages/api/test/maintenance.spec.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import test from 'ava'
import {
READ_ONLY,
READ_WRITE,
READ_WRITE_ONLY,
NO_READ_OR_WRITE,
READ_WRITE_CREATE,
} from '../src/middleware/maintenance.js'
import { createClientWithUser } from './scripts/helpers.js'

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

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

await setMode(t, READ_WRITE)
await setMode(t, READ_WRITE_CREATE)
await t.notThrowsAsync(tryRead(t, token))
await t.notThrowsAsync(tryWrite(t, token))
await t.notThrowsAsync(tryCreate(t, token))

await setMode(t, READ_WRITE_ONLY)
await t.notThrowsAsync(tryRead(t, token))
await t.notThrowsAsync(tryWrite(t, token))
await t.throwsAsync(tryCreate(t, token), expectedError)

await setMode(t, READ_ONLY)
await t.notThrowsAsync(tryRead(t, token))
await t.throwsAsync(tryWrite(t, token), expectedError)
await t.throwsAsync(tryCreate(t, token), expectedError)

await setMode(t, NO_READ_OR_WRITE)
await t.throwsAsync(tryRead(t, token), expectedError)
await t.throwsAsync(tryWrite(t, token), expectedError)
await t.throwsAsync(tryCreate(t, token), expectedError)
})

/**
*
* @param {import('ava').ExecutionContext} t
* @param {string} token
*/
async function tryWrite(t, token) {
async function tryCreate(t, token) {
const mf = getMiniflareContext(t)
const res = await mf.dispatchFetch('http://miniflare.test/upload', {
headers: { authorization: `Bearer ${token}` },
Expand All @@ -77,6 +86,26 @@ async function tryWrite(t, token) {
}
}

/**
*
* @param {import('ava').ExecutionContext} t
* @param {string} token
*/
async function tryWrite(t, token) {
const mf = getMiniflareContext(t)
const res = await mf.dispatchFetch('http://miniflare.test/internal/tokens', {
headers: {
authorization: `Bearer ${token}`,
},
method: 'POST',
body: JSON.stringify({ name: `new key ${new Date().toISOString()}` }),
})
if (!res.ok) {
const { error } = await res.json()
throw new Error(error.message)
}
}

/**
*
* @param {import('ava').ExecutionContext} t
Expand Down
2 changes: 1 addition & 1 deletion packages/api/test/scripts/globals.js
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ globalThis.PICKUP_API_URL = 'http://127.0.0.1:9094'
// will be used with we can active auth in cluster base64 of test:test
globalThis.PICKUP_BASIC_AUTH_TOKEN = 'dGVzdDp0ZXN0'

globalThis.MAINTENANCE_MODE = 'rw'
globalThis.MAINTENANCE_MODE = 'rwc'

globalThis.S3_ENDPOINT = 'http://127.0.0.1:9000'
globalThis.S3_REGION = 'us-east-1'
Expand Down

0 comments on commit ec91ae7

Please sign in to comment.