@rushstack/rush-azure-storage-build-cache-plugin
Version:
Rush plugin for Azure storage cloud build cache
176 lines • 10.3 kB
JavaScript
// Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT license.
// See LICENSE in the project root for license information.
Object.defineProperty(exports, "__esModule", { value: true });
exports.AzureStorageBuildCacheProvider = void 0;
const rush_sdk_1 = require("@rushstack/rush-sdk");
const storage_blob_1 = require("@azure/storage-blob");
const identity_1 = require("@azure/identity");
const AzureStorageAuthentication_1 = require("./AzureStorageAuthentication");
class AzureStorageBuildCacheProvider extends AzureStorageAuthentication_1.AzureStorageAuthentication {
get isCacheWriteAllowed() {
var _a;
return (_a = rush_sdk_1.EnvironmentConfiguration.buildCacheWriteAllowed) !== null && _a !== void 0 ? _a : this._isCacheWriteAllowedByConfiguration;
}
constructor(options) {
super({
credentialUpdateCommandForLogging: `rush ${rush_sdk_1.RushConstants.updateCloudCredentialsCommandName}`,
...options
});
this._blobPrefix = options.blobPrefix;
this._environmentCredential = rush_sdk_1.EnvironmentConfiguration.buildCacheCredential;
this._readRequiresAuthentication = !!options.readRequiresAuthentication;
if (!(this._azureEnvironment in identity_1.AzureAuthorityHosts)) {
throw new Error(`The specified Azure Environment ("${this._azureEnvironment}") is invalid. If it is specified, it must ` +
`be one of: ${Object.keys(identity_1.AzureAuthorityHosts).join(', ')}`);
}
}
async tryGetCacheEntryBufferByIdAsync(terminal, cacheId) {
var _a, _b, _c, _d, _e, _f, _g, _h, _j;
const blobClient = await this._getBlobClientForCacheIdAsync(cacheId, terminal);
try {
const blobExists = await blobClient.exists();
if (blobExists) {
return await blobClient.downloadToBuffer();
}
else {
return undefined;
}
}
catch (err) {
const e = err;
const errorMessage = 'Error getting cache entry from Azure Storage: ' +
[e.name, e.message, (_a = e.response) === null || _a === void 0 ? void 0 : _a.status, (_c = (_b = e.response) === null || _b === void 0 ? void 0 : _b.parsedHeaders) === null || _c === void 0 ? void 0 : _c.errorCode]
.filter((piece) => piece)
.join(' ');
if (((_e = (_d = e.response) === null || _d === void 0 ? void 0 : _d.parsedHeaders) === null || _e === void 0 ? void 0 : _e.errorCode) === 'PublicAccessNotPermitted') {
// This error means we tried to read the cache with no credentials, but credentials are required.
// We'll assume that the configuration of the cache is correct and the user has to take action.
terminal.writeWarningLine(`${errorMessage}\n\n` +
`You need to configure Azure Storage SAS credentials to access the build cache.\n` +
`Update the credentials by running "rush ${rush_sdk_1.RushConstants.updateCloudCredentialsCommandName}", \n` +
`or provide a SAS in the ` +
`${rush_sdk_1.EnvironmentVariableNames.RUSH_BUILD_CACHE_CREDENTIAL} environment variable.`);
}
else if (((_g = (_f = e.response) === null || _f === void 0 ? void 0 : _f.parsedHeaders) === null || _g === void 0 ? void 0 : _g.errorCode) === 'AuthenticationFailed') {
// This error means the user's credentials are incorrect, but not expired normally. They might have
// gotten corrupted somehow, or revoked manually in Azure Portal.
terminal.writeWarningLine(`${errorMessage}\n\n` +
`Your Azure Storage SAS credentials are not valid.\n` +
`Update the credentials by running "rush ${rush_sdk_1.RushConstants.updateCloudCredentialsCommandName}", \n` +
`or provide a SAS in the ` +
`${rush_sdk_1.EnvironmentVariableNames.RUSH_BUILD_CACHE_CREDENTIAL} environment variable.`);
}
else if (((_j = (_h = e.response) === null || _h === void 0 ? void 0 : _h.parsedHeaders) === null || _j === void 0 ? void 0 : _j.errorCode) === 'AuthorizationPermissionMismatch') {
// This error is not solvable by the user, so we'll assume it is a configuration error, and revert
// to providing likely next steps on configuration. (Hopefully this error is rare for a regular
// developer, more likely this error will appear while someone is configuring the cache for the
// first time.)
terminal.writeWarningLine(`${errorMessage}\n\n` +
`Your Azure Storage SAS credentials are valid, but do not have permission to read the build cache.\n` +
`Make sure you have added the role 'Storage Blob Data Reader' to the appropriate user(s) or group(s)\n` +
`on your storage account in the Azure Portal.`);
}
else {
// We don't know what went wrong, hopefully we'll print something useful.
terminal.writeWarningLine(errorMessage);
}
return undefined;
}
}
async trySetCacheEntryBufferAsync(terminal, cacheId, entryStream) {
var _a, _b, _c;
if (!this.isCacheWriteAllowed) {
terminal.writeErrorLine('Writing to Azure Blob Storage cache is not allowed in the current configuration.');
return false;
}
const blobClient = await this._getBlobClientForCacheIdAsync(cacheId, terminal);
const blockBlobClient = blobClient.getBlockBlobClient();
let blobAlreadyExists = false;
try {
blobAlreadyExists = await blockBlobClient.exists();
}
catch (err) {
const e = err;
// If RUSH_BUILD_CACHE_CREDENTIAL is set but is corrupted or has been rotated
// in Azure Portal, or the user's own cached credentials have been corrupted or
// invalidated, we'll print the error and continue (this way we don't fail the
// actual rush build).
const errorMessage = 'Error checking if cache entry exists in Azure Storage: ' +
[e.name, e.message, (_a = e.response) === null || _a === void 0 ? void 0 : _a.status, (_c = (_b = e.response) === null || _b === void 0 ? void 0 : _b.parsedHeaders) === null || _c === void 0 ? void 0 : _c.errorCode]
.filter((piece) => piece)
.join(' ');
terminal.writeWarningLine(errorMessage);
}
if (blobAlreadyExists) {
terminal.writeVerboseLine('Build cache entry blob already exists.');
return true;
}
else {
try {
await blockBlobClient.upload(entryStream, entryStream.length);
return true;
}
catch (e) {
if (e.statusCode === 409 /* conflict */) {
// If something else has written to the blob at the same time,
// it's probably a concurrent process that is attempting to write
// the same cache entry. That is an effective success.
terminal.writeVerboseLine('Azure Storage returned status 409 (conflict). The cache entry has ' +
`probably already been set by another builder. Code: "${e.code}".`);
return true;
}
else {
terminal.writeWarningLine(`Error uploading cache entry to Azure Storage: ${e}`);
return false;
}
}
}
}
async _getBlobClientForCacheIdAsync(cacheId, terminal) {
const client = await this._getContainerClientAsync(terminal);
const blobName = this._blobPrefix ? `${this._blobPrefix}/${cacheId}` : cacheId;
return client.getBlobClient(blobName);
}
async _getContainerClientAsync(terminal) {
if (!this._containerClient) {
let sasString = this._environmentCredential;
if (!sasString) {
const credentialEntry = await this.tryGetCachedCredentialAsync({
expiredCredentialBehavior: 'logWarning',
terminal
});
sasString = credentialEntry === null || credentialEntry === void 0 ? void 0 : credentialEntry.credential;
}
let blobServiceClient;
if (sasString) {
const connectionString = this._getConnectionString(sasString);
blobServiceClient = storage_blob_1.BlobServiceClient.fromConnectionString(connectionString);
}
else if (!this._readRequiresAuthentication && !this._isCacheWriteAllowedByConfiguration) {
// If we don't have a credential and read doesn't require authentication, we can still read from the cache.
blobServiceClient = new storage_blob_1.BlobServiceClient(this._storageAccountUrl);
}
else {
throw new Error("An Azure Storage SAS credential hasn't been provided, or has expired. " +
`Update the credentials by running "rush ${rush_sdk_1.RushConstants.updateCloudCredentialsCommandName}", ` +
`or provide a SAS in the ` +
`${rush_sdk_1.EnvironmentVariableNames.RUSH_BUILD_CACHE_CREDENTIAL} environment variable`);
}
this._containerClient = blobServiceClient.getContainerClient(this._storageContainerName);
}
return this._containerClient;
}
_getConnectionString(sasString) {
const blobEndpoint = `BlobEndpoint=${this._storageAccountUrl}`;
if (sasString) {
const connectionString = `${blobEndpoint};SharedAccessSignature=${sasString}`;
return connectionString;
}
else {
return blobEndpoint;
}
}
}
exports.AzureStorageBuildCacheProvider = AzureStorageBuildCacheProvider;
//# sourceMappingURL=AzureStorageBuildCacheProvider.js.map
;