Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[APM] Foundation for migrating APM tests to deployment agnostic approach #198775

Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -8,13 +8,14 @@ import expect from '@kbn/expect';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { RecursivePartial } from '@kbn/apm-plugin/typings/common';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import { keyBy } from 'lodash';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';

export default function ApiTest({ getService }: FtrProviderContext) {
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const apmSynthtraceEsClient = getService('apmSynthtraceEsClient');
const synthtrace = getService('synthtrace');

const start = new Date('2021-01-01T00:00:00.000Z').getTime();
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
Expand Down Expand Up @@ -42,7 +43,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
}

registry.when('Agent explorer when data is not loaded', { config: 'basic', archives: [] }, () => {
registry.when('Agent explorer when data is not loaded', () => {
crespocarlos marked this conversation as resolved.
Show resolved Hide resolved
it('handles empty state', async () => {
const { status, body } = await callApi();

Expand All @@ -51,9 +52,13 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
});

registry.when('Agent explorer', { config: 'basic', archives: [] }, () => {
registry.when('Agent explorer', () => {
describe('when data is loaded', () => {
let apmSynthtraceEsClient: ApmSynthtraceEsClient;

before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();

const serviceOtelJava = apm
.service({
name: otelJavaServiceName,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/*
* 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 { ElasticApmAgentLatestVersion } from '@kbn/apm-plugin/common/agent_explorer';
import expect from '@kbn/expect';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';

export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApi');

const nodeAgentName = 'nodejs';
const unlistedAgentName = 'unlistedAgent';

async function callApi() {
return await apmApiClient.readUser({
endpoint: 'GET /internal/apm/get_latest_agent_versions',
});
}
crespocarlos marked this conversation as resolved.
Show resolved Hide resolved

registry.when('Agent latest versions when configuration is defined', () => {
it('returns a version when agent is listed in the file', async () => {
const { status, body } = await callApi();
expect(status).to.be(200);

const agents = body.data;

const nodeAgent = agents[nodeAgentName] as ElasticApmAgentLatestVersion;
expect(nodeAgent?.latest_version).not.to.be(undefined);
});

it('returns undefined when agent is not listed in the file', async () => {
const { status, body } = await callApi();
expect(status).to.be(200);

const agents = body.data;

// @ts-ignore
const unlistedAgent = agents[unlistedAgentName] as ElasticApmAgentLatestVersion;
expect(unlistedAgent?.latest_version).to.be(undefined);
});
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
/*
* 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 globby from 'globby';
import path from 'path';
import type { DeploymentAgnosticFtrProviderContext } from '../../../ftr_provider_context';

const cwd = path.join(__dirname);
const envGrepFiles = process.env.APM_TEST_GREP_FILES as string;

function getGlobPattern() {
try {
const envGrepFilesParsed = JSON.parse(envGrepFiles as string) as string[];
return envGrepFilesParsed.map((pattern) => {
return pattern.includes('spec') ? `**/${pattern}**` : `**/${pattern}**.spec.ts`;
});
} catch (e) {
// ignore
}
return '**/*.spec.ts';
}

export default function apmApiIntegrationTests({
getService,
loadTestFile,
}: DeploymentAgnosticFtrProviderContext) {
// DO NOT SKIP
// Skipping here will skip the entire apm api test suite
// Instead skip (flaky) tests individually
// Failing: See https://github.com/elastic/kibana/issues/176948
describe('APM API tests', function () {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How about renaming it to distinguish these tests from the ones running in stateful?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

what is the concern if they remain the same? I imagine that most of the tests will be moved.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. easily identify which one is failing
  2. easily pick the correct test to run in the flaky runner

Copy link
Contributor Author

@crespocarlos crespocarlos Nov 5, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤔 doesn't the flaky test runner list tests by config instead of name?

I can change it but I'm not sure if it will solve the concerns above. I just want to avoid naming it APM API deployment agnostic tests

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought it was like cypress test but you're right, I just checked it.

leave it as is and if there is a need to change we can update it later. Thanks

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Totally up to you about the naming, just since it is located in x-pack/test/api_integration you can probably omit API word.

const registry = getService('registry');
const filePattern = getGlobPattern();
const tests = globby.sync(filePattern, { cwd });

if (envGrepFiles) {
// eslint-disable-next-line no-console
console.log(
`\nCommand "--grep-files=${filePattern}" matched ${tests.length} file(s):\n${tests
.map((name) => ` - ${name}`)
.join('\n')}\n`
);
}

tests.forEach((testName) => {
describe(testName, () => {
loadTestFile(require.resolve(`./${testName}`));
registry.run();
});
});
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,5 +19,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
loadTestFile(require.resolve('../../apis/painless_lab'));
loadTestFile(require.resolve('../../apis/saved_objects_management'));
loadTestFile(require.resolve('../../apis/observability/slo'));
loadTestFile(require.resolve('../../apis/observability/apm'));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import { createServerlessTestConfig } from '../../default_configs/serverless.con
export default createServerlessTestConfig({
serverlessProject: 'oblt',
testFiles: [require.resolve('./oblt.index.ts')],
servicesRequiredForTestAnalysis: ['registry'],
junit: {
reportName: 'Serverless Observability - Deployment-agnostic API Integration Tests',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,5 +13,6 @@ export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext)
loadTestFile(require.resolve('../../apis/observability/alerting'));
loadTestFile(require.resolve('../../apis/observability/dataset_quality'));
loadTestFile(require.resolve('../../apis/observability/slo'));
loadTestFile(require.resolve('../../apis/observability/apm'));
});
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import { createStatefulTestConfig } from '../../default_configs/stateful.config.

export default createStatefulTestConfig({
testFiles: [require.resolve('./oblt.index.ts')],
servicesRequiredForTestAnalysis: ['registry'],
junit: {
reportName: 'Stateful Observability - Deployment-agnostic API Integration Tests',
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ interface CreateTestConfigOptions<T extends DeploymentAgnosticCommonServices> {
esServerArgs?: string[];
kbnServerArgs?: string[];
services?: T;
servicesRequiredForTestAnalysis?: string[];
testFiles: string[];
junit: { reportName: string };
suiteTags?: { include?: string[]; exclude?: string[] };
Expand Down Expand Up @@ -85,6 +86,7 @@ export function createServerlessTestConfig<T extends DeploymentAgnosticCommonSer
// services can be customized, but must extend DeploymentAgnosticCommonServices
...(options.services || services),
},
servicesRequiredForTestAnalysis: options.servicesRequiredForTestAnalysis,
dockerServers: defineDockerServersConfig({
registry: {
enabled: !!dockerRegistryPort,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ interface CreateTestConfigOptions<T extends DeploymentAgnosticCommonServices> {
kbnServerArgs?: string[];
services?: T;
testFiles: string[];
servicesRequiredForTestAnalysis?: string[];
junit: { reportName: string };
suiteTags?: { include?: string[]; exclude?: string[] };
}
Expand Down Expand Up @@ -100,6 +101,7 @@ export function createStatefulTestConfig<T extends DeploymentAgnosticCommonServi
security: { disableTestUser: true },
// services can be customized, but must extend DeploymentAgnosticCommonServices
services: options.services || services,
servicesRequiredForTestAnalysis: options.servicesRequiredForTestAnalysis,
junit: options.junit,
suiteTags: options.suiteTags,

Expand Down
117 changes: 117 additions & 0 deletions x-pack/test/api_integration/deployment_agnostic/services/apm_api.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* 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 { format } from 'url';
import request from 'superagent';
import type {
APIReturnType,
APIClientRequestParamsOf,
} from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { APIEndpoint } from '@kbn/apm-plugin/server';
import { formatRequest } from '@kbn/server-route-repository';
import type { DeploymentAgnosticFtrProviderContext } from '../ftr_provider_context';

function createApmApiClient({ getService }: DeploymentAgnosticFtrProviderContext, role: string) {
const supertestWithoutAuth = getService('supertestWithoutAuth');
const samlAuth = getService('samlAuth');

return async <TEndpoint extends APIEndpoint>(
options: {
type?: 'form-data';
endpoint: TEndpoint;
spaceId?: string;
} & 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 roleAuthc = await samlAuth.createM2mApiKeyWithRoleScope(role);

const headers: Record<string, string> = {
...samlAuth.getInternalRequestHeader(),
...roleAuthc.apiKeyHeader,
};
crespocarlos marked this conversation as resolved.
Show resolved Hide resolved

const { method, pathname, version } = formatRequest(endpoint, params.path);
const pathnameWithSpaceId = options.spaceId ? `/s/${options.spaceId}${pathname}` : pathname;
const url = format({ pathname: pathnameWithSpaceId, query: params?.query });

// eslint-disable-next-line no-console
console.debug(`Calling APM API: ${method.toUpperCase()} ${url}`);
crespocarlos marked this conversation as resolved.
Show resolved Hide resolved

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 = supertestWithoutAuth[method](url)
.set(headers)
.set('Content-type', 'multipart/form-data');

for (const field of fields) {
void formDataRequest.field(field[0], field[1]);
}

res = await formDataRequest;
} else if (params.body) {
res = await supertestWithoutAuth[method](url).send(params.body).set(headers);
} else {
res = await supertestWithoutAuth[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;
}
}

export interface SupertestReturnType<TEndpoint extends APIEndpoint> {
status: number;
body: APIReturnType<TEndpoint>;
}

export function ApmApiProvider(context: DeploymentAgnosticFtrProviderContext) {
return {
readUser: createApmApiClient(context, 'viewer'),
adminUser: createApmApiClient(context, 'admin'),
writeUser: createApmApiClient(context, 'editor'),
};
crespocarlos marked this conversation as resolved.
Show resolved Hide resolved
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ import { PackageApiProvider } from './package_api';
import { RoleScopedSupertestProvider, SupertestWithRoleScope } from './role_scoped_supertest';
import { SloApiProvider } from './slo_api';
import { LogsSynthtraceEsClientProvider } from './logs_synthtrace_es_client';
import { SynthtraceProvider } from './synthtrace';
import { RegistryProvider } from './registry';
import { ApmApiProvider } from './apm_api';

export type {
InternalRequestHeader,
Expand All @@ -31,6 +34,9 @@ export const services = {
roleScopedSupertest: RoleScopedSupertestProvider,
logsSynthtraceEsClient: LogsSynthtraceEsClientProvider,
// create a new deployment-agnostic service and load here
synthtrace: SynthtraceProvider,
apmApi: ApmApiProvider,
registry: RegistryProvider,
};

export type SupertestWithRoleScopeType = SupertestWithRoleScope;
Expand Down
Loading