412 lines
13 KiB
JavaScript
412 lines
13 KiB
JavaScript
|
|
import { API_ORIGIN, APP_TYPES, PROXY_APP_ID, HSTATES, ISTATES, RSTATES } from '../constants.js';
|
|
import { eachLimit } from 'async';
|
|
import { fetcher } from 'pankow';
|
|
import { sleep } from 'pankow/utils';
|
|
import moment from 'moment-timezone';
|
|
import DashboardModel from './DashboardModel.js';
|
|
import ProfileModel from './ProfileModel.js';
|
|
|
|
const dashboardModel = DashboardModel.create();
|
|
const profileModel = ProfileModel.create();
|
|
|
|
function installationStateLabel(app) {
|
|
if (!app) return '';
|
|
|
|
const waiting = app.progress === 0 ? ' (Queued)' : '';
|
|
|
|
switch (app.installationState) {
|
|
case ISTATES.PENDING_INSTALL:
|
|
return 'Installing' + waiting;
|
|
case ISTATES.PENDING_CLONE:
|
|
return 'Cloning' + waiting;
|
|
case ISTATES.PENDING_LOCATION_CHANGE:
|
|
case ISTATES.PENDING_CONFIGURE:
|
|
case ISTATES.PENDING_RECREATE_CONTAINER:
|
|
case ISTATES.PENDING_SERVICES_CHANGE:
|
|
case ISTATES.PENDING_DEBUG:
|
|
return 'Configuring' + waiting;
|
|
case ISTATES.PENDING_RESIZE:
|
|
return 'Resizing' + waiting;
|
|
case ISTATES.PENDING_DATA_DIR_MIGRATION:
|
|
return 'Migrating data' + waiting;
|
|
case ISTATES.PENDING_UNINSTALL: return 'Uninstalling' + waiting;
|
|
case ISTATES.PENDING_RESTORE: return 'Restoring' + waiting;
|
|
case ISTATES.PENDING_IMPORT: return 'Importing' + waiting;
|
|
case ISTATES.PENDING_UPDATE: return 'Updating' + waiting;
|
|
case ISTATES.PENDING_BACKUP: return 'Backing up' + waiting;
|
|
case ISTATES.PENDING_START: return 'Starting' + waiting;
|
|
case ISTATES.PENDING_STOP: return 'Stopping' + waiting;
|
|
case ISTATES.PENDING_RESTART: return 'Restarting' + waiting;
|
|
case ISTATES.ERROR: {
|
|
if (app.error && app.error.message === 'ETRYAGAIN') return 'DNS Error';
|
|
return 'Error';
|
|
}
|
|
case ISTATES.INSTALLED: {
|
|
if (app.debugMode) {
|
|
return 'Recovery Mode';
|
|
} else if (app.runState === RSTATES.RUNNING) {
|
|
if (!app.health) return 'Starting...'; // no data yet
|
|
if (app.type === APP_TYPES.LINK) return '';
|
|
if (app.health === HSTATES.HEALTHY) return 'Running';
|
|
return 'Not responding'; // dead/exit/unhealthy
|
|
} else if (app.runState === RSTATES.STOPPED) {
|
|
return 'Stopped';
|
|
} else {
|
|
return app.runState;
|
|
}
|
|
}
|
|
default: return app.installationState;
|
|
}
|
|
}
|
|
|
|
function installationActive(app) {
|
|
if (app.installationState === ISTATES.ERROR) return false;
|
|
if (app.installationState === ISTATES.INSTALLED) return false;
|
|
return true;
|
|
}
|
|
|
|
function appProgressMessage(app) {
|
|
return app.message || (app.error ? app.error.message : '');
|
|
}
|
|
|
|
function create() {
|
|
const accessToken = localStorage.token;
|
|
|
|
// TODO maybe we can share those globally
|
|
let config = null;
|
|
let profile = null;
|
|
|
|
async function loadConfigAndProfile() {
|
|
let [error, result] = await dashboardModel.config();
|
|
if (error) return console.error(error);
|
|
config = result;
|
|
|
|
[error, result] = await profileModel.get();
|
|
if (error) return console.error(error);
|
|
profile = result;
|
|
}
|
|
|
|
async function getTask(appId) {
|
|
let error, result;
|
|
try {
|
|
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${appId}/task`, { access_token: accessToken });
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
if (error || result.status !== 200) {
|
|
console.error('Failed to get task for app.', error || result.status);
|
|
return null;
|
|
}
|
|
|
|
return result.body;
|
|
}
|
|
|
|
async function postProcess(app) {
|
|
if (!profile || !config) await loadConfigAndProfile();
|
|
|
|
app.ssoAuth = app.sso && (app.manifest.addons['ldap'] || app.manifest.addons['oidc'] || app.manifest.addons['proxyAuth']); // checking app.sso first ensures app.manifest.addons is not null
|
|
app.type = app.manifest.id === PROXY_APP_ID ? APP_TYPES.PROXIED : APP_TYPES.APP;
|
|
app.iconUrl = app.iconUrl ? `${API_ORIGIN}${app.iconUrl}?ts=${new Date(app.ts).getTime()}` : `${API_ORIGIN}/img/appicon_fallback.png`; // calculate full icon url with cache busting
|
|
|
|
// only fetch if we have permissions and a taskId is set/active
|
|
if (!app.taskId || (app.accessLevel !== 'operator' && app.accessLevel !== 'admin')) {
|
|
app.progress = 0;
|
|
app.message = '';
|
|
app.taskMinutesActive = 0;
|
|
} else {
|
|
const task = await getTask(app.id);
|
|
if (task) {
|
|
app.progress = task.percent;
|
|
app.message = task.message;
|
|
app.taskMinutesActive = moment.duration(moment.utc().diff(moment.utc(task.creationTime))).asMinutes();
|
|
} else {
|
|
app.progress = 0;
|
|
app.message = '';
|
|
app.taskMinutesActive = 0;
|
|
}
|
|
}
|
|
|
|
if (app.manifest.postInstallMessage) {
|
|
let text = app.manifest.postInstallMessage;
|
|
// we chose - because underscore has special meaning in markdown
|
|
text = text.replace(/\$CLOUDRON-APP-LOCATION/g, app.subdomain);
|
|
text = text.replace(/\$CLOUDRON-APP-DOMAIN/g, app.domain);
|
|
text = text.replace(/\$CLOUDRON-APP-FQDN/g, app.fqdn);
|
|
text = text.replace(/\$CLOUDRON-APP-ORIGIN/g, 'https://' + app.fqdn);
|
|
text = text.replace(/\$CLOUDRON-API-DOMAIN/g, config.adminFqdn);
|
|
text = text.replace(/\$CLOUDRON-API-ORIGIN/g, 'https://' + config.adminFqdn);
|
|
text = text.replace(/\$CLOUDRON-USERNAME/g, profile.username);
|
|
text = text.replace(/\$CLOUDRON-APP-ID/g, app.id);
|
|
|
|
// [^] matches even newlines. '?' makes it non-greedy
|
|
if (app.sso) text = text.replace(/<nosso>[^]*?<\/nosso>/g, '');
|
|
else text = text.replace(/<sso>[^]*?<\/sso>/g, '');
|
|
|
|
app.manifest.postInstallMessage = text.trim();
|
|
}
|
|
|
|
return app;
|
|
}
|
|
|
|
return {
|
|
name: 'AppsModel',
|
|
getTask,
|
|
isStopped(app) {
|
|
if (app.installationState === ISTATES.PENDING_START || app.installationState === ISTATES.PENDING_STOP) {
|
|
return app.installationState === ISTATES.PENDING_START;
|
|
} else {
|
|
return app.runState === RSTATES.STOPPED;
|
|
}
|
|
},
|
|
async install(manifest, config) {
|
|
const data = {
|
|
appStoreId: manifest.id + '@' + manifest.version,
|
|
subdomain: config.subdomain,
|
|
domain: config.domain,
|
|
secondaryDomains: config.secondaryDomains,
|
|
ports: config.ports,
|
|
accessRestriction: config.accessRestriction,
|
|
cert: config.cert,
|
|
key: config.key,
|
|
sso: config.sso,
|
|
overwriteDns: config.overwriteDns,
|
|
upstreamUri: config.upstreamUri,
|
|
backupId: config.backupId // when restoring from archive
|
|
};
|
|
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps`, data, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 202) return [result];
|
|
|
|
return [null];
|
|
},
|
|
async list() {
|
|
let error, result;
|
|
try {
|
|
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps`, { access_token: accessToken });
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
if (error || result.status !== 200) return [error || result];
|
|
|
|
await eachLimit(result.body.apps, 10, postProcess);
|
|
|
|
return [null, result.body.apps];
|
|
},
|
|
async get(id) {
|
|
let error, result;
|
|
try {
|
|
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}`, { access_token: accessToken });
|
|
} catch (e) {
|
|
error = e;
|
|
}
|
|
|
|
if (error || result.status !== 200) return [error || result];
|
|
|
|
return [null, await postProcess(result.body)];
|
|
},
|
|
async restart(id) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/restart`, null, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 202) return [result];
|
|
|
|
while(true) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}`, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 200) return [result];
|
|
|
|
// are we done here?
|
|
if (result.body.installationState !== ISTATES.INSTALLED) await sleep(2000);
|
|
else return [null];
|
|
}
|
|
},
|
|
async start(id) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/start`, {}, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 202) return [result];
|
|
return [null, result.body.taskId];
|
|
|
|
},
|
|
async stop(id) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/stop`, {}, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 202) return [result];
|
|
return [null, result.body.taskId];
|
|
|
|
},
|
|
async configure(id, setting, data) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/configure/${setting}`, data, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 200 && result.status !== 202) return [result];
|
|
return [null];
|
|
},
|
|
async uninstall(id) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/uninstall`, {}, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 202) return [result];
|
|
return [null];
|
|
},
|
|
async getEvents(id) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}/eventlog`, { page: 1, per_page: 100, access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 200) return [result];
|
|
return [null, result.body.eventlogs];
|
|
},
|
|
async checkForUpdates(id) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/check_for_updates`, {}, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 200) return [result];
|
|
return [null, result.body.update];
|
|
},
|
|
async update(id, manifest, skipBackup = false) {
|
|
const data = {
|
|
appStoreId: `${manifest.id}@${manifest.version}`,
|
|
skipBackup: !!skipBackup,
|
|
};
|
|
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/update`, data, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 202) return [result];
|
|
return [null];
|
|
},
|
|
async backups(id) {
|
|
// we fetch probably enough to avoid pagination
|
|
let result;
|
|
try {
|
|
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}/backups`, { page: 1, per_page: 100, access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 200) return [result];
|
|
return [null, result.body.backups];
|
|
},
|
|
async backup(id) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/backup`, {}, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 202) return [result];
|
|
return [null];
|
|
},
|
|
async updateBackup(id, backupId, label, preserveSecs) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/backups/${backupId}`, { label, preserveSecs }, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 200) return [result];
|
|
return [null];
|
|
},
|
|
async restore(id, backupId) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/restore`, { backupId }, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 202) return [result];
|
|
return [null];
|
|
},
|
|
async graphs(id, fromMinutes) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.get(`${API_ORIGIN}/api/v1/apps/${id}/graphs`, { fromMinutes, access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 200) return [result];
|
|
return [null, result.body];
|
|
},
|
|
async repair(id, data) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.post(`${API_ORIGIN}/api/v1/apps/${id}/repair`, data, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 200 && result.status !== 202) return [result];
|
|
return [null, result.body];
|
|
},
|
|
async ackChecklistItem(id, key, done) {
|
|
let result;
|
|
try {
|
|
result = await fetcher.put(`${API_ORIGIN}/api/v1/apps/${id}/checklist/${key}`, { done }, { access_token: accessToken });
|
|
} catch (e) {
|
|
return [e];
|
|
}
|
|
|
|
if (result.status !== 202) return [result];
|
|
return [null];
|
|
},
|
|
};
|
|
}
|
|
|
|
export default {
|
|
create,
|
|
installationStateLabel,
|
|
installationActive,
|
|
appProgressMessage,
|
|
};
|