[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:
Jen Huang 2024-10-15 10:18:41 -07:00 committed by GitHub
parent 0764261189
commit 9512f6c26f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
20 changed files with 252 additions and 91 deletions

View file

@ -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) {

View file

@ -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}

View file

@ -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: (

View file

@ -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

View file

@ -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,
}}

View file

@ -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}
/>

View file

@ -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);

View file

@ -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,

View file

@ -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;

View file

@ -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 {

View file

@ -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();
});
});

View file

@ -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 } =

View file

@ -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));

View file

@ -0,0 +1,5 @@
- version: 0.1.0
changes:
- description: Initial release
type: enhancement
link: https://github.com/elastic/package-spec/pull/777

View file

@ -0,0 +1 @@
# Reference package of content type

Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

View file

@ -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

View file

@ -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

View file

@ -0,0 +1,3 @@
errors:
exclude_checks:
- PSR00002 # Allow to use non-GA features.

View file

@ -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