[APM] Api test for feature flags in serverless (#162128)

Closes https://github.com/elastic/kibana/issues/159020

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Achyut Jhunjhunwala <achyut.jhunjhunwala@elastic.co>
This commit is contained in:
Miriam 2023-07-28 10:06:12 +01:00 committed by GitHub
parent 45f7a0b2fc
commit 0706dd3b1a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 400 additions and 2 deletions

View file

@ -111,7 +111,6 @@ const getMigrationCheckRoute = createApmServerRoute({
options: { tags: ['access:apm'] },
handler: async (resources): Promise<RunMigrationCheckResponse> => {
const { core, plugins, context, config, request } = resources;
throwNotFoundIfFleetMigrationNotAvailable(resources.featureFlags);
const { fleet, security } = plugins;

View file

@ -15,7 +15,11 @@ export function createTestConfig(options: CreateTestConfigOptions) {
return {
...svlSharedConfig.getAll(),
services,
services: {
...services,
...options.services,
},
kbnTestServer: {
...svlSharedConfig.get('kbnTestServer'),
serverArgs: [

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import { GenericFtrProviderContext } from '@kbn/test';
// eslint-disable-next-line @kbn/imports/no_boundary_crossing
import { services as xpackApiIntegrationServices } from '../../../test/api_integration/services';
import { services as svlSharedServices } from '../../shared/services';
@ -17,3 +18,12 @@ export const services = {
svlCommonApi: SvlCommonApiServiceProvider,
};
export type InheritedFtrProviderContext = GenericFtrProviderContext<typeof services, {}>;
export type InheritedServices = InheritedFtrProviderContext extends GenericFtrProviderContext<
infer TServices,
{}
>
? TServices
: {};

View file

@ -0,0 +1,131 @@
/*
* 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 {
ApmUsername,
APM_TEST_PASSWORD,
// eslint-disable-next-line @kbn/imports/no_boundary_crossing
} from '@kbn/apm-plugin/server/test_helpers/create_apm_users/authentication';
import { format, UrlObject } from 'url';
import supertest from 'supertest';
import request from 'superagent';
import type {
APIReturnType,
APIClientRequestParamsOf,
} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import type { APIEndpoint } from '@kbn/apm-plugin/server';
import { formatRequest } from '@kbn/server-route-repository';
import { InheritedFtrProviderContext } from '../../../../services';
export function createApmApiClient(st: supertest.SuperTest<supertest.Test>) {
return async <TEndpoint extends APIEndpoint>(
options: {
type?: 'form-data';
endpoint: TEndpoint;
} & APIClientRequestParamsOf<TEndpoint> & { params?: { query?: { _inspect?: boolean } } }
): Promise<SupertestReturnType<TEndpoint>> => {
const { endpoint, type } = options;
const params = 'params' in options ? (options.params as Record<string, any>) : {};
const { method, pathname, version } = formatRequest(endpoint, params.path);
const url = format({ pathname, query: params?.query });
const headers: Record<string, string> = { 'kbn-xsrf': 'foo' };
if (version) {
headers['Elastic-Api-Version'] = version;
}
let res: request.Response;
if (type === 'form-data') {
const fields: Array<[string, any]> = Object.entries(params.body);
const formDataRequest = st[method](url)
.set(headers)
.set('Content-type', 'multipart/form-data');
for (const field of fields) {
formDataRequest.field(field[0], field[1]);
}
res = await formDataRequest;
} else if (params.body) {
res = await st[method](url).send(params.body).set(headers);
} else {
res = await st[method](url).set(headers);
}
// supertest doesn't throw on http errors
if (res?.status !== 200) {
throw new ApmApiError(res, endpoint);
}
return res;
};
}
type ApiErrorResponse = Omit<request.Response, 'body'> & {
body: {
statusCode: number;
error: string;
message: string;
attributes: object;
};
};
export type ApmApiSupertest = ReturnType<typeof createApmApiClient>;
export class ApmApiError extends Error {
res: ApiErrorResponse;
constructor(res: request.Response, endpoint: string) {
super(
`Unhandled ApmApiError.
Status: "${res.status}"
Endpoint: "${endpoint}"
Body: ${JSON.stringify(res.body)}`
);
this.res = res;
}
}
async function getApmApiClient({
kibanaServer,
username,
}: {
kibanaServer: UrlObject;
username: ApmUsername | 'elastic';
}) {
const url = format({
...kibanaServer,
auth: `${username}:${APM_TEST_PASSWORD}`,
});
return createApmApiClient(supertest(url));
}
export interface SupertestReturnType<TEndpoint extends APIEndpoint> {
status: number;
body: APIReturnType<TEndpoint>;
}
type ApmApiClientKey = 'slsUser';
export type ApmApiClient = Record<ApmApiClientKey, Awaited<ReturnType<typeof getApmApiClient>>>;
export async function getApmApiClientService({
getService,
}: InheritedFtrProviderContext): Promise<ApmApiClient> {
const svlSharedConfig = getService('config');
const kibanaServer = svlSharedConfig.get('servers.kibana');
return {
slsUser: await getApmApiClient({
kibanaServer,
username: 'elastic',
}),
};
}

View file

@ -0,0 +1,25 @@
/*
* 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 { GenericFtrProviderContext } from '@kbn/test';
import { ApmApiClient, getApmApiClientService } from './apm_api_supertest';
import {
InheritedServices,
InheritedFtrProviderContext,
services as inheritedServices,
} from '../../../../services';
export type APMServices = InheritedServices & {
apmApiClient: (context: InheritedFtrProviderContext) => Promise<ApmApiClient>;
};
export const services: APMServices = {
...inheritedServices,
apmApiClient: getApmApiClientService,
};
export type APMFtrContextProvider = GenericFtrProviderContext<typeof services, {}>;

View file

@ -0,0 +1,221 @@
/*
* 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 'expect';
import { APMFtrContextProvider } from './common/services';
const fleetMigrationResponse = {
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
attributes: {
data: null,
_inspect: [],
},
};
const agentConfigurationResponse = {
statusCode: 404,
error: 'Not Found',
message: 'Not Found',
attributes: {
data: null,
_inspect: [],
},
};
const sourceMapsResponse = {
statusCode: 501,
error: 'Not Implemented',
message: 'Not Implemented',
attributes: {
data: null,
_inspect: [],
},
};
const SAMPLE_SOURCEMAP = {
version: 3,
file: 'out.js',
sourceRoot: '',
sources: ['foo.js', 'bar.js'],
sourcesContent: ['', null],
names: ['src', 'maps', 'are', 'fun'],
mappings: 'A,AAAB;;ABCDE;',
};
async function uploadSourcemap(apmApiClient: any) {
const response = await apmApiClient.slsUser({
endpoint: 'POST /api/apm/sourcemaps 2023-10-31',
type: 'form-data',
params: {
body: {
service_name: 'uploading-test',
service_version: '1.0.0',
bundle_filepath: 'bar',
sourcemap: JSON.stringify(SAMPLE_SOURCEMAP),
},
},
});
return response.body;
}
export default function ({ getService }: APMFtrContextProvider) {
const apmApiClient = getService('apmApiClient');
describe('apm feature flags', () => {
describe('fleet migrations', () => {
it('rejects requests to save apm server schema', async () => {
try {
await apmApiClient.slsUser({
endpoint: 'POST /api/apm/fleet/apm_server_schema 2023-10-31',
params: {
body: {
schema: {
tail_sampling_enabled: true,
},
},
},
});
} catch (err) {
expect(err.res.status).toBe(fleetMigrationResponse.statusCode);
expect(err.res.body).toStrictEqual(fleetMigrationResponse);
}
});
it('rejects requests to get unsupported apm server schema', async () => {
try {
await apmApiClient.slsUser({
endpoint: 'GET /internal/apm/fleet/apm_server_schema/unsupported',
});
} catch (err) {
expect(err.res.status).toBe(fleetMigrationResponse.statusCode);
expect(err.res.body).toStrictEqual(fleetMigrationResponse);
}
});
it('rejects requests to get migration check', async () => {
try {
await apmApiClient.slsUser({
endpoint: 'GET /internal/apm/fleet/migration_check',
});
} catch (err) {
expect(err.res.status).toBe(fleetMigrationResponse.statusCode);
expect(err.res.body).toStrictEqual(fleetMigrationResponse);
}
});
});
describe('agent configuration', () => {
it('rejects requests to get agent configurations', async () => {
try {
await apmApiClient.slsUser({
endpoint: 'GET /api/apm/settings/agent-configuration 2023-10-31',
});
} catch (err) {
expect(err.res.status).toBe(agentConfigurationResponse.statusCode);
expect(err.res.body).toStrictEqual(agentConfigurationResponse);
}
});
it('rejects requests to get single agent configuration', async () => {
try {
await apmApiClient.slsUser({
endpoint: 'GET /api/apm/settings/agent-configuration/view 2023-10-31',
});
} catch (err) {
expect(err.res.status).toBe(agentConfigurationResponse.statusCode);
expect(err.res.body).toStrictEqual(agentConfigurationResponse);
}
});
it('rejects requests to delete agent configuration', async () => {
try {
await apmApiClient.slsUser({
endpoint: 'DELETE /api/apm/settings/agent-configuration 2023-10-31',
params: {
body: {
service: {},
},
},
});
} catch (err) {
expect(err.res.status).toBe(agentConfigurationResponse.statusCode);
expect(err.res.body).toStrictEqual(agentConfigurationResponse);
}
});
it('rejects requests to create/update agent configuration', async () => {
try {
await apmApiClient.slsUser({
endpoint: 'PUT /api/apm/settings/agent-configuration 2023-10-31',
params: {
body: {
service: {},
settings: { transaction_sample_rate: '0.55' },
},
},
});
} catch (err) {
expect(err.res.status).toBe(agentConfigurationResponse.statusCode);
expect(err.res.body).toStrictEqual(agentConfigurationResponse);
}
});
it('rejects requests to lookup single configuration', async () => {
try {
await apmApiClient.slsUser({
endpoint: 'POST /api/apm/settings/agent-configuration/search 2023-10-31',
params: {
body: {
service: { name: 'myservice', environment: 'development' },
etag: '7312bdcc34999629a3d39df24ed9b2a7553c0c39',
},
},
});
} catch (err) {
expect(err.res.status).toBe(agentConfigurationResponse.statusCode);
expect(err.res.body).toStrictEqual(agentConfigurationResponse);
}
});
});
describe('source maps', () => {
it('rejects requests to list source maps', async () => {
try {
await apmApiClient.slsUser({
endpoint: 'GET /api/apm/sourcemaps 2023-10-31',
});
} catch (err) {
expect(err.res.status).toBe(sourceMapsResponse.statusCode);
expect(err.res.body).toStrictEqual(sourceMapsResponse);
}
});
it('rejects requests to upload source maps', async () => {
try {
await uploadSourcemap(apmApiClient);
} catch (err) {
expect(err.res.status).toBe(sourceMapsResponse.statusCode);
expect(err.res.body).toStrictEqual(sourceMapsResponse);
}
});
it('rejects requests to delete source map', async () => {
try {
await apmApiClient.slsUser({
endpoint: 'DELETE /api/apm/sourcemaps/{id} 2023-10-31',
params: { path: { id: 'foo' } },
});
} catch (err) {
expect(err.res.status).toBe(sourceMapsResponse.statusCode);
expect(err.res.body).toStrictEqual(sourceMapsResponse);
}
});
});
});
}

View file

@ -6,6 +6,7 @@
*/
import { createTestConfig } from '../../config.base';
import { services } from './apm_api_integration/common/services';
export default createTestConfig({
serverlessProject: 'oblt',
@ -14,4 +15,5 @@ export default createTestConfig({
reportName: 'Serverless Observability API Integration Tests',
},
suiteTags: { exclude: ['skipSvlOblt'] },
services,
});

View file

@ -11,6 +11,7 @@ export default function ({ loadTestFile }: FtrProviderContext) {
describe('serverless observability API', function () {
loadTestFile(require.resolve('./fleet'));
loadTestFile(require.resolve('./snapshot_telemetry'));
loadTestFile(require.resolve('./apm_api_integration/feature_flags.ts'));
loadTestFile(require.resolve('./threshold_rule/avg_pct_fired'));
loadTestFile(require.resolve('./threshold_rule/avg_pct_no_data'));
loadTestFile(require.resolve('./threshold_rule/documents_count_fired'));

View file

@ -5,9 +5,12 @@
* 2.0.
*/
import { InheritedServices } from '../../api_integration/services';
export interface CreateTestConfigOptions {
serverlessProject: 'es' | 'oblt' | 'security';
testFiles: string[];
junit: { reportName: string };
suiteTags?: { include?: string[]; exclude?: string[] };
services?: InheritedServices;
}

View file

@ -30,6 +30,8 @@
"@kbn/observability-plugin",
"@kbn/infra-forge",
"@kbn/ftr-common-functional-services",
"@kbn/apm-plugin",
"@kbn/server-route-repository",
"@kbn/core-chrome-browser",
"@kbn/default-nav-ml",
"@kbn/default-nav-analytics",