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>
This commit is contained in:
Gerard Soldevila 2024-10-22 13:34:19 +02:00 committed by GitHub
parent 300678ca85
commit 2a085e103a
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
36 changed files with 1278 additions and 49 deletions

View file

@ -0,0 +1,63 @@
/*
* 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 type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
interface ModuleAttrs {
group: ModuleGroup;
visibility: ModuleVisibility;
}
const DEFAULT_MODULE_ATTRS: ModuleAttrs = {
group: 'common',
visibility: 'shared',
};
const MODULE_GROUPING_BY_PATH: Record<string, ModuleAttrs> = {
'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',
},
};
/**
* Determine a plugin's grouping information based on the path where it is defined
* @param packageRelativePath the path in the repo where the package is located
* @returns The grouping information that corresponds to the given path
*/
export function inferGroupAttrsFromPath(packageRelativePath: string): ModuleAttrs {
const grouping = Object.entries(MODULE_GROUPING_BY_PATH).find(([chunk]) =>
packageRelativePath.startsWith(chunk)
)?.[1];
return grouping ?? DEFAULT_MODULE_ATTRS;
}

View file

@ -7,16 +7,24 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ModuleType } from './module_type';
import { PkgInfo } from './pkg_info';
import type { KibanaPackageManifest } from '@kbn/repo-packages';
import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
import type { ModuleType } from './module_type';
import type { PkgInfo } from './pkg_info';
export interface ModuleId {
/** Type of the module */
type: ModuleType;
/** Specifies the group to which this module belongs */
group: ModuleGroup;
/** Specifies the module visibility, i.e. whether it can be accessed by everybody or only modules in the same group */
visibility: ModuleVisibility;
/** repo relative path to the module's source file */
repoRel: string;
/** info about the package the source file is within, in the case the file is found within a package */
pkgInfo?: PkgInfo;
/** The type of package, as described in the manifest */
manifest?: KibanaPackageManifest;
/** path segments of the dirname of this */
dirs: string[];
}

View file

@ -7,11 +7,14 @@
* License v3.0 only", or the "Server Side Public License, v 1".
*/
import { ImportResolver } from '@kbn/import-resolver';
import { ModuleId } from './module_id';
import { ModuleType } from './module_type';
import type { ImportResolver } from '@kbn/import-resolver';
import type { ModuleGroup, ModuleVisibility } from '@kbn/repo-info/types';
import type { KibanaPackageManifest } from '@kbn/repo-packages/modern/types';
import type { ModuleId } from './module_id';
import type { ModuleType } from './module_type';
import { RANDOM_TEST_FILE_NAMES, TEST_DIR, TEST_TAG } from './config';
import { RepoPath } from './repo_path';
import { inferGroupAttrsFromPath } from './group';
const STATIC_EXTS = new Set(
'json|woff|woff2|ttf|eot|svg|ico|png|jpg|gif|jpeg|html|md|txt|tmpl|xml'
@ -231,7 +234,43 @@ export class RepoSourceClassifier {
return 'common package';
}
classify(absolute: string) {
private getManifest(path: RepoPath): KibanaPackageManifest | undefined {
const pkgInfo = path.getPkgInfo();
return pkgInfo?.pkgId ? this.resolver.getPkgManifest(pkgInfo!.pkgId) : undefined;
}
/**
* Determine the "group" of a file
*/
private getGroup(path: RepoPath): ModuleGroup {
const attrs = inferGroupAttrsFromPath(path.getRepoRel());
const manifest = this.getManifest(path);
if (attrs.group !== 'common') {
// this package has been moved to a 'group-specific' folder, the group is determined by its location
return attrs.group;
} else {
// the package is still in its original location, allow Manifest to dictate its group
return manifest?.group ?? 'common';
}
}
/**
* Determine the "visibility" of a file
*/
private getVisibility(path: RepoPath): ModuleVisibility {
const attrs = inferGroupAttrsFromPath(path.getRepoRel());
const manifest = this.getManifest(path);
if (attrs.group !== 'common') {
// this package has been moved to a 'group-specific' folder, the visibility is determined by its location
return attrs.visibility;
} else {
// the package is still in its original location, allow Manifest to dictate its visibility
return manifest?.visibility ?? 'shared';
}
}
classify(absolute: string): ModuleId {
const path = this.getRepoPath(absolute);
const cached = this.ids.get(path);
@ -241,8 +280,12 @@ export class RepoSourceClassifier {
const id: ModuleId = {
type: this.getType(path),
group: this.getGroup(path),
visibility: this.getVisibility(path),
repoRel: path.getRepoRel(),
pkgInfo: path.getPkgInfo() ?? undefined,
manifest:
(path.getPkgInfo() && this.resolver.getPkgManifest(path.getPkgInfo()!.pkgId)) ?? undefined,
dirs: path.getSegs(),
};
this.ids.set(path, id);

View file

@ -13,6 +13,7 @@
"kbn_references": [
"@kbn/import-resolver",
"@kbn/repo-info",
"@kbn/repo-packages",
],
"exclude": [
"target/**/*",