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] Can't select managed agent. Bulk upgrade agent UI changes #96087

Merged
merged 6 commits into from
Apr 6, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
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 @@ -148,12 +148,21 @@ export const AgentBulkActions: React.FunctionComponent<{
},
];

const showSelectEverything =
selectionMode === 'manual' &&
selectedAgents.length === selectableAgents &&
Copy link
Member

Choose a reason for hiding this comment

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

selectableAgents is only on the current page right? so you can select everything but have non selectable agents on other page?

Copy link
Contributor Author

@jfsiii jfsiii Apr 6, 2021

Choose a reason for hiding this comment

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

I think that's correct. The current behavior shouldn't change. This expression was just moved from lower and named.

selectableAgents < totalAgents;

const totalActiveAgents = totalAgents - totalInactiveAgents;
const agentCount = selectionMode === 'manual' ? selectedAgents.length : totalActiveAgents;
const agents = selectionMode === 'manual' ? selectedAgents : currentQuery;

return (
<>
{isReassignFlyoutOpen && (
<EuiPortal>
<AgentReassignAgentPolicyFlyout
agents={selectionMode === 'manual' ? selectedAgents : currentQuery}
agents={agents}
onClose={() => {
setIsReassignFlyoutOpen(false);
refreshAgents();
Expand All @@ -164,10 +173,8 @@ export const AgentBulkActions: React.FunctionComponent<{
{isUnenrollModalOpen && (
<EuiPortal>
<AgentUnenrollAgentModal
agents={selectionMode === 'manual' ? selectedAgents : currentQuery}
agentCount={
selectionMode === 'manual' ? selectedAgents.length : totalAgents - totalInactiveAgents
}
agents={agents}
agentCount={agentCount}
onClose={() => {
setIsUnenrollModalOpen(false);
refreshAgents();
Expand All @@ -179,10 +186,8 @@ export const AgentBulkActions: React.FunctionComponent<{
<EuiPortal>
<AgentUpgradeAgentModal
version={kibanaVersion}
agents={selectionMode === 'manual' ? selectedAgents : currentQuery}
agentCount={
selectionMode === 'manual' ? selectedAgents.length : totalAgents - totalInactiveAgents
}
agents={agents}
agentCount={agentCount}
onClose={() => {
setIsUpgradeModalOpen(false);
refreshAgents();
Expand Down Expand Up @@ -230,12 +235,9 @@ export const AgentBulkActions: React.FunctionComponent<{
>
<FormattedMessage
id="xpack.fleet.agentBulkActions.agentsSelected"
defaultMessage="{count, plural, one {# agent} other {# agents}} selected"
defaultMessage="{count, plural, one {# agent} other {# agents} =all {All agents}} selected"
values={{
count:
selectionMode === 'manual'
? selectedAgents.length
: Math.min(totalAgents - totalInactiveAgents, SO_SEARCH_LIMIT),
count: selectionMode === 'manual' ? selectedAgents.length : 'all',
}}
/>
</Button>
Expand All @@ -248,9 +250,7 @@ export const AgentBulkActions: React.FunctionComponent<{
<EuiContextMenu initialPanelId={0} panels={panels} />
</EuiPopover>
</EuiFlexItem>
{selectionMode === 'manual' &&
selectedAgents.length === selectableAgents &&
selectableAgents < totalAgents ? (
{showSelectEverything ? (
<EuiFlexItem grow={false}>
<Button
size="xs"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,14 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
}, {} as { [k: string]: AgentPolicy });
}, [agentPolicies]);

const isAgentSelectable = (agent: Agent) => {
if (!agent.active) return false;

const agentPolicy = agentPolicies.find((p) => p.id === agent.policy_id);
const isManaged = agent.policy_id && agentPolicy?.is_managed === true;
return !isManaged;
};

const columns = [
{
field: 'local_metadata.host.hostname',
Expand All @@ -358,7 +366,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
defaultMessage: 'Agent policy',
}),
render: (policyId: string, agent: Agent) => {
const policyName = agentPolicies.find((p) => p.id === policyId)?.name;
const policyName = agentPoliciesIndexedById[policyId]?.name;
return (
<EuiFlexGroup gutterSize="s" alignItems="center" style={{ minWidth: 0 }}>
<EuiFlexItem grow={false} className="eui-textTruncate">
Expand Down Expand Up @@ -549,7 +557,7 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
totalAgents={totalAgents}
totalInactiveAgents={totalInactiveAgents}
agentStatus={agentsStatus}
selectableAgents={agents?.filter((agent) => agent.active).length || 0}
selectableAgents={agents?.filter(isAgentSelectable).length || 0}
selectionMode={selectionMode}
setSelectionMode={setSelectionMode}
currentQuery={kuery}
Expand Down Expand Up @@ -613,7 +621,17 @@ export const AgentListPage: React.FunctionComponent<{}> = () => {
setSelectedAgents(newAgents);
setSelectionMode('manual');
},
selectable: (agent: Agent) => agent.active,
selectable: isAgentSelectable,
selectableMessage: (selectable, agent) => {
if (selectable) return '';
if (!agent.active) {
return 'This agent is not active';
}
if (agent.policy_id && agentPoliciesIndexedById[agent.policy_id].is_managed) {
return 'This action is not available for agents enrolled in an externally managed agent policy';
}
return '';
},
}
: undefined
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
const { notifications } = useStartServices();
const [isSubmitting, setIsSubmitting] = useState(false);
const isSingleAgent = Array.isArray(agents) && agents.length === 1;
const isAllAgents = agents === '';
async function onSubmit() {
try {
setIsSubmitting(true);
const { error } = isSingleAgent
const { data, error } = isSingleAgent
? await sendPostAgentUpgrade((agents[0] as Agent).id, {
version,
})
Expand All @@ -47,22 +48,57 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
if (error) {
throw error;
}

const counts = Object.entries(data || {}).reduce(
(acc, [agentId, result]) => {
++acc.total;
++acc[result.success ? 'success' : 'error'];
return acc;
},
{
total: 0,
success: 0,
error: 0,
}
);
setIsSubmitting(false);
const successMessage = isSingleAgent
? i18n.translate('xpack.fleet.upgradeAgents.successSingleNotificationTitle', {
defaultMessage: 'Upgrading agent',
defaultMessage: 'Upgraded {count} agent',
values: { count: 1 },
})
: i18n.translate('xpack.fleet.upgradeAgents.successMultiNotificationTitle', {
defaultMessage: 'Upgrading agents',
defaultMessage: 'Upgraded {success} of {total} agents',
values: { count: isAllAgents || agentCount, ...counts },
});
notifications.toasts.addSuccess(successMessage);
if (counts.success === counts.total) {
notifications.toasts.addSuccess(successMessage);
} else if (counts.error === counts.total) {
notifications.toasts.addDanger(
i18n.translate('xpack.fleet.upgradeAgents.bulkResultAllErrorsNotificationTitle', {
defaultMessage:
'Error upgrading {count, plural, one {agent} other {{count} agents} =true {all selected agents}}',
values: { count: isAllAgents || agentCount },
})
);
} else {
notifications.toasts.addWarning({
title: successMessage,
text: i18n.translate('xpack.fleet.upgradeAgents.bulkResultErrorResultsSummary', {
defaultMessage:
'{count, plural, one {agent was} other {{count} agents were} not successful}',
values: { count: isAllAgents || agentCount },
}),
});
}
onClose();
} catch (error) {
setIsSubmitting(false);
notifications.toasts.addError(error, {
title: i18n.translate('xpack.fleet.upgradeAgents.fatalErrorNotificationTitle', {
defaultMessage: 'Error upgrading {count, plural, one {agent} other {agents}}',
values: { count: agentCount },
defaultMessage:
'Error upgrading {count, plural, one {agent} other {{count} agents} =true {all selected agents}}',
values: { count: isAllAgents || agentCount },
}),
});
}
Expand All @@ -75,19 +111,20 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
<EuiFlexItem grow={false}>
{isSingleAgent ? (
<FormattedMessage
id="xpack.fleet.upgradeAgents.deleteSingleTitle"
defaultMessage="Upgrade agent?"
id="xpack.fleet.upgradeAgents.upgradeSingleTitle"
defaultMessage="Upgrade agent to latest version"
/>
) : (
<FormattedMessage
id="xpack.fleet.upgradeAgents.deleteMultipleTitle"
defaultMessage="Upgrade {count} agents?"
values={{ count: agentCount }}
id="xpack.fleet.upgradeAgents.upgradeMultipleTitle"
defaultMessage="Upgrade {count, plural, one {agent} other {{count} agents} =true {all selected agents}} to latest version"
values={{ count: isAllAgents || agentCount }}
/>
)}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBetaBadge
iconType="beaker"
label={
<FormattedMessage
id="xpack.fleet.upgradeAgents.experimentalLabel"
Expand Down Expand Up @@ -122,8 +159,8 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
) : (
<FormattedMessage
id="xpack.fleet.upgradeAgents.confirmMultipleButtonLabel"
defaultMessage="Upgrade {count} agents"
values={{ count: agentCount }}
defaultMessage="Upgrade {count, plural, one {agent} other {{count} agents} =true {all selected agents}}"
values={{ count: isAllAgents || agentCount }}
/>
)
}
Expand All @@ -132,7 +169,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
{isSingleAgent ? (
<FormattedMessage
id="xpack.fleet.upgradeAgents.upgradeSingleDescription"
defaultMessage="This action upgrades the agent running on '{hostName}' to version {version}. You can't undo this upgrade."
defaultMessage="This action upgrades the agent running on '{hostName}' to version {version}. This action can not be undone. Are you sure you wish to continue?"
jfsiii marked this conversation as resolved.
Show resolved Hide resolved
values={{
hostName: ((agents[0] as Agent).local_metadata.host as any).hostname,
version,
Expand All @@ -141,7 +178,7 @@ export const AgentUpgradeAgentModal: React.FunctionComponent<Props> = ({
) : (
<FormattedMessage
id="xpack.fleet.upgradeAgents.upgradeMultipleDescription"
defaultMessage="This action upgrades multiple agents to version {version}. You can't undo this upgrade."
defaultMessage="This action upgrades multiple agents to version {version}. This action can not be undone. Are you sure you wish to continue?"
values={{ version }}
/>
)}
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/server/services/agent_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -397,7 +397,7 @@ class AgentPolicyService {
soClient: SavedObjectsClientContract,
esClient: ElasticsearchClient,
options?: { user?: AuthenticatedUser }
): Promise<Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>>> {
): Promise<SavedObjectsBulkUpdateResponse<AgentPolicy>> {
const currentPolicies = await soClient.find<AgentPolicySOAttributes>({
type: SAVED_OBJECT_TYPE,
fields: ['revision'],
Expand Down
26 changes: 18 additions & 8 deletions x-pack/plugins/fleet/server/services/agents/upgrade.ts
Original file line number Diff line number Diff line change
Expand Up @@ -109,8 +109,6 @@ export async function sendUpgradeAgentsActions(
} else if ('kuery' in options) {
givenAgents = await getAgents(esClient, options);
}
const givenOrder =
'agentIds' in options ? options.agentIds : givenAgents.map((agent) => agent.id);

// get any policy ids from upgradable agents
const policyIdsToGet = new Set(
Expand All @@ -125,25 +123,33 @@ export async function sendUpgradeAgentsActions(
acc[policy.id] = policy.is_managed;
return acc;
}, {});
const isManagedAgent = (agent: Agent) => agent.policy_id && managedPolicies[agent.policy_id];

// results from getAgents with options.kuery '' (or even 'active:false') may include managed agents
// filter them out unless options.force
const agentsToCheckUpgradeable =
'kuery' in options && !options.force
? givenAgents.filter((agent: Agent) => !isManagedAgent(agent))
: givenAgents;

// Filter out agents currently unenrolling, unenrolled, or not upgradeable b/c of version check
const kibanaVersion = appContextService.getKibanaVersion();
const agentResults = await Promise.allSettled(
givenAgents.map(async (agent) => {
const upgradeableResults = await Promise.allSettled(
agentsToCheckUpgradeable.map(async (agent) => {
// Filter out agents currently unenrolling, unenrolled, or not upgradeable b/c of version check
const isAllowed = options.force || isAgentUpgradeable(agent, kibanaVersion);
if (!isAllowed) {
throw new IngestManagerError(`${agent.id} is not upgradeable`);
}

if (!options.force && agent.policy_id && managedPolicies[agent.policy_id]) {
if (!options.force && isManagedAgent(agent)) {
throw new IngestManagerError(`Cannot upgrade agent in managed policy ${agent.policy_id}`);
}
return agent;
})
);

// Filter to agents that do not already use the new agent policy ID
const agentsToUpdate = agentResults.reduce<Agent[]>((agents, result, index) => {
// Filter & record errors from results
const agentsToUpdate = upgradeableResults.reduce<Agent[]>((agents, result, index) => {
if (result.status === 'fulfilled') {
agents.push(result.value);
} else {
Expand Down Expand Up @@ -182,6 +188,10 @@ export async function sendUpgradeAgentsActions(
},
}))
);

const givenOrder =
'agentIds' in options ? options.agentIds : agentsToCheckUpgradeable.map((agent) => agent.id);

const orderedOut = givenOrder.map((agentId) => {
const hasError = agentId in outgoingErrors;
const result: BulkActionResult = {
Expand Down