Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[rush] Use a local filesystem build cache in addition to a cloud build cache if one is configured. #2404

Merged
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 12 additions & 13 deletions apps/rush-lib/src/api/BuildCacheConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,21 +4,21 @@
import * as path from 'path';
import { JsonFile, JsonSchema, FileSystem } from '@rushstack/node-core-library';

import { BuildCacheProviderBase } from '../logic/buildCache/BuildCacheProviderBase';
import {
AzureEnvironmentNames,
AzureStorageBuildCacheProvider
} from '../logic/buildCache/AzureStorageBuildCacheProvider';
import { RushConfiguration } from './RushConfiguration';
import { FileSystemBuildCacheProvider } from '../logic/buildCache/FileSystemBuildCacheProvider';
import { RushConstants } from '../logic/RushConstants';
import { CloudBuildCacheProviderBase } from '../logic/buildCache/CloudBuildCacheProviderBase';
import { RushUserConfiguration } from './RushUserConfiguration';

/**
* Describes the file structure for the "common/config/rush/build-cache.json" config file.
*/
interface IBuildCacheJson {
cacheProvider: 'azure-blob-storage' | 'filesystem';
cacheProvider: 'azure-blob-storage' | 'local-only';
}

interface IAzureBlobStorageBuildCacheJson extends IBuildCacheJson {
Expand Down Expand Up @@ -54,10 +54,6 @@ interface IAzureStorageConfigurationJson {
isCacheWriteAllowed?: boolean;
}

interface IFileSystemBuildCacheJson extends IBuildCacheJson {
cacheProvider: 'filesystem';
}

interface IBuildCacheConfigurationOptions {
buildCacheJson: IBuildCacheJson;
rushConfiguration: RushConfiguration;
Expand All @@ -74,24 +70,27 @@ export class BuildCacheConfiguration {
path.join(__dirname, '..', 'schemas', 'build-cache.schema.json')
);

public readonly cacheProvider: BuildCacheProviderBase;
public readonly localCacheProvider: FileSystemBuildCacheProvider;
public readonly cloudCacheProvider: CloudBuildCacheProviderBase | undefined;

private constructor(options: IBuildCacheConfigurationOptions) {
const { buildCacheJson, rushConfiguration, rushUserConfiguration } = options;
this.localCacheProvider = new FileSystemBuildCacheProvider({
rushUserConfiguration,
rushConfiguration
});

switch (buildCacheJson.cacheProvider) {
case 'filesystem': {
this.cacheProvider = new FileSystemBuildCacheProvider({
rushConfiguration,
rushUserConfiguration
});
case 'local-only': {
// Don't configure a cloud cache provider
break;
}

case 'azure-blob-storage': {
const azureStorageBuildCacheJson: IAzureBlobStorageBuildCacheJson = buildCacheJson as IAzureBlobStorageBuildCacheJson;
const azureStorageConfigurationJson: IAzureStorageConfigurationJson =
azureStorageBuildCacheJson.azureBlobStorageConfiguration;
this.cacheProvider = new AzureStorageBuildCacheProvider({
this.cloudCacheProvider = new AzureStorageBuildCacheProvider({
storageAccountName: azureStorageConfigurationJson.storageAccountName,
storageContainerName: azureStorageConfigurationJson.storageContainerName,
azureEnvironment: azureStorageConfigurationJson.azureEnvironment,
Expand Down
25 changes: 19 additions & 6 deletions apps/rush-lib/src/cli/actions/UpdateCloudCredentials.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,12 @@ export class UpdateCloudCredentials extends BaseRushAction {
`If the ${this._deleteFlag.longName} is provided, no other parameters may be provided.`
);
throw new AlreadyReportedError();
} else if (buildCacheConfiguration.cloudCacheProvider) {
await buildCacheConfiguration.cloudCacheProvider.deleteCachedCredentialsAsync(terminal);
} else {
await buildCacheConfiguration.cacheProvider.deleteCachedCredentialsAsync(terminal);
terminal.writeLine(
'No cloud build cache is configured. No credentials are stored and can be deleted.'
iclanton marked this conversation as resolved.
Show resolved Hide resolved
);
}
} else if (this._interactiveModeFlag.value && this._credentialParameter.value !== undefined) {
terminal.writeErrorLine(
Expand All @@ -86,12 +90,21 @@ export class UpdateCloudCredentials extends BaseRushAction {
);
throw new AlreadyReportedError();
} else if (this._interactiveModeFlag.value) {
await buildCacheConfiguration.cacheProvider.updateCachedCredentialInteractiveAsync(terminal);
if (buildCacheConfiguration.cloudCacheProvider) {
await buildCacheConfiguration.cloudCacheProvider.updateCachedCredentialInteractiveAsync(terminal);
} else {
terminal.writeLine('No cloud build cache is configured. Credentials are not required.');
iclanton marked this conversation as resolved.
Show resolved Hide resolved
}
} else if (this._credentialParameter.value !== undefined) {
await buildCacheConfiguration.cacheProvider.updateCachedCredentialAsync(
terminal,
this._credentialParameter.value
);
if (buildCacheConfiguration.cloudCacheProvider) {
await buildCacheConfiguration.cloudCacheProvider.updateCachedCredentialAsync(
terminal,
this._credentialParameter.value
);
} else {
terminal.writeErrorLine('No cloud build cache is configured. Credentials are not supported.');
iclanton marked this conversation as resolved.
Show resolved Hide resolved
throw new AlreadyReportedError();
}
} else {
terminal.writeErrorLine(
`One of the ${this._interactiveModeFlag.longName} parameter, the ` +
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { BuildCacheProviderBase, IBuildCacheProviderBaseOptions } from './BuildCacheProviderBase';
import { Terminal } from '@rushstack/node-core-library';
import {
BlobClient,
Expand All @@ -19,10 +18,11 @@ import { EnvironmentConfiguration, EnvironmentVariableNames } from '../../api/En
import { CredentialCache, ICredentialCacheEntry } from '../CredentialCache';
import { RushConstants } from '../RushConstants';
import { Utilities } from '../../utilities/Utilities';
import { CloudBuildCacheProviderBase } from './CloudBuildCacheProviderBase';

export type AzureEnvironmentNames = keyof typeof AzureAuthorityHosts;

export interface IAzureStorageBuildCacheProviderOptions extends IBuildCacheProviderBaseOptions {
export interface IAzureStorageBuildCacheProviderOptions {
storageContainerName: string;
storageAccountName: string;
azureEnvironment?: AzureEnvironmentNames;
Expand All @@ -32,23 +32,24 @@ export interface IAzureStorageBuildCacheProviderOptions extends IBuildCacheProvi

const SAS_TTL_MILLISECONDS: number = 7 * 24 * 60 * 60 * 1000; // Seven days

export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase {
export class AzureStorageBuildCacheProvider extends CloudBuildCacheProviderBase {
private readonly _storageAccountName: string;
private readonly _storageContainerName: string;
private readonly _azureEnvironment: AzureEnvironmentNames;
private readonly _blobPrefix: string | undefined;
private readonly _isCacheWriteAllowed: boolean;
private __credentialCacheId: string | undefined;

public readonly isCacheWriteAllowed: boolean;

private _containerClient: ContainerClient | undefined;

public constructor(options: IAzureStorageBuildCacheProviderOptions) {
super(options);
super();
this._storageAccountName = options.storageAccountName;
this._storageContainerName = options.storageContainerName;
this._azureEnvironment = options.azureEnvironment || 'AzurePublicCloud';
this._blobPrefix = options.blobPrefix;
this._isCacheWriteAllowed = options.isCacheWriteAllowed;
this.isCacheWriteAllowed = options.isCacheWriteAllowed;

if (!(this._azureEnvironment in AzureAuthorityHosts)) {
throw new Error(
Expand All @@ -67,7 +68,7 @@ export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase {
this._storageContainerName
];

if (this._isCacheWriteAllowed) {
if (this.isCacheWriteAllowed) {
cacheIdParts.push('cacheWriteAllowed');
}

Expand Down Expand Up @@ -104,6 +105,13 @@ export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase {
cacheId: string,
entryStream: Buffer
): Promise<boolean> {
if (!this.isCacheWriteAllowed) {
terminal.writeErrorLine(
'Writing to Azure Blob Storage cache is not allowed in the current configuration.'
);
return false;
}

const blobClient: BlobClient = await this._getBlobClientForCacheIdAsync(cacheId);
const blockBlobClient: BlockBlobClient = blobClient.getBlockBlobClient();
try {
Expand Down Expand Up @@ -186,9 +194,12 @@ export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase {
}

let blobServiceClient: BlobServiceClient;
if (sasString || !this._isCacheWriteAllowed) {
if (sasString) {
const connectionString: string = this._getConnectionString(sasString);
blobServiceClient = BlobServiceClient.fromConnectionString(connectionString);
} else if (!this.isCacheWriteAllowed) {
// If cache write isn't allowed and we don't have a credential, assume the blob supports anonymous read
blobServiceClient = new BlobServiceClient(this._storageAccountUrl);
} else {
throw new Error(
"An Azure Storage SAS credential hasn't been provided, or has expired. " +
Expand Down Expand Up @@ -232,7 +243,7 @@ export class AzureStorageBuildCacheProvider extends BuildCacheProviderBase {

const containerSasPermissions: ContainerSASPermissions = new ContainerSASPermissions();
containerSasPermissions.read = true;
containerSasPermissions.write = this._isCacheWriteAllowed;
containerSasPermissions.write = this.isCacheWriteAllowed;

const queryParameters: SASQueryParameters = generateBlobSASQueryParameters(
{
Expand Down
91 changes: 0 additions & 91 deletions apps/rush-lib/src/logic/buildCache/BuildCacheProviderBase.ts

This file was deleted.

21 changes: 21 additions & 0 deletions apps/rush-lib/src/logic/buildCache/CloudBuildCacheProviderBase.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.

import { Terminal } from '@rushstack/node-core-library';

export abstract class CloudBuildCacheProviderBase {
public abstract readonly isCacheWriteAllowed: boolean;

public abstract tryGetCacheEntryBufferByIdAsync(
terminal: Terminal,
cacheId: string
): Promise<Buffer | undefined>;
public abstract trySetCacheEntryBufferAsync(
terminal: Terminal,
cacheId: string,
entryBuffer: Buffer
): Promise<boolean>;
public abstract updateCachedCredentialAsync(terminal: Terminal, credential: string): Promise<void>;
public abstract updateCachedCredentialInteractiveAsync(terminal: Terminal): Promise<void>;
public abstract deleteCachedCredentialsAsync(terminal: Terminal): Promise<void>;
}
34 changes: 5 additions & 29 deletions apps/rush-lib/src/logic/buildCache/FileSystemBuildCacheProvider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,33 +2,28 @@
// See LICENSE in the project root for license information.

import * as path from 'path';
import { AlreadyReportedError, FileSystem, Terminal } from '@rushstack/node-core-library';
import { FileSystem } from '@rushstack/node-core-library';

import { RushConfiguration } from '../../api/RushConfiguration';
import { RushUserConfiguration } from '../../api/RushUserConfiguration';
import { BuildCacheProviderBase, IBuildCacheProviderBaseOptions } from './BuildCacheProviderBase';

export interface IFileSystemBuildCacheProviderOptions extends IBuildCacheProviderBaseOptions {
export interface IFileSystemBuildCacheProviderOptions {
rushConfiguration: RushConfiguration;
rushUserConfiguration: RushUserConfiguration;
}

const BUILD_CACHE_FOLDER_NAME: string = 'build-cache';

export class FileSystemBuildCacheProvider extends BuildCacheProviderBase {
export class FileSystemBuildCacheProvider {
private readonly _cacheFolderPath: string;

public constructor(options: IFileSystemBuildCacheProviderOptions) {
super(options);
this._cacheFolderPath =
options.rushUserConfiguration.buildCacheFolder ||
path.join(options.rushConfiguration.commonTempFolder, BUILD_CACHE_FOLDER_NAME);
}

public async tryGetCacheEntryBufferByIdAsync(
terminal: Terminal,
cacheId: string
): Promise<Buffer | undefined> {
public async tryGetCacheEntryBufferByIdAsync(cacheId: string): Promise<Buffer | undefined> {
const cacheEntryFilePath: string = path.join(this._cacheFolderPath, cacheId);
try {
return await FileSystem.readFileToBufferAsync(cacheEntryFilePath);
Expand All @@ -41,28 +36,9 @@ export class FileSystemBuildCacheProvider extends BuildCacheProviderBase {
}
}

public async trySetCacheEntryBufferAsync(
terminal: Terminal,
cacheId: string,
entryBuffer: Buffer
): Promise<boolean> {
public async trySetCacheEntryBufferAsync(cacheId: string, entryBuffer: Buffer): Promise<boolean> {
const cacheEntryFilePath: string = path.join(this._cacheFolderPath, cacheId);
await FileSystem.writeFileAsync(cacheEntryFilePath, entryBuffer, { ensureFolderExists: true });
return true;
}

public async updateCachedCredentialAsync(terminal: Terminal, credential: string): Promise<void> {
terminal.writeErrorLine('A filesystem build cache is configured. Credentials are not supported.');
throw new AlreadyReportedError();
}

public async updateCachedCredentialInteractiveAsync(terminal: Terminal): Promise<void> {
terminal.writeLine('A filesystem build cache is configured. Credentials are not required.');
}

public async deleteCachedCredentialsAsync(terminal: Terminal): Promise<void> {
terminal.writeLine(
'A filesystem build cache is configured. No credentials are stored and can be deleted.'
);
}
}
Loading