Skip to content

Commit

Permalink
[UII] Fix first time integration flow when using multiple spaces (#19…
Browse files Browse the repository at this point in the history
…4350)

## Summary

Resolves #182736. This PR fixes the first time Elastic Agent user
onboarding flow (only shown on Cloud) by prefixing the user's current
space ID to the default agent policy that is created behind the scenes
if the user has opted into using Fleet's space awareness.

This makes it so that Fleet correctly identifies whether there are any
agents enrolled in the current space, and directs the user to the
multi-step onboarding flow for adding integrations if there are no
agents enrolled in the current space's scoped agent policies.

An example policy ID created this way may look like
`second-space:fleet-first-agent-policy`. If the current space is the
`default` space, the ID remains as `fleet-first-agent-policy`.

Due to prefixing using a `:`, I also had to put policy ID in quotes
where ever it appears in kueries for filtering, i.e. `policy_id:"<some
id>"`. This change comprises most of the changed files in this PR.
  • Loading branch information
jen-huang authored Oct 3, 2024
1 parent 2e6a257 commit 6f213cf
Show file tree
Hide file tree
Showing 10 changed files with 76 additions and 53 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -259,7 +259,7 @@ export const AgentPolicyActionMenu = memo<{
{isUpgradeAgentsModalOpen && (
<EuiPortal>
<AgentUpgradeAgentModal
agents={`policy_id: ${agentPolicy.id}`}
agents={`policy_id:"${agentPolicy.id}"`}
agentCount={agentPolicy.agents || 0}
onClose={() => {
setIsUpgradeAgentsModalOpen(false);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 2.0.
*/

import { useEffect, useState } from 'react';
import { useEffect, useState, useMemo } from 'react';
import { i18n } from '@kbn/i18n';

import { generateNewAgentPolicyWithDefaults } from '../../../../../../../../common/services/generate_new_agent_policy';
Expand All @@ -14,26 +14,13 @@ import {
sendCreateAgentPolicy,
sendGetOneAgentPolicy,
sendGetEnrollmentAPIKeys,
useFleetStatus,
} from '../../../../../../../hooks';

import type { AgentPolicy, NewAgentPolicy, EnrollmentAPIKey } from '../../../../../../../types';
import type { AgentPolicy, EnrollmentAPIKey } from '../../../../../../../types';

interface UseGetAgentPolicyOrDefaultResponse {
isLoading: boolean;
error?: Error;
agentPolicy?: AgentPolicy;
enrollmentAPIKey?: EnrollmentAPIKey;
created?: boolean;
}
export const DEFAULT_AGENT_POLICY_ID: string = 'fleet-first-agent-policy';
export const DEFAULT_AGENT_POLICY: NewAgentPolicy = Object.freeze(
generateNewAgentPolicyWithDefaults({
id: DEFAULT_AGENT_POLICY_ID,
name: i18n.translate('xpack.fleet.createPackagePolicy.firstAgentPolicyNameText', {
defaultMessage: 'My first agent policy',
}),
})
);
// Manual default space ID because importing from `@kbn/core-saved-objects-utils-server` is not allowed here
const DEFAULT_NAMESPACE_STRING = 'default';

const sendGetAgentPolicy = async (agentPolicyId: string) => {
let result;
Expand All @@ -54,71 +41,107 @@ const sendGetAgentPolicy = async (agentPolicyId: string) => {
return { data: result?.data };
};

const sendCreateDefaultAgentPolicy = sendCreateAgentPolicy.bind(null, DEFAULT_AGENT_POLICY);

export function useGetAgentPolicyOrDefault(agentPolicyIdIn?: string) {
const [result, setResult] = useState<UseGetAgentPolicyOrDefaultResponse>({ isLoading: true });
const { spaceId, isSpaceAwarenessEnabled } = useFleetStatus();
const [isLoading, setIsLoading] = useState(true);
const [error, setError] = useState<Error>();
const [agentPolicyResponse, setAgentPolicyResponse] = useState<AgentPolicy>();
const [enrollmentAPIKey, setEnrollmentAPIKey] = useState<EnrollmentAPIKey>();
const [created, setCreated] = useState<boolean>();

// If space awareness is enabled, append current space ID to the agent policy ID
// If current space is the default space, do not append the space ID for BWC
const defaultFirstPolicyIdBase = 'fleet-first-agent-policy';
const defaultFirstPolicyId = useMemo(
() =>
isSpaceAwarenessEnabled && spaceId !== DEFAULT_NAMESPACE_STRING
? `${spaceId}:${defaultFirstPolicyIdBase}`
: defaultFirstPolicyIdBase,
[isSpaceAwarenessEnabled, spaceId]
);

const defaultFirstPolicy = useMemo(
() =>
Object.freeze(
generateNewAgentPolicyWithDefaults({
id: defaultFirstPolicyId,
name: i18n.translate('xpack.fleet.createPackagePolicy.firstAgentPolicyNameText', {
defaultMessage: 'My first agent policy',
}),
})
),
[defaultFirstPolicyId]
);

useEffect(() => {
const getAgentPolicyOrDefault = async () => {
const agentPolicyId = agentPolicyIdIn || DEFAULT_AGENT_POLICY_ID;
const { data: agentPolicyData, error: getError } = await sendGetAgentPolicy(agentPolicyId);
setIsLoading(true);

const agentPolicyId = agentPolicyIdIn || defaultFirstPolicyId;
const { data: agentPolicyData, error: getError } = await sendGetAgentPolicy(agentPolicyId);
const existingAgentPolicy = agentPolicyData?.item;

if (agentPolicyIdIn && !existingAgentPolicy) {
setResult({
isLoading: false,
error: new Error(`Agent policy ${agentPolicyId} not found`),
});
setIsLoading(false);
setError(new Error(`Agent policy ${agentPolicyId} not found`));
return;
}

let createdAgentPolicy;

if (getError) {
setResult({ isLoading: false, error: getError });
setIsLoading(false);
setError(getError);
return;
}

if (!existingAgentPolicy) {
const { data: createdAgentPolicyData, error: createError } =
await sendCreateDefaultAgentPolicy();
await sendCreateAgentPolicy.bind(null, defaultFirstPolicy)();

if (createError) {
setResult({ isLoading: false, error: createError });
setIsLoading(false);
setError(createError);
return;
}

createdAgentPolicy = createdAgentPolicyData!.item;
setCreated(true);
}

const agentPolicy = (existingAgentPolicy || createdAgentPolicy) as AgentPolicy;
setAgentPolicyResponse(agentPolicy);

const { data: apiKeysData, error: apiKeysError } = await sendGetEnrollmentAPIKeys({
page: 1,
perPage: 1,
kuery: `policy_id:${agentPolicyId}`,
kuery: `policy_id:"${agentPolicyId}"`,
});

if (apiKeysError) {
setResult({ isLoading: false, error: apiKeysError });
setIsLoading(false);
setError(apiKeysError);
return;
}

if (!apiKeysData || !apiKeysData.items?.length) {
setResult({
isLoading: false,
error: new Error(`No enrollment API key found for policy ${agentPolicyId}`),
});
setIsLoading(false);
setError(new Error(`No enrollment API key found for policy ${agentPolicyId}`));
return;
}

const enrollmentAPIKey = apiKeysData.items[0];

setResult({ isLoading: false, created: !!createdAgentPolicy, agentPolicy, enrollmentAPIKey });
setIsLoading(false);
setEnrollmentAPIKey(apiKeysData.items[0]);
};

getAgentPolicyOrDefault();
}, [agentPolicyIdIn]);

return result;
}, [agentPolicyIdIn, defaultFirstPolicy, defaultFirstPolicyId]);

return {
isLoading,
error,
agentPolicy: agentPolicyResponse,
enrollmentAPIKey,
created,
};
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const PostInstallAzureArmTemplateModal: React.FunctionComponent<{
sendGetEnrollmentAPIKeys({
page: 1,
perPage: 1,
kuery: `policy_id:${agentPolicy.id}`,
kuery: `policy_id:"${agentPolicy.id}"`,
})
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ export const PostInstallCloudFormationModal: React.FunctionComponent<{
sendGetEnrollmentAPIKeys({
page: 1,
perPage: 1,
kuery: `policy_id:${agentPolicy.id}`,
kuery: `policy_id:"${agentPolicy.id}"`,
})
);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ export const PostInstallGoogleCloudShellModal: React.FunctionComponent<{
sendGetEnrollmentAPIKeys({
page: 1,
perPage: 1,
kuery: `policy_id:${agentPolicy.id}`,
kuery: `policy_id:"${agentPolicy.id}"`,
})
);
const { fleetServerHost, fleetProxy, downloadSource } = useFleetServerHostsForPolicy(agentPolicy);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ export const useIsFirstTimeAgentUserQuery = (): UseIsFirstTimeAgentUserResponse
const policyIds = [...new Set(packagePolicies?.items.flatMap((item) => item.policy_ids) ?? [])];

// now get all agents that are NOT part of a fleet server policy
const serverPolicyIdsQuery = policyIds.map((policyId) => `policy_id:${policyId}`).join(' or ');
const serverPolicyIdsQuery = policyIds.map((policyId) => `policy_id:"${policyId}"`).join(' or ');

// get agents that are not unenrolled and not fleet server
const kuery =
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/fleet/server/routes/agent_policy/handlers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -70,15 +70,15 @@ export async function populateAssignedAgentsCount(
showInactive: true,
perPage: 0,
page: 1,
kuery: `${AGENTS_PREFIX}.policy_id:${agentPolicy.id}`,
kuery: `${AGENTS_PREFIX}.policy_id:"${agentPolicy.id}"`,
})
.then(({ total }) => (agentPolicy.agents = total));
const unprivilegedAgents = agentClient
.listAgents({
showInactive: true,
perPage: 0,
page: 1,
kuery: `${AGENTS_PREFIX}.policy_id:${agentPolicy.id} and ${UNPRIVILEGED_AGENT_KUERY}`,
kuery: `${AGENTS_PREFIX}.policy_id:"${agentPolicy.id}" and ${UNPRIVILEGED_AGENT_KUERY}`,
})
.then(({ total }) => (agentPolicy.unprivileged_agents = total));
return Promise.all([totalAgents, unprivilegedAgents]);
Expand Down
4 changes: 2 additions & 2 deletions x-pack/plugins/fleet/server/services/agent_policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ class AgentPolicyService {
showInactive: true,
perPage: 0,
page: 1,
kuery: `${AGENTS_PREFIX}.policy_id:${agentPolicy.id}`,
kuery: `${AGENTS_PREFIX}.policy_id:"${agentPolicy.id}"`,
}).then(({ total }) => (agentPolicy.agents = total));
} else {
agentPolicy.agents = 0;
Expand Down Expand Up @@ -1161,7 +1161,7 @@ class AgentPolicyService {
showInactive: true,
perPage: 0,
page: 1,
kuery: `${AGENTS_PREFIX}.policy_id:${id}`,
kuery: `${AGENTS_PREFIX}.policy_id:"${id}"`,
});

if (total > 0 && !agentPolicy?.supports_agentless) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ export async function deleteEnrollmentApiKeyForAgentPolicyId(
const { items } = await listEnrollmentApiKeys(esClient, {
page: page++,
perPage: 100,
kuery: `policy_id:${agentPolicyId}`,
kuery: `policy_id:"${agentPolicyId}"`,
});

if (items.length === 0) {
Expand Down
2 changes: 1 addition & 1 deletion x-pack/plugins/fleet/server/services/fleet_server/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ export const hasFleetServersForPolicies = async (
? `namespaces:"${spaceIds?.[0]}"`
: `not namespaces:* or namespaces:"${DEFAULT_SPACE_ID}"`;

return `(policy_id:${id} and (${space}))`;
return `(policy_id:"${id}" and (${space}))`;
})
.join(' or ')
);
Expand Down

0 comments on commit 6f213cf

Please sign in to comment.