Platinum integration test (#772)

🎉
This commit is contained in:
Tomas Della Vedova
2019-03-01 08:42:56 +01:00
committed by GitHub
parent 36163f4822
commit ed3cca0fe6
24 changed files with 563 additions and 78 deletions

View File

@ -0,0 +1,64 @@
'use strict'
const esDefaultRoles = [
'apm_system',
'apm_user',
'beats_admin',
'beats_system',
'code_admin',
'code_user',
'ingest_admin',
'kibana_dashboard_only_user',
'kibana_system',
'kibana_user',
'logstash_admin',
'logstash_system',
'machine_learning_admin',
'machine_learning_user',
'monitoring_user',
'remote_monitoring_agent',
'remote_monitoring_collector',
'reporting_user',
'rollup_admin',
'rollup_user',
'snapshot_user',
'superuser',
'transport_client',
'watcher_admin',
'watcher_user'
]
const esDefaultUsers = [
'apm_system',
'beats_system',
'elastic',
'logstash_system',
'kibana',
'remote_monitoring_user'
]
function runInParallel (client, operation, options) {
if (options.length === 0) return Promise.resolve()
const operations = options.map(opts => {
const api = delve(client, operation).bind(client)
return api(opts)
})
return Promise.all(operations)
}
// code from https://github.com/developit/dlv
// needed to support an edge case: `a\.b`
// where `a.b` is a single field: { 'a.b': true }
function delve (obj, key, def, p) {
p = 0
// handle the key with a dot inside that is not a part of the path
// and removes the backslashes from the key
key = key.split
? key.split(/(?<!\\)\./g).map(k => k.replace(/\\/g, ''))
: key.replace(/\\/g, '')
while (obj && p < key.length) obj = obj[key[p++]]
return (obj === undefined || p < key.length) ? def : obj
}
module.exports = { runInParallel, esDefaultRoles, esDefaultUsers, delve }

View File

@ -6,7 +6,6 @@ const { join, sep } = require('path')
const yaml = require('js-yaml')
const Git = require('simple-git')
const ora = require('ora')
const minimist = require('minimist')
const tap = require('tap')
const { Client } = require('../../index')
const TestRunner = require('./test-runner')
@ -14,7 +13,7 @@ const TestRunner = require('./test-runner')
const esRepo = 'https://github.com/elastic/elasticsearch.git'
const esFolder = join(__dirname, '..', '..', 'elasticsearch')
const yamlFolder = join(esFolder, 'rest-api-spec', 'src', 'main', 'resources', 'rest-api-spec', 'test')
// const xPackYamlFolder = join(esFolder, 'x-pack', 'plugin', 'src', 'test', 'resources', 'rest-api-spec', 'test')
const xPackYamlFolder = join(esFolder, 'x-pack', 'plugin', 'src', 'test', 'resources', 'rest-api-spec', 'test')
const customSkips = [
// skipping because we are booting ES with `discovery.type=single-node`
// and this test will fail because of this configuration
@ -23,6 +22,31 @@ const customSkips = [
// which triggers a retry and the node to be marked as dead
'search.aggregation/240_max_buckets.yml'
]
const platinumBlackList = {
// file path: test name
'cat.aliases/10_basic.yml': 'Empty cluster',
'index/10_with_id.yml': 'Index with ID',
'indices.get_alias/10_basic.yml': 'Get alias against closed indices',
'indices.get_alias/20_empty.yml': 'Check empty aliases when getting all aliases via /_alias',
// https://github.com/elastic/elasticsearch/pull/39400
'ml/jobs_crud.yml': 'Test put job with id that is already taken',
// TODO: investigate why this is failing
'monitoring/bulk/10_basic.yml': '*',
'monitoring/bulk/20_privileges.yml': '*',
'license/20_put_license.yml': '*',
'snapshot/10_basic.yml': '*',
// the body is correct, but the regex is failing
'sql/sql.yml': 'Getting textual representation',
// we are setting two certificates in the docker config
'ssl/10_basic.yml': '*',
// docker issue?
'watcher/execute_watch/60_http_input.yml': '*',
// the checks are correct, but for some reason the test is failing on js side
// I bet is because the backslashes in the rg
'watcher/execute_watch/70_invalid.yml': '*',
'watcher/put_watch/10_basic.yml': '*',
'xpack/15_basic.yml': '*'
}
function Runner (opts) {
if (!(this instanceof Runner)) {
@ -32,14 +56,39 @@ function Runner (opts) {
assert(opts.node, 'Missing base node')
this.bailout = opts.bailout
this.client = new Client({ node: opts.node })
const options = { node: opts.node }
if (opts.isPlatinum) {
options.ssl = {
// NOTE: this path works only if we run
// the suite with npm scripts
ca: readFileSync('.ci/certs/ca.crt', 'utf8'),
rejectUnauthorized: false
}
}
this.client = new Client(options)
this.log = ora('Loading yaml suite').start()
}
Runner.prototype.waitCluster = function (callback, times = 0) {
this.log.text = 'Waiting for ElasticSearch'
this.client.cluster.health(
{ waitForStatus: 'green', timeout: '50s' },
(err, res) => {
if (++times < 10) {
setTimeout(() => {
this.waitCluster(callback, times)
}, 5000)
} else {
callback(err)
}
}
)
}
/**
* Runs the test suite
*/
Runner.prototype.start = function () {
Runner.prototype.start = function (opts) {
const parse = this.parse.bind(this)
const client = this.client
@ -53,36 +102,36 @@ Runner.prototype.start = function () {
// console.log()
// })
// Get the build hash of Elasticsearch
client.info((err, { body }) => {
this.waitCluster(err => {
if (err) {
this.log.fail(err.message)
process.exit(1)
}
const { number: version, build_hash: sha } = body.version
// Get the build hash of Elasticsearch
client.info((err, { body }) => {
if (err) {
this.log.fail(err.message)
process.exit(1)
}
const { number: version, build_hash: sha } = body.version
// Set the repository to the given sha and run the test suite
this.withSHA(sha, () => {
this.log.succeed('Done!')
runTest.call(this, version)
// Set the repository to the given sha and run the test suite
this.withSHA(sha, () => {
this.log.succeed(`Testing ${opts.isPlatinum ? 'platinum' : 'oss'} api...`)
runTest.call(this, version)
})
})
// client.xpack.license.postStartTrial({ acknowledge: true }, (err, { body }) => {
// if (err) {
// this.log.fail(err.message)
// return
// }
// })
})
function runTest (version) {
const files = []
.concat(getAllFiles(yamlFolder))
// .concat(getAllFiles(xPackYamlFolder))
.concat(opts.isPlatinum ? getAllFiles(xPackYamlFolder) : [])
.filter(t => !/(README|TODO)/g.test(t))
files.forEach(runTestFile.bind(this))
function runTestFile (file) {
// if (!file.endsWith('watcher/execute_watch/70_invalid.yml')) return
for (var i = 0; i < customSkips.length; i++) {
if (file.endsWith(customSkips[i])) return
}
@ -94,7 +143,7 @@ Runner.prototype.start = function () {
// every document is separated by '---', so we split on the separator
// and then we remove the empty strings, finally we parse them
const tests = data
.split('---')
.split('\n---\n')
.map(s => s.trim())
.filter(Boolean)
.map(parse)
@ -111,9 +160,26 @@ Runner.prototype.start = function () {
tests.forEach(test => {
const name = Object.keys(test)[0]
if (name === 'setup' || name === 'teardown') return
// should skip the test inside `platinumBlackList`
// if we are testing the platinum apis
if (opts.isPlatinum) {
const list = Object.keys(platinumBlackList)
for (i = 0; i < list.length; i++) {
if (file.endsWith(list[i]) && (name === platinumBlackList[list[i]] || platinumBlackList[list[i]] === '*')) {
const testName = file.slice(file.indexOf(`${sep}elasticsearch${sep}`)) + ' / ' + name
tap.skip(`Skipping test ${testName} because is blacklisted in the platinum test`)
return
}
}
}
// create a subtest for the specific folder + test file + test name
tap1.test(name, { jobs: 1, bail: this.bailout }, tap2 => {
const testRunner = TestRunner({ client, version, tap: tap2 })
const testRunner = TestRunner({
client,
version,
tap: tap2,
isPlatinum: file.includes('x-pack')
})
testRunner.run(setupTest, test[name], teardownTest, () => tap2.end())
})
})
@ -245,19 +311,13 @@ Runner.prototype.createFolder = function (name) {
}
if (require.main === module) {
const opts = minimist(process.argv.slice(2), {
string: ['node', 'version'],
boolean: ['bailout'],
default: {
// node: 'http://elastic:passw0rd@localhost:9200',
node: process.env.TEST_ES_SERVER || 'http://localhost:9200',
version: '7.0',
bailout: false
}
})
const url = process.env.TEST_ES_SERVER || 'http://localhost:9200'
const opts = {
node: url,
isPlatinum: url.indexOf('@') > -1
}
const runner = Runner(opts)
runner.start()
runner.start(opts)
}
const getAllFiles = dir =>

View File

@ -3,15 +3,20 @@
const t = require('tap')
const semver = require('semver')
const workq = require('workq')
const helper = require('./helper')
const { ConfigurationError } = require('../../lib/errors')
const { delve } = helper
const supportedFeatures = [
'gtelte',
'regex',
'benchmark',
'stash_in_path',
'groovy_scripting',
'headers'
'headers',
'transform_and_set',
'catch_unauthorized'
]
function TestRunner (opts) {
@ -25,6 +30,7 @@ function TestRunner (opts) {
this.response = null
this.stash = new Map()
this.tap = opts.tap || t
this.isPlatinum = opts.isPlatinum
this.q = opts.q || workq()
}
@ -55,14 +61,148 @@ TestRunner.prototype.cleanup = function (q, done) {
q.add((q, done) => {
this.client.snapshot.delete({ repository: '*', snapshot: '*' }, { ignore: 404 }, err => {
this.tap.error(err, 'should not error:snapshot.delete')
this.tap.error(err, 'should not error: snapshot.delete')
done()
})
})
q.add((q, done) => {
this.client.snapshot.deleteRepository({ repository: '*' }, { ignore: 404 }, err => {
this.tap.error(err, 'should not error:snapshot.deleteRepository')
this.tap.error(err, 'should not error: snapshot.deleteRepository')
done()
})
})
done()
}
/**
* Runs some additional API calls to prepare ES for the Platinum test,
* This set of calls should be executed before the final clenup.
* @param {queue}
* @param {function} done
*/
TestRunner.prototype.cleanupPlatinum = function (q, done) {
this.tap.comment('Platinum Cleanup')
q.add((q, done) => {
this.client.security.getRole((err, { body }) => {
this.tap.error(err, 'should not error: security.getRole')
const roles = Object.keys(body).filter(n => helper.esDefaultRoles.indexOf(n) === -1)
helper.runInParallel(
this.client, 'security.deleteRole',
roles.map(r => ({ name: r, refresh: 'wait_for' }))
)
.then(() => done())
.catch(err => this.tap.error(err, 'should not error: security.deleteRole'))
})
})
q.add((q, done) => {
this.client.security.getUser((err, { body }) => {
this.tap.error(err, 'should not error: security.getUser')
const users = Object.keys(body).filter(n => helper.esDefaultUsers.indexOf(n) === -1)
helper.runInParallel(
this.client, 'security.deleteUser',
users.map(r => ({ username: r, refresh: 'wait_for' }))
)
.then(() => done())
.catch(err => this.tap.error(err, 'should not error: security.deleteUser'))
})
})
q.add((q, done) => {
this.client.security.getPrivileges((err, { body }) => {
this.tap.error(err, 'should not error: security.getPrivileges')
const privileges = []
Object.keys(body).forEach(app => {
Object.keys(body[app]).forEach(priv => {
privileges.push({
name: body[app][priv].name,
application: body[app][priv].application,
refresh: 'wait_for'
})
})
})
helper.runInParallel(this.client, 'security.deletePrivileges', privileges)
.then(() => done())
.catch(err => this.tap.error(err, 'should not error: security.deletePrivileges'))
})
})
q.add((q, done) => {
this.client.ml.stopDatafeed({ datafeedId: '*', force: true }, err => {
this.tap.error(err, 'should not error: ml.stopDatafeed')
this.client.ml.getDatafeeds({ datafeedId: '*' }, (err, { body }) => {
this.tap.error(err, 'should error: not ml.getDatafeeds')
const feeds = body.datafeeds.map(f => f.datafeed_id)
helper.runInParallel(
this.client, 'ml.deleteDatafeed',
feeds.map(f => ({ datafeedId: f }))
)
.then(() => done())
.catch(err => this.tap.error(err, 'should not error: ml.deleteDatafeed'))
})
})
})
q.add((q, done) => {
this.client.ml.closeJob({ jobId: '*', force: true }, err => {
this.tap.error(err, 'should not error: ml.closeJob')
this.client.ml.getJobs({ jobId: '*' }, (err, { body }) => {
this.tap.error(err, 'should not error: ml.getJobs')
const jobs = body.jobs.map(j => j.job_id)
helper.runInParallel(
this.client, 'ml.deleteJob',
jobs.map(j => ({ jobId: j, waitForCompletion: true, force: true }))
)
.then(() => done())
.catch(err => this.tap.error(err, 'should not error: ml.deleteJob'))
})
})
})
q.add((q, done) => {
this.client.xpack.rollup.getJobs({ id: '_all' }, (err, { body }) => {
this.tap.error(err, 'should not error: rollup.getJobs')
const jobs = body.jobs.map(j => j.config.id)
helper.runInParallel(
this.client, 'xpack.rollup.stopJob',
jobs.map(j => ({ id: j, waitForCompletion: true }))
)
.then(() => helper.runInParallel(
this.client, 'xpack.rollup.deleteJob',
jobs.map(j => ({ id: j }))
))
.then(() => done())
.catch(err => this.tap.error(err, 'should not error: rollup.stopJob/deleteJob'))
})
})
q.add((q, done) => {
this.client.tasks.list((err, { body }) => {
this.tap.error(err, 'should not error: tasks.list')
const tasks = Object.keys(body.nodes)
.reduce((acc, node) => {
const { tasks } = body.nodes[node]
Object.keys(tasks).forEach(id => {
if (tasks[id].cancellable) acc.push(id)
})
return acc
}, [])
helper.runInParallel(
this.client, 'tasks.cancel',
tasks.map(id => ({ taskId: id }))
)
.then(() => done())
.catch(err => this.tap.error(err, 'should not error: tasks.cancel'))
})
})
q.add((q, done) => {
this.client.indices.delete({ index: '.ml-*' }, { ignore: 404 }, err => {
this.tap.error(err, 'should not error: indices.delete (ml indices)')
done()
})
})
@ -77,7 +217,7 @@ TestRunner.prototype.cleanup = function (q, done) {
* - the actual test
* - teardown
* - cleanup
* Internally uses a queue to guarantee the order of the test sections.
* Internally uses a queue to guarantee the order of the test sections.
* @param {object} setup (null if not needed)
* @param {object} test
* @oaram {object} teardown (null if not needed)
@ -92,6 +232,20 @@ TestRunner.prototype.run = function (setup, test, teardown, end) {
return end()
}
if (this.isPlatinum) {
this.tap.comment('Creating x-pack user')
// Some platinum test requires this user
this.q.add((q, done) => {
this.client.security.putUser({
username: 'x_pack_rest_user',
body: { password: 'x-pack-test-password', roles: ['superuser'] }
}, (err, { body }) => {
this.tap.error(err, 'should not error: security.putUser')
done()
})
})
}
if (setup) {
this.q.add((q, done) => {
this.exec('Setup', setup, q, done)
@ -108,6 +262,12 @@ TestRunner.prototype.run = function (setup, test, teardown, end) {
})
}
if (this.isPlatinum) {
this.q.add((q, done) => {
this.cleanupPlatinum(q, done)
})
}
this.q.add((q, done) => {
this.cleanup(q, done)
})
@ -211,11 +371,33 @@ TestRunner.prototype.fillStashedValues = function (obj) {
// iterate every key of the object
for (const key in obj) {
const val = obj[key]
// if the key value is a string, and the string includes '${'
// that we must update the content of '${...}'.
// eg: 'Basic ${auth}' we search the stahed value 'auth'
// and the resulting value will be 'Basic valueOfAuth'
if (typeof val === 'string' && val.includes('${')) {
const start = val.indexOf('${')
const end = val.indexOf('}', val.indexOf('${'))
const stashedKey = val.slice(start + 2, end)
const stashed = this.stash.get(stashedKey)
obj[key] = val.slice(0, start) + stashed + val.slice(end + 1)
continue
}
// handle json strings, eg: '{"hello":"$world"}'
if (typeof val === 'string' && val.includes('"$')) {
const start = val.indexOf('"$')
const end = val.indexOf('"', start + 1)
const stashedKey = val.slice(start + 2, end)
const stashed = '"' + this.stash.get(stashedKey) + '"'
obj[key] = val.slice(0, start) + stashed + val.slice(end + 1)
continue
}
// if the key value is a string, and the string includes '$'
// we run the "update value" code
if (typeof val === 'string' && val.includes('$')) {
// update the key value
obj[key] = getStashedValues.call(this, val)
continue
}
// go deep in the object
@ -261,6 +443,26 @@ TestRunner.prototype.set = function (key, name) {
return this
}
/**
* Applies a given transformation and stashes the result.
* @param {string} the name to identify the stashed value
* @param {string} the transformation function as string
* @returns {TestRunner}
*/
TestRunner.prototype.transform_and_set = function (name, transform) {
if (/base64EncodeCredentials/.test(transform)) {
const [user, password] = transform
.slice(transform.indexOf('(') + 1, -1)
.replace(/ /g, '')
.split(',')
const userAndPassword = `${delve(this.response, user)}:${delve(this.response, password)}`
this.stash.set(name, Buffer.from(userAndPassword).toString('base64'))
} else {
throw new Error(`Unknown transform: '${transform}'`)
}
return this
}
/**
* Runs a client command
* @param {object} the action to perform
@ -353,7 +555,15 @@ TestRunner.prototype.exec = function (name, actions, q, done) {
if (action.set) {
q.add((q, done) => {
const key = Object.keys(action.set)[0]
this.set(key, action.set[key])
this.set(this.fillStashedValues(key), action.set[key])
done()
})
}
if (action.transform_and_set) {
q.add((q, done) => {
const key = Object.keys(action.transform_and_set)[0]
this.transform_and_set(key, action.transform_and_set[key])
done()
})
}
@ -423,8 +633,12 @@ TestRunner.prototype.exec = function (name, actions, q, done) {
q.add((q, done) => {
const key = Object.keys(action.length)[0]
this.length(
delve(this.response, this.fillStashedValues(key)),
this.fillStashedValues(action.length)[key]
key === '$body' || key === ''
? this.response
: delve(this.response, this.fillStashedValues(key)),
key === '$body'
? action.length[key]
: this.fillStashedValues(action.length)[key]
)
done()
})
@ -694,20 +908,6 @@ function getSkip (arr) {
return null
}
// code from https://github.com/developit/dlv
// needed to support an edge case: `a\.b`
// where `a.b` is a single field: { 'a.b': true }
function delve (obj, key, def, p) {
p = 0
// handle the key with a dot inside that is not a part of the path
// and removes the backslashes from the key
key = key.split
? key.split(/(?<!\\)\./g).map(k => k.replace(/\\/g, ''))
: key.replace(/\\/g, '')
while (obj && p < key.length) obj = obj[key[p++]]
return (obj === undefined || p < key.length) ? def : obj
}
// Gets two *maybe* numbers and returns two valida numbers
// it throws if one or both are not a valid number
// the returned value is an array with the new values