Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -22,6 +22,8 @@ import path from 'path';
import os from 'os';
import pRetry from 'p-retry';

import { ELASTIC_HTTP_VERSION_HEADER } from '@kbn/core-http-common';
import { INITIAL_REST_VERSION } from '@kbn/data-views-plugin/server/constants';
import { renderSummaryTable } from './print_run';
import type { SecuritySolutionDescribeBlockFtrConfig } from './utils';
import { parseTestFileConfig, retrieveIntegrations } from './utils';
Expand All @@ -31,13 +33,13 @@ interface ProductType {
product_tier: string;
}

interface CreateEnvironmentRequestBody {
interface CreateProjectRequestBody {
name: string;
region_id: string;
product_types?: ProductType[];
}

interface Environment {
interface Project {
name: string;
id: string;
region: string;
Expand All @@ -55,10 +57,15 @@ const DEFAULT_REGION = 'aws-eu-west-1';
const PROJECT_NAME_PREFIX = 'kibana-cypress-security-solution-ephemeral';
const BASE_ENV_URL = 'https://global.qa.cld.elstc.co';
let log: ToolingLog;

const delay = async (timeout: number) => {
await new Promise((r) => setTimeout(r, timeout));
};
const API_HEADERS = Object.freeze({
'kbn-xsrf': 'cypress-creds',
'x-elastic-internal-origin': 'security-solution',
[ELASTIC_HTTP_VERSION_HEADER]: [INITIAL_REST_VERSION],
});
const PROVIDERS = Object.freeze({
providerType: 'basic',
providerName: 'cloud-basic',
});

const getApiKeyFromElasticCloudJsonFile = (): string | undefined => {
const userHomeDir = os.homedir();
Expand All @@ -71,13 +78,13 @@ const getApiKeyFromElasticCloudJsonFile = (): string | undefined => {
}
};

// Method to invoke the create environment API for serverless.
async function createEnvironment(
// Method to invoke the create project API for serverless.
async function createSecurityProject(
projectName: string,
apiKey: string,
ftrConfig: SecuritySolutionDescribeBlockFtrConfig
): Promise<Environment | undefined> {
const body: CreateEnvironmentRequestBody = {
): Promise<Project | undefined> {
const body: CreateProjectRequestBody = {
name: projectName,
region_id: DEFAULT_REGION,
};
Expand Down Expand Up @@ -111,8 +118,8 @@ async function createEnvironment(
}
}

// Method to invoke the delete environment API for serverless.
async function deleteEnvironment(
// Method to invoke the delete project API for serverless.
async function deleteSecurityProject(
projectId: string,
projectName: string,
apiKey: string
Expand All @@ -123,7 +130,7 @@ async function deleteEnvironment(
Authorization: `ApiKey ${apiKey}`,
},
});
log.info(`Environment ${projectName} was successfully deleted!`);
log.info(`Project ${projectName} was successfully deleted!`);
} catch (error) {
if (error instanceof AxiosError) {
log.error(`${error.response?.status}:${error.response?.data}`);
Expand All @@ -133,16 +140,16 @@ async function deleteEnvironment(
}
}

// Method to reset the credentials for the created environment.
// Method to reset the credentials for the created project.
async function resetCredentials(
environmentId: string,
projectId: string,
runnerId: string,
apiKey: string
): Promise<Credentials | undefined> {
log.info(`${runnerId} : Reseting credentials`);
try {
const response = await axios.post(
`${BASE_ENV_URL}/api/v1/serverless/projects/security/${environmentId}/_reset-credentials`,
`${BASE_ENV_URL}/api/v1/serverless/projects/security/${projectId}/_reset-credentials`,
{},
{
headers: {
Expand All @@ -159,10 +166,43 @@ async function resetCredentials(
}
}

// Wait until Project is initialized
function waitForProjectInitialized(projectId: string, apiKey: string): Promise<void> {
const fetchProjectStatusAttempt = async (attemptNum: number) => {
log.info(`Retry number ${attemptNum} to check if project is initialized.`);
const response = await axios.get(
`${BASE_ENV_URL}/api/v1/serverless/projects/security/${projectId}/status`,
{
headers: {
Authorization: `ApiKey ${apiKey}`,
},
}
);
if (response.data.phase !== 'initialized') {
throw new Error('Project is not initialized. Retrying in 20s...');
} else {
log.info('Project is initialized');
}
};
const retryOptions = {
onFailedAttempt: (error: Error | AxiosError) => {
if (error instanceof AxiosError && error.code === 'ENOTFOUND') {
log.info('Project is not reachable. Retrying in 20s...');
} else {
log.info(error);
}
},
retries: 100,
factor: 2,
maxTimeout: 20000,
};
return pRetry(fetchProjectStatusAttempt, retryOptions);
}

// Wait until elasticsearch status goes green
function waitForEsStatusGreen(esUrl: string, auth: string, runnerId: string): Promise<void> {
const fetchHealthStatusAttempt = async (attemptNum: number) => {
log.info(`Retry number ${attemptNum} to check if es is green.`);
log.info(`Retry number ${attemptNum} to check if Elasticsearch is green.`);

const response = await axios.get(`${esUrl}/_cluster/health?wait_for_status=green&timeout=50s`, {
headers: {
Expand All @@ -176,7 +216,7 @@ function waitForEsStatusGreen(esUrl: string, auth: string, runnerId: string): Pr
onFailedAttempt: (error: Error | AxiosError) => {
if (error instanceof AxiosError && error.code === 'ENOTFOUND') {
log.info(
`${runnerId}: The elasticsearch url is not yet reachable. A retry will be triggered soon...`
`${runnerId}: The Elasticsearch URL is not yet reachable. A retry will be triggered soon...`
);
}
},
Expand Down Expand Up @@ -206,7 +246,7 @@ function waitForKibanaAvailable(kbUrl: string, auth: string, runnerId: string):
const retryOptions = {
onFailedAttempt: (error: Error | AxiosError) => {
if (error instanceof AxiosError && error.code === 'ENOTFOUND') {
log.info(`${runnerId}: The kibana url is not yet reachable. Retrying in 20s...`);
log.info(`${runnerId}: The Kibana URL is not yet reachable. Retrying in 20s...`);
} else {
log.info(`${runnerId}: ${error}`);
}
Expand All @@ -218,6 +258,73 @@ function waitForKibanaAvailable(kbUrl: string, auth: string, runnerId: string):
return pRetry(fetchKibanaStatusAttempt, retryOptions);
}

// Wait for Elasticsearch to be accessible
function waitForEsAccess(esUrl: string, auth: string, runnerId: string): Promise<void> {
const fetchEsAccessAttempt = async (attemptNum: number) => {
log.info(`Retry number ${attemptNum} to check if can be accessed.`);

const response = await axios.get(`${esUrl}`, {
headers: {
Authorization: `Basic ${auth}`,
},
});

if (response.status !== 200) {
throw new Error('Cannot access. Retrying in 20s...');
} else {
log.info('Access performed successfully');
}
};
const retryOptions = {
onFailedAttempt: (error: Error | AxiosError) => {
if (error instanceof AxiosError && error.code === 'ENOTFOUND') {
log.info(
`${runnerId}: The elasticsearch url is not yet reachable. A retry will be triggered soon...`
);
}
},
retries: 100,
factor: 2,
maxTimeout: 20000,
};

return pRetry(fetchEsAccessAttempt, retryOptions);
}

// Wait until application is ready
function waitForKibanaLogin(kbUrl: string, credentials: Credentials): Promise<void> {
const body = {
...PROVIDERS,
currentURL: '/',
params: credentials,
};

const fetchLoginStatusAttempt = async (attemptNum: number) => {
log.info(`Retry number ${attemptNum} to check if login can be performed.`);
const response = await axios.post(`${kbUrl}/internal/security/login`, body, {
headers: API_HEADERS,
});
if (response.status !== 200) {
throw new Error('Cannot login. Retrying in 20s...');
} else {
log.info('Login can be performed successfully');
}
};
const retryOptions = {
onFailedAttempt: (error: Error | AxiosError) => {
if (error instanceof AxiosError && error.code === 'ENOTFOUND') {
log.info('Project is not reachable. Retrying login in 20s...');
} else {
log.info(error);
}
},
retries: 100,
factor: 2,
maxTimeout: 20000,
};
return pRetry(fetchLoginStatusAttempt, retryOptions);
}

export const cli = () => {
run(
async () => {
Expand All @@ -228,9 +335,9 @@ export const cli = () => {

// Checking if API key is either provided via env variable or in ~/.elastic.cloud.json
if (!process.env.CLOUD_QA_API_KEY && !getApiKeyFromElasticCloudJsonFile()) {
log.error('The api key for the environment needs to be provided with the env var API_KEY.');
log.error('The API key for the environment needs to be provided with the env var API_KEY.');
log.error(
'If running locally, ~/.elastic/cloud.json is attempted to be read which contains the api key.'
'If running locally, ~/.elastic/cloud.json is attempted to be read which contains the API key.'
);
// eslint-disable-next-line no-process-exit
return process.exit(1);
Expand Down Expand Up @@ -352,51 +459,57 @@ ${JSON.stringify(cypressConfigFile, null, 2)}
const specFileFTRConfig = parseTestFileConfig(filePath);

if (!API_KEY) {
log.info('API KEY to create environment could not be retrieved.');
log.info('API KEY to create project could not be retrieved.');
// eslint-disable-next-line no-process-exit
return process.exit(1);
}

log.info(`${id}: Creating environment ${PROJECT_NAME}...`);
// Creating environment for the test to run
const environment = await createEnvironment(PROJECT_NAME, API_KEY, specFileFTRConfig);
log.info(`${id}: Creating project ${PROJECT_NAME}...`);
// Creating project for the test to run
const project = await createSecurityProject(PROJECT_NAME, API_KEY, specFileFTRConfig);

if (!environment) {
log.info('Failed to create environment.');
if (!project) {
log.info('Failed to create project.');
// eslint-disable-next-line no-process-exit
return process.exit(1);
}

// Reset credentials for elastic user
const credentials = await resetCredentials(environment.id, id, API_KEY);
const credentials = await resetCredentials(project.id, id, API_KEY);

if (!credentials) {
log.info('Credentials could not be reset.');
// eslint-disable-next-line no-process-exit
return process.exit(1);
}

// Wait for 8 minutes in order for the environment to be ready
delay(480000);
// Wait for project to be initialized
await waitForProjectInitialized(project.id, API_KEY);

// Base64 encode the credentials in order to invoke ES and KB APIs
const auth = btoa(`${credentials.username}:${credentials.password}`);

// Wait for elasticsearch status to go green.
await waitForEsStatusGreen(environment.es_url, auth, id);
await waitForEsStatusGreen(project.es_url, auth, id);

// Wait until Kibana is available
await waitForKibanaAvailable(environment.kb_url, auth, id);
await waitForKibanaAvailable(project.kb_url, auth, id);

// Wait for Elasticsearch to be accessible
await waitForEsAccess(project.es_url, auth, id);

// Wait until application is ready
await waitForKibanaLogin(project.kb_url, credentials);

// Normalized the set of available env vars in cypress
const cyCustomEnv = {
CYPRESS_BASE_URL: environment.kb_url,
CYPRESS_BASE_URL: project.kb_url,

ELASTICSEARCH_URL: environment.es_url,
ELASTICSEARCH_URL: project.es_url,
ELASTICSEARCH_USERNAME: credentials.username,
ELASTICSEARCH_PASSWORD: credentials.password,

KIBANA_URL: environment.kb_url,
KIBANA_URL: project.kb_url,
KIBANA_USERNAME: credentials.username,
KIBANA_PASSWORD: credentials.password,

Expand All @@ -418,7 +531,7 @@ ${JSON.stringify(cypressConfigFile, null, 2)}
configFile: cypressConfigFilePath,
config: {
e2e: {
baseUrl: environment.kb_url,
baseUrl: project.kb_url,
},
env: cyCustomEnv,
},
Expand All @@ -434,7 +547,7 @@ ${JSON.stringify(cypressConfigFile, null, 2)}
headed: argv.headed as boolean,
config: {
e2e: {
baseUrl: environment.kb_url,
baseUrl: project.kb_url,
},
numTestsKeptInMemory: 0,
env: cyCustomEnv,
Expand All @@ -445,9 +558,9 @@ ${JSON.stringify(cypressConfigFile, null, 2)}
}
}

// Delete serverless environment
log.info(`${id} : Deleting Environment ${PROJECT_NAME}...`);
await deleteEnvironment(environment.id, PROJECT_NAME, API_KEY);
// Delete serverless project
log.info(`${id} : Deleting project ${PROJECT_NAME}...`);
await deleteSecurityProject(project.id, PROJECT_NAME, API_KEY);

return result;
});
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/security_solution/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,5 +178,6 @@
"@kbn/unified-doc-viewer-plugin",
"@kbn/shared-ux-error-boundary",
"@kbn/zod-helpers",
"@kbn/core-http-common",
]
}