Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
7 changes: 7 additions & 0 deletions conf/locationConfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,5 +42,12 @@
"legacyAwsBehavior": false,
"isCold": true,
"details": {}
},
"location-crr-source": {
"type": "scality",
"objectId": "location-crr-source",
"legacyAwsBehavior": false,
"isCRR": true,
"details": {}
}
}
10 changes: 10 additions & 0 deletions docker-entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,16 @@ if [[ "$EXTENSIONS_REPLICATION_DEST_BOOTSTRAPLIST" ]]; then
fi
fi

# Clean room: localize objects whose data still lives on the source (isCRR)
# location. Setting the target location enables the trigger.
if [[ "$EXTENSIONS_REPLICATION_LOCALIZATION_TO_LOCATION" ]]; then
JQ_FILTERS_CONFIG="$JQ_FILTERS_CONFIG | .extensions.replication.localization.toLocation=\"$EXTENSIONS_REPLICATION_LOCALIZATION_TO_LOCATION\""
fi

if [[ "$EXTENSIONS_REPLICATION_LOCALIZATION_RESULTS_TOPIC" ]]; then
JQ_FILTERS_CONFIG="$JQ_FILTERS_CONFIG | .extensions.replication.localization.resultsTopic=\"$EXTENSIONS_REPLICATION_LOCALIZATION_RESULTS_TOPIC\""
fi

# START Retry config

# AWS_S3
Expand Down
1 change: 1 addition & 0 deletions extensions/lifecycle/LifecycleConfigValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ const joiSchema = joi.object({
concurrency: joi.number().greater(0).default(10),
maxQueued: joi.number().greater(0).default(MAX_QUEUED_DEFAULT),
probeServer: probeServerJoi.default(),
vaultAdmin: hostPortJoi,
circuitBreaker: joi.object().optional(),
},
coldStorageArchiveTopicPrefix: joi.string().default('cold-archive-req-'),
Expand Down
84 changes: 83 additions & 1 deletion extensions/lifecycle/objectProcessor/LifecycleObjectProcessor.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,15 @@

const { EventEmitter } = require('events');
const Logger = require('werelogs').Logger;
const { errors } = require('arsenal');

const BackbeatConsumerManager = require('../../../lib/BackbeatConsumerManager');
const ActionQueueEntry = require('../../../lib/models/ActionQueueEntry');
const ClientManager = require('../../../lib/clients/ClientManager');
const BackbeatTask = require('../../../lib/tasks/BackbeatTask');
const VaultClientWrapper = require('../../utils/VaultClientWrapper');
const { AccountIdCache } = require('../../utils/AccountIdCache');
const { authTypeAssumeRole } = require('../../../lib/constants');

const logIdFromType = {
'object-processor': 'Backbeat:Lifecycle:ObjectProcessor',
Expand Down Expand Up @@ -61,9 +65,81 @@ class LifecycleObjectProcessor extends EventEmitter {
transport,
}, this._log);

this.vaultClientWrapper = new VaultClientWrapper(
`lifecycle:${this.getProcessorType()}`,
this._processConfig.vaultAdmin,
this.getAuthConfig(this._lcConfig),
this._log,
);
this._accountIdCache = new AccountIdCache(
this._processConfig.concurrency);

this.retryWrapper = new BackbeatTask(this._processConfig.retry);
}

/**
* Whether this processor can resolve canonical ids through Vault. Only the
* transition processor receives actions published without an account id
* (clean room localization), and only it is configured with a Vault admin
* endpoint - so leave the Vault client alone everywhere else.
* @return {Boolean} true if account id lookups are available
*/
_accountIdLookupEnabled() {
const authConfig = this.getAuthConfig(this._lcConfig);
return authConfig.type === authTypeAssumeRole &&
!!(this._processConfig.vaultAdmin || authConfig.vault);
}

/**
* Resolve the account id of a canonical id. Actions published by the
* lifecycle conductor already carry the account id; those published by the
* queue populator (clean room localization) only know the canonical id.
* @param {String} ownerId - canonical id of the object owner
* @param {Logger} log - logger instance
* @param {Function} cb - callback: cb(err, accountId)
* @return {undefined}
*/
getAccountId(ownerId, log, cb) {
if (this.getAuthConfig(this._lcConfig).type !== authTypeAssumeRole) {
log.debug('skipping: not assume role auth type');
return process.nextTick(cb);
}

if (!this._accountIdLookupEnabled()) {
log.error('cannot resolve canonical id: no vault endpoint configured');
return process.nextTick(cb, errors.InternalError.customizeDescription(
'account id resolution requires a vault endpoint'));
}

// A cached miss must fail like a fresh lookup would: `isKnown()` is also
// true for misses, and `get()` would then hand back `undefined`.
if (this._accountIdCache.isMiss(ownerId)) {
log.error('canonical id does not exist (cached)', { ownerId });
return process.nextTick(cb, errors.NoSuchEntity);
}

if (this._accountIdCache.has(ownerId)) {
return process.nextTick(cb, null, this._accountIdCache.get(ownerId));
}

return this.vaultClientWrapper.getAccountId(ownerId, (err, accountId) => {
if (err) {
if (err.NoSuchEntity) {
log.error('canonical id does not exist', { error: err, ownerId });
this._accountIdCache.miss(ownerId);
} else {
log.error('could not get account id', { error: err, ownerId });
}
return cb(err);
}

this._accountIdCache.set(ownerId, accountId);
this._accountIdCache.expireOldest();

return cb(null, accountId);
});
}

