Skip to content
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

[Fleet] Add config revision to fleet agents #60292

Merged
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
3 changes: 2 additions & 1 deletion x-pack/plugins/ingest_manager/common/types/models/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,8 +56,9 @@ interface AgentBase {
access_api_key_id?: string;
default_api_key?: string;
config_id?: string;
config_revision?: number;
config_newest_revision?: number;
last_checkin?: string;
config_updated_at?: string;
actions: AgentAction[];
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
EuiButtonIcon,
EuiContextMenuPanel,
EuiContextMenuItem,
EuiIcon,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react';
Expand Down Expand Up @@ -289,6 +290,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
},
{
field: 'active',
width: '100px',
name: i18n.translate('xpack.ingestManager.agentList.statusColumnTitle', {
defaultMessage: 'Status',
}),
Expand All @@ -299,10 +301,10 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
name: i18n.translate('xpack.ingestManager.agentList.configColumnTitle', {
defaultMessage: 'Configuration',
}),
render: (configId: string) => {
render: (configId: string, agent: Agent) => {
const configName = agentConfigs.find(p => p.id === configId)?.name;
return (
<EuiFlexGroup gutterSize="s" alignItems="baseline" style={{ minWidth: 0 }}>
<EuiFlexGroup gutterSize="s" alignItems="center" style={{ minWidth: 0 }}>
<EuiFlexItem grow={false} style={NO_WRAP_TRUNCATE_STYLE}>
<EuiLink
href={`${CONFIG_DETAILS_URI}${configId}`}
Expand All @@ -312,21 +314,42 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
{configName || configId}
</EuiLink>
</EuiFlexItem>
<EuiFlexItem grow={true}>
<EuiText color="subdued" size="xs" style={{ whiteSpace: 'nowrap' }}>
<FormattedMessage
id="xpack.ingestManager.agentList.revisionNumber"
defaultMessage="rev. {revNumber}"
values={{ revNumber: '999' }} // TODO fix when we have revision
/>
</EuiText>
</EuiFlexItem>
{agent.config_revision && (
<EuiFlexItem grow={false}>
<EuiText color="default" size="xs" className="eui-textNoWrap">
<FormattedMessage
id="xpack.ingestManager.agentList.revisionNumber"
defaultMessage="rev. {revNumber}"
values={{ revNumber: agent.config_revision }}
/>
</EuiText>
</EuiFlexItem>
)}
{agent.config_revision &&
agent.config_newest_revision &&
agent.config_newest_revision > agent.config_revision && (
<EuiFlexItem grow={false}>
<EuiText color="subdued" size="xs" className="eui-textNoWrap">
<EuiIcon size="m" type="alert" color="warning" />
&nbsp;
{true && (
<>
<FormattedMessage
id="xpack.ingestManager.agentList.outOfDateLabel"
defaultMessage="Out-of-date"
/>
</>
)}
</EuiText>
</EuiFlexItem>
)}
</EuiFlexGroup>
);
},
},
{
field: 'local_metadata.agent_version',
width: '100px',
name: i18n.translate('xpack.ingestManager.agentList.versionTitle', {
defaultMessage: 'Version',
}),
Expand Down
3 changes: 2 additions & 1 deletion x-pack/plugins/ingest_manager/server/saved_objects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export const savedObjectMappings = {
config_id: { type: 'keyword' },
last_updated: { type: 'date' },
last_checkin: { type: 'date' },
config_updated_at: { type: 'date' },
config_revision: { type: 'integer' },
config_newest_revision: { type: 'integer' },
// FIXME_INGEST https://github.com/elastic/kibana/issues/56554
default_api_key: { type: 'keyword' },
updated_at: { type: 'date' },
Expand Down
14 changes: 14 additions & 0 deletions x-pack/plugins/ingest_manager/server/services/agents/acks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,22 @@ export async function acknowledgeAgentActions(
});

if (matchedUpdatedActions.length > 0) {
const configRevision = matchedUpdatedActions.reduce((acc, action) => {
if (action.type !== 'CONFIG_CHANGE') {
return acc;
}
const data = action.data ? JSON.parse(action.data as string) : {};

if (data?.config?.id !== agent.config_id) {
return acc;
}

return data?.config?.revision > acc ? data?.config?.revision : acc;
}, agent.config_revision || 0);

await soClient.update<AgentSOAttributes>(AGENT_SAVED_OBJECT_TYPE, agent.id, {
actions: matchedUpdatedActions,
config_revision: configRevision,
});
}

Expand Down
117 changes: 117 additions & 0 deletions x-pack/plugins/ingest_manager/server/services/agents/checkin.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { shouldCreateConfigAction } from './checkin';
import { Agent } from '../../types';

function getAgent(data: Partial<Agent>) {
return { actions: [], ...data } as Agent;
}

describe('Agent checkin service', () => {
describe('shouldCreateConfigAction', () => {
it('should return false if the agent do not have an assigned config', () => {
const res = shouldCreateConfigAction(getAgent({}));

expect(res).toBeFalsy();
});

it('should return true if this is agent first checkin', () => {
const res = shouldCreateConfigAction(getAgent({ config_id: 'config1' }));

expect(res).toBeTruthy();
});

it('should return false agent is already running latest revision', () => {
const res = shouldCreateConfigAction(
getAgent({
config_id: 'config1',
last_checkin: '2018-01-02T00:00:00',
config_revision: 1,
config_newest_revision: 1,
})
);

expect(res).toBeFalsy();
});

it('should return false agent has already latest revision config change action', () => {
const res = shouldCreateConfigAction(
getAgent({
config_id: 'config1',
last_checkin: '2018-01-02T00:00:00',
config_revision: 1,
config_newest_revision: 2,
actions: [
{
id: 'action1',
type: 'CONFIG_CHANGE',
created_at: new Date().toISOString(),
data: JSON.stringify({
config: {
id: 'config1',
revision: 2,
},
}),
},
],
})
);

expect(res).toBeFalsy();
});

it('should return true agent has unrelated config change actions', () => {
const res = shouldCreateConfigAction(
getAgent({
config_id: 'config1',
last_checkin: '2018-01-02T00:00:00',
config_revision: 1,
config_newest_revision: 2,
actions: [
{
id: 'action1',
type: 'CONFIG_CHANGE',
created_at: new Date().toISOString(),
data: JSON.stringify({
config: {
id: 'config2',
revision: 2,
},
}),
},
{
id: 'action1',
type: 'CONFIG_CHANGE',
created_at: new Date().toISOString(),
data: JSON.stringify({
config: {
id: 'config1',
revision: 1,
},
}),
},
],
})
);

expect(res).toBeTruthy();
});

it('should return true if this agent has a new revision', () => {
const res = shouldCreateConfigAction(
getAgent({
config_id: 'config1',
last_checkin: '2018-01-02T00:00:00',
config_revision: 1,
config_newest_revision: 2,
})
);

expect(res).toBeTruthy();
});
});
});
35 changes: 30 additions & 5 deletions x-pack/plugins/ingest_manager/server/services/agents/checkin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ export async function agentCheckin(
const actions = filterActionsForCheckin(agent);

// Generate new agent config if config is updated
if (isNewAgentConfig(agent) && agent.config_id) {
if (agent.config_id && shouldCreateConfigAction(agent)) {
const config = await agentConfigService.getFullConfig(soClient, agent.config_id);
if (config) {
// Assign output API keys
Expand Down Expand Up @@ -149,12 +149,37 @@ function isActionEvent(event: AgentEvent) {
);
}

function isNewAgentConfig(agent: Agent) {
export function shouldCreateConfigAction(agent: Agent): boolean {
if (!agent.config_id) {
return false;
}

const isFirstCheckin = !agent.last_checkin;
const isConfigUpdatedSinceLastCheckin =
agent.last_checkin && agent.config_updated_at && agent.last_checkin <= agent.config_updated_at;
if (isFirstCheckin) {
return true;
}

const isAgentConfigOutdated =
agent.config_revision &&
agent.config_newest_revision &&
agent.config_revision < agent.config_newest_revision;
if (!isAgentConfigOutdated) {
return false;
}

const isActionAlreadyGenerated = !!agent.actions.find(action => {
if (!action.data || action.type !== 'CONFIG_CHANGE') {
return false;
}

const data = JSON.parse(action.data);

return (
data.config.id === agent.config_id && data.config.revision === agent.config_newest_revision
);
});

return isFirstCheckin || isConfigUpdatedSinceLastCheckin;
return !isActionAlreadyGenerated;
}

function filterActionsForCheckin(agent: Agent): AgentAction[] {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ export async function enroll(
current_error_events: undefined,
actions: [],
access_api_key_id: undefined,
config_updated_at: undefined,
last_checkin: undefined,
default_api_key: undefined,
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,18 @@ import { SavedObjectsClientContract } from 'src/core/server';
import { listAgents } from './crud';
import { AGENT_SAVED_OBJECT_TYPE } from '../../constants';
import { unenrollAgents } from './unenroll';
import { agentConfigService } from '../agent_config';

export async function updateAgentsForConfigId(
soClient: SavedObjectsClientContract,
configId: string
) {
const config = await agentConfigService.get(soClient, configId);
if (!config) {
throw new Error('Config not found');
}
let hasMore = true;
let page = 1;
const now = new Date().toISOString();
while (hasMore) {
const { agents } = await listAgents(soClient, {
kuery: `agents.config_id:"${configId}"`,
Expand All @@ -30,7 +34,7 @@ export async function updateAgentsForConfigId(
const agentUpdate = agents.map(agent => ({
id: agent.id,
type: AGENT_SAVED_OBJECT_TYPE,
attributes: { config_updated_at: now },
attributes: { config_newest_revision: config.revision },
}));

await soClient.bulkUpdate(agentUpdate);
Expand Down