mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
/api/status
- always return a consistent status code (#159768)
## Summary Fix https://github.com/elastic/kibana/issues/158910 Changes the behavior of the `/api/status` endpoint to always returns a consistent http status code, and in particular: - during the preboot stage - when accessed by unauthenticated users and `status.allowAnonymous` is `false`. That way, `/api/status` can properly be used for readiness checks. Please refer to https://github.com/elastic/kibana/issues/158910 for more details. --------- Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
cc04704cb5
commit
9e0c9a7ad5
19 changed files with 660 additions and 420 deletions
|
@ -196,6 +196,7 @@ enabled:
|
|||
- x-pack/test/api_integration/apis/security/config.ts
|
||||
- x-pack/test/api_integration/apis/security_solution/config.ts
|
||||
- x-pack/test/api_integration/apis/spaces/config.ts
|
||||
- x-pack/test/api_integration/apis/status/config.ts
|
||||
- x-pack/test/api_integration/apis/synthetics/config.ts
|
||||
- x-pack/test/api_integration/apis/telemetry/config.ts
|
||||
- x-pack/test/api_integration/apis/transform/config.ts
|
||||
|
|
|
@ -348,42 +348,6 @@ test('register preboot route handler on preboot', async () => {
|
|||
await service.stop();
|
||||
});
|
||||
|
||||
test('register preboot route handler on setup', async () => {
|
||||
const registerRouterMock = jest.fn();
|
||||
mockHttpServer
|
||||
.mockImplementationOnce(() => ({
|
||||
setup: () => ({
|
||||
server: { start: jest.fn(), stop: jest.fn(), route: jest.fn() },
|
||||
registerStaticDir: jest.fn(),
|
||||
registerRouterAfterListening: registerRouterMock,
|
||||
}),
|
||||
start: noop,
|
||||
stop: noop,
|
||||
isListening: jest.fn(),
|
||||
}))
|
||||
.mockImplementationOnce(() => ({
|
||||
setup: () => ({ server: {} }),
|
||||
start: noop,
|
||||
stop: noop,
|
||||
isListening: jest.fn(),
|
||||
}));
|
||||
|
||||
const service = new HttpService({ coreId, configService: createConfigService(), env, logger });
|
||||
await service.preboot(prebootDeps);
|
||||
|
||||
const registerRoutesMock = jest.fn();
|
||||
const { registerPrebootRoutes } = await service.setup(setupDeps);
|
||||
registerPrebootRoutes('some-path', registerRoutesMock);
|
||||
|
||||
expect(registerRoutesMock).toHaveBeenCalledTimes(1);
|
||||
expect(registerRoutesMock).toHaveBeenCalledWith(expect.any(Router));
|
||||
|
||||
const [[router]] = registerRoutesMock.mock.calls;
|
||||
expect(registerRouterMock).toHaveBeenCalledTimes(1);
|
||||
expect(registerRouterMock).toHaveBeenCalledWith(router);
|
||||
await service.stop();
|
||||
});
|
||||
|
||||
test('returns `preboot` http server contract on preboot', async () => {
|
||||
const configService = createConfigService();
|
||||
const httpServer = {
|
||||
|
@ -442,7 +406,6 @@ test('returns http server contract on setup', async () => {
|
|||
expect(setupContract).toMatchObject(httpServer);
|
||||
expect(setupContract).toMatchObject({
|
||||
createRouter: expect.any(Function),
|
||||
registerPrebootRoutes: expect.any(Function),
|
||||
});
|
||||
await service.stop();
|
||||
});
|
||||
|
|
|
@ -189,8 +189,6 @@ export class HttpService
|
|||
contextName: ContextName,
|
||||
provider: IContextProvider<Context, ContextName>
|
||||
) => this.requestHandlerContext!.registerContext(pluginOpaqueId, contextName, provider),
|
||||
|
||||
registerPrebootRoutes: this.internalPreboot!.registerRoutes,
|
||||
};
|
||||
|
||||
return this.internalSetup;
|
||||
|
|
|
@ -61,8 +61,6 @@ export interface InternalHttpServiceSetup
|
|||
contextName: ContextName,
|
||||
provider: IContextProvider<Context, ContextName>
|
||||
) => IContextContainer;
|
||||
|
||||
registerPrebootRoutes(path: string, callback: (router: IRouter) => void): void;
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
|
|
|
@ -153,7 +153,6 @@ const createInternalSetupContractMock = () => {
|
|||
auth: createAuthMock(),
|
||||
authRequestHeaders: createAuthHeaderStorageMock(),
|
||||
getServerInfo: jest.fn(),
|
||||
registerPrebootRoutes: jest.fn(),
|
||||
registerRouterAfterListening: jest.fn(),
|
||||
};
|
||||
mock.createCookieSessionStorageFactory.mockResolvedValue(sessionStorageMock.createFactory());
|
||||
|
|
|
@ -79,6 +79,7 @@ test('preboot services on "preboot"', async () => {
|
|||
expect(mockLoggingService.preboot).not.toHaveBeenCalled();
|
||||
expect(mockPluginsService.preboot).not.toHaveBeenCalled();
|
||||
expect(mockPrebootService.preboot).not.toHaveBeenCalled();
|
||||
expect(mockStatusService.preboot).not.toHaveBeenCalled();
|
||||
|
||||
await server.preboot();
|
||||
|
||||
|
@ -93,6 +94,7 @@ test('preboot services on "preboot"', async () => {
|
|||
expect(mockLoggingService.preboot).toHaveBeenCalledTimes(1);
|
||||
expect(mockPluginsService.preboot).toHaveBeenCalledTimes(1);
|
||||
expect(mockPrebootService.preboot).toHaveBeenCalledTimes(1);
|
||||
expect(mockStatusService.preboot).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
test('sets up services on "setup"', async () => {
|
||||
|
|
|
@ -190,6 +190,7 @@ export class Server {
|
|||
this.capabilities.preboot({ http: httpPreboot });
|
||||
const elasticsearchServicePreboot = await this.elasticsearch.preboot();
|
||||
const uiSettingsPreboot = await this.uiSettings.preboot();
|
||||
await this.status.preboot({ http: httpPreboot });
|
||||
|
||||
const renderingPreboot = await this.rendering.preboot({ http: httpPreboot, uiPlugins });
|
||||
const httpResourcesPreboot = this.httpResources.preboot({
|
||||
|
|
|
@ -7,3 +7,4 @@
|
|||
*/
|
||||
|
||||
export { registerStatusRoute } from './status';
|
||||
export { registerPrebootStatusRoute } from './status_preboot';
|
||||
|
|
|
@ -14,7 +14,12 @@ import type { IRouter } from '@kbn/core-http-server';
|
|||
import type { MetricsServiceSetup } from '@kbn/core-metrics-server';
|
||||
import type { CoreIncrementUsageCounter } from '@kbn/core-usage-data-server';
|
||||
import type { StatusResponse } from '@kbn/core-status-common-internal';
|
||||
import { ServiceStatus, CoreStatus, ServiceStatusLevels } from '@kbn/core-status-common';
|
||||
import {
|
||||
ServiceStatus,
|
||||
ServiceStatusLevel,
|
||||
CoreStatus,
|
||||
ServiceStatusLevels,
|
||||
} from '@kbn/core-status-common';
|
||||
import { calculateLegacyStatus, LegacyStatusInfo } from '../legacy_status';
|
||||
|
||||
const SNAPSHOT_POSTFIX = /-SNAPSHOT$/;
|
||||
|
@ -48,6 +53,14 @@ interface StatusHttpBody extends Omit<StatusResponse, 'status'> {
|
|||
status: StatusInfo | LegacyStatusInfo;
|
||||
}
|
||||
|
||||
export interface RedactedStatusHttpBody {
|
||||
status: {
|
||||
overall: {
|
||||
level: ServiceStatusLevel;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
export const registerStatusRoute = ({
|
||||
router,
|
||||
config,
|
||||
|
@ -68,7 +81,7 @@ export const registerStatusRoute = ({
|
|||
{
|
||||
path: '/api/status',
|
||||
options: {
|
||||
authRequired: !config.allowAnonymous,
|
||||
authRequired: 'optional',
|
||||
tags: ['api'], // ensures that unauthenticated calls receive a 401 rather than a 302 redirect to login page
|
||||
},
|
||||
validate: {
|
||||
|
@ -88,60 +101,107 @@ export const registerStatusRoute = ({
|
|||
},
|
||||
},
|
||||
async (context, req, res) => {
|
||||
const { version, buildSha, buildNum, buildDate } = config.packageInfo;
|
||||
const versionWithoutSnapshot = version.replace(SNAPSHOT_POSTFIX, '');
|
||||
const authRequired = !config.allowAnonymous;
|
||||
const isAuthenticated = req.auth.isAuthenticated;
|
||||
const redactedStatus = authRequired && !isAuthenticated;
|
||||
const [overall, coreOverall, core, plugins] = await firstValueFrom(combinedStatus$);
|
||||
|
||||
const { v8format = true, v7format = false } = req.query ?? {};
|
||||
|
||||
let statusInfo: StatusInfo | LegacyStatusInfo;
|
||||
if (!v7format && v8format) {
|
||||
statusInfo = {
|
||||
overall,
|
||||
core,
|
||||
plugins,
|
||||
};
|
||||
} else {
|
||||
incrementUsageCounter({ counterName: 'status_v7format' });
|
||||
statusInfo = calculateLegacyStatus({
|
||||
overall,
|
||||
core,
|
||||
plugins,
|
||||
versionWithoutSnapshot,
|
||||
});
|
||||
}
|
||||
|
||||
const lastMetrics = await firstValueFrom(metrics.getOpsMetrics$());
|
||||
|
||||
const body: StatusHttpBody = {
|
||||
name: config.serverName,
|
||||
uuid: config.uuid,
|
||||
version: {
|
||||
number: versionWithoutSnapshot,
|
||||
build_hash: buildSha,
|
||||
build_number: buildNum,
|
||||
build_snapshot: SNAPSHOT_POSTFIX.test(version),
|
||||
build_date: buildDate.toISOString(),
|
||||
},
|
||||
status: statusInfo,
|
||||
metrics: {
|
||||
last_updated: lastMetrics.collected_at.toISOString(),
|
||||
collection_interval_in_millis: metrics.collectionInterval,
|
||||
os: lastMetrics.os,
|
||||
process: lastMetrics.process,
|
||||
processes: lastMetrics.processes,
|
||||
response_times: lastMetrics.response_times,
|
||||
concurrent_connections: lastMetrics.concurrent_connections,
|
||||
requests: {
|
||||
...lastMetrics.requests,
|
||||
status_codes: lastMetrics.requests.statusCodes,
|
||||
},
|
||||
elasticsearch_client: lastMetrics.elasticsearch_client,
|
||||
},
|
||||
};
|
||||
const responseBody = redactedStatus
|
||||
? getRedactedStatusResponse({ coreOverall })
|
||||
: await getFullStatusResponse({
|
||||
incrementUsageCounter,
|
||||
config,
|
||||
query: req.query,
|
||||
metrics,
|
||||
statuses: { overall, core, plugins },
|
||||
});
|
||||
|
||||
const statusCode = coreOverall.level >= ServiceStatusLevels.unavailable ? 503 : 200;
|
||||
return res.custom({ body, statusCode, bypassErrorFormat: true });
|
||||
return res.custom({ body: responseBody, statusCode, bypassErrorFormat: true });
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
const getFullStatusResponse = async ({
|
||||
config,
|
||||
incrementUsageCounter,
|
||||
metrics,
|
||||
statuses: { plugins, overall, core },
|
||||
query: { v7format = false, v8format = true },
|
||||
}: {
|
||||
config: Deps['config'];
|
||||
incrementUsageCounter: CoreIncrementUsageCounter;
|
||||
metrics: MetricsServiceSetup;
|
||||
statuses: {
|
||||
overall: ServiceStatus<unknown>;
|
||||
core: CoreStatus;
|
||||
plugins: Record<string, ServiceStatus<unknown>>;
|
||||
};
|
||||
query: { v8format?: boolean; v7format?: boolean };
|
||||
}): Promise<StatusHttpBody> => {
|
||||
const { version, buildSha, buildNum, buildDate } = config.packageInfo;
|
||||
const versionWithoutSnapshot = version.replace(SNAPSHOT_POSTFIX, '');
|
||||
|
||||
let statusInfo: StatusInfo | LegacyStatusInfo;
|
||||
if (!v7format && v8format) {
|
||||
statusInfo = {
|
||||
overall,
|
||||
core,
|
||||
plugins,
|
||||
};
|
||||
} else {
|
||||
incrementUsageCounter({ counterName: 'status_v7format' });
|
||||
statusInfo = calculateLegacyStatus({
|
||||
overall,
|
||||
core,
|
||||
plugins,
|
||||
versionWithoutSnapshot,
|
||||
});
|
||||
}
|
||||
|
||||
const lastMetrics = await firstValueFrom(metrics.getOpsMetrics$());
|
||||
|
||||
const body: StatusHttpBody = {
|
||||
name: config.serverName,
|
||||
uuid: config.uuid,
|
||||
version: {
|
||||
number: versionWithoutSnapshot,
|
||||
build_hash: buildSha,
|
||||
build_number: buildNum,
|
||||
build_snapshot: SNAPSHOT_POSTFIX.test(version),
|
||||
build_date: buildDate.toISOString(),
|
||||
},
|
||||
status: statusInfo,
|
||||
metrics: {
|
||||
last_updated: lastMetrics.collected_at.toISOString(),
|
||||
collection_interval_in_millis: metrics.collectionInterval,
|
||||
os: lastMetrics.os,
|
||||
process: lastMetrics.process,
|
||||
processes: lastMetrics.processes,
|
||||
response_times: lastMetrics.response_times,
|
||||
concurrent_connections: lastMetrics.concurrent_connections,
|
||||
requests: {
|
||||
...lastMetrics.requests,
|
||||
status_codes: lastMetrics.requests.statusCodes,
|
||||
},
|
||||
elasticsearch_client: lastMetrics.elasticsearch_client,
|
||||
},
|
||||
};
|
||||
|
||||
return body;
|
||||
};
|
||||
|
||||
const getRedactedStatusResponse = ({
|
||||
coreOverall,
|
||||
}: {
|
||||
coreOverall: ServiceStatus;
|
||||
}): RedactedStatusHttpBody => {
|
||||
const body: RedactedStatusHttpBody = {
|
||||
status: {
|
||||
overall: {
|
||||
level: coreOverall.level,
|
||||
},
|
||||
},
|
||||
};
|
||||
return body;
|
||||
};
|
||||
|
|
|
@ -0,0 +1,38 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import type { IRouter } from '@kbn/core-http-server';
|
||||
import { ServiceStatusLevels } from '@kbn/core-status-common';
|
||||
import type { RedactedStatusHttpBody } from './status';
|
||||
|
||||
export const registerPrebootStatusRoute = ({ router }: { router: IRouter }) => {
|
||||
router.get(
|
||||
{
|
||||
path: '/api/status',
|
||||
options: {
|
||||
authRequired: false,
|
||||
tags: ['api'],
|
||||
},
|
||||
validate: false,
|
||||
},
|
||||
async (context, req, res) => {
|
||||
const body: RedactedStatusHttpBody = {
|
||||
status: {
|
||||
overall: {
|
||||
level: ServiceStatusLevels.unavailable,
|
||||
},
|
||||
},
|
||||
};
|
||||
return res.custom({
|
||||
body,
|
||||
statusCode: 503,
|
||||
bypassErrorFormat: true,
|
||||
});
|
||||
}
|
||||
);
|
||||
};
|
|
@ -65,7 +65,32 @@ describe('StatusService', () => {
|
|||
};
|
||||
};
|
||||
|
||||
describe('setup', () => {
|
||||
describe('#preboot', () => {
|
||||
it('registers `status` route', async () => {
|
||||
const configService = configServiceMock.create();
|
||||
configService.atPath.mockReturnValue(new BehaviorSubject({ allowAnonymous: true }));
|
||||
service = new StatusService(mockCoreContext.create({ configService }));
|
||||
|
||||
const prebootRouterMock: RouterMock = mockRouter.create();
|
||||
const httpPreboot = httpServiceMock.createInternalPrebootContract();
|
||||
httpPreboot.registerRoutes.mockImplementation((path, callback) =>
|
||||
callback(prebootRouterMock)
|
||||
);
|
||||
await service.preboot({ http: httpPreboot });
|
||||
|
||||
expect(prebootRouterMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(prebootRouterMock.get).toHaveBeenCalledWith(
|
||||
{
|
||||
path: '/api/status',
|
||||
options: { authRequired: false, tags: ['api'] },
|
||||
validate: false,
|
||||
},
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('#setup', () => {
|
||||
describe('core$', () => {
|
||||
it('rolls up core status observables into single observable', async () => {
|
||||
const setup = await service.setup(
|
||||
|
@ -500,45 +525,6 @@ describe('StatusService', () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe('preboot status routes', () => {
|
||||
let prebootRouterMock: RouterMock;
|
||||
beforeEach(async () => {
|
||||
prebootRouterMock = mockRouter.create();
|
||||
});
|
||||
|
||||
it('does not register `status` route if anonymous access is not allowed', async () => {
|
||||
const httpSetup = httpServiceMock.createInternalSetupContract();
|
||||
httpSetup.registerPrebootRoutes.mockImplementation((path, callback) =>
|
||||
callback(prebootRouterMock)
|
||||
);
|
||||
await service.setup(setupDeps({ http: httpSetup }));
|
||||
|
||||
expect(prebootRouterMock.get).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('registers `status` route if anonymous access is allowed', async () => {
|
||||
const configService = configServiceMock.create();
|
||||
configService.atPath.mockReturnValue(new BehaviorSubject({ allowAnonymous: true }));
|
||||
service = new StatusService(mockCoreContext.create({ configService }));
|
||||
|
||||
const httpSetup = httpServiceMock.createInternalSetupContract();
|
||||
httpSetup.registerPrebootRoutes.mockImplementation((path, callback) =>
|
||||
callback(prebootRouterMock)
|
||||
);
|
||||
await service.setup(setupDeps({ http: httpSetup }));
|
||||
|
||||
expect(prebootRouterMock.get).toHaveBeenCalledTimes(1);
|
||||
expect(prebootRouterMock.get).toHaveBeenCalledWith(
|
||||
{
|
||||
path: '/api/status',
|
||||
options: { authRequired: false, tags: ['api'] },
|
||||
validate: expect.anything(),
|
||||
},
|
||||
expect.any(Function)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('analytics', () => {
|
||||
let analyticsMock: jest.Mocked<AnalyticsServiceSetup>;
|
||||
let setup: InternalStatusServiceSetup;
|
||||
|
|
|
@ -24,13 +24,16 @@ import type { CoreContext, CoreService } from '@kbn/core-base-server-internal';
|
|||
import type { PluginName } from '@kbn/core-base-common';
|
||||
import type { AnalyticsServiceSetup } from '@kbn/core-analytics-server';
|
||||
import type { InternalEnvironmentServiceSetup } from '@kbn/core-environment-server-internal';
|
||||
import type { InternalHttpServiceSetup } from '@kbn/core-http-server-internal';
|
||||
import type {
|
||||
InternalHttpServiceSetup,
|
||||
InternalHttpServicePreboot,
|
||||
} from '@kbn/core-http-server-internal';
|
||||
import type { InternalElasticsearchServiceSetup } from '@kbn/core-elasticsearch-server-internal';
|
||||
import type { InternalMetricsServiceSetup } from '@kbn/core-metrics-server-internal';
|
||||
import type { InternalSavedObjectsServiceSetup } from '@kbn/core-saved-objects-server-internal';
|
||||
import type { InternalCoreUsageDataSetup } from '@kbn/core-usage-data-base-server-internal';
|
||||
import type { ServiceStatus, CoreStatus } from '@kbn/core-status-common';
|
||||
import { registerStatusRoute } from './routes';
|
||||
import { registerStatusRoute, registerPrebootStatusRoute } from './routes';
|
||||
|
||||
import { statusConfig as config, StatusConfigType } from './status_config';
|
||||
import type { InternalStatusServiceSetup } from './types';
|
||||
|
@ -47,6 +50,10 @@ interface StatusAnalyticsPayload {
|
|||
overall_status_summary: string;
|
||||
}
|
||||
|
||||
export interface StatusServicePrebootDeps {
|
||||
http: InternalHttpServicePreboot;
|
||||
}
|
||||
|
||||
export interface StatusServiceSetupDeps {
|
||||
analytics: AnalyticsServiceSetup;
|
||||
elasticsearch: Pick<InternalElasticsearchServiceSetup, 'status$'>;
|
||||
|
@ -72,6 +79,12 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
|
|||
this.config$ = coreContext.configService.atPath<StatusConfigType>(config.path);
|
||||
}
|
||||
|
||||
public async preboot({ http }: StatusServicePrebootDeps) {
|
||||
http.registerRoutes('', (router) => {
|
||||
registerPrebootStatusRoute({ router });
|
||||
});
|
||||
}
|
||||
|
||||
public async setup({
|
||||
analytics,
|
||||
elasticsearch,
|
||||
|
@ -149,19 +162,6 @@ export class StatusService implements CoreService<InternalStatusServiceSetup> {
|
|||
...commonRouteDeps,
|
||||
});
|
||||
|
||||
if (commonRouteDeps.config.allowAnonymous) {
|
||||
http.registerPrebootRoutes('', (prebootRouter) => {
|
||||
registerStatusRoute({
|
||||
router: prebootRouter,
|
||||
...commonRouteDeps,
|
||||
config: {
|
||||
...commonRouteDeps.config,
|
||||
allowAnonymous: true,
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
core$,
|
||||
coreOverall$,
|
||||
|
|
|
@ -54,6 +54,7 @@ type StatusServiceContract = PublicMethodsOf<StatusService>;
|
|||
|
||||
const createMock = () => {
|
||||
const mocked: jest.Mocked<StatusServiceContract> = {
|
||||
preboot: jest.fn(),
|
||||
setup: jest.fn().mockReturnValue(createInternalSetupContractMock()),
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
import { esTestConfig } from '@kbn/test';
|
||||
import * as http from 'http';
|
||||
import supertest from 'supertest';
|
||||
import { firstValueFrom, ReplaySubject } from 'rxjs';
|
||||
|
||||
import { Root } from '@kbn/core-root-server-internal';
|
||||
import {
|
||||
|
@ -17,6 +17,8 @@ import {
|
|||
type TestElasticsearchUtils,
|
||||
type TestKibanaUtils,
|
||||
} from '@kbn/core-test-helpers-kbn-server';
|
||||
import { ServiceStatus } from '@kbn/core-status-common';
|
||||
import { ElasticsearchStatusMeta } from '@kbn/core-elasticsearch-server-internal';
|
||||
|
||||
describe('elasticsearch clients', () => {
|
||||
let esServer: TestElasticsearchUtils;
|
||||
|
@ -71,15 +73,17 @@ function createFakeElasticsearchServer() {
|
|||
describe('fake elasticsearch', () => {
|
||||
let esServer: http.Server;
|
||||
let kibanaServer: Root;
|
||||
let kibanaHttpServer: http.Server;
|
||||
let esStatus$: ReplaySubject<ServiceStatus<ElasticsearchStatusMeta>>;
|
||||
|
||||
beforeAll(async () => {
|
||||
kibanaServer = createRootWithCorePlugins({ status: { allowAnonymous: true } });
|
||||
esServer = createFakeElasticsearchServer();
|
||||
|
||||
const kibanaPreboot = await kibanaServer.preboot();
|
||||
kibanaHttpServer = kibanaPreboot.http.server.listener; // Mind that we are using the prebootServer at this point because the migration gets hanging, while waiting for ES to be correct
|
||||
await kibanaServer.setup();
|
||||
await kibanaServer.preboot();
|
||||
const { elasticsearch } = await kibanaServer.setup();
|
||||
esStatus$ = new ReplaySubject(1);
|
||||
elasticsearch.status$.subscribe(esStatus$);
|
||||
|
||||
// give kibanaServer's status Observables enough time to bootstrap
|
||||
// and emit a status after the initial "unavailable: Waiting for Elasticsearch"
|
||||
// see https://github.com/elastic/kibana/issues/129754
|
||||
|
@ -94,9 +98,9 @@ describe('fake elasticsearch', () => {
|
|||
});
|
||||
|
||||
test('should return unknown product when it cannot perform the Product check (503 response)', async () => {
|
||||
const resp = await supertest(kibanaHttpServer).get('/api/status').expect(503);
|
||||
expect(resp.body.status.overall.level).toBe('critical');
|
||||
expect(resp.body.status.core.elasticsearch.summary).toBe(
|
||||
const esStatus = await firstValueFrom(esStatus$);
|
||||
expect(esStatus.level.toString()).toBe('critical');
|
||||
expect(esStatus.summary).toBe(
|
||||
'Unable to retrieve version information from Elasticsearch nodes. The client noticed that the server is not Elasticsearch and we do not support this unknown product.'
|
||||
);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0 and the Server Side Public License, v 1; you may not use this file except
|
||||
* in compliance with, at your election, the Elastic License 2.0 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import supertest from 'supertest';
|
||||
import { createCoreContext, createHttpServer } from '@kbn/core-http-server-mocks';
|
||||
import type { HttpService, InternalHttpServicePreboot } from '@kbn/core-http-server-internal';
|
||||
import { contextServiceMock } from '@kbn/core-http-context-server-mocks';
|
||||
|
||||
import { registerPrebootStatusRoute } from '@kbn/core-status-server-internal/src/routes';
|
||||
|
||||
const coreId = Symbol('core');
|
||||
|
||||
describe('GET /api/status', () => {
|
||||
let server: HttpService;
|
||||
let httpPreboot: InternalHttpServicePreboot;
|
||||
|
||||
const setupServer = async () => {
|
||||
const coreContext = createCoreContext({ coreId });
|
||||
|
||||
server = createHttpServer(coreContext);
|
||||
httpPreboot = await server.preboot({
|
||||
context: contextServiceMock.createPrebootContract(),
|
||||
});
|
||||
|
||||
httpPreboot.registerRoutes('', (router) => {
|
||||
registerPrebootStatusRoute({ router });
|
||||
});
|
||||
};
|
||||
|
||||
afterEach(async () => {
|
||||
await server.stop();
|
||||
});
|
||||
|
||||
it('respond with a 503 and with redacted status', async () => {
|
||||
await setupServer();
|
||||
const response = await supertest(httpPreboot.server.listener).get('/api/status').expect(503);
|
||||
expect(response.body).toEqual({ status: { overall: { level: 'unavailable' } } });
|
||||
});
|
||||
});
|
|
@ -19,7 +19,6 @@ import { ServiceStatus, ServiceStatusLevels, ServiceStatusLevel } from '@kbn/cor
|
|||
import { statusServiceMock } from '@kbn/core-status-server-mocks';
|
||||
import { executionContextServiceMock } from '@kbn/core-execution-context-server-mocks';
|
||||
import { contextServiceMock } from '@kbn/core-http-context-server-mocks';
|
||||
|
||||
import { registerStatusRoute } from '@kbn/core-status-server-internal';
|
||||
|
||||
const coreId = Symbol('core');
|
||||
|
@ -40,7 +39,12 @@ describe('GET /api/status', () => {
|
|||
const setupServer = async ({
|
||||
allowAnonymous = true,
|
||||
coreOverall,
|
||||
}: { allowAnonymous?: boolean; coreOverall?: ServiceStatus } = {}) => {
|
||||
overall,
|
||||
}: {
|
||||
allowAnonymous?: boolean;
|
||||
coreOverall?: ServiceStatus;
|
||||
overall?: ServiceStatus;
|
||||
} = {}) => {
|
||||
const coreContext = createCoreContext({ coreId });
|
||||
const contextService = new ContextService(coreContext);
|
||||
|
||||
|
@ -57,6 +61,9 @@ describe('GET /api/status', () => {
|
|||
if (coreOverall) {
|
||||
status.coreOverall$ = new BehaviorSubject(coreOverall);
|
||||
}
|
||||
if (overall) {
|
||||
status.overall$ = new BehaviorSubject(overall);
|
||||
}
|
||||
|
||||
const pluginsStatus$ = new BehaviorSubject<Record<string, ServiceStatus>>({
|
||||
a: { level: ServiceStatusLevels.available, summary: 'a is available' },
|
||||
|
@ -110,284 +117,342 @@ describe('GET /api/status', () => {
|
|||
});
|
||||
|
||||
describe('allowAnonymous: false', () => {
|
||||
it('rejects requests with no credentials', async () => {
|
||||
await setupServer({ allowAnonymous: false });
|
||||
await supertest(httpSetup.server.listener).get('/api/status').expect(401);
|
||||
describe('http response code', () => {
|
||||
it('respond with a 200 when core.overall.status is available', async () => {
|
||||
await setupServer({
|
||||
allowAnonymous: false,
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.available),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status').expect(200);
|
||||
});
|
||||
it('respond with a 200 when core.overall.status is degraded', async () => {
|
||||
await setupServer({
|
||||
allowAnonymous: false,
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.degraded),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status').expect(200);
|
||||
});
|
||||
it('respond with a 503 when core.overall.status is unavailable', async () => {
|
||||
await setupServer({
|
||||
allowAnonymous: false,
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.unavailable),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status').expect(503);
|
||||
});
|
||||
it('respond with a 503 when core.overall.status is critical', async () => {
|
||||
await setupServer({
|
||||
allowAnonymous: false,
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.critical),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status').expect(503);
|
||||
});
|
||||
it('does not depend on the overall status', async () => {
|
||||
await setupServer({
|
||||
allowAnonymous: false,
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.available),
|
||||
overall: createServiceStatus(ServiceStatusLevels.critical),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status').expect(200);
|
||||
});
|
||||
});
|
||||
|
||||
it('rejects requests with bad credentials', async () => {
|
||||
await setupServer({ allowAnonymous: false });
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/status')
|
||||
.set('Authorization', 'fake creds')
|
||||
.expect(401);
|
||||
});
|
||||
describe('response payload', () => {
|
||||
it('returns redacted body for requests with no credentials', async () => {
|
||||
await setupServer({
|
||||
allowAnonymous: false,
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.available),
|
||||
});
|
||||
const response = await supertest(httpSetup.server.listener).get('/api/status').expect(200);
|
||||
expect(response.body).toEqual({ status: { overall: { level: 'available' } } });
|
||||
});
|
||||
|
||||
it('accepts authenticated requests', async () => {
|
||||
await setupServer({ allowAnonymous: false });
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/status')
|
||||
.set('Authorization', 'let me in')
|
||||
.expect(200);
|
||||
it('returns redacted body for requests with bad credentials', async () => {
|
||||
await setupServer({
|
||||
allowAnonymous: false,
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.available),
|
||||
});
|
||||
const response = await supertest(httpSetup.server.listener)
|
||||
.get('/api/status')
|
||||
.set('Authorization', 'fake creds')
|
||||
.expect(200);
|
||||
expect(response.body).toEqual({ status: { overall: { level: 'available' } } });
|
||||
});
|
||||
|
||||
it('returns full body for authenticated requests', async () => {
|
||||
await setupServer({ allowAnonymous: false });
|
||||
const response = await supertest(httpSetup.server.listener)
|
||||
.get('/api/status')
|
||||
.set('Authorization', 'let me in')
|
||||
.expect(200);
|
||||
expect(response.body).toEqual(
|
||||
expect.objectContaining({
|
||||
name: expect.any(String),
|
||||
status: expect.any(Object),
|
||||
metrics: expect.any(Object),
|
||||
})
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('returns basic server info & metrics', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200);
|
||||
|
||||
expect(result.body.name).toEqual('xkibana');
|
||||
expect(result.body.uuid).toEqual('xxxx-xxxxx');
|
||||
expect(result.body.version).toEqual({
|
||||
number: '9.9.9',
|
||||
build_hash: 'xsha',
|
||||
build_number: 1234,
|
||||
build_snapshot: true,
|
||||
build_date: new Date('2023-05-15T23:12:09.000Z').toISOString(),
|
||||
});
|
||||
const metricsMockValue = await firstValueFrom(metrics.getOpsMetrics$());
|
||||
expect(result.body.metrics).toEqual({
|
||||
last_updated: expect.any(String),
|
||||
collection_interval_in_millis: metrics.collectionInterval,
|
||||
...omit(metricsMockValue, ['collected_at']),
|
||||
requests: {
|
||||
...metricsMockValue.requests,
|
||||
status_codes: metricsMockValue.requests.statusCodes,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('legacy status format', () => {
|
||||
const legacyFormat = {
|
||||
overall: {
|
||||
icon: 'success',
|
||||
nickname: 'Looking good',
|
||||
since: expect.any(String),
|
||||
state: 'green',
|
||||
title: 'Green',
|
||||
uiColor: 'success',
|
||||
},
|
||||
statuses: [
|
||||
{
|
||||
icon: 'success',
|
||||
id: 'core:elasticsearch@9.9.9',
|
||||
message: 'Service is working',
|
||||
since: expect.any(String),
|
||||
state: 'green',
|
||||
uiColor: 'success',
|
||||
},
|
||||
{
|
||||
icon: 'success',
|
||||
id: 'core:savedObjects@9.9.9',
|
||||
message: 'Service is working',
|
||||
since: expect.any(String),
|
||||
state: 'green',
|
||||
uiColor: 'success',
|
||||
},
|
||||
{
|
||||
icon: 'success',
|
||||
id: 'plugin:a@9.9.9',
|
||||
message: 'a is available',
|
||||
since: expect.any(String),
|
||||
state: 'green',
|
||||
uiColor: 'success',
|
||||
},
|
||||
{
|
||||
icon: 'warning',
|
||||
id: 'plugin:b@9.9.9',
|
||||
message: 'b is degraded',
|
||||
since: expect.any(String),
|
||||
state: 'yellow',
|
||||
uiColor: 'warning',
|
||||
},
|
||||
{
|
||||
icon: 'danger',
|
||||
id: 'plugin:c@9.9.9',
|
||||
message: 'c is unavailable',
|
||||
since: expect.any(String),
|
||||
state: 'red',
|
||||
uiColor: 'danger',
|
||||
},
|
||||
{
|
||||
icon: 'danger',
|
||||
id: 'plugin:d@9.9.9',
|
||||
message: 'd is critical',
|
||||
since: expect.any(String),
|
||||
state: 'red',
|
||||
uiColor: 'danger',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('returns legacy status format when v7format=true is provided', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v7format=true')
|
||||
.expect(200);
|
||||
expect(result.body.status).toEqual(legacyFormat);
|
||||
expect(incrementUsageCounter).toHaveBeenCalledTimes(1);
|
||||
expect(incrementUsageCounter).toHaveBeenCalledWith({ counterName: 'status_v7format' });
|
||||
});
|
||||
|
||||
it('returns legacy status format when v8format=false is provided', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=false')
|
||||
.expect(200);
|
||||
expect(result.body.status).toEqual(legacyFormat);
|
||||
expect(incrementUsageCounter).toHaveBeenCalledTimes(1);
|
||||
expect(incrementUsageCounter).toHaveBeenCalledWith({ counterName: 'status_v7format' });
|
||||
});
|
||||
});
|
||||
|
||||
describe('v8format', () => {
|
||||
const newFormat = {
|
||||
core: {
|
||||
elasticsearch: {
|
||||
level: 'available',
|
||||
summary: 'Service is working',
|
||||
},
|
||||
savedObjects: {
|
||||
level: 'available',
|
||||
summary: 'Service is working',
|
||||
},
|
||||
},
|
||||
overall: {
|
||||
level: 'available',
|
||||
summary: 'Service is working',
|
||||
},
|
||||
plugins: {
|
||||
a: {
|
||||
level: 'available',
|
||||
summary: 'a is available',
|
||||
},
|
||||
b: {
|
||||
level: 'degraded',
|
||||
summary: 'b is degraded',
|
||||
},
|
||||
c: {
|
||||
level: 'unavailable',
|
||||
summary: 'c is unavailable',
|
||||
},
|
||||
d: {
|
||||
level: 'critical',
|
||||
summary: 'd is critical',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('returns new status format when no query params are provided', async () => {
|
||||
describe('allowAnonymous: true', () => {
|
||||
it('returns basic server info & metrics', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200);
|
||||
expect(result.body.status).toEqual(newFormat);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns new status format when v8format=true is provided', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=true')
|
||||
.expect(200);
|
||||
expect(result.body.status).toEqual(newFormat);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns new status format when v7format=false is provided', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v7format=false')
|
||||
.expect(200);
|
||||
expect(result.body.status).toEqual(newFormat);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid query parameters', () => {
|
||||
it('v8format=true and v7format=true', async () => {
|
||||
await setupServer();
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=true&v7format=true')
|
||||
.expect(400);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('v8format=true and v7format=false', async () => {
|
||||
await setupServer();
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=true&v7format=false')
|
||||
.expect(400);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('v8format=false and v7format=false', async () => {
|
||||
await setupServer();
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=false&v7format=false')
|
||||
.expect(400);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('v8format=false and v7format=true', async () => {
|
||||
await setupServer();
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=false&v7format=true')
|
||||
.expect(400);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('status level and http response code', () => {
|
||||
describe('using standard format', () => {
|
||||
it('respond with a 200 when core.overall.status is available', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.available),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v8format=true').expect(200);
|
||||
expect(result.body.name).toEqual('xkibana');
|
||||
expect(result.body.uuid).toEqual('xxxx-xxxxx');
|
||||
expect(result.body.version).toEqual({
|
||||
number: '9.9.9',
|
||||
build_hash: 'xsha',
|
||||
build_number: 1234,
|
||||
build_snapshot: true,
|
||||
build_date: new Date('2023-05-15T23:12:09.000Z').toISOString(),
|
||||
});
|
||||
it('respond with a 200 when core.overall.status is degraded', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.degraded),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v8format=true').expect(200);
|
||||
});
|
||||
it('respond with a 503 when core.overall.status is unavailable', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.unavailable),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v8format=true').expect(503);
|
||||
});
|
||||
it('respond with a 503 when core.overall.status is critical', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.critical),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v8format=true').expect(503);
|
||||
const metricsMockValue = await firstValueFrom(metrics.getOpsMetrics$());
|
||||
expect(result.body.metrics).toEqual({
|
||||
last_updated: expect.any(String),
|
||||
collection_interval_in_millis: metrics.collectionInterval,
|
||||
...omit(metricsMockValue, ['collected_at']),
|
||||
requests: {
|
||||
...metricsMockValue.requests,
|
||||
status_codes: metricsMockValue.requests.statusCodes,
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
describe('using legacy format', () => {
|
||||
it('respond with a 200 when core.overall.status is available', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.available),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v7format=true').expect(200);
|
||||
describe('legacy status format', () => {
|
||||
const legacyFormat = {
|
||||
overall: {
|
||||
icon: 'success',
|
||||
nickname: 'Looking good',
|
||||
since: expect.any(String),
|
||||
state: 'green',
|
||||
title: 'Green',
|
||||
uiColor: 'success',
|
||||
},
|
||||
statuses: [
|
||||
{
|
||||
icon: 'success',
|
||||
id: 'core:elasticsearch@9.9.9',
|
||||
message: 'Service is working',
|
||||
since: expect.any(String),
|
||||
state: 'green',
|
||||
uiColor: 'success',
|
||||
},
|
||||
{
|
||||
icon: 'success',
|
||||
id: 'core:savedObjects@9.9.9',
|
||||
message: 'Service is working',
|
||||
since: expect.any(String),
|
||||
state: 'green',
|
||||
uiColor: 'success',
|
||||
},
|
||||
{
|
||||
icon: 'success',
|
||||
id: 'plugin:a@9.9.9',
|
||||
message: 'a is available',
|
||||
since: expect.any(String),
|
||||
state: 'green',
|
||||
uiColor: 'success',
|
||||
},
|
||||
{
|
||||
icon: 'warning',
|
||||
id: 'plugin:b@9.9.9',
|
||||
message: 'b is degraded',
|
||||
since: expect.any(String),
|
||||
state: 'yellow',
|
||||
uiColor: 'warning',
|
||||
},
|
||||
{
|
||||
icon: 'danger',
|
||||
id: 'plugin:c@9.9.9',
|
||||
message: 'c is unavailable',
|
||||
since: expect.any(String),
|
||||
state: 'red',
|
||||
uiColor: 'danger',
|
||||
},
|
||||
{
|
||||
icon: 'danger',
|
||||
id: 'plugin:d@9.9.9',
|
||||
message: 'd is critical',
|
||||
since: expect.any(String),
|
||||
state: 'red',
|
||||
uiColor: 'danger',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
it('returns legacy status format when v7format=true is provided', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v7format=true')
|
||||
.expect(200);
|
||||
expect(result.body.status).toEqual(legacyFormat);
|
||||
expect(incrementUsageCounter).toHaveBeenCalledTimes(1);
|
||||
expect(incrementUsageCounter).toHaveBeenCalledWith({ counterName: 'status_v7format' });
|
||||
});
|
||||
it('respond with a 200 when core.overall.status is degraded', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.degraded),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v7format=true').expect(200);
|
||||
|
||||
it('returns legacy status format when v8format=false is provided', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=false')
|
||||
.expect(200);
|
||||
expect(result.body.status).toEqual(legacyFormat);
|
||||
expect(incrementUsageCounter).toHaveBeenCalledTimes(1);
|
||||
expect(incrementUsageCounter).toHaveBeenCalledWith({ counterName: 'status_v7format' });
|
||||
});
|
||||
it('respond with a 503 when core.overall.status is unavailable', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.unavailable),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v7format=true').expect(503);
|
||||
});
|
||||
|
||||
describe('v8format', () => {
|
||||
const newFormat = {
|
||||
core: {
|
||||
elasticsearch: {
|
||||
level: 'available',
|
||||
summary: 'Service is working',
|
||||
},
|
||||
savedObjects: {
|
||||
level: 'available',
|
||||
summary: 'Service is working',
|
||||
},
|
||||
},
|
||||
overall: {
|
||||
level: 'available',
|
||||
summary: 'Service is working',
|
||||
},
|
||||
plugins: {
|
||||
a: {
|
||||
level: 'available',
|
||||
summary: 'a is available',
|
||||
},
|
||||
b: {
|
||||
level: 'degraded',
|
||||
summary: 'b is degraded',
|
||||
},
|
||||
c: {
|
||||
level: 'unavailable',
|
||||
summary: 'c is unavailable',
|
||||
},
|
||||
d: {
|
||||
level: 'critical',
|
||||
summary: 'd is critical',
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
it('returns new status format when no query params are provided', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener).get('/api/status').expect(200);
|
||||
expect(result.body.status).toEqual(newFormat);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
it('respond with a 503 when core.overall.status is critical', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.critical),
|
||||
|
||||
it('returns new status format when v8format=true is provided', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=true')
|
||||
.expect(200);
|
||||
expect(result.body.status).toEqual(newFormat);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('returns new status format when v7format=false is provided', async () => {
|
||||
await setupServer();
|
||||
const result = await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v7format=false')
|
||||
.expect(200);
|
||||
expect(result.body.status).toEqual(newFormat);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('invalid query parameters', () => {
|
||||
it('v8format=true and v7format=true', async () => {
|
||||
await setupServer();
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=true&v7format=true')
|
||||
.expect(400);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('v8format=true and v7format=false', async () => {
|
||||
await setupServer();
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=true&v7format=false')
|
||||
.expect(400);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('v8format=false and v7format=false', async () => {
|
||||
await setupServer();
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=false&v7format=false')
|
||||
.expect(400);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('v8format=false and v7format=true', async () => {
|
||||
await setupServer();
|
||||
await supertest(httpSetup.server.listener)
|
||||
.get('/api/status?v8format=false&v7format=true')
|
||||
.expect(400);
|
||||
expect(incrementUsageCounter).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
describe('status level and http response code', () => {
|
||||
describe('using standard format', () => {
|
||||
it('respond with a 200 when core.overall.status is available', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.available),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v8format=true').expect(200);
|
||||
});
|
||||
it('respond with a 200 when core.overall.status is degraded', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.degraded),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v8format=true').expect(200);
|
||||
});
|
||||
it('respond with a 503 when core.overall.status is unavailable', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.unavailable),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v8format=true').expect(503);
|
||||
});
|
||||
it('respond with a 503 when core.overall.status is critical', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.critical),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v8format=true').expect(503);
|
||||
});
|
||||
});
|
||||
|
||||
describe('using legacy format', () => {
|
||||
it('respond with a 200 when core.overall.status is available', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.available),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v7format=true').expect(200);
|
||||
});
|
||||
it('respond with a 200 when core.overall.status is degraded', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.degraded),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v7format=true').expect(200);
|
||||
});
|
||||
it('respond with a 503 when core.overall.status is unavailable', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.unavailable),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v7format=true').expect(503);
|
||||
});
|
||||
it('respond with a 503 when core.overall.status is critical', async () => {
|
||||
await setupServer({
|
||||
coreOverall: createServiceStatus(ServiceStatusLevels.critical),
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v7format=true').expect(503);
|
||||
});
|
||||
await supertest(httpSetup.server.listener).get('/api/status?v7format=true').expect(503);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
24
x-pack/test/api_integration/apis/status/config.ts
Normal file
24
x-pack/test/api_integration/apis/status/config.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrConfigProviderContext } from '@kbn/test';
|
||||
|
||||
export default async function ({ readConfigFile }: FtrConfigProviderContext) {
|
||||
const baseIntegrationTestsConfig = await readConfigFile(require.resolve('../../config.ts'));
|
||||
|
||||
return {
|
||||
...baseIntegrationTestsConfig.getAll(),
|
||||
kbnTestServer: {
|
||||
...baseIntegrationTestsConfig.get('kbnTestServer'),
|
||||
serverArgs: [
|
||||
...baseIntegrationTestsConfig.get('kbnTestServer.serverArgs'),
|
||||
'--status.allowAnonymous=false',
|
||||
],
|
||||
},
|
||||
testFiles: [require.resolve('.')],
|
||||
};
|
||||
}
|
14
x-pack/test/api_integration/apis/status/index.ts
Normal file
14
x-pack/test/api_integration/apis/status/index.ts
Normal file
|
@ -0,0 +1,14 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ loadTestFile }: FtrProviderContext) {
|
||||
describe('Status API', () => {
|
||||
loadTestFile(require.resolve('./status'));
|
||||
});
|
||||
}
|
41
x-pack/test/api_integration/apis/status/status.ts
Normal file
41
x-pack/test/api_integration/apis/status/status.ts
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import expect from '@kbn/expect';
|
||||
|
||||
import { FtrProviderContext } from '../../ftr_provider_context';
|
||||
|
||||
export default function ({ getService }: FtrProviderContext) {
|
||||
const supertest = getService('supertest');
|
||||
const supertestWithoutAuth = getService('supertestWithoutAuth');
|
||||
|
||||
describe('GET /api/status', () => {
|
||||
describe('When status.allowAnonymous is false', () => {
|
||||
it('returns full status payload for authenticated requests', async () => {
|
||||
const { body } = await supertest.get('/api/status').set('kbn-xsrf', 'kibana');
|
||||
|
||||
expect(body.name).to.be.a('string');
|
||||
expect(body.uuid).to.be.a('string');
|
||||
expect(body.version.number).to.be.a('string');
|
||||
|
||||
expect(body.status.overall).to.be.an('object');
|
||||
expect(body.status.core).to.be.an('object');
|
||||
expect(body.status.plugins).to.be.an('object');
|
||||
});
|
||||
it('returns redacted payload for unauthenticated requests', async () => {
|
||||
const { body } = await supertestWithoutAuth.get('/api/status').set('kbn-xsrf', 'kibana');
|
||||
|
||||
expect(Object.keys(body)).to.eql(['status']);
|
||||
expect(body.status).to.be.an('object');
|
||||
expect(Object.keys(body.status)).to.eql(['overall']);
|
||||
expect(body.status.overall).to.be.an('object');
|
||||
expect(Object.keys(body.status.overall)).to.eql(['level']);
|
||||
expect(body.status.overall.level).to.be.a('string');
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue