migrate app dataDir to volumes

This commit is contained in:
Girish Ramakrishnan
2022-06-01 22:44:52 -07:00
parent 8fc8128957
commit dddc5a1994
12 changed files with 117 additions and 62 deletions

View File

@@ -42,7 +42,7 @@ exports = module.exports = {
setMailbox,
setInbox,
setLocation,
setDataDir,
setStorage,
repair,
restore,
@@ -81,7 +81,7 @@ exports = module.exports = {
schedulePendingTasks,
restartAppsUsingAddons,
getDataDir,
getStorageDir,
getIcon,
getMemoryLimit,
getLimits,
@@ -178,6 +178,7 @@ const appstore = require('./appstore.js'),
util = require('util'),
uuid = require('uuid'),
validator = require('validator'),
volumes = require('./volumes.js'),
_ = require('underscore');
const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationState', 'apps.errorJson', 'apps.runState',
@@ -186,7 +187,7 @@ const APPS_FIELDS_PREFIXED = [ 'apps.id', 'apps.appStoreId', 'apps.installationS
'apps.sso', 'apps.debugModeJson', 'apps.enableBackup', 'apps.proxyAuth', 'apps.containerIp', 'apps.crontab',
'apps.creationTime', 'apps.updateTime', 'apps.enableAutomaticUpdate',
'apps.enableMailbox', 'apps.mailboxDisplayName', 'apps.mailboxName', 'apps.mailboxDomain', 'apps.enableInbox', 'apps.inboxName', 'apps.inboxDomain',
'apps.dataDir', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(',');
'apps.storageVolumeId', 'apps.storageVolumePrefix', 'apps.ts', 'apps.healthTime', '(apps.icon IS NOT NULL) AS hasIcon', '(apps.appStoreIcon IS NOT NULL) AS hasAppStoreIcon' ].join(',');
// const PORT_BINDINGS_FIELDS = [ 'hostPort', 'type', 'environmentVariable', 'appId' ].join(',');
@@ -477,28 +478,15 @@ function validateEnv(env) {
return null;
}
function validateDataDir(dataDir) {
if (dataDir === null) return null;
function validateStorage(volume, prefix) {
assert.strictEqual(typeof volume, 'object');
assert.strictEqual(typeof prefix, 'string');
if (!path.isAbsolute(dataDir)) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not an absolute path`);
if (dataDir.endsWith('/')) return new BoxError(BoxError.BAD_FIELD, `${dataDir} contains trailing slash`);
if (path.normalize(dataDir) !== dataDir) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not a normalized path`);
// TODO: check the volume type
// nfs shares will have the directory mounted already
let stat = safe.fs.lstatSync(dataDir);
if (stat) {
if (!stat.isDirectory()) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not a directory`);
let entries = safe.fs.readdirSync(dataDir);
if (!entries) return new BoxError(BoxError.BAD_FIELD, `${dataDir} could not be listed`);
if (entries.length !== 0) return new BoxError(BoxError.BAD_FIELD, `${dataDir} is not empty. If this is the root of a mounted volume, provide a subdirectory.`);
}
// backup logic relies on paths not overlapping (because it recurses)
if (dataDir.startsWith(paths.APPS_DATA_DIR)) return new BoxError(BoxError.BAD_FIELD, `${dataDir} cannot be inside apps data`);
// if we made it this far, it cannot start with any of these realistically
const fhs = [ '/bin', '/boot', '/etc', '/lib', '/lib32', '/lib64', '/proc', '/run', '/sbin', '/tmp', '/usr' ];
if (fhs.some((p) => dataDir.startsWith(p))) return new BoxError(BoxError.BAD_FIELD, `${dataDir} cannot be placed inside this location`);
if (path.isAbsolute(prefix)) return new BoxError(BoxError.BAD_FIELD, `prefix "${prefix}" must be a relative path`);
if (prefix.endsWith('/')) return new BoxError(BoxError.BAD_FIELD, `prefix "${prefix}" contains trailing slash`);
if (path.normalize(prefix) !== prefix) return new BoxError(BoxError.BAD_FIELD, `prefix "${prefix}" is not a normalized path`);
return null;
}
@@ -530,17 +518,20 @@ function getDuplicateErrorDetails(errorMessage, locations, domainObjectMap, port
if (portBindings[portName] === parseInt(match[1])) return new BoxError(BoxError.ALREADY_EXISTS, `Port ${match[1]} is in use`);
}
if (match[2] === 'dataDir') {
return new BoxError(BoxError.BAD_FIELD, `Data directory ${match[1]} is in use`);
if (match[2] === 'apps_storageVolume') {
return new BoxError(BoxError.BAD_FIELD, `Storage directory ${match[1]} is in use`);
}
return new BoxError(BoxError.ALREADY_EXISTS, `${match[2]} '${match[1]}' is in use`);
}
async function getDataDir(app, dataDir) {
assert(dataDir === null || typeof dataDir === 'string');
async function getStorageDir(app) {
assert.strictEqual(typeof app, 'object');
return dataDir || path.join(paths.APPS_DATA_DIR, app.id, 'data');
if (!app.storageVolumeId) return path.join(paths.APPS_DATA_DIR, app.id, 'data');
const volume = await volumes.get(app.storageVolumeId);
if (!volume) throw new BoxError(BoxError.NOT_FOUND, 'Volume not found'); // not possible
return path.join(volume.hostPath, app.storageVolumePrefix);
}
function removeInternalFields(app) {
@@ -549,7 +540,7 @@ function removeInternalFields(app) {
'subdomain', 'domain', 'fqdn', 'crontab',
'accessRestriction', 'manifest', 'portBindings', 'iconUrl', 'memoryLimit', 'cpuShares', 'operators',
'sso', 'debugMode', 'reverseProxyConfig', 'enableBackup', 'creationTime', 'updateTime', 'ts', 'tags',
'label', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'dataDir', 'mounts',
'label', 'secondaryDomains', 'redirectDomains', 'aliasDomains', 'env', 'enableAutomaticUpdate', 'storageVolumeId', 'storageVolumePrefix', 'mounts',
'enableMailbox', 'mailboxDisplayName', 'mailboxName', 'mailboxDomain', 'enableInbox', 'inboxName', 'inboxDomain');
}
@@ -1794,25 +1785,31 @@ async function setLocation(app, data, auditSource) {
return { taskId };
}
async function setDataDir(app, dataDir, auditSource) {
async function setStorage(app, volumeId, volumePrefix, auditSource) {
assert.strictEqual(typeof app, 'object');
assert(dataDir === null || typeof dataDir === 'string');
assert(volumeId === null || typeof volumeId === 'string');
assert(volumePrefix === null || typeof volumePrefix === 'string');
assert.strictEqual(typeof auditSource, 'object');
const appId = app.id;
let error = checkAppState(app, exports.ISTATE_PENDING_DATA_DIR_MIGRATION);
if (error) throw error;
error = validateDataDir(dataDir);
if (error) throw error;
if (volumeId) {
const volume = await volumes.get(volumeId);
if (volume === null) return new BoxError(BoxError.BAD_FIELD, 'Storage volume not found');
error = validateStorage(volume, volumePrefix);
if (error) throw error;
}
const task = {
args: { newDataDir: dataDir },
args: { newStorageVolumeId: volumeId, newStorageVolumePrefix: volumePrefix },
values: {}
};
const taskId = await addTask(appId, exports.ISTATE_PENDING_DATA_DIR_MIGRATION, task, auditSource);
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, dataDir, taskId });
await eventlog.add(eventlog.ACTION_APP_CONFIGURE, auditSource, { appId, app, volumeId, volumePrefix, taskId });
return { taskId };
}