[Logs onboarding] Added api tests for progress endpoints (#160750)

Relates to https://github.com/elastic/kibana/issues/159451.

This PR adds

* api integration tests to:

- [x] GET
/internal/observability_onboarding/custom_logs/{onboardingId}/progress
- [x] ~~GET /api/observability_onboarding/custom_logs/{id}/step/{name}
2023-05-24~~ POST
/internal/observability_onboarding/custom_logs/{id}/step/{name}
- [x] GET /api/observability_onboarding/elastic_agent/config

* unit tests to:
- [x] `generate_yml.ts` as part of GET
/api/observability_onboarding/elastic_agent/config
This commit is contained in:
Yngrid Coello 2023-06-29 21:35:01 +02:00 committed by GitHub
parent ff4559cb77
commit 1e918df659
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 612 additions and 28 deletions

View file

@ -0,0 +1,18 @@
/*
* 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.
*/
require('@kbn/babel-register').install();
const { run } = require('jest');
process.env.NODE_ENV = process.env.NODE_ENV || 'test';
const config = require('../../jest.config');
const argv = [...process.argv.slice(2), '--config', JSON.stringify(config)];
run(argv);

View file

@ -5,6 +5,7 @@
* 2.0.
*/
import Boom from '@hapi/boom';
import * as t from 'io-ts';
import { ObservabilityOnboardingState } from '../../saved_objects/observability_onboarding_status';
import { createObservabilityOnboardingServerRoute } from '../create_observability_onboarding_server_route';
@ -167,10 +168,9 @@ const stepProgressUpdateRoute = createObservabilityOnboardingServerRoute({
});
if (!savedObservabilityOnboardingState) {
return {
message:
'Unable to report setup progress - onboarding session not found.',
};
throw Boom.notFound(
'Unable to report setup progress - onboarding session not found.'
);
}
const {
@ -212,39 +212,44 @@ const getProgressRoute = createObservabilityOnboardingServerRoute({
request,
} = resources;
const coreStart = await core.start();
const savedObjectsClient =
coreStart.savedObjects.createInternalRepository();
const savedObjectsClient = coreStart.savedObjects.getScopedClient(request);
const savedObservabilityOnboardingState =
(await getObservabilityOnboardingState({
await getObservabilityOnboardingState({
savedObjectsClient,
savedObjectId: onboardingId,
})) || null;
});
if (!savedObservabilityOnboardingState) {
throw Boom.notFound(
'Unable to report setup progress - onboarding session not found.'
);
}
const progress = { ...savedObservabilityOnboardingState?.progress };
const esClient =
coreStart.elasticsearch.client.asScoped(request).asCurrentUser;
if (savedObservabilityOnboardingState) {
const {
state: { datasetName: dataset, namespace },
} = savedObservabilityOnboardingState;
if (progress['ea-status'] === 'complete') {
try {
const hasLogs = await getHasLogs({
dataset,
namespace,
esClient,
});
if (hasLogs) {
progress['logs-ingest'] = 'complete';
} else {
progress['logs-ingest'] = 'loading';
}
} catch (error) {
progress['logs-ingest'] = 'warning';
const {
state: { datasetName: dataset, namespace },
} = savedObservabilityOnboardingState;
if (progress['ea-status'] === 'complete') {
try {
const hasLogs = await getHasLogs({
dataset,
namespace,
esClient,
});
if (hasLogs) {
progress['logs-ingest'] = 'complete';
} else {
progress['logs-ingest'] = 'loading';
}
} else {
progress['logs-ingest'] = 'incomplete';
} catch (error) {
progress['logs-ingest'] = 'warning';
}
} else {
progress['logs-ingest'] = 'incomplete';
}
return { progress };

View file

@ -0,0 +1,96 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`generateYml should return a basic yml configuration 1`] = `
"outputs:
default:
type: elasticsearch
hosts:
- 'http://localhost:9200'
api_key: 'elastic:changeme'
inputs:
- id: my-logs-id
type: logfile
data_stream:
namespace: default
streams:
- id: logs-onboarding-my-dataset
data_stream:
dataset: my-dataset
paths:
- /my-service.logs
"
`;
exports[`generateYml should return a yml configuration with customConfigurations 1`] = `
"outputs:
default:
type: elasticsearch
hosts:
- 'http://localhost:9200'
api_key: 'elastic:changeme'
inputs:
- id: my-logs-id
type: logfile
data_stream:
namespace: default
streams:
- id: logs-onboarding-my-dataset
data_stream:
dataset: my-dataset
paths:
- /my-service.logs
agent.retry:
enabled: true
retriesCount: 3
agent.monitoring:
metrics: false
"
`;
exports[`generateYml should return a yml configuration with multiple logFilePaths 1`] = `
"outputs:
default:
type: elasticsearch
hosts:
- 'http://localhost:9200'
api_key: 'elastic:changeme'
inputs:
- id: my-logs-id
type: logfile
data_stream:
namespace: default
streams:
- id: logs-onboarding-my-dataset
data_stream:
dataset: my-dataset
paths:
- /my-service-1.logs
- /my-service-2.logs
"
`;
exports[`generateYml should return a yml configuration with service name 1`] = `
"outputs:
default:
type: elasticsearch
hosts:
- 'http://localhost:9200'
api_key: 'elastic:changeme'
inputs:
- id: my-logs-id
type: logfile
data_stream:
namespace: default
streams:
- id: logs-onboarding-my-dataset
data_stream:
dataset: my-dataset
paths:
- /my-service.logs
processors:
- add_fields:
target: service
fields:
name: my-service
"
`;

View file

@ -0,0 +1,63 @@
/*
* 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 { dump } from 'js-yaml';
import { generateYml } from './generate_yml';
const baseMockConfig = {
datasetName: 'my-dataset',
namespace: 'default',
logFilePaths: ['/my-service.logs'],
apiKey: 'elastic:changeme',
esHost: ['http://localhost:9200'],
logfileId: 'my-logs-id',
};
describe('generateYml', () => {
it('should return a basic yml configuration', () => {
const result = generateYml(baseMockConfig);
expect(result).toMatchSnapshot();
});
it('should return a yml configuration with multiple logFilePaths', () => {
const mockConfig = {
...baseMockConfig,
logFilePaths: ['/my-service-1.logs', '/my-service-2.logs'],
};
const result = generateYml(mockConfig);
expect(result).toMatchSnapshot();
});
it('should return a yml configuration with service name', () => {
const mockConfig = {
...baseMockConfig,
serviceName: 'my-service',
};
const result = generateYml(mockConfig);
expect(result).toMatchSnapshot();
});
it('should return a yml configuration with customConfigurations', () => {
const mockConfig = {
...baseMockConfig,
customConfigurations: dump({
['agent.retry']: {
enabled: true,
retriesCount: 3,
},
['agent.monitoring']: {
metrics: false,
},
}),
};
const result = generateYml(mockConfig);
expect(result).toMatchSnapshot();
});
});

View file

@ -92,6 +92,7 @@ export class ObservabilityOnboardingApiError extends Error {
}
export interface SupertestReturnType<TEndpoint extends APIEndpoint> {
text: string;
status: number;
body: APIReturnType<TEndpoint>;
}

View file

@ -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.
*/
export function createLogDoc({
time,
logFilepath,
serviceName,
namespace,
datasetName,
message,
}: {
time: number;
logFilepath: string;
serviceName: string;
namespace: string;
datasetName: string;
message: string;
}) {
return {
input: {
type: 'log',
},
'@timestamp': new Date(time).toISOString(),
log: {
file: {
path: logFilepath,
},
},
service: {
name: serviceName,
},
data_stream: {
namespace,
type: 'logs',
dataset: datasetName,
},
message,
event: {
dataset: datasetName,
},
};
}

View file

@ -0,0 +1,173 @@
/*
* 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 { ObservabilityOnboardingApiClientKey } from '../../../common/config';
import { FtrProviderContext } from '../../../common/ftr_provider_context';
import { ObservabilityOnboardingApiError } from '../../../common/observability_onboarding_api_supertest';
import { expectToReject } from '../../../common/utils/expect_to_reject';
import { createLogDoc } from './es_utils';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const observabilityOnboardingApiClient = getService('observabilityOnboardingApiClient');
const es = getService('es');
async function callApi({
onboardingId,
user = 'logMonitoringUser',
}: {
onboardingId: string;
user?: ObservabilityOnboardingApiClientKey;
}) {
return await observabilityOnboardingApiClient[user]({
endpoint: 'GET /internal/observability_onboarding/custom_logs/{onboardingId}/progress',
params: {
path: {
onboardingId,
},
},
});
}
registry.when('Get progress', { config: 'basic' }, () => {
let onboardingId: string;
const datasetName = 'api-tests';
const namespace = 'default';
before(async () => {
const req = await observabilityOnboardingApiClient.logMonitoringUser({
endpoint: 'POST /internal/observability_onboarding/custom_logs/save',
params: {
body: {
name: 'name',
state: {
datasetName,
namespace,
},
},
},
});
onboardingId = req.body.onboardingId;
});
describe('when missing required privileges', () => {
it('fails with a 404 error', async () => {
const err = await expectToReject<ObservabilityOnboardingApiError>(
async () =>
await callApi({
onboardingId,
user: 'noAccessUser',
})
);
expect(err.res.status).to.be(404);
expect(err.res.body.message).to.contain('onboarding session not found');
});
});
describe('when required privileges are set', () => {
describe("when onboardingId doesn't exists", () => {
it('fails with a 404 error', async () => {
const err = await expectToReject<ObservabilityOnboardingApiError>(
async () =>
await callApi({
onboardingId: 'my-onboarding-id',
})
);
expect(err.res.status).to.be(404);
expect(err.res.body.message).to.contain('onboarding session not found');
});
});
describe('when onboardingId exists', () => {
describe('when ea-status is not complete', () => {
it('should skip log verification and return log-ingest as incomplete', async () => {
const request = await callApi({
onboardingId,
});
expect(request.status).to.be(200);
expect(request.body.progress['logs-ingest']).to.be('incomplete');
});
});
describe('when ea-status is complete', () => {
describe('should not skip logs verification', () => {
before(async () => {
await observabilityOnboardingApiClient.logMonitoringUser({
endpoint: 'POST /internal/observability_onboarding/custom_logs/{id}/step/{name}',
params: {
path: {
id: onboardingId,
name: 'ea-status',
},
body: {
status: 'complete',
},
},
});
});
describe('when no logs have been ingested', () => {
it('should return log-ingest as loading', async () => {
const request = await callApi({
onboardingId,
});
expect(request.status).to.be(200);
expect(request.body.progress['logs-ingest']).to.be('loading');
});
});
describe('when logs have been ingested', () => {
before(async () => {
await es.indices.createDataStream({
name: `logs-${datasetName}-${namespace}`,
});
const doc = createLogDoc({
time: new Date('06/28/2023').getTime(),
logFilepath: '/my-service.log',
serviceName: 'my-service',
namespace,
datasetName,
message: 'This is a log message',
});
await es.bulk({
body: [{ create: { _index: `logs-${datasetName}-${namespace}` } }, doc],
refresh: 'wait_for',
});
});
it('should return log-ingest as complete', async () => {
const request = await callApi({
onboardingId,
});
expect(request.status).to.be(200);
expect(request.body.progress['logs-ingest']).to.be('complete');
});
after(async () => {
await es.indices.deleteDataStream({
name: `logs-${datasetName}-${namespace}`,
});
});
});
});
});
});
});
});
}

View file

@ -0,0 +1,99 @@
/*
* 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 { OBSERVABILITY_ONBOARDING_STATE_SAVED_OBJECT_TYPE } from '@kbn/observability-onboarding-plugin/server/saved_objects/observability_onboarding_status';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { ObservabilityOnboardingApiError } from '../../common/observability_onboarding_api_supertest';
import { expectToReject } from '../../common/utils/expect_to_reject';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const kibanaServer = getService('kibanaServer');
const observabilityOnboardingApiClient = getService('observabilityOnboardingApiClient');
async function callApi({ id, name, status }: { id: string; name: string; status: string }) {
return await observabilityOnboardingApiClient.logMonitoringUser({
endpoint: 'POST /internal/observability_onboarding/custom_logs/{id}/step/{name}',
params: {
path: {
id,
name,
},
body: {
status,
},
},
});
}
registry.when('Update step progress', { config: 'basic' }, () => {
let onboardingId: string;
before(async () => {
const req = await observabilityOnboardingApiClient.logMonitoringUser({
endpoint: 'POST /internal/observability_onboarding/custom_logs/save',
params: {
body: {
name: 'name',
state: {},
},
},
});
onboardingId = req.body.onboardingId;
});
describe("when onboardingId doesn't exists", () => {
it('fails with a 404 error', async () => {
const err = await expectToReject<ObservabilityOnboardingApiError>(
async () =>
await callApi({
id: 'my-onboarding-id',
name: 'ea-download',
status: 'complete',
})
);
expect(err.res.status).to.be(404);
expect(err.res.body.message).to.contain('onboarding session not found');
});
});
describe('when onboardingId exists', () => {
const step = {
name: 'ea-download',
status: 'complete',
};
before(async () => {
const savedState = await kibanaServer.savedObjects.get({
type: OBSERVABILITY_ONBOARDING_STATE_SAVED_OBJECT_TYPE,
id: onboardingId,
});
expect(savedState.attributes.progress?.[step.name]).not.ok();
});
it('updates step status', async () => {
const request = await callApi({
id: onboardingId,
...step,
});
expect(request.status).to.be(200);
const savedState = await kibanaServer.savedObjects.get({
type: OBSERVABILITY_ONBOARDING_STATE_SAVED_OBJECT_TYPE,
id: onboardingId,
});
expect(savedState.attributes.progress?.[step.name]).to.be(step.status);
});
});
});
}

View file

@ -0,0 +1,83 @@
/*
* 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 { load } from 'js-yaml';
import { FtrProviderContext } from '../../common/ftr_provider_context';
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const observabilityOnboardingApiClient = getService('observabilityOnboardingApiClient');
async function callApi({ onboardingId }: { onboardingId: string }) {
return await observabilityOnboardingApiClient.logMonitoringUser({
endpoint: 'GET /internal/observability_onboarding/elastic_agent/config',
params: {
query: {
onboardingId,
},
},
});
}
registry.when('Generate elastic_agent yml', { config: 'basic' }, () => {
let onboardingId: string;
const datasetName = 'api-tests';
const namespace = 'default';
const logFilepath = '/my-logs.log';
const serviceName = 'my-service';
before(async () => {
const req = await observabilityOnboardingApiClient.logMonitoringUser({
endpoint: 'POST /internal/observability_onboarding/custom_logs/save',
params: {
body: {
name: 'name',
state: {
datasetName,
namespace,
logFilePaths: [logFilepath],
serviceName,
},
},
},
});
onboardingId = req.body.onboardingId;
});
describe("when onboardingId doesn't exists", () => {
it('should return input properties empty', async () => {
const req = await callApi({
onboardingId: 'my-onboarding-id',
});
expect(req.status).to.be(200);
const ymlConfig = load(req.text);
expect(ymlConfig.inputs[0].data_stream.namespace).to.be('');
expect(ymlConfig.inputs[0].streams[0].data_stream.dataset).to.be('');
expect(ymlConfig.inputs[0].streams[0].paths).to.be.empty();
});
});
describe('when onboardingId exists', () => {
it('should return input properties configured', async () => {
const req = await callApi({
onboardingId,
});
expect(req.status).to.be(200);
const ymlConfig = load(req.text);
expect(ymlConfig.inputs[0].data_stream.namespace).to.be(namespace);
expect(ymlConfig.inputs[0].streams[0].data_stream.dataset).to.be(datasetName);
expect(ymlConfig.inputs[0].streams[0].paths).to.be.eql([logFilepath]);
});
});
});
}