Skip to content

Commit

Permalink
[Ops] Bump Node.js to version 18 (elastic#160289)
Browse files Browse the repository at this point in the history
## Summary

Bumps node.js to 18.17.0 (replacement for PR elastic#144012 which was later
reverted)

As a result, these categorical additions were needed: 
- `node` evocations will need the `--openssl-legacy-provider` flag,
wherever it would use certain crypto functionalities
- tests required updating of the expected HTTPS Agent call arguments,
`noDelay` seems to be a default
 - `window.[NAME]` fields cannot be written directly
 - some stricter typechecks

This is using our in-house built node.js 18 versions through the URLs
the proxy-cache. (built with
elastic/kibana-custom-nodejs-builds#4)

These urls are served from a bucket, where the RHEL7/Centos7 compatible
node distributables are. (see:
elastic/kibana-ci-proxy-cache#7)

Further todos: 
 - [x] check docs wording and consistency
 - [ ] update the dependency report
 - [x] explain custom builds in documentation
 - [x] node_sass prebuilts

---------

Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Tiago Costa <tiago.costa@elastic.co>
Co-authored-by: Thomas Watson <w@tson.dk>
  • Loading branch information
4 people authored and Devon Thomson committed Aug 1, 2023
1 parent dc953de commit 0006496
Show file tree
Hide file tree
Showing 38 changed files with 378 additions and 108 deletions.
2 changes: 1 addition & 1 deletion .ci/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
# NOTE: This Dockerfile is ONLY used to run certain tasks in CI. It is not used to run Kibana or as a distributable.
# If you're looking for the Kibana Docker image distributable, please see: src/dev/build/tasks/os_packages/docker_generator/templates/dockerfile.template.ts

ARG NODE_VERSION=16.20.1
ARG NODE_VERSION=18.17.0

FROM node:${NODE_VERSION} AS base

Expand Down
2 changes: 1 addition & 1 deletion .node-version
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16.20.1
18.17.0
2 changes: 1 addition & 1 deletion .nvmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
16.20.1
18.17.0
12 changes: 6 additions & 6 deletions WORKSPACE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -22,13 +22,13 @@ load("@build_bazel_rules_nodejs//:index.bzl", "node_repositories", "yarn_install
# Setup the Node.js toolchain for the architectures we want to support
node_repositories(
node_repositories = {
"16.20.1-darwin_amd64": ("node-v16.20.1-darwin-x64.tar.gz", "node-v16.20.1-darwin-x64", "d1f9c2a7c3a0fe09860f701af5fb8ff9ac72d72faa7ebabfeb5794503e79f955"),
"16.20.1-darwin_arm64": ("node-v16.20.1-darwin-arm64.tar.gz", "node-v16.20.1-darwin-arm64", "5f6b31c5a75567d382ba67220f3d7a2d9bb0c03d8af9307cd35a9cb32a6fde9d"),
"16.20.1-linux_arm64": ("node-v16.20.1-linux-arm64.tar.xz", "node-v16.20.1-linux-arm64", "7fce19f3d1c2952599a0b47f9f5d8f497265ad577f37f256a8c6a03be6353234"),
"16.20.1-linux_amd64": ("node-v16.20.1-linux-x64.tar.xz", "node-v16.20.1-linux-x64", "b6c60e1e106ad7d8881e83945a5208c1b1d1b63e6901c04b9dafa607aff3a154"),
"16.20.1-windows_amd64": ("node-v16.20.1-win-x64.zip", "node-v16.20.1-win-x64", "2a7fde996c57a969f0498742f99385a520eb14aac864e0eff9c32e3f3633ff0a"),
"18.17.0-darwin_amd64": ("node-v18.17.0-darwin-x64.tar.gz", "node-v18.17.0-darwin-x64", "2f381442381f7fbde2ca644c3275bec9c9c2a8d361f467b40e39428acdd6ccff"),
"18.17.0-darwin_arm64": ("node-v18.17.0-darwin-arm64.tar.gz", "node-v18.17.0-darwin-arm64", "19731ef427e77ad9c5f476eb62bfb02a7f179d3012feed0bbded62e45f23e679"),
"18.17.0-linux_arm64": ("node-v18.17.0-linux-arm64.tar.xz", "node-v18.17.0-linux-arm64", "fbd2904178ee47da6e0386bc9704a12b1f613da6ad194878a517d4a69ba56544"),
"18.17.0-linux_amd64": ("node-v18.17.0-linux-x64.tar.xz", "node-v18.17.0-linux-x64", "f36facda28c4d5ce76b3a1b4344e688d29d9254943a47f2f1909b1a10acb1959"),
"18.17.0-windows_amd64": ("node-v18.17.0-win-x64.zip", "node-v18.17.0-win-x64", "06e30b4e70b18d794651ef132c39080e5eaaa1187f938721d57edae2824f4e96"),
},
node_version = "16.20.1",
node_version = "18.17.0",
node_urls = [
"https://nodejs.org/dist/v{version}/{filename}",
],
Expand Down
9 changes: 8 additions & 1 deletion docs/developer/advanced/upgrading-nodejs.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ These files must be updated when upgrading Node.js:
- {kib-repo}blob/{branch}/WORKSPACE.bazel[`WORKSPACE.bazel`] - The version is specified in the `node_version` property.
Besides this property, the list of files under `node_repositories` must be updated along with their respective SHA256 hashes.
These can be found on the https://nodejs.org[nodejs.org] website.
Example for Node.js v16.20.1: https://nodejs.org/dist/v16.20.1/SHASUMS256.txt.asc
Example for Node.js v18.17.0: https://nodejs.org/dist/v18.17.0/SHASUMS256.txt.asc

See PR {kib-repo}pull/128123[#128123] for an example of how the Node.js version has been upgraded previously.

Expand All @@ -30,6 +30,13 @@ When upgrading to a new major version of Node.js, the following extra steps must
You can change which Node.js major version to view, by changing the selected branch.
If Node.js has dropped support for platform still supported by Kibana, appropriate steps must be taken as soon as possible to deprecate support for this platform. This way support for it can be dropped before the currently used major version of Node.js https://github.com/nodejs/release#release-schedule[reaches End-of-Life].

[[custom-nodejs-builds]]
=== Custom builds of Node.js

Due to Node.js 16 coming to an https://nodejs.org/en/blog/announcements/nodejs16-eol[early End-of-Life] and Node.js 18 no longer supporting the same platforms that {kib} supports (most notably CentOS7/RHEL7), we cannot bundle the official Node.js binaries with the Linux distributable of {kib}.
To keep support for these older platforms, we're bundling the Linux distributable of {kib} with a https://github.com/elastic/kibana-custom-nodejs-builds[custom build of Node.js] with extended backwards compatibility.
The only difference between the offical Node.js build and our custom build, is the version of `glibc` that it's compiled against.

=== Backporting

The following rules are not set in stone.
Expand Down
4 changes: 4 additions & 0 deletions docs/user/setup.asciidoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@ Windows. Since Kibana runs on Node.js, we include the necessary Node.js
binaries for these platforms. Running Kibana against a separately maintained
version of Node.js is not supported.

To support certain older Linux platforms (most notably CentOS7/RHEL7), {kib}
for Linux ships with a custom build of Node.js with glibc 2.17 support. For
details, see <<custom-nodejs-builds>>.

[float]
[[elasticsearch-version]]
== Elasticsearch version
Expand Down
13 changes: 6 additions & 7 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@
"serverless-security": "node scripts/kibana --dev --serverless=security",
"spec_to_console": "node scripts/spec_to_console",
"start": "node scripts/kibana --dev",
"storybook": "node scripts/storybook",
"storybook": "node --openssl-legacy-provider scripts/storybook",
"test:ftr": "node scripts/functional_tests",
"test:ftr:runner": "node scripts/functional_test_runner",
"test:ftr:server": "node scripts/functional_tests_server",
Expand All @@ -73,11 +73,11 @@
"url": "https://github.com/elastic/kibana.git"
},
"engines": {
"node": "16.20.1",
"node": "18.17.0",
"yarn": "^1.22.19"
},
"resolutions": {
"**/@types/node": "16.11.68",
"**/@types/node": "18.17.1",
"**/chokidar": "^3.5.3",
"**/globule/minimatch": "^3.1.2",
"**/hoist-non-react-statics": "^3.3.2",
Expand Down Expand Up @@ -788,7 +788,6 @@
"@turf/length": "^6.0.2",
"@xstate/react": "^3.2.2",
"JSONStream": "1.3.5",
"abort-controller": "^3.0.0",
"adm-zip": "^0.5.9",
"ajv": "^8.12.0",
"antlr4ts": "^0.5.0-alpha.3",
Expand Down Expand Up @@ -929,7 +928,7 @@
"query-string": "^6.13.2",
"rbush": "^3.0.1",
"re-resizable": "^6.9.9",
"re2": "1.17.4",
"re2": "1.17.7",
"react": "^17.0.2",
"react-ace": "^7.0.5",
"react-beautiful-dnd": "^13.1.0",
Expand Down Expand Up @@ -1298,8 +1297,8 @@
"@types/multistream": "^4.1.0",
"@types/mustache": "^0.8.31",
"@types/nock": "^10.0.3",
"@types/node": "16.11.68",
"@types/node-fetch": "^2.6.0",
"@types/node": "18.17.1",
"@types/node-fetch": "2.6.4",
"@types/node-forge": "^1.3.1",
"@types/nodemailer": "^6.4.0",
"@types/normalize-path": "^3.0.0",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,14 @@ class FakeApp implements App {
const store = new Map();
const originalLocalStorage = window.localStorage;

(window as any).localStorage = {
setItem: (key: string, value: string) => store.set(String(key), String(value)),
getItem: (key: string) => store.get(String(key)),
removeItem: (key: string) => store.delete(String(key)),
};
Object.defineProperty(window, 'localStorage', {
value: {
setItem: (key: string, value: string) => store.set(String(key), String(value)),
getItem: (key: string) => store.get(String(key)),
removeItem: (key: string) => store.delete(String(key)),
},
writable: true,
});

function defaultStartDeps(availableApps?: App[]) {
const deps = {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,10 @@ describe('RecentlyAccessed#start()', () => {
let originalLocalStorage: Storage;
beforeAll(() => {
originalLocalStorage = window.localStorage;
window.localStorage = new LocalStorageMock();
Object.defineProperty(window, 'localStorage', {
value: new LocalStorageMock(),
writable: true,
});
});
beforeEach(() => localStorage.clear());
afterAll(() => (window.localStorage = originalLocalStorage));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ import { AgentManager } from './agent_manager';
jest.mock('http');
jest.mock('https');

const HttpAgentMock = HttpAgent as jest.Mock<HttpAgent>;
const HttpsAgentMock = HttpsAgent as jest.Mock<HttpsAgent>;
const HttpAgentMock = HttpAgent as unknown as jest.Mock<HttpAgent>;
const HttpsAgentMock = HttpsAgent as unknown as jest.Mock<HttpsAgent>;

describe('AgentManager', () => {
let logger: MockedLogger;
Expand Down
1 change: 0 additions & 1 deletion packages/kbn-es/src/artifact.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import { pipeline, Transform } from 'stream';
import { setTimeout } from 'timers/promises';

import fetch, { Headers } from 'node-fetch';
import AbortController from 'abort-controller';
import chalk from 'chalk';
import { ToolingLog } from '@kbn/tooling-log';

Expand Down
2 changes: 2 additions & 0 deletions packages/kbn-monaco/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -56,9 +56,11 @@ webpack_cli(
env = select({
"//:dist": {
"NODE_ENV": "production",
"NODE_OPTIONS": "--openssl-legacy-provider",
},
"//conditions:default": {
"NODE_ENV": "development",
"NODE_OPTIONS": "--openssl-legacy-provider",
},
}),
visibility = ["//visibility:public"],
Expand Down
1 change: 1 addition & 0 deletions packages/kbn-optimizer/src/optimizer/observe_worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ function usingWorkerProc<T>(config: OptimizerConfig, fn: (proc: ChildProcess) =>
const proc = fork(require.resolve('../worker/run_worker'), [], {
execArgv: [
`--require=@kbn/babel-register/install`,
'--openssl-legacy-provider',
...(inspectFlag && config.inspectWorkers
? [`${inspectFlag}=${inspectPortCounter++}`]
: []),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ it('builds a generated plugin into a viable archive', async () => {
process.execPath,
['scripts/generate_plugin', '-y', '--name', 'fooTestPlugin'],
{
env: {
NODE_OPTIONS: '--openssl-legacy-provider',
},
cwd: REPO_ROOT,
all: true,
}
Expand All @@ -61,6 +64,9 @@ it('builds a generated plugin into a viable archive', async () => {
process.execPath,
['../../scripts/plugin_helpers', 'build', '--kibana-version', '7.5.0'],
{
env: {
NODE_OPTIONS: '--openssl-legacy-provider',
},
cwd: PLUGIN_DIR,
all: true,
}
Expand Down
2 changes: 2 additions & 0 deletions packages/kbn-ui-shared-deps-npm/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -86,9 +86,11 @@ webpack_cli(
env = select({
"//:dist": {
"NODE_ENV": "production",
"NODE_OPTIONS": "--openssl-legacy-provider",
},
"//conditions:default": {
"NODE_ENV": "development",
"NODE_OPTIONS": "--openssl-legacy-provider",
},
})
)
Expand Down
2 changes: 2 additions & 0 deletions packages/kbn-ui-shared-deps-src/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,11 @@ webpack_cli(
env = select({
"//:dist": {
"NODE_ENV": "production",
"NODE_OPTIONS": "--openssl-legacy-provider",
},
"//conditions:default": {
"NODE_ENV": "development",
"NODE_OPTIONS": "--openssl-legacy-provider",
},
}),
visibility = ["//visibility:public"],
Expand Down
2 changes: 1 addition & 1 deletion src/dev/build/lib/download.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,7 @@ export async function downloadToDisk({
const downloadedSha = hash.digest('hex');
if (downloadedSha !== shaChecksum) {
throw new Error(
`Downloaded checksum ${downloadedSha} does not match the expected ${shaAlgorithm} checksum.`
`Downloaded checksum ${downloadedSha} does not match the expected (${shaAlgorithm}) checksum ${shaChecksum}, for file: ${url}.`
);
}
}
Expand Down
4 changes: 2 additions & 2 deletions src/dev/build/lib/integration_tests/download.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ describe('downloadToDisk', () => {
shaAlgorithm: 'sha256',
});
await expect(promise).rejects.toMatchInlineSnapshot(
`[Error: Downloaded checksum 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae does not match the expected sha256 checksum.]`
`[Error: Downloaded checksum ${FOO_SHA256} does not match the expected (sha256) checksum bar, for file: ${serverUrl}.]`
);

try {
Expand Down Expand Up @@ -175,7 +175,7 @@ describe('downloadToDisk', () => {
" info Retrying in 0.1 seconds",
" debg [2/3] Attempting download of TEST_SERVER_URL sha256",
" debg Downloaded 3 bytes to TMP_DIR/__tmp_download_js_test_file__",
" debg Download failed: Downloaded checksum fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 does not match the expected sha256 checksum.",
" debg Download failed: Downloaded checksum fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9 does not match the expected (sha256) checksum 2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae, for file: TEST_SERVER_URL.",
" debg Deleting downloaded data at TMP_DIR/__tmp_download_js_test_file__",
" info Retrying in 0.2 seconds",
" debg [3/3] Attempting download of TEST_SERVER_URL sha256",
Expand Down
27 changes: 14 additions & 13 deletions src/dev/build/tasks/patch_native_modules_task.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,17 +41,13 @@ interface Package {
const packages: Package[] = [
{
name: 're2',
version: '1.17.4',
version: '1.17.7',
destinationPath: 'node_modules/re2/build/Release/re2.node',
extractMethod: 'gunzip',
archives: {
'darwin-x64': {
url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.4/darwin-x64-93.gz',
sha256: '9558c5cb39622e9b3653203e772b129d6c634e7dbd7af1b244352fc1d704601f',
},
'linux-x64': {
url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.4/linux-x64-93.gz',
sha256: '4d06747b266c75b6f7ced93977692c0586ce6a52924cabb569bd966378941aa1',
url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.7/linux-x64-108.gz',
sha256: 'e0b62ff7c415c95f57232f2726711c0fd71056c848538f095ba3fa1126ef5e31',
},

// ARM builds are currently done manually as Github Actions used in upstream project
Expand All @@ -67,8 +63,13 @@ const packages: Package[] = [
// * capture the sha256 with: `shasum -a 256 linux-arm64-*`
// * upload the `linux-arm64-*.gz` artifact to the `yarn-prebuilt-artifacts` bucket in GCS using the correct version number
'linux-arm64': {
url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.4/linux-arm64-93.gz',
sha256: '25409584f76f3d6ed85463d84adf094eb6e256ed1cb0b754b95bcbda6691fc26',
url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.7/linux-arm64-108.gz',
sha256: 'e2025ead87be9f1ec4a9d892d1cce69c573101762720d56f52b1d52ed7ae0fef',
},

'darwin-x64': {
url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.7/darwin-x64-108.gz',
sha256: '4ed378c5a7fe6134b717afe7642254aff1ed7a881cbcaa53a012ac3efab49f99',
},

// A similar process is necessary for building on ARM macs:
Expand All @@ -78,13 +79,13 @@ const packages: Package[] = [
// * capture the sha256 with: `shasum -a 256 darwin-arm64-*`
// * upload the `darwin-arm64-*.gz` artifact to the `yarn-prebuilt-artifacts` bucket in GCS using the correct version number
'darwin-arm64': {
url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.4/darwin-arm64-93.gz',
sha256: 'd4b708749ddef1c87019f6b80e051ed0c29ccd1de34f233c47d8dcaddf803872',
url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.7/darwin-arm64-108.gz',
sha256: '42afc32137ff5c5bebae5d68347a9786906748c2f28e06194d8950707f2ae90e',
},

'win32-x64': {
url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.4/win32-x64-93.gz',
sha256: '0320d0c0385432944c6fb3c8c8fcd78d440ce5626f7618f9ec71d88e44820674',
url: 'https://us-central1-elastic-kibana-184716.cloudfunctions.net/kibana-ci-proxy-cache/node-re2/uhop/node-re2/releases/download/1.17.7/win32-x64-108.gz',
sha256: 'ff72fe02de652262659c8e17e44a932f3c873362233756b40d1a97538d05de92',
},
},
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ describe('Editor', () => {
oldUrl = global.URL;
olldWindow = { ...global.window };
global.URL = URL;
global.window = Object.create(window);
Object.defineProperty(global, 'window', {
value: Object.create(window),
writable: true,
});
Object.defineProperty(window, 'location', {
value: {
origin: 'http://localhost:5620',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

import { firstValueFrom, type Observable } from 'rxjs';
import AbortController from 'abort-controller';
import { UsageCollectionSetup } from '@kbn/usage-collection-plugin/server';
import { CloudDetector } from './detector';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import { readFile } from 'fs/promises';
import { get, omit } from 'lodash';
import fetch from 'node-fetch';
import { AbortSignal } from 'abort-controller';
import { CloudService } from './cloud_service';
import { CloudServiceResponse } from './cloud_response';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@

import { get, omit } from 'lodash';
import fetch from 'node-fetch';
import { AbortSignal } from 'abort-controller';
import { CloudService } from './cloud_service';
import { CloudServiceResponse } from './cloud_response';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* Side Public License, v 1.
*/

import { AbortSignal } from 'abort-controller';
import type { CloudService } from './cloud_service';
import type { CloudServiceResponseJson } from './cloud_response';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
* Side Public License, v 1.
*/

import { AbortSignal } from 'abort-controller';
import { isObject, isPlainObject } from 'lodash';
import { CloudServiceResponse } from './cloud_response';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@
*/

import fetch, { Response } from 'node-fetch';
import { AbortSignal } from 'abort-controller';
import { CloudService } from './cloud_service';
import { CloudServiceResponse } from './cloud_response';

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ describe('requestOAuthClientCredentialsToken', () => {
"maxSockets": Infinity,
"maxTotalSockets": Infinity,
"options": Object {
"noDelay": true,
"path": null,
"rejectUnauthorized": true,
},
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ describe('requestOAuthJWTToken', () => {
"maxSockets": Infinity,
"maxTotalSockets": Infinity,
"options": Object {
"noDelay": true,
"path": null,
"rejectUnauthorized": true,
},
Expand Down
Loading

0 comments on commit 0006496

Please sign in to comment.