Compare commits
64 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 848b745fcb | |||
| 9a35c40b24 | |||
| 1f1e6124cd | |||
| 033df970ad | |||
| dd80a795a0 | |||
| 1eec6a39c6 | |||
| dd6b8face9 | |||
| 288de7e03a | |||
| a760ef4d22 | |||
| 0dd745bce4 | |||
| d4d5d371ac | |||
| 205bf4ddbd | |||
| 4ab84d42c6 | |||
| ee74badf3a | |||
| aa173ff74c | |||
| b584fc33f5 | |||
| 15c9d8682e | |||
| 361be8c26b | |||
| 4db9a5edd6 | |||
| bcc878da43 | |||
| 79f179fed4 | |||
| a924a9a627 | |||
| 45d444df0e | |||
| 92461a3366 | |||
| 032a430c51 | |||
| a6a3855e79 | |||
| 2386545814 | |||
| 2059152dd3 | |||
| 32d2c260ab | |||
| 384c7873aa | |||
| 9266302c4c | |||
| 755dce7bc4 | |||
| dd3e38ae55 | |||
| 9dfaa2d20f | |||
| d6a4ff23e2 | |||
| c2ab7e2c1f | |||
| b9e4662dbb | |||
| 10df0a527f | |||
| 9aad3688e1 | |||
| e78dbcb5d4 | |||
| 5e8cd09f51 | |||
| 22f65a9364 | |||
| 81b7432044 | |||
| d49b90d9f2 | |||
| 9face9cf35 | |||
| 33ac34296e | |||
| 670ffcd489 | |||
| ec7b365c31 | |||
| 433d78c7ff | |||
| ed041fdca6 | |||
| b8e4ed2369 | |||
| d12f260d12 | |||
| ba7989b57b | |||
| 88df410f5b | |||
| 2436db3b1f | |||
| d15874df63 | |||
| 8fb90254cd | |||
| cbd712c20e | |||
| 8c004798f2 | |||
| c1b0cbe78d | |||
| 5ee72c8e98 | |||
| c125cc17dc | |||
| 18feff1bfb | |||
| f74f713bbd |
Generated
+22
-25
@@ -105,8 +105,9 @@
|
||||
}
|
||||
},
|
||||
"cloudron-manifestformat": {
|
||||
"version": "1.6.0",
|
||||
"from": "cloudron-manifestformat@1.6.0",
|
||||
"version": "1.7.0",
|
||||
"from": "cloudron-manifestformat@1.7.0",
|
||||
"resolved": "https://registry.npmjs.org/cloudron-manifestformat/-/cloudron-manifestformat-1.7.0.tgz",
|
||||
"dependencies": {
|
||||
"java-packagename-regex": {
|
||||
"version": "1.0.0",
|
||||
@@ -119,9 +120,9 @@
|
||||
"resolved": "http://registry.npmjs.org/safetydance/-/safetydance-0.0.15.tgz"
|
||||
},
|
||||
"tv4": {
|
||||
"version": "1.1.12",
|
||||
"version": "1.2.3",
|
||||
"from": "tv4@>=1.1.9 <2.0.0",
|
||||
"resolved": "http://registry.npmjs.org/tv4/-/tv4-1.1.12.tgz"
|
||||
"resolved": "https://registry.npmjs.org/tv4/-/tv4-1.2.3.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -133,15 +134,16 @@
|
||||
"connect-lastmile": {
|
||||
"version": "0.0.13",
|
||||
"from": "connect-lastmile@0.0.13",
|
||||
"resolved": "https://registry.npmjs.org/connect-lastmile/-/connect-lastmile-0.0.13.tgz",
|
||||
"dependencies": {
|
||||
"debug": {
|
||||
"version": "2.1.3",
|
||||
"from": "debug@>=2.1.0 <2.2.0",
|
||||
"from": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/debug/-/debug-2.1.3.tgz",
|
||||
"dependencies": {
|
||||
"ms": {
|
||||
"version": "0.7.0",
|
||||
"from": "ms@0.7.0",
|
||||
"from": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz",
|
||||
"resolved": "http://registry.npmjs.org/ms/-/ms-0.7.0.tgz"
|
||||
}
|
||||
}
|
||||
@@ -1706,7 +1708,19 @@
|
||||
"bunyan": {
|
||||
"version": "0.22.1",
|
||||
"from": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz"
|
||||
"resolved": "https://registry.npmjs.org/bunyan/-/bunyan-0.22.1.tgz",
|
||||
"dependencies": {
|
||||
"mv": {
|
||||
"version": "0.0.5",
|
||||
"from": "mv@0.0.5",
|
||||
"resolved": "https://registry.npmjs.org/mv/-/mv-0.0.5.tgz"
|
||||
},
|
||||
"dtrace-provider": {
|
||||
"version": "0.2.8",
|
||||
"from": "dtrace-provider@0.2.8",
|
||||
"resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.2.8.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nopt": {
|
||||
"version": "2.1.1",
|
||||
@@ -1937,23 +1951,6 @@
|
||||
"from": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz",
|
||||
"resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.3.tgz"
|
||||
},
|
||||
"nodejs-disks": {
|
||||
"version": "0.2.1",
|
||||
"from": "https://registry.npmjs.org/nodejs-disks/-/nodejs-disks-0.2.1.tgz",
|
||||
"resolved": "https://registry.npmjs.org/nodejs-disks/-/nodejs-disks-0.2.1.tgz",
|
||||
"dependencies": {
|
||||
"async": {
|
||||
"version": "0.2.10",
|
||||
"from": "https://registry.npmjs.org/async/-/async-0.2.10.tgz",
|
||||
"resolved": "https://registry.npmjs.org/async/-/async-0.2.10.tgz"
|
||||
},
|
||||
"numeral": {
|
||||
"version": "1.4.8",
|
||||
"from": "https://registry.npmjs.org/numeral/-/numeral-1.4.8.tgz",
|
||||
"resolved": "https://registry.npmjs.org/numeral/-/numeral-1.4.8.tgz"
|
||||
}
|
||||
}
|
||||
},
|
||||
"nodemailer": {
|
||||
"version": "1.3.4",
|
||||
"from": "https://registry.npmjs.org/nodemailer/-/nodemailer-1.3.4.tgz",
|
||||
@@ -2261,7 +2258,7 @@
|
||||
},
|
||||
"safetydance": {
|
||||
"version": "0.0.19",
|
||||
"from": "safetydance@0.0.19",
|
||||
"from": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz",
|
||||
"resolved": "https://registry.npmjs.org/safetydance/-/safetydance-0.0.19.tgz"
|
||||
},
|
||||
"semver": {
|
||||
|
||||
+1
-2
@@ -18,7 +18,7 @@
|
||||
"dependencies": {
|
||||
"async": "^1.2.1",
|
||||
"body-parser": "^1.13.1",
|
||||
"cloudron-manifestformat": "^1.6.0",
|
||||
"cloudron-manifestformat": "^1.7.0",
|
||||
"connect-ensure-login": "^0.1.1",
|
||||
"connect-lastmile": "0.0.13",
|
||||
"connect-timeout": "^1.5.0",
|
||||
@@ -43,7 +43,6 @@
|
||||
"mysql": "^2.7.0",
|
||||
"native-dns": "^0.7.0",
|
||||
"node-uuid": "^1.4.3",
|
||||
"nodejs-disks": "^0.2.1",
|
||||
"nodemailer": "^1.3.0",
|
||||
"nodemailer-smtp-transport": "^1.0.3",
|
||||
"oauth2orize": "^1.0.1",
|
||||
|
||||
+12
-1
@@ -3,4 +3,15 @@
|
||||
# If you change the infra version, be sure to put a warning
|
||||
# in the change log
|
||||
|
||||
INFRA_VERSION=4
|
||||
INFRA_VERSION=8
|
||||
|
||||
# WARNING WARNING WARNING WARNING WARNING WARNING WARNING WARNING
|
||||
# These constants are used in the installer script as well
|
||||
BASE_IMAGE=cloudron/base:0.3.3
|
||||
MYSQL_IMAGE=cloudron/mysql:0.3.3
|
||||
POSTGRESQL_IMAGE=cloudron/postgresql:0.3.2
|
||||
MONGODB_IMAGE=cloudron/mongodb:0.3.2
|
||||
REDIS_IMAGE=cloudron/redis:0.3.2 # if you change this, fix src/addons.js as well
|
||||
MAIL_IMAGE=cloudron/mail:0.3.2
|
||||
GRAPHITE_IMAGE=cloudron/graphite:0.3.4
|
||||
|
||||
|
||||
@@ -34,6 +34,9 @@ ln -sfF "${DATA_DIR}/collectd" /etc/collectd
|
||||
unlink /etc/nginx 2>/dev/null || rm -rf /etc/nginx
|
||||
ln -s "${DATA_DIR}/nginx" /etc/nginx
|
||||
|
||||
########## mysql
|
||||
cp "${container_files}/mysql.cnf" /etc/mysql/mysql.cnf
|
||||
|
||||
########## Enable services
|
||||
update-rc.d -f collectd defaults
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
!includedir /etc/mysql/conf.d/
|
||||
!includedir /etc/mysql/mysql.conf.d/
|
||||
|
||||
# http://bugs.mysql.com/bug.php?id=68514
|
||||
[mysqld]
|
||||
performance_schema=OFF
|
||||
max_connection=50
|
||||
@@ -56,6 +56,9 @@ echo "{ \"version\": \"${arg_version}\", \"boxVersionsUrl\": \"${arg_box_version
|
||||
echo "Cleaning up snapshots"
|
||||
find "${DATA_DIR}/snapshots" -mindepth 1 -maxdepth 1 | xargs --no-run-if-empty btrfs subvolume delete
|
||||
|
||||
# restart mysql to make sure it has latest config
|
||||
service mysql restart
|
||||
|
||||
readonly mysql_root_password="password"
|
||||
mysqladmin -u root -ppassword password password # reset default root password
|
||||
mysql -u root -p${mysql_root_password} -e 'CREATE DATABASE IF NOT EXISTS box'
|
||||
@@ -86,6 +89,10 @@ EOF
|
||||
|
||||
set_progress "28" "Setup collectd"
|
||||
cp "${script_dir}/start/collectd.conf" "${DATA_DIR}/collectd/collectd.conf"
|
||||
# collectd 5.4.1 has some bug where we simply cannot get it to create df-vda1
|
||||
mkdir -p "${DATA_DIR}/graphite/whisper/collectd/localhost/"
|
||||
vda1_id=$(blkid -s UUID -o value /dev/vda1)
|
||||
ln -sfF "df-disk_by-uuid_${vda1_id}" "${DATA_DIR}/graphite/whisper/collectd/localhost/df-vda1"
|
||||
service collectd restart
|
||||
|
||||
set_progress "30" "Setup nginx"
|
||||
|
||||
@@ -193,12 +193,11 @@ LoadPlugin write_graphite
|
||||
</Plugin>
|
||||
|
||||
<Plugin df>
|
||||
Device "/dev/vda1"
|
||||
Device "/dev/loop0"
|
||||
Device "/dev/loop1"
|
||||
FSType "tmpfs"
|
||||
MountPoint "/dev"
|
||||
|
||||
ReportByDevice true
|
||||
IgnoreSelected false
|
||||
IgnoreSelected true
|
||||
|
||||
ValuesAbsolute true
|
||||
ValuesPercentage true
|
||||
|
||||
@@ -27,11 +27,13 @@ if [[ -n "${existing_containers}" ]]; then
|
||||
fi
|
||||
|
||||
# graphite
|
||||
docker run --restart=always -d --name="graphite" \
|
||||
graphite_container_id=$(docker run --restart=always -d --name="graphite" \
|
||||
-p 127.0.0.1:2003:2003 \
|
||||
-p 127.0.0.1:2004:2004 \
|
||||
-p 127.0.0.1:8000:8000 \
|
||||
-v "${DATA_DIR}/graphite:/app/data" cloudron/graphite:0.3.1
|
||||
-v "${DATA_DIR}/graphite:/app/data" \
|
||||
"${GRAPHITE_IMAGE}")
|
||||
echo "Graphite container id: ${graphite_container_id}"
|
||||
|
||||
# mail
|
||||
mail_container_id=$(docker run --restart=always -d --name="mail" \
|
||||
@@ -39,7 +41,7 @@ mail_container_id=$(docker run --restart=always -d --name="mail" \
|
||||
-h "${arg_fqdn}" \
|
||||
-e "DOMAIN_NAME=${arg_fqdn}" \
|
||||
-v "${DATA_DIR}/box/mail:/app/data" \
|
||||
cloudron/mail:0.3.0)
|
||||
"${MAIL_IMAGE}")
|
||||
echo "Mail container id: ${mail_container_id}"
|
||||
|
||||
# mysql
|
||||
@@ -53,7 +55,7 @@ mysql_container_id=$(docker run --restart=always -d --name="mysql" \
|
||||
-h "${arg_fqdn}" \
|
||||
-v "${DATA_DIR}/mysql:/var/lib/mysql" \
|
||||
-v "${DATA_DIR}/addons/mysql_vars.sh:/etc/mysql/mysql_vars.sh:ro" \
|
||||
cloudron/mysql:0.3.0)
|
||||
"${MYSQL_IMAGE}")
|
||||
echo "MySQL container id: ${mysql_container_id}"
|
||||
|
||||
# postgresql
|
||||
@@ -65,7 +67,7 @@ postgresql_container_id=$(docker run --restart=always -d --name="postgresql" \
|
||||
-h "${arg_fqdn}" \
|
||||
-v "${DATA_DIR}/postgresql:/var/lib/postgresql" \
|
||||
-v "${DATA_DIR}/addons/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh:ro" \
|
||||
cloudron/postgresql:0.3.0)
|
||||
"${POSTGRESQL_IMAGE}")
|
||||
echo "PostgreSQL container id: ${postgresql_container_id}"
|
||||
|
||||
# mongodb
|
||||
@@ -77,7 +79,7 @@ mongodb_container_id=$(docker run --restart=always -d --name="mongodb" \
|
||||
-h "${arg_fqdn}" \
|
||||
-v "${DATA_DIR}/mongodb:/var/lib/mongodb" \
|
||||
-v "${DATA_DIR}/addons/mongodb_vars.sh:/etc/mongodb_vars.sh:ro" \
|
||||
cloudron/mongodb:0.3.0)
|
||||
"${MONGODB_IMAGE}")
|
||||
echo "Mongodb container id: ${mongodb_container_id}"
|
||||
|
||||
if [[ "${infra_version}" == "none" ]]; then
|
||||
|
||||
+2
-3
@@ -38,8 +38,7 @@ var appdb = require('./appdb.js'),
|
||||
tokendb = require('./tokendb.js'),
|
||||
util = require('util'),
|
||||
uuid = require('node-uuid'),
|
||||
vbox = require('./vbox.js'),
|
||||
_ = require('underscore');
|
||||
vbox = require('./vbox.js');
|
||||
|
||||
var NOOP = function (app, callback) { return callback(); };
|
||||
|
||||
@@ -665,7 +664,7 @@ function setupRedis(app, callback) {
|
||||
name: 'redis-' + app.id,
|
||||
Hostname: config.appFqdn(app.location),
|
||||
Tty: true,
|
||||
Image: 'cloudron/redis:0.3.0',
|
||||
Image: 'cloudron/redis:0.3.2', // if you change this, fix setup/INFRA_VERSION as well
|
||||
Cmd: null,
|
||||
Volumes: {},
|
||||
VolumesFrom: []
|
||||
|
||||
+3
-1
@@ -35,12 +35,13 @@ exports = module.exports = {
|
||||
ISTATE_ERROR: 'error', // error executing last pending_* command
|
||||
ISTATE_INSTALLED: 'installed', // app is installed
|
||||
|
||||
// run codes (keep in sync in UI)
|
||||
RSTATE_RUNNING: 'running',
|
||||
RSTATE_PENDING_START: 'pending_start',
|
||||
RSTATE_PENDING_STOP: 'pending_stop',
|
||||
RSTATE_STOPPED: 'stopped', // app stopped by use
|
||||
RSTATE_ERROR: 'error',
|
||||
|
||||
// run codes (keep in sync in UI)
|
||||
HEALTH_HEALTHY: 'healthy',
|
||||
HEALTH_UNHEALTHY: 'unhealthy',
|
||||
HEALTH_ERROR: 'error',
|
||||
@@ -335,6 +336,7 @@ function setInstallationCommand(appId, installationState, values, callback) {
|
||||
|
||||
// Rules are:
|
||||
// uninstall is allowed in any state
|
||||
// force update is allowed in any state including pending_uninstall! (for better or worse)
|
||||
// restore is allowed from installed or error state
|
||||
// update and configure are allowed only in installed state
|
||||
|
||||
|
||||
+24
-20
@@ -490,28 +490,30 @@ function restore(appId, callback) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.NOT_FOUND));
|
||||
if (error) return callback(new AppsError(AppsError.INTERNAL_ERROR, error));
|
||||
|
||||
var restoreConfig = app.lastBackupConfig;
|
||||
if (!restoreConfig) return callback(new AppsError(AppsError.BAD_STATE, 'No restore point'));
|
||||
// restore without a backup is the same as re-install
|
||||
var restoreConfig = app.lastBackupConfig, values = { };
|
||||
if (restoreConfig) {
|
||||
// re-validate because this new box version may not accept old configs.
|
||||
// if we restore location, it should be validated here as well
|
||||
error = checkManifestConstraints(restoreConfig.manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
|
||||
|
||||
// re-validate because this new box version may not accept old configs. if we restore location, it should be validated here as well
|
||||
error = checkManifestConstraints(restoreConfig.manifest);
|
||||
if (error) return callback(new AppsError(AppsError.BAD_FIELD, 'Manifest cannot be installed: ' + error.message));
|
||||
error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now
|
||||
if (error) return callback(error);
|
||||
|
||||
error = validatePortBindings(restoreConfig.portBindings, restoreConfig.manifest.tcpPorts); // maybe new ports got reserved now
|
||||
if (error) return callback(error);
|
||||
// ## should probably query new location, access restriction from user
|
||||
values = {
|
||||
manifest: restoreConfig.manifest,
|
||||
portBindings: restoreConfig.portBindings,
|
||||
|
||||
// ## should probably query new location, access restriction from user
|
||||
var values = {
|
||||
manifest: restoreConfig.manifest,
|
||||
portBindings: restoreConfig.portBindings,
|
||||
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
manifest: app.manifest
|
||||
}
|
||||
};
|
||||
oldConfig: {
|
||||
location: app.location,
|
||||
accessRestriction: app.accessRestriction,
|
||||
portBindings: app.portBindings,
|
||||
manifest: app.manifest
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
appdb.setInstallationCommand(appId, appdb.ISTATE_PENDING_RESTORE, values, function (error) {
|
||||
if (error && error.reason === DatabaseError.NOT_FOUND) return callback(new AppsError(AppsError.BAD_STATE)); // might be a bad guess
|
||||
@@ -573,6 +575,8 @@ function stop(appId, callback) {
|
||||
}
|
||||
|
||||
function checkManifestConstraints(manifest) {
|
||||
if (!manifest.dockerImage) return new Error('Missing dockerImage'); // dockerImage is optional in manifest
|
||||
|
||||
if (semver.valid(manifest.maxBoxVersion) && semver.gt(config.version(), manifest.maxBoxVersion)) {
|
||||
return new Error('Box version exceeds Apps maxBoxVersion');
|
||||
}
|
||||
@@ -664,7 +668,7 @@ function autoupdateApps(updateInfo, callback) { // updateInfo is { appId -> { ma
|
||||
return iteratorDone();
|
||||
}
|
||||
|
||||
update(appId, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) {
|
||||
update(appId, false /* force */, updateInfo[appId].manifest, app.portBindings, null /* icon */, function (error) {
|
||||
if (error) debug('Error initiating autoupdate of %s', appId);
|
||||
|
||||
iteratorDone(null);
|
||||
|
||||
+25
-8
@@ -201,8 +201,6 @@ function createContainer(app, callback) {
|
||||
Tty: true,
|
||||
Image: app.manifest.dockerImage,
|
||||
Cmd: null,
|
||||
Volumes: {},
|
||||
VolumesFrom: [],
|
||||
Env: env.concat(addonEnv),
|
||||
ExposedPorts: exposedPorts
|
||||
};
|
||||
@@ -250,7 +248,7 @@ function deleteImage(app, manifest, callback) {
|
||||
noprune: false
|
||||
};
|
||||
|
||||
// delete image by id because docker pull pulls down all the tags and this is the only way to delete all tags
|
||||
// delete image by id because 'docker pull' pulls down all the tags and this is the only way to delete all tags
|
||||
docker.getImage(result.Id).remove(removeOptions, function (error) {
|
||||
if (error && error.statusCode === 404) return callback(null);
|
||||
if (error && error.statusCode === 409) return callback(null); // another container using the image
|
||||
@@ -333,15 +331,20 @@ function startContainer(app, callback) {
|
||||
vbox.forwardFromHostToVirtualBox(app.id + '-tcp' + containerPort, hostPort);
|
||||
}
|
||||
|
||||
var memoryLimit = manifest.memoryLimit || 1024 * 1024 * 200; // 200mb by default
|
||||
|
||||
var startOptions = {
|
||||
Binds: addons.getBindsSync(app, app.manifest.addons),
|
||||
Memory: memoryLimit / 2,
|
||||
MemorySwap: memoryLimit, // Memory + Swap
|
||||
PortBindings: dockerPortBindings,
|
||||
PublishAllPorts: false,
|
||||
Links: addons.getLinksSync(app, app.manifest.addons),
|
||||
RestartPolicy: {
|
||||
"Name": "always",
|
||||
"MaximumRetryCount": 0
|
||||
}
|
||||
},
|
||||
CpuShares: 512 // relative to 1024 for system processes
|
||||
};
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
@@ -356,6 +359,11 @@ function startContainer(app, callback) {
|
||||
}
|
||||
|
||||
function stopContainer(app, callback) {
|
||||
if (!app.containerId) {
|
||||
debugApp(app, 'No previous container to stop');
|
||||
return callback();
|
||||
}
|
||||
|
||||
var container = docker.getContainer(app.containerId);
|
||||
debugApp(app, 'Stopping container %s', container.id);
|
||||
|
||||
@@ -533,7 +541,7 @@ function install(app, callback) {
|
||||
deleteVolume.bind(null, app),
|
||||
unregisterSubdomain.bind(null, app),
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
removeIcon.bind(null, app),
|
||||
// removeIcon.bind(null, app), // do not remove icon for non-appstore installs
|
||||
unconfigureNginx.bind(null, app),
|
||||
|
||||
updateApp.bind(null, app, { installationProgress: '15, Configure nginx' }),
|
||||
@@ -618,7 +626,11 @@ function restore(app, callback) {
|
||||
// oldConfig can be null during upgrades
|
||||
addons.teardownAddons.bind(null, app, app.oldConfig ? app.oldConfig.manifest.addons : null),
|
||||
deleteVolume.bind(null, app),
|
||||
deleteImage.bind(null, app, app.manifest),
|
||||
function deleteImageIfChanged(done) {
|
||||
if (!app.oldConfig || (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage)) return done();
|
||||
|
||||
deleteImage(app, app.oldConfig.manifest, done);
|
||||
},
|
||||
removeOAuthProxyCredentials.bind(null, app),
|
||||
removeIcon.bind(null, app),
|
||||
unconfigureNginx.bind(null, app),
|
||||
@@ -672,7 +684,8 @@ function restore(app, callback) {
|
||||
|
||||
// note that configure is called after an infra update as well
|
||||
function configure(app, callback) {
|
||||
var locationChanged = app.oldConfig.location !== app.location;
|
||||
// oldConfig can be null during an infra update
|
||||
var locationChanged = app.oldConfig ? app.oldConfig.location !== app.location : true;
|
||||
|
||||
async.series([
|
||||
updateApp.bind(null, app, { installationProgress: '10, Cleaning up old install' }),
|
||||
@@ -754,7 +767,11 @@ function update(app, callback) {
|
||||
stopApp.bind(null, app),
|
||||
deleteContainer.bind(null, app),
|
||||
addons.teardownAddons.bind(null, app, unusedAddons),
|
||||
deleteImage.bind(null, app, app.manifest), // delete image even if did not change (see df158b111f)
|
||||
function deleteImageIfChanged(done) {
|
||||
if (app.oldConfig.manifest.dockerImage === app.manifest.dockerImage) return done();
|
||||
|
||||
deleteImage(app, app.oldConfig.manifest, done);
|
||||
},
|
||||
// removeIcon.bind(null, app), // do not remove icon, otherwise the UI breaks for a short time...
|
||||
|
||||
function (next) {
|
||||
|
||||
+2
-3
@@ -271,8 +271,7 @@ function getConfig(callback) {
|
||||
function sendHeartbeat() {
|
||||
var url = config.apiServerOrigin() + '/api/v1/boxes/' + config.fqdn() + '/heartbeat';
|
||||
|
||||
// TODO: this must be a POST
|
||||
superagent.get(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
|
||||
superagent.post(url).query({ token: config.token(), version: config.version() }).timeout(10000).end(function (error, result) {
|
||||
if (error) debug('Error sending heartbeat.', error);
|
||||
else if (result.statusCode !== 200) debug('Server responded to heartbeat with %s %s', result.statusCode, result.text);
|
||||
else debug('Heartbeat sent to %s', url);
|
||||
@@ -609,7 +608,7 @@ function backupBoxAndApps(callback) {
|
||||
++processed;
|
||||
|
||||
apps.backupApp(app, app.manifest.addons, function (error, backupId) {
|
||||
progress.set(progress.BACKUP, step * processed, 'Backing up app at ' + app.location);
|
||||
progress.set(progress.BACKUP, step * processed, 'Backed up app at ' + app.location);
|
||||
|
||||
if (error && error.reason === AppsError.BAD_STATE) {
|
||||
debugApp(app, 'Skipping backup (istate:%s health:%s). using lastBackupId:%s', app.installationState, app.health, app.lastBackupId);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
LoadPlugin "table"
|
||||
<Plugin table>
|
||||
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.stat">
|
||||
<Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.stat">
|
||||
Instance "<%= appId %>-memory"
|
||||
Separator " \\n"
|
||||
<Result>
|
||||
@@ -10,7 +10,7 @@ LoadPlugin "table"
|
||||
</Result>
|
||||
</Table>
|
||||
|
||||
<Table "/sys/fs/cgroup/memory/docker/<%= containerId %>/memory.max_usage_in_bytes">
|
||||
<Table "/sys/fs/cgroup/memory/system.slice/docker-<%= containerId %>.scope/memory.max_usage_in_bytes">
|
||||
Instance "<%= appId %>-memory"
|
||||
Separator "\\n"
|
||||
<Result>
|
||||
@@ -20,7 +20,7 @@ LoadPlugin "table"
|
||||
</Result>
|
||||
</Table>
|
||||
|
||||
<Table "/sys/fs/cgroup/cpuacct/docker/<%= containerId %>/cpuacct.stat">
|
||||
<Table "/sys/fs/cgroup/cpuacct/system.slice/docker-<%= containerId %>.scope/cpuacct.stat">
|
||||
Instance "<%= appId %>-cpu"
|
||||
Separator " \\n"
|
||||
<Result>
|
||||
|
||||
+32
-15
@@ -24,6 +24,9 @@ var gLogger = {
|
||||
fatal: console.error
|
||||
};
|
||||
|
||||
var GROUP_USERS_DN = 'cn=users,ou=groups,dc=cloudron';
|
||||
var GROUP_ADMINS_DN = 'cn=admins,ou=groups,dc=cloudron';
|
||||
|
||||
function start(callback) {
|
||||
assert(typeof callback === 'function');
|
||||
|
||||
@@ -39,6 +42,9 @@ function start(callback) {
|
||||
result.forEach(function (entry) {
|
||||
var dn = ldap.parseDN('cn=' + entry.id + ',ou=users,dc=cloudron');
|
||||
|
||||
var groups = [ GROUP_USERS_DN ];
|
||||
if (entry.admin) groups.push(GROUP_ADMINS_DN);
|
||||
|
||||
var tmp = {
|
||||
dn: dn.toString(),
|
||||
attributes: {
|
||||
@@ -49,7 +55,8 @@ function start(callback) {
|
||||
mail: entry.email,
|
||||
displayname: entry.username,
|
||||
username: entry.username,
|
||||
samaccountname: entry.username // to support ActiveDirectory clients
|
||||
samaccountname: entry.username, // to support ActiveDirectory clients
|
||||
memberof: groups
|
||||
}
|
||||
};
|
||||
|
||||
@@ -69,22 +76,32 @@ function start(callback) {
|
||||
user.list(function (error, result){
|
||||
if (error) return next(new ldap.OperationsError(error.toString()));
|
||||
|
||||
// we only have an admin group
|
||||
var dn = ldap.parseDN('cn=admin,ou=groups,dc=cloudron');
|
||||
var groups = [{
|
||||
name: 'users',
|
||||
admin: false
|
||||
}, {
|
||||
name: 'admins',
|
||||
admin: true
|
||||
}];
|
||||
|
||||
var tmp = {
|
||||
dn: dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['group'],
|
||||
cn: 'admin',
|
||||
memberuid: result.filter(function (entry) { return entry.admin; }).map(function(entry) { return entry.id; })
|
||||
groups.forEach(function (group) {
|
||||
var dn = ldap.parseDN('cn=' + group.name + ',ou=groups,dc=cloudron');
|
||||
var members = group.admin ? result.filter(function (entry) { return entry.admin; }) : result;
|
||||
|
||||
var tmp = {
|
||||
dn: dn.toString(),
|
||||
attributes: {
|
||||
objectclass: ['group'],
|
||||
cn: group.name,
|
||||
memberuid: members.map(function(entry) { return entry.id; })
|
||||
}
|
||||
};
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
|
||||
res.send(tmp);
|
||||
debug('ldap group send:', tmp);
|
||||
}
|
||||
};
|
||||
|
||||
if ((req.dn.equals(dn) || req.dn.parentOf(dn)) && req.filter.matches(tmp.attributes)) {
|
||||
res.send(tmp);
|
||||
debug('ldap group send:', tmp);
|
||||
}
|
||||
});
|
||||
|
||||
res.end();
|
||||
});
|
||||
|
||||
@@ -5,8 +5,10 @@
|
||||
<div class="col-md-6 col-md-offset-3">
|
||||
<div class="card">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1><img width="64" height="64" src="<%= applicationLogo %>"/> Login to <%= applicationName %> on <%= cloudronName %></h1>
|
||||
<div class="col-md-12" style="text-align: center;">
|
||||
<img width="128" height="128" src="<%= applicationLogo %>"/>
|
||||
<h1>Login to <%= applicationName %> on <%= cloudronName %></h1>
|
||||
<br/>
|
||||
</div>
|
||||
</div>
|
||||
<br/>
|
||||
|
||||
@@ -171,6 +171,8 @@ function sendErrorPageOrRedirect(req, res, message) {
|
||||
}
|
||||
}
|
||||
|
||||
// use this instead of sendErrorPageOrRedirect(), in case we have a returnTo provided in the query, to avoid login loops
|
||||
// This usually happens when the OAuth client ID is wrong
|
||||
function sendError(req, res, message) {
|
||||
assert.strictEqual(typeof req, 'object');
|
||||
assert.strictEqual(typeof res, 'object');
|
||||
|
||||
@@ -2,6 +2,10 @@
|
||||
|
||||
set -eu -o pipefail
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../../.." && pwd)"
|
||||
|
||||
source ${SOURCE_DIR}/setup/INFRA_VERSION
|
||||
|
||||
readonly mysqldatadir="/tmp/mysqldata-$(date +%s)"
|
||||
readonly postgresqldatadir="/tmp/postgresqldata-$(date +%s)"
|
||||
readonly mongodbdatadir="/tmp/mongodbdata-$(date +%s)"
|
||||
@@ -20,7 +24,7 @@ start_postgresql() {
|
||||
|
||||
docker rm -f postgresql 2>/dev/null 1>&2 || true
|
||||
|
||||
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" -v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh cloudron/postgresql:0.3.0 >/dev/null
|
||||
docker run -dtP --name=postgresql -v "${postgresqldatadir}:/var/lib/postgresql" -v /tmp/postgresql_vars.sh:/etc/postgresql/postgresql_vars.sh "${POSTGRESQL_IMAGE}" >/dev/null
|
||||
}
|
||||
|
||||
start_mysql() {
|
||||
@@ -36,7 +40,7 @@ start_mysql() {
|
||||
|
||||
docker rm -f mysql 2>/dev/null 1>&2 || true
|
||||
|
||||
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" -v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh cloudron/mysql:0.3.0 >/dev/null
|
||||
docker run -dP --name=mysql -v "${mysqldatadir}:/var/lib/mysql" -v /tmp/mysql_vars.sh:/etc/mysql/mysql_vars.sh "${MYSQL_IMAGE}" >/dev/null
|
||||
}
|
||||
|
||||
start_mongodb() {
|
||||
@@ -52,7 +56,7 @@ start_mongodb() {
|
||||
|
||||
docker rm -f mongodb 2>/dev/null 1>&2 || true
|
||||
|
||||
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" -v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh cloudron/mongodb:0.3.0 >/dev/null
|
||||
docker run -dP --name=mongodb -v "${mongodbdatadir}:/var/lib/mongodb" -v /tmp/mongodb_vars.sh:/etc/mongodb_vars.sh "${MONGODB_IMAGE}" >/dev/null
|
||||
}
|
||||
|
||||
start_mysql
|
||||
|
||||
+21
-19
@@ -2,22 +2,24 @@
|
||||
|
||||
set -eu
|
||||
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
readonly SOURCE_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)"
|
||||
|
||||
source ${SOURCE_DIR}/setup/INFRA_VERSION
|
||||
|
||||
# reset sudo timestamp to avoid wrong success
|
||||
sudo -k || sudo --reset-timestamp
|
||||
|
||||
# checks if all scripts are sudo access
|
||||
scripts=("${SOURCE_DIR}/scripts/rmappdir.sh" \
|
||||
"${SOURCE_DIR}/scripts/createappdir.sh" \
|
||||
"${SOURCE_DIR}/scripts/reloadnginx.sh" \
|
||||
"${SOURCE_DIR}/scripts/backupbox.sh" \
|
||||
"${SOURCE_DIR}/scripts/backupapp.sh" \
|
||||
"${SOURCE_DIR}/scripts/restoreapp.sh" \
|
||||
"${SOURCE_DIR}/scripts/reboot.sh" \
|
||||
"${SOURCE_DIR}/scripts/backupswap.sh" \
|
||||
"${SOURCE_DIR}/scripts/collectlogs.sh" \
|
||||
"${SOURCE_DIR}/scripts/reloadcollectd.sh")
|
||||
scripts=("${SOURCE_DIR}/src/scripts/rmappdir.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/createappdir.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/reloadnginx.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/backupbox.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/backupapp.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/restoreapp.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/reboot.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/backupswap.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/collectlogs.sh" \
|
||||
"${SOURCE_DIR}/src/scripts/reloadcollectd.sh")
|
||||
|
||||
for script in "${scripts[@]}"; do
|
||||
if [[ $(sudo -n "${script}" --check 2>/dev/null) != "OK" ]]; then
|
||||
@@ -37,23 +39,23 @@ if ! docker inspect girish/test:0.2.0 >/dev/null 2>/dev/null; then
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker inspect cloudron/redis:0.3.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/redis:0.3.0 for tests to run"
|
||||
if ! docker inspect "${REDIS_IMAGE}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${REDIS_IMAGE} for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker inspect cloudron/mysql:0.3.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/mysql:0.3.0 for tests to run"
|
||||
if ! docker inspect "${MYSQL_IMAGE}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${MYSQL_IMAGE} for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker inspect cloudron/postgresql:0.3.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/postgresql:0.3.0 for tests to run"
|
||||
if ! docker inspect "${POSTGRESQL_IMAGE}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${POSTGRESQL_IMAGE} for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if ! docker inspect cloudron/mongodb:0.3.0 >/dev/null 2>/dev/null; then
|
||||
echo "docker pull cloudron/mongodb:0.3.0 for tests to run"
|
||||
if ! docker inspect "${MONGODB_IMAGE}" >/dev/null 2>/dev/null; then
|
||||
echo "docker pull ${MONGODB_IMAGE} for tests to run"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
/* jslint node:true */
|
||||
/* global it:false */
|
||||
/* global describe:false */
|
||||
/* global before:false */
|
||||
/* global after:false */
|
||||
|
||||
'use strict';
|
||||
|
||||
require('supererror', { splatchError: true});
|
||||
|
||||
var database = require('../database.js'),
|
||||
expect = require('expect.js'),
|
||||
EventEmitter = require('events').EventEmitter,
|
||||
async = require('async'),
|
||||
user = require('../user.js'),
|
||||
config = require('../config.js'),
|
||||
ldapServer = require('../ldap.js'),
|
||||
ldap = require('ldapjs');
|
||||
|
||||
var USER_0 = {
|
||||
username: 'foobar0',
|
||||
password: 'password0',
|
||||
email: 'foo0@bar.com'
|
||||
};
|
||||
|
||||
var USER_1 = {
|
||||
username: 'foobar1',
|
||||
password: 'password1',
|
||||
email: 'foo1@bar.com'
|
||||
};
|
||||
|
||||
function setup(done) {
|
||||
async.series([
|
||||
database.initialize.bind(null),
|
||||
database._clear.bind(null),
|
||||
ldapServer.start.bind(null),
|
||||
user.create.bind(null, USER_0.username, USER_0.password, USER_0.email, true, null),
|
||||
user.create.bind(null, USER_1.username, USER_1.password, USER_1.email, false, USER_0)
|
||||
], done);
|
||||
}
|
||||
|
||||
function cleanup(done) {
|
||||
database._clear(done);
|
||||
}
|
||||
|
||||
describe('Ldap', function () {
|
||||
before(setup);
|
||||
after(cleanup);
|
||||
|
||||
describe('bind', function () {
|
||||
it('fails for nonexisting user', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=doesnotexist,ou=users,dc=cloudron', 'password', function (error) {
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('fails with wrong password', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + USER_0.username + ',ou=users,dc=cloudron', 'wrongpassword', function (error) {
|
||||
expect(error).to.be.a(ldap.InvalidCredentialsError);
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
client.bind('cn=' + USER_0.username + ',ou=users,dc=cloudron', USER_0.password, function (error) {
|
||||
expect(error).to.be(null);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('search users', function () {
|
||||
it ('fails for non existing tree', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: '(&(l=Seattle)(email=*@foo.com))'
|
||||
};
|
||||
|
||||
client.search('o=example', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
result.on('error', function (error) {
|
||||
expect(error).to.be.a(ldap.NoSuchObjectError);
|
||||
done();
|
||||
});
|
||||
result.on('end', function (result) {
|
||||
done(new Error('Should not succeed. Status ' + result.status));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds with basic filter', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: 'objectcategory=person'
|
||||
};
|
||||
|
||||
client.search('ou=users,dc=cloudron', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
var entries = [];
|
||||
|
||||
result.on('searchEntry', function (entry) { entries.push(entry.object); });
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(2);
|
||||
expect(entries[0].username).to.equal(USER_0.username);
|
||||
expect(entries[1].username).to.equal(USER_1.username);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds with username wildcard filter', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: '&(objectcategory=person)(username=foobar*)'
|
||||
};
|
||||
|
||||
client.search('ou=users,dc=cloudron', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
var entries = [];
|
||||
|
||||
result.on('searchEntry', function (entry) { entries.push(entry.object); });
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(2);
|
||||
expect(entries[0].username).to.equal(USER_0.username);
|
||||
expect(entries[1].username).to.equal(USER_1.username);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds with username filter', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: '&(objectcategory=person)(username=' + USER_0.username + ')'
|
||||
};
|
||||
|
||||
client.search('ou=users,dc=cloudron', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
var entries = [];
|
||||
|
||||
result.on('searchEntry', function (entry) { entries.push(entry.object); });
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(1);
|
||||
expect(entries[0].username).to.equal(USER_0.username);
|
||||
expect(entries[0].memberof.length).to.equal(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('search groups', function () {
|
||||
it ('succeeds with basic filter', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: 'objectclass=group'
|
||||
};
|
||||
|
||||
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
var entries = [];
|
||||
|
||||
result.on('searchEntry', function (entry) { entries.push(entry.object); });
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(2);
|
||||
expect(entries[0].cn).to.equal('users');
|
||||
expect(entries[0].memberuid.length).to.equal(2);
|
||||
expect(entries[0].memberuid[0]).to.equal(USER_0.username);
|
||||
expect(entries[0].memberuid[1]).to.equal(USER_1.username);
|
||||
expect(entries[1].cn).to.equal('admins');
|
||||
// if only one entry, the array becomes a string :-/
|
||||
expect(entries[1].memberuid).to.equal(USER_0.username);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it ('succeeds with cn wildcard filter', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: '&(objectclass=group)(cn=*)'
|
||||
};
|
||||
|
||||
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
var entries = [];
|
||||
|
||||
result.on('searchEntry', function (entry) { entries.push(entry.object); });
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(2);
|
||||
expect(entries[0].cn).to.equal('users');
|
||||
expect(entries[0].memberuid.length).to.equal(2);
|
||||
expect(entries[0].memberuid[0]).to.equal(USER_0.username);
|
||||
expect(entries[0].memberuid[1]).to.equal(USER_1.username);
|
||||
expect(entries[1].cn).to.equal('admins');
|
||||
// if only one entry, the array becomes a string :-/
|
||||
expect(entries[1].memberuid).to.equal(USER_0.username);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('succeeds with memberuid filter', function (done) {
|
||||
var client = ldap.createClient({ url: 'ldap://127.0.0.1:' + config.get('ldapPort') });
|
||||
|
||||
var opts = {
|
||||
filter: '&(objectclass=group)(memberuid=' + USER_1.username + ')'
|
||||
};
|
||||
|
||||
client.search('ou=groups,dc=cloudron', opts, function (error, result) {
|
||||
expect(error).to.be(null);
|
||||
expect(result).to.be.an(EventEmitter);
|
||||
|
||||
var entries = [];
|
||||
|
||||
result.on('searchEntry', function (entry) { entries.push(entry.object); });
|
||||
result.on('error', done);
|
||||
result.on('end', function (result) {
|
||||
expect(result.status).to.equal(0);
|
||||
expect(entries.length).to.equal(1);
|
||||
expect(entries[0].cn).to.equal('users');
|
||||
expect(entries[0].memberuid.length).to.equal(2);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -81,7 +81,7 @@
|
||||
<li ng-repeat="change in config.update.box.changelog">{{change}}</li>
|
||||
</ul>
|
||||
<br/>
|
||||
<fieldset>
|
||||
<fieldset ng-show="installedApps | readyToUpdate">
|
||||
<form name="update_form" role="form" ng-submit="doUpdate()" autocomplete="off">
|
||||
<div class="form-group" ng-class="{ 'has-error': update_form.password.$dirty && update_form.password.$invalid }">
|
||||
<label class="control-label" for="inputUpdatePassword">Give your password to verify that you are performing that action</label>
|
||||
@@ -99,7 +99,7 @@
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="update_form.$invalid || update.busy"><i class="fa fa-spinner fa-pulse" ng-show="update.busy"></i> Update</button>
|
||||
<button type="button" class="btn btn-danger" ng-click="doUpdate()" ng-disabled="update_form.$invalid || update.busy" ng-show="installedApps | readyToUpdate"><i class="fa fa-spinner fa-pulse" ng-show="update.busy"></i> Update</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+66
-21
@@ -120,6 +120,14 @@ html {
|
||||
.grid-item {
|
||||
padding: 10px;
|
||||
min-width: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.grid-item:hover .grid-item-bottom {
|
||||
@media(min-width:768px) {
|
||||
opacity: 1;
|
||||
right: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-item-content {
|
||||
@@ -132,10 +140,39 @@ html {
|
||||
padding: 10px 15px;
|
||||
}
|
||||
|
||||
.grid-item-bottom {
|
||||
.grid-item-top-title {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
font-size: 24px;
|
||||
}
|
||||
|
||||
.grid-item-bottom-mobile {
|
||||
padding: 10px 15px;
|
||||
border-top: 1px solid #ddd;
|
||||
background-color: white
|
||||
background-color: white;
|
||||
|
||||
@media(min-width:768px) {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.grid-item-bottom {
|
||||
display: none;
|
||||
padding: 10px 15px;
|
||||
border-top: 1px solid #ddd;
|
||||
background-color: white;
|
||||
|
||||
@media(min-width:768px) {
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: -10px;
|
||||
opacity: 0;
|
||||
background-color: transparent;
|
||||
|
||||
transition: all 250ms;
|
||||
}
|
||||
}
|
||||
|
||||
// ----------------------------
|
||||
@@ -158,8 +195,8 @@ html {
|
||||
|
||||
.appstore-item-badge-testing {
|
||||
position: absolute;
|
||||
right: 15px;
|
||||
top: 15px;
|
||||
right: 0;
|
||||
top: 2px;
|
||||
}
|
||||
|
||||
.appstore-item-content-testing {
|
||||
@@ -195,17 +232,30 @@ html {
|
||||
}
|
||||
|
||||
.appstore-category-link {
|
||||
display: block;
|
||||
padding: 10px;
|
||||
margin: 0;
|
||||
overflow: hidden;
|
||||
color: black;
|
||||
color: inherit;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
font-size: 18px;
|
||||
|
||||
&:hover,
|
||||
&:focus,
|
||||
&.category-active {
|
||||
text-decoration: none;
|
||||
background-color: white;
|
||||
color: black;
|
||||
}
|
||||
|
||||
&.category-active {
|
||||
background-color: $navbar-default-link-hover-color;
|
||||
color: white;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.appstore-category-missing {
|
||||
padding: 10px;
|
||||
background-color: white;
|
||||
@@ -251,14 +301,6 @@ html {
|
||||
color: $text-muted;
|
||||
}
|
||||
|
||||
.appstore-category-link:hover,
|
||||
.appstore-category-link:focus,
|
||||
.appstore-category-link.category-active {
|
||||
text-decoration: none;
|
||||
background-color: $navbar-default-link-hover-color;
|
||||
color: white;
|
||||
}
|
||||
|
||||
.appstore-item-rating {
|
||||
color: $navbar-default-link-hover-color;
|
||||
}
|
||||
@@ -288,6 +330,10 @@ html {
|
||||
background-color: #5CB85C;
|
||||
}
|
||||
|
||||
.badge-warning {
|
||||
background-color: #EFBD48;
|
||||
}
|
||||
|
||||
.badge-danger {
|
||||
background-color: $brand-danger;
|
||||
}
|
||||
@@ -358,12 +404,9 @@ html {
|
||||
.grid-item-top .progress {
|
||||
border-radius: 0;
|
||||
box-shadown: none;
|
||||
margin-left: -15px;
|
||||
margin-right: -15px;
|
||||
margin-bottom: -11px;
|
||||
margin-top: 9px;
|
||||
margin-top: 10px;
|
||||
width: inherit;
|
||||
height: 2px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.grid-item-top .progress-bar {
|
||||
@@ -676,7 +719,7 @@ footer a {
|
||||
background: #F7F7F7;
|
||||
|
||||
h1 {
|
||||
margin-top: 0;
|
||||
font-size: 33px;
|
||||
}
|
||||
|
||||
.card {
|
||||
@@ -842,6 +885,8 @@ $graphs-success-alt: lighten(#27CE65, 20%);
|
||||
|
||||
.support {
|
||||
|
||||
max-width: 600px;
|
||||
|
||||
h3 {
|
||||
margin-top: 0;
|
||||
margin-bottom: 20px;
|
||||
|
||||
@@ -89,6 +89,8 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="text-left">
|
||||
<h1>Account</h1>
|
||||
|
||||
@@ -54,7 +54,7 @@
|
||||
</form>
|
||||
</fieldset>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="modal-footer ">
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-click="startApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'stopped' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-play"></i> Start</button>
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-show="appConfigure.app.runState !== 'stopped' && appConfigure.app.runState !== 'running' || appConfigure.runStateBusy && !(appConfigure.app | installationActive)" disabled ><i class="fa fa-refresh fa-spin"></i></button>
|
||||
<button type="button" class="btn btn-default" style="float: left;" ng-click="stopApp(appConfigure.app)" ng-show="appConfigure.app.runState === 'running' && !appConfigure.runStateBusy && !(appConfigure.app | installationActive)"><i class="fa fa-pause"></i> Stop</button>
|
||||
@@ -183,6 +183,9 @@
|
||||
</script>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row animateMeOpacity ng-hide" ng-show="installedApps.length > 0">
|
||||
<div class="col-lg-12">
|
||||
<h1>Installed Applications</h1>
|
||||
@@ -201,11 +204,12 @@
|
||||
</div>
|
||||
<br/>
|
||||
<div class="row">
|
||||
<div class="col-xs-12 text-left">
|
||||
<div style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">{{ app.location || app.fqdn }}</div>
|
||||
<div class="col-xs-12 text-center">
|
||||
<div class="grid-item-top-title">{{ app.location || app.fqdn }}</div>
|
||||
<div class="text-muted" style="text-overflow: ellipsis; white-space: nowrap; overflow: hidden">
|
||||
{{ app | installationStateLabel }}
|
||||
</div>
|
||||
<br ng-hide="app | installationActive"/>
|
||||
<div ng-show="app | installationActive">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" role="progressbar" style="width: {{ app.progress }}%"></div>
|
||||
@@ -214,32 +218,57 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
<div class="grid-item-bottom" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-left">
|
||||
<a href="" ng-click="showRestore(app)" ng-show="(app | installError) === true">
|
||||
<i class="fa fa-undo scale"></i>
|
||||
</a>
|
||||
<div class="grid-item-bottom-mobile" ng-show="user.admin">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 text-left">
|
||||
<a href="" ng-click="showRestore(app)" ng-show="(app | installError) === true">
|
||||
<i class="fa fa-undo scale"></i>
|
||||
</a>
|
||||
|
||||
<a href="" ng-click="showConfigure(app)" ng-show="(app | installSuccess) == true">
|
||||
<i class="fa fa-wrench scale"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-4 text-center">
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<a href="" ng-click="showUpdate(app)" class="ng-hide animateMe" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<i class="fa fa-arrow-up text-success scale"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-4 text-right">
|
||||
<a href="" ng-click="showUninstall(app)">
|
||||
<i class="fa fa-remove scale"></i>
|
||||
</a>
|
||||
<a href="" ng-click="showConfigure(app)" ng-show="(app | installSuccess) == true">
|
||||
<i class="fa fa-wrench scale"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-4 text-center">
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<a href="" ng-click="showUpdate(app)" class="ng-hide animateMe" ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<i class="fa fa-arrow-up text-success scale"></i>
|
||||
</a>
|
||||
</div>
|
||||
<div class="col-xs-4 text-right">
|
||||
<a href="" ng-click="showUninstall(app)">
|
||||
<i class="fa fa-remove scale"></i>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid-item-bottom" ng-show="user.admin">
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<a href="" ng-click="showUninstall(app)"><i class="fa fa-remove scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="(app | installError) === true">
|
||||
<a href="" ng-click="showRestore(app)"><i class="fa fa-undo scale"></i></a>
|
||||
</div>
|
||||
|
||||
<div ng-show="(app | installSuccess) == true">
|
||||
<a href="" ng-click="showConfigure(app)"><i class="fa fa-wrench scale"></i></a>
|
||||
</div>
|
||||
|
||||
<!-- we check the version here because the box updater does not know when an app gets updated -->
|
||||
<div ng-show="config.update.apps[app.id].manifest.version && config.update.apps[app.id].manifest.version !== app.manifest.version && (app | installSuccess)">
|
||||
<a href="" ng-click="showUpdate(app)"><i class="fa fa-arrow-up text-success scale"></i></a>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
</div>
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Offset the footer -->
|
||||
<br/><br/>
|
||||
|
||||
@@ -334,7 +334,7 @@ angular.module('Application').controller('AppsController', ['$scope', '$location
|
||||
};
|
||||
|
||||
// setup all the dialog focus handling
|
||||
['appConfigureModal', 'appUninstallModal', 'appUpdateModal'].forEach(function (id) {
|
||||
['appConfigureModal', 'appUninstallModal', 'appUpdateModal', 'appRestoreModal'].forEach(function (id) {
|
||||
$('#' + id).on('shown.bs.modal', function () {
|
||||
$(this).find("[autofocus]:first").focus();
|
||||
});
|
||||
|
||||
@@ -147,8 +147,9 @@
|
||||
<div class="col-md-10" ng-show="ready && apps.length">
|
||||
<div class="row-no-margin">
|
||||
<div class="col-sm-1 appstore-item" ng-repeat="app in apps">
|
||||
<div class="appstore-item-content highlight" ng-click="showInstall(app)" ng-class="{ 'appstore-item-content-testing': app.publishState === 'testing' }">
|
||||
<div class="appstore-item-content highlight" ng-click="showInstall(app)" ng-class="{ 'appstore-item-content-testing': (app.publishState === 'testing' || app.publishState === 'pending_approval') }">
|
||||
<span class="badge badge-danger appstore-item-badge-testing" ng-show="app.publishState === 'testing'">Testing</span>
|
||||
<span class="badge badge-warning appstore-item-badge-testing" ng-show="app.publishState === 'pending_approval'">Pending Approval</span>
|
||||
<div class="appstore-item-content-icon col-same-height">
|
||||
<img ng-src="{{app.iconUrl}}" onerror="this.onerror=null;this.src='img/appicon_fallback.png'" class="app-icon"/>
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
|
||||
<br/>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<h1>Graphs</h1>
|
||||
|
||||
@@ -98,7 +98,7 @@ angular.module('Application').controller('GraphsController', ['$scope', '$locati
|
||||
var options = {
|
||||
scaleOverride: true,
|
||||
scaleSteps: 10,
|
||||
scaleStepWidth: $scope.activeApp === 'system' ? 100 : 10,
|
||||
scaleStepWidth: $scope.activeApp === 'system' ? 200 : 20,
|
||||
scaleStartValue: 0
|
||||
};
|
||||
|
||||
|
||||
@@ -107,6 +107,14 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<br/>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;">
|
||||
<div class="text-left">
|
||||
<h1>Settings</h1>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="max-width: 600px; margin: 0 auto;" ng-show="user.admin">
|
||||
<div class="text-left">
|
||||
<h3>Backups</h3>
|
||||
|
||||
@@ -1,4 +1,6 @@
|
||||
<div class="content support">
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<div class="text-left">
|
||||
<h1>Support</h1>
|
||||
|
||||
@@ -80,6 +80,9 @@
|
||||
</div>
|
||||
|
||||
<div class="content">
|
||||
|
||||
<br/>
|
||||
|
||||
<div>
|
||||
<div class="text-left">
|
||||
<h1>Users <button class="btn btn-primary btn-outline pull-right" data-toggle="modal" data-target="#userAddModal"><i class="fa fa-user-plus"></i> New User</button></h1>
|
||||
|
||||
Reference in New Issue
Block a user