-
Notifications
You must be signed in to change notification settings - Fork 23
Copy location task reads source location with s3 sdk with STS #2805
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
base: development/9.5
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -3,12 +3,14 @@ const { v4: uuid } = require('uuid'); | |
|
|
||
| const { errors, jsutil, models } = require('arsenal'); | ||
| const { ObjectMD } = models; | ||
| const { S3Client: AwsS3Client, GetObjectCommand: AwsGetObjectCommand } = | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can discuss these renaming |
||
| require('@aws-sdk/client-s3'); | ||
|
|
||
| const BackbeatMetadataProxy = require('../../../lib/BackbeatMetadataProxy'); | ||
| const BackbeatTask = require('../../../lib/tasks/BackbeatTask'); | ||
| const { | ||
| const { | ||
| BackbeatRoutesClient, | ||
| GetObjectCommand, | ||
| GetObjectCommand: BackbeatRoutesGetObjectCommand, | ||
| MultipleBackendPutObjectCommand, | ||
| MultipleBackendInitiateMPUCommand, | ||
| MultipleBackendPutMPUPartCommand, | ||
|
|
@@ -24,6 +26,8 @@ const { getAccountCredentials } = | |
| require('../../../lib/credentials/AccountCredentials'); | ||
| const RoleCredentials = | ||
| require('../../../lib/credentials/RoleCredentials'); | ||
| const config = require('../../../lib/Config'); | ||
| const { authTypeAssumeRole } = require('../../../lib/constants'); | ||
| const { metricsExtension, metricsTypeQueued, metricsTypeCompleted } = | ||
| require('../constants'); | ||
|
|
||
|
|
@@ -101,6 +105,117 @@ class CopyLocationTask extends BackbeatTask { | |
| .setSourceClient(log); | ||
| } | ||
|
|
||
| /** | ||
| * Get a cached S3 client, authenticated with the assumed-role | ||
| * credentials for the role carried on a location part. | ||
| * @param {Object} locationConfig - the location's config | ||
| * @param {String} roleArn - the role ARN carried by the location part | ||
| * @param {Werelogs} log - the logger instance | ||
| * @return {AwsS3Client} the client | ||
| * @throws {ArsenalError} AccessDenied (retryable) if credentials | ||
| * could not be obtained for the role | ||
| */ | ||
| _getAssumedRoleS3Client(locationConfig, roleArn, log) { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't we have this (or similar) function in CRR ? |
||
| const { details } = locationConfig; | ||
| const s3Endpoint = `${details.transport}://${details.servers[0]}`; | ||
| const cacheKey = `${s3Endpoint}::${roleArn}`; | ||
| if (this.assumedRoleS3Clients[cacheKey]) { | ||
| return this.assumedRoleS3Clients[cacheKey]; | ||
| } | ||
| const accountId = roleArn.split(':')[4]; | ||
| const roleName = roleArn.split(':role/')[1]; | ||
| const credentials = this.assumedRoleCredentialsManager.getCredentials({ | ||
| id: roleArn, | ||
| accountId, | ||
| authConfig: { | ||
| type: authTypeAssumeRole, | ||
| roleName, | ||
| }, | ||
| stsConfig: { | ||
| endpoint: `${details.transport}://${details.sts.host}:${details.sts.port}`, | ||
| credentials: { | ||
| accessKeyId: details.sts.accessKey, | ||
| secretAccessKey: details.sts.secretKey, | ||
| }, | ||
| }, | ||
| }); | ||
| if (!credentials) { | ||
| log.error('unable to obtain assumed-role credentials for source location', { | ||
| method: 'CopyLocationTask._getAssumedRoleS3Client', | ||
| roleArn, | ||
| endpoint: s3Endpoint, | ||
| }); | ||
| const err = errors.AccessDenied.customizeDescription( | ||
| `unable to assume role ${roleArn} for isCRR source location`); | ||
| err.retryable = true; | ||
| throw err; | ||
| } | ||
| const isHttps = details.transport === 'https'; | ||
| const client = new AwsS3Client({ | ||
| endpoint: s3Endpoint, | ||
| credentials: credentials.getCredentialsProvider(), | ||
| region: 'us-east-1', | ||
| forcePathStyle: true, | ||
| requestHandler: { | ||
| [isHttps ? 'httpsAgent' : 'httpAgent']: | ||
| isHttps ? this.assumedRoleHTTPSAgent : this.assumedRoleHTTPAgent, | ||
| requestTimeout: TIMEOUT_MS, | ||
| }, | ||
|
SylvainSenechal marked this conversation as resolved.
|
||
| maxAttempts: 1, | ||
| }); | ||
| client.middlewareStack.add(isRetryableMiddleware(), { | ||
| step: 'deserialize', | ||
| priority: 'high', | ||
| }); | ||
| this.assumedRoleS3Clients[cacheKey] = client; | ||
| return this.assumedRoleS3Clients[cacheKey]; | ||
| } | ||
|
|
||
| /** | ||
| * Send a GetObject request for the object's data, | ||
| * reading either through Cloudserver's multiple-backend routes, | ||
| * or directly from a CRR source location's own S3 | ||
| * endpoint via an assumed role. | ||
| * @param {ActionQueueEntry} actionEntry - the action entry | ||
| * @param {ObjectMD} objMD - metadata object | ||
| * @param {Object} [range] - byte range to request, or undefined for the whole object | ||
| * @param {Werelogs} log - the logger instance | ||
| * @param {AbortController} abortController - abort controller for the GET request | ||
| * @return {Promise} resolves to the GetObject response | ||
| */ | ||
| async _sendGetObject(actionEntry, objMD, range, log, abortController) { | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. For those who wanna understand the pr's main idea : Before : We only used our backbeatClient (cloudserver client) to get object, that backbeat client asked cloudserver to deal with reading the data based on the location. But Cloudserver is not capable of reading data from external CRR locations. Now : When the object's location is "CRR", we use a classic S3 client with sts to directly get the data without going through cloudserver
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this function does 2 things, which are orthogonal concerns:
it would seem more appropriate to split responsability, and introduce a "getClient" function - then fallthrough to the existing code.......but I see you don't pass the same parameter, to the command: see https://github.com/scality/backbeat/pull/2805/changes#r3819951224 |
||
| const locationConfig = config.getLocationConstraint(objMD.getDataStoreName()); | ||
| if (locationConfig?.isCRR === true) { | ||
| const locations = objMD.getLocation(); | ||
| const part = locations && locations[0]; | ||
| if (!part || !part.role) { | ||
| const err = errors.AccessDenied.customizeDescription( | ||
| 'missing role on location part for isCRR source location'); | ||
| err.retryable = true; | ||
| throw err; | ||
| } | ||
|
Comment on lines
+189
to
+196
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't have the full design in mind anymore, but I think params should be in the kafka message instea? when we create the message, we will anyway parse the this also matches the current 'design' of CopyLocationTask : it copies data from (STS could even be 'triggered' not just by What do you think? Can you evaluate impact and confirm? |
||
| const s3Client = this._getAssumedRoleS3Client(locationConfig, part.role, log); | ||
| const command = new AwsGetObjectCommand({ | ||
| Bucket: part.bucket, | ||
| Key: objMD.getKey(), | ||
| VersionId: part.dataStoreVersionId, | ||
| Range: range && `bytes=${range.start}-${range.end}`, | ||
| }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the parameters should be mostly the same as the regular ones:
the only difference is the locationConstraint indeed.....and the object type AwsGetObjectCommand vs BackbeatRoutesGetObjectCommand
|
||
| return await s3Client.send(command, { abortSignal: abortController.signal }); | ||
| } | ||
|
|
||
| const { bucket, key, version } = actionEntry.getAttribute('target'); | ||
| const command = new BackbeatRoutesGetObjectCommand({ | ||
| Bucket: bucket, | ||
| Key: key, | ||
| VersionId: version, | ||
| Range: range && `bytes=${range.start}-${range.end}`, | ||
| LocationConstraint: objMD.getDataStoreName(), | ||
| RequestUids: log.getSerializedUids(), | ||
| }); | ||
| return await this.backbeatClient.send(command, { abortSignal: abortController.signal }); | ||
| } | ||
|
|
||
| processQueueEntry(actionEntry, kafkaEntry, done) { | ||
| const startTime = Date.now(); | ||
| const log = this.logger.newRequestLogger(); | ||
|
|
@@ -237,16 +352,8 @@ class CopyLocationTask extends BackbeatTask { | |
| let sourceStreamAborted = false; | ||
| let abortedByPut = false; | ||
| const abortController = new AbortController(); | ||
| const { bucket, key, version } = actionEntry.getAttribute('target'); | ||
| const getObjectCommand = new GetObjectCommand({ | ||
| Bucket: bucket, | ||
| Key: key, | ||
| VersionId: version, | ||
| LocationConstraint: objMD.getDataStoreName(), | ||
| RequestUids: log.getSerializedUids(), | ||
| }); | ||
|
|
||
| return this.backbeatClient.send(getObjectCommand, { abortSignal: abortController.signal }) | ||
| return this._sendGetObject(actionEntry, objMD, undefined, log, abortController) | ||
| .then(response => { | ||
| const incomingMsg = response.Body; | ||
| incomingMsg.on('error', err => { | ||
|
|
@@ -293,6 +400,14 @@ class CopyLocationTask extends BackbeatTask { | |
| actionEntry, objMD, size, incomingMsg, log, putDone); | ||
| }) | ||
| .catch(err => { | ||
| if (err.name === 'NoSuchVersion') { | ||
| log.info('source version no longer exists', Object.assign({ | ||
| method: 'CopyLocationTask._getAndPutObjectOnce', | ||
| error: err.message, | ||
| }, actionEntry.getLogInfo())); | ||
| return doneOnce(errors.InvalidObjectState.customizeDescription( | ||
| 'source version no longer exists')); | ||
| } | ||
| if (err.$metadata?.httpStatusCode === 404) { | ||
| log.error('the source object was not found', Object.assign({ | ||
| method: 'CopyLocationTask._getAndPutObjectOnce', | ||
|
|
@@ -307,6 +422,7 @@ class CopyLocationTask extends BackbeatTask { | |
| method: 'CopyLocationTask._getAndPutObjectOnce', | ||
| peer: this.sourceConfig.s3, | ||
| error: err.message, | ||
| errorName: err.name, | ||
| httpStatus: err.$metadata?.httpStatusCode, | ||
| }, actionEntry.getLogInfo())); | ||
| return doneOnce(err); | ||
|
|
@@ -418,27 +534,27 @@ class CopyLocationTask extends BackbeatTask { | |
| } | ||
|
|
||
| const abortController = new AbortController(); | ||
| const { bucket, key, version } = actionEntry.getAttribute('target'); | ||
| const getObjectCommand = new GetObjectCommand({ | ||
| Bucket: bucket, | ||
| Key: key, | ||
| VersionId: version, | ||
| Range: range && `bytes=${range.start}-${range.end}`, | ||
| LocationConstraint: objMD.getDataStoreName(), | ||
| RequestUids: log.getSerializedUids(), | ||
| }); | ||
|
|
||
| return this.backbeatClient.send(getObjectCommand, { abortSignal: abortController.signal }) | ||
| return this._sendGetObject(actionEntry, objMD, range, log, abortController) | ||
| .then(response => this._putMPUPart(actionEntry, objMD, response.Body, size, | ||
| uploadId, partNumber, log, abortController, done)) | ||
| .catch(err => { | ||
| if (err.name === 'NoSuchVersion') { | ||
| log.info('source version no longer exists', Object.assign({ | ||
| method: 'CopyLocationTask._getRangeAndPutMPUPartOnce', | ||
| error: err.message, | ||
| }, actionEntry.getLogInfo())); | ||
| return done(errors.InvalidObjectState.customizeDescription( | ||
| 'source version no longer exists')); | ||
| } | ||
| if (err.$metadata?.httpStatusCode === 404) { | ||
| return done(err); | ||
| } | ||
| log.error('an error occurred on getObject from S3', | ||
| Object.assign({ | ||
| method: 'CopyLocationTask._getRangeAndPutMPUPartOnce', | ||
| error: err.message, | ||
| errorName: err.name, | ||
| httpStatus: err.$metadata?.httpStatusCode, | ||
| }, actionEntry.getLogInfo())); | ||
| return done(err); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -78,6 +78,7 @@ function initManagement(params, done) { | |
| })); | ||
| const locations = require('../../conf/locationConfig.json') || {}; | ||
|
|
||
| config.setLocationConstraints(locations); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Add
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I don't think this is relevant, we don't really care bout backbeat running in orbit mode 🤔 |
||
| Object.keys(locations).forEach(locName => { | ||
| config.setIsTransientLocation( | ||
| locName, locations[locName].isTransient); | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not sure if this is the best place : queueProcessor does create a new copyLocation Task for each kafka entry so we can't put these in CopyLocationTask class otherwise its useless, but we may also have multiple instances of queue processor depending on the config which means we would have multiple credentials managers/clients for the same authentification