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

In case of kubernetes integration detected return manifest in standalone agent layout instead of policy #114439

Merged
Show file tree
Hide file tree
Changes from 10 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
4 changes: 4 additions & 0 deletions x-pack/plugins/fleet/common/constants/epm.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export const FLEET_SERVER_PACKAGE = 'fleet_server';
export const FLEET_ENDPOINT_PACKAGE = 'endpoint';
export const FLEET_APM_PACKAGE = 'apm';
export const FLEET_SYNTHETICS_PACKAGE = 'synthetics';
export const FLEET_KUBERNETES_PACKAGE = 'kubernetes';
export const KUBERNETES_RUN_INSTRUCTIONS =
'kubectl apply -f elastic-agent-standalone-kubernetes.yaml';
export const STANDALONE_RUN_INSTRUCTIONS = './elastic-agent install';

/*
Package rules:
Expand Down
34 changes: 34 additions & 0 deletions x-pack/plugins/fleet/common/services/agent_cm_to_yaml.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/*
* 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 type { safeDump } from 'js-yaml';

import type { FullAgentConfigMap } from '../types/models/agent_cm';

const CM_KEYS_ORDER = ['apiVersion', 'kind', 'metadata', 'data'];

export const fullAgentConfigMapToYaml = (
policy: FullAgentConfigMap,
toYaml: typeof safeDump
): string => {
return toYaml(policy, {
skipInvalid: true,
sortKeys: (keyA: string, keyB: string) => {
const indexA = CM_KEYS_ORDER.indexOf(keyA);
const indexB = CM_KEYS_ORDER.indexOf(keyB);
if (indexA >= 0 && indexB < 0) {
return -1;
}

if (indexA < 0 && indexB >= 0) {
return 1;
}

return indexA - indexB;
},
});
};
25 changes: 25 additions & 0 deletions x-pack/plugins/fleet/common/types/models/agent_cm.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
/*
* 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 type { FullAgentPolicy } from './agent_policy';

export interface FullAgentConfigMap {
apiVersion: string;
kind: string;
metadata: Metadata;
data: FullAgentPolicy;
}

interface Metadata {
name: string;
namespace: string;
labels: Labels;
}

interface Labels {
'k8s-app': string;
}
4 changes: 4 additions & 0 deletions x-pack/plugins/fleet/common/types/rest_spec/agent_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,3 +78,7 @@ export interface GetFullAgentPolicyRequest {
export interface GetFullAgentPolicyResponse {
item: FullAgentPolicy;
}

export interface GetFullAgentConfigMapResponse {
item: string;
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,56 +23,174 @@ import { i18n } from '@kbn/i18n';
import { FormattedMessage } from '@kbn/i18n/react';
import { safeDump } from 'js-yaml';

import { useStartServices, useLink, sendGetOneAgentPolicyFull } from '../../hooks';
import {
useStartServices,
useLink,
sendGetOneAgentPolicyFull,
sendGetOneAgentPolicy,
} from '../../hooks';
import { fullAgentPolicyToYaml, agentPolicyRouteService } from '../../services';

import type { PackagePolicy } from '../../../common';

import {
FLEET_KUBERNETES_PACKAGE,
KUBERNETES_RUN_INSTRUCTIONS,
STANDALONE_RUN_INSTRUCTIONS,
} from '../../../common';

import { DownloadStep, AgentPolicySelectionStep } from './steps';
import type { BaseProps } from './types';

type Props = BaseProps;

const RUN_INSTRUCTIONS = './elastic-agent install';

export const StandaloneInstructions = React.memo<Props>(({ agentPolicy, agentPolicies }) => {
const { getHref } = useLink();
const core = useStartServices();
const { notifications } = core;

const [selectedPolicyId, setSelectedPolicyId] = useState<string | undefined>(agentPolicy?.id);
const [fullAgentPolicy, setFullAgentPolicy] = useState<any | undefined>();
const [isK8s, setIsK8s] = useState<'IS_LOADING' | 'IS_KUBERNETES' | 'IS_NOT_KUBERNETES'>(
'IS_LOADING'
);
const [yaml, setYaml] = useState<string | string>('');
const [downloadLink, setDownloadLink] = useState<string | undefined>();
const runInstructions =
isK8s === 'IS_KUBERNETES' ? KUBERNETES_RUN_INSTRUCTIONS : STANDALONE_RUN_INSTRUCTIONS;

const downloadLink = selectedPolicyId
? core.http.basePath.prepend(
`${agentPolicyRouteService.getInfoFullDownloadPath(selectedPolicyId)}?standalone=true`
)
: undefined;
useEffect(() => {
async function checkifK8s() {
if (!selectedPolicyId) {
return;
}
const agentPolicyRequest = await sendGetOneAgentPolicy(selectedPolicyId);
const agentPol = agentPolicyRequest.data ? agentPolicyRequest.data.item : null;

if (!agentPol) {
setIsK8s('IS_NOT_KUBERNETES');
return;
}
const k8s = (pkg: PackagePolicy) => pkg.package?.name === FLEET_KUBERNETES_PACKAGE;
setIsK8s(
(agentPol.package_policies as PackagePolicy[]).some(k8s)
? 'IS_KUBERNETES'
: 'IS_NOT_KUBERNETES'
);
}
checkifK8s();
}, [selectedPolicyId, notifications.toasts]);

useEffect(() => {
async function fetchFullPolicy() {
try {
if (!selectedPolicyId) {
return;
}
const res = await sendGetOneAgentPolicyFull(selectedPolicyId, { standalone: true });
let query = { standalone: true, kubernetes: false };
let downloandLinkUrl = `${agentPolicyRouteService.getInfoFullDownloadPath(
selectedPolicyId
)}?standalone=true`;
if (isK8s === 'IS_KUBERNETES') {
query = { standalone: true, kubernetes: true };
downloandLinkUrl = `${agentPolicyRouteService.getInfoFullDownloadPath(
selectedPolicyId
)}?kubernetes=true`;
}
const res = await sendGetOneAgentPolicyFull(selectedPolicyId, query);
if (res.error) {
throw res.error;
}

if (!res.data) {
throw new Error('No data while fetching full agent policy');
}

setFullAgentPolicy(res.data.item);
setDownloadLink(core.http.basePath.prepend(downloandLinkUrl));
} catch (error) {
notifications.toasts.addError(error, {
title: 'Error',
});
}
}
fetchFullPolicy();
}, [selectedPolicyId, notifications.toasts]);
if (isK8s !== 'IS_LOADING') {
fetchFullPolicy();
}
}, [selectedPolicyId, notifications.toasts, isK8s, core.http.basePath]);

useMemo(() => {
if (isK8s === 'IS_KUBERNETES') {
if (typeof fullAgentPolicy === 'object') {
return;
}
setYaml(fullAgentPolicy);
Copy link
Member

Choose a reason for hiding this comment

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

You probably do not need the setYaml neither the way I would implement this useMemo return a value and compute it only if dependencies changes.

So you could do

const yaml = useMemo(() => {
      return fullAgentPolicyToYaml(fullAgentPolicy);
}

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is very tricky. The reason I did it like this is that I didn't want always this usememo to return a value for the yaml. There are unfortunately some race conditions where isk8s === true but fullAgentPolicy hasn't beed updated yet(remains an object instead of string) and also the opposite.
So the way I did it, these cases are skipped (setYaml does not run, so value of yaml remains as it was before).
If I go with the const yaml = useMemo(() => { return fullAgentPolicyToYaml(fullAgentPolicy); } approach then I always have to return a value which in those cases I shouldn't.

Copy link
Member

Choose a reason for hiding this comment

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

In this case you should probably use useEffect instead of useMemo

} else {
if (typeof fullAgentPolicy === 'string') {
return;
}
setYaml(fullAgentPolicyToYaml(fullAgentPolicy, safeDump));
}
}, [fullAgentPolicy, isK8s]);

const policyMsg =
isK8s === 'IS_KUBERNETES' ? (
<FormattedMessage
id="xpack.fleet.agentEnrollment.stepConfigureAgentDescriptionk8s"
defaultMessage="Copy or download the Kubernetes manifest inside the Kubernetes cluster. Modify {ESUsernameVariable} and {ESPasswordVariable} in the Daemonset environment variables and apply the manifest."
values={{
ESUsernameVariable: <EuiCode>ES_USERNAME</EuiCode>,
ESPasswordVariable: <EuiCode>ES_PASSWORD</EuiCode>,
}}
/>
) : (
<FormattedMessage
id="xpack.fleet.agentEnrollment.stepConfigureAgentDescription"
defaultMessage="Copy this policy to the {fileName} on the host where the Elastic Agent is installed. Modify {ESUsernameVariable} and {ESPasswordVariable} in the {outputSection} section of {fileName} to use your Elasticsearch credentials."
values={{
fileName: <EuiCode>elastic-agent.yml</EuiCode>,
ESUsernameVariable: <EuiCode>ES_USERNAME</EuiCode>,
ESPasswordVariable: <EuiCode>ES_PASSWORD</EuiCode>,
outputSection: <EuiCode>outputs</EuiCode>,
}}
/>
);

function downloadMsg() {
if (isK8s === 'IS_KUBERNETES') {
return (
<FormattedMessage
id="xpack.fleet.agentEnrollment.downloadPolicyButtonk8s"
defaultMessage="Download Manifest"
/>
);
} else {
return (
<FormattedMessage
id="xpack.fleet.agentEnrollment.downloadPolicyButton"
defaultMessage="Download Policy"
/>
);
}
}

function applyMsg() {
if (isK8s === 'IS_KUBERNETES') {
return (
<FormattedMessage
id="xpack.fleet.agentEnrollment.stepRunAgentDescriptionk8s"
defaultMessage="From the directory where the Kubernetes manifest is downloaded, run the apply command."
/>
);
} else {
return (
<FormattedMessage
id="xpack.fleet.agentEnrollment.stepRunAgentDescription"
defaultMessage="From the agent directory, run this command to install, enroll and start an Elastic Agent. You can reuse this command to set up agents on more than one host. Requires administrator privileges."
/>
);
}
}

const yaml = useMemo(() => fullAgentPolicyToYaml(fullAgentPolicy, safeDump), [fullAgentPolicy]);
const steps = [
DownloadStep(),
!agentPolicy
Expand All @@ -85,16 +203,7 @@ export const StandaloneInstructions = React.memo<Props>(({ agentPolicy, agentPol
children: (
<>
<EuiText>
<FormattedMessage
id="xpack.fleet.agentEnrollment.stepConfigureAgentDescription"
defaultMessage="Copy this policy to the {fileName} on the host where the Elastic Agent is installed. Modify {ESUsernameVariable} and {ESPasswordVariable} in the {outputSection} section of {fileName} to use your Elasticsearch credentials."
values={{
fileName: <EuiCode>elastic-agent.yml</EuiCode>,
ESUsernameVariable: <EuiCode>ES_USERNAME</EuiCode>,
ESPasswordVariable: <EuiCode>ES_PASSWORD</EuiCode>,
outputSection: <EuiCode>outputs</EuiCode>,
}}
/>
<>{policyMsg}</>
<EuiSpacer size="m" />
<EuiFlexGroup gutterSize="m">
<EuiFlexItem grow={false}>
Expand All @@ -111,10 +220,7 @@ export const StandaloneInstructions = React.memo<Props>(({ agentPolicy, agentPol
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiButton iconType="download" href={downloadLink} isDisabled={!downloadLink}>
<FormattedMessage
id="xpack.fleet.agentEnrollment.downloadPolicyButton"
defaultMessage="Download policy"
/>
{downloadMsg()}
</EuiButton>
</EuiFlexItem>
</EuiFlexGroup>
Expand All @@ -133,14 +239,11 @@ export const StandaloneInstructions = React.memo<Props>(({ agentPolicy, agentPol
children: (
<>
<EuiText>
<FormattedMessage
id="xpack.fleet.agentEnrollment.stepRunAgentDescription"
defaultMessage="From the agent directory, run this command to install, enroll and start an Elastic Agent. You can reuse this command to set up agents on more than one host. Requires administrator privileges."
/>
{applyMsg()}
<EuiSpacer size="m" />
<EuiCodeBlock fontSize="m">{RUN_INSTRUCTIONS}</EuiCodeBlock>
<EuiCodeBlock fontSize="m">{runInstructions}</EuiCodeBlock>
<EuiSpacer size="m" />
<EuiCopy textToCopy={RUN_INSTRUCTIONS}>
<EuiCopy textToCopy={runInstructions}>
{(copy) => (
<EuiButton onClick={copy} iconType="copyClipboard">
<FormattedMessage
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ export const useGetOneAgentPolicyFull = (agentPolicyId: string) => {

export const sendGetOneAgentPolicyFull = (
agentPolicyId: string,
query: { standalone?: boolean } = {}
query: { standalone?: boolean; kubernetes?: boolean } = {}
) => {
return sendRequest<GetFullAgentPolicyResponse>({
path: agentPolicyRouteService.getInfoFullPath(agentPolicyId),
Expand Down
Loading