[Fleet] Add support for "Edit Package Policy" extensions using latest version of a package (#114914)

* Add support for extensions using latest version of a package and forcing upgrade state for edit policy view

* Fix isUpgrade flag on integrations UI version of edit page

* Treat non-validation errors as general failures in server and UI

* Fix tests + don't call upgrade API when saving

* fix i18n

* Fix default name always appearing when editing package policies via extension UI

* Opt security solution plugin out of new extension option

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Kyle Pollich 2021-10-20 14:35:45 -04:00 committed by GitHub
parent 1da11dfdc0
commit 6d4cfc5e39
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
14 changed files with 194 additions and 68 deletions

View file

@ -367,6 +367,7 @@ export class ApmPlugin implements Plugin<ApmPluginSetup, ApmPluginStart> {
fleet.registerExtension({
package: 'apm',
view: 'package-policy-edit',
useLatestPackageVersion: true,
Component: getLazyAPMPolicyEditExtension(),
});

View file

@ -61,7 +61,11 @@ export const CreatePackagePolicyPageLayout: React.FunctionComponent<{
const isEdit = useMemo(() => ['edit', 'package-edit'].includes(from), [from]);
const isUpgrade = useMemo(
() =>
['upgrade-from-fleet-policy-list', 'upgrade-from-integrations-policy-list'].includes(from),
[
'upgrade-from-fleet-policy-list',
'upgrade-from-integrations-policy-list',
'upgrade-from-extension',
].includes(from),
[from]
);

View file

@ -105,11 +105,12 @@ export const StepDefinePackagePolicy: React.FunctionComponent<{
agentPolicy?.id || '',
packagePolicy.output_id,
packagePolicy.namespace,
`${packageInfo.name}-${
pkgPoliciesWithMatchingNames.length
? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1
: 1
}`,
packagePolicy.name ||
`${packageInfo.name}-${
pkgPoliciesWithMatchingNames.length
? pkgPoliciesWithMatchingNames[pkgPoliciesWithMatchingNames.length - 1] + 1
: 1
}`,
packagePolicy.description,
integrationToEnable
)

View file

@ -11,7 +11,9 @@ export type EditPackagePolicyFrom =
| 'policy'
| 'edit'
| 'upgrade-from-fleet-policy-list'
| 'upgrade-from-integrations-policy-list';
| 'upgrade-from-integrations-policy-list'
| 'upgrade-from-extension';
export type PackagePolicyFormState =
| 'VALID'
| 'INVALID'

View file

@ -41,10 +41,12 @@ import {
sendGetOneAgentPolicy,
sendGetOnePackagePolicy,
sendGetPackageInfoByKey,
sendUpgradePackagePolicy,
sendUpgradePackagePolicyDryRun,
} from '../../../hooks';
import { useBreadcrumbs as useIntegrationsBreadcrumbs } from '../../../../integrations/hooks';
import {
useBreadcrumbs as useIntegrationsBreadcrumbs,
useGetOnePackagePolicy,
} from '../../../../integrations/hooks';
import { Loading, Error, ExtensionWrapper } from '../../../components';
import { ConfirmDeployAgentPolicyModal } from '../components';
import { CreatePackagePolicyPageLayout } from '../create_package_policy_page/components';
@ -68,7 +70,23 @@ export const EditPackagePolicyPage = memo(() => {
params: { packagePolicyId },
} = useRouteMatch<{ policyId: string; packagePolicyId: string }>();
return <EditPackagePolicyForm packagePolicyId={packagePolicyId} />;
const packagePolicy = useGetOnePackagePolicy(packagePolicyId);
const extensionView = useUIExtension(
packagePolicy.data?.item?.package?.name ?? '',
'package-policy-edit'
);
return (
<EditPackagePolicyForm
packagePolicyId={packagePolicyId}
// If an extension opts in to this `useLatestPackageVersion` flag, we want to display
// the edit form in an "upgrade" state regardless of whether the user intended to
// "edit" their policy or "upgrade" it. This ensures the new policy generated will be
// set to use the latest version of the package, not its current version.
isUpgrade={extensionView?.useLatestPackageVersion}
/>
);
});
export const EditPackagePolicyForm = memo<{
@ -345,29 +363,6 @@ export const EditPackagePolicyForm = memo<{
const { error } = await savePackagePolicy();
if (!error) {
if (isUpgrade) {
const { error: upgradeError } = await sendUpgradePackagePolicy([packagePolicyId]);
if (upgradeError) {
notifications.toasts.addError(upgradeError, {
title: i18n.translate('xpack.fleet.upgradePackagePolicy.failedNotificationTitle', {
defaultMessage: 'Error upgrading {packagePolicyName}',
values: {
packagePolicyName: packagePolicy.name,
},
}),
toastMessage: i18n.translate(
'xpack.fleet.editPackagePolicy.failedConflictNotificationMessage',
{
defaultMessage: `Data is out of date. Refresh the page to get the latest policy.`,
}
),
});
return;
}
}
application.navigateToUrl(successRedirectPath);
notifications.toasts.addSuccess({
title: i18n.translate('xpack.fleet.editPackagePolicy.updatedNotificationTitle', {
@ -426,7 +421,7 @@ export const EditPackagePolicyForm = memo<{
const [selectedTab, setSelectedTab] = useState(0);
const layoutProps = {
from,
from: extensionView?.useLatestPackageVersion ? 'upgrade-from-extension' : from,
cancelUrl,
agentPolicy,
packageInfo,

View file

@ -10,11 +10,25 @@ import { useRouteMatch } from 'react-router-dom';
// TODO: Needs to be moved
import { EditPackagePolicyForm } from '../../../../../fleet/sections/agent_policy/edit_package_policy_page';
import { useGetOnePackagePolicy, useUIExtension } from '../../../../hooks';
export const Policy = memo(() => {
const {
params: { packagePolicyId },
} = useRouteMatch<{ packagePolicyId: string }>();
return <EditPackagePolicyForm packagePolicyId={packagePolicyId} from="package-edit" />;
const packagePolicy = useGetOnePackagePolicy(packagePolicyId);
const extensionView = useUIExtension(
packagePolicy.data?.item?.package?.name ?? '',
'package-policy-edit'
);
return (
<EditPackagePolicyForm
packagePolicyId={packagePolicyId}
from="package-edit"
isUpgrade={extensionView?.useLatestPackageVersion}
/>
);
});

View file

@ -59,6 +59,13 @@ export function useGetPackagePolicies(query: GetPackagePoliciesRequest['query'])
});
}
export const useGetOnePackagePolicy = (packagePolicyId: string) => {
return useRequest<GetOnePackagePolicyResponse>({
path: packagePolicyRouteService.getInfoPath(packagePolicyId),
method: 'get',
});
};
export const sendGetOnePackagePolicy = (packagePolicyId: string) => {
return sendRequest<GetOnePackagePolicyResponse>({
path: packagePolicyRouteService.getInfoPath(packagePolicyId),

View file

@ -50,6 +50,7 @@ export interface PackagePolicyEditExtensionComponentProps {
export interface PackagePolicyEditExtension {
package: string;
view: 'package-policy-edit';
useLatestPackageVersion?: boolean;
Component: LazyExoticComponent<PackagePolicyEditExtensionComponent>;
}

View file

@ -38,6 +38,8 @@ export class PackageCacheError extends IngestManagerError {}
export class PackageOperationNotSupportedError extends IngestManagerError {}
export class ConcurrentInstallOperationError extends IngestManagerError {}
export class AgentReassignmentError extends IngestManagerError {}
export class PackagePolicyIneligibleForUpgradeError extends IngestManagerError {}
export class PackagePolicyValidationError extends IngestManagerError {}
export class HostedAgentPolicyRestrictionRelatedError extends IngestManagerError {
constructor(message = 'Cannot perform that action') {
super(

View file

@ -45,6 +45,8 @@ import {
HostedAgentPolicyRestrictionRelatedError,
IngestManagerError,
ingestErrorToResponseOptions,
PackagePolicyIneligibleForUpgradeError,
PackagePolicyValidationError,
} from '../errors';
import { NewPackagePolicySchema, UpdatePackagePolicySchema } from '../types';
import type {
@ -528,25 +530,25 @@ class PackagePolicyService {
pkgName: packagePolicy.package.name,
pkgVersion: installedPackage?.version ?? '',
});
}
const isInstalledVersionLessThanOrEqualToPolicyVersion = semverLte(
installedPackage?.version ?? '',
packagePolicy.package.version
const isInstalledVersionLessThanOrEqualToPolicyVersion = semverLte(
packageInfo?.version ?? '',
packagePolicy.package.version
);
if (isInstalledVersionLessThanOrEqualToPolicyVersion) {
throw new PackagePolicyIneligibleForUpgradeError(
i18n.translate('xpack.fleet.packagePolicy.ineligibleForUpgradeError', {
defaultMessage:
"Package policy {id}'s package version {version} of package {name} is up to date with the installed package. Please install the latest version of {name}.",
values: {
id: packagePolicy.id,
name: packagePolicy.package.name,
version: packagePolicy.package.version,
},
})
);
if (isInstalledVersionLessThanOrEqualToPolicyVersion) {
throw new IngestManagerError(
i18n.translate('xpack.fleet.packagePolicy.ineligibleForUpgradeError', {
defaultMessage:
"Package policy {id}'s package version {version} of package {name} is up to date with the installed package. Please install the latest version of {name}.",
values: {
id: packagePolicy.id,
name: packagePolicy.package.name,
version: packagePolicy.package.version,
},
})
);
}
}
return {
@ -600,6 +602,13 @@ class PackagePolicyService {
currentVersion: packageInfo.version,
});
} catch (error) {
// We only want to specifically handle validation errors for the new package policy. If a more severe or
// general error is thrown elsewhere during the upgrade process, we want to surface that directly in
// order to preserve any status code mappings, etc that might be included w/ the particular error type
if (!(error instanceof PackagePolicyValidationError)) {
throw error;
}
result.push({
id,
success: false,
@ -653,6 +662,10 @@ class PackagePolicyService {
hasErrors,
};
} catch (error) {
if (!(error instanceof PackagePolicyValidationError)) {
throw error;
}
return {
hasErrors: true,
...ingestErrorToResponseOptions(error),
@ -1089,7 +1102,7 @@ export function overridePackageInputs(
return { ...resultingPackagePolicy, errors: responseFormattedValidationErrors };
}
throw new IngestManagerError(
throw new PackagePolicyValidationError(
i18n.translate('xpack.fleet.packagePolicyInvalidError', {
defaultMessage: 'Package policy is invalid: {errors}',
values: {

View file

@ -11255,7 +11255,6 @@
"xpack.fleet.upgradeAgents.upgradeMultipleDescription": "このアクションにより、複数のエージェントがバージョン{version}にアップグレードされます。このアクションは元に戻せません。続行していいですか?",
"xpack.fleet.upgradeAgents.upgradeSingleDescription": "このアクションにより、「{hostName}」で実行中のエージェントがバージョン{version}にアップグレードされます。このアクションは元に戻せません。続行していいですか?",
"xpack.fleet.upgradeAgents.upgradeSingleTitle": "エージェントを最新バージョンにアップグレード",
"xpack.fleet.upgradePackagePolicy.failedNotificationTitle": "{packagePolicyName}のアップグレードエラー",
"xpack.fleet.upgradePackagePolicy.pageDescriptionFromUpgrade": "この統合をアップグレードし、選択したエージェントポリシーに変更をデプロイします",
"xpack.fleet.upgradePackagePolicy.previousVersionFlyout.title": "'{name}'パッケージポリシー",
"xpack.fleet.upgradePackagePolicy.statusCallout.errorContent": "この統合には、バージョン{currentVersion}から{upgradeVersion}で競合するフィールドがあります。構成を確認して保存し、アップグレードを実行してください。{previousConfigurationLink}を参照して比較できます。",

View file

@ -11378,7 +11378,6 @@
"xpack.fleet.upgradeAgents.upgradeMultipleTitle": "将{count, plural, one {代理} other { {count} 个代理} =true {所有选定代理}}升级到最新版本",
"xpack.fleet.upgradeAgents.upgradeSingleDescription": "此操作会将“{hostName}”上运行的代理升级到版本 {version}。此操作无法撤消。是否确定要继续?",
"xpack.fleet.upgradeAgents.upgradeSingleTitle": "将代理升级到最新版本",
"xpack.fleet.upgradePackagePolicy.failedNotificationTitle": "升级 {packagePolicyName} 时出错",
"xpack.fleet.upgradePackagePolicy.pageDescriptionFromUpgrade": "升级此集成并将更改部署到选定代理策略",
"xpack.fleet.upgradePackagePolicy.previousVersionFlyout.title": "“{name}”软件包策略",
"xpack.fleet.upgradePackagePolicy.statusCallout.errorContent": "此集成在版本 {currentVersion} 和 {upgradeVersion} 之间有冲突字段。请复查配置并保存,以执行升级。您可以参考您的 {previousConfigurationLink}以进行比较。",

View file

@ -221,6 +221,7 @@ export class UptimePlugin
registerExtension({
package: 'synthetics',
view: 'package-policy-edit',
useLatestPackageVersion: true,
Component: LazySyntheticsPolicyEditExtension,
});

View file

@ -146,7 +146,7 @@ export default function (providerContext: FtrProviderContext) {
describe('upgrade', function () {
it('should respond with an error when "dryRun: false" is provided', async function () {
const { body }: { body: UpgradePackagePolicyResponse } = await supertest
await supertest
.post(`/api/fleet/package_policies/upgrade`)
.set('kbn-xsrf', 'xxxx')
.send({
@ -154,10 +154,7 @@ export default function (providerContext: FtrProviderContext) {
dryRun: false,
packageVersion: '0.2.0-add-non-required-test-var',
})
.expect(200);
expect(body.length).to.be(1);
expect(body[0].success).to.be(false);
.expect(400);
});
});
});
@ -1041,31 +1038,121 @@ export default function (providerContext: FtrProviderContext) {
describe('when package policy is not found', function () {
it('should return an 200 with errors when "dryRun:true" is provided', async function () {
const { body }: { body: UpgradePackagePolicyDryRunResponse } = await supertest
await supertest
.post(`/api/fleet/package_policies/upgrade`)
.set('kbn-xsrf', 'xxxx')
.send({
packagePolicyIds: ['xxxx', 'yyyy'],
dryRun: true,
})
.expect(200);
expect(body[0].hasErrors).to.be(true);
expect(body[1].hasErrors).to.be(true);
.expect(404);
});
it('should return a 200 with errors and "success:false" when "dryRun:false" is provided', async function () {
const { body }: { body: UpgradePackagePolicyResponse } = await supertest
await supertest
.post(`/api/fleet/package_policies/upgrade`)
.set('kbn-xsrf', 'xxxx')
.send({
packagePolicyIds: ['xxxx', 'yyyy'],
dryRun: false,
})
.expect(404);
});
});
describe("when policy's package version is up to date", function () {
withTestPackageVersion('0.1.0');
beforeEach(async function () {
const { body: agentPolicyResponse } = await supertest
.post(`/api/fleet/agent_policies`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'Test policy',
namespace: 'default',
})
.expect(200);
expect(body[0].success).to.be(false);
expect(body[1].success).to.be(false);
agentPolicyId = agentPolicyResponse.item.id;
const { body: packagePolicyResponse } = await supertest
.post(`/api/fleet/package_policies`)
.set('kbn-xsrf', 'xxxx')
.send({
name: 'package_policy_upgrade_1',
description: '',
namespace: 'default',
policy_id: agentPolicyId,
enabled: true,
output_id: '',
inputs: [
{
policy_template: 'package_policy_upgrade',
type: 'test_input',
enabled: true,
streams: [
{
id: 'test-package_policy_upgrade-xxxx',
enabled: true,
data_stream: {
type: 'test_stream',
dataset: 'package_policy_upgrade.test_stream',
},
},
],
},
],
package: {
name: 'package_policy_upgrade',
title: 'This is a test package for upgrading package policies',
version: '0.1.0',
},
})
.expect(200);
packagePolicyId = packagePolicyResponse.item.id;
});
afterEach(async function () {
await supertest
.post(`/api/fleet/package_policies/delete`)
.set('kbn-xsrf', 'xxxx')
.send({ packagePolicyIds: [packagePolicyId] })
.expect(200);
await supertest
.post('/api/fleet/agent_policies/delete')
.set('kbn-xsrf', 'xxxx')
.send({ agentPolicyId })
.expect(200);
});
describe('dry run', function () {
it('should respond with a bad request', async function () {
await supertest
.post(`/api/fleet/package_policies/upgrade`)
.set('kbn-xsrf', 'xxxx')
.send({
packagePolicyIds: [packagePolicyId],
dryRun: true,
packageVersion: '0.1.0',
})
.expect(400);
});
});
describe('upgrade', function () {
it('should respond with a bad request', async function () {
await supertest
.post(`/api/fleet/package_policies/upgrade`)
.set('kbn-xsrf', 'xxxx')
.send({
packagePolicyIds: [packagePolicyId],
dryRun: false,
packageVersion: '0.1.0',
})
.expect(400);
});
});
});
});