mirror of
https://github.com/Unleash/unleash.git
synced 2025-06-04 01:18:20 +02:00
Maintenance mode for users (#2716)
This commit is contained in:
parent
1ef84da688
commit
a0619e963d
@ -18,6 +18,7 @@
|
|||||||
"start:enterprise": "UNLEASH_API=https://unleash4.herokuapp.com yarn run start",
|
"start:enterprise": "UNLEASH_API=https://unleash4.herokuapp.com yarn run start",
|
||||||
"start:demo": "UNLEASH_BASE_PATH=/demo/ yarn start",
|
"start:demo": "UNLEASH_BASE_PATH=/demo/ yarn start",
|
||||||
"test": "tsc && vitest run",
|
"test": "tsc && vitest run",
|
||||||
|
"test:snapshot": "yarn test -u",
|
||||||
"test:watch": "vitest watch",
|
"test:watch": "vitest watch",
|
||||||
"fmt": "prettier src --write --loglevel warn",
|
"fmt": "prettier src --write --loglevel warn",
|
||||||
"fmt:check": "prettier src --check",
|
"fmt:check": "prettier src --check",
|
||||||
|
@ -19,7 +19,7 @@ import { useStyles } from './App.styles';
|
|||||||
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
import useProjects from '../hooks/api/getters/useProjects/useProjects';
|
import useProjects from '../hooks/api/getters/useProjects/useProjects';
|
||||||
import { useLastViewedProject } from '../hooks/useLastViewedProject';
|
import { useLastViewedProject } from '../hooks/useLastViewedProject';
|
||||||
import Maintenance from './maintenance/Maintenance';
|
import MaintenanceBanner from './maintenance/MaintenanceBanner';
|
||||||
|
|
||||||
const InitialRedirect = () => {
|
const InitialRedirect = () => {
|
||||||
const { lastViewed } = useLastViewedProject();
|
const { lastViewed } = useLastViewedProject();
|
||||||
@ -75,10 +75,13 @@ export const App = () => {
|
|||||||
elseShow={
|
elseShow={
|
||||||
<>
|
<>
|
||||||
<ConditionallyRender
|
<ConditionallyRender
|
||||||
condition={Boolean(
|
condition={
|
||||||
uiConfig?.flags?.maintenance
|
Boolean(
|
||||||
)}
|
uiConfig?.flags?.maintenance
|
||||||
show={<Maintenance />}
|
) &&
|
||||||
|
Boolean(uiConfig?.maintenanceMode)
|
||||||
|
}
|
||||||
|
show={<MaintenanceBanner />}
|
||||||
/>
|
/>
|
||||||
<div className={styles.container}>
|
<div className={styles.container}>
|
||||||
<ToastRenderer />
|
<ToastRenderer />
|
||||||
|
@ -0,0 +1,77 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
FormControlLabel,
|
||||||
|
styled,
|
||||||
|
Switch,
|
||||||
|
Typography,
|
||||||
|
} from '@mui/material';
|
||||||
|
import { useMaintenance } from 'hooks/api/getters/useMaintenance/useMaintenance';
|
||||||
|
import { useMaintenanceApi } from 'hooks/api/actions/useMaintenanceApi/useMaintenanceApi';
|
||||||
|
|
||||||
|
const StyledCard = styled(Card)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
padding: theme.spacing(2.5),
|
||||||
|
border: `1px solid ${theme.palette.dividerAlternative}`,
|
||||||
|
borderRadius: theme.shape.borderRadiusLarge,
|
||||||
|
boxShadow: theme.boxShadows.card,
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CardTitleRow = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'row',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
}));
|
||||||
|
|
||||||
|
const CardDescription = styled(Box)(({ theme }) => ({
|
||||||
|
color: theme.palette.text.secondary,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const SwitchLabel = styled(Typography)(({ theme }) => ({
|
||||||
|
fontSize: theme.fontSizes.smallBody,
|
||||||
|
}));
|
||||||
|
|
||||||
|
export const MaintenanceToggle = () => {
|
||||||
|
const { enabled, refetchMaintenance } = useMaintenance();
|
||||||
|
const { toggleMaintenance } = useMaintenanceApi();
|
||||||
|
const updateEnabled = async () => {
|
||||||
|
await toggleMaintenance({ enabled: !enabled });
|
||||||
|
refetchMaintenance();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<StyledCard>
|
||||||
|
<CardContent>
|
||||||
|
<CardTitleRow>
|
||||||
|
<b>Maintenance Mode</b>
|
||||||
|
<FormControlLabel
|
||||||
|
sx={{ fontSize: '10px' }}
|
||||||
|
control={
|
||||||
|
<Switch
|
||||||
|
onChange={updateEnabled}
|
||||||
|
value={enabled}
|
||||||
|
name="enabled"
|
||||||
|
checked={enabled}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={
|
||||||
|
<SwitchLabel>
|
||||||
|
{enabled ? 'Enabled' : 'Disabled'}
|
||||||
|
</SwitchLabel>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</CardTitleRow>
|
||||||
|
<CardDescription>
|
||||||
|
Maintenance Mode is useful when you want to freeze your
|
||||||
|
system so nobody can do any changes during this time. When
|
||||||
|
enabled it will show a banner at the top of the applications
|
||||||
|
and only an admin can enable it or disable it.
|
||||||
|
</CardDescription>
|
||||||
|
</CardContent>
|
||||||
|
</StyledCard>
|
||||||
|
);
|
||||||
|
};
|
@ -0,0 +1,15 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { Alert } from '@mui/material';
|
||||||
|
|
||||||
|
export const MaintenanceTooltip = () => {
|
||||||
|
return (
|
||||||
|
<Alert severity="warning">
|
||||||
|
<p>
|
||||||
|
<b>Heads up!</b> If you enable maintenance mode, edit access in
|
||||||
|
the entire system will be disabled for all the users (admins,
|
||||||
|
editors, custom roles, etc). During this time nobody will be
|
||||||
|
able to do changes or to make new configurations.
|
||||||
|
</p>
|
||||||
|
</Alert>
|
||||||
|
);
|
||||||
|
};
|
56
frontend/src/component/admin/maintenance/index.tsx
Normal file
56
frontend/src/component/admin/maintenance/index.tsx
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { useLocation } from 'react-router-dom';
|
||||||
|
import { ConditionallyRender } from 'component/common/ConditionallyRender/ConditionallyRender';
|
||||||
|
import AdminMenu from '../menu/AdminMenu';
|
||||||
|
import { AdminAlert } from 'component/common/AdminAlert/AdminAlert';
|
||||||
|
import { ADMIN } from 'component/providers/AccessProvider/permissions';
|
||||||
|
import AccessContext from 'contexts/AccessContext';
|
||||||
|
import React, { useContext } from 'react';
|
||||||
|
import { PageContent } from 'component/common/PageContent/PageContent';
|
||||||
|
import { PageHeader } from 'component/common/PageHeader/PageHeader';
|
||||||
|
import { Box, CardContent, styled } from '@mui/material';
|
||||||
|
import { CorsHelpAlert } from 'component/admin/cors/CorsHelpAlert';
|
||||||
|
import { CorsForm } from 'component/admin/cors/CorsForm';
|
||||||
|
import useUiConfig from 'hooks/api/getters/useUiConfig/useUiConfig';
|
||||||
|
import { MaintenanceTooltip } from './MaintenanceTooltip';
|
||||||
|
import { MaintenanceToggle } from './MaintenanceToggle';
|
||||||
|
|
||||||
|
export const MaintenanceAdmin = () => {
|
||||||
|
const { pathname } = useLocation();
|
||||||
|
const showAdminMenu = pathname.includes('/admin/');
|
||||||
|
const { hasAccess } = useContext(AccessContext);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={showAdminMenu}
|
||||||
|
show={<AdminMenu />}
|
||||||
|
/>
|
||||||
|
<ConditionallyRender
|
||||||
|
condition={hasAccess(ADMIN)}
|
||||||
|
show={<MaintenancePage />}
|
||||||
|
elseShow={<AdminAlert />}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const StyledBox = styled(Box)(({ theme }) => ({
|
||||||
|
display: 'grid',
|
||||||
|
gap: theme.spacing(2),
|
||||||
|
}));
|
||||||
|
const MaintenancePage = () => {
|
||||||
|
const { uiConfig, loading } = useUiConfig();
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<PageContent header={<PageHeader title="Maintenance" />}>
|
||||||
|
<StyledBox>
|
||||||
|
<MaintenanceTooltip />
|
||||||
|
<MaintenanceToggle />
|
||||||
|
</StyledBox>
|
||||||
|
</PageContent>
|
||||||
|
);
|
||||||
|
};
|
@ -98,6 +98,17 @@ function AdminMenu() {
|
|||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{flags.maintenance && (
|
||||||
|
<Tab
|
||||||
|
value="maintenance"
|
||||||
|
label={
|
||||||
|
<CenteredNavLink to="/admin/maintenance">
|
||||||
|
Maintenance
|
||||||
|
</CenteredNavLink>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
|
||||||
{isBilling && (
|
{isBilling && (
|
||||||
<Tab
|
<Tab
|
||||||
value="billing"
|
value="billing"
|
||||||
|
@ -19,7 +19,7 @@ const StyledDiv = styled('div')(({ theme }) => ({
|
|||||||
whiteSpace: 'pre-wrap',
|
whiteSpace: 'pre-wrap',
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const Maintenance = () => {
|
const MaintenanceBanner = () => {
|
||||||
return (
|
return (
|
||||||
<StyledDiv>
|
<StyledDiv>
|
||||||
<StyledErrorRoundedIcon />
|
<StyledErrorRoundedIcon />
|
||||||
@ -33,4 +33,4 @@ const Maintenance = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default Maintenance;
|
export default MaintenanceBanner;
|
@ -476,6 +476,17 @@ exports[`returns all baseRoutes 1`] = `
|
|||||||
"title": "Network",
|
"title": "Network",
|
||||||
"type": "protected",
|
"type": "protected",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"component": [Function],
|
||||||
|
"flag": "maintenance",
|
||||||
|
"menu": {
|
||||||
|
"adminSettings": true,
|
||||||
|
},
|
||||||
|
"parent": "/admin",
|
||||||
|
"path": "/admin/maintenance",
|
||||||
|
"title": "Maintenance",
|
||||||
|
"type": "protected",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"component": [Function],
|
"component": [Function],
|
||||||
"flag": "embedProxyFrontend",
|
"flag": "embedProxyFrontend",
|
||||||
|
@ -61,6 +61,7 @@ import { InviteLink } from 'component/admin/users/InviteLink/InviteLink';
|
|||||||
import { Profile } from 'component/user/Profile/Profile';
|
import { Profile } from 'component/user/Profile/Profile';
|
||||||
import { InstanceAdmin } from '../admin/instance-admin/InstanceAdmin';
|
import { InstanceAdmin } from '../admin/instance-admin/InstanceAdmin';
|
||||||
import { Network } from 'component/admin/network/Network';
|
import { Network } from 'component/admin/network/Network';
|
||||||
|
import { MaintenanceAdmin } from '../admin/maintenance';
|
||||||
|
|
||||||
export const routes: IRoute[] = [
|
export const routes: IRoute[] = [
|
||||||
// Splash
|
// Splash
|
||||||
@ -519,6 +520,15 @@ export const routes: IRoute[] = [
|
|||||||
menu: { adminSettings: true },
|
menu: { adminSettings: true },
|
||||||
flag: 'networkView',
|
flag: 'networkView',
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '/admin/maintenance',
|
||||||
|
parent: '/admin',
|
||||||
|
title: 'Maintenance',
|
||||||
|
component: MaintenanceAdmin,
|
||||||
|
type: 'protected',
|
||||||
|
menu: { adminSettings: true },
|
||||||
|
flag: 'maintenance',
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: '/admin/cors',
|
path: '/admin/cors',
|
||||||
parent: '/admin',
|
parent: '/admin',
|
||||||
|
@ -0,0 +1,29 @@
|
|||||||
|
import useAPI from '../useApi/useApi';
|
||||||
|
|
||||||
|
export interface IMaintenancePayload {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMaintenanceApi = () => {
|
||||||
|
const { makeRequest, createRequest, errors, loading } = useAPI({
|
||||||
|
propagateErrors: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const toggleMaintenance = async (payload: IMaintenancePayload) => {
|
||||||
|
const path = `api/admin/maintenance`;
|
||||||
|
const req = createRequest(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(payload),
|
||||||
|
});
|
||||||
|
try {
|
||||||
|
await makeRequest(req.caller, req.id);
|
||||||
|
} catch (e) {
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
return {
|
||||||
|
toggleMaintenance,
|
||||||
|
errors,
|
||||||
|
loading,
|
||||||
|
};
|
||||||
|
};
|
@ -0,0 +1,36 @@
|
|||||||
|
import useSWR from 'swr';
|
||||||
|
import { useMemo } from 'react';
|
||||||
|
import { formatApiPath } from 'utils/formatPath';
|
||||||
|
import handleErrorResponses from '../httpErrorResponseHandler';
|
||||||
|
import { IMaintenancePayload } from 'hooks/api/actions/useMaintenanceApi/useMaintenanceApi';
|
||||||
|
|
||||||
|
export interface IUseMaintenance extends IMaintenancePayload {
|
||||||
|
enabled: boolean;
|
||||||
|
refetchMaintenance: () => void;
|
||||||
|
loading: boolean;
|
||||||
|
status?: number;
|
||||||
|
error?: Error;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const useMaintenance = (): IUseMaintenance => {
|
||||||
|
const { data, error, mutate } = useSWR(
|
||||||
|
formatApiPath(`api/admin/maintenance`),
|
||||||
|
fetcher
|
||||||
|
);
|
||||||
|
|
||||||
|
return useMemo(
|
||||||
|
() => ({
|
||||||
|
enabled: Boolean(data?.enabled),
|
||||||
|
loading: !error && !data,
|
||||||
|
refetchMaintenance: mutate,
|
||||||
|
error,
|
||||||
|
}),
|
||||||
|
[data, error, mutate]
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetcher = (path: string) => {
|
||||||
|
return fetch(path)
|
||||||
|
.then(handleErrorResponses('Maintenance'))
|
||||||
|
.then(res => res.json());
|
||||||
|
};
|
@ -13,6 +13,8 @@ export interface IUiConfig {
|
|||||||
links: ILinks[];
|
links: ILinks[];
|
||||||
disablePasswordAuth?: boolean;
|
disablePasswordAuth?: boolean;
|
||||||
emailEnabled?: boolean;
|
emailEnabled?: boolean;
|
||||||
|
|
||||||
|
maintenanceMode?: boolean;
|
||||||
toast?: IProclamationToast;
|
toast?: IProclamationToast;
|
||||||
segmentValuesLimit?: number;
|
segmentValuesLimit?: number;
|
||||||
strategySegmentsLimit?: number;
|
strategySegmentsLimit?: number;
|
||||||
|
@ -76,6 +76,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxyFrontend": true,
|
"embedProxyFrontend": true,
|
||||||
"favorites": false,
|
"favorites": false,
|
||||||
"maintenance": false,
|
"maintenance": false,
|
||||||
|
"maintenanceMode": false,
|
||||||
"networkView": false,
|
"networkView": false,
|
||||||
"proxyReturnAllToggles": false,
|
"proxyReturnAllToggles": false,
|
||||||
"responseTimeWithAppName": false,
|
"responseTimeWithAppName": false,
|
||||||
@ -93,6 +94,7 @@ exports[`should create default config 1`] = `
|
|||||||
"embedProxyFrontend": true,
|
"embedProxyFrontend": true,
|
||||||
"favorites": false,
|
"favorites": false,
|
||||||
"maintenance": false,
|
"maintenance": false,
|
||||||
|
"maintenanceMode": false,
|
||||||
"networkView": false,
|
"networkView": false,
|
||||||
"proxyReturnAllToggles": false,
|
"proxyReturnAllToggles": false,
|
||||||
"responseTimeWithAppName": false,
|
"responseTimeWithAppName": false,
|
||||||
|
@ -139,7 +139,13 @@ export default async function getApp(
|
|||||||
rbacMiddleware(config, stores, services.accessService),
|
rbacMiddleware(config, stores, services.accessService),
|
||||||
);
|
);
|
||||||
|
|
||||||
app.use('/api/admin', maintenanceMiddleware(config));
|
app.use(
|
||||||
|
`${baseUriPath}/api/admin`,
|
||||||
|
conditionalMiddleware(
|
||||||
|
() => config.flagResolver.isEnabled('maintenance'),
|
||||||
|
maintenanceMiddleware(config, services.maintenanceService),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
if (typeof config.preRouterHook === 'function') {
|
if (typeof config.preRouterHook === 'function') {
|
||||||
config.preRouterHook(app, config, services, stores, db);
|
config.preRouterHook(app, config, services, stores, db);
|
||||||
|
@ -1,16 +1,22 @@
|
|||||||
import { IUnleashConfig } from '../types';
|
import { IUnleashConfig } from '../types';
|
||||||
import { Request } from 'express';
|
import MaintenanceService from '../services/maintenance-service';
|
||||||
|
import { IAuthRequest } from '../routes/unleash-types';
|
||||||
|
|
||||||
const maintenanceMiddleware = ({
|
const maintenanceMiddleware = (
|
||||||
getLogger,
|
{ getLogger }: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>,
|
||||||
flagResolver,
|
maintenanceService: MaintenanceService,
|
||||||
}: Pick<IUnleashConfig, 'getLogger' | 'flagResolver'>): any => {
|
): any => {
|
||||||
const logger = getLogger('/middleware/maintenance-middleware.ts');
|
const logger = getLogger('/middleware/maintenance-middleware.ts');
|
||||||
logger.debug('Enabling Maintenance middleware');
|
logger.debug('Enabling Maintenance middleware');
|
||||||
|
|
||||||
return async (req: Request, res, next) => {
|
return async (req: IAuthRequest, res, next) => {
|
||||||
|
const isProtectedPath = !req.path.includes('/maintenance');
|
||||||
const writeMethod = ['POST', 'PUT', 'DELETE'].includes(req.method);
|
const writeMethod = ['POST', 'PUT', 'DELETE'].includes(req.method);
|
||||||
if (writeMethod && flagResolver.isEnabled('maintenance')) {
|
if (
|
||||||
|
isProtectedPath &&
|
||||||
|
writeMethod &&
|
||||||
|
(await maintenanceService.isMaintenanceMode())
|
||||||
|
) {
|
||||||
res.status(503).send({});
|
res.status(503).send({});
|
||||||
} else {
|
} else {
|
||||||
next();
|
next();
|
||||||
|
@ -129,6 +129,7 @@ import { mapValues, omitKeys } from '../util';
|
|||||||
import { openApiTags } from './util';
|
import { openApiTags } from './util';
|
||||||
import { URL } from 'url';
|
import { URL } from 'url';
|
||||||
import apiVersion from '../util/version';
|
import apiVersion from '../util/version';
|
||||||
|
import { maintenanceSchema } from './spec/maintenance-schema';
|
||||||
|
|
||||||
// All schemas in `openapi/spec` should be listed here.
|
// All schemas in `openapi/spec` should be listed here.
|
||||||
export const schemas = {
|
export const schemas = {
|
||||||
@ -189,6 +190,7 @@ export const schemas = {
|
|||||||
instanceAdminStatsSchema,
|
instanceAdminStatsSchema,
|
||||||
legalValueSchema,
|
legalValueSchema,
|
||||||
loginSchema,
|
loginSchema,
|
||||||
|
maintenanceSchema,
|
||||||
meSchema,
|
meSchema,
|
||||||
nameSchema,
|
nameSchema,
|
||||||
overrideSchema,
|
overrideSchema,
|
||||||
|
16
src/lib/openapi/spec/maintenance-schema.ts
Normal file
16
src/lib/openapi/spec/maintenance-schema.ts
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
import { FromSchema } from 'json-schema-to-ts';
|
||||||
|
|
||||||
|
export const maintenanceSchema = {
|
||||||
|
$id: '#/components/schemas/maintenanceSchema',
|
||||||
|
type: 'object',
|
||||||
|
additionalProperties: false,
|
||||||
|
required: ['enabled'],
|
||||||
|
properties: {
|
||||||
|
enabled: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
components: {},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export type MaintenanceSchema = FromSchema<typeof maintenanceSchema>;
|
@ -31,6 +31,9 @@ export const uiConfigSchema = {
|
|||||||
emailEnabled: {
|
emailEnabled: {
|
||||||
type: 'boolean',
|
type: 'boolean',
|
||||||
},
|
},
|
||||||
|
maintenanceMode: {
|
||||||
|
type: 'boolean',
|
||||||
|
},
|
||||||
segmentValuesLimit: {
|
segmentValuesLimit: {
|
||||||
type: 'number',
|
type: 'number',
|
||||||
},
|
},
|
||||||
|
@ -96,6 +96,10 @@ const OPENAPI_TAGS = [
|
|||||||
'Experimental endpoints that may change or disappear at any time.',
|
'Experimental endpoints that may change or disappear at any time.',
|
||||||
},
|
},
|
||||||
{ name: 'Edge', description: 'Endpoints related to Unleash on the Edge.' },
|
{ name: 'Edge', description: 'Endpoints related to Unleash on the Edge.' },
|
||||||
|
{
|
||||||
|
name: 'Maintenance',
|
||||||
|
description: 'Enable/disable the maintenance mode of Unleash.',
|
||||||
|
},
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// make the export mutable, so it can be used in a schema
|
// make the export mutable, so it can be used in a schema
|
||||||
|
@ -4,8 +4,8 @@ import { IUnleashServices } from '../../types/services';
|
|||||||
import { IAuthType, IUnleashConfig } from '../../types/option';
|
import { IAuthType, IUnleashConfig } from '../../types/option';
|
||||||
import version from '../../util/version';
|
import version from '../../util/version';
|
||||||
import Controller from '../controller';
|
import Controller from '../controller';
|
||||||
import VersionService from '../../services/version-service';
|
import VersionService from 'lib/services/version-service';
|
||||||
import SettingService from '../../services/setting-service';
|
import SettingService from 'lib/services/setting-service';
|
||||||
import {
|
import {
|
||||||
simpleAuthSettingsKey,
|
simpleAuthSettingsKey,
|
||||||
SimpleAuthSettings,
|
SimpleAuthSettings,
|
||||||
@ -25,6 +25,7 @@ import NotFoundError from '../../error/notfound-error';
|
|||||||
import { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema';
|
import { SetUiConfigSchema } from '../../openapi/spec/set-ui-config-schema';
|
||||||
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
import { createRequestSchema } from '../../openapi/util/create-request-schema';
|
||||||
import { ProxyService } from 'lib/services';
|
import { ProxyService } from 'lib/services';
|
||||||
|
import MaintenanceService from 'lib/services/maintenance-service';
|
||||||
|
|
||||||
class ConfigController extends Controller {
|
class ConfigController extends Controller {
|
||||||
private versionService: VersionService;
|
private versionService: VersionService;
|
||||||
@ -35,6 +36,8 @@ class ConfigController extends Controller {
|
|||||||
|
|
||||||
private emailService: EmailService;
|
private emailService: EmailService;
|
||||||
|
|
||||||
|
private maintenanceService: MaintenanceService;
|
||||||
|
|
||||||
private readonly openApiService: OpenApiService;
|
private readonly openApiService: OpenApiService;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@ -45,6 +48,7 @@ class ConfigController extends Controller {
|
|||||||
emailService,
|
emailService,
|
||||||
openApiService,
|
openApiService,
|
||||||
proxyService,
|
proxyService,
|
||||||
|
maintenanceService,
|
||||||
}: Pick<
|
}: Pick<
|
||||||
IUnleashServices,
|
IUnleashServices,
|
||||||
| 'versionService'
|
| 'versionService'
|
||||||
@ -52,6 +56,7 @@ class ConfigController extends Controller {
|
|||||||
| 'emailService'
|
| 'emailService'
|
||||||
| 'openApiService'
|
| 'openApiService'
|
||||||
| 'proxyService'
|
| 'proxyService'
|
||||||
|
| 'maintenanceService'
|
||||||
>,
|
>,
|
||||||
) {
|
) {
|
||||||
super(config);
|
super(config);
|
||||||
@ -60,6 +65,7 @@ class ConfigController extends Controller {
|
|||||||
this.emailService = emailService;
|
this.emailService = emailService;
|
||||||
this.openApiService = openApiService;
|
this.openApiService = openApiService;
|
||||||
this.proxyService = proxyService;
|
this.proxyService = proxyService;
|
||||||
|
this.maintenanceService = maintenanceService;
|
||||||
|
|
||||||
this.route({
|
this.route({
|
||||||
method: 'get',
|
method: 'get',
|
||||||
@ -97,10 +103,14 @@ class ConfigController extends Controller {
|
|||||||
req: AuthedRequest,
|
req: AuthedRequest,
|
||||||
res: Response<UiConfigSchema>,
|
res: Response<UiConfigSchema>,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const [frontendSettings, simpleAuthSettings] = await Promise.all([
|
const [frontendSettings, simpleAuthSettings, maintenanceMode] =
|
||||||
this.proxyService.getFrontendSettings(false),
|
await Promise.all([
|
||||||
this.settingService.get<SimpleAuthSettings>(simpleAuthSettingsKey),
|
this.proxyService.getFrontendSettings(false),
|
||||||
]);
|
this.settingService.get<SimpleAuthSettings>(
|
||||||
|
simpleAuthSettingsKey,
|
||||||
|
),
|
||||||
|
this.maintenanceService.isMaintenanceMode(),
|
||||||
|
]);
|
||||||
|
|
||||||
const disablePasswordAuth =
|
const disablePasswordAuth =
|
||||||
simpleAuthSettings?.disabled ||
|
simpleAuthSettings?.disabled ||
|
||||||
@ -124,6 +134,7 @@ class ConfigController extends Controller {
|
|||||||
frontendApiOrigins: frontendSettings.frontendApiOrigins,
|
frontendApiOrigins: frontendSettings.frontendApiOrigins,
|
||||||
versionInfo: this.versionService.getVersionInfo(),
|
versionInfo: this.versionService.getVersionInfo(),
|
||||||
disablePasswordAuth,
|
disablePasswordAuth,
|
||||||
|
maintenanceMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
this.openApiService.respondWithValidation(
|
this.openApiService.respondWithValidation(
|
||||||
|
@ -28,6 +28,7 @@ import { PublicSignupController } from './public-signup';
|
|||||||
import InstanceAdminController from './instance-admin';
|
import InstanceAdminController from './instance-admin';
|
||||||
import FavoritesController from './favorites';
|
import FavoritesController from './favorites';
|
||||||
import { conditionalMiddleware } from '../../middleware';
|
import { conditionalMiddleware } from '../../middleware';
|
||||||
|
import MaintenanceController from './maintenance';
|
||||||
|
|
||||||
class AdminApi extends Controller {
|
class AdminApi extends Controller {
|
||||||
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
constructor(config: IUnleashConfig, services: IUnleashServices) {
|
||||||
@ -124,6 +125,11 @@ class AdminApi extends Controller {
|
|||||||
new FavoritesController(config, services).router,
|
new FavoritesController(config, services).router,
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
|
|
||||||
|
this.app.use(
|
||||||
|
'/maintenance',
|
||||||
|
new MaintenanceController(config, services).router,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
100
src/lib/routes/admin-api/maintenance.ts
Normal file
100
src/lib/routes/admin-api/maintenance.ts
Normal file
@ -0,0 +1,100 @@
|
|||||||
|
import { ADMIN, IUnleashConfig, IUnleashServices } from '../../types';
|
||||||
|
import { Request, Response } from 'express';
|
||||||
|
import Controller from '../controller';
|
||||||
|
import { Logger } from '../../logger';
|
||||||
|
import {
|
||||||
|
createRequestSchema,
|
||||||
|
createResponseSchema,
|
||||||
|
emptyResponse,
|
||||||
|
} from '../../openapi';
|
||||||
|
import { OpenApiService } from '../../services';
|
||||||
|
import { IAuthRequest } from '../unleash-types';
|
||||||
|
import { extractUsername } from '../../util';
|
||||||
|
import {
|
||||||
|
MaintenanceSchema,
|
||||||
|
maintenanceSchema,
|
||||||
|
} from '../../openapi/spec/maintenance-schema';
|
||||||
|
import MaintenanceService from 'lib/services/maintenance-service';
|
||||||
|
import { InvalidOperationError } from '../../error';
|
||||||
|
|
||||||
|
export default class MaintenanceController extends Controller {
|
||||||
|
private maintenanceService: MaintenanceService;
|
||||||
|
|
||||||
|
private openApiService: OpenApiService;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
config: IUnleashConfig,
|
||||||
|
{
|
||||||
|
maintenanceService,
|
||||||
|
openApiService,
|
||||||
|
}: Pick<IUnleashServices, 'maintenanceService' | 'openApiService'>,
|
||||||
|
) {
|
||||||
|
super(config);
|
||||||
|
this.maintenanceService = maintenanceService;
|
||||||
|
this.openApiService = openApiService;
|
||||||
|
this.logger = config.getLogger('routes/admin-api/maintenance');
|
||||||
|
this.route({
|
||||||
|
method: 'post',
|
||||||
|
path: '',
|
||||||
|
permission: ADMIN,
|
||||||
|
handler: this.toggleMaintenance,
|
||||||
|
middleware: [
|
||||||
|
this.openApiService.validPath({
|
||||||
|
tags: ['Maintenance'],
|
||||||
|
operationId: 'toggleMaintenance',
|
||||||
|
responses: {
|
||||||
|
204: emptyResponse,
|
||||||
|
},
|
||||||
|
requestBody: createRequestSchema('maintenanceSchema'),
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
this.route({
|
||||||
|
method: 'get',
|
||||||
|
path: '',
|
||||||
|
permission: ADMIN,
|
||||||
|
handler: this.getMaintenance,
|
||||||
|
middleware: [
|
||||||
|
this.openApiService.validPath({
|
||||||
|
tags: ['Maintenance'],
|
||||||
|
operationId: 'getMaintenance',
|
||||||
|
responses: {
|
||||||
|
200: createResponseSchema('maintenanceSchema'),
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleMaintenance(
|
||||||
|
req: IAuthRequest<unknown, unknown, MaintenanceSchema>,
|
||||||
|
res: Response,
|
||||||
|
): Promise<void> {
|
||||||
|
this.verifyMaintenanceEnabled();
|
||||||
|
await this.maintenanceService.toggleMaintenanceMode(
|
||||||
|
req.body,
|
||||||
|
extractUsername(req),
|
||||||
|
);
|
||||||
|
res.status(204).end();
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMaintenance(req: Request, res: Response): Promise<void> {
|
||||||
|
this.verifyMaintenanceEnabled();
|
||||||
|
const settings = await this.maintenanceService.getMaintenanceSetting();
|
||||||
|
this.openApiService.respondWithValidation(
|
||||||
|
200,
|
||||||
|
res,
|
||||||
|
maintenanceSchema.$id,
|
||||||
|
settings,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private verifyMaintenanceEnabled() {
|
||||||
|
if (!this.config.flagResolver.isEnabled('maintenance')) {
|
||||||
|
throw new InvalidOperationError('Maintenance is not enabled');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
module.exports = MaintenanceController;
|
@ -38,6 +38,7 @@ import { PublicSignupTokenService } from './public-signup-token-service';
|
|||||||
import { LastSeenService } from './client-metrics/last-seen-service';
|
import { LastSeenService } from './client-metrics/last-seen-service';
|
||||||
import { InstanceStatsService } from './instance-stats-service';
|
import { InstanceStatsService } from './instance-stats-service';
|
||||||
import { FavoritesService } from './favorites-service';
|
import { FavoritesService } from './favorites-service';
|
||||||
|
import MaintenanceService from './maintenance-service';
|
||||||
|
|
||||||
export const createServices = (
|
export const createServices = (
|
||||||
stores: IUnleashStores,
|
stores: IUnleashStores,
|
||||||
@ -128,6 +129,12 @@ export const createServices = (
|
|||||||
versionService,
|
versionService,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const maintenanceService = new MaintenanceService(
|
||||||
|
stores,
|
||||||
|
config,
|
||||||
|
settingService,
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
accessService,
|
accessService,
|
||||||
addonService,
|
addonService,
|
||||||
@ -168,6 +175,7 @@ export const createServices = (
|
|||||||
lastSeenService,
|
lastSeenService,
|
||||||
instanceStatsService,
|
instanceStatsService,
|
||||||
favoritesService,
|
favoritesService,
|
||||||
|
maintenanceService,
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
62
src/lib/services/maintenance-service.ts
Normal file
62
src/lib/services/maintenance-service.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { IUnleashConfig, IUnleashStores } from '../types';
|
||||||
|
import { Logger } from '../logger';
|
||||||
|
import { IPatStore } from '../types/stores/pat-store';
|
||||||
|
import { IEventStore } from '../types/stores/event-store';
|
||||||
|
import SettingService from './setting-service';
|
||||||
|
import { maintenanceSettingsKey } from '../types/settings/maintenance-settings';
|
||||||
|
import { MaintenanceSchema } from '../openapi/spec/maintenance-schema';
|
||||||
|
|
||||||
|
export default class MaintenanceService {
|
||||||
|
private config: IUnleashConfig;
|
||||||
|
|
||||||
|
private logger: Logger;
|
||||||
|
|
||||||
|
private patStore: IPatStore;
|
||||||
|
|
||||||
|
private eventStore: IEventStore;
|
||||||
|
|
||||||
|
private settingService: SettingService;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
{
|
||||||
|
patStore,
|
||||||
|
eventStore,
|
||||||
|
}: Pick<IUnleashStores, 'patStore' | 'eventStore'>,
|
||||||
|
config: IUnleashConfig,
|
||||||
|
settingService: SettingService,
|
||||||
|
) {
|
||||||
|
this.config = config;
|
||||||
|
this.logger = config.getLogger('services/pat-service.ts');
|
||||||
|
this.patStore = patStore;
|
||||||
|
this.eventStore = eventStore;
|
||||||
|
this.settingService = settingService;
|
||||||
|
}
|
||||||
|
|
||||||
|
async isMaintenanceMode(): Promise<boolean> {
|
||||||
|
return (
|
||||||
|
this.config.flagResolver.isEnabled('maintenanceMode') ||
|
||||||
|
(await this.getMaintenanceSetting()).enabled
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async getMaintenanceSetting(): Promise<MaintenanceSchema> {
|
||||||
|
return (
|
||||||
|
(await this.settingService.get(maintenanceSettingsKey)) || {
|
||||||
|
enabled: false,
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async toggleMaintenanceMode(
|
||||||
|
setting: MaintenanceSchema,
|
||||||
|
user: string,
|
||||||
|
): Promise<void> {
|
||||||
|
return this.settingService.insert(
|
||||||
|
maintenanceSettingsKey,
|
||||||
|
setting,
|
||||||
|
user,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
module.exports = MaintenanceService;
|
@ -47,6 +47,10 @@ const flags = {
|
|||||||
process.env.UNLEASH_EXPERIMENTAL_MAINTENANCE,
|
process.env.UNLEASH_EXPERIMENTAL_MAINTENANCE,
|
||||||
false,
|
false,
|
||||||
),
|
),
|
||||||
|
maintenanceMode: parseEnvVarBoolean(
|
||||||
|
process.env.UNLEASH_EXPERIMENTAL_MAINTENANCE_MODE,
|
||||||
|
false,
|
||||||
|
),
|
||||||
};
|
};
|
||||||
|
|
||||||
export const defaultExperimentalOptions: IExperimentalOptions = {
|
export const defaultExperimentalOptions: IExperimentalOptions = {
|
||||||
|
@ -36,6 +36,7 @@ import { PublicSignupTokenService } from '../services/public-signup-token-servic
|
|||||||
import { LastSeenService } from '../services/client-metrics/last-seen-service';
|
import { LastSeenService } from '../services/client-metrics/last-seen-service';
|
||||||
import { InstanceStatsService } from '../services/instance-stats-service';
|
import { InstanceStatsService } from '../services/instance-stats-service';
|
||||||
import { FavoritesService } from '../services';
|
import { FavoritesService } from '../services';
|
||||||
|
import MaintenanceService from '../services/maintenance-service';
|
||||||
|
|
||||||
export interface IUnleashServices {
|
export interface IUnleashServices {
|
||||||
accessService: AccessService;
|
accessService: AccessService;
|
||||||
@ -77,4 +78,5 @@ export interface IUnleashServices {
|
|||||||
lastSeenService: LastSeenService;
|
lastSeenService: LastSeenService;
|
||||||
instanceStatsService: InstanceStatsService;
|
instanceStatsService: InstanceStatsService;
|
||||||
favoritesService: FavoritesService;
|
favoritesService: FavoritesService;
|
||||||
|
maintenanceService: MaintenanceService;
|
||||||
}
|
}
|
||||||
|
5
src/lib/types/settings/maintenance-settings.ts
Normal file
5
src/lib/types/settings/maintenance-settings.ts
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
export const maintenanceSettingsKey = 'maintenance.mode';
|
||||||
|
|
||||||
|
export interface MaintenanceSettings {
|
||||||
|
enabled: boolean;
|
||||||
|
}
|
@ -42,7 +42,6 @@ process.nextTick(async () => {
|
|||||||
changeRequests: true,
|
changeRequests: true,
|
||||||
favorites: true,
|
favorites: true,
|
||||||
variantsPerEnvironment: true,
|
variantsPerEnvironment: true,
|
||||||
maintenance: false,
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
authentication: {
|
authentication: {
|
||||||
|
@ -837,21 +837,3 @@ test('should have access to the get all features endpoint even if api is disable
|
|||||||
.get('/api/admin/features')
|
.get('/api/admin/features')
|
||||||
.expect(200);
|
.expect(200);
|
||||||
});
|
});
|
||||||
|
|
||||||
test('should not allow creation of feature toggle in maintenance mode', async () => {
|
|
||||||
const appWithMaintenanceMode = await setupAppWithCustomConfig(db.stores, {
|
|
||||||
experimental: {
|
|
||||||
flags: {
|
|
||||||
maintenance: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return appWithMaintenanceMode.request
|
|
||||||
.post('/api/admin/features')
|
|
||||||
.send({
|
|
||||||
name: 'maintenance-feature',
|
|
||||||
})
|
|
||||||
.set('Content-Type', 'application/json')
|
|
||||||
.expect(503);
|
|
||||||
});
|
|
||||||
|
144
src/test/e2e/api/admin/maintenance.e2e.test.ts
Normal file
144
src/test/e2e/api/admin/maintenance.e2e.test.ts
Normal file
@ -0,0 +1,144 @@
|
|||||||
|
import dbInit, { ITestDb } from '../../helpers/database-init';
|
||||||
|
import { setupAppWithCustomConfig } from '../../helpers/test-helper';
|
||||||
|
import getLogger from '../../../fixtures/no-logger';
|
||||||
|
|
||||||
|
let db: ITestDb;
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
db = await dbInit('maintenance_api_serial', getLogger);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(async () => {
|
||||||
|
await db.stores.featureToggleStore.deleteAll();
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(async () => {
|
||||||
|
await db.destroy();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not allow to create feature toggles in maintenance mode', async () => {
|
||||||
|
const appWithMaintenanceMode = await setupAppWithCustomConfig(db.stores, {
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
maintenance: true,
|
||||||
|
maintenanceMode: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return appWithMaintenanceMode.request
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({
|
||||||
|
name: 'maintenance-feature',
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(503);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should not go into maintenance, when maintenance feature is off', async () => {
|
||||||
|
const appWithMaintenanceMode = await setupAppWithCustomConfig(db.stores, {
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
maintenance: false,
|
||||||
|
maintenanceMode: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return appWithMaintenanceMode.request
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({
|
||||||
|
name: 'maintenance-feature1',
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maintenance mode is off by default', async () => {
|
||||||
|
const appWithMaintenanceMode = await setupAppWithCustomConfig(db.stores, {
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
maintenance: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return appWithMaintenanceMode.request
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({
|
||||||
|
name: 'maintenance-feature1',
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(201);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should go into maintenance mode, when user has set it', async () => {
|
||||||
|
const appWithMaintenanceMode = await setupAppWithCustomConfig(db.stores, {
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
maintenance: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await appWithMaintenanceMode.request
|
||||||
|
.post('/api/admin/maintenance')
|
||||||
|
.send({
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(204);
|
||||||
|
|
||||||
|
return appWithMaintenanceMode.request
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({
|
||||||
|
name: 'maintenance-feature1',
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(503);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('should 404 on maintenance endpoint, when disabled', async () => {
|
||||||
|
const appWithMaintenanceMode = await setupAppWithCustomConfig(db.stores, {
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
maintenance: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await appWithMaintenanceMode.request
|
||||||
|
.post('/api/admin/maintenance')
|
||||||
|
.send({
|
||||||
|
enabled: true,
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(403);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('maintenance mode flag should take precedence over maintenance mode setting', async () => {
|
||||||
|
const appWithMaintenanceMode = await setupAppWithCustomConfig(db.stores, {
|
||||||
|
experimental: {
|
||||||
|
flags: {
|
||||||
|
maintenance: true,
|
||||||
|
maintenanceMode: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
await appWithMaintenanceMode.request
|
||||||
|
.post('/api/admin/maintenance')
|
||||||
|
.send({
|
||||||
|
enabled: false,
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(204);
|
||||||
|
|
||||||
|
return appWithMaintenanceMode.request
|
||||||
|
.post('/api/admin/features')
|
||||||
|
.send({
|
||||||
|
name: 'maintenance-feature1',
|
||||||
|
})
|
||||||
|
.set('Content-Type', 'application/json')
|
||||||
|
.expect(503);
|
||||||
|
});
|
@ -1763,6 +1763,18 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
],
|
],
|
||||||
"type": "object",
|
"type": "object",
|
||||||
},
|
},
|
||||||
|
"maintenanceSchema": {
|
||||||
|
"additionalProperties": false,
|
||||||
|
"properties": {
|
||||||
|
"enabled": {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"enabled",
|
||||||
|
],
|
||||||
|
"type": "object",
|
||||||
|
},
|
||||||
"meSchema": {
|
"meSchema": {
|
||||||
"additionalProperties": false,
|
"additionalProperties": false,
|
||||||
"properties": {
|
"properties": {
|
||||||
@ -3321,6 +3333,9 @@ exports[`should serve the OpenAPI spec 1`] = `
|
|||||||
},
|
},
|
||||||
"type": "array",
|
"type": "array",
|
||||||
},
|
},
|
||||||
|
"maintenanceMode": {
|
||||||
|
"type": "boolean",
|
||||||
|
},
|
||||||
"name": {
|
"name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
},
|
},
|
||||||
@ -4999,6 +5014,48 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
],
|
],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
"/api/admin/maintenance": {
|
||||||
|
"get": {
|
||||||
|
"operationId": "getMaintenance",
|
||||||
|
"responses": {
|
||||||
|
"200": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/maintenanceSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "maintenanceSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Maintenance",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"post": {
|
||||||
|
"operationId": "toggleMaintenance",
|
||||||
|
"requestBody": {
|
||||||
|
"content": {
|
||||||
|
"application/json": {
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/components/schemas/maintenanceSchema",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"description": "maintenanceSchema",
|
||||||
|
"required": true,
|
||||||
|
},
|
||||||
|
"responses": {
|
||||||
|
"204": {
|
||||||
|
"description": "This response has no body.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"tags": [
|
||||||
|
"Maintenance",
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
"/api/admin/metrics/applications": {
|
"/api/admin/metrics/applications": {
|
||||||
"get": {
|
"get": {
|
||||||
"operationId": "getApplications",
|
"operationId": "getApplications",
|
||||||
@ -8008,6 +8065,10 @@ If the provided project does not exist, the list of events will be empty.",
|
|||||||
"description": "Instance admin endpoints used to manage the Unleash instance itself.",
|
"description": "Instance admin endpoints used to manage the Unleash instance itself.",
|
||||||
"name": "Instance Admin",
|
"name": "Instance Admin",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"description": "Enable/disable the maintenance mode of Unleash.",
|
||||||
|
"name": "Maintenance",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"description": "Register, read, or delete metrics recorded by Unleash.",
|
"description": "Register, read, or delete metrics recorded by Unleash.",
|
||||||
"name": "Metrics",
|
"name": "Metrics",
|
||||||
|
Loading…
Reference in New Issue
Block a user