Skip to content

Commit

Permalink
Merge pull request microsoft#3976 from elliot-nelson/enelson/http-cache
Browse files Browse the repository at this point in the history
[rush] rush-http-build-cache-plugin
  • Loading branch information
iclanton authored Apr 19, 2023
2 parents a702f65 + 5afc844 commit bcc1cc5
Show file tree
Hide file tree
Showing 22 changed files with 965 additions and 2 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -82,6 +82,7 @@ These GitHub repositories provide supplementary resources for Rush Stack:
| [/rigs/heft-web-rig](./rigs/heft-web-rig/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fheft-web-rig.svg)](https://badge.fury.io/js/%40rushstack%2Fheft-web-rig) | [changelog](./rigs/heft-web-rig/CHANGELOG.md) | [@rushstack/heft-web-rig](https://www.npmjs.com/package/@rushstack/heft-web-rig) |
| [/rush-plugins/rush-amazon-s3-build-cache-plugin](./rush-plugins/rush-amazon-s3-build-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-amazon-s3-build-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-amazon-s3-build-cache-plugin) | | [@rushstack/rush-amazon-s3-build-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-amazon-s3-build-cache-plugin) |
| [/rush-plugins/rush-azure-storage-build-cache-plugin](./rush-plugins/rush-azure-storage-build-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-azure-storage-build-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-azure-storage-build-cache-plugin) | | [@rushstack/rush-azure-storage-build-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-azure-storage-build-cache-plugin) |
| [/rush-plugins/rush-http-build-cache-plugin](./rush-plugins/rush-http-build-cache-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-http-build-cache-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-http-build-cache-plugin) | | [@rushstack/rush-http-build-cache-plugin](https://www.npmjs.com/package/@rushstack/rush-http-build-cache-plugin) |
| [/rush-plugins/rush-serve-plugin](./rush-plugins/rush-serve-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Frush-serve-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Frush-serve-plugin) | [changelog](./rush-plugins/rush-serve-plugin/CHANGELOG.md) | [@rushstack/rush-serve-plugin](https://www.npmjs.com/package/@rushstack/rush-serve-plugin) |
| [/webpack/hashed-folder-copy-plugin](./webpack/hashed-folder-copy-plugin/) | [![npm version](https://badge.fury.io/js/%40rushstack%2Fhashed-folder-copy-plugin.svg)](https://badge.fury.io/js/%40rushstack%2Fhashed-folder-copy-plugin) | [changelog](./webpack/hashed-folder-copy-plugin/CHANGELOG.md) | [@rushstack/hashed-folder-copy-plugin](https://www.npmjs.com/package/@rushstack/hashed-folder-copy-plugin) |
| [/webpack/loader-load-themed-styles](./webpack/loader-load-themed-styles/) | [![npm version](https://badge.fury.io/js/%40microsoft%2Floader-load-themed-styles.svg)](https://badge.fury.io/js/%40microsoft%2Floader-load-themed-styles) | [changelog](./webpack/loader-load-themed-styles/CHANGELOG.md) | [@microsoft/loader-load-themed-styles](https://www.npmjs.com/package/@microsoft/loader-load-themed-styles) |
Expand Down
1 change: 1 addition & 0 deletions apps/rush/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@
"@rushstack/heft-node-rig": "workspace:*",
"@rushstack/rush-amazon-s3-build-cache-plugin": "workspace:*",
"@rushstack/rush-azure-storage-build-cache-plugin": "workspace:*",
"@rushstack/rush-http-build-cache-plugin": "workspace:*",
"@types/heft-jest": "1.0.1",
"@types/node": "14.18.36",
"@types/semver": "7.3.5"
Expand Down
1 change: 1 addition & 0 deletions apps/rush/src/start-dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ function includePlugin(pluginName: string, pluginPackageName?: string): void {

includePlugin('rush-amazon-s3-build-cache-plugin');
includePlugin('rush-azure-storage-build-cache-plugin');
includePlugin('rush-http-build-cache-plugin');
// Including this here so that developers can reuse it without installing the plugin a second time
includePlugin('rush-azure-interactive-auth-plugin', '@rushstack/rush-azure-storage-build-cache-plugin');

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"changes": [
{
"packageName": "@microsoft/rush",
"comment": "Add built-in plugin rush-http-build-cache-plugin",
"type": "none"
}
],
"packageName": "@microsoft/rush"
}
4 changes: 4 additions & 0 deletions common/config/rush/nonbrowser-approved-packages.json
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,10 @@
"name": "@rushstack/rush-azure-storage-build-cache-plugin",
"allowedCategories": [ "libraries" ]
},
{
"name": "@rushstack/rush-http-build-cache-plugin",
"allowedCategories": [ "libraries" ]
},
{
"name": "@rushstack/rush-sdk",
"allowedCategories": [ "libraries", "tests" ]
Expand Down
29 changes: 29 additions & 0 deletions common/config/rush/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -96,5 +96,48 @@
* If set to true, allow writing to the cache. Defaults to false.
*/
// "isCacheWriteAllowed": true
},

/**
* Use this configuration with "cacheProvider"="http"
*/
"httpConfiguration": {
/**
* (Required) The URL of the server that stores the caches.
* Example: "https://build-cacches.example.com/"
*/
// "url": "https://build-cacches.example.com/",

/**
* (Optional) The HTTP method to use when writing to the cache (defaults to PUT).
* Should be one of PUT, POST, or PATCH.
* Example: "PUT"
*/
// "uploadMethod": "PUT",

/**
* (Optional) HTTP headers to pass to the cache server.
* Example: { "X-HTTP-Company-Id": "109283" }
*/
// "headers": {},

/**
* (Optional) Shell command that prints the authorization token needed to communicate with the
* cache server, and exits with exit code 0. This command will be executed from the root of
* the monorepo.
* Example: { "exec": "node", "args": ["common/scripts/auth.js"] }
*/
// "tokenHandler": { "exec": "node", "args": ["common/scripts/auth.js"] },

/**
* (Optional) Prefix for cache keys.
* Example: "my-company-"
*/
// "cacheKeyPrefix": "",

/**
* (Optional) If set to true, allow writing to the cache. Defaults to false.
*/
// "isCacheWriteAllowed": true
}
}
3 changes: 2 additions & 1 deletion libraries/rush-lib/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,8 @@
},
"publishOnlyDependencies": {
"@rushstack/rush-amazon-s3-build-cache-plugin": "workspace:*",
"@rushstack/rush-azure-storage-build-cache-plugin": "workspace:*"
"@rushstack/rush-azure-storage-build-cache-plugin": "workspace:*",
"@rushstack/rush-http-build-cache-plugin": "workspace:*"
},
"sideEffects": [
"lib-esnext/start-pnpm.js",
Expand Down
1 change: 1 addition & 0 deletions libraries/rush-lib/src/pluginFramework/PluginManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ export class PluginManager {

tryAddBuiltInPlugin('rush-amazon-s3-build-cache-plugin');
tryAddBuiltInPlugin('rush-azure-storage-build-cache-plugin');
tryAddBuiltInPlugin('rush-http-build-cache-plugin');
// This is a secondary plugin inside the `@rushstack/rush-azure-storage-build-cache-plugin`
// package. Because that package comes with Rush (for now), it needs to get registered here.
// If the necessary config file doesn't exist, this plugin doesn't do anything.
Expand Down
72 changes: 71 additions & 1 deletion libraries/rush-lib/src/schemas/build-cache.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,56 @@
"description": "If set to true, allow writing to the cache. Defaults to false."
}
}
},
"httpConfiguration": {
"type": "object",
"additionalProperties": false,
"properties": {
"url": {
"type": "string",
"description": "(Required) The URL of the server that stores the caches (e.g. \"https://build-caches.example.com\").",
"format": "uri"
},
"uploadMethod": {
"type": "string",
"description": "(Optional) The HTTP method to use when writing to the cache (defaults to PUT).",
"enum": ["PUT", "POST", "PATCH"],
"default": "PUT"
},
"headers": {
"type": "object",
"description": "(Optional) HTTP headers to pass to the cache server",
"properties": {},
"additionalProperties": {
"type": "string"
}
},
"tokenHandler": {
"type": "object",
"description": "(Optional) Shell command that prints the authorization token needed to communicate with the HTTPS server and exits with code 0. This command will be executed from the root of the monorepo.",
"properties": {
"exec": {
"type": "string",
"description": "(Required) The command or script to execute."
},
"args": {
"type": "array",
"description": "(Optional) Arguments to pass to the command or script.",
"items": {
"type": "string"
}
}
}
},
"cacheKeyPrefix": {
"type": "string",
"description": "(Optional) prefix for cache keys."
},
"isCacheWriteAllowed": {
"type": "boolean",
"description": "(Optional) If set to true, allow writing to the cache. Defaults to false."
}
}
}
},
"oneOf": [
Expand All @@ -94,7 +144,7 @@
"properties": {
"cacheProvider": {
"type": "string",
"pattern": "^(?:(?!azure-blob-storage|amazon-s3).)*$"
"pattern": "^(?:(?!azure-blob-storage|amazon-s3|http).)*$"
}
}
},
Expand Down Expand Up @@ -160,6 +210,26 @@
]
}
}
},
{
"type": "object",
"additionalProperties": true,
"properties": {
"cacheProvider": {
"type": "string",
"enum": ["http"]
},
"httpConfiguration": {
"type": "object",
"additionalProperties": true,
"required": ["url"],
"properties": {
"url": {
"$ref": "#/definitions/anything"
}
}
}
}
}
]
}
Expand Down
116 changes: 116 additions & 0 deletions rush-plugins/rush-http-build-cache-plugin/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
# @rushstack/rush-http-build-cache-plugin

