diff --git a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx index 3375c5137034a..1488e9c83f4d3 100644 --- a/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx +++ b/x-pack/plugins/fleet/public/applications/fleet/sections/agent_policy/create_package_policy_page/single_page_layout/hooks/form.tsx @@ -391,7 +391,7 @@ export function useOnSubmit({ // Check if agentless is configured in ESS and Serverless until Agentless API migrates to Serverless const isAgentlessConfigured = - isAgentlessAgentPolicy(createdPolicy) || isAgentlessPackagePolicy(data!.item); + isAgentlessAgentPolicy(createdPolicy) || (data && isAgentlessPackagePolicy(data.item)); // Removing this code will disabled the Save and Continue button. We need code below update form state and trigger correct modal depending on agent count if (hasFleetAddAgentsPrivileges && !isAgentlessConfigured) { diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx index 31213e5f9554a..52a3a90ae641e 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/components/package_card.tsx @@ -57,6 +57,7 @@ export function PackageCard({ name, title, version, + type, icons, integration, url, @@ -78,7 +79,6 @@ export function PackageCard({ maxCardHeight, }: PackageCardProps) { let releaseBadge: React.ReactNode | null = null; - if (release && release !== 'ga') { releaseBadge = ( @@ -108,7 +108,6 @@ export function PackageCard({ } let hasDeferredInstallationsBadge: React.ReactNode | null = null; - if (isReauthorizationRequired && showLabels) { hasDeferredInstallationsBadge = ( @@ -127,7 +126,6 @@ export function PackageCard({ } let updateAvailableBadge: React.ReactNode | null = null; - if (isUpdateAvailable && showLabels) { updateAvailableBadge = ( @@ -145,7 +143,6 @@ export function PackageCard({ } let collectionButton: React.ReactNode | null = null; - if (isCollectionCard) { collectionButton = ( @@ -163,6 +160,23 @@ export function PackageCard({ ); } + let contentBadge: React.ReactNode | null = null; + if (type === 'content') { + contentBadge = ( + + + + + + + + + ); + } + const { application } = useStartServices(); const isGuidedOnboardingActive = useIsGuidedOnboardingActive(name); @@ -235,6 +249,7 @@ export function PackageCard({ {showLabels && extraLabelsBadges ? extraLabelsBadges : null} {verifiedBadge} {updateAvailableBadge} + {contentBadge} {releaseBadge} {hasDeferredInstallationsBadge} {collectionButton} diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx index 51f54fc26c9cb..9a707500bb03d 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/index.tsx @@ -90,6 +90,7 @@ import { Configs } from './configs'; import './index.scss'; import type { InstallPkgRouteOptions } from './utils/get_install_route_options'; +import { InstallButton } from './settings/install_button'; export type DetailViewPanelName = | 'overview' @@ -362,13 +363,23 @@ export function Detail() { - - - {i18n.translate('xpack.fleet.epm.elasticAgentBadgeLabel', { - defaultMessage: 'Elastic Agent', - })} - - + {packageInfo?.type === 'content' ? ( + + + {i18n.translate('xpack.fleet.epm.contentPackageBadgeLabel', { + defaultMessage: 'Content only', + })} + + + ) : ( + + + {i18n.translate('xpack.fleet.epm.elasticAgentBadgeLabel', { + defaultMessage: 'Elastic Agent', + })} + + + )} {packageInfo?.release && packageInfo.release !== 'ga' ? ( @@ -520,7 +531,7 @@ export function Detail() { ), }, - ...(isInstalled + ...(isInstalled && packageInfo.type !== 'content' ? [ { isDivider: true }, { @@ -532,31 +543,37 @@ export function Detail() { }, ] : []), - { isDivider: true }, - { - content: ( - - - - ), - }, + ...(packageInfo.type === 'content' + ? !isInstalled + ? [{ isDivider: true }, { content: }] + : [] // if content package is already installed, don't show install button in header + : [ + { isDivider: true }, + { + content: ( + + + + ), + }, + ]), ].map((item, index) => ( {item.isDivider ?? false ? ( @@ -619,7 +636,7 @@ export function Detail() { }, ]; - if (canReadIntegrationPolicies && isInstalled) { + if (canReadIntegrationPolicies && isInstalled && packageInfo.type !== 'content') { tabs.push({ id: 'policies', name: ( diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/confirm_package_install.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/confirm_package_install.tsx index 31e4fc32233e9..5fdcdc49223e1 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/confirm_package_install.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/confirm_package_install.tsx @@ -14,9 +14,13 @@ interface ConfirmPackageInstallProps { onConfirm: () => void; packageName: string; numOfAssets: number; + numOfTransformAssets: number; } + +import { TransformInstallWithCurrentUserPermissionCallout } from '../../../../../../../components/transform_install_as_current_user_callout'; + export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => { - const { onCancel, onConfirm, packageName, numOfAssets } = props; + const { onCancel, onConfirm, packageName, numOfAssets, numOfTransformAssets } = props; return ( { /> } /> + {numOfTransformAssets > 0 ? ( + <> + + + + ) : null}

& { + +type InstallationButtonProps = Pick & { disabled?: boolean; dryRunData?: UpgradePackagePolicyDryRunResponse | null; isUpgradingPackagePolicies?: boolean; latestVersion?: string; - numOfAssets: number; packagePolicyIds?: string[]; setIsUpgradingPackagePolicies?: React.Dispatch>; }; export function InstallButton(props: InstallationButtonProps) { - const { name, numOfAssets, title, version } = props; + const { name, title, version, assets } = props; + const canInstallPackages = useAuthz().integrations.installPackages; const installPackage = useInstallPackage(); const getPackageInstallStatus = useGetPackageInstallStatus(); const { status: installationStatus } = getPackageInstallStatus(name); + const numOfAssets = Object.entries(assets).reduce( + (acc, [serviceName, serviceNameValue]) => + acc + + Object.entries(serviceNameValue || {}).reduce( + (acc2, [assetName, assetNameValue]) => acc2 + assetNameValue.length, + 0 + ), + 0 + ); + const numOfTransformAssets = getNumTransformAssets(assets); + const isInstalling = installationStatus === InstallStatus.installing; const [isInstallModalVisible, setIsInstallModalVisible] = useState(false); const toggleInstallModal = useCallback(() => { @@ -44,6 +58,7 @@ export function InstallButton(props: InstallationButtonProps) { const installModal = ( = memo( const isUpdating = installationStatus === InstallStatus.installing && installedVersion; - const { numOfAssets, numTransformAssets } = useMemo( - () => ({ - numTransformAssets: getNumTransformAssets(packageInfo.assets), - numOfAssets: Object.entries(packageInfo.assets).reduce( - (acc, [serviceName, serviceNameValue]) => - acc + - Object.entries(serviceNameValue).reduce( - (acc2, [assetName, assetNameValue]) => acc2 + assetNameValue.length, - 0 - ), - 0 - ), - }), - [packageInfo.assets] - ); - return ( <> @@ -365,15 +344,6 @@ export const SettingsPage: React.FC = memo( - - {numTransformAssets > 0 ? ( - <> - - - - ) : null}

= memo(

@@ -418,7 +387,6 @@ export const SettingsPage: React.FC = memo(
diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx index df472c765c09a..aba40aeba2397 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/detail/settings/uninstall_button.tsx @@ -16,17 +16,16 @@ import { useAuthz, useGetPackageInstallStatus, useUninstallPackage } from '../.. import { ConfirmPackageUninstall } from './confirm_package_uninstall'; -interface UninstallButtonProps extends Pick { +interface UninstallButtonProps extends Pick { disabled?: boolean; latestVersion?: string; - numOfAssets: number; } export const UninstallButton: React.FunctionComponent = ({ disabled = false, latestVersion, name, - numOfAssets, + assets, title, version, }) => { @@ -38,6 +37,16 @@ export const UninstallButton: React.FunctionComponent = ({ const [isUninstallModalVisible, setIsUninstallModalVisible] = useState(false); + const numOfAssets = Object.entries(assets).reduce( + (acc, [serviceName, serviceNameValue]) => + acc + + Object.entries(serviceNameValue || {}).reduce( + (acc2, [assetName, assetNameValue]) => acc2 + assetNameValue.length, + 0 + ), + 0 + ); + const handleClickUninstall = useCallback(() => { uninstallPackage({ name, version, title, redirectToVersion: latestVersion ?? version }); setIsUninstallModalVisible(false); diff --git a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx index 2fc6a82b960c9..7dce4b8dff790 100644 --- a/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx +++ b/x-pack/plugins/fleet/public/applications/integrations/sections/epm/screens/home/card_utils.tsx @@ -65,6 +65,7 @@ export interface IntegrationCardItem { titleLineClamp?: number; url: string; version: string; + type?: string; } export const mapToCard = ({ @@ -114,7 +115,7 @@ export const mapToCard = ({ const release: IntegrationCardReleaseLabel = getPackageReleaseLabel(version); let extraLabelsBadges: React.ReactNode[] | undefined; - if (item.type === 'integration') { + if (item.type === 'integration' || item.type === 'content') { extraLabelsBadges = getIntegrationLabels(item); } @@ -128,6 +129,7 @@ export const mapToCard = ({ integration: 'integration' in item ? item.integration || '' : '', name: 'name' in item ? item.name : item.id, version, + type: item.type, release, categories: ((item.categories || []) as string[]).filter((c: string) => !!c), isReauthorizationRequired, diff --git a/x-pack/plugins/fleet/server/errors/handlers.ts b/x-pack/plugins/fleet/server/errors/handlers.ts index d8971948397d3..31e4b9d6704c7 100644 --- a/x-pack/plugins/fleet/server/errors/handlers.ts +++ b/x-pack/plugins/fleet/server/errors/handlers.ts @@ -45,6 +45,7 @@ import { PackageSavedObjectConflictError, FleetTooManyRequestsError, AgentlessPolicyExistsRequestError, + PackagePolicyContentPackageError, } from '.'; type IngestErrorHandler = ( @@ -84,6 +85,9 @@ const getHTTPResponseCode = (error: FleetError): number => { if (error instanceof PackagePolicyRequestError) { return 400; } + if (error instanceof PackagePolicyContentPackageError) { + return 400; + } // Unauthorized if (error instanceof FleetUnauthorizedError) { return 403; diff --git a/x-pack/plugins/fleet/server/errors/index.ts b/x-pack/plugins/fleet/server/errors/index.ts index 6782b8122a552..de528f082c096 100644 --- a/x-pack/plugins/fleet/server/errors/index.ts +++ b/x-pack/plugins/fleet/server/errors/index.ts @@ -73,6 +73,7 @@ export class BundledPackageLocationNotFoundError extends FleetError {} export class PackagePolicyRequestError extends FleetError {} export class PackagePolicyMultipleAgentPoliciesError extends FleetError {} export class PackagePolicyOutputError extends FleetError {} +export class PackagePolicyContentPackageError extends FleetError {} export class EnrollmentKeyNameExistsError extends FleetError {} export class HostedAgentPolicyRestrictionRelatedError extends FleetError { diff --git a/x-pack/plugins/fleet/server/services/package_policies/utils.test.ts b/x-pack/plugins/fleet/server/services/package_policies/utils.test.ts index 9d68dde10a13e..7075990620ef5 100644 --- a/x-pack/plugins/fleet/server/services/package_policies/utils.test.ts +++ b/x-pack/plugins/fleet/server/services/package_policies/utils.test.ts @@ -153,16 +153,41 @@ describe('Package Policy Utils', () => { ).rejects.toThrowError('Output type "kafka" is not usable with package "apm"'); }); - it('should not throw if valid license and valid output_id is provided', async () => { + it('should throw if content package is being used', async () => { jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); jest .spyOn(outputService, 'get') .mockResolvedValueOnce({ id: 'es-output', type: 'elasticsearch' } as any); await expect( - preflightCheckPackagePolicy(soClient, { - ...testPolicy, - output_id: 'es-output', - }) + preflightCheckPackagePolicy( + soClient, + { + ...testPolicy, + output_id: 'es-output', + }, + { + type: 'content', + } + ) + ).rejects.toThrowError('Cannot create policy for content only packages'); + }); + + it('should not throw if valid license and valid output_id is provided and is not content package', async () => { + jest.spyOn(licenseService, 'hasAtLeast').mockReturnValue(true); + jest + .spyOn(outputService, 'get') + .mockResolvedValueOnce({ id: 'es-output', type: 'elasticsearch' } as any); + await expect( + preflightCheckPackagePolicy( + soClient, + { + ...testPolicy, + output_id: 'es-output', + }, + { + type: 'integration', + } + ) ).resolves.not.toThrow(); }); }); diff --git a/x-pack/plugins/fleet/server/services/package_policies/utils.ts b/x-pack/plugins/fleet/server/services/package_policies/utils.ts index 5c19345a58f79..ef59c643a8b35 100644 --- a/x-pack/plugins/fleet/server/services/package_policies/utils.ts +++ b/x-pack/plugins/fleet/server/services/package_policies/utils.ts @@ -13,8 +13,17 @@ import { LICENCE_FOR_MULTIPLE_AGENT_POLICIES, } from '../../../common/constants'; import { getAllowedOutputTypesForIntegration } from '../../../common/services/output_helpers'; -import type { PackagePolicy, NewPackagePolicy, PackagePolicySOAttributes } from '../../types'; -import { PackagePolicyMultipleAgentPoliciesError, PackagePolicyOutputError } from '../../errors'; +import type { + PackagePolicy, + NewPackagePolicy, + PackagePolicySOAttributes, + PackageInfo, +} from '../../types'; +import { + PackagePolicyMultipleAgentPoliciesError, + PackagePolicyOutputError, + PackagePolicyContentPackageError, +} from '../../errors'; import { licenseService } from '../license'; import { outputService } from '../output'; import { appContextService } from '../app_context'; @@ -35,8 +44,14 @@ export const mapPackagePolicySavedObjectToPackagePolicy = ({ export async function preflightCheckPackagePolicy( soClient: SavedObjectsClientContract, - packagePolicy: PackagePolicy | NewPackagePolicy + packagePolicy: PackagePolicy | NewPackagePolicy, + packageInfo?: Pick ) { + // Package policies cannot be created for content type packages + if (packageInfo?.type === 'content') { + throw new PackagePolicyContentPackageError('Cannot create policy for content only packages'); + } + // If package policy has multiple agent policies IDs, or no agent policies (orphaned integration policy) // check if user can use multiple agent policies feature const { canUseReusablePolicies, errorMessage: canUseMultipleAgentPoliciesErrorMessage } = diff --git a/x-pack/plugins/fleet/server/services/package_policy.ts b/x-pack/plugins/fleet/server/services/package_policy.ts index efc1352e30dbd..82b76e553031e 100644 --- a/x-pack/plugins/fleet/server/services/package_policy.ts +++ b/x-pack/plugins/fleet/server/services/package_policy.ts @@ -233,6 +233,17 @@ class PackagePolicyClientImpl implements PackagePolicyClient { } const savedObjectType = await getPackagePolicySavedObjectType(); + const basePkgInfo = + options?.packageInfo ?? + (packagePolicy.package + ? await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: packagePolicy.package.version, + ignoreUnverified: true, + prerelease: true, + }) + : undefined); auditLoggingService.writeCustomSoAuditLog({ action: 'create', @@ -245,7 +256,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { logger.debug(`Creating new package policy`); this.keepPolicyIdInSync(packagePolicy); - await preflightCheckPackagePolicy(soClient, packagePolicy); + await preflightCheckPackagePolicy(soClient, packagePolicy, basePkgInfo); let enrichedPackagePolicy = await packagePolicyService.runExternalCallbacks( 'packagePolicyCreate', @@ -448,6 +459,15 @@ class PackagePolicyClientImpl implements PackagePolicyClient { }> { const savedObjectType = await getPackagePolicySavedObjectType(); for (const packagePolicy of packagePolicies) { + const basePkgInfo = packagePolicy.package + ? await getPackageInfo({ + savedObjectsClient: soClient, + pkgName: packagePolicy.package.name, + pkgVersion: packagePolicy.package.version, + ignoreUnverified: true, + prerelease: true, + }) + : undefined; if (!packagePolicy.id) { packagePolicy.id = SavedObjectsUtils.generateId(); } @@ -458,7 +478,7 @@ class PackagePolicyClientImpl implements PackagePolicyClient { }); this.keepPolicyIdInSync(packagePolicy); - await preflightCheckPackagePolicy(soClient, packagePolicy); + await preflightCheckPackagePolicy(soClient, packagePolicy, basePkgInfo); } const agentPolicyIds = new Set(packagePolicies.flatMap((pkgPolicy) => pkgPolicy.policy_ids)); diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/changelog.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/changelog.yml new file mode 100644 index 0000000000000..c8397a8b6082d --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/changelog.yml @@ -0,0 +1,5 @@ +- version: 0.1.0 + changes: + - description: Initial release + type: enhancement + link: https://github.com/elastic/package-spec/pull/777 diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/docs/README.md b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/docs/README.md new file mode 100644 index 0000000000000..3a6090d840af5 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/docs/README.md @@ -0,0 +1 @@ +# Reference package of content type diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/img/kibana-system.png b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/img/kibana-system.png new file mode 100644 index 0000000000000..8741a5662417f Binary files /dev/null and b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/img/kibana-system.png differ diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/img/system.svg b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/img/system.svg new file mode 100644 index 0000000000000..0aba96275e24e --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/img/system.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/manifest.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/manifest.yml new file mode 100644 index 0000000000000..4f03b0d37a444 --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/manifest.yml @@ -0,0 +1,32 @@ +format_version: 3.2.0 +name: good_content +title: Good content package +description: > + This package is a dummy example for packages with the content type. + These packages contain resources that are useful with data ingested by other integrations. + They are not used to configure data sources. +version: 0.1.0 +type: content +source: + license: "Apache-2.0" +conditions: + kibana: + version: '^8.16.0' #TBD + elastic: + subscription: 'basic' +discovery: + fields: + - name: process.pid +screenshots: + - src: /img/kibana-system.png + title: kibana system + size: 1220x852 + type: image/png +icons: + - src: /img/system.svg + title: system + size: 1000x1000 + type: image/svg+xml +owner: + github: elastic/ecosystem + type: elastic diff --git a/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/validation.yml b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/validation.yml new file mode 100644 index 0000000000000..fa39a1d6f18bf --- /dev/null +++ b/x-pack/test/fleet_api_integration/apis/fixtures/test_packages/good_content/0.1.0/validation.yml @@ -0,0 +1,3 @@ +errors: + exclude_checks: + - PSR00002 # Allow to use non-GA features. diff --git a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts index 159c40926f4e7..4619636a30d8b 100644 --- a/x-pack/test/fleet_api_integration/apis/package_policy/create.ts +++ b/x-pack/test/fleet_api_integration/apis/package_policy/create.ts @@ -642,6 +642,24 @@ export default function (providerContext: FtrProviderContext) { expect(body.item.inputs[0].enabled).to.eql(false); }); + it('should return 400 for content packages', async function () { + const response = await supertest + .post(`/api/fleet/package_policies`) + .set('kbn-xsrf', 'xxxx') + .send({ + name: 'content-pkg-policy', + description: '', + namespace: 'default', + policy_ids: [], + package: { + name: 'good_content', + version: '0.1.0', + }, + }) + .expect(400); + expect(response.body.message).to.eql('Cannot create policy for content only packages'); + }); + describe('input only packages', () => { it('should default dataset if not provided for input only pkg', async function () { await supertest