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
65 changes: 0 additions & 65 deletions oas_docs/output/kibana.serverless.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -32362,71 +32362,6 @@ paths:
x-metaTags:
- content: Kibana, Elastic Cloud Serverless
name: product_name
/api/fleet/agents/{agentId}/effective_config:
get:
description: |-
**Spaces method and path for this operation:**

<div><span class="operation-verb get">get</span>&nbsp;<span class="operation-path">/s/{space_id}/api/fleet/agents/{agentId}/effective_config</span></div>

Refer to [Spaces](https://www.elastic.co/docs/deploy-manage/manage-spaces) for more information.

Get an agent's effective config by ID.<br/><br/>[Required authorization] Route required privileges: fleet-agents-read.
operationId: get-fleet-agents-agentid-effective-config
parameters:
- description: The agent ID to get effective config of
in: path
name: agentId
required: true
schema:
type: string
responses:
'200':
content:
application/json:
examples:
successResponse:
value:
effective_config: {}
schema:
additionalProperties: false
type: object
properties:
effective_config: {}
required:
- effective_config
description: 'OK: A successful request.'
'400':
content:
application/json:
examples:
badRequestResponse:
value:
message: Bad Request
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
attributes: {}
error:
type: string
errorType:
type: string
message:
type: string
statusCode:
type: number
required:
- message
- attributes
description: A bad request.
summary: Get an agent's effective config
tags:
- Elastic Agents
x-metaTags:
- content: Kibana, Elastic Cloud Serverless
name: product_name
/api/fleet/agents/{agentId}/migrate:
post:
description: |-
Expand Down
65 changes: 0 additions & 65 deletions oas_docs/output/kibana.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -34935,71 +34935,6 @@ paths:
x-metaTags:
- content: Kibana
name: product_name
/api/fleet/agents/{agentId}/effective_config:
get:
description: |-
**Spaces method and path for this operation:**

<div><span class="operation-verb get">get</span>&nbsp;<span class="operation-path">/s/{space_id}/api/fleet/agents/{agentId}/effective_config</span></div>

Refer to [Spaces](https://www.elastic.co/docs/deploy-manage/manage-spaces) for more information.

Get an agent's effective config by ID.<br/><br/>[Required authorization] Route required privileges: fleet-agents-read.
operationId: get-fleet-agents-agentid-effective-config
parameters:
- description: The agent ID to get effective config of
in: path
name: agentId
required: true
schema:
type: string
responses:
'200':
content:
application/json:
examples:
successResponse:
value:
effective_config: {}
schema:
additionalProperties: false
type: object
properties:
effective_config: {}
required:
- effective_config
description: 'OK: A successful request.'
'400':
content:
application/json:
examples:
badRequestResponse:
value:
message: Bad Request
schema:
additionalProperties: false
description: Generic Error
type: object
properties:
attributes: {}
error:
type: string
errorType:
type: string
message:
type: string
statusCode:
type: number
required:
- message
- attributes
description: A bad request.
summary: Get an agent's effective config
tags:
- Elastic Agents
x-metaTags:
- content: Kibana
name: product_name
/api/fleet/agents/{agentId}/migrate:
post:
description: |-
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ const _allowedExperimentalValues = {
enableVarGroups: false, // When enabled, var_groups from the package spec drive conditional variable visibility and input filtering.
enableIntegrationInactivityAlerting: true, // When enabled, an inactivity monitoring alerting rule template is created on fresh integration package install.
enableSimplifiedAgentlessUX: true, // When enabled, the agentless deployment mode will be simplified for single input/datastreams integrations.
enableOpAMP: false, // When enabled, OpAMP features will be available in the API and UI.
};

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,8 @@ import { SearchBar } from '../../../components';
import { AgentPolicySummaryLine } from '../../../../../components';
import { LinkedAgentCount, AgentPolicyActionMenu } from '../components';

import { OPAMP_POLICY_NAME } from '../../agents/agent_list_page/components/add_collector_flyout';

import { CreateAgentPolicyFlyout } from './components';

export const AgentPolicyListPage: React.FunctionComponent<{}> = () => {
Expand Down Expand Up @@ -88,10 +90,11 @@ export const AgentPolicyListPage: React.FunctionComponent<{}> = () => {

// Hide agentless policies by default unless showAgentless toggle is enabled
const getSearchWithDefaults = (newSearch: string) => {
const kueryHideOpAMP = `NOT ${agentPolicySavedObjectType}.name:"${OPAMP_POLICY_NAME}"`;
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why hiding OpAmp agents?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The agents won't be hidden, only the agent policy "OpAMP" because it's not intended to be used for anything else. We could probably show it as a managed policy (so users can't enroll Elastic Agents to it).

if (showAgentless) {
return newSearch;
return newSearch.trim() ? `(${kueryHideOpAMP}) AND (${newSearch})` : kueryHideOpAMP;
}
const defaultSearch = `NOT ${agentPolicySavedObjectType}.supports_agentless:true`;
const defaultSearch = `NOT ${agentPolicySavedObjectType}.supports_agentless:true AND (${kueryHideOpAMP})`;
return newSearch.trim() ? `(${defaultSearch}) AND (${newSearch})` : defaultSearch;
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,7 +89,8 @@ export const AgentDashboardLink: React.FunctionComponent<{
const { isInstalled, link, isLoading } = useAgentDashboardLink(agent, packageName);
const { getHref } = useLink();

const isLogAndMetricsEnabled = agentPolicy?.monitoring_enabled?.length ?? 0 > 0;
const isLogAndMetricsEnabled =
(agentPolicy?.monitoring_enabled?.length ?? 0) > 0 || agent.type === 'OPAMP';
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fix a small bug where the metrics dashboard link was hidden because the opamp agent policy doesn't have monitoring enabled.

Before:
Image

After:
Image


const buttonArgs =
!isInstalled || isLoading || !isLogAndMetricsEnabled ? { disabled: true } : { href: link };
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,209 @@
/*
* 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 React from 'react';
import { waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@kbn/react-query';

import type { TestRenderer } from '../../../../../../mock';
import { createFleetTestRendererMock } from '../../../../../../mock';
import {
sendCreateAgentPolicyForRq,
sendGetEnrollmentAPIKeys,
sendGetOneAgentPolicy,
useGetFleetServerHosts,
useFleetStatus,
} from '../../../../hooks';
import { usePollingAgentCount } from '../../../../components';

import { AddCollectorFlyout } from './add_collector_flyout';

jest.mock('../../../../hooks', () => ({
...jest.requireActual('../../../../hooks'),
sendGetOneAgentPolicy: jest.fn(),
sendCreateAgentPolicyForRq: jest.fn(),
sendGetEnrollmentAPIKeys: jest.fn(),
useGetFleetServerHosts: jest.fn(),
useFleetStatus: jest.fn(),
}));
jest.mock('../../../../components', () => ({
AgentEnrollmentConfirmationStep: () => ({
title: 'Confirm enrollment',
children: <div>Confirmation</div>,
}),
usePollingAgentCount: jest.fn(),
}));

const mockedSendGetOneAgentPolicy = jest.mocked(sendGetOneAgentPolicy);
const mockedSendCreateAgentPolicyForRq = jest.mocked(sendCreateAgentPolicyForRq);
const mockedSendGetEnrollmentAPIKeys = jest.mocked(sendGetEnrollmentAPIKeys);
const mockedUseGetFleetServerHosts = jest.mocked(useGetFleetServerHosts);
const mockedUsePollingAgentCount = jest.mocked(usePollingAgentCount);
const mockedUseFleetStatus = jest.mocked(useFleetStatus);

describe('AddCollectorFlyout', () => {
let renderer: TestRenderer;

const renderFlyout = () =>
renderer.render(<AddCollectorFlyout onClose={jest.fn()} onClickViewAgents={jest.fn()} />);

beforeEach(() => {
renderer = createFleetTestRendererMock();
jest.clearAllMocks();

mockedUseGetFleetServerHosts.mockReturnValue({
data: {
items: [
{
is_default: true,
host_urls: ['https://fleet.example:8220'],
},
],
},
isLoading: false,
isError: false,
resendRequest: jest.fn(),
} as any);

mockedUsePollingAgentCount.mockReturnValue({
enrolledAgentIds: [],
total: 0,
} as any);

mockedUseFleetStatus.mockReturnValue({ spaceId: 'default' } as any);
});

it('uses existing OpAMP policy and renders generated configuration', async () => {
mockedSendGetOneAgentPolicy.mockResolvedValue({
data: { item: { id: 'opamp' } },
} as any);
mockedSendGetEnrollmentAPIKeys.mockResolvedValue({
data: { items: [{ api_key: 'existing-token' }] },
} as any);

const component = renderFlyout();

await waitFor(() => {
expect(mockedSendGetOneAgentPolicy).toHaveBeenCalledWith('opamp');
expect(mockedSendCreateAgentPolicyForRq).not.toHaveBeenCalled();
expect(mockedSendGetEnrollmentAPIKeys).toHaveBeenCalledWith({
page: 1,
perPage: 1,
kuery: 'policy_id:"opamp"',
});
});

const configYaml = component.getByTestId('opampConfigYaml').textContent;
expect(configYaml).toContain('Authorization: ApiKey existing-token');
expect(configYaml).toContain('endpoint: https://fleet.example:8220/v1/opamp');
});

it('creates OpAMP policy when missing and then fetches enrollment token', async () => {
mockedSendGetOneAgentPolicy.mockResolvedValue({
error: { statusCode: 404 },
} as any);
mockedSendCreateAgentPolicyForRq.mockResolvedValue({
item: { id: 'opamp' },
} as any);
mockedSendGetEnrollmentAPIKeys.mockResolvedValue({
data: { items: [{ api_key: 'created-token' }] },
} as any);

const component = renderFlyout();

await waitFor(() => {
expect(mockedSendCreateAgentPolicyForRq).toHaveBeenCalledWith({
name: 'OpAMP',
id: 'opamp',
namespace: 'default',
description: 'Agent policy for OpAMP collectors',
is_managed: true,
});
});

const configYaml = component.getByTestId('opampConfigYaml').textContent;
expect(configYaml).toContain('Authorization: ApiKey created-token');
});

it('uses space-prefixed policy ID when spaceId is non-default', async () => {
mockedUseFleetStatus.mockReturnValue({ spaceId: 'my-space' } as any);

mockedSendGetOneAgentPolicy.mockResolvedValue({
data: { item: { id: 'my-space-opamp' } },
} as any);
mockedSendGetEnrollmentAPIKeys.mockResolvedValue({
data: { items: [{ api_key: 'space-token' }] },
} as any);

const component = renderFlyout();

await waitFor(() => {
expect(mockedSendGetOneAgentPolicy).toHaveBeenCalledWith('my-space-opamp');
expect(mockedSendGetEnrollmentAPIKeys).toHaveBeenCalledWith({
page: 1,
perPage: 1,
kuery: 'policy_id:"my-space-opamp"',
});
});

const configYaml = component.getByTestId('opampConfigYaml').textContent;
expect(configYaml).toContain('Authorization: ApiKey space-token');
});

it('creates space-prefixed policy when missing in non-default space', async () => {
mockedUseFleetStatus.mockReturnValue({ spaceId: 'my-space' } as any);

mockedSendGetOneAgentPolicy.mockResolvedValue({
error: { statusCode: 404 },
} as any);
mockedSendCreateAgentPolicyForRq.mockResolvedValue({
item: { id: 'my-space-opamp' },
} as any);
mockedSendGetEnrollmentAPIKeys.mockResolvedValue({
data: { items: [{ api_key: 'space-created-token' }] },
} as any);

const component = renderFlyout();

await waitFor(() => {
expect(mockedSendCreateAgentPolicyForRq).toHaveBeenCalledWith({
name: 'OpAMP',
id: 'my-space-opamp',
namespace: 'default',
description: 'Agent policy for OpAMP collectors',
is_managed: true,
});
});

const configYaml = component.getByTestId('opampConfigYaml').textContent;
expect(configYaml).toContain('Authorization: ApiKey space-created-token');
});

it('renders a user-facing error when policy/token setup fails', async () => {
mockedSendGetOneAgentPolicy.mockRejectedValue(new Error('setup failed'));

const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
},
});
const wrapper = ({ children }: { children: React.ReactNode }) => (
<renderer.AppWrapper>
<QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
</renderer.AppWrapper>
);

const component = renderer.render(
<AddCollectorFlyout onClose={jest.fn()} onClickViewAgents={jest.fn()} />,
{ wrapper }
);

await waitFor(() => {
expect(component.getByText('setup failed')).toBeInTheDocument();
});
});
});
Loading
Loading