A Rush plugin that uses HTTP/HTTPS to manage cache objects.

Authentication is provided via standard `Authorization` HTTP headers, with the value (a Bearer or Basic token) configured using a custom node script. Your "tokenHandler" node script is automatically called when the user updates their cloud credentials.

## Configuration

To use the HTTP build cache plugin, enable it in `common/config/rush/build-cache.json`:

```json
{
"buildCacheEnabled": true,

"cacheProvider": "http"
}
```

Then customize the `httpConfiguration` block. For typical use cases, where you'll use a remote HTTP URL with authentication, you'll need to provide at least the `url` and `tokenHandler` options:

```json
{
"httpConfiguration": {
"url": "https://build-cache.example.com",
"tokenHandler": {
"exec": "node",
"args": ["common/scripts/custom-script-that-returns-an-authentication-header.js"]
},
"isCacheWriteAllowed": false
}
}
```

(For more detail on the above properties and additional optional properties, consult the default `build-cache.json` file.)

## Authorization Details

The HTTP build cache plugin offloads authorization to an external executable script that you define. A typical use case would be to create a simple script, for example `common/scripts/cache-auth.js`, that prints an Authorization header value when executed.

For example:

```console
node common/scripts/cache-auth.js
# => Bearer 0284357923592790DDb979dBcd2zz
```