getProcessorType() {
return 'object-processor';
}
Expand Down Expand Up @@ -130,6 +206,9 @@ class LifecycleObjectProcessor extends EventEmitter {
start(done) {
this.clientManager.initSTSConfig();
this.clientManager.initCredentialsManager();
if (this._accountIdLookupEnabled()) {
this.vaultClientWrapper.init();
}
this._setupConsumers(done);
}

Expand Down Expand Up @@ -225,12 +304,15 @@ class LifecycleObjectProcessor extends EventEmitter {
this.clientManager.getBackbeatClient.bind(this.clientManager),
getBackbeatMetadataProxy:
this.clientManager.getBackbeatMetadataProxy.bind(this.clientManager),
getAccountId: this.getAccountId.bind(this),
logger: this._log,
};
}

isReady() {
return this._consumers && this._consumers.isReady();
return this._consumers && this._consumers.isReady() &&
(!this._accountIdLookupEnabled() ||
this.vaultClientWrapper.tempCredentialsReady());
}
}

Expand Down
17 changes: 14 additions & 3 deletions extensions/lifecycle/tasks/LifecycleTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ const errorTransitionInProgress = errors.InternalError.
customizeDescription('transition is currently in progress');
const errorTransitionColdObject = errors.InternalError.
customizeDescription('transitioning a cold object is forbidden');
const errorTransitionNonLocalizedObject = errors.InternalError.
customizeDescription('transitioning a non-localized object is forbidden');
const errorObjectTemporarilyRestored = errors.InternalError.
customizeDescription('object temporarily restored');
const errorReplicationInProgress = errors.InternalError.
Expand Down Expand Up @@ -1270,12 +1272,21 @@ class LifecycleTask extends BackbeatTask {
return next(errorReplicationInProgress);
}
const dataStoreName = objectMD.getDataStoreName();
const isObjectCold = dataStoreName && locationsConfig[dataStoreName]
&& locationsConfig[dataStoreName].isCold;
const locationConfig = (dataStoreName
&& locationsConfig[dataStoreName]) || {};
// We do not transition cold objects
if (isObjectCold) {
if (locationConfig.isCold) {
return next(errorTransitionColdObject);
}
// Clean room: the object data still lives on the source
// (isCRR) location. Localization is the only valid transition
// out of such a location, and it is triggered by its own path
// (the queue populator), not by lifecycle rules. The version
// is simply re-evaluated on a later scan, once localization
// has completed.
if (locationConfig.isCRR) {
return next(errorTransitionNonLocalizedObject);
}
// If transition is in progress, do not re-publish entry
// to data-mover or cold-archive topic.
if (objectMD.getTransitionInProgress()) {
Expand Down
44 changes: 40 additions & 4 deletions extensions/lifecycle/tasks/LifecycleUpdateTransitionTask.js
Original file line number Diff line number Diff line change
Expand Up @@ -252,6 +252,36 @@ class LifecycleUpdateTransitionTask extends BackbeatTask {
], done);
}

/**
* Actions published by the lifecycle conductor carry the account id;
* those published by the queue populator (clean room localization) only
* know the object owner's canonical id. Resolve it once, up-front, so the
* rest of the task - and the garbage collection entry it emits - can use
* `target.accountId` as usual.
* @param {ActionQueueEntry} entry - action entry to execute
* @param {Logger} log - logger instance
* @param {Function} cb - callback function
* @return {undefined}
*/
_resolveAccountId(entry, log, cb) {
const { accountId, owner } = this.getTargetAttribute(entry);
if (accountId || !owner) {
return process.nextTick(cb);
}

log.debug('no account id in entry, resolving from canonical id',
{ owner });
return this.getAccountId(owner, log, (err, resolvedAccountId) => {
if (err) {
return cb(err);
}
if (resolvedAccountId) {
entry.setAttribute('target.accountId', resolvedAccountId);
}
return cb();
});
}

/**
*
* @param {ActionQueueEntry} entry - action entry to execute
Expand All @@ -268,11 +298,17 @@ class LifecycleUpdateTransitionTask extends BackbeatTask {
lastModified: 'target.lastModified',
});
log.addDefaultFields(entry.getLogInfo());
if (entry.getStatus() === 'success') {
return this.handleSuccessfullTransition(entry, log, done);
}

return this.handleFailedTransition(entry, log, done);
return this._resolveAccountId(entry, log, err => {
if (err) {
return done(err);
}
if (entry.getStatus() === 'success') {
return this.handleSuccessfullTransition(entry, log, done);
}

return this.handleFailedTransition(entry, log, done);
});
}
}

Expand Down
7 changes: 7 additions & 0 deletions extensions/replication/ReplicationConfigValidator.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,13 @@ const joiSchema = joi.object({
probeServer: probeServerPerSite,
}).optional(),
objectSizeMetrics: joi.array().items(joi.number()).default(OBJECT_SIZE_METRICS),
// Clean room: localization of objects whose data still lives on the source
// (isCRR) location. Enabled by setting `toLocation`.
localization: joi.object({
toLocation: joi.string().required(),
resultsTopic: joi.string()
.default('backbeat-lifecycle-transition-tasks'),
}).optional(),
});

/**
Expand Down
Loading
Loading