diff --git a/packages/analytics/shippers/fullstory/src/fullstory_shipper.ts b/packages/analytics/shippers/fullstory/src/fullstory_shipper.ts index a4ad730f87b2e16..d02e87cace6b1f7 100644 --- a/packages/analytics/shippers/fullstory/src/fullstory_shipper.ts +++ b/packages/analytics/shippers/fullstory/src/fullstory_shipper.ts @@ -30,7 +30,7 @@ const PAGE_VARS_KEYS = [ // Deployment-specific keys 'version', // x4, split to version_major, version_minor, version_patch for easier filtering - 'buildNum', // May be useful for Serverless + 'buildNum', // May be useful for Serverless, TODO: replace with buildHash 'cloudId', 'deploymentId', 'projectId', // projectId and deploymentId are mutually exclusive. They shouldn't be sent in the same offering. diff --git a/packages/core/apps/core-apps-server-internal/src/bundle_routes/register_bundle_routes.test.ts b/packages/core/apps/core-apps-server-internal/src/bundle_routes/register_bundle_routes.test.ts index e6550f6e86cb6b4..8a0ae599150fd9d 100644 --- a/packages/core/apps/core-apps-server-internal/src/bundle_routes/register_bundle_routes.test.ts +++ b/packages/core/apps/core-apps-server-internal/src/bundle_routes/register_bundle_routes.test.ts @@ -13,10 +13,12 @@ import { httpServiceMock } from '@kbn/core-http-server-mocks'; import type { InternalPluginInfo, UiPlugins } from '@kbn/core-plugins-base-server-internal'; import { registerBundleRoutes } from './register_bundle_routes'; import { FileHashCache } from './file_hash_cache'; +import { BasePath, StaticAssets } from '@kbn/core-http-server-internal'; const createPackageInfo = (parts: Partial = {}): PackageInfo => ({ buildNum: 42, - buildSha: 'sha', + buildSha: 'shasha', + buildShaShort: 'sha', dist: true, branch: 'master', version: '8.0.0', @@ -41,9 +43,12 @@ const createUiPlugins = (...ids: string[]): UiPlugins => ({ describe('registerBundleRoutes', () => { let router: ReturnType; + let staticAssets: StaticAssets; beforeEach(() => { router = httpServiceMock.createRouter(); + const basePath = httpServiceMock.createBasePath('/server-base-path') as unknown as BasePath; + staticAssets = new StaticAssets({ basePath, cdnConfig: {} as any, shaDigest: 'sha' }); }); afterEach(() => { @@ -53,7 +58,7 @@ describe('registerBundleRoutes', () => { it('registers core and shared-dep bundles', () => { registerBundleRoutes({ router, - serverBasePath: '/server-base-path', + staticAssets, packageInfo: createPackageInfo(), uiPlugins: createUiPlugins(), }); @@ -64,39 +69,39 @@ describe('registerBundleRoutes', () => { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: 'uiSharedDepsSrcDistDir', - publicPath: '/server-base-path/42/bundles/kbn-ui-shared-deps-src/', - routePath: '/42/bundles/kbn-ui-shared-deps-src/', + publicPath: '/server-base-path/sha/bundles/kbn-ui-shared-deps-src/', + routePath: '/sha/bundles/kbn-ui-shared-deps-src/', }); expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: 'uiSharedDepsNpmDistDir', - publicPath: '/server-base-path/42/bundles/kbn-ui-shared-deps-npm/', - routePath: '/42/bundles/kbn-ui-shared-deps-npm/', + publicPath: '/server-base-path/sha/bundles/kbn-ui-shared-deps-npm/', + routePath: '/sha/bundles/kbn-ui-shared-deps-npm/', }); expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: expect.stringMatching(/\/@kbn\/core\/target\/public$/), - publicPath: '/server-base-path/42/bundles/core/', - routePath: '/42/bundles/core/', + publicPath: '/server-base-path/sha/bundles/core/', + routePath: '/sha/bundles/core/', }); expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: 'kbnMonacoBundleDir', - publicPath: '/server-base-path/42/bundles/kbn-monaco/', - routePath: '/42/bundles/kbn-monaco/', + publicPath: '/server-base-path/sha/bundles/kbn-monaco/', + routePath: '/sha/bundles/kbn-monaco/', }); }); it('registers plugin bundles', () => { registerBundleRoutes({ router, - serverBasePath: '/server-base-path', + staticAssets, packageInfo: createPackageInfo(), uiPlugins: createUiPlugins('plugin-a', 'plugin-b'), }); @@ -107,16 +112,16 @@ describe('registerBundleRoutes', () => { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-a/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-a/8.0.0/', - routePath: '/42/bundles/plugin/plugin-a/8.0.0/', + publicPath: '/server-base-path/sha/bundles/plugin/plugin-a/8.0.0/', + routePath: '/sha/bundles/plugin/plugin-a/8.0.0/', }); expect(registerRouteForBundleMock).toHaveBeenCalledWith(router, { fileHashCache: expect.any(FileHashCache), isDist: true, bundlesPath: '/plugins/plugin-b/public-target-dir', - publicPath: '/server-base-path/42/bundles/plugin/plugin-b/8.0.0/', - routePath: '/42/bundles/plugin/plugin-b/8.0.0/', + publicPath: '/server-base-path/sha/bundles/plugin/plugin-b/8.0.0/', + routePath: '/sha/bundles/plugin/plugin-b/8.0.0/', }); }); }); diff --git a/packages/core/apps/core-apps-server-internal/src/bundle_routes/register_bundle_routes.ts b/packages/core/apps/core-apps-server-internal/src/bundle_routes/register_bundle_routes.ts index 22266e97355e37d..617f085c8ad43a6 100644 --- a/packages/core/apps/core-apps-server-internal/src/bundle_routes/register_bundle_routes.ts +++ b/packages/core/apps/core-apps-server-internal/src/bundle_routes/register_bundle_routes.ts @@ -13,6 +13,7 @@ import { distDir as UiSharedDepsSrcDistDir } from '@kbn/ui-shared-deps-src'; import * as KbnMonaco from '@kbn/monaco/server'; import type { IRouter } from '@kbn/core-http-server'; import type { UiPlugins } from '@kbn/core-plugins-base-server-internal'; +import { InternalStaticAssets } from '@kbn/core-http-server-internal'; import { FileHashCache } from './file_hash_cache'; import { registerRouteForBundle } from './bundles_route'; @@ -28,56 +29,61 @@ import { registerRouteForBundle } from './bundles_route'; */ export function registerBundleRoutes({ router, - serverBasePath, uiPlugins, packageInfo, + staticAssets, }: { router: IRouter; - serverBasePath: string; uiPlugins: UiPlugins; packageInfo: PackageInfo; + staticAssets: InternalStaticAssets; }) { - const { dist: isDist, buildNum } = packageInfo; + const { dist: isDist } = packageInfo; // rather than calculate the fileHash on every request, we // provide a cache object to `resolveDynamicAssetResponse()` that // will store the most recently used hashes. const fileHashCache = new FileHashCache(); + const sharedNpmDepsPath = '/bundles/kbn-ui-shared-deps-npm/'; registerRouteForBundle(router, { - publicPath: `${serverBasePath}/${buildNum}/bundles/kbn-ui-shared-deps-npm/`, - routePath: `/${buildNum}/bundles/kbn-ui-shared-deps-npm/`, + publicPath: staticAssets.prependPublicUrl(sharedNpmDepsPath) + '/', + routePath: staticAssets.prependServerPath(sharedNpmDepsPath) + '/', bundlesPath: UiSharedDepsNpm.distDir, fileHashCache, isDist, }); + const sharedDepsPath = '/bundles/kbn-ui-shared-deps-src/'; registerRouteForBundle(router, { - publicPath: `${serverBasePath}/${buildNum}/bundles/kbn-ui-shared-deps-src/`, - routePath: `/${buildNum}/bundles/kbn-ui-shared-deps-src/`, + publicPath: staticAssets.prependPublicUrl(sharedDepsPath) + '/', + routePath: staticAssets.prependServerPath(sharedDepsPath) + '/', bundlesPath: UiSharedDepsSrcDistDir, fileHashCache, isDist, }); + const coreBundlePath = '/bundles/core/'; registerRouteForBundle(router, { - publicPath: `${serverBasePath}/${buildNum}/bundles/core/`, - routePath: `/${buildNum}/bundles/core/`, + publicPath: staticAssets.prependPublicUrl(coreBundlePath) + '/', + routePath: staticAssets.prependServerPath(coreBundlePath) + '/', bundlesPath: isDist ? fromRoot('node_modules/@kbn/core/target/public') : fromRoot('src/core/target/public'), fileHashCache, isDist, }); + const monacoEditorPath = '/bundles/kbn-monaco/'; registerRouteForBundle(router, { - publicPath: `${serverBasePath}/${buildNum}/bundles/kbn-monaco/`, - routePath: `/${buildNum}/bundles/kbn-monaco/`, + publicPath: staticAssets.prependPublicUrl(monacoEditorPath) + '/', + routePath: staticAssets.prependServerPath(monacoEditorPath) + '/', bundlesPath: KbnMonaco.bundleDir, fileHashCache, isDist, }); [...uiPlugins.internal.entries()].forEach(([id, { publicTargetDir, version }]) => { + const pluginBundlesPath = `/bundles/plugin/${id}/${version}/`; registerRouteForBundle(router, { - publicPath: `${serverBasePath}/${buildNum}/bundles/plugin/${id}/${version}/`, - routePath: `/${buildNum}/bundles/plugin/${id}/${version}/`, + publicPath: staticAssets.prependPublicUrl(pluginBundlesPath) + '/', + routePath: staticAssets.prependServerPath(pluginBundlesPath) + '/', bundlesPath: publicTargetDir, fileHashCache, isDist, diff --git a/packages/core/apps/core-apps-server-internal/src/core_app.test.ts b/packages/core/apps/core-apps-server-internal/src/core_app.test.ts index 851f443cd3e1ce3..f0bde326b7b7413 100644 --- a/packages/core/apps/core-apps-server-internal/src/core_app.test.ts +++ b/packages/core/apps/core-apps-server-internal/src/core_app.test.ts @@ -16,8 +16,8 @@ import { httpResourcesMock } from '@kbn/core-http-resources-server-mocks'; import { PluginType } from '@kbn/core-base-common'; import type { RequestHandlerContext } from '@kbn/core-http-request-handler-context-server'; import { coreInternalLifecycleMock } from '@kbn/core-lifecycle-server-mocks'; -import { CoreAppsService } from './core_app'; import { of } from 'rxjs'; +import { CoreAppsService } from './core_app'; const emptyPlugins = (): UiPlugins => ({ internal: new Map(), @@ -146,7 +146,7 @@ describe('CoreApp', () => { uiPlugins: prebootUIPlugins, router: expect.any(Object), packageInfo: coreContext.env.packageInfo, - serverBasePath: internalCorePreboot.http.basePath.serverBasePath, + staticAssets: expect.any(Object), }); }); @@ -245,7 +245,23 @@ describe('CoreApp', () => { uiPlugins, router: expect.any(Object), packageInfo: coreContext.env.packageInfo, - serverBasePath: internalCoreSetup.http.basePath.serverBasePath, + staticAssets: expect.any(Object), }); }); + + it('registers SHA-scoped and non-SHA-scoped UI bundle routes', async () => { + const uiPlugins = emptyPlugins(); + internalCoreSetup.http.staticAssets.prependServerPath.mockReturnValue('/some-path'); + await coreApp.setup(internalCoreSetup, uiPlugins); + + expect(internalCoreSetup.http.registerStaticDir).toHaveBeenCalledTimes(2); + expect(internalCoreSetup.http.registerStaticDir).toHaveBeenCalledWith( + '/some-path', + expect.any(String) + ); + expect(internalCoreSetup.http.registerStaticDir).toHaveBeenCalledWith( + '/ui/{path*}', + expect.any(String) + ); + }); }); diff --git a/packages/core/apps/core-apps-server-internal/src/core_app.ts b/packages/core/apps/core-apps-server-internal/src/core_app.ts index 3de295874d3fec4..1e54d7d8aaa26b6 100644 --- a/packages/core/apps/core-apps-server-internal/src/core_app.ts +++ b/packages/core/apps/core-apps-server-internal/src/core_app.ts @@ -22,6 +22,7 @@ import type { import type { UiPlugins } from '@kbn/core-plugins-base-server-internal'; import type { HttpResources, HttpResourcesServiceToolkit } from '@kbn/core-http-resources-server'; import type { InternalCorePreboot, InternalCoreSetup } from '@kbn/core-lifecycle-server-internal'; +import type { InternalStaticAssets } from '@kbn/core-http-server-internal'; import { firstValueFrom, map, type Observable } from 'rxjs'; import { CoreAppConfig, type CoreAppConfigType, CoreAppPath } from './core_app_config'; import { registerBundleRoutes } from './bundle_routes'; @@ -33,6 +34,7 @@ interface CommonRoutesParams { httpResources: HttpResources; basePath: IBasePath; uiPlugins: UiPlugins; + staticAssets: InternalStaticAssets; onResourceNotFound: ( req: KibanaRequest, res: HttpResourcesServiceToolkit & KibanaResponseFactory @@ -77,10 +79,11 @@ export class CoreAppsService { this.registerCommonDefaultRoutes({ basePath: corePreboot.http.basePath, httpResources: corePreboot.httpResources.createRegistrar(router), + staticAssets: corePreboot.http.staticAssets, router, uiPlugins, onResourceNotFound: async (req, res) => - // THe API consumers might call various Kibana APIs (e.g. `/api/status`) when Kibana is still at the preboot + // The API consumers might call various Kibana APIs (e.g. `/api/status`) when Kibana is still at the preboot // stage, and the main HTTP server that registers API handlers isn't up yet. At this stage we don't know if // the API endpoint exists or not, and hence cannot reply with `404`. We also should not reply with completely // unexpected response (`200 text/html` for the Core app). The only suitable option is to reply with `503` @@ -125,6 +128,7 @@ export class CoreAppsService { this.registerCommonDefaultRoutes({ basePath: coreSetup.http.basePath, httpResources: resources, + staticAssets: coreSetup.http.staticAssets, router, uiPlugins, onResourceNotFound: async (req, res) => res.notFound(), @@ -210,6 +214,7 @@ export class CoreAppsService { private registerCommonDefaultRoutes({ router, basePath, + staticAssets, uiPlugins, onResourceNotFound, httpResources, @@ -259,17 +264,23 @@ export class CoreAppsService { registerBundleRoutes({ router, uiPlugins, + staticAssets, packageInfo: this.env.packageInfo, - serverBasePath: basePath.serverBasePath, }); } // After the package is built and bootstrap extracts files to bazel-bin, // assets are exposed at the root of the package and in the package's node_modules dir private registerStaticDirs(core: InternalCoreSetup | InternalCorePreboot) { - core.http.registerStaticDir( - '/ui/{path*}', - fromRoot('node_modules/@kbn/core-apps-server-internal/assets') - ); + /** + * Serve UI from sha-scoped and not-sha-scoped paths to allow time for plugin code to migrate + * Eventually we only want to serve from the sha scoped path + */ + [core.http.staticAssets.prependServerPath('/ui/{path*}'), '/ui/{path*}'].forEach((path) => { + core.http.registerStaticDir( + path, + fromRoot('node_modules/@kbn/core-apps-server-internal/assets') + ); + }); } } diff --git a/packages/core/apps/core-apps-server-internal/tsconfig.json b/packages/core/apps/core-apps-server-internal/tsconfig.json index 36ecc68c7cbc11f..fc8aa9f25349cec 100644 --- a/packages/core/apps/core-apps-server-internal/tsconfig.json +++ b/packages/core/apps/core-apps-server-internal/tsconfig.json @@ -32,6 +32,7 @@ "@kbn/core-lifecycle-server-mocks", "@kbn/core-ui-settings-server", "@kbn/monaco", + "@kbn/core-http-server-internal", ], "exclude": [ "target/**/*", diff --git a/packages/core/base/core-base-browser-mocks/src/core_context.mock.ts b/packages/core/base/core-base-browser-mocks/src/core_context.mock.ts index 53933d4146df393..cdbafc09c5c03e0 100644 --- a/packages/core/base/core-base-browser-mocks/src/core_context.mock.ts +++ b/packages/core/base/core-base-browser-mocks/src/core_context.mock.ts @@ -24,6 +24,7 @@ function createCoreContext({ production = false }: { production?: boolean } = {} branch: 'branch', buildNum: 100, buildSha: 'buildSha', + buildShaShort: 'buildShaShort', dist: false, buildDate: new Date('2023-05-15T23:12:09.000Z'), buildFlavor: 'traditional', diff --git a/packages/core/http/core-http-server-internal/index.ts b/packages/core/http/core-http-server-internal/index.ts index 2867b5d2a03177e..b9d4edd8a862842 100644 --- a/packages/core/http/core-http-server-internal/index.ts +++ b/packages/core/http/core-http-server-internal/index.ts @@ -17,6 +17,7 @@ export type { InternalHttpServiceStart, } from './src/types'; export { BasePath } from './src/base_path_service'; +export { type InternalStaticAssets, StaticAssets } from './src/static_assets'; export { cspConfig, CspConfig, type CspConfigType } from './src/csp'; diff --git a/packages/core/http/core-http-server-internal/src/cdn.test.ts b/packages/core/http/core-http-server-internal/src/cdn_config.test.ts similarity index 98% rename from packages/core/http/core-http-server-internal/src/cdn.test.ts rename to packages/core/http/core-http-server-internal/src/cdn_config.test.ts index 74f165bfd0f2273..b6a954782f523b5 100644 --- a/packages/core/http/core-http-server-internal/src/cdn.test.ts +++ b/packages/core/http/core-http-server-internal/src/cdn_config.test.ts @@ -6,7 +6,7 @@ * Side Public License, v 1. */ -import { CdnConfig } from './cdn'; +import { CdnConfig } from './cdn_config'; describe('CdnConfig', () => { it.each([ diff --git a/packages/core/http/core-http-server-internal/src/cdn.ts b/packages/core/http/core-http-server-internal/src/cdn_config.ts similarity index 90% rename from packages/core/http/core-http-server-internal/src/cdn.ts rename to packages/core/http/core-http-server-internal/src/cdn_config.ts index 0f9b386b09237fd..e6fa29200ac74e0 100644 --- a/packages/core/http/core-http-server-internal/src/cdn.ts +++ b/packages/core/http/core-http-server-internal/src/cdn_config.ts @@ -14,15 +14,15 @@ export interface Input { } export class CdnConfig { - private url: undefined | URL; + private readonly url: undefined | URL; constructor(url?: string) { if (url) { - this.url = new URL(url); // This will throw for invalid URLs + this.url = new URL(url); // This will throw for invalid URLs, although should be validated before reaching this point } } public get host(): undefined | string { - return this.url?.host ?? undefined; + return this.url?.host; } public get baseHref(): undefined | string { diff --git a/packages/core/http/core-http-server-internal/src/http_config.test.ts b/packages/core/http/core-http-server-internal/src/http_config.test.ts index 28abe6513ced694..f0bd5773b40b903 100644 --- a/packages/core/http/core-http-server-internal/src/http_config.test.ts +++ b/packages/core/http/core-http-server-internal/src/http_config.test.ts @@ -16,8 +16,8 @@ const invalidHostnames = ['asdf$%^', '0']; let mockHostname = 'kibana-hostname'; -jest.mock('os', () => { - const original = jest.requireActual('os'); +jest.mock('node:os', () => { + const original = jest.requireActual('node:os'); return { ...original, @@ -530,6 +530,29 @@ describe('restrictInternalApis', () => { }); }); +describe('cdn', () => { + it('allows correct URL', () => { + expect(config.schema.validate({ cdn: { url: 'https://cdn.example.com' } })).toMatchObject({ + cdn: { url: 'https://cdn.example.com' }, + }); + }); + it.each([['foo'], ['http:./']])('throws for invalid URL %s', (url) => { + expect(() => config.schema.validate({ cdn: { url } })).toThrowErrorMatchingInlineSnapshot( + `"[cdn.url]: expected URI with scheme [http|https]."` + ); + }); + it.each([ + ['https://cdn.example.com:1234/asd?thing=1', 'URL query string not allowed'], + ['https://cdn.example.com:1234/asd#cool', 'URL fragment not allowed'], + [ + 'https://cdn.example.com:1234/asd?thing=1#cool', + 'URL fragment not allowed, but found "#cool"\nURL query string not allowed, but found "?thing=1"', + ], + ])('throws for disallowed values %s', (url, expecterError) => { + expect(() => config.schema.validate({ cdn: { url } })).toThrow(expecterError); + }); +}); + describe('HttpConfig', () => { it('converts customResponseHeaders to strings or arrays of strings', () => { const httpSchema = config.schema; diff --git a/packages/core/http/core-http-server-internal/src/http_config.ts b/packages/core/http/core-http-server-internal/src/http_config.ts index f6880c38e49baa6..54b8e808f675b5e 100644 --- a/packages/core/http/core-http-server-internal/src/http_config.ts +++ b/packages/core/http/core-http-server-internal/src/http_config.ts @@ -12,8 +12,8 @@ import type { ServiceConfigDescriptor } from '@kbn/core-base-server-internal'; import { uuidRegexp } from '@kbn/core-base-server-internal'; import type { ICspConfig, IExternalUrlConfig } from '@kbn/core-http-server'; -import { hostname } from 'os'; -import url from 'url'; +import { hostname, EOL } from 'node:os'; +import url, { URL } from 'node:url'; import type { Duration } from 'moment'; import type { IHttpEluMonitorConfig } from '@kbn/core-http-server/src/elu_monitor'; @@ -24,7 +24,7 @@ import { securityResponseHeadersSchema, parseRawSecurityResponseHeadersConfig, } from './security_response_headers_config'; -import { CdnConfig } from './cdn'; +import { CdnConfig } from './cdn_config'; const validBasePathRegex = /^\/.*[^\/]$/; @@ -40,6 +40,24 @@ const validHostName = () => { return hostname().replace(/[^\x00-\x7F]/g, ''); }; +/** + * We assume the URL does not contain anything after the pathname so that + * we can safely append values to the pathname at runtime. + */ +function validateCdnURL(urlString: string): undefined | string { + const cdnURL = new URL(urlString); + const errors: string[] = []; + if (cdnURL.hash.length) { + errors.push(`URL fragment not allowed, but found "${cdnURL.hash}"`); + } + if (cdnURL.search.length) { + errors.push(`URL query string not allowed, but found "${cdnURL.search}"`); + } + if (errors.length) { + return `CDN URL "${cdnURL.href}" is invalid:${EOL}${errors.join(EOL)}`; + } +} + const configSchema = schema.object( { name: schema.string({ defaultValue: () => validHostName() }), @@ -60,7 +78,7 @@ const configSchema = schema.object( }, }), cdn: schema.object({ - url: schema.maybe(schema.uri({ scheme: ['http', 'https'] })), + url: schema.maybe(schema.uri({ scheme: ['http', 'https'], validate: validateCdnURL })), }), cors: schema.object( { diff --git a/packages/core/http/core-http-server-internal/src/http_server.test.ts b/packages/core/http/core-http-server-internal/src/http_server.test.ts index 8f5ae22612b45d0..129efea82cc8c57 100644 --- a/packages/core/http/core-http-server-internal/src/http_server.test.ts +++ b/packages/core/http/core-http-server-internal/src/http_server.test.ts @@ -30,6 +30,7 @@ import { Readable } from 'stream'; import { KBN_CERT_PATH, KBN_KEY_PATH } from '@kbn/dev-utils'; import moment from 'moment'; import { of, Observable, BehaviorSubject } from 'rxjs'; +import { mockCoreContext } from '@kbn/core-base-server-mocks'; const routerOptions: RouterOptions = { isDev: false, @@ -54,8 +55,9 @@ let config$: Observable; let configWithSSL: HttpConfig; let configWithSSL$: Observable; -const loggingService = loggingSystemMock.create(); -const logger = loggingService.get(); +const coreContext = mockCoreContext.create(); +const loggingService = coreContext.logger; +const logger = coreContext.logger.get(); const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); let certificate: string; @@ -99,7 +101,7 @@ beforeEach(() => { } as HttpConfig; configWithSSL$ = of(configWithSSL); - server = new HttpServer(loggingService, 'tests', of(config.shutdownTimeout)); + server = new HttpServer(coreContext, 'tests', of(config.shutdownTimeout)); }); afterEach(async () => { diff --git a/packages/core/http/core-http-server-internal/src/http_server.ts b/packages/core/http/core-http-server-internal/src/http_server.ts index 1361c64bb67ce20..ae9025d5cd9a740 100644 --- a/packages/core/http/core-http-server-internal/src/http_server.ts +++ b/packages/core/http/core-http-server-internal/src/http_server.ts @@ -48,6 +48,8 @@ import { performance } from 'perf_hooks'; import { isBoom } from '@hapi/boom'; import { identity } from 'lodash'; import { IHttpEluMonitorConfig } from '@kbn/core-http-server/src/elu_monitor'; +import { Env } from '@kbn/config'; +import { CoreContext } from '@kbn/core-base-server-internal'; import { HttpConfig } from './http_config'; import { adoptToHapiAuthFormat } from './lifecycle/auth'; import { adoptToHapiOnPreAuth } from './lifecycle/on_pre_auth'; @@ -178,15 +180,20 @@ export class HttpServer { private stopped = false; private readonly log: Logger; + private readonly logger: LoggerFactory; private readonly authState: AuthStateStorage; private readonly authRequestHeaders: AuthHeadersStorage; private readonly authResponseHeaders: AuthHeadersStorage; + private readonly env: Env; constructor( - private readonly logger: LoggerFactory, + private readonly coreContext: CoreContext, private readonly name: string, private readonly shutdownTimeout$: Observable ) { + const { logger, env } = this.coreContext; + this.logger = logger; + this.env = env; this.authState = new AuthStateStorage(() => this.authRegistered); this.authRequestHeaders = new AuthHeadersStorage(); this.authResponseHeaders = new AuthHeadersStorage(); @@ -269,7 +276,11 @@ export class HttpServer { this.setupResponseLogging(); this.setupGracefulShutdownHandlers(); - const staticAssets = new StaticAssets(basePathService, config.cdn); + const staticAssets = new StaticAssets({ + basePath: basePathService, + cdnConfig: config.cdn, + shaDigest: this.env.packageInfo.buildShaShort, + }); return { registerRouter: this.registerRouter.bind(this), diff --git a/packages/core/http/core-http-server-internal/src/http_service.ts b/packages/core/http/core-http-server-internal/src/http_service.ts index 1ce15eb06b231e0..3dcab5be510e92e 100644 --- a/packages/core/http/core-http-server-internal/src/http_service.ts +++ b/packages/core/http/core-http-server-internal/src/http_service.ts @@ -76,8 +76,8 @@ export class HttpService configService.atPath(externalUrlConfig.path), ]).pipe(map(([http, csp, externalUrl]) => new HttpConfig(http, csp, externalUrl))); const shutdownTimeout$ = this.config$.pipe(map(({ shutdownTimeout }) => shutdownTimeout)); - this.prebootServer = new HttpServer(logger, 'Preboot', shutdownTimeout$); - this.httpServer = new HttpServer(logger, 'Kibana', shutdownTimeout$); + this.prebootServer = new HttpServer(coreContext, 'Preboot', shutdownTimeout$); + this.httpServer = new HttpServer(coreContext, 'Kibana', shutdownTimeout$); this.httpsRedirectServer = new HttpsRedirectServer(logger.get('http', 'redirect', 'server')); } diff --git a/packages/core/http/core-http-server-internal/src/static_assets.test.ts b/packages/core/http/core-http-server-internal/src/static_assets.test.ts deleted file mode 100644 index 0b1acd4e73fd9a3..000000000000000 --- a/packages/core/http/core-http-server-internal/src/static_assets.test.ts +++ /dev/null @@ -1,61 +0,0 @@ -/* - * 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 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 or the Server - * Side Public License, v 1. - */ - -import { StaticAssets } from './static_assets'; -import { BasePath } from './base_path_service'; -import { CdnConfig } from './cdn'; - -describe('StaticAssets', () => { - let basePath: BasePath; - let cdnConfig: CdnConfig; - let staticAssets: StaticAssets; - - beforeEach(() => { - basePath = new BasePath('/base-path'); - }); - - describe('#getHrefBase()', () => { - it('provides fallback to server base path', () => { - cdnConfig = CdnConfig.from(); - staticAssets = new StaticAssets(basePath, cdnConfig); - expect(staticAssets.getHrefBase()).toEqual('/base-path'); - }); - - it('provides the correct HREF given a CDN is configured', () => { - cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' }); - staticAssets = new StaticAssets(basePath, cdnConfig); - expect(staticAssets.getHrefBase()).toEqual('https://cdn.example.com/test'); - }); - }); - - describe('#getPluginAssetHref()', () => { - it('returns the expected value when CDN config is not set', () => { - cdnConfig = CdnConfig.from(); - staticAssets = new StaticAssets(basePath, cdnConfig); - expect(staticAssets.getPluginAssetHref('foo', 'path/to/img.gif')).toEqual( - '/base-path/plugins/foo/assets/path/to/img.gif' - ); - }); - - it('returns the expected value when CDN config is set', () => { - cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' }); - staticAssets = new StaticAssets(basePath, cdnConfig); - expect(staticAssets.getPluginAssetHref('bar', 'path/to/img.gif')).toEqual( - 'https://cdn.example.com/test/plugins/bar/assets/path/to/img.gif' - ); - }); - - it('removes leading slash from the', () => { - cdnConfig = CdnConfig.from(); - staticAssets = new StaticAssets(basePath, cdnConfig); - expect(staticAssets.getPluginAssetHref('dolly', '/path/for/something.svg')).toEqual( - '/base-path/plugins/dolly/assets/path/for/something.svg' - ); - }); - }); -}); diff --git a/packages/core/http/core-http-server-internal/src/static_assets.ts b/packages/core/http/core-http-server-internal/src/static_assets.ts deleted file mode 100644 index 4dfe46d8d31a6cf..000000000000000 --- a/packages/core/http/core-http-server-internal/src/static_assets.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 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 or the Server - * Side Public License, v 1. - */ - -import type { BasePath } from './base_path_service'; -import { CdnConfig } from './cdn'; - -export interface InternalStaticAssets { - getHrefBase(): string; - getPluginAssetHref(pluginName: string, assetPath: string): string; -} - -export class StaticAssets implements InternalStaticAssets { - private readonly assetsHrefBase: string; - - constructor(basePath: BasePath, cdnConfig: CdnConfig) { - const hrefToUse = cdnConfig.baseHref ?? basePath.serverBasePath; - this.assetsHrefBase = hrefToUse.endsWith('/') ? hrefToUse.slice(0, -1) : hrefToUse; - } - - /** - * Returns a href (hypertext reference) intended to be used as the base for constructing - * other hrefs to static assets. - */ - getHrefBase(): string { - return this.assetsHrefBase; - } - - getPluginAssetHref(pluginName: string, assetPath: string): string { - if (assetPath.startsWith('/')) { - assetPath = assetPath.slice(1); - } - return `${this.assetsHrefBase}/plugins/${pluginName}/assets/${assetPath}`; - } -} diff --git a/packages/core/http/core-http-server-internal/src/static_assets/index.ts b/packages/core/http/core-http-server-internal/src/static_assets/index.ts new file mode 100644 index 000000000000000..1f4dc880583c322 --- /dev/null +++ b/packages/core/http/core-http-server-internal/src/static_assets/index.ts @@ -0,0 +1,9 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +export { type InternalStaticAssets, StaticAssets } from './static_assets'; diff --git a/packages/core/http/core-http-server-internal/src/static_assets/static_assets.test.ts b/packages/core/http/core-http-server-internal/src/static_assets/static_assets.test.ts new file mode 100644 index 000000000000000..438a87765d85ee3 --- /dev/null +++ b/packages/core/http/core-http-server-internal/src/static_assets/static_assets.test.ts @@ -0,0 +1,132 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { StaticAssets, type StaticAssetsParams } from './static_assets'; +import { BasePath } from '../base_path_service'; +import { CdnConfig } from '../cdn_config'; + +describe('StaticAssets', () => { + let basePath: BasePath; + let cdnConfig: CdnConfig; + let staticAssets: StaticAssets; + let args: StaticAssetsParams; + + beforeEach(() => { + basePath = new BasePath('/base-path'); + cdnConfig = CdnConfig.from(); + args = { basePath, cdnConfig, shaDigest: '' }; + }); + + describe('#getHrefBase()', () => { + it('provides fallback to server base path', () => { + staticAssets = new StaticAssets(args); + expect(staticAssets.getHrefBase()).toEqual('/base-path'); + }); + + it('provides the correct HREF given a CDN is configured', () => { + args.cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' }); + staticAssets = new StaticAssets(args); + expect(staticAssets.getHrefBase()).toEqual('https://cdn.example.com/test'); + }); + }); + + describe('#getPluginAssetHref()', () => { + it('returns the expected value when CDN is not configured', () => { + staticAssets = new StaticAssets(args); + expect(staticAssets.getPluginAssetHref('foo', 'path/to/img.gif')).toEqual( + '/base-path/plugins/foo/assets/path/to/img.gif' + ); + }); + + it('returns the expected value when CDN is configured', () => { + args.cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' }); + staticAssets = new StaticAssets(args); + expect(staticAssets.getPluginAssetHref('bar', 'path/to/img.gif')).toEqual( + 'https://cdn.example.com/test/plugins/bar/assets/path/to/img.gif' + ); + }); + + it('removes leading and trailing slash from the assetPath', () => { + staticAssets = new StaticAssets(args); + expect(staticAssets.getPluginAssetHref('dolly', '/path/for/something.svg/')).toEqual( + '/base-path/plugins/dolly/assets/path/for/something.svg' + ); + }); + it('removes leading and trailing slash from the assetPath when CDN is configured', () => { + args.cdnConfig = CdnConfig.from({ url: 'https://cdn.example.com/test' }); + staticAssets = new StaticAssets(args); + expect(staticAssets.getPluginAssetHref('dolly', '/path/for/something.svg/')).toEqual( + 'https://cdn.example.com/test/plugins/dolly/assets/path/for/something.svg' + ); + }); + }); + + describe('with a SHA digest provided', () => { + describe('cdn', () => { + it.each([ + ['https://cdn.example.com', 'https://cdn.example.com/beef', undefined], + ['https://cdn.example.com:1234', 'https://cdn.example.com:1234/beef', undefined], + [ + 'https://cdn.example.com:1234/roast', + 'https://cdn.example.com:1234/roast/beef', + undefined, + ], + // put slashes around shaDigest + [ + 'https://cdn.example.com:1234/roast-slash', + 'https://cdn.example.com:1234/roast-slash/beef', + '/beef/', + ], + ])('suffixes the digest to the CDNs path value (%s)', (url, expectedHref, shaDigest) => { + args.shaDigest = shaDigest ?? 'beef'; + args.cdnConfig = CdnConfig.from({ url }); + staticAssets = new StaticAssets(args); + expect(staticAssets.getHrefBase()).toEqual(expectedHref); + }); + }); + + describe('base path', () => { + it.each([ + ['', '/beef', undefined], + ['/', '/beef', undefined], + ['/roast', '/roast/beef', undefined], + ['/roast/', '/roast/beef', '/beef/'], // cheeky test adding a slashes to digest + ])('suffixes the digest to the server base path "%s")', (url, expectedPath, shaDigest) => { + basePath = new BasePath(url); + args.basePath = basePath; + args.shaDigest = shaDigest ?? 'beef'; + staticAssets = new StaticAssets(args); + expect(staticAssets.getHrefBase()).toEqual(expectedPath); + }); + }); + }); + + describe('#getPluginServerPath()', () => { + it('provides the path plugin assets can use for server routes', () => { + args.shaDigest = '1234'; + staticAssets = new StaticAssets(args); + expect(staticAssets.getPluginServerPath('myPlugin', '/fun/times')).toEqual( + '/1234/plugins/myPlugin/assets/fun/times' + ); + }); + }); + describe('#prependPublicUrl()', () => { + it('with a CDN it appends as expected', () => { + args.cdnConfig = CdnConfig.from({ url: 'http://cdn.example.com/cool?123=true' }); + staticAssets = new StaticAssets(args); + expect(staticAssets.prependPublicUrl('beans')).toEqual( + 'http://cdn.example.com/cool/beans?123=true' + ); + }); + + it('without a CDN it appends as expected', () => { + staticAssets = new StaticAssets(args); + expect(staticAssets.prependPublicUrl('/cool/beans')).toEqual('/base-path/cool/beans'); + }); + }); +}); diff --git a/packages/core/http/core-http-server-internal/src/static_assets/static_assets.ts b/packages/core/http/core-http-server-internal/src/static_assets/static_assets.ts new file mode 100644 index 000000000000000..f5f7d7ac8043044 --- /dev/null +++ b/packages/core/http/core-http-server-internal/src/static_assets/static_assets.ts @@ -0,0 +1,103 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import type { BasePath } from '../base_path_service'; +import { CdnConfig } from '../cdn_config'; +import { + suffixPathnameToPathname, + suffixPathnameToURLPathname, + removeSurroundingSlashes, +} from './util'; + +export interface InternalStaticAssets { + getHrefBase(): string; + /** + * Intended for use by server code rendering UI or generating links to static assets + * that will ultimately be called from the browser and must respect settings like + * serverBasePath + */ + getPluginAssetHref(pluginName: string, assetPath: string): string; + /** + * Intended for use by server code wanting to register static assets against Kibana + * as server paths + */ + getPluginServerPath(pluginName: string, assetPath: string): string; + /** + * Similar to getPluginServerPath, but not plugin-scoped + */ + prependServerPath(pathname: string): string; + + /** + * Will append the given path segment to the configured public path. + * + * @note This could return a path or full URL depending on whether a CDN is configured. + */ + prependPublicUrl(pathname: string): string; +} + +/** @internal */ +export interface StaticAssetsParams { + basePath: BasePath; + cdnConfig: CdnConfig; + shaDigest: string; +} + +/** + * Convention is for trailing slashes in pathnames are stripped. + */ +export class StaticAssets implements InternalStaticAssets { + private readonly assetsHrefBase: string; + private readonly assetsServerPathBase: string; + private readonly hasCdnHost: boolean; + + constructor({ basePath, cdnConfig, shaDigest }: StaticAssetsParams) { + const cdnBaseHref = cdnConfig.baseHref; + if (cdnBaseHref) { + this.hasCdnHost = true; + this.assetsHrefBase = suffixPathnameToURLPathname(cdnBaseHref, shaDigest); + } else { + this.hasCdnHost = false; + this.assetsHrefBase = suffixPathnameToPathname(basePath.serverBasePath, shaDigest); + } + this.assetsServerPathBase = `/${shaDigest}`; + } + + /** + * Returns a href (hypertext reference) intended to be used as the base for constructing + * other hrefs to static assets. + */ + public getHrefBase(): string { + return this.assetsHrefBase; + } + + public getPluginAssetHref(pluginName: string, assetPath: string): string { + if (assetPath.startsWith('/')) { + assetPath = assetPath.slice(1); + } + return `${this.assetsHrefBase}/plugins/${pluginName}/assets/${removeSurroundingSlashes( + assetPath + )}`; + } + + public prependServerPath(path: string): string { + return `${this.assetsServerPathBase}/${removeSurroundingSlashes(path)}`; + } + + public prependPublicUrl(pathname: string): string { + if (this.hasCdnHost) { + return suffixPathnameToURLPathname(this.assetsHrefBase, pathname); + } + return suffixPathnameToPathname(this.assetsHrefBase, pathname); + } + + public getPluginServerPath(pluginName: string, assetPath: string): string { + return `${this.assetsServerPathBase}/plugins/${pluginName}/assets/${removeSurroundingSlashes( + assetPath + )}`; + } +} diff --git a/packages/core/http/core-http-server-internal/src/static_assets/util.ts b/packages/core/http/core-http-server-internal/src/static_assets/util.ts new file mode 100644 index 000000000000000..c86f56fc239a20d --- /dev/null +++ b/packages/core/http/core-http-server-internal/src/static_assets/util.ts @@ -0,0 +1,45 @@ +/* + * 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 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 or the Server + * Side Public License, v 1. + */ + +import { URL, format } from 'node:url'; + +function isEmptyPathname(pathname: string): boolean { + return !pathname || pathname === '/'; +} + +function removeTailSlashes(pathname: string): string { + return pathname.replace(/\/+$/, ''); +} + +function removeLeadSlashes(pathname: string): string { + return pathname.replace(/^\/+/, ''); +} + +export function removeSurroundingSlashes(pathname: string): string { + return removeLeadSlashes(removeTailSlashes(pathname)); +} + +export function suffixPathnameToURLPathname(urlString: string, pathname: string): string { + const url = new URL(urlString); + url.pathname = suffixPathnameToPathname(url.pathname, pathname); + return format(url); +} + +/** + * Appends a value to pathname. Pathname is assumed to come from URL.pathname + * Also do some quality control on the path to ensure that it matches URL.pathname. + */ +export function suffixPathnameToPathname(pathnameA: string, pathnameB: string): string { + if (isEmptyPathname(pathnameA)) { + return `/${removeSurroundingSlashes(pathnameB)}`; + } + if (isEmptyPathname(pathnameB)) { + return `/${removeSurroundingSlashes(pathnameA)}`; + } + return `/${removeSurroundingSlashes(pathnameA)}/${removeSurroundingSlashes(pathnameB)}`; +} diff --git a/packages/core/http/core-http-server-internal/tsconfig.json b/packages/core/http/core-http-server-internal/tsconfig.json index e163741c21c7ee2..7c52ff584a5325d 100644 --- a/packages/core/http/core-http-server-internal/tsconfig.json +++ b/packages/core/http/core-http-server-internal/tsconfig.json @@ -33,6 +33,7 @@ "@kbn/core-execution-context-server-mocks", "@kbn/core-http-context-server-mocks", "@kbn/logging-mocks", + "@kbn/core-base-server-mocks", ], "exclude": [ "target/**/*", diff --git a/packages/core/http/core-http-server-mocks/src/http_service.mock.ts b/packages/core/http/core-http-server-mocks/src/http_service.mock.ts index f8ecfadfeb87e58..7172accf98a9f92 100644 --- a/packages/core/http/core-http-server-mocks/src/http_service.mock.ts +++ b/packages/core/http/core-http-server-mocks/src/http_service.mock.ts @@ -87,8 +87,11 @@ const createInternalStaticAssetsMock = ( basePath: BasePathMocked, cdnUrl: undefined | string = undefined ): InternalStaticAssetsMocked => ({ - getHrefBase: jest.fn(() => cdnUrl ?? basePath.serverBasePath), + getHrefBase: jest.fn().mockReturnValue(cdnUrl ?? basePath.serverBasePath), getPluginAssetHref: jest.fn().mockReturnValue(cdnUrl ?? basePath.serverBasePath), + getPluginServerPath: jest.fn((v, _) => v), + prependServerPath: jest.fn((v) => v), + prependPublicUrl: jest.fn((v) => v), }); const createAuthMock = () => { @@ -212,6 +215,7 @@ const createSetupContractMock = < getServerInfo: internalMock.getServerInfo, staticAssets: { getPluginAssetHref: jest.fn().mockImplementation((assetPath: string) => assetPath), + prependPublicUrl: jest.fn().mockImplementation((pathname: string) => pathname), }, }; @@ -227,6 +231,7 @@ const createStartContractMock = () => { getServerInfo: jest.fn(), staticAssets: { getPluginAssetHref: jest.fn().mockImplementation((assetPath: string) => assetPath), + prependPublicUrl: jest.fn().mockImplementation((pathname: string) => pathname), }, }; diff --git a/packages/core/http/core-http-server/src/static_assets.ts b/packages/core/http/core-http-server/src/static_assets.ts index c0cc8597d1540d2..a839beae8023e41 100644 --- a/packages/core/http/core-http-server/src/static_assets.ts +++ b/packages/core/http/core-http-server/src/static_assets.ts @@ -23,4 +23,23 @@ export interface IStaticAssets { * ``` */ getPluginAssetHref(assetPath: string): string; + + /** + * Will return an href, either a path for or full URL with the provided path + * appended to the static assets public base path. + * + * Useful for instances were you need to render your own HTML page and link to + * certain static assets. + * + * @example + * ```ts + * // I want to retrieve the href for Kibana's favicon, requires knowledge of path: + * const favIconHref = core.http.statisAssets.prependPublicUrl('/ui/favicons/favicon.svg'); + * ``` + * + * @note Only use this if you know what you are doing and there is no other option. + * This creates a strong coupling between asset dir structure and your code. + * @param pathname + */ + prependPublicUrl(pathname: string): string; } diff --git a/packages/core/plugins/core-plugins-browser-internal/src/test_helpers/mocks.ts b/packages/core/plugins/core-plugins-browser-internal/src/test_helpers/mocks.ts index 787a7d767819409..38b86ed553cf232 100644 --- a/packages/core/plugins/core-plugins-browser-internal/src/test_helpers/mocks.ts +++ b/packages/core/plugins/core-plugins-browser-internal/src/test_helpers/mocks.ts @@ -23,6 +23,7 @@ export const createPluginInitializerContextMock = (config: unknown = {}) => { branch: 'branch', buildNum: 100, buildSha: 'buildSha', + buildShaShort: 'buildShaShort', dist: false, buildDate: new Date('2023-05-15T23:12:09.000Z'), buildFlavor: 'traditional', diff --git a/packages/core/plugins/core-plugins-browser-mocks/src/plugins_service.mock.ts b/packages/core/plugins/core-plugins-browser-mocks/src/plugins_service.mock.ts index ff3d60c5c5706bb..6a52194d520f219 100644 --- a/packages/core/plugins/core-plugins-browser-mocks/src/plugins_service.mock.ts +++ b/packages/core/plugins/core-plugins-browser-mocks/src/plugins_service.mock.ts @@ -49,6 +49,7 @@ const createPluginInitializerContextMock = ( branch: 'branch', buildNum: 100, buildSha: 'buildSha', + buildShaShort: 'buildShaShort', dist: false, buildDate: new Date('2023-05-15T23:12:09.000Z'), buildFlavor, diff --git a/packages/core/plugins/core-plugins-server-internal/src/discovery/plugin_manifest_parser.test.ts b/packages/core/plugins/core-plugins-server-internal/src/discovery/plugin_manifest_parser.test.ts index 202cef2ca09d41b..cdcf7ac7063f390 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/discovery/plugin_manifest_parser.test.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/discovery/plugin_manifest_parser.test.ts @@ -19,6 +19,7 @@ const packageInfo: PackageInfo = { branch: 'master', buildNum: 1, buildSha: '', + buildShaShort: '', version: '7.0.0-alpha1', dist: false, buildDate: new Date('2023-05-15T23:12:09.000Z'), diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts index c3ed7f6a433b9d6..3cb8777d647d115 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugin_context.ts @@ -236,6 +236,7 @@ export function createPluginSetupContext({ registerOnPreResponse: deps.http.registerOnPreResponse, basePath: deps.http.basePath, staticAssets: { + prependPublicUrl: (pathname: string) => deps.http.staticAssets.prependPublicUrl(pathname), getPluginAssetHref: (assetPath: string) => deps.http.staticAssets.getPluginAssetHref(plugin.name, assetPath), }, @@ -329,6 +330,7 @@ export function createPluginStartContext({ basePath: deps.http.basePath, getServerInfo: deps.http.getServerInfo, staticAssets: { + prependPublicUrl: (pathname: string) => deps.http.staticAssets.prependPublicUrl(pathname), getPluginAssetHref: (assetPath: string) => deps.http.staticAssets.getPluginAssetHref(plugin.name, assetPath), }, diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugins_service.test.ts b/packages/core/plugins/core-plugins-server-internal/src/plugins_service.test.ts index 2185bfe13fb19dc..e25a1b924e4202f 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugins_service.test.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugins_service.test.ts @@ -1165,8 +1165,10 @@ describe('PluginsService', () => { }); describe('plugin initialization', () => { + let prebootPlugins: PluginWrapper[]; + let standardPlugins: PluginWrapper[]; beforeEach(() => { - const prebootPlugins = [ + prebootPlugins = [ createPlugin('plugin-1-preboot', { type: PluginType.preboot, path: 'path-1-preboot', @@ -1178,7 +1180,7 @@ describe('PluginsService', () => { version: 'version-2', }), ]; - const standardPlugins = [ + standardPlugins = [ createPlugin('plugin-1-standard', { path: 'path-1-standard', version: 'version-1', @@ -1299,6 +1301,31 @@ describe('PluginsService', () => { expect(standardMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); }); + it('#preboot registers expected static dirs', async () => { + prebootDeps.http.staticAssets.getPluginServerPath.mockImplementation( + (pluginName: string) => `/static-assets/${pluginName}` + ); + await pluginsService.discover({ environment: environmentPreboot, node: nodePreboot }); + await pluginsService.preboot(prebootDeps); + expect(prebootDeps.http.registerStaticDir).toHaveBeenCalledTimes(prebootPlugins.length * 2); + expect(prebootDeps.http.registerStaticDir).toHaveBeenCalledWith( + '/static-assets/plugin-1-preboot', + expect.any(String) + ); + expect(prebootDeps.http.registerStaticDir).toHaveBeenCalledWith( + '/plugins/plugin-1-preboot/assets/{path*}', + expect.any(String) + ); + expect(prebootDeps.http.registerStaticDir).toHaveBeenCalledWith( + '/static-assets/plugin-2-preboot', + expect.any(String) + ); + expect(prebootDeps.http.registerStaticDir).toHaveBeenCalledWith( + '/plugins/plugin-2-preboot/assets/{path*}', + expect.any(String) + ); + }); + it('#setup does initialize `standard` plugins if plugins.initialize is true', async () => { config$.next({ plugins: { initialize: true } }); await pluginsService.discover({ environment: environmentPreboot, node: nodePreboot }); @@ -1319,6 +1346,32 @@ describe('PluginsService', () => { expect(prebootMockPluginSystem.setupPlugins).not.toHaveBeenCalled(); expect(initialized).toBe(false); }); + + it('#setup registers expected static dirs', async () => { + await pluginsService.discover({ environment: environmentPreboot, node: nodePreboot }); + await pluginsService.preboot(prebootDeps); + setupDeps.http.staticAssets.getPluginServerPath.mockImplementation( + (pluginName: string) => `/static-assets/${pluginName}` + ); + await pluginsService.setup(setupDeps); + expect(setupDeps.http.registerStaticDir).toHaveBeenCalledTimes(standardPlugins.length * 2); + expect(setupDeps.http.registerStaticDir).toHaveBeenCalledWith( + '/static-assets/plugin-1-standard', + expect.any(String) + ); + expect(setupDeps.http.registerStaticDir).toHaveBeenCalledWith( + '/plugins/plugin-1-standard/assets/{path*}', + expect.any(String) + ); + expect(setupDeps.http.registerStaticDir).toHaveBeenCalledWith( + '/static-assets/plugin-2-standard', + expect.any(String) + ); + expect(setupDeps.http.registerStaticDir).toHaveBeenCalledWith( + '/plugins/plugin-2-standard/assets/{path*}', + expect.any(String) + ); + }); }); describe('#getExposedPluginConfigsToUsage', () => { diff --git a/packages/core/plugins/core-plugins-server-internal/src/plugins_service.ts b/packages/core/plugins/core-plugins-server-internal/src/plugins_service.ts index a5f7bfaef7d732e..da5d77d8be675ef 100644 --- a/packages/core/plugins/core-plugins-server-internal/src/plugins_service.ts +++ b/packages/core/plugins/core-plugins-server-internal/src/plugins_service.ts @@ -448,10 +448,16 @@ export class PluginsService uiPluginInternalInfo: Map ) { for (const [pluginName, pluginInfo] of uiPluginInternalInfo) { - deps.http.registerStaticDir( + /** + * Serve UI from sha-scoped and not-sha-scoped paths to allow time for plugin code to migrate + * Eventually we only want to serve from the sha scoped path + */ + [ + deps.http.staticAssets.getPluginServerPath(pluginName, '{path*}'), `/plugins/${pluginName}/assets/{path*}`, - pluginInfo.publicAssetsDir - ); + ].forEach((path) => { + deps.http.registerStaticDir(path, pluginInfo.publicAssetsDir); + }); } } } diff --git a/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap b/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap index 535624e4a832027..b6fedfd8644e420 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap +++ b/packages/core/rendering/core-rendering-server-internal/src/__snapshots__/rendering_service.test.ts.snap @@ -24,6 +24,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -92,6 +93,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -156,6 +158,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -224,6 +227,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -288,6 +292,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -352,6 +357,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -420,6 +426,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -484,6 +491,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -553,6 +561,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -621,6 +630,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -690,6 +700,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -763,6 +774,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -827,6 +839,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -896,6 +909,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -969,6 +983,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, @@ -1038,6 +1053,7 @@ Object { "buildFlavor": Any, "buildNum": Any, "buildSha": Any, + "buildShaShort": "XXXXXX", "dist": Any, "version": Any, }, diff --git a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts index 5c699e905e9cd9b..e959b3aff356d11 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.test.ts @@ -25,6 +25,7 @@ const createPackageInfo = (parts: Partial = {}): PackageInfo => ({ branch: 'master', buildNum: 42, buildSha: 'buildSha', + buildShaShort: 'buildShaShort', buildDate: new Date('2023-05-15T23:12:09.000Z'), dist: false, version: '8.0.0', @@ -62,7 +63,7 @@ describe('bootstrapRenderer', () => { auth, packageInfo, uiPlugins, - baseHref: '/base-path', + baseHref: `/base-path/${packageInfo.buildShaShort}`, // the base href as provided by static assets module }); }); @@ -319,7 +320,7 @@ describe('bootstrapRenderer', () => { expect(getPluginsBundlePathsMock).toHaveBeenCalledWith({ isAnonymousPage, uiPlugins, - bundlesHref: '/base-path/42/bundles', + bundlesHref: '/base-path/buildShaShort/bundles', }); }); }); @@ -338,7 +339,7 @@ describe('bootstrapRenderer', () => { expect(getJsDependencyPathsMock).toHaveBeenCalledTimes(1); expect(getJsDependencyPathsMock).toHaveBeenCalledWith( - '/base-path/42/bundles', + '/base-path/buildShaShort/bundles', pluginsBundlePaths ); }); diff --git a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts index e8c30819a0b6ebb..57cd247b4f6f50b 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/bootstrap/bootstrap_renderer.ts @@ -79,8 +79,7 @@ export const bootstrapRendererFactory: BootstrapRendererFactory = ({ themeVersion, darkMode, }); - const buildHash = packageInfo.buildNum; - const bundlesHref = getBundlesHref(baseHref, String(buildHash)); + const bundlesHref = getBundlesHref(baseHref); const bundlePaths = getPluginsBundlePaths({ uiPlugins, diff --git a/packages/core/rendering/core-rendering-server-internal/src/render_utils.test.ts b/packages/core/rendering/core-rendering-server-internal/src/render_utils.test.ts index 8a5d2e4c7377b5d..e52e18e03776b34 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/render_utils.test.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/render_utils.test.ts @@ -16,14 +16,14 @@ describe('getStylesheetPaths', () => { getStylesheetPaths({ darkMode: true, themeVersion: 'v8', - baseHref: '/base-path', + baseHref: '/base-path/buildShaShort', buildNum: 17, }) ).toMatchInlineSnapshot(` Array [ - "/base-path/17/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.dark.css", - "/base-path/17/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css", - "/base-path/ui/legacy_dark_theme.min.css", + "/base-path/buildShaShort/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.dark.css", + "/base-path/buildShaShort/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css", + "/base-path/buildShaShort/ui/legacy_dark_theme.min.css", ] `); }); @@ -36,14 +36,14 @@ describe('getStylesheetPaths', () => { getStylesheetPaths({ darkMode: false, themeVersion: 'v8', - baseHref: '/base-path', + baseHref: '/base-path/buildShaShort', buildNum: 69, }) ).toMatchInlineSnapshot(` Array [ - "/base-path/69/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css", - "/base-path/69/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css", - "/base-path/ui/legacy_light_theme.min.css", + "/base-path/buildShaShort/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.v8.light.css", + "/base-path/buildShaShort/bundles/kbn-ui-shared-deps-src/kbn-ui-shared-deps-src.css", + "/base-path/buildShaShort/ui/legacy_light_theme.min.css", ] `); }); diff --git a/packages/core/rendering/core-rendering-server-internal/src/render_utils.ts b/packages/core/rendering/core-rendering-server-internal/src/render_utils.ts index 6f74320098b16d8..51f15a2ba034d77 100644 --- a/packages/core/rendering/core-rendering-server-internal/src/render_utils.ts +++ b/packages/core/rendering/core-rendering-server-internal/src/render_utils.ts @@ -22,8 +22,7 @@ export const getSettingValue = ( return convert(value); }; -export const getBundlesHref = (baseHref: string, buildNr: string): string => - `${baseHref}/${buildNr}/bundles`; +export const getBundlesHref = (baseHref: string): string => `${baseHref}/bundles`; export const getStylesheetPaths = ({ themeVersion, @@ -36,7 +35,7 @@ export const getStylesheetPaths = ({ buildNum: number; baseHref: string; }) => { - const bundlesHref = getBundlesHref(baseHref, String(buildNum)); + const bundlesHref = getBundlesHref(baseHref); return [ ...(darkMode ? [ diff --git a/packages/kbn-config/src/__snapshots__/env.test.ts.snap b/packages/kbn-config/src/__snapshots__/env.test.ts.snap index e5d5a3816ced312..4bc87fb5f240ea5 100644 --- a/packages/kbn-config/src/__snapshots__/env.test.ts.snap +++ b/packages/kbn-config/src/__snapshots__/env.test.ts.snap @@ -32,6 +32,7 @@ Env { "buildFlavor": "traditional", "buildNum": 9007199254740991, "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "buildShaShort": "XXXXXXXXXXXX", "dist": false, "version": "v1", }, @@ -75,6 +76,7 @@ Env { "buildFlavor": "traditional", "buildNum": 9007199254740991, "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "buildShaShort": "XXXXXXXXXXXX", "dist": false, "version": "v1", }, @@ -117,6 +119,7 @@ Env { "buildFlavor": "traditional", "buildNum": 9007199254740991, "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "buildShaShort": "XXXXXXXXXXXX", "dist": false, "version": "some-version", }, @@ -159,6 +162,7 @@ Env { "buildFlavor": "traditional", "buildNum": 100, "buildSha": "feature-v1-build-sha", + "buildShaShort": "feature-v1-b", "dist": true, "version": "v1", }, @@ -201,6 +205,7 @@ Env { "buildFlavor": "traditional", "buildNum": 9007199254740991, "buildSha": "XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX", + "buildShaShort": "XXXXXXXXXXXX", "dist": false, "version": "v1", }, @@ -243,6 +248,7 @@ Env { "buildFlavor": "traditional", "buildNum": 100, "buildSha": "feature-v1-build-sha", + "buildShaShort": "feature-v1-b", "dist": true, "version": "v1", }, diff --git a/packages/kbn-config/src/env.test.ts b/packages/kbn-config/src/env.test.ts index 7c301ff83e6f4cc..45f037500b77e58 100644 --- a/packages/kbn-config/src/env.test.ts +++ b/packages/kbn-config/src/env.test.ts @@ -248,3 +248,35 @@ describe('packageInfo.buildFlavor', () => { expect(env.packageInfo.buildFlavor).toEqual('traditional'); }); }); + +describe('packageInfo.buildShaShort', () => { + const sha = 'c6e1a25bea71a623929a8f172c0273bf0c811ca0'; + it('provides the sha and a short version of the sha', () => { + mockPackage.raw = { + branch: 'some-branch', + version: 'some-version', + }; + + const env = new Env( + '/some/home/dir', + { + branch: 'whathaveyou', + version: 'v1', + build: { + distributable: true, + number: 100, + sha, + date: BUILD_DATE, + }, + }, + getEnvOptions({ + cliArgs: { dev: false }, + configs: ['/some/other/path/some-kibana.yml'], + repoPackages: ['FakePackage1', 'FakePackage2'] as unknown as Package[], + }) + ); + + expect(env.packageInfo.buildSha).toEqual('c6e1a25bea71a623929a8f172c0273bf0c811ca0'); + expect(env.packageInfo.buildShaShort).toEqual('c6e1a25bea71'); + }); +}); diff --git a/packages/kbn-config/src/env.ts b/packages/kbn-config/src/env.ts index 99728f0dfc4132d..4b2c93611615924 100644 --- a/packages/kbn-config/src/env.ts +++ b/packages/kbn-config/src/env.ts @@ -121,6 +121,7 @@ export class Env { branch: pkg.branch, buildNum: isKibanaDistributable ? pkg.build.number : Number.MAX_SAFE_INTEGER, buildSha: isKibanaDistributable ? pkg.build.sha : 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX', + buildShaShort: isKibanaDistributable ? pkg.build.sha.slice(0, 12) : 'XXXXXXXXXXXX', version: pkg.version, dist: isKibanaDistributable, buildDate: isKibanaDistributable ? new Date(pkg.build.date) : new Date(), diff --git a/packages/kbn-config/src/types.ts b/packages/kbn-config/src/types.ts index f9038a1a7fd2684..91706bb9f2cb8a2 100644 --- a/packages/kbn-config/src/types.ts +++ b/packages/kbn-config/src/types.ts @@ -14,6 +14,7 @@ export interface PackageInfo { branch: string; buildNum: number; buildSha: string; + buildShaShort: string; buildDate: Date; buildFlavor: BuildFlavor; dist: boolean; diff --git a/src/core/server/integration_tests/core_app/bundle_routes.test.ts b/src/core/server/integration_tests/core_app/bundle_routes.test.ts index af3782b01591258..b53bc07a8549271 100644 --- a/src/core/server/integration_tests/core_app/bundle_routes.test.ts +++ b/src/core/server/integration_tests/core_app/bundle_routes.test.ts @@ -17,7 +17,7 @@ import { HttpService } from '@kbn/core-http-server-internal'; import { createHttpServer } from '@kbn/core-http-server-mocks'; import { registerRouteForBundle, FileHashCache } from '@kbn/core-apps-server-internal'; -const buildNum = 1234; +const buildHash = 'buildHash'; const fooPluginFixture = resolve(__dirname, './__fixtures__/plugin/foo'); describe('bundle routes', () => { @@ -47,8 +47,8 @@ describe('bundle routes', () => { isDist, fileHashCache, bundlesPath: fooPluginFixture, - routePath: `/${buildNum}/bundles/plugin/foo/`, - publicPath: `/${buildNum}/bundles/plugin/foo/`, + routePath: `/${buildHash}/bundles/plugin/foo/`, + publicPath: `/${buildHash}/bundles/plugin/foo/`, }); }; @@ -62,7 +62,7 @@ describe('bundle routes', () => { await server.start(); const response = await supertest(innerServer.listener) - .get(`/${buildNum}/bundles/plugin/foo/image.png`) + .get(`/${buildHash}/bundles/plugin/foo/image.png`) .expect(200); const actualImage = await readFile(resolve(fooPluginFixture, 'image.png')); @@ -80,7 +80,7 @@ describe('bundle routes', () => { await server.start(); const response = await supertest(innerServer.listener) - .get(`/${buildNum}/bundles/plugin/foo/plugin.js`) + .get(`/${buildHash}/bundles/plugin/foo/plugin.js`) .expect(200); const actualFile = await readFile(resolve(fooPluginFixture, 'plugin.js')); @@ -98,7 +98,7 @@ describe('bundle routes', () => { await server.start(); await supertest(innerServer.listener) - .get(`/${buildNum}/bundles/plugin/foo/../outside_output.js`) + .get(`/${buildHash}/bundles/plugin/foo/../outside_output.js`) .expect(404); }); @@ -112,7 +112,7 @@ describe('bundle routes', () => { await server.start(); await supertest(innerServer.listener) - .get(`/${buildNum}/bundles/plugin/foo/missing.js`) + .get(`/${buildHash}/bundles/plugin/foo/missing.js`) .expect(404); }); @@ -126,7 +126,7 @@ describe('bundle routes', () => { await server.start(); const response = await supertest(innerServer.listener) - .get(`/${buildNum}/bundles/plugin/foo/gzip_chunk.js`) + .get(`/${buildHash}/bundles/plugin/foo/gzip_chunk.js`) .expect(200); expect(response.get('content-encoding')).toEqual('gzip'); @@ -151,7 +151,7 @@ describe('bundle routes', () => { await server.start(); const response = await supertest(innerServer.listener) - .get(`/${buildNum}/bundles/plugin/foo/gzip_chunk.js`) + .get(`/${buildHash}/bundles/plugin/foo/gzip_chunk.js`) .expect(200); expect(response.get('cache-control')).toEqual('max-age=31536000'); @@ -170,7 +170,7 @@ describe('bundle routes', () => { await server.start(); const response = await supertest(innerServer.listener) - .get(`/${buildNum}/bundles/plugin/foo/gzip_chunk.js`) + .get(`/${buildHash}/bundles/plugin/foo/gzip_chunk.js`) .expect(200); expect(response.get('cache-control')).toEqual('must-revalidate'); diff --git a/src/core/server/integration_tests/http/http_server.test.ts b/src/core/server/integration_tests/http/http_server.test.ts index eeb6b46c9ff46fa..f58bfba11186d4a 100644 --- a/src/core/server/integration_tests/http/http_server.test.ts +++ b/src/core/server/integration_tests/http/http_server.test.ts @@ -11,19 +11,21 @@ import supertest from 'supertest'; import moment from 'moment'; import { of } from 'rxjs'; import { ByteSizeValue } from '@kbn/config-schema'; -import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { Router } from '@kbn/core-http-router-server-internal'; import { HttpServer, HttpConfig } from '@kbn/core-http-server-internal'; +import { mockCoreContext } from '@kbn/core-base-server-mocks'; +import type { Logger } from '@kbn/logging'; describe('Http server', () => { let server: HttpServer; let config: HttpConfig; - let logger: ReturnType; + let logger: Logger; + let coreContext: ReturnType; const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); beforeEach(() => { - const loggingService = loggingSystemMock.create(); - logger = loggingSystemMock.createLogger(); + coreContext = mockCoreContext.create(); + logger = coreContext.logger.get(); config = { name: 'kibana', @@ -43,7 +45,7 @@ describe('Http server', () => { shutdownTimeout: moment.duration(5, 's'), } as any; - server = new HttpServer(loggingService, 'tests', of(config.shutdownTimeout)); + server = new HttpServer(coreContext, 'tests', of(config.shutdownTimeout)); }); describe('Graceful shutdown', () => { diff --git a/src/core/server/integration_tests/http/tls_config_reload.test.ts b/src/core/server/integration_tests/http/tls_config_reload.test.ts index 3603f76ebc67092..ad2c530faae5213 100644 --- a/src/core/server/integration_tests/http/tls_config_reload.test.ts +++ b/src/core/server/integration_tests/http/tls_config_reload.test.ts @@ -10,7 +10,6 @@ import supertest from 'supertest'; import { duration } from 'moment'; import { BehaviorSubject, of } from 'rxjs'; import { KBN_CERT_PATH, KBN_KEY_PATH, ES_KEY_PATH, ES_CERT_PATH } from '@kbn/dev-utils'; -import { loggingSystemMock } from '@kbn/core-logging-server-mocks'; import { Router } from '@kbn/core-http-router-server-internal'; import { HttpServer, @@ -20,6 +19,8 @@ import { externalUrlConfig, } from '@kbn/core-http-server-internal'; import { isServerTLS, flattenCertificateChain, fetchPeerCertificate } from './tls_utils'; +import { mockCoreContext } from '@kbn/core-base-server-mocks'; +import type { Logger } from '@kbn/logging'; const CSP_CONFIG = cspConfig.schema.validate({}); const EXTERNAL_URL_CONFIG = externalUrlConfig.schema.validate({}); @@ -27,16 +28,16 @@ const enhanceWithContext = (fn: (...args: any[]) => any) => fn.bind(null, {}); describe('HttpServer - TLS config', () => { let server: HttpServer; - let logger: ReturnType; + let logger: Logger; beforeAll(() => { process.env.NODE_TLS_REJECT_UNAUTHORIZED = '0'; }); beforeEach(() => { - const loggingService = loggingSystemMock.create(); - logger = loggingSystemMock.createLogger(); - server = new HttpServer(loggingService, 'tests', of(duration('1s'))); + const coreContext = mockCoreContext.create(); + logger = coreContext.logger.get(); + server = new HttpServer(coreContext, 'tests', of(duration('1s'))); }); it('supports dynamic reloading of the TLS configuration', async () => { diff --git a/src/core/server/integration_tests/status/routes/status.test.ts b/src/core/server/integration_tests/status/routes/status.test.ts index 0d7d6a84e24977a..755b9fc9b46f609 100644 --- a/src/core/server/integration_tests/status/routes/status.test.ts +++ b/src/core/server/integration_tests/status/routes/status.test.ts @@ -83,6 +83,7 @@ describe('GET /api/status', () => { branch: 'xbranch', buildNum: 1234, buildSha: 'xsha', + buildShaShort: 'x', dist: true, version: '9.9.9-SNAPSHOT', buildDate: new Date('2023-05-15T23:12:09.000Z'), diff --git a/src/core/server/mocks.ts b/src/core/server/mocks.ts index 62dd66f63ec62a3..b76a9c37dd6dab3 100644 --- a/src/core/server/mocks.ts +++ b/src/core/server/mocks.ts @@ -98,6 +98,7 @@ function pluginInitializerContextMock(config: T = {} as T) { branch: 'branch', buildNum: 100, buildSha: 'buildSha', + buildShaShort: 'buildShaShort', dist: false, buildDate: new Date('2023-05-15T23:12:09.000Z'), buildFlavor: 'traditional', diff --git a/src/core/tsconfig.json b/src/core/tsconfig.json index 78219114a51dfeb..06e6cf68d3a94e5 100644 --- a/src/core/tsconfig.json +++ b/src/core/tsconfig.json @@ -157,6 +157,7 @@ "@kbn/core-plugins-contracts-server", "@kbn/dev-utils", "@kbn/server-http-tools", + "@kbn/core-base-server-mocks", ], "exclude": [ "target/**/*", diff --git a/test/functional/apps/bundles/index.js b/test/functional/apps/bundles/index.js index aa175f16e5d4899..7363d9f0b5256ec 100644 --- a/test/functional/apps/bundles/index.js +++ b/test/functional/apps/bundles/index.js @@ -14,38 +14,38 @@ export default function ({ getService }) { const supertest = getService('supertest'); describe('bundle compression', function () { - let buildNum; + let buildHash; before(async () => { const resp = await supertest.get('/api/status').expect(200); - buildNum = resp.body.version.build_number; + buildHash = resp.body.version.build_hash.slice(0, 12); }); it('returns gzip files when client only supports gzip', () => supertest // We use the kbn-ui-shared-deps for these tests since they are always built with br compressed outputs, // even in dev. Bundles built by @kbn/optimizer are only built with br compression in dist mode. - .get(`/${buildNum}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`) + .get(`/${buildHash}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`) .set('Accept-Encoding', 'gzip') .expect(200) .expect('Content-Encoding', 'gzip')); it('returns br files when client only supports br', () => supertest - .get(`/${buildNum}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`) + .get(`/${buildHash}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`) .set('Accept-Encoding', 'br') .expect(200) .expect('Content-Encoding', 'br')); it('returns br files when client only supports gzip and br', () => supertest - .get(`/${buildNum}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`) + .get(`/${buildHash}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`) .set('Accept-Encoding', 'gzip, br') .expect(200) .expect('Content-Encoding', 'br')); it('returns gzip files when client prefers gzip', () => supertest - .get(`/${buildNum}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`) + .get(`/${buildHash}/bundles/kbn-ui-shared-deps-npm/kbn-ui-shared-deps-npm.dll.js`) .set('Accept-Encoding', 'gzip;q=1.0, br;q=0.5') .expect(200) .expect('Content-Encoding', 'gzip')); diff --git a/x-pack/plugins/cloud_integrations/cloud_full_story/public/plugin.ts b/x-pack/plugins/cloud_integrations/cloud_full_story/public/plugin.ts index 7636d38b681e1b4..a9af52c9a6b318c 100755 --- a/x-pack/plugins/cloud_integrations/cloud_full_story/public/plugin.ts +++ b/x-pack/plugins/cloud_integrations/cloud_full_story/public/plugin.ts @@ -77,7 +77,10 @@ export class CloudFullStoryPlugin implements Plugin { ...(pageVarsDebounceTime ? { pageVarsDebounceTimeMs: duration(pageVarsDebounceTime).asMilliseconds() } : {}), - // Load an Elastic-internally audited script. Ideally, it should be hosted on a CDN. + /** + * FIXME: this should use the {@link IStaticAssets['getPluginAssetHref']} + * function. Then we can avoid registering our own endpoint in this plugin. + */ scriptUrl: basePath.prepend( `/internal/cloud/${this.initializerContext.env.packageInfo.buildNum}/fullstory.js` ), diff --git a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/integration_tests/pdfmaker.test.ts b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/integration_tests/pdfmaker.test.ts index 2243f68b7ad71ba..9643a3bbcd4a154 100644 --- a/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/integration_tests/pdfmaker.test.ts +++ b/x-pack/plugins/screenshotting/server/formats/pdf/pdf_maker/integration_tests/pdfmaker.test.ts @@ -33,6 +33,7 @@ describe('PdfMaker', () => { branch: 'screenshot-test', buildNum: 567891011, buildSha: 'screenshot-dfdfed0a', + buildShaShort: 'scr-dfdfed0a', dist: false, version: '1000.0.0', buildDate: new Date('2023-05-15T23:12:09.000Z'), diff --git a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts index 4a177a94a147b89..3c6540f28770680 100644 --- a/x-pack/plugins/screenshotting/server/screenshots/index.test.ts +++ b/x-pack/plugins/screenshotting/server/screenshots/index.test.ts @@ -56,6 +56,7 @@ describe('Screenshot Observable Pipeline', () => { branch: 'screenshot-test', buildNum: 567891011, buildSha: 'screenshot-dfdfed0a', + buildShaShort: 'scrn-dfdfed0a', dist: false, version: '5000.0.0', buildDate: new Date('2023-05-15T23:12:09.000Z'), diff --git a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap index f24357ae373fa6d..b6bb95e80744bdc 100644 --- a/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap +++ b/x-pack/plugins/security/server/__snapshots__/prompt_page.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`PromptPage renders as expected with additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; +exports[`PromptPage renders as expected with additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; -exports[`PromptPage renders as expected without additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; +exports[`PromptPage renders as expected without additional scripts 1`] = `"ElasticMockedFonts

Some Title

Some Body
Action#1
Action#2
"`; diff --git a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap index fb377c8a0b9247a..8b78fb132c3f8fc 100644 --- a/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap +++ b/x-pack/plugins/security/server/authentication/__snapshots__/unauthenticated_page.test.tsx.snap @@ -1,5 +1,5 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`UnauthenticatedPage renders as expected 1`] = `"ElasticMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; +exports[`UnauthenticatedPage renders as expected 1`] = `"ElasticMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; -exports[`UnauthenticatedPage renders as expected with custom title 1`] = `"My Company NameMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; +exports[`UnauthenticatedPage renders as expected with custom title 1`] = `"My Company NameMockedFonts

We hit an authentication error

Try logging in again, and if the problem persists, contact your system administrator.

"`; diff --git a/x-pack/plugins/security/server/authentication/authentication_service.test.ts b/x-pack/plugins/security/server/authentication/authentication_service.test.ts index a779d30891b865e..3f985df49cef19f 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.test.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.test.ts @@ -19,6 +19,7 @@ import type { ElasticsearchServiceSetup, HttpServiceSetup, HttpServiceStart, + IStaticAssets, KibanaRequest, Logger, LoggerFactory, @@ -63,7 +64,7 @@ describe('AuthenticationService', () => { elasticsearch: jest.Mocked; config: ConfigType; license: jest.Mocked; - buildNumber: number; + staticAssets: IStaticAssets; customBranding: jest.Mocked; }; let mockStartAuthenticationParams: { @@ -96,7 +97,7 @@ describe('AuthenticationService', () => { isTLSEnabled: false, }), license: licenseMock.create(), - buildNumber: 100500, + staticAssets: coreSetupMock.http.staticAssets, customBranding: customBrandingServiceMock.createSetupContract(), }; mockCanRedirectRequest.mockReturnValue(false); @@ -983,7 +984,7 @@ describe('AuthenticationService', () => { expect(mockRenderUnauthorizedPage).toHaveBeenCalledWith({ basePath: mockSetupAuthenticationParams.http.basePath, - buildNumber: 100500, + staticAssets: expect.any(Object), originalURL: '/mock-server-basepath/app/some', }); }); @@ -1015,7 +1016,7 @@ describe('AuthenticationService', () => { expect(mockRenderUnauthorizedPage).toHaveBeenCalledWith({ basePath: mockSetupAuthenticationParams.http.basePath, - buildNumber: 100500, + staticAssets: expect.any(Object), originalURL: '/mock-server-basepath/app/some', }); }); @@ -1050,7 +1051,7 @@ describe('AuthenticationService', () => { expect(mockRenderUnauthorizedPage).toHaveBeenCalledWith({ basePath: mockSetupAuthenticationParams.http.basePath, - buildNumber: 100500, + staticAssets: expect.any(Object), originalURL: '/mock-server-basepath/', }); }); diff --git a/x-pack/plugins/security/server/authentication/authentication_service.ts b/x-pack/plugins/security/server/authentication/authentication_service.ts index d6f955b8b455805..cbe4e5f96569111 100644 --- a/x-pack/plugins/security/server/authentication/authentication_service.ts +++ b/x-pack/plugins/security/server/authentication/authentication_service.ts @@ -40,12 +40,14 @@ import type { Session } from '../session_management'; import type { UserProfileServiceStartInternal } from '../user_profile'; interface AuthenticationServiceSetupParams { - http: Pick; + http: Pick< + HttpServiceSetup, + 'basePath' | 'csp' | 'registerAuth' | 'registerOnPreResponse' | 'staticAssets' + >; customBranding: CustomBrandingSetup; elasticsearch: Pick; config: ConfigType; license: SecurityLicense; - buildNumber: number; } interface AuthenticationServiceStartParams { @@ -92,7 +94,6 @@ export class AuthenticationService { config, http, license, - buildNumber, elasticsearch, customBranding, }: AuthenticationServiceSetupParams) { @@ -204,8 +205,8 @@ export class AuthenticationService { }); return toolkit.render({ body: renderUnauthenticatedPage({ - buildNumber, basePath: http.basePath, + staticAssets: http.staticAssets, originalURL, customBranding: customBrandingValue, }), diff --git a/x-pack/plugins/security/server/authentication/unauthenticated_page.test.tsx b/x-pack/plugins/security/server/authentication/unauthenticated_page.test.tsx index d65c032911a03ff..0abd444f8036549 100644 --- a/x-pack/plugins/security/server/authentication/unauthenticated_page.test.tsx +++ b/x-pack/plugins/security/server/authentication/unauthenticated_page.test.tsx @@ -26,7 +26,7 @@ describe('UnauthenticatedPage', () => { const body = renderToStaticMarkup( @@ -44,7 +44,7 @@ describe('UnauthenticatedPage', () => { const body = renderToStaticMarkup( diff --git a/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx b/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx index fce29bbe89bc337..694f8f16bba0db3 100644 --- a/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx +++ b/x-pack/plugins/security/server/authentication/unauthenticated_page.tsx @@ -11,6 +11,7 @@ import { renderToStaticMarkup } from 'react-dom/server'; import type { IBasePath } from '@kbn/core/server'; import type { CustomBranding } from '@kbn/core-custom-branding-common'; +import type { IStaticAssets } from '@kbn/core-http-server'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -18,16 +19,21 @@ import { PromptPage } from '../prompt_page'; interface Props { originalURL: string; - buildNumber: number; basePath: IBasePath; + staticAssets: IStaticAssets; customBranding: CustomBranding; } -export function UnauthenticatedPage({ basePath, originalURL, buildNumber, customBranding }: Props) { +export function UnauthenticatedPage({ + basePath, + originalURL, + staticAssets, + customBranding, +}: Props) { return ( ElasticMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; +exports[`ResetSessionPage renders as expected 1`] = `"ElasticMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; -exports[`ResetSessionPage renders as expected with custom page title 1`] = `"My Company NameMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; +exports[`ResetSessionPage renders as expected with custom page title 1`] = `"My Company NameMockedFonts

You do not have permission to access the requested page

Either go back to the previous page or log in as a different user.

"`; diff --git a/x-pack/plugins/security/server/authorization/authorization_service.test.ts b/x-pack/plugins/security/server/authorization/authorization_service.test.ts index a052d6753248019..ddc5e26903c2b4f 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.test.ts +++ b/x-pack/plugins/security/server/authorization/authorization_service.test.ts @@ -76,7 +76,6 @@ it(`#setup returns exposed services`, () => { loggers: loggingSystemMock.create(), kibanaIndexName, packageVersion: 'some-version', - buildNumber: 42, features: mockFeaturesSetup, getSpacesService: mockGetSpacesService, getCurrentUser: jest.fn(), @@ -138,7 +137,6 @@ describe('#start', () => { loggers: loggingSystemMock.create(), kibanaIndexName, packageVersion: 'some-version', - buildNumber: 42, features: featuresPluginMock.createSetup(), getSpacesService: jest .fn() @@ -211,7 +209,6 @@ it('#stop unsubscribes from license and ES updates.', async () => { loggers: loggingSystemMock.create(), kibanaIndexName, packageVersion: 'some-version', - buildNumber: 42, features: featuresPluginMock.createSetup(), getSpacesService: jest .fn() diff --git a/x-pack/plugins/security/server/authorization/authorization_service.tsx b/x-pack/plugins/security/server/authorization/authorization_service.tsx index 16f2ed3b446e102..795016874dc9769 100644 --- a/x-pack/plugins/security/server/authorization/authorization_service.tsx +++ b/x-pack/plugins/security/server/authorization/authorization_service.tsx @@ -57,7 +57,6 @@ export { Actions } from './actions'; interface AuthorizationServiceSetupParams { packageVersion: string; - buildNumber: number; http: HttpServiceSetup; capabilities: CapabilitiesSetup; getClusterClient: () => Promise; @@ -100,7 +99,6 @@ export class AuthorizationService { http, capabilities, packageVersion, - buildNumber, getClusterClient, license, loggers, @@ -179,7 +177,7 @@ export class AuthorizationService { const next = `${http.basePath.get(request)}${request.url.pathname}${request.url.search}`; const body = renderToString( { const body = renderToStaticMarkup( @@ -44,7 +44,7 @@ describe('ResetSessionPage', () => { const body = renderToStaticMarkup( diff --git a/x-pack/plugins/security/server/authorization/reset_session_page.tsx b/x-pack/plugins/security/server/authorization/reset_session_page.tsx index 85c78ddfcbaec62..27af66a8a404844 100644 --- a/x-pack/plugins/security/server/authorization/reset_session_page.tsx +++ b/x-pack/plugins/security/server/authorization/reset_session_page.tsx @@ -10,6 +10,7 @@ import React from 'react'; import type { IBasePath } from '@kbn/core/server'; import type { CustomBranding } from '@kbn/core-custom-branding-common'; +import type { IStaticAssets } from '@kbn/core-http-server'; import { i18n } from '@kbn/i18n'; import { FormattedMessage } from '@kbn/i18n-react'; @@ -22,18 +23,18 @@ import { PromptPage } from '../prompt_page'; */ export function ResetSessionPage({ logoutUrl, - buildNumber, + staticAssets, basePath, customBranding, }: { logoutUrl: string; - buildNumber: number; + staticAssets: IStaticAssets; basePath: IBasePath; customBranding: CustomBranding; }) { return ( spaces?.spacesService, features, getCurrentUser: (request) => this.getAuthentication().getCurrentUser(request), diff --git a/x-pack/plugins/security/server/prompt_page.test.tsx b/x-pack/plugins/security/server/prompt_page.test.tsx index 268a7ac640e7125..754584284035a9d 100644 --- a/x-pack/plugins/security/server/prompt_page.test.tsx +++ b/x-pack/plugins/security/server/prompt_page.test.tsx @@ -25,7 +25,7 @@ describe('PromptPage', () => { const body = renderToStaticMarkup( Some Body} @@ -45,7 +45,7 @@ describe('PromptPage', () => { const body = renderToStaticMarkup(