kibana/packages/kbn-manifest/manifest.ts
Gerard Soldevila 2a085e103a
Add ESLINT constraints to detect inter-group dependencies (#194810)
## Summary

Addresses https://github.com/elastic/kibana-team/issues/1175

As part of the **Sustainable Kibana Architecture** initiative, this PR
sets the foundation to start classifying plugins in isolated groups,
matching our current solutions / project types:

* It adds support for the following fields in the packages' manifests
(kibana.jsonc):
* `group?: 'search' | 'security' | 'observability' | 'platform' |
'common'`
  * `visibility?: 'private' | 'shared'`

* It proposes a folder structure to automatically infer groups:
```javascript
  'src/platform/plugins/shared': {
    group: 'platform',
    visibility: 'shared',
  },
  'src/platform/plugins/internal': {
    group: 'platform',
    visibility: 'private',
  },
  'x-pack/platform/plugins/shared': {
    group: 'platform',
    visibility: 'shared',
  },
  'x-pack/platform/plugins/internal': {
    group: 'platform',
    visibility: 'private',
  },
  'x-pack/solutions/observability/plugins': {
    group: 'observability',
    visibility: 'private',
  },
  'x-pack/solutions/security/plugins': {
    group: 'security',
    visibility: 'private',
  },
  'x-pack/solutions/search/plugins': {
    group: 'search',
    visibility: 'private',
  },
```

* If a plugin is moved to one of the specific locations above, the group
and visibility in the manifest (if specified) must match those inferred
from the path.
* Plugins that are not relocated are considered: `group: 'common',
visibility: 'shared'` by default. As soon as we specify a custom
`group`, the ESLINT rules will check violations against dependencies /
dependants.

The ESLINT rules are pretty simple:
* Plugins can only depend on:
  * Plugins in the same group
  * OR plugins with `'shared'` visibility
* Plugins in `'observability', 'security', 'search'` groups are
mandatorily `'private'`.

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
2024-10-22 06:34:19 -05:00

113 lines
3.7 KiB
TypeScript

/*
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
* Public License v 1"; you may not use this file except in compliance with, at
* your election, the "Elastic License 2.0", the "GNU Affero General Public
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { join } from 'path';
import { writeFile } from 'fs/promises';
import { flatMap, unset } from 'lodash';
import { set } from '@kbn/safer-lodash-set';
import type { ToolingLog } from '@kbn/tooling-log';
import type { Flags } from '@kbn/dev-cli-runner';
import { type Package, getPackages } from '@kbn/repo-packages';
import { REPO_ROOT } from '@kbn/repo-info';
const MANIFEST_FILE = 'kibana.jsonc';
const getKibanaJsonc = (flags: Flags, log: ToolingLog): Package[] => {
const modules = getPackages(REPO_ROOT);
let packageIds: string[] = [];
let pluginIds: string[] = [];
if (typeof flags.package === 'string') {
packageIds = [flags.package].filter(Boolean);
} else if (Array.isArray(flags.package)) {
packageIds = [...flags.package].filter(Boolean);
}
if (typeof flags.plugin === 'string') {
pluginIds = [flags.plugin].filter(Boolean);
} else if (Array.isArray(flags.plugin)) {
pluginIds = [...flags.plugin].filter(Boolean);
}
return modules.filter(
(pkg) =>
packageIds.includes(pkg.id) || (pkg.isPlugin() && pluginIds.includes(pkg.manifest.plugin.id))
);
};
export const listManifestFiles = (flags: Flags, log: ToolingLog) => {
const modules = getPackages(REPO_ROOT);
modules
.filter((module) => module.manifest.type === 'plugin')
.forEach((module) => {
log.info(join(module.directory, MANIFEST_FILE), module.id);
});
};
export const printManifest = (flags: Flags, log: ToolingLog) => {
const kibanaJsoncs = getKibanaJsonc(flags, log);
kibanaJsoncs.forEach((kibanaJsonc) => {
const manifestPath = join(kibanaJsonc.directory, MANIFEST_FILE);
log.info('\n\nShowing manifest: ', manifestPath);
log.info(JSON.stringify(kibanaJsonc, null, 2));
});
};
export const updateManifest = async (flags: Flags, log: ToolingLog) => {
let toSet: string[] = [];
let toUnset: string[] = [];
if (typeof flags.set === 'string') {
toSet = [flags.set].filter(Boolean);
} else if (Array.isArray(flags.set)) {
toSet = [...flags.set].filter(Boolean);
}
if (typeof flags.unset === 'string') {
toUnset = [flags.unset].filter(Boolean);
} else if (Array.isArray(flags.unset)) {
toUnset = [...flags.unset].filter(Boolean);
}
if (!toSet.length && !toUnset.length) {
// no need to update anything
return;
}
const kibanaJsoncs = getKibanaJsonc(flags, log);
for (let i = 0; i < kibanaJsoncs.length; ++i) {
const kibanaJsonc = kibanaJsoncs[i];
if (kibanaJsonc?.manifest) {
const manifestPath = join(kibanaJsonc.directory, MANIFEST_FILE);
log.info('Updating manifest: ', manifestPath);
toSet.forEach((propValue) => {
const [prop, value] = propValue.split('=');
log.info(`Setting "${prop}": "${value}"`);
set(kibanaJsonc.manifest, prop, value);
});
toUnset.forEach((prop) => {
log.info(`Removing "${prop}"`);
unset(kibanaJsonc.manifest, prop);
});
sanitiseManifest(kibanaJsonc);
await writeFile(manifestPath, JSON.stringify(kibanaJsonc.manifest, null, 2));
log.info('DONE');
}
}
};
const sanitiseManifest = (kibanaJsonc: Package) => {
kibanaJsonc.manifest.owner = flatMap(kibanaJsonc.manifest.owner.map((owner) => owner.split(' ')));
};