// Licensed to Elasticsearch B.V under one or more agreements. // Elasticsearch B.V licenses this file to you under the Apache 2.0 License. // See the LICENSE file in the project root for more information 'use strict' const { readFileSync, accessSync, mkdirSync, readdirSync, statSync } = require('fs') const { join, sep } = require('path') const yaml = require('js-yaml') const Git = require('simple-git') const tap = require('tap') const { Client } = require('../../index') const TestRunner = require('./test-runner') const { sleep } = require('./helper') 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 ossSkips = { // TODO: remove this once 'arbitrary_key' is implemented // https://github.com/elastic/elasticsearch/pull/41492 'indices.split/30_copy_settings.yml': ['*'], // skipping because we are booting ES with `discovery.type=single-node` // and this test will fail because of this configuration 'nodes.stats/30_discovery.yml': ['*'], // the expected error is returning a 503, // which triggers a retry and the node to be marked as dead 'search.aggregation/240_max_buckets.yml': ['*'], // fails with ES5 with x-pack enabled 'cat.allocation/10_basic.yaml': ['*'], 'cat.indices/10_basic.yaml': ['*'], 'cat.shards/10_basic.yaml': ['*'], // fails with ES5,`repository` is a required field 'cat.snapshots/10_basic.yaml': ['*'], // fails with ES5 with x-pack enabled 'cat.templates/10_basic.yaml': ['*'], 'cluster.health/10_basic.yaml': ['*'], 'cluster.health/20_request_timeout.yaml': ['*'], 'delete/11_shard_header.yaml': ['*'], 'delete/45_parent_with_routing.yaml': ['*'], 'delete/50_refresh.yaml': ['*'], 'exists/40_routing.yaml': ['*'], 'exists/55_parent_with_routing.yaml': ['*'], 'get/55_parent_with_routing.yaml': ['*'], 'get_source/55_parent_with_routing.yaml': ['*'], 'indices.get_mapping/50_wildcard_expansion.yaml': ['*'], 'indices.flush/10_basic.yaml': ['*'], 'indices.open/10_basic.yaml': ['*'], 'indices.open/20_multiple_indices.yaml': ['*'], 'indices.stats/10_index.yaml': ['*'], 'indices.shard_stores/10_basic.yaml': ['*'], 'indices.segments/10_basic.yaml': ['*'], 'mget/40_routing.yaml': ['*'], 'mlt/20_docs.yaml': ['*'], 'search/10_source_filtering.yaml': ['*'], 'search/140_pre_filter_search_shards.yml': ['*'], 'search.aggregation/10_histogram.yaml': ['*'], 'search.aggregation/20_terms.yaml': ['*'], 'search.aggregation/40_range.yaml': ['*'], 'search.aggregation/50_filter.yaml': ['*'], 'search.highlight/30_fvh.yml': ['*'], 'snapshot.restore/10_basic.yaml': ['*'], 'snapshot.status/10_basic.yaml': ['*'], 'update/11_shard_header.yaml': ['*'], 'update/55_parent_with_routing.yaml': ['*'] } const xPackBlackList = { // 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': ['*'] } class Runner { constructor (opts = {}) { const options = { node: opts.node } if (opts.isXPack) { options.ssl = { ca: readFileSync(join(__dirname, '..', '..', '.ci', 'certs', 'ca.crt'), 'utf8'), rejectUnauthorized: false } } this.client = new Client(options) console.log('Loading yaml suite') } async waitCluster (client, times = 0) { try { await client.cluster.health({ waitForStatus: 'green', timeout: '50s' }) } catch (err) { if (++times < 10) { await sleep(5000) return this.waitCluster(client, times) } console.error(err) process.exit(1) } } async start ({ isXPack }) { const { client } = this const parse = this.parse.bind(this) console.log('Waiting for Elasticsearch') await this.waitCluster(client) const { body } = await client.info() const { number: version, build_hash: sha } = body.version console.log(`Checking out sha ${sha}...`) await this.withSHA(sha) console.log(`Testing ${isXPack ? 'XPack' : 'oss'} api...`) const folders = [] .concat(getAllFiles(yamlFolder)) .concat(isXPack ? getAllFiles(xPackYamlFolder) : []) .filter(t => !/(README|TODO)/g.test(t)) // we cluster the array based on the folder names, // to provide a better test log output .reduce((arr, file) => { const path = file.slice(file.indexOf('/rest-api-spec/test'), file.lastIndexOf('/')) var inserted = false for (var i = 0; i < arr.length; i++) { if (arr[i][0].includes(path)) { inserted = true arr[i].push(file) break } } if (!inserted) arr.push([file]) return arr }, []) for (const folder of folders) { // pretty name const apiName = folder[0].slice( folder[0].indexOf(`${sep}rest-api-spec${sep}test`) + 19, folder[0].lastIndexOf(sep) ) tap.test(`Testing ${apiName}`, { bail: true, timeout: 0 }, t => { for (const file of folder) { const data = readFileSync(file, 'utf8') // get the test yaml (as object), some file has multiple yaml documents inside, // 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('\n---\n') .map(s => s.trim()) .filter(Boolean) .map(parse) t.test( file.slice(file.lastIndexOf(apiName)), testFile(file, tests) ) } t.end() }) } function testFile (file, tests) { return t => { // get setup and teardown if present var setupTest = null var teardownTest = null for (const test of tests) { if (test.setup) setupTest = test.setup if (test.teardown) teardownTest = test.teardown } tests.forEach(test => { const name = Object.keys(test)[0] if (name === 'setup' || name === 'teardown') return if (shouldSkip(t, isXPack, file, name)) return // create a subtest for the specific folder + test file + test name t.test(name, async t => { const testRunner = new TestRunner({ client, version, tap: t, isXPack: file.includes('x-pack') }) await testRunner.run(setupTest, test[name], teardownTest) }) }) t.end() } } } parse (data) { try { var doc = yaml.safeLoad(data) } catch (err) { console.error(err) return } return doc } getTest (folder) { const tests = readdirSync(folder) return tests.filter(t => !/(README|TODO)/g.test(t)) } /** * Sets the elasticsearch repository to the given sha. * If the repository is not present in `esFolder` it will * clone the repository and the checkout the sha. * If the repository is already present but it cannot checkout to * the given sha, it will perform a pull and then try again. * @param {string} sha * @param {function} callback */ withSHA (sha) { return new Promise((resolve, reject) => { _withSHA.call(this, err => err ? reject(err) : resolve()) }) function _withSHA (callback) { var fresh = false var retry = 0 if (!this.pathExist(esFolder)) { if (!this.createFolder(esFolder)) { return callback(new Error('Failed folder creation')) } fresh = true } const git = Git(esFolder) if (fresh) { clone(checkout) } else { checkout() } function checkout () { console.log(`Checking out sha '${sha}'`) git.checkout(sha, err => { if (err) { if (retry++ > 0) { return callback(err) } return pull(checkout) } callback() }) } function pull (cb) { console.log('Pulling elasticsearch repository...') git.pull(err => { if (err) { return callback(err) } cb() }) } function clone (cb) { console.log('Cloning elasticsearch repository...') git.clone(esRepo, esFolder, err => { if (err) { return callback(err) } cb() }) } } } /** * Checks if the given path exists * @param {string} path * @returns {boolean} true if exists, false if not */ pathExist (path) { try { accessSync(path) return true } catch (err) { return false } } /** * Creates the given folder * @param {string} name * @returns {boolean} true on success, false on failure */ createFolder (name) { try { mkdirSync(name) return true } catch (err) { return false } } } if (require.main === module) { const node = process.env.TEST_ES_SERVER || 'http://localhost:9200' const opts = { node, isXPack: node.indexOf('@') > -1 } const runner = new Runner(opts) runner.start(opts).catch(console.log) } const shouldSkip = (t, isXPack, file, name) => { var list = Object.keys(ossSkips) for (var i = 0; i < list.length; i++) { const ossTest = ossSkips[list[i]] for (var j = 0; j < ossTest.length; j++) { if (file.endsWith(list[i]) && (name === ossTest[j] || ossTest[j] === '*')) { const testName = file.slice(file.indexOf(`${sep}elasticsearch${sep}`)) + ' / ' + name t.comment(`Skipping test ${testName} because is blacklisted in the oss test`) return true } } } if (file.includes('x-pack') || isXPack) { list = Object.keys(xPackBlackList) for (i = 0; i < list.length; i++) { const platTest = xPackBlackList[list[i]] for (j = 0; j < platTest.length; j++) { if (file.endsWith(list[i]) && (name === platTest[j] || platTest[j] === '*')) { const testName = file.slice(file.indexOf(`${sep}elasticsearch${sep}`)) + ' / ' + name t.comment(`Skipping test ${testName} because is blacklisted in the XPack test`) return true } } } } return false } const getAllFiles = dir => readdirSync(dir).reduce((files, file) => { const name = join(dir, file) const isDirectory = statSync(name).isDirectory() return isDirectory ? [...files, ...getAllFiles(name)] : [...files, name] }, []) module.exports = Runner