feat(core): throw an error when unknown local cache source
This commit is contained in:
parent
95a2256a34
commit
1bc58c997d
@ -3291,6 +3291,14 @@
|
||||
"isExternal": false,
|
||||
"children": [],
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"name": "Unknown Local Cache Error",
|
||||
"path": "/recipes/other/unknown-local-cache",
|
||||
"id": "unknown-local-cache",
|
||||
"isExternal": false,
|
||||
"children": [],
|
||||
"disableCollapsible": false
|
||||
}
|
||||
],
|
||||
"disableCollapsible": false
|
||||
@ -3494,6 +3502,14 @@
|
||||
"isExternal": false,
|
||||
"children": [],
|
||||
"disableCollapsible": false
|
||||
},
|
||||
{
|
||||
"name": "Unknown Local Cache Error",
|
||||
"path": "/recipes/other/unknown-local-cache",
|
||||
"id": "unknown-local-cache",
|
||||
"isExternal": false,
|
||||
"children": [],
|
||||
"disableCollapsible": false
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@ -1597,6 +1597,16 @@
|
||||
"isExternal": false,
|
||||
"path": "/recipes/other/standalone-ngrx-apis",
|
||||
"tags": []
|
||||
},
|
||||
{
|
||||
"id": "unknown-local-cache",
|
||||
"name": "Unknown Local Cache Error",
|
||||
"description": "",
|
||||
"file": "shared/guides/unknown-local-cache",
|
||||
"itemList": [],
|
||||
"isExternal": false,
|
||||
"path": "/recipes/other/unknown-local-cache",
|
||||
"tags": []
|
||||
}
|
||||
],
|
||||
"isExternal": false,
|
||||
@ -1852,5 +1862,15 @@
|
||||
"isExternal": false,
|
||||
"path": "/recipes/other/standalone-ngrx-apis",
|
||||
"tags": []
|
||||
},
|
||||
"/recipes/other/unknown-local-cache": {
|
||||
"id": "unknown-local-cache",
|
||||
"name": "Unknown Local Cache Error",
|
||||
"description": "",
|
||||
"file": "shared/guides/unknown-local-cache",
|
||||
"itemList": [],
|
||||
"isExternal": false,
|
||||
"path": "/recipes/other/unknown-local-cache",
|
||||
"tags": []
|
||||
}
|
||||
}
|
||||
|
||||
@ -1458,6 +1458,12 @@
|
||||
"id": "standalone-ngrx-apis",
|
||||
"tags": [],
|
||||
"file": "shared/recipes/standalone-ngrx-apis"
|
||||
},
|
||||
{
|
||||
"name": "Unknown Local Cache Error",
|
||||
"id": "unknown-local-cache",
|
||||
"tags": [],
|
||||
"file": "shared/guides/unknown-local-cache"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
86
docs/shared/guides/unknown-local-cache.md
Normal file
86
docs/shared/guides/unknown-local-cache.md
Normal file
@ -0,0 +1,86 @@
|
||||
# Unknown Local Cache Error
|
||||
|
||||
This document will explain why the following error happens and how to address it.
|
||||
|
||||
```
|
||||
NX Invalid Cache Directory for Task "myapp:build"
|
||||
|
||||
The local cache artifact in "node_modules/.cache/nx/786524780459028195" was not been generated on this machine.
|
||||
As a result, the cache's content integrity cannot be confirmed, which may make cache restoration potentially unsafe.
|
||||
If your machine ID has changed since the artifact was cached, run "nx reset" to fix this issue.
|
||||
Read about the error and how to address it here: https://nx.dev/recipes/other/unknown-local-cache
|
||||
```
|
||||
|
||||
## Nx Tracks Cache Source
|
||||
|
||||
Nx can cache tasks, which can drastically speed up your CI and local builds. However, this comes with the potential risk
|
||||
of "cache poisoning", where cache artifacts could be intentionally or inadvertently overwritten. If another user
|
||||
executes a task that matches the hash of the tainted artifact, they could retrieve the corrupted artifact and use it as
|
||||
the outcome of the task. Nx and Nx Cloud contain several safeguards to minimize the likelihood of cache poisoning or, in
|
||||
the case of Nx Cloud, completely prevent it.
|
||||
|
||||
The error above is one such safeguard.
|
||||
|
||||
Nx trusts the local cache. If you executed a task and stored the corresponding cached artifact on your machine, you can
|
||||
safely restore it on the same machine without worrying about cache poisoning. After all, in order to tamper with the
|
||||
cache artifact, the actor would need access to the machine itself.
|
||||
|
||||
However, when artifacts in the local cache are created by a different machine, we cannot make such assumption. By
|
||||
default, Nx will refuse to use such artifacts and will throw the "Invalid Cache Directory" error.
|
||||
|
||||
## Your MachineId Has Changed
|
||||
|
||||
Upgrading your computer's hardware may alter its Machine ID, yielding the error above. To fix it execute `nx reset` to
|
||||
remove all the cache directories created under the previous Machine ID. After doing so, you should no longer see the
|
||||
error. After doing so, you should no longer see the error.
|
||||
|
||||
## You Share Cache with Another Machine Using a Network Drive
|
||||
|
||||
You can prefix any Nx command with `NX_REJECT_UNKNOWN_LOCAL_CACHE=0` to ignore the error (
|
||||
e.g., `NX REJECT_UNKNOWN_LOCAL_CACHE=0 nx run-many -t build test`). This is similar to
|
||||
setting `NODE_TLS_REJECT_UNAUTHORIZED=0` to ignore any errors stemming form self-signed certificates. Even though it
|
||||
will make it work, this approach is discouraged.
|
||||
|
||||
Storing Nx's local cache on a network drive can present security risks. When a network drive is shared, every CI run has
|
||||
access to all the previously created Nx cache artifacts. Hence, it is plausible for every single artifact - for every
|
||||
single task hash - to be accessed without leaving any trace. This is feasible due to the network drive's capability to
|
||||
allow overwrites.
|
||||
|
||||
Instead of sharing the network drive, we highly recommend you to implement the `RemoteCache` interface.
|
||||
|
||||
## Implementing Remote Cache Interface
|
||||
|
||||
This is the interface:
|
||||
|
||||
```typescript
|
||||
interface RemoteCache {
|
||||
retrieve(hash: string, cachePath: string);
|
||||
|
||||
store(hash: string, cachePath: string);
|
||||
}
|
||||
```
|
||||
|
||||
> You will need to wrap the default tasks runner to provide the remote cache implementation.
|
||||
|
||||
## How Nx Cloud Makes Sure Sharing Cache is Safe
|
||||
|
||||
The Nx Cloud runner provides an implementation of `RemoteCache` which does the following things making sharing the cache safe:
|
||||
|
||||
1. **Immutable Artifacts:** Nx Cloud allows you to create and store new artifacts without the ability to override the
|
||||
existing ones. This prevents any possibility of poisoning an existing artifact. This is achieved by managing the
|
||||
cache using short-lived signed URLs.
|
||||
|
||||
2. **Artifact Accessibility:** Nx Cloud provides access to the cache artifact specifically for the task that is
|
||||
currently being executed. It restricts the ability to list all cache artifacts.
|
||||
|
||||
3. **Visibility Control:** Nx Cloud comes with options to manage the visibility of your cache artifacts. For instance,
|
||||
the cache artifacts created in `main` might be accessible by anyone across any branch, whereas the artifacts created
|
||||
in your PR could be shared only within your PR runs.
|
||||
|
||||
4. **Access Token Traceability:** Nx Cloud keeps a record of the access token used to create a cache artifact. In case
|
||||
an access token gets compromised it can be easily removed, in turn deleting all the cache artifacts that were created
|
||||
using it.
|
||||
|
||||
Nx Cloud is not the only remote cache you can use. If you are using a different remote cache or using your
|
||||
own implementation, we would highly recommend ensuring that the same safety mechanisms as Nx Cloud have been put in
|
||||
place.
|
||||
@ -219,6 +219,7 @@
|
||||
- [Identify Dependencies Between Folders](/recipes/other/identify-dependencies-between-folders)
|
||||
- [Rescope Packages from @nrwl to @nx](/recipes/other/rescope)
|
||||
- [Standalone NgRx APIs](/recipes/other/standalone-ngrx-apis)
|
||||
- [Unknown Local Cache Error](/recipes/other/unknown-local-cache)
|
||||
|
||||
- Plugins
|
||||
|
||||
|
||||
@ -330,7 +330,8 @@
|
||||
"tailwindcss": "3.2.4",
|
||||
"tslib": "^2.3.0",
|
||||
"vitest": "^0.32.0",
|
||||
"weak-napi": "^2.0.2"
|
||||
"weak-napi": "^2.0.2",
|
||||
"node-machine-id": "1.1.12"
|
||||
},
|
||||
"resolutions": {
|
||||
"minimist": "^1.2.6",
|
||||
|
||||
@ -64,7 +64,8 @@
|
||||
"tslib": "^2.3.0",
|
||||
"v8-compile-cache": "2.3.0",
|
||||
"yargs": "^17.6.2",
|
||||
"yargs-parser": "21.1.1"
|
||||
"yargs-parser": "21.1.1",
|
||||
"node-machine-id": "1.1.12"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@swc-node/register": "^1.4.2",
|
||||
|
||||
@ -6,6 +6,7 @@ import { DefaultTasksRunnerOptions } from './default-tasks-runner';
|
||||
import { spawn } from 'child_process';
|
||||
import { cacheDir } from '../utils/cache-directory';
|
||||
import { Task } from '../config/task-graph';
|
||||
import { machineId } from 'node-machine-id';
|
||||
|
||||
export type CachedResult = {
|
||||
terminalOutput: string;
|
||||
@ -20,6 +21,8 @@ export class Cache {
|
||||
cachePath = this.createCacheDir();
|
||||
terminalOutputsDir = this.createTerminalOutputsDir();
|
||||
|
||||
private _currentMachineId: string = null;
|
||||
|
||||
constructor(private readonly options: DefaultTasksRunnerOptions) {}
|
||||
|
||||
removeOldCacheRecords() {
|
||||
@ -44,6 +47,20 @@ export class Cache {
|
||||
}
|
||||
}
|
||||
|
||||
async currentMachineId() {
|
||||
if (!this._currentMachineId) {
|
||||
try {
|
||||
this._currentMachineId = await machineId();
|
||||
} catch (e) {
|
||||
if (process.env.NX_VERBOSE_LOGGING == 'true') {
|
||||
console.log(`Unable to get machineId. Error: ${e.message}`);
|
||||
}
|
||||
this._currentMachineId = '';
|
||||
}
|
||||
}
|
||||
return this._currentMachineId;
|
||||
}
|
||||
|
||||
async get(task: Task): Promise<CachedResult | null> {
|
||||
const res = await this.getFromLocalDir(task);
|
||||
|
||||
@ -98,6 +115,7 @@ export class Cache {
|
||||
// so if the process gets terminated while we are copying stuff into cache,
|
||||
// the cache entry won't be used.
|
||||
await writeFile(join(td, 'code'), code.toString());
|
||||
await writeFile(join(td, 'source'), await this.currentMachineId());
|
||||
await writeFile(tdCommit, 'true');
|
||||
|
||||
if (this.options.remoteCache) {
|
||||
@ -208,6 +226,32 @@ export class Cache {
|
||||
try {
|
||||
code = Number(await readFile(join(td, 'code'), 'utf-8'));
|
||||
} catch {}
|
||||
|
||||
let sourceMachineId = null;
|
||||
try {
|
||||
sourceMachineId = await readFile(join(td, 'source'), 'utf-8');
|
||||
} catch {}
|
||||
|
||||
if (
|
||||
sourceMachineId &&
|
||||
sourceMachineId != (await this.currentMachineId())
|
||||
) {
|
||||
if (
|
||||
process.env.NX_REJECT_UNKNOWN_LOCAL_CACHE != '0' &&
|
||||
process.env.NX_REJECT_UNKNOWN_LOCAL_CACHE != 'false'
|
||||
) {
|
||||
const error = [
|
||||
`Invalid Cache Directory for Task "${task.id}"`,
|
||||
`The local cache artifact in "${td}" was not been generated on this machine.`,
|
||||
`As a result, the cache's content integrity cannot be confirmed, which may make cache restoration potentially unsafe.`,
|
||||
`If your machine ID has changed since the artifact was cached, run "nx reset" to fix this issue.`,
|
||||
`Read about the error and how to address it here: https://nx.dev/recipes/other/unknown-local-cache`,
|
||||
``,
|
||||
].join('\n');
|
||||
throw new Error(error);
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
terminalOutput,
|
||||
outputsPath: join(td, 'outputs'),
|
||||
|
||||
4
pnpm-lock.yaml
generated
4
pnpm-lock.yaml
generated
@ -101,6 +101,9 @@ dependencies:
|
||||
next-seo:
|
||||
specifier: ^5.13.0
|
||||
version: 5.13.0(next@13.3.4)(react-dom@18.2.0)(react@18.2.0)
|
||||
node-machine-id:
|
||||
specifier: 1.1.12
|
||||
version: 1.1.12
|
||||
npm-run-path:
|
||||
specifier: ^4.0.1
|
||||
version: 4.0.1
|
||||
@ -20359,7 +20362,6 @@ packages:
|
||||
|
||||
/node-machine-id@1.1.12:
|
||||
resolution: {integrity: sha512-QNABxbrPa3qEIfrE6GOJ7BYIuignnJw7iQ2YPbc3Nla1HzRJjXzZOiikfF8m7eAMfichLt3M4VgLOetqgDmgGQ==}
|
||||
dev: true
|
||||
|
||||
/node-releases@2.0.10:
|
||||
resolution: {integrity: sha512-5GFldHPXVG/YZmFzJvKK2zDSzPKhEp0+ZR5SVaoSag9fsL5YgHbUHDfnG5494ISANDcK4KwPXAx2xqVEydmd7w==}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user