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] unenroll agent from the details page #48286

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
11 changes: 11 additions & 0 deletions x-pack/legacy/plugins/fleet/common/return_types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,17 @@ export interface ReturnTypeBulkUpsert extends BaseReturnType {
}>;
}

export interface ReturnTypeBulkUnenroll extends BaseReturnType {
results: Array<{
id: string;
success: boolean;
action: 'unenrolled';
error?: {
message: string;
};
}>;
}

// list
export interface ReturnTypeList<T> extends BaseReturnType {
list: T[];
Expand Down
40 changes: 40 additions & 0 deletions x-pack/legacy/plugins/fleet/dev_docs/api/agents_unenroll.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Enroll Fleet agent API

Unenroll an agent

## Request

`POST /api/fleet/agents/unenroll`

## Request body

- `ids` (Optional, string) An list of agent id to unenroll.
- `kuery` (Optional, string) a kibana query to search for agent to unenroll.
nchaulet marked this conversation as resolved.
Show resolved Hide resolved

> Note: one and only of this keys should be present:

## Response code

`200` Indicates a successful call.

## Example

```js
POST /api/fleet/agents/enroll
{
"ids": ['agent1'],
}
```

The API returns the following:

```js
{
"results": [{
"success":true,
"id":"agent1",
"action":"unenrolled"
}],
"success":true
}
```
39 changes: 24 additions & 15 deletions x-pack/legacy/plugins/fleet/public/components/agent_health.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ const Status = {
<FormattedMessage id="xpack.fleet.agentHealth.offlineStatusText" defaultMessage="Offline" />
</EuiHealth>
),
Inactive: (
<EuiHealth color="subdued">
<FormattedMessage id="xpack.fleet.agentHealth.inactiveStatusText" defaultMessage="Inactive" />
</EuiHealth>
),
Warning: (
<EuiHealth color="warning">
<FormattedMessage id="xpack.fleet.agentHealth.warningStatusText" defaultMessage="Error" />
Expand All @@ -50,21 +55,25 @@ export const AgentHealth: React.SFC<Props> = ({ agent }) => {

let status: React.ReactElement = Status.Online;

switch (type) {
case AGENT_TYPE_PERMANENT:
if (intervalsSinceLastCheckIn >= 4) {
status = Status.Error;
break;
}
if (intervalsSinceLastCheckIn >= 2) {
status = Status.Warning;
break;
}
case AGENT_TYPE_TEMPORARY:
if (intervalsSinceLastCheckIn >= 3) {
status = Status.Offline;
break;
}
if (!agent.active) {
status = Status.Inactive;
} else {
switch (type) {
case AGENT_TYPE_PERMANENT:
if (intervalsSinceLastCheckIn >= 4) {
status = Status.Error;
break;
}
if (intervalsSinceLastCheckIn >= 2) {
status = Status.Warning;
break;
}
case AGENT_TYPE_TEMPORARY:
if (intervalsSinceLastCheckIn >= 3) {
status = Status.Offline;
break;
}
}
}

return (
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

import { omit } from 'lodash';
import { Agent, AgentEvent } from '../../../../common/types/domain_data';
import { ReturnTypeBulkUnenroll } from '../../../../common/return_types';

export class AgentAdapter {
private memoryDB: Agent[];
Expand Down Expand Up @@ -52,4 +53,18 @@ export class AgentAdapter {
public async getWithToken(enrollmentToken: string): Promise<Agent | null> {
return this.memoryDB.map<Agent>((beat: any) => omit(beat, ['access_token']))[0];
}

public async unenrollByIds(ids: string[]): Promise<ReturnTypeBulkUnenroll> {
return {
results: [],
success: true,
};
}

public async unenrollByKuery(ids: string): Promise<ReturnTypeBulkUnenroll> {
return {
results: [],
success: true,
};
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,12 @@
*/

import { Agent } from '../../../../common/types/domain_data';
import { ReturnTypeGet, ReturnTypeList, ReturnTypeUpdate } from '../../../../common/return_types';
import {
ReturnTypeGet,
ReturnTypeList,
ReturnTypeUpdate,
ReturnTypeBulkUnenroll,
} from '../../../../common/return_types';
import { RestAPIAdapter } from '../rest_api/adapter_types';
import { AgentAdapter } from './memory_agent_adapter';
import { AgentEvent } from '../../../../common/types/domain_data';
Expand Down Expand Up @@ -87,4 +92,16 @@ export class RestAgentAdapter extends AgentAdapter {
await this.REST.put<ReturnTypeUpdate<Agent>>(`/api/fleet/agent/${id}`, beatData);
return true;
}

public async unenrollByIds(ids: string[]): Promise<ReturnTypeBulkUnenroll> {
return await this.REST.post<ReturnTypeBulkUnenroll>(`/api/fleet/agents/unenroll`, {
ids,
});
}

public async unenrollByKuery(kuery: string): Promise<ReturnTypeBulkUnenroll> {
return await this.REST.post<ReturnTypeBulkUnenroll>(`/api/fleet/agents/unenroll`, {
kuery,
});
}
}
4 changes: 4 additions & 0 deletions x-pack/legacy/plugins/fleet/public/lib/agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,4 +58,8 @@ export class AgentsLib {
public update = async (id: string, agentData: Partial<Agent>): Promise<boolean> => {
return await this.adapter.update(id, agentData);
};

public unenroll = async (ids: string[]) => {
return await this.adapter.unenrollByIds(ids);
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@

import React, { SFC } from 'react';
import { FormattedMessage, FormattedRelative } from '@kbn/i18n/react';
import { EuiTitle, EuiSpacer, EuiFlexGroup, EuiFlexItem, EuiDescriptionList } from '@elastic/eui';
import {
EuiTitle,
EuiSpacer,
EuiFlexGroup,
EuiFlexItem,
EuiDescriptionList,
EuiButton,
} from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { Agent } from '../../../../common/types/domain_data';
import { AgentHealth } from '../../../components/agent_health';
Expand Down Expand Up @@ -37,7 +44,12 @@ function getMetadataTitle(key: string): string {
}
}

export const AgentDetailSection: SFC<{ agent: Agent }> = ({ agent }) => {
interface Props {
agent: Agent;
unenrollment: { loading: boolean };
onClickUnenroll: () => void;
}
export const AgentDetailSection: SFC<Props> = ({ agent, onClickUnenroll, unenrollment }) => {
const mapMetadata = (obj: { [key: string]: string } | undefined) => {
return Object.keys(obj || {}).map(key => ({
key,
Expand Down Expand Up @@ -95,6 +107,16 @@ export const AgentDetailSection: SFC<{ agent: Agent }> = ({ agent }) => {
</EuiFlexGroup>
<EuiSpacer size="l" />
<EuiDescriptionList type="column" compressed listItems={items} />
<EuiSpacer size="m" />
<EuiFlexItem grow={false}>
<EuiButton
disabled={unenrollment.loading === true || agent.active === false}
isLoading={unenrollment.loading}
onClick={onClickUnenroll}
>
<FormattedMessage id="xpack.fleet.agentDetails.unenroll" defaultMessage="Unenroll" />
</EuiButton>
</EuiFlexItem>
</>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/*
* 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 React, { SFC } from 'react';
import { EuiConfirmModal, EuiOverlayMask } from '@elastic/eui';
import { i18n } from '@kbn/i18n';

interface Props {
onConfirm: () => void;
onCancel: () => void;
}

export const ModalConfirmUnenroll: SFC<Props> = ({ onConfirm, onCancel }) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

this component could be written as a provider pattern that could be used from any UI, for any number of agents (useful as we will need unenroll in the agent list view, at least!).

an example "delete policy(ies)" modal used in Snapshot and Restore: policy_delete_provider.tsx. note that it handles modal open state, making request for deletion, and displaying toast notification for failure/success within the component. then it can be used for multiple delete and single delete anywhere!

what do you think of this pattern? just want to see if you like or dislike the pattern. no need to change in this PR unless you want to 🙂

Copy link
Contributor

Choose a reason for hiding this comment

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

Eh I have seen this pattern go well, and I have seen it get super messy. For now my vote would be to keep is simple.
As long term things like this for fleet have the potential to be exposed throughout kibana, I would like to see us not over think this for now until we get through the next phase and see better where fleet will be going

Copy link
Member Author

Choose a reason for hiding this comment

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

I am not really familiar with this pattern, maybe something we can refactor while implementing unenrolling in the listing,

return (
<EuiOverlayMask>
<EuiConfirmModal
title={i18n.translate('xpack.fleet.enrollmentModal.title', {
defaultMessage: 'Are you sure you want to unenroll this agent?',
})}
onCancel={onCancel}
onConfirm={onConfirm}
confirmButtonText={i18n.translate('xpack.fleet.enrollmentModal.confirmButton', {
defaultMessage: 'Yes, do it',
})}
cancelButtonText={i18n.translate('xpack.fleet.enrollmentModal.cancelButton', {
defaultMessage: "No, don't do it",
})}
defaultFocusedButton="confirm"
></EuiConfirmModal>
</EuiOverlayMask>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
/*
* 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 { useState, useEffect } from 'react';
import { AgentsLib } from '../../../lib/agent';
import { Agent } from '../../../../common/types/domain_data';

export function useGetAgent(agents: AgentsLib, id: string) {
const [state, setState] = useState<{
isLoading: boolean;
agent: Agent | null;
error: Error | null;
}>({
isLoading: false,
agent: null,
error: null,
});

const fetchAgent = async () => {
setState({
isLoading: true,
agent: null,
error: null,
});
try {
const agent = await agents.get(id);
setState({
isLoading: false,
agent,
error: null,
});
} catch (error) {
setState({
isLoading: false,
agent: null,
error,
});
}
};
useEffect(() => {
fetchAgent();
}, [id]);

return {
...state,
refreshAgent: fetchAgent,
};
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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 { useState } from 'react';
import { AgentsLib } from '../../../lib/agent';

export function useUnenroll(agents: AgentsLib, refreshAgent: () => Promise<void>, agentId: string) {
const [state, setState] = useState<
| {
confirm: false;
loading: false;
}
| {
confirm: true;
loading: false;
}
| {
confirm: false;
loading: true;
}
>({
confirm: false,
loading: false,
});

return {
state,
showConfirmModal: () =>
setState({
confirm: true,
loading: false,
}),
confirmUnenrollement: async () => {
setState({
confirm: false,
loading: true,
});

await agents.unenroll([agentId]);

setState({
confirm: false,
loading: false,
});
refreshAgent();
},
clear: () => {
setState({
confirm: false,
loading: false,
});
},
};
}
Loading