How the script generates authorization values is up to you, and depends on the configuration of your remote cache server.

Possible implementations:

- The script could read simple environment variables (`CACHE_USER` and `CACHE_TOKEN`) defined by the developer
- It could reuse existing credentials your developers have, like NPM tokens, GitHub credentials, or jFrog API tokens
- It could make an HTTP call to another server accessible by your developers that returns temporary credentials

## HTTP Cache Server Requirements

The HTTP build cache plugin can use almost any HTTP/HTTPS backend for remote caching, as long as it honors the following rules:

- Uses `Authorization: Bearer xxx` or `Authorization: Basic xxx` headers for authentication.
- Accepts GET requests for cache reads.
- Accepts PUT requests for cache writes (with a raw request body -- no `form/multipart` MIME types).
- Cache hits return HTTP 200 with the file in the response body.
- Successful cache writes return HTTP 2xx (200-299).
- Cache misses return HTTP 404 or HTTP 403.
- Invalid or missing authentication returns HTTP 401.

## Examples

### Gradle Build Cache Server

The Gradle Build Cache Server (typically used to support Gradle Remote Build Cache) meets all of the requirements above, so if you don't have another server in mind, you can use it as your remote backend.

First, start up and configure your build cache node locally:

- Download latest JAR file from https://docs.gradle.com/build-cache-node/
- Start the service: `java -jar build-cache-node-14.0.jar start`
- Copy the startup banner information, and navigate to the specified localhost port
- Enter the temporary username/password credentials printed in the startup banner
- Click Build Cache > Settings > Cache Access Control and grant "Read & write" access to Anonymous

Second, configure your `build-cache.json` file as described in the Configuration section:

- Note that your `url` must end with `/cache/`, for example, `http://localhost:5071/cache/`.
- To test reading and writing, set `isCacheWriteAllowed: true`.
- Configure `tokenHandler` to point to a script that prints a Basic or Bearer Authorization value (this can be a dummy string if you granted Read and Write to Anonymous in your build cache node configuration).

Note that the Gradle Build Cache Server has a stricter format for its cache keys (they should be a simple hexadecimal hash with no non-alphanumeric characters). Configure this setting in your `build-cache.json` file:

```json
{
"cacheEntryNamePattern": "[hash]"
}
```

Last, initialize your cache credentials using Rush:

```console
rush update-cloud-credentials --interactive
```

To test out your remote build cache with full debugging output (for spotting any errors reading or writing the cache), run with the `--debug` flag:

```console
rush --debug build --verbose
```

> If you go on to deploy Rush remote build caching to your developers using the Gradle Build Cache, update your `tokenHandler`
> script to reflect your use case -- for example, you could require each developer to have a designated username/token configured
> via environment variables, and configure Cache Access Control with the corresponding entries. In this case the `tokenHandler`
> script should read the environment variables and print out an Authorization header, for example:
>
> ```javascript
> // common/scripts/build-cache-auth.js
> const credentials = `${process.env.CACHE_USER}:${process.env.CACHE_TOKEN}`;
> console.log('Basic ' + Buffer.from(credentials).toString('base64'));
> ```
14 changes: 14 additions & 0 deletions rush-plugins/rush-http-build-cache-plugin/config/jest.config.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"extends": "@rushstack/heft-node-rig/profiles/default/config/jest.config.json",
"clearMocks": true,
"restoreMocks": true,
"collectCoverage": true,
"coverageThreshold": {
"global": {
"branches": 4,
"functions": 15,
"lines": 4,
"statements": 4
}
}
}
Loading

0 comments on commit bcc1cc5

Please sign in to comment.