feat: add isolated read-only gray deployment
This commit is contained in:
@@ -118,3 +118,32 @@ test('image_make admin routes require an admin user and an enabled service', asy
|
||||
assert.equal(unavailable.status, 503);
|
||||
});
|
||||
});
|
||||
|
||||
test('gray read-only mode blocks admin writes while keeping reads available', async () => {
|
||||
let updateCalled = false;
|
||||
const service = {
|
||||
getAdminConfig: async () => ({ config: { defaultProvider: 'mock', providers: {} } }),
|
||||
updateAdminConfig: async () => {
|
||||
updateCalled = true;
|
||||
return { ok: true };
|
||||
},
|
||||
};
|
||||
const services = {
|
||||
...createServices({ imageMakeAdminConfigService: service }),
|
||||
readOnlyMode: true,
|
||||
};
|
||||
|
||||
await withServer(services, async (baseUrl) => {
|
||||
const readResponse = await fetch(`${baseUrl}/admin-api/image-make/config`, { headers: adminHeaders });
|
||||
assert.equal(readResponse.status, 200);
|
||||
|
||||
const writeResponse = await fetch(`${baseUrl}/admin-api/image-make/config`, {
|
||||
method: 'PATCH',
|
||||
headers: adminHeaders,
|
||||
body: JSON.stringify({ defaultProvider: 'mock' }),
|
||||
});
|
||||
assert.equal(writeResponse.status, 423);
|
||||
assert.match((await writeResponse.json()).message, /灰度只读模式/);
|
||||
assert.equal(updateCalled, false);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -113,6 +113,8 @@ export function createAdminApp(services) {
|
||||
} = services;
|
||||
const app = express();
|
||||
app.set('trust proxy', 1);
|
||||
const readOnlyMode = services.readOnlyMode
|
||||
?? /^(1|true|yes|on)$/i.test(String(process.env.ADM_READ_ONLY_MODE ?? ''));
|
||||
|
||||
const isSecureRequest = (req) =>
|
||||
req.secure || req.get('x-forwarded-proto')?.split(',')[0]?.trim() === 'https';
|
||||
@@ -150,6 +152,13 @@ export function createAdminApp(services) {
|
||||
);
|
||||
}
|
||||
|
||||
const requireWritableMode = (req, res, next) => {
|
||||
if (readOnlyMode && !['GET', 'HEAD', 'OPTIONS'].includes(req.method)) {
|
||||
return res.status(423).json({ message: '当前为灰度只读模式,禁止修改生产数据' });
|
||||
}
|
||||
next();
|
||||
};
|
||||
|
||||
app.get('/health', (_req, res) => {
|
||||
res.json({ ok: true, service: 'memind_adm' });
|
||||
});
|
||||
@@ -206,6 +215,8 @@ export function createAdminApp(services) {
|
||||
next();
|
||||
}));
|
||||
|
||||
adminApi.use(requireWritableMode);
|
||||
|
||||
const requireAdmin = (req, res, next) => {
|
||||
if (!req.currentUser || req.currentUser.role !== 'admin') {
|
||||
res.status(403).json({ message: '需要管理员权限' });
|
||||
@@ -1123,6 +1134,7 @@ export function createAdminApp(services) {
|
||||
req.currentUser = me;
|
||||
next();
|
||||
}));
|
||||
opsApi.use(requireWritableMode);
|
||||
opsApi.use(resolveSessionUser);
|
||||
opsApi.use('/ops/v1', services.createOpsApi({ jsonBody, plazaOps: services.plazaOps }));
|
||||
app.use('/api', opsApi);
|
||||
|
||||
Reference in New Issue
Block a user