-
Notifications
You must be signed in to change notification settings - Fork 8.6k
[Security Solution][Endpoint] New enroll endpoint host function CI specific for Cypress tests to use cached agent files #171399
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
09ab15f
048d982
1cdc29e
85ed629
afb49d7
0c98fd6
ada8e9d
e8f3b98
dd64fc3
296e16e
798ae8d
d2fab22
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| /* | ||
| * 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 { kibanaPackageJson } from '@kbn/repo-info'; | ||
| import type { ToolingLog } from '@kbn/tooling-log'; | ||
| import type { KbnClient } from '@kbn/test/src/kbn_client'; | ||
| import { isFleetServerRunning } from '../../../../scripts/endpoint/common/fleet_server/fleet_server_services'; | ||
| import type { HostVm } from '../../../../scripts/endpoint/common/types'; | ||
| import type { BaseVmCreateOptions } from '../../../../scripts/endpoint/common/vm_services'; | ||
| import { createVm } from '../../../../scripts/endpoint/common/vm_services'; | ||
| import { | ||
| fetchAgentPolicyEnrollmentKey, | ||
| fetchFleetServerUrl, | ||
| getAgentDownloadUrl, | ||
| getAgentFileName, | ||
| getOrCreateDefaultAgentPolicy, | ||
| waitForHostToEnroll, | ||
| } from '../../../../scripts/endpoint/common/fleet_services'; | ||
| import type { DownloadedAgentInfo } from '../../../../scripts/endpoint/common/agent_downloads_service'; | ||
| import { | ||
| downloadAndStoreAgent, | ||
| isAgentDownloadFromDiskAvailable, | ||
| } from '../../../../scripts/endpoint/common/agent_downloads_service'; | ||
|
|
||
| export interface CreateAndEnrollEndpointHostCIOptions | ||
| extends Pick<BaseVmCreateOptions, 'disk' | 'cpus' | 'memory'> { | ||
| kbnClient: KbnClient; | ||
| log: ToolingLog; | ||
| /** The fleet Agent Policy ID to use for enrolling the agent */ | ||
| agentPolicyId: string; | ||
| /** version of the Agent to install. Defaults to stack version */ | ||
| version?: string; | ||
| /** The name for the host. Will also be the name of the VM */ | ||
| hostname?: string; | ||
| /** If `version` should be exact, or if this is `true`, then the closest version will be used. Defaults to `false` */ | ||
| useClosestVersionMatch?: boolean; | ||
| } | ||
|
|
||
| export interface CreateAndEnrollEndpointHostCIResponse { | ||
| hostname: string; | ||
| agentId: string; | ||
| hostVm: HostVm; | ||
| } | ||
|
|
||
| /** | ||
| * Creates a new virtual machine (host) and enrolls that with Fleet | ||
| */ | ||
| export const createAndEnrollEndpointHostCI = async ({ | ||
| kbnClient, | ||
| log, | ||
| agentPolicyId, | ||
| cpus, | ||
| disk, | ||
| memory, | ||
| hostname, | ||
| version = kibanaPackageJson.version, | ||
| useClosestVersionMatch = true, | ||
| }: CreateAndEnrollEndpointHostCIOptions): Promise<CreateAndEnrollEndpointHostCIResponse> => { | ||
| const vmName = hostname ?? `test-host-${Math.random().toString().substring(2, 6)}`; | ||
|
|
||
| const fileNameNoExtension = getAgentFileName(version); | ||
| const agentFileName = `${fileNameNoExtension}.tar.gz`; | ||
| let agentDownload: DownloadedAgentInfo | undefined; | ||
|
|
||
| // Check if agent file is already on disk before downloading it again | ||
| agentDownload = isAgentDownloadFromDiskAvailable(agentFileName); | ||
|
|
||
| // If it has not been already downloaded, it should be downloaded. | ||
| if (!agentDownload) { | ||
| log.warning( | ||
| `There is no agent installer for ${agentFileName} present on disk, trying to download it now.` | ||
| ); | ||
| const { url: agentUrl } = await getAgentDownloadUrl(version, useClosestVersionMatch, log); | ||
| agentDownload = await downloadAndStoreAgent(agentUrl, agentFileName); | ||
| } | ||
|
|
||
| const hostVm = await createVm({ | ||
| type: 'vagrant', | ||
| name: vmName, | ||
| log, | ||
| // eslint-disable-next-line @typescript-eslint/no-non-null-assertion | ||
| agentDownload: agentDownload!, | ||
| disk, | ||
| cpus, | ||
| memory, | ||
| }); | ||
|
|
||
| if (!(await isFleetServerRunning(kbnClient))) { | ||
| throw new Error(`Fleet server does not seem to be running on this instance of kibana!`); | ||
| } | ||
|
|
||
| const policyId = agentPolicyId || (await getOrCreateDefaultAgentPolicy({ kbnClient, log })).id; | ||
| const [fleetServerUrl, enrollmentToken] = await Promise.all([ | ||
| fetchFleetServerUrl(kbnClient), | ||
| fetchAgentPolicyEnrollmentKey(kbnClient, policyId), | ||
| ]); | ||
|
|
||
| const agentEnrollCommand = [ | ||
| 'sudo', | ||
|
|
||
| `./${fileNameNoExtension}/elastic-agent`, | ||
|
|
||
| 'install', | ||
|
|
||
| '--insecure', | ||
|
|
||
| '--force', | ||
|
|
||
| '--url', | ||
| fleetServerUrl, | ||
|
|
||
| '--enrollment-token', | ||
| enrollmentToken, | ||
| ].join(' '); | ||
|
|
||
| log.info(`Enrolling Elastic Agent with Fleet`); | ||
| log.verbose('Enrollment command:', agentEnrollCommand); | ||
|
|
||
| await hostVm.exec(agentEnrollCommand); | ||
|
|
||
| const { id: agentId } = await waitForHostToEnroll(kbnClient, log, hostVm.name, 240000); | ||
|
|
||
| return { | ||
| hostname: hostVm.name, | ||
| agentId, | ||
| hostVm, | ||
| }; | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,10 +26,7 @@ import { | |
| import type { DeleteAllEndpointDataResponse } from '../../../../scripts/endpoint/common/delete_all_endpoint_data'; | ||
| import { deleteAllEndpointData } from '../../../../scripts/endpoint/common/delete_all_endpoint_data'; | ||
| import { waitForEndpointToStreamData } from '../../../../scripts/endpoint/common/endpoint_metadata_services'; | ||
| import type { | ||
| CreateAndEnrollEndpointHostOptions, | ||
| CreateAndEnrollEndpointHostResponse, | ||
| } from '../../../../scripts/endpoint/common/endpoint_host_services'; | ||
| import type { CreateAndEnrollEndpointHostResponse } from '../../../../scripts/endpoint/common/endpoint_host_services'; | ||
| import { | ||
| createAndEnrollEndpointHost, | ||
| destroyEndpointHost, | ||
|
|
@@ -66,6 +63,11 @@ import { | |
| indexFleetEndpointPolicy, | ||
| } from '../../../../common/endpoint/data_loaders/index_fleet_endpoint_policy'; | ||
| import { cyLoadEndpointDataHandler } from './plugin_handlers/endpoint_data_loader'; | ||
| import type { | ||
| CreateAndEnrollEndpointHostCIOptions, | ||
| CreateAndEnrollEndpointHostCIResponse, | ||
| } from './create_and_enroll_endpoint_host_ci'; | ||
| import { createAndEnrollEndpointHostCI } from './create_and_enroll_endpoint_host_ci'; | ||
|
|
||
| /** | ||
| * Test Role/User loader for cypress. Checks to see if running in serverless and handles it as appropriate | ||
|
|
@@ -290,40 +292,48 @@ export const dataLoadersForRealEndpoints = ( | |
|
|
||
| on('task', { | ||
| createEndpointHost: async ( | ||
| options: Omit<CreateAndEnrollEndpointHostOptions, 'log' | 'kbnClient'> | ||
| ): Promise<CreateAndEnrollEndpointHostResponse> => { | ||
| options: Omit<CreateAndEnrollEndpointHostCIOptions, 'log' | 'kbnClient'> | ||
| ): Promise<CreateAndEnrollEndpointHostCIResponse> => { | ||
| const { kbnClient, log } = await stackServicesPromise; | ||
|
|
||
| let retryAttempt = 0; | ||
| const attemptCreateEndpointHost = async (): Promise<CreateAndEnrollEndpointHostResponse> => { | ||
| try { | ||
| log.info(`Creating endpoint host, attempt ${retryAttempt}`); | ||
| const newHost = await createAndEnrollEndpointHost({ | ||
| useClosestVersionMatch: true, | ||
| ...options, | ||
| log, | ||
| kbnClient, | ||
| }); | ||
| await waitForEndpointToStreamData(kbnClient, newHost.agentId, 360000); | ||
| return newHost; | ||
| } catch (err) { | ||
| log.info(`Caught error when setting up the agent: ${err}`); | ||
| if (retryAttempt === 0 && err.agentId) { | ||
| retryAttempt++; | ||
| await destroyEndpointHost(kbnClient, { | ||
| hostname: err.hostname || '', // No hostname in CI env for vagrant | ||
| agentId: err.agentId, | ||
| }); | ||
| log.info(`Deleted endpoint host ${err.agentId} and retrying`); | ||
| return attemptCreateEndpointHost(); | ||
| } else { | ||
| log.info( | ||
| `${retryAttempt} attempts of creating endpoint host failed, reason for the last failure was ${err}` | ||
| ); | ||
| throw err; | ||
| const attemptCreateEndpointHost = | ||
| async (): Promise<CreateAndEnrollEndpointHostCIResponse> => { | ||
| try { | ||
| log.info(`Creating endpoint host, attempt ${retryAttempt}`); | ||
| const newHost = process.env.CI | ||
| ? await createAndEnrollEndpointHostCI({ | ||
| useClosestVersionMatch: true, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if I understand correctly options can contain
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. even more confusing since we set useClosestVersionMatch to default to true in the function params
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This looks like a typo in the order introduced here. 🤔
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Good call, this was a copy/paste of the existing code here. I'm ok removing this since the default value is |
||
| ...options, | ||
| log, | ||
| kbnClient, | ||
| }) | ||
| : await createAndEnrollEndpointHost({ | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You can now remove the CI check and dependent code in
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, I added a note in the description about doing that in a follow up pr. |
||
| useClosestVersionMatch: true, | ||
| ...options, | ||
| log, | ||
| kbnClient, | ||
| }); | ||
| await waitForEndpointToStreamData(kbnClient, newHost.agentId, 360000); | ||
| return newHost; | ||
| } catch (err) { | ||
| log.info(`Caught error when setting up the agent: ${err}`); | ||
| if (retryAttempt === 0 && err.agentId) { | ||
| retryAttempt++; | ||
| await destroyEndpointHost(kbnClient, { | ||
| hostname: err.hostname || '', // No hostname in CI env for vagrant | ||
| agentId: err.agentId, | ||
| }); | ||
| log.info(`Deleted endpoint host ${err.agentId} and retrying`); | ||
| return attemptCreateEndpointHost(); | ||
| } else { | ||
| log.info( | ||
| `${retryAttempt} attempts of creating endpoint host failed, reason for the last failure was ${err}` | ||
| ); | ||
| throw err; | ||
| } | ||
| } | ||
| } | ||
| }; | ||
| }; | ||
|
|
||
| return attemptCreateEndpointHost(); | ||
| }, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| /* | ||
| * 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('../../../../../src/setup_node_env'); | ||
| require('./agent_downloader_cli').cli(); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| /* | ||
| * 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 { ok } from 'assert'; | ||
| import type { RunFn } from '@kbn/dev-cli-runner'; | ||
| import type { ToolingLog } from '@kbn/tooling-log'; | ||
| import { getAgentDownloadUrl, getAgentFileName } from '../common/fleet_services'; | ||
| import { downloadAndStoreAgent } from '../common/agent_downloads_service'; | ||
|
|
||
| const downloadAndStoreElasticAgent = async ( | ||
| version: string, | ||
| closestMatch: boolean, | ||
| log: ToolingLog | ||
| ) => { | ||
| const downloadUrlResponse = await getAgentDownloadUrl(version, closestMatch, log); | ||
| const fileNameNoExtension = getAgentFileName(version); | ||
| const agentFile = `${fileNameNoExtension}.tar.gz`; | ||
| await downloadAndStoreAgent(downloadUrlResponse.url, agentFile); | ||
| }; | ||
|
|
||
| export const agentDownloaderRunner: RunFn = async (cliContext) => { | ||
| ok(cliContext.flags.version, 'version argument is required'); | ||
| await downloadAndStoreElasticAgent( | ||
| cliContext.flags.version as string, | ||
| cliContext.flags.closestMatch as boolean, | ||
| cliContext.log | ||
| ); | ||
| }; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| /* | ||
| * 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 { run } from '@kbn/dev-cli-runner'; | ||
| import { agentDownloaderRunner } from './agent_downloader'; | ||
|
|
||
| export const cli = () => { | ||
| run( | ||
| agentDownloaderRunner, | ||
|
|
||
| // Options | ||
| { | ||
| description: `Elastic Agent downloader`, | ||
| flags: { | ||
| string: ['version'], | ||
| boolean: ['closestMatch'], | ||
| default: { | ||
| closestMatch: true, | ||
| }, | ||
| help: ` | ||
| --version Required. Elastic agent version to be downloaded. | ||
| --closestMatch Optional. Use closest elastic agent version to match with. | ||
| `, | ||
| }, | ||
| } | ||
| ); | ||
| }; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do you think combining two for loops into one would be possible? What are the beenfits of doing it separately?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah I think should be okay to download ES snapshot and agent in the same loop.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I went for that path in order to not block the existing code path if something fails with the elastic agent download