UNPKG

@cloud-copilot/iam-collect

Version:

Collect IAM information from AWS Accounts

182 lines 7.66 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.S3PathBasedPersistenceAdapter = void 0; const client_s3_1 = require("@aws-sdk/client-s3"); const iam_utils_1 = require("@cloud-copilot/iam-utils"); const auth_js_1 = require("../../aws/auth.js"); const ClientPool_js_1 = require("../../aws/ClientPool.js"); const coreAuth_js_1 = require("../../aws/coreAuth.js"); const client_tools_js_1 = require("../../utils/client-tools.js"); const log_js_1 = require("../../utils/log.js"); class S3PathBasedPersistenceAdapter { storageConfig; storageAuthAccountId; constructor(storageConfig) { this.storageConfig = storageConfig; } async getClient() { /* * We have a bit of a bootstrap problem with S3 auth similar to how we set up * initial download. We don't necessarily know the accountId unless there is an * arn specified. Otherwise we just have to use the default credentials and see * what we get back. */ if (!this.storageAuthAccountId) { const authConfig = this.storageConfig.auth; // Try getting it from the auth config if (authConfig && authConfig.initialRole && 'arn' in authConfig.initialRole) { this.storageAuthAccountId = (0, iam_utils_1.splitArnParts)(authConfig.initialRole.arn).accountId; } // If that doesn't work get it from the default credentials if (!this.storageAuthAccountId) { const initialCredentials = await (0, coreAuth_js_1.getNewInitialCredentials)(authConfig, { phase: 's3 persistence auth bootstrap' }); this.storageAuthAccountId = initialCredentials.accountId; } } const credentials = await (0, auth_js_1.getCredentials)(this.storageAuthAccountId, this.storageConfig.auth); const client = ClientPool_js_1.AwsClientPool.defaultInstance.client(client_s3_1.S3Client, credentials, this.storageConfig.region, this.storageConfig.endpoint); return client; } async writeFile(filePath, data) { const client = await this.getClient(); await client.send(new client_s3_1.PutObjectCommand({ Bucket: this.storageConfig.bucket, Key: filePath, Body: data })); } async writeWithOptimisticLock(filePath, data, lockId) { const client = await this.getClient(); try { const params = { Bucket: this.storageConfig.bucket, Key: filePath, Body: data }; // The API will throw an error if Etag is an empty string if (lockId && lockId.trim() !== '') { params['IfMatch'] = lockId; } await client.send(new client_s3_1.PutObjectCommand(params)); return true; } catch (error) { if (error.name === 'PreconditionFailed' || error.name === 'ConditionalRequestConflict') { log_js_1.log.debug({ filePath, lockId }, 'Optimistic locking failed. The object may have been modified by another process.'); // PreconditionFailed indicates that the ETag does not match the current version of the object // ConditionalRequestConflict indicates a conflicting operation occurred during the request // In either case, we can return false and let the caller handle the conflict return false; } // If the error is not related to optimistic locking, it's someone else's problem throw error; } } async readFile(filePath) { const response = await this.readFileWithHash(filePath); return response?.data; } async readFileWithHash(filePath) { const client = await this.getClient(); const response = await (0, client_tools_js_1.runAndCatch404)(async () => { return client.send(new client_s3_1.GetObjectCommand({ Bucket: this.storageConfig.bucket, Key: filePath })); }); if (!response) { return undefined; } const data = await response.Body?.transformToString(); const hash = response.ETag; return { data, hash }; } async deleteFile(filePath) { const client = await this.getClient(); await client.send(new client_s3_1.DeleteObjectCommand({ Bucket: this.storageConfig.bucket, Key: filePath })); } async deleteDirectory(dirPath) { const client = await this.getClient(); if (!dirPath.endsWith('/')) { dirPath += '/'; } // This code is a little less elegant than most, but given the possibility of // pagination combined with the need to batch delete, it's worth the tradeoff let ContinuationToken = undefined; do { // 1) List objects in the directory const list = await client.send(new client_s3_1.ListObjectsV2Command({ Bucket: this.storageConfig.bucket, Prefix: dirPath, ContinuationToken })); const objects = list.Contents ?? []; if (objects.length > 0) { // 2) Delete them in one batch const toDelete = objects.map((o) => ({ Key: o.Key })); await client.send(new client_s3_1.DeleteObjectsCommand({ Bucket: this.storageConfig.bucket, Delete: { Objects: toDelete, Quiet: true } })); } // 3) If more remain, loop ContinuationToken = list.IsTruncated ? list.NextContinuationToken : undefined; } while (ContinuationToken); } async listDirectory(dirPath) { const client = await this.getClient(); if (!dirPath.endsWith('/')) { dirPath += '/'; } const response = await client.send(new client_s3_1.ListObjectsV2Command({ Bucket: this.storageConfig.bucket, Prefix: dirPath, Delimiter: '/' })); if (!response.CommonPrefixes) { return []; } return (response.CommonPrefixes.map((cp) => cp.Prefix) .filter((key) => key !== dirPath) //Trim off the prefix of the directory being listed and the trailing slash .map((key) => key.slice(dirPath.length).slice(0, -1))); } async findWithPattern(baseDir, pathParts, filename) { let baseDirs = [baseDir]; for (const part of pathParts) { if (part == '*') { const subDirs = []; for (const dir of baseDirs) { const entries = await this.listDirectory(dir); for (const entry of entries) { if (entry !== filename) { subDirs.push(dir + '/' + entry); } } } baseDirs = subDirs; } else { baseDirs = baseDirs.map((dir) => dir + '/' + part); } } const results = []; for (const dir of baseDirs) { const data = await this.readFile(dir + '/' + filename); if (data) { results.push(data); } } return results; } } exports.S3PathBasedPersistenceAdapter = S3PathBasedPersistenceAdapter; //# sourceMappingURL=S3PathBasedPersistenceAdapter.js.map