Skip to content

Commit

Permalink
[ML] Data Frame Analytic: adds api integration tests for _start and _…
Browse files Browse the repository at this point in the history
…stop endpoints (#92532)

* add start and stop endpoint tests

* add start and stop within spaces endpoint tests

* move start and start_spaces tests to separate files

* wip: move stop and stop_spaces to separate files

* use slow running job config so job is still running when stop request happens

* check started job state is actually started

* check job is stopped after stopping

* add debug logs

* ensure jobs are created/started before stopping attempt

* remove unnecessary debug logs
  • Loading branch information
alvarezmelissa87 authored Mar 3, 2021
1 parent bf8417e commit ccc70b4
Show file tree
Hide file tree
Showing 7 changed files with 463 additions and 2 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,10 @@ export default function ({ loadTestFile }: FtrProviderContext) {
loadTestFile(require.resolve('./delete'));
loadTestFile(require.resolve('./update'));
loadTestFile(require.resolve('./create_job'));
loadTestFile(require.resolve('./start'));
loadTestFile(require.resolve('./stop'));
loadTestFile(require.resolve('./start_spaces'));
loadTestFile(require.resolve('./stop_spaces'));
loadTestFile(require.resolve('./get_spaces'));
loadTestFile(require.resolve('./update_spaces'));
loadTestFile(require.resolve('./delete_spaces'));
Expand Down
136 changes: 136 additions & 0 deletions x-pack/test/api_integration/apis/ml/data_frame_analytics/start.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
/*
* 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';
import { USER } from '../../../../functional/services/ml/security_common';
import { DataFrameAnalyticsConfig } from '../../../../../plugins/ml/public/application/data_frame_analytics/common';
import { DeepPartial } from '../../../../../plugins/ml/common/types/common';
import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api';
import { DATA_FRAME_TASK_STATE } from '../../../../../plugins/ml/common/constants/data_frame_analytics';

export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertestWithoutAuth');
const ml = getService('ml');

const jobId = `bm_${Date.now()}`;
const generateDestinationIndex = (analyticsId: string) => `user-${analyticsId}`;
const commonJobConfig = {
source: {
index: ['ft_bank_marketing'],
query: {
match_all: {},
},
},
analysis: {
classification: {
dependent_variable: 'y',
training_percent: 20,
},
},
analyzed_fields: {
includes: [],
excludes: [],
},
model_memory_limit: '60mb',
allow_lazy_start: false, // default value
max_num_threads: 1, // default value
};
const destinationIndex = generateDestinationIndex(`${jobId}_0`);

const testJobConfigs: Array<DeepPartial<DataFrameAnalyticsConfig>> = [
{
id: `${jobId}_0`,
description: 'Test start for analytics',
dest: {
index: destinationIndex,
results_field: 'ml',
},
...commonJobConfig,
},
];

async function createJobs(mockJobConfigs: Array<DeepPartial<DataFrameAnalyticsConfig>>) {
for (const jobConfig of mockJobConfigs) {
await ml.api.createDataFrameAnalyticsJob(jobConfig as DataFrameAnalyticsConfig);
}
}

describe('POST data_frame/analytics/{analyticsId}/_start', () => {
before(async () => {
await esArchiver.loadIfNeeded('ml/bm_classification');
await ml.testResources.setKibanaTimeZoneToUTC();
await createJobs(testJobConfigs);
});

after(async () => {
await ml.api.deleteDataFrameAnalyticsJobES(`${jobId}_0`);
await ml.api.cleanMlIndices();
await ml.api.deleteIndices(destinationIndex);
});

describe('StartDataFrameAnalyticsJob', () => {
it('should start analytics job for specified id if job exists', async () => {
const analyticsId = `${jobId}_0`;

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_start`)
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(COMMON_REQUEST_HEADERS)
.expect(200);

expect(body).not.to.be(undefined);
expect(body.acknowledged).to.be(true);
expect(body.node).not.to.be('');

await ml.api.waitForAnalyticsState(analyticsId, DATA_FRAME_TASK_STATE.STARTED);
await ml.api.assertIndicesExist(destinationIndex);
});

it('should show 404 error if job does not exist', async () => {
const id = `${jobId}_invalid`;
const message = `No known job with id '${id}'`;

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${id}/_start`)
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(COMMON_REQUEST_HEADERS)
.expect(404);

expect(body.error).to.eql('Not Found');
expect(body.message).to.eql(message);
});

it('should not allow to start analytics job for unauthorized user', async () => {
const analyticsId = `${jobId}_0`;

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_start`)
.auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED))
.set(COMMON_REQUEST_HEADERS)
.expect(403);

expect(body.error).to.eql('Forbidden');
expect(body.message).to.eql('Forbidden');
});

it('should not allow to start analytics job for user with view only permission', async () => {
const analyticsId = `${jobId}_0`;

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_start`)
.auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER))
.set(COMMON_REQUEST_HEADERS)
.expect(403);

expect(body.error).to.eql('Forbidden');
expect(body.message).to.eql('Forbidden');
});
});
});
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
/*
* 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';
import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api';
import { USER } from '../../../../functional/services/ml/security_common';
import { DATA_FRAME_TASK_STATE } from '../../../../../plugins/ml/common/constants/data_frame_analytics';

export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const ml = getService('ml');
const spacesService = getService('spaces');
const supertest = getService('supertestWithoutAuth');

const jobIdSpace1 = 'ihp_od_space1';
const jobIdSpace2 = 'ihp_od_space2';
const idSpace1 = 'space1';
const idSpace2 = 'space2';

const initialModelMemoryLimit = '17mb';

async function runStartRequest(jobId: string, space: string, expectedStatusCode: number) {
const { body } = await supertest
.post(`/s/${space}/api/ml/data_frame/analytics/${jobId}/_start`)
.auth(
USER.ML_POWERUSER_ALL_SPACES,
ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER_ALL_SPACES)
)
.set(COMMON_REQUEST_HEADERS)
.expect(expectedStatusCode);

return body;
}

let space1JobDestIndex: string;
let space2JobDestIndex: string;

describe('POST data_frame/analytics/{analyticsId}/_start with spaces', function () {
before(async () => {
await esArchiver.loadIfNeeded('ml/ihp_outlier');
await spacesService.create({ id: idSpace1, name: 'space_one', disabledFeatures: [] });
await spacesService.create({ id: idSpace2, name: 'space_two', disabledFeatures: [] });

const jobConfigSpace1 = ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(jobIdSpace1);
await ml.api.createDataFrameAnalyticsJob(
{ ...jobConfigSpace1, model_memory_limit: initialModelMemoryLimit },
idSpace1
);

const jobConfigSpace2 = ml.commonConfig.getDFAIhpOutlierDetectionJobConfig(jobIdSpace2);
await ml.api.createDataFrameAnalyticsJob(
{ ...jobConfigSpace2, model_memory_limit: initialModelMemoryLimit },
idSpace2
);

space1JobDestIndex = jobConfigSpace1.dest.index;
space2JobDestIndex = jobConfigSpace2.dest.index;

await ml.testResources.setKibanaTimeZoneToUTC();
});

after(async () => {
await ml.api.deleteDataFrameAnalyticsJobES(jobIdSpace1);
await ml.api.deleteDataFrameAnalyticsJobES(jobIdSpace2);
await spacesService.delete(idSpace1);
await spacesService.delete(idSpace2);
await ml.api.cleanMlIndices();
await ml.api.deleteIndices(space1JobDestIndex);
await ml.api.deleteIndices(space2JobDestIndex);
await ml.testResources.cleanMLSavedObjects();
});

it('should start job from same space', async () => {
const body = await runStartRequest(jobIdSpace1, idSpace1, 200);
expect(body).to.have.property('acknowledged', true);

await ml.api.waitForAnalyticsState(jobIdSpace1, DATA_FRAME_TASK_STATE.STARTED);
await ml.api.assertIndicesExist(space1JobDestIndex);
});

it('should fail to start job from different space', async () => {
const body = await runStartRequest(jobIdSpace2, idSpace1, 404);
expect(body.error).to.eql('Not Found');

await ml.api.waitForAnalyticsState(jobIdSpace1, DATA_FRAME_TASK_STATE.STOPPED);
});
});
};
91 changes: 91 additions & 0 deletions x-pack/test/api_integration/apis/ml/data_frame_analytics/stop.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
/*
* 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';
import { USER } from '../../../../functional/services/ml/security_common';
import { COMMON_REQUEST_HEADERS } from '../../../../functional/services/ml/common_api';
import { DATA_FRAME_TASK_STATE } from '../../../../../plugins/ml/common/constants/data_frame_analytics';

export default ({ getService }: FtrProviderContext) => {
const esArchiver = getService('esArchiver');
const supertest = getService('supertestWithoutAuth');
const ml = getService('ml');

const jobId = `bm_${Date.now()}`;
const analyticsId = `${jobId}_1`;
let destinationIndex: string;

describe('POST data_frame/analytics/{analyticsId}/_stop', () => {
before(async () => {
await esArchiver.loadIfNeeded('ml/bm_classification');
await ml.testResources.setKibanaTimeZoneToUTC();
// job config with high training percent so it takes longer to run
const slowRunningConfig = ml.commonConfig.getDFABmClassificationJobConfig(analyticsId);
destinationIndex = slowRunningConfig.dest.index;

await ml.api.createDataFrameAnalyticsJob(slowRunningConfig);
await ml.api.runDFAJob(analyticsId);
});

after(async () => {
await ml.api.deleteDataFrameAnalyticsJobES(analyticsId);
await ml.api.cleanMlIndices();
await ml.api.deleteIndices(destinationIndex);
});

describe('StopsDataFrameAnalyticsJob', () => {
it('should stop analytics job for specified id when job exists', async () => {
const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_stop`)
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(COMMON_REQUEST_HEADERS)
.expect(200);

expect(body).not.to.be(undefined);
expect(body.stopped).to.be(true);
await ml.api.waitForAnalyticsState(analyticsId, DATA_FRAME_TASK_STATE.STOPPED, 5000);
});

it('should show 404 error if job does not exist', async () => {
const id = `${jobId}_invalid`;
const message = `No known job with id '${id}'`;

const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${id}/_stop`)
.auth(USER.ML_POWERUSER, ml.securityCommon.getPasswordForUser(USER.ML_POWERUSER))
.set(COMMON_REQUEST_HEADERS)
.expect(404);

expect(body.error).to.eql('Not Found');
expect(body.message).to.eql(message);
});

it('should not allow to stop analytics job for unauthorized user', async () => {
const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_stop`)
.auth(USER.ML_UNAUTHORIZED, ml.securityCommon.getPasswordForUser(USER.ML_UNAUTHORIZED))
.set(COMMON_REQUEST_HEADERS)
.expect(403);

expect(body.error).to.eql('Forbidden');
expect(body.message).to.eql('Forbidden');
});

it('should not allow to stop analytics job for user with view only permission', async () => {
const { body } = await supertest
.post(`/api/ml/data_frame/analytics/${analyticsId}/_stop`)
.auth(USER.ML_VIEWER, ml.securityCommon.getPasswordForUser(USER.ML_VIEWER))
.set(COMMON_REQUEST_HEADERS)
.expect(403);

expect(body.error).to.eql('Forbidden');
expect(body.message).to.eql('Forbidden');
});
});
});
};
Loading

0 comments on commit ccc70b4

Please sign in to comment.