[Fleet] Make asset tags space aware (#144066)

* fix: use space ID in managed tag SO ID

* Add SO migration

* add integration test for installing pkg in 2 spaces

* Revert "Add SO migration"

This reverts commit 4aeeea658c79d30cfb7ad96090b87418a3b72ad2.

* use legacy tags if they exist

* add tags integration test

* test working in isolation

* neaten tests

* remove test pkg

* revert test file

* tidy for PR

* fix type errors
This commit is contained in:
Mark Hopkin 2022-10-27 09:49:40 +01:00 committed by GitHub
parent 30f585be41
commit 2a8e93311b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 383 additions and 62 deletions

View file

@ -141,6 +141,7 @@ export async function installKibanaAssetsAndReferences({
pkgTitle,
paths,
installedPkg,
spaceId,
}: {
savedObjectsClient: SavedObjectsClientContract;
savedObjectsImporter: Pick<ISavedObjectsImporter, 'import' | 'resolveImportErrors'>;
@ -151,6 +152,7 @@ export async function installKibanaAssetsAndReferences({
pkgTitle: string;
paths: string[];
installedPkg?: SavedObject<Installation>;
spaceId: string;
}) {
const kibanaAssets = await getKibanaAssets(paths);
if (installedPkg) await deleteKibanaSavedObjectsAssets({ savedObjectsClient, installedPkg });
@ -167,7 +169,6 @@ export async function installKibanaAssetsAndReferences({
pkgName,
kibanaAssets,
});
await withPackageSpan('Create and assign package tags', () =>
tagKibanaAssets({
savedObjectTagAssignmentService,
@ -175,6 +176,7 @@ export async function installKibanaAssetsAndReferences({
kibanaAssets,
pkgTitle,
pkgName,
spaceId,
})
);

View file

@ -11,18 +11,18 @@ describe('tagKibanaAssets', () => {
updateTagAssignments: jest.fn(),
} as any;
const savedObjectTagClient = {
getAll: jest.fn(),
get: jest.fn(),
create: jest.fn(),
} as any;
beforeEach(() => {
savedObjectTagAssignmentService.updateTagAssignments.mockReset();
savedObjectTagClient.getAll.mockReset();
savedObjectTagClient.get.mockReset();
savedObjectTagClient.create.mockReset();
});
it('should create Managed and System tags when tagKibanaAssets with System package', async () => {
savedObjectTagClient.getAll.mockResolvedValue([]);
it('should create Managed and System tags when tagKibanaAssets with System package when no tags exist', async () => {
savedObjectTagClient.get.mockRejectedValue(new Error('not found'));
savedObjectTagClient.create.mockImplementation(({ name }: { name: string }) =>
Promise.resolve({ id: name.toLowerCase(), name })
);
@ -34,6 +34,7 @@ describe('tagKibanaAssets', () => {
kibanaAssets,
pkgTitle: 'System',
pkgName: 'system',
spaceId: 'default',
});
expect(savedObjectTagClient.create).toHaveBeenCalledWith(
@ -42,7 +43,7 @@ describe('tagKibanaAssets', () => {
description: '',
color: '#FFFFFF',
},
{ id: 'managed', overwrite: true, refresh: false }
{ id: 'fleet-managed-default', overwrite: true, refresh: false }
);
expect(savedObjectTagClient.create).toHaveBeenCalledWith(
{
@ -50,10 +51,10 @@ describe('tagKibanaAssets', () => {
description: '',
color: '#FFFFFF',
},
{ id: 'system', overwrite: true, refresh: false }
{ id: 'fleet-pkg-system-default', overwrite: true, refresh: false }
);
expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({
tags: ['managed', 'system'],
tags: ['fleet-managed-default', 'fleet-pkg-system-default'],
assign: kibanaAssets.dashboard,
unassign: [],
refresh: false,
@ -61,10 +62,7 @@ describe('tagKibanaAssets', () => {
});
it('should only assign Managed and System tags when tags already exist', async () => {
savedObjectTagClient.getAll.mockResolvedValue([
{ id: 'managed', name: 'Managed' },
{ id: 'system', name: 'System' },
]);
savedObjectTagClient.get.mockResolvedValue({ name: '', color: '', description: '' });
const kibanaAssets = { dashboard: [{ id: 'dashboard1', type: 'dashboard' }] } as any;
await tagKibanaAssets({
@ -73,11 +71,12 @@ describe('tagKibanaAssets', () => {
kibanaAssets,
pkgTitle: 'System',
pkgName: 'system',
spaceId: 'default',
});
expect(savedObjectTagClient.create).not.toHaveBeenCalled();
expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({
tags: ['managed', 'system'],
tags: ['fleet-managed-default', 'fleet-pkg-system-default'],
assign: kibanaAssets.dashboard,
unassign: [],
refresh: false,
@ -85,7 +84,7 @@ describe('tagKibanaAssets', () => {
});
it('should skip non taggable asset types', async () => {
savedObjectTagClient.getAll.mockResolvedValue([]);
savedObjectTagClient.get.mockRejectedValue(new Error('tag not found'));
savedObjectTagClient.create.mockImplementation(({ name }: { name: string }) =>
Promise.resolve({ id: name.toLowerCase(), name })
);
@ -104,10 +103,11 @@ describe('tagKibanaAssets', () => {
kibanaAssets,
pkgTitle: 'System',
pkgName: 'system',
spaceId: 'default',
});
expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({
tags: ['managed', 'system'],
tags: ['fleet-managed-default', 'fleet-pkg-system-default'],
assign: [
...kibanaAssets.dashboard,
...kibanaAssets.search,
@ -129,8 +129,132 @@ describe('tagKibanaAssets', () => {
kibanaAssets,
pkgTitle: 'System',
pkgName: 'system',
spaceId: 'default',
});
expect(savedObjectTagAssignmentService.updateTagAssignments).not.toHaveBeenCalled();
});
it('should use legacy managed tag if it exists', async () => {
savedObjectTagClient.get.mockImplementation(async (id: string) => {
if (id === 'managed') return { name: 'managed', description: '', color: '' };
throw new Error('not found');
});
savedObjectTagClient.create.mockImplementation(({ name }: { name: string }) =>
Promise.resolve({ id: name.toLowerCase(), name })
);
const kibanaAssets = { dashboard: [{ id: 'dashboard1', type: 'dashboard' }] } as any;
await tagKibanaAssets({
savedObjectTagAssignmentService,
savedObjectTagClient,
kibanaAssets,
pkgTitle: 'System',
pkgName: 'system',
spaceId: 'default',
});
expect(savedObjectTagClient.create).not.toHaveBeenCalledWith(
{
name: 'Managed',
description: '',
color: '#FFFFFF',
},
{ id: 'fleet-managed-default', overwrite: true, refresh: false }
);
expect(savedObjectTagClient.create).toHaveBeenCalledWith(
{
name: 'System',
description: '',
color: '#FFFFFF',
},
{ id: 'fleet-pkg-system-default', overwrite: true, refresh: false }
);
expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({
tags: ['managed', 'fleet-pkg-system-default'],
assign: kibanaAssets.dashboard,
unassign: [],
refresh: false,
});
});
it('should use legacy package tag if it exists', async () => {
savedObjectTagClient.get.mockImplementation(async (id: string) => {
if (id === 'system') return { name: 'system', description: '', color: '' };
throw new Error('not found');
});
savedObjectTagClient.create.mockImplementation(({ name }: { name: string }) =>
Promise.resolve({ id: name.toLowerCase(), name })
);
const kibanaAssets = { dashboard: [{ id: 'dashboard1', type: 'dashboard' }] } as any;
await tagKibanaAssets({
savedObjectTagAssignmentService,
savedObjectTagClient,
kibanaAssets,
pkgTitle: 'System',
pkgName: 'system',
spaceId: 'default',
});
expect(savedObjectTagClient.create).toHaveBeenCalledWith(
{
name: 'Managed',
description: '',
color: '#FFFFFF',
},
{ id: 'fleet-managed-default', overwrite: true, refresh: false }
);
expect(savedObjectTagClient.create).not.toHaveBeenCalledWith(
{
name: 'System',
description: '',
color: '#FFFFFF',
},
{ id: 'system', overwrite: true, refresh: false }
);
expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({
tags: ['fleet-managed-default', 'system'],
assign: kibanaAssets.dashboard,
unassign: [],
refresh: false,
});
});
it('should use both legacy tags if they exist', async () => {
savedObjectTagClient.get.mockImplementation(async (id: string) => {
if (id === 'managed') return { name: 'managed', description: '', color: '' };
if (id === 'system') return { name: 'system', description: '', color: '' };
throw new Error('not found');
});
savedObjectTagClient.create.mockImplementation(({ name }: { name: string }) =>
Promise.resolve({ id: name.toLowerCase(), name })
);
const kibanaAssets = { dashboard: [{ id: 'dashboard1', type: 'dashboard' }] } as any;
await tagKibanaAssets({
savedObjectTagAssignmentService,
savedObjectTagClient,
kibanaAssets,
pkgTitle: 'System',
pkgName: 'system',
spaceId: 'default',
});
expect(savedObjectTagClient.create).not.toHaveBeenCalled();
expect(savedObjectTagAssignmentService.updateTagAssignments).toHaveBeenCalledWith({
tags: ['managed', 'system'],
assign: kibanaAssets.dashboard,
unassign: [],
refresh: false,
});
});
});

View file

@ -15,22 +15,45 @@ import { KibanaSavedObjectTypeMapping } from './install';
const TAG_COLOR = '#FFFFFF';
const MANAGED_TAG_NAME = 'Managed';
const MANAGED_TAG_ID = 'managed';
const LEGACY_MANAGED_TAG_ID = 'managed';
export async function tagKibanaAssets({
savedObjectTagAssignmentService,
savedObjectTagClient,
kibanaAssets,
pkgTitle,
pkgName,
}: {
const getManagedTagId = (spaceId: string) => `fleet-managed-${spaceId}`;
const getPackageTagId = (spaceId: string, pkgName: string) => `fleet-pkg-${pkgName}-${spaceId}`;
const getLegacyPackageTagId = (pkgName: string) => pkgName;
interface TagAssetsParams {
savedObjectTagAssignmentService: IAssignmentService;
savedObjectTagClient: ITagsClient;
kibanaAssets: Record<KibanaAssetType, ArchiveAsset[]>;
pkgTitle: string;
pkgName: string;
}) {
const taggableAssets = Object.entries(kibanaAssets).flatMap(([assetType, assets]) => {
spaceId: string;
}
export async function tagKibanaAssets(opts: TagAssetsParams) {
const { savedObjectTagAssignmentService, kibanaAssets } = opts;
const taggableAssets = getTaggableAssets(kibanaAssets);
// no assets to tag
if (taggableAssets.length === 0) {
return;
}
const [managedTagId, packageTagId] = await Promise.all([
ensureManagedTag(opts),
ensurePackageTag(opts),
]);
await savedObjectTagAssignmentService.updateTagAssignments({
tags: [managedTagId, packageTagId],
assign: taggableAssets,
unassign: [],
refresh: false,
});
}
function getTaggableAssets(kibanaAssets: TagAssetsParams['kibanaAssets']) {
return Object.entries(kibanaAssets).flatMap(([assetType, assets]) => {
if (!taggableTypes.includes(KibanaSavedObjectTypeMapping[assetType as KibanaAssetType])) {
return [];
}
@ -41,41 +64,57 @@ export async function tagKibanaAssets({
return assets;
});
// no assets to tag
if (taggableAssets.length === 0) {
return;
}
const allTags = await savedObjectTagClient.getAll();
let managedTag = allTags.find((tag) => tag.name === MANAGED_TAG_NAME);
if (!managedTag) {
managedTag = await savedObjectTagClient.create(
{
name: MANAGED_TAG_NAME,
description: '',
color: TAG_COLOR,
},
{ id: MANAGED_TAG_ID, overwrite: true, refresh: false }
);
}
let packageTag = allTags.find((tag) => tag.name === pkgTitle);
if (!packageTag) {
packageTag = await savedObjectTagClient.create(
{
name: pkgTitle,
description: '',
color: TAG_COLOR,
},
{ id: pkgName, overwrite: true, refresh: false }
);
}
await savedObjectTagAssignmentService.updateTagAssignments({
tags: [managedTag.id, packageTag.id],
assign: taggableAssets,
unassign: [],
refresh: false,
});
}
async function ensureManagedTag(
opts: Pick<TagAssetsParams, 'spaceId' | 'savedObjectTagClient'>
): Promise<string> {
const { spaceId, savedObjectTagClient } = opts;
const managedTagId = getManagedTagId(spaceId);
const managedTag = await savedObjectTagClient.get(managedTagId).catch(() => {});
if (managedTag) return managedTagId;
const legacyManagedTag = await savedObjectTagClient.get(LEGACY_MANAGED_TAG_ID).catch(() => {});
if (legacyManagedTag) return LEGACY_MANAGED_TAG_ID;
await savedObjectTagClient.create(
{
name: MANAGED_TAG_NAME,
description: '',
color: TAG_COLOR,
},
{ id: managedTagId, overwrite: true, refresh: false }
);
return managedTagId;
}
async function ensurePackageTag(
opts: Pick<TagAssetsParams, 'spaceId' | 'savedObjectTagClient' | 'pkgName' | 'pkgTitle'>
): Promise<string> {
const { spaceId, savedObjectTagClient, pkgName, pkgTitle } = opts;
const packageTagId = getPackageTagId(spaceId, pkgName);
const packageTag = await savedObjectTagClient.get(packageTagId).catch(() => {});
if (packageTag) return packageTagId;
const legacyPackageTagId = getLegacyPackageTagId(pkgName);
const legacyPackageTag = await savedObjectTagClient.get(legacyPackageTagId).catch(() => {});
if (legacyPackageTag) return legacyPackageTagId;
await savedObjectTagClient.create(
{
name: pkgTitle,
description: '',
color: TAG_COLOR,
},
{ id: packageTagId, overwrite: true, refresh: false }
);
return packageTagId;
}

View file

@ -133,6 +133,7 @@ export async function _installPackage({
paths,
installedPkg,
logger,
spaceId,
})
);
// Necessary to avoid async promise rejection warning

View file

@ -23,6 +23,7 @@ export default function loadTests({ loadTestFile }) {
loadTestFile(require.resolve('./install_remove_kbn_assets_in_space'));
loadTestFile(require.resolve('./install_remove_multiple'));
loadTestFile(require.resolve('./install_update'));
loadTestFile(require.resolve('./install_tag_assets'));
loadTestFile(require.resolve('./bulk_upgrade'));
loadTestFile(require.resolve('./update_assets'));
loadTestFile(require.resolve('./data_stream'));

View file

@ -0,0 +1,154 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import expect from '@kbn/expect';
import { FtrProviderContext } from '../../../api_integration/ftr_provider_context';
import { skipIfNoDockerRegistry } from '../../helpers';
import { setupFleetAndAgents } from '../agents/services';
const testSpaceId = 'fleet_test_space';
export default function (providerContext: FtrProviderContext) {
const { getService } = providerContext;
const kibanaServer = getService('kibanaServer');
const supertest = getService('supertest');
const dockerServers = getService('dockerServers');
const server = dockerServers.get('registry');
const pkgName = 'only_dashboard';
const pkgVersion = '0.1.0';
const uninstallPackage = async (pkg: string, version: string) => {
await supertest.delete(`/api/fleet/epm/packages/${pkg}/${version}`).set('kbn-xsrf', 'xxxx');
};
const installPackageInSpace = async (pkg: string, version: string, spaceId: string) => {
await supertest
.post(`/s/${spaceId}/api/fleet/epm/packages/${pkg}/${version}`)
.set('kbn-xsrf', 'xxxx')
.send({ force: true })
.expect(200);
};
const createSpace = async (spaceId: string) => {
await supertest
.post(`/api/spaces/space`)
.set('kbn-xsrf', 'xxxx')
.send({
name: spaceId,
id: spaceId,
initials: 's',
color: '#D6BF57',
disabledFeatures: [],
imageUrl: '',
})
.expect(200);
};
const getTag = async (id: string, space?: string) =>
kibanaServer.savedObjects
.get({
type: 'tag',
id,
...(space && { space }),
})
.catch(() => {});
const deleteTag = async (id: string) =>
kibanaServer.savedObjects
.delete({
type: 'tag',
id,
})
.catch(() => {});
const deleteSpace = async (spaceId: string) => {
await supertest.delete(`/api/spaces/space/${spaceId}`).set('kbn-xsrf', 'xxxx').send();
};
describe('asset tagging', () => {
skipIfNoDockerRegistry(providerContext);
setupFleetAndAgents(providerContext);
before(async () => {
await createSpace(testSpaceId);
});
after(async () => {
await deleteSpace(testSpaceId);
});
describe('creates correct tags when installing a package in non default space after installing in default space', async () => {
before(async () => {
if (!server.enabled) return;
await installPackageInSpace('all_assets', pkgVersion, 'default');
await installPackageInSpace(pkgName, pkgVersion, testSpaceId);
});
after(async () => {
if (!server.enabled) return;
await uninstallPackage('all_assets', pkgVersion);
await uninstallPackage(pkgName, pkgVersion);
});
it('Should create managed tag saved objects', async () => {
const defaultTag = await getTag('fleet-managed-default');
expect(defaultTag).not.equal(undefined);
const spaceTag = await getTag('fleet-managed-fleet_test_space', testSpaceId);
expect(spaceTag).not.equal(undefined);
});
it('Should create package tag saved objects', async () => {
const defaultTag = await getTag(`fleet-pkg-all_assets-default`);
expect(defaultTag).not.equal(undefined);
const spaceTag = await getTag(`fleet-pkg-${pkgName}-fleet_test_space`, testSpaceId);
expect(spaceTag).not.equal(undefined);
});
});
describe('Handles presence of legacy tags', async () => {
before(async () => {
if (!server.enabled) return;
// first clean up any existing tag saved objects as they arent cleaned on uninstall
await deleteTag('fleet-managed-default');
await deleteTag(`fleet-pkg-${pkgName}-default`);
// now create the legacy tags
await kibanaServer.savedObjects.create({
type: 'tag',
id: 'managed',
overwrite: false,
attributes: {
name: 'managed',
description: '',
color: '#FFFFFF',
},
});
await kibanaServer.savedObjects.create({
type: 'tag',
id: pkgName,
overwrite: false,
attributes: {
name: pkgName,
description: '',
color: '#FFFFFF',
},
});
await installPackageInSpace(pkgName, pkgVersion, 'default');
});
after(async () => {
if (!server.enabled) return;
await uninstallPackage(pkgName, pkgVersion);
await deleteTag('managed');
await deleteTag('tag');
});
it('Should not create space aware tag saved objects if legacy tags exist', async () => {
const managedTag = await getTag('fleet-managed-default');
expect(managedTag).equal(undefined);
const pkgTag = await getTag(`fleet-pkg-${pkgName}-default`);
expect(pkgTag).equal(undefined);
});
});
});
}