mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[UII] Support content packages in UI (#195831)
## Summary Resolves #192484. This PR adds support for content packages in UI. When a package is of `type: content`: - `Content only` badge is shown on its card in Integrations list, and on header of its details page - `Add integration` button is replaced by `Install assets` button in header - References to agent policies are hidden - Package policy service throws error if attempting to create or bulk create policies for content packages <img width="1403" alt="image" src="https://github.com/user-attachments/assets/a82c310a-f849-4b68-b56c-ff6bb31cd6bf"> <img width="1401" alt="image" src="https://github.com/user-attachments/assets/63eb3982-9ec9-494f-a95a-2b8992a408ba"> ## How to test The only current content package is `kubernetes_otel`. You will need to bump up the max allowed spec version and search with beta (prerelease) packages enabled to find it: ``` xpack.fleet.internal.registry.spec.max: '3.4' ``` Test UI scenarios as above. The API can be tested by running: ``` POST kbn:/api/fleet/package_policies { "policy_ids": [ "" ], "package": { "name": "kubernetes_otel", "version": "0.0.2" }, "name": "kubernetes_otel-1", "description": "", "namespace": "", "inputs": {} } ``` ### Checklist Delete any items that are not applicable to this PR. - [x] Any text added follows [EUI's writing guidelines](https://elastic.github.io/eui/#/guidelines/writing), uses sentence case text and includes [i18n support](https://github.com/elastic/kibana/blob/main/packages/kbn-i18n/README.md) - [x] [Unit or functional tests](https://www.elastic.co/guide/en/kibana/master/development-tests.html) were updated or added to match the most common scenarios
This commit is contained in:
parent
0764261189
commit
9512f6c26f
20 changed files with 252 additions and 91 deletions
|
@ -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) {
|
||||
|
|
|
@ -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 = (
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -108,7 +108,6 @@ export function PackageCard({
|
|||
}
|
||||
|
||||
let hasDeferredInstallationsBadge: React.ReactNode | null = null;
|
||||
|
||||
if (isReauthorizationRequired && showLabels) {
|
||||
hasDeferredInstallationsBadge = (
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -127,7 +126,6 @@ export function PackageCard({
|
|||
}
|
||||
|
||||
let updateAvailableBadge: React.ReactNode | null = null;
|
||||
|
||||
if (isUpdateAvailable && showLabels) {
|
||||
updateAvailableBadge = (
|
||||
<EuiFlexItem grow={false}>
|
||||
|
@ -145,7 +143,6 @@ export function PackageCard({
|
|||
}
|
||||
|
||||
let collectionButton: React.ReactNode | null = null;
|
||||
|
||||
if (isCollectionCard) {
|
||||
collectionButton = (
|
||||
<EuiFlexItem>
|
||||
|
@ -163,6 +160,23 @@ export function PackageCard({
|
|||
);
|
||||
}
|
||||
|
||||
let contentBadge: React.ReactNode | null = null;
|
||||
if (type === 'content') {
|
||||
contentBadge = (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSpacer size="xs" />
|
||||
<span>
|
||||
<EuiBadge color="hollow">
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.packageCard.contentPackageLabel"
|
||||
defaultMessage="Content only"
|
||||
/>
|
||||
</EuiBadge>
|
||||
</span>
|
||||
</EuiFlexItem>
|
||||
);
|
||||
}
|
||||
|
||||
const { application } = useStartServices();
|
||||
const isGuidedOnboardingActive = useIsGuidedOnboardingActive(name);
|
||||
|
||||
|
@ -235,6 +249,7 @@ export function PackageCard({
|
|||
{showLabels && extraLabelsBadges ? extraLabelsBadges : null}
|
||||
{verifiedBadge}
|
||||
{updateAvailableBadge}
|
||||
{contentBadge}
|
||||
{releaseBadge}
|
||||
{hasDeferredInstallationsBadge}
|
||||
{collectionButton}
|
||||
|
|
|
@ -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() {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="xs">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="default">
|
||||
{i18n.translate('xpack.fleet.epm.elasticAgentBadgeLabel', {
|
||||
defaultMessage: 'Elastic Agent',
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
{packageInfo?.type === 'content' ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="default">
|
||||
{i18n.translate('xpack.fleet.epm.contentPackageBadgeLabel', {
|
||||
defaultMessage: 'Content only',
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
) : (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge color="default">
|
||||
{i18n.translate('xpack.fleet.epm.elasticAgentBadgeLabel', {
|
||||
defaultMessage: 'Elastic Agent',
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
{packageInfo?.release && packageInfo.release !== 'ga' ? (
|
||||
<EuiFlexItem grow={false}>
|
||||
<HeaderReleaseBadge release={getPackageReleaseLabel(packageInfo.version)} />
|
||||
|
@ -520,7 +531,7 @@ export function Detail() {
|
|||
</EuiFlexGroup>
|
||||
),
|
||||
},
|
||||
...(isInstalled
|
||||
...(isInstalled && packageInfo.type !== 'content'
|
||||
? [
|
||||
{ isDivider: true },
|
||||
{
|
||||
|
@ -532,31 +543,37 @@ export function Detail() {
|
|||
},
|
||||
]
|
||||
: []),
|
||||
{ isDivider: true },
|
||||
{
|
||||
content: (
|
||||
<WithGuidedOnboardingTour
|
||||
packageKey={pkgkey}
|
||||
tourType={'addIntegrationButton'}
|
||||
isTourVisible={isOverviewPage && isGuidedOnboardingActive}
|
||||
tourOffset={10}
|
||||
>
|
||||
<AddIntegrationButton
|
||||
userCanInstallPackages={userCanInstallPackages}
|
||||
href={getHref('add_integration_to_policy', {
|
||||
pkgkey,
|
||||
...(integration ? { integration } : {}),
|
||||
...(agentPolicyIdFromContext
|
||||
? { agentPolicyId: agentPolicyIdFromContext }
|
||||
: {}),
|
||||
})}
|
||||
missingSecurityConfiguration={missingSecurityConfiguration}
|
||||
packageName={integrationInfo?.title || packageInfo.title}
|
||||
onClick={handleAddIntegrationPolicyClick}
|
||||
/>
|
||||
</WithGuidedOnboardingTour>
|
||||
),
|
||||
},
|
||||
...(packageInfo.type === 'content'
|
||||
? !isInstalled
|
||||
? [{ isDivider: true }, { content: <InstallButton {...packageInfo} /> }]
|
||||
: [] // if content package is already installed, don't show install button in header
|
||||
: [
|
||||
{ isDivider: true },
|
||||
{
|
||||
content: (
|
||||
<WithGuidedOnboardingTour
|
||||
packageKey={pkgkey}
|
||||
tourType={'addIntegrationButton'}
|
||||
isTourVisible={isOverviewPage && isGuidedOnboardingActive}
|
||||
tourOffset={10}
|
||||
>
|
||||
<AddIntegrationButton
|
||||
userCanInstallPackages={userCanInstallPackages}
|
||||
href={getHref('add_integration_to_policy', {
|
||||
pkgkey,
|
||||
...(integration ? { integration } : {}),
|
||||
...(agentPolicyIdFromContext
|
||||
? { agentPolicyId: agentPolicyIdFromContext }
|
||||
: {}),
|
||||
})}
|
||||
missingSecurityConfiguration={missingSecurityConfiguration}
|
||||
packageName={integrationInfo?.title || packageInfo.title}
|
||||
onClick={handleAddIntegrationPolicyClick}
|
||||
/>
|
||||
</WithGuidedOnboardingTour>
|
||||
),
|
||||
},
|
||||
]),
|
||||
].map((item, index) => (
|
||||
<EuiFlexItem grow={false} key={index} data-test-subj={item['data-test-subj']}>
|
||||
{item.isDivider ?? false ? (
|
||||
|
@ -619,7 +636,7 @@ export function Detail() {
|
|||
},
|
||||
];
|
||||
|
||||
if (canReadIntegrationPolicies && isInstalled) {
|
||||
if (canReadIntegrationPolicies && isInstalled && packageInfo.type !== 'content') {
|
||||
tabs.push({
|
||||
id: 'policies',
|
||||
name: (
|
||||
|
|
|
@ -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 (
|
||||
<EuiConfirmModal
|
||||
title={
|
||||
|
@ -53,6 +57,12 @@ export const ConfirmPackageInstall = (props: ConfirmPackageInstallProps) => {
|
|||
/>
|
||||
}
|
||||
/>
|
||||
{numOfTransformAssets > 0 ? (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<TransformInstallWithCurrentUserPermissionCallout count={numOfTransformAssets} />
|
||||
</>
|
||||
) : null}
|
||||
<EuiSpacer size="l" />
|
||||
<p>
|
||||
<FormattedMessage
|
||||
|
|
|
@ -13,23 +13,37 @@ import type { PackageInfo, UpgradePackagePolicyDryRunResponse } from '../../../.
|
|||
import { InstallStatus } from '../../../../../types';
|
||||
import { useAuthz, useGetPackageInstallStatus, useInstallPackage } from '../../../../../hooks';
|
||||
|
||||
import { getNumTransformAssets } from '../../../../../../../components/transform_install_as_current_user_callout';
|
||||
|
||||
import { ConfirmPackageInstall } from './confirm_package_install';
|
||||
type InstallationButtonProps = Pick<PackageInfo, 'name' | 'title' | 'version'> & {
|
||||
|
||||
type InstallationButtonProps = Pick<PackageInfo, 'name' | 'title' | 'version' | 'assets'> & {
|
||||
disabled?: boolean;
|
||||
dryRunData?: UpgradePackagePolicyDryRunResponse | null;
|
||||
isUpgradingPackagePolicies?: boolean;
|
||||
latestVersion?: string;
|
||||
numOfAssets: number;
|
||||
packagePolicyIds?: string[];
|
||||
setIsUpgradingPackagePolicies?: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
};
|
||||
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<boolean>(false);
|
||||
const toggleInstallModal = useCallback(() => {
|
||||
|
@ -44,6 +58,7 @@ export function InstallButton(props: InstallationButtonProps) {
|
|||
const installModal = (
|
||||
<ConfirmPackageInstall
|
||||
numOfAssets={numOfAssets}
|
||||
numOfTransformAssets={numOfTransformAssets}
|
||||
packageName={title}
|
||||
onCancel={toggleInstallModal}
|
||||
onConfirm={handleClickInstall}
|
||||
|
@ -61,7 +76,7 @@ export function InstallButton(props: InstallationButtonProps) {
|
|||
{isInstalling ? (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrations.installPackage.installingPackageButtonLabel"
|
||||
defaultMessage="Installing {title} assets"
|
||||
defaultMessage="Installing {title}"
|
||||
values={{
|
||||
title,
|
||||
}}
|
||||
|
@ -69,7 +84,7 @@ export function InstallButton(props: InstallationButtonProps) {
|
|||
) : (
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrations.installPackage.installPackageButtonLabel"
|
||||
defaultMessage="Install {title} assets"
|
||||
defaultMessage="Install {title}"
|
||||
values={{
|
||||
title,
|
||||
}}
|
||||
|
|
|
@ -23,11 +23,6 @@ import {
|
|||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
import {
|
||||
getNumTransformAssets,
|
||||
TransformInstallWithCurrentUserPermissionCallout,
|
||||
} from '../../../../../../../components/transform_install_as_current_user_callout';
|
||||
|
||||
import type { FleetStartServices } from '../../../../../../../plugin';
|
||||
import type { PackageInfo, PackageMetadata } from '../../../../../types';
|
||||
import { InstallStatus } from '../../../../../types';
|
||||
|
@ -238,22 +233,6 @@ export const SettingsPage: React.FC<Props> = 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 (
|
||||
<>
|
||||
<EuiFlexGroup alignItems="flexStart">
|
||||
|
@ -365,15 +344,6 @@ export const SettingsPage: React.FC<Props> = memo(
|
|||
</h4>
|
||||
</EuiTitle>
|
||||
<EuiSpacer size="s" />
|
||||
|
||||
{numTransformAssets > 0 ? (
|
||||
<>
|
||||
<TransformInstallWithCurrentUserPermissionCallout
|
||||
count={numTransformAssets}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
</>
|
||||
) : null}
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.fleet.integrations.settings.packageInstallDescription"
|
||||
|
@ -388,7 +358,6 @@ export const SettingsPage: React.FC<Props> = memo(
|
|||
<p>
|
||||
<InstallButton
|
||||
{...packageInfo}
|
||||
numOfAssets={numOfAssets}
|
||||
disabled={packageMetadata?.has_policies}
|
||||
/>
|
||||
</p>
|
||||
|
@ -418,7 +387,6 @@ export const SettingsPage: React.FC<Props> = memo(
|
|||
<div>
|
||||
<UninstallButton
|
||||
{...packageInfo}
|
||||
numOfAssets={numOfAssets}
|
||||
latestVersion={latestVersion}
|
||||
disabled={packageMetadata?.has_policies}
|
||||
/>
|
||||
|
|
|
@ -16,17 +16,16 @@ import { useAuthz, useGetPackageInstallStatus, useUninstallPackage } from '../..
|
|||
|
||||
import { ConfirmPackageUninstall } from './confirm_package_uninstall';
|
||||
|
||||
interface UninstallButtonProps extends Pick<PackageInfo, 'name' | 'title' | 'version'> {
|
||||
interface UninstallButtonProps extends Pick<PackageInfo, 'name' | 'title' | 'version' | 'assets'> {
|
||||
disabled?: boolean;
|
||||
latestVersion?: string;
|
||||
numOfAssets: number;
|
||||
}
|
||||
|
||||
export const UninstallButton: React.FunctionComponent<UninstallButtonProps> = ({
|
||||
disabled = false,
|
||||
latestVersion,
|
||||
name,
|
||||
numOfAssets,
|
||||
assets,
|
||||
title,
|
||||
version,
|
||||
}) => {
|
||||
|
@ -38,6 +37,16 @@ export const UninstallButton: React.FunctionComponent<UninstallButtonProps> = ({
|
|||
|
||||
const [isUninstallModalVisible, setIsUninstallModalVisible] = useState<boolean>(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);
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -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<PackageInfo, 'type'>
|
||||
) {
|
||||
// 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 } =
|
||||
|
|
|
@ -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));
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
- version: 0.1.0
|
||||
changes:
|
||||
- description: Initial release
|
||||
type: enhancement
|
||||
link: https://github.com/elastic/package-spec/pull/777
|
|
@ -0,0 +1 @@
|
|||
# Reference package of content type
|
Binary file not shown.
After Width: | Height: | Size: 200 KiB |
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 1000"><path d="M433.41 29.92c-9.81 5.14-15.18 10.04-19.15 17.74-1.87 3.74-9.57 43.43-17.28 88.49-23.58 140.32-43.19 243.98-72.61 384.3l-2.57 12.38-10.27-10.97c-19.61-20.31-45.76-25.92-67.71-14.48-14.71 7.47-24.05 20.31-34.55 46.93-7.7 20.32-9.11 22.18-15.64 23.58-3.97.7-39.22 1.4-78.45 1.4-67.94 0-71.91.23-81.72 4.9C7.54 596.8 1.94 631.82 22.49 651.9c13.08 12.84 17.04 13.31 88.72 13.54 106 0 125.38-3.04 145.69-24.05l7.94-8.17 6.3 13.54c12.14 25.45 28.72 38.76 51.6 41.56 24.52 2.8 42.49-10.97 56.03-43.19 13.31-31.98 39.93-147.56 63.28-272.93 5.6-29.88 10.51-54.4 11.21-54.4.7 0 26.85 140.09 58.14 311.22 31.28 170.91 58.37 314.73 60.24 319.17 4.2 10.51 9.11 15.87 19.85 21.25 18.45 9.57 43.19 3.04 54.4-14.01 5.14-7.71 7.24-16.34 13.07-57.67 12.36-85.22 33.84-204.06 36.87-204.06.7 0 4.67 5.37 8.4 11.91 14.48 24.75 37.82 38.06 66.54 38.29 29.18 0 40.63-9.34 72.15-58.84l16.11-25.21 57.2-1.17c56.5-1.17 57.44-1.17 63.27-6.77 7.94-7.47 11.44-19.15 10.27-34.09-1.4-15.88-8.17-28.72-20.08-37.12l-9.57-6.77-57.43-1.17c-69.58-1.4-77.51-.23-94.33 14.94-6.3 5.84-17.74 19.84-24.98 31.29-7.47 11.44-13.78 20.78-14.01 20.31-.24-.23-2.57-12.61-5.37-27.32-8.64-48.8-19.38-69.81-40.86-80.55-22.41-11.21-48.33-6.31-64.21 11.91-14.47 16.34-30.12 56.03-43.66 110.67-3.5 14.47-6.77 25.68-7.24 24.52-.23-1.4-26.38-142.66-57.9-314.03-63.04-342.74-58.6-323.83-78.21-333.87-10.96-5.61-28.71-6.08-38.51-.71z"/></svg>
|
After Width: | Height: | Size: 1.4 KiB |
|
@ -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
|
|
@ -0,0 +1,3 @@
|
|||
errors:
|
||||
exclude_checks:
|
||||
- PSR00002 # Allow to use non-GA features.
|
|
@ -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
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue