Skip to content

Commit

Permalink
[Fleet] Fix isPackageVersionOrLaterInstalled to check for installed p…
Browse files Browse the repository at this point in the history
…ackage (elastic#176532)
  • Loading branch information
nchaulet authored and CoenWarmer committed Feb 15, 2024
1 parent e495567 commit 0f1207a
Show file tree
Hide file tree
Showing 3 changed files with 154 additions and 17 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -110,7 +110,7 @@ export async function bulkInstallPackages({
result: {
assets: [...installedEs, ...installedKibana],
status: 'already_installed',
installType: installedPackageResult.installType,
installType: 'unknown',
} as InstallResult,
};
}
Expand Down
108 changes: 108 additions & 0 deletions x-pack/plugins/fleet/server/services/epm/packages/install.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
handleInstallPackageFailure,
installAssetsForInputPackagePolicy,
installPackage,
isPackageVersionOrLaterInstalled,
} from './install';
import * as install from './_install_package';
import { getBundledPackageByPkgKey } from './bundled_packages';
Expand Down Expand Up @@ -715,3 +716,110 @@ describe('handleInstallPackageFailure', () => {
);
});
});

describe('isPackageVersionOrLaterInstalled', () => {
beforeEach(() => {
jest.mocked(getInstallationObject).mockReset();
});
it('should return true if package is installed in the same version as expected', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
jest.mocked(getInstallationObject).mockResolvedValueOnce({
attributes: { name: 'test', version: '1.0.0', install_status: 'installed' },
} as any);
const res = await isPackageVersionOrLaterInstalled({
savedObjectsClient,
pkgName: 'test',
pkgVersion: '1.0.0',
});

expect(res).toEqual(
expect.objectContaining({
package: expect.objectContaining({
name: 'test',
version: '1.0.0',
install_status: 'installed',
}),
})
);
});

it('should return true if package is installed in an higher version as expected', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
jest.mocked(getInstallationObject).mockResolvedValueOnce({
attributes: { name: 'test', version: '1.2.0', install_status: 'installed' },
} as any);
const res = await isPackageVersionOrLaterInstalled({
savedObjectsClient,
pkgName: 'test',
pkgVersion: '1.0.0',
});

expect(res).toEqual(
expect.objectContaining({
package: expect.objectContaining({
name: 'test',
version: '1.2.0',
install_status: 'installed',
}),
})
);
});

it('should return false if package is installed in an lower version as expected', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
jest.mocked(getInstallationObject).mockResolvedValueOnce({
attributes: { name: 'test', version: '0.9.0', install_status: 'installed' },
} as any);
const res = await isPackageVersionOrLaterInstalled({
savedObjectsClient,
pkgName: 'test',
pkgVersion: '1.0.0',
});

expect(res).toEqual(false);
});

it('should retry if package is currently installing', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
jest.mocked(getInstallationObject).mockResolvedValueOnce({
attributes: { name: 'test', version: '1.0.0', install_status: 'installing' },
} as any);
jest.mocked(getInstallationObject).mockResolvedValueOnce({
attributes: { name: 'test', version: '1.0.0', install_status: 'installing' },
} as any);
jest.mocked(getInstallationObject).mockResolvedValueOnce({
attributes: { name: 'test', version: '1.0.0', install_status: 'installed' },
} as any);

const res = await isPackageVersionOrLaterInstalled({
savedObjectsClient,
pkgName: 'test',
pkgVersion: '1.0.0',
});

expect(res).toEqual(
expect.objectContaining({
package: expect.objectContaining({
name: 'test',
version: '1.0.0',
install_status: 'installed',
}),
})
);

expect(getInstallationObject).toBeCalledTimes(3);
});

it('should throw on unexpected error', async () => {
const savedObjectsClient = savedObjectsClientMock.create();
jest.mocked(getInstallationObject).mockRejectedValueOnce(new Error('test unexpected error'));

const res = isPackageVersionOrLaterInstalled({
savedObjectsClient,
pkgName: 'test',
pkgVersion: '1.0.0',
});

await expect(res).rejects.toThrowError('test unexpected error');
});
});
61 changes: 45 additions & 16 deletions x-pack/plugins/fleet/server/services/epm/packages/install.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ import { addErrorToLatestFailedAttempts } from './install_errors_helpers';
import { installIndexTemplatesAndPipelines } from './install_index_template_pipeline';
import { optimisticallyAddEsAssetReferences } from './es_assets_reference';

const MAX_ENSURE_INSTALL_TIME = 60 * 1000;

export async function isPackageInstalled(options: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
Expand All @@ -95,27 +97,53 @@ export async function isPackageInstalled(options: {
return installedPackage !== undefined;
}

// Error used to retry in isPackageVersionOrLaterInstalled
class CurrentlyInstallingError extends Error {}

/**
* Check if a package is currently installed,
* if the package is currently installing it will retry until MAX_ENSURE_INSTALL_TIME is reached
*/
export async function isPackageVersionOrLaterInstalled(options: {
savedObjectsClient: SavedObjectsClientContract;
pkgName: string;
pkgVersion: string;
}): Promise<{ package: Installation; installType: InstallType } | false> {
const { savedObjectsClient, pkgName, pkgVersion } = options;
const installedPackageObject = await getInstallationObject({ savedObjectsClient, pkgName });
const installedPackage = installedPackageObject?.attributes;
if (
installedPackage &&
(installedPackage.version === pkgVersion || semverLt(pkgVersion, installedPackage.version))
) {
let installType: InstallType;
try {
installType = getInstallType({ pkgVersion, installedPkg: installedPackageObject });
} catch (e) {
installType = 'unknown';
}): Promise<{ package: Installation } | false> {
return pRetry(
async () => {
const { savedObjectsClient, pkgName, pkgVersion } = options;
const installedPackageObject = await getInstallationObject({ savedObjectsClient, pkgName });
const installedPackage = installedPackageObject?.attributes;
if (
installedPackage &&
(installedPackage.version === pkgVersion || semverLt(pkgVersion, installedPackage.version))
) {
if (installedPackage.install_status === 'installing') {
throw new CurrentlyInstallingError(
`Package ${pkgName}-${pkgVersion} is currently installing`
);
} else if (installedPackage.install_status === 'install_failed') {
return false;
}

return { package: installedPackage };
}
return false;
},
{
maxRetryTime: MAX_ENSURE_INSTALL_TIME,
onFailedAttempt: (error) => {
if (!(error instanceof CurrentlyInstallingError)) {
throw error;
}
},
}
return { package: installedPackage, installType };
}
return false;
).catch((err): false => {
if (err instanceof CurrentlyInstallingError) {
return false;
}
throw err;
});
}

export async function ensureInstalledPackage(options: {
Expand Down Expand Up @@ -147,6 +175,7 @@ export async function ensureInstalledPackage(options: {
pkgName: pkgKeyProps.name,
pkgVersion: pkgKeyProps.version,
});

if (installedPackageResult) {
return installedPackageResult.package;
}
Expand Down

0 comments on commit 0f1207a

Please sign in to comment.