migrate app dataDir to volumes
This commit is contained in:
67
src/apps.js
67
src/apps.js
@@ -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 };
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user