diff --git a/CHANGES b/CHANGES index e4cba83bb..9c8d57d43 100644 --- a/CHANGES +++ b/CHANGES @@ -2929,6 +2929,7 @@ * UI redesign * notifications: email notification when server reboot is required * notifications: email notification when cloudron update failed +* notifications: email notification for low disk space (90%) * node: update to 22.13.1 * docker: update to 27.5.1 * s3: automatically abort old multipart uploads @@ -2940,5 +2941,4 @@ * sendmail: requiresValidCertificate option for using mail server domain * mail: update haraka to 3.1.0 * sshfs: implement rm via ssh -* notification: low disk notification - +* multiple docker registries diff --git a/migrations/20250507122501-dockerRegistries-create-table.js b/migrations/20250507122501-dockerRegistries-create-table.js new file mode 100644 index 000000000..f790533e2 --- /dev/null +++ b/migrations/20250507122501-dockerRegistries-create-table.js @@ -0,0 +1,34 @@ +'use strict'; + +const uuid = require('uuid'); + +exports.up = async function (db) { + const cmd = 'CREATE TABLE IF NOT EXISTS dockerRegistries(' + + 'id VARCHAR(128) NOT NULL UNIQUE,' + + 'provider VARCHAR(16) NOT NULL,' + + 'serverAddress VARCHAR(128) NOT NULL,' + + 'username VARCHAR(128),' + + 'email VARCHAR(128),' + + 'password VARCHAR(128),' + + 'PRIMARY KEY (id)) CHARACTER SET utf8 COLLATE utf8_bin'; + + await db.runSql(cmd); + + const result = await db.runSql('SELECT * FROM settings WHERE name=?', [ 'registry_config' ]); + if (!result.length) return; + + await db.runSql('START TRANSACTION'); + const registryConfig = JSON.parse(result[0].value); + + if (registryConfig.provider !== 'noop') { + await db.runSql('INSERT INTO dockerRegistries (id, provider, serverAddress, username, email, password) VALUES (?, ?, ?, ?, ?, ?)', + [ `rc-${uuid.v4()}`, registryConfig.provider, registryConfig.serverAddress, registryConfig.username, registryConfig.email || null, registryConfig.password ]); + } + + await db.runSql('DELETE settings WHERE name=?', [ 'registry_config']); + await db.runSql('COMMIT'); +}; + +exports.down = async function (db) { + await db.runSql('DROP TABLE dockerRegistries'); +}; diff --git a/migrations/schema.sql b/migrations/schema.sql index 1a51a2df4..b2d3e2528 100644 --- a/migrations/schema.sql +++ b/migrations/schema.sql @@ -293,6 +293,17 @@ CREATE TABLE IF NOT EXISTS appPasswords( PRIMARY KEY (id) ); +CREATE TABLE IF NOT EXISTS dockerRegistries( + id VARCHAR(128) NOT NULL UNIQUE, + provider VARCHAR(16) NOT NULL, + serverAddress VARCHAR(128) NOT NULL, + username VARCHAR(128), + email VARCHAR(128), + password VARCHAR(128), + + PRIMARY KEY (id) +); + CREATE TABLE IF NOT EXISTS volumes( id VARCHAR(128) NOT NULL UNIQUE, name VARCHAR(256) NOT NULL UNIQUE, diff --git a/src/docker.js b/src/docker.js index 0ee563753..a92c600dd 100644 --- a/src/docker.js +++ b/src/docker.js @@ -1,10 +1,6 @@ 'use strict'; exports = module.exports = { - removePrivateFields, - getRegistryConfig, - setRegistryConfig, - ping, info, @@ -41,42 +37,19 @@ const apps = require('./apps.js'), dashboard = require('./dashboard.js'), debug = require('debug')('box:docker'), Docker = require('dockerode'), + dockerRegistries = require('./dockerregistries.js'), fs = require('fs'), mailServer = require('./mailserver.js'), os = require('os'), paths = require('./paths.js'), promiseRetry = require('./promise-retry.js'), services = require('./services.js'), - settings = require('./settings.js'), shell = require('./shell.js')('docker'), safe = require('safetydance'), timers = require('timers/promises'), - tld = require('tldjs'), volumes = require('./volumes.js'); -const DOCKER_SOCKET_PATH = '/var/run/docker.sock'; -const gConnection = new Docker({ socketPath: DOCKER_SOCKET_PATH }); - -async function testRegistryConfig(config) { - assert.strictEqual(typeof config, 'object'); - - if (config.provider === 'noop') return; - - const [error] = await safe(gConnection.checkAuth(config)); // this returns a 500 even for auth errors - if (error) throw new BoxError(BoxError.BAD_FIELD, `Invalid serverAddress: ${error.message}`); -} - -function injectPrivateFields(newConfig, currentConfig) { - if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password; -} - -function removePrivateFields(registryConfig) { - assert.strictEqual(typeof registryConfig, 'object'); - - if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER; - - return registryConfig; -} +const gConnection = new Docker({ socketPath: paths.DOCKER_SOCKET_PATH }); function parseImageRef(imageRef) { assert.strictEqual(typeof imageRef, 'string'); @@ -101,7 +74,7 @@ function parseImageRef(imageRef) { async function ping() { // do not let the request linger - const connection = new Docker({ socketPath: DOCKER_SOCKET_PATH, timeout: 1000 }); + const connection = new Docker({ socketPath: paths.DOCKER_SOCKET_PATH, timeout: 1000 }); const [error, result] = await safe(connection.ping()); if (error) throw new BoxError(BoxError.DOCKER_ERROR, error); @@ -119,24 +92,27 @@ async function getAuthConfig(imageRef) { // images in our cloudron namespace are always unauthenticated to not interfere with any user limits if (parsedRef.registry === null && parsedRef.fullRepositoryName.startsWith('cloudron/')) return null; - const registryConfig = await getRegistryConfig(); - if (registryConfig.provider === 'noop') return null; + const registries = await dockerRegistries.list(); - if (registryConfig.serverAddress !== parsedRef.registry) { // ideally they match but there's too many docker registry domains! - if (!registryConfig.serverAddress.includes('.docker.')) return null; - if (parsedRef.registry !== null && !parsedRef.includes('.docker.')) return null; + for (const registry of registries) { + if (registry.serverAddress !== parsedRef.registry) { // ideally they match but there's too many docker registry domains! + if (!registry.serverAddress.includes('.docker.')) continue; + if (parsedRef.registry !== null && !parsedRef.includes('.docker.')) continue; + } + + // https://github.com/apocas/dockerode#pull-from-private-repos + const authConfig = { + username: registry.username, + password: registry.password, + auth: registry.auth || '', // the auth token at login time + email: registry.email || '', + serveraddress: registry.serverAddress + }; + + return authConfig; } - // https://github.com/apocas/dockerode#pull-from-private-repos - const autoConfig = { - username: registryConfig.username, - password: registryConfig.password, - auth: registryConfig.auth || '', // the auth token at login time - email: registryConfig.email || '', - serveraddress: registryConfig.serverAddress - }; - - return autoConfig; + return null; } async function pullImage(imageRef) { @@ -722,36 +698,3 @@ async function update(name, memory) { throw new BoxError(BoxError.DOCKER_ERROR, 'Unable to update container'); } - -async function getRegistryConfig() { - const value = await settings.getJson(settings.REGISTRY_CONFIG_KEY); - return value || { provider: 'noop' }; -} - -function validateServerAddress(serverAddress) { - assert.strictEqual(typeof serverAddress, 'string'); - - // workaround https://github.com/oncletom/tld.js/issues/73 - const tmp = serverAddress.replace('_', '-'); - if (!tld.isValid(tmp)) return new BoxError(BoxError.BAD_FIELD, 'Hostname is not a valid domain name'); - if (tmp.length > 253) return new BoxError(BoxError.BAD_FIELD, 'Hostname length exceeds 253 characters'); - - return null; -} - -async function setRegistryConfig(registryConfig) { - assert.strictEqual(typeof registryConfig, 'object'); - - if (registryConfig.provider !== 'noop') { - const error = validateServerAddress(registryConfig.serverAddress); - if (error) throw error; - } - - const currentConfig = await getRegistryConfig(); - - injectPrivateFields(registryConfig, currentConfig); - - await testRegistryConfig(registryConfig); - - await settings.setJson(settings.REGISTRY_CONFIG_KEY, registryConfig); -} diff --git a/src/dockerregistries.js b/src/dockerregistries.js new file mode 100644 index 000000000..01b706177 --- /dev/null +++ b/src/dockerregistries.js @@ -0,0 +1,123 @@ +'use strict'; + +exports = module.exports = { + removePrivateFields, + + list, + add, + get, + del, + update, +}; + +const assert = require('assert'), + BoxError = require('./boxerror.js'), + constants = require('./constants.js'), + database = require('./database.js'), + Docker = require('dockerode'), + eventlog = require('./eventlog.js'), + paths = require('./paths.js'), + safe = require('safetydance'), + tld = require('tldjs'), + uuid = require('uuid'); + +const REGISTRY_FIELDS = [ 'id', 'provider', 'serverAddress', 'username', 'email', 'password' ].join(','); + +function removePrivateFields(registryConfig) { + assert.strictEqual(typeof registryConfig, 'object'); + + if (registryConfig.password) registryConfig.password = constants.SECRET_PLACEHOLDER; + + return registryConfig; +} + +async function get(id) { + assert.strictEqual(typeof id, 'string'); + + const result = await database.query(`SELECT ${REGISTRY_FIELDS} FROM dockerRegistries WHERE id=?`, [ id ]); + if (result.length === 0) return null; + return result[0]; +} + +async function list() { + const result = await database.query(`SELECT ${REGISTRY_FIELDS} FROM dockerRegistries`); + return result; +} + +function validateServerAddress(serverAddress) { + assert.strictEqual(typeof serverAddress, 'string'); + + // workaround https://github.com/oncletom/tld.js/issues/73 + const tmp = serverAddress.replace('_', '-'); + if (!tld.isValid(tmp)) return new BoxError(BoxError.BAD_FIELD, 'Hostname is not a valid domain name'); + if (tmp.length > 253) return new BoxError(BoxError.BAD_FIELD, 'Hostname length exceeds 253 characters'); + + return null; +} + +async function testRegistryConfig(config) { + assert.strictEqual(typeof config, 'object'); + + if (constants.TEST) return; + + const connection = new Docker({ socketPath: paths.DOCKER_SOCKET_PATH, timeout: 3000 }); + const [error] = await safe(connection.checkAuth(config)); // this returns a 500 even for auth errors + if (error) throw new BoxError(BoxError.BAD_FIELD, `Invalid serverAddress: ${error.message}`); +} + +function injectPrivateFields(newConfig, currentConfig) { + if (newConfig.password === constants.SECRET_PLACEHOLDER) newConfig.password = currentConfig.password; +} + +async function add(registry, auditSource) { + assert.strictEqual(typeof registry, 'object'); + assert(auditSource && typeof auditSource === 'object'); + + const error = validateServerAddress(registry.serverAddress); + if (error) throw error; + + await testRegistryConfig(registry); + + const id = `rc-${uuid.v4()}`; + await database.query('INSERT INTO dockerRegistries (id, provider, serverAddress, username, email, password) VALUES (?, ?, ?, ?, ?, ?)', + [ id , registry.provider, registry.serverAddress, registry.username || null, registry.email || null, registry.password || null ]); + + await eventlog.add(eventlog.ACTION_REGISTRY_ADD, auditSource, { registry: removePrivateFields(registry) }); + + return id; +} + +async function update(oldConfig, newConfig, auditSource) { + assert.strictEqual(typeof oldConfig, 'object'); + assert.strictEqual(typeof newConfig, 'object'); + assert(auditSource && typeof auditSource === 'object'); + + const error = validateServerAddress(newConfig.serverAddress); + if (error) throw error; + + injectPrivateFields(newConfig, oldConfig); + + await testRegistryConfig(newConfig); + + const args = [], fields = []; + for (const k in newConfig) { + fields.push(k + ' = ?'); + args.push(newConfig[k]); + } + args.push(oldConfig.id); + + const result = await database.query('UPDATE dockerRegistries SET ' + fields.join(', ') + ' WHERE id=?', args); + if (result.affectedRows === 0) throw new BoxError(BoxError.NOT_FOUND, 'Registry not found'); + + await eventlog.add(eventlog.ACTION_REGISTRY_DEL, auditSource, { oldRegistry: removePrivateFields(oldConfig), newRegistry: removePrivateFields(newConfig) }); +} + +async function del(registry, auditSource) { + assert.strictEqual(typeof registry, 'object'); + assert(auditSource && typeof auditSource === 'object'); + + const result = await database.query(`DELETE FROM dockerRegistries WHERE id=?`, [ registry.id ]); + if (result.affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Registry not found'); + + await eventlog.add(eventlog.ACTION_REGISTRY_DEL, auditSource, { registry: removePrivateFields(registry) }); +} diff --git a/src/eventlog.js b/src/eventlog.js index 930543dd9..a6d6d8f6d 100644 --- a/src/eventlog.js +++ b/src/eventlog.js @@ -77,6 +77,10 @@ exports = module.exports = { ACTION_RESTORE: 'cloudron.restore', // unused ACTION_START: 'cloudron.start', + ACTION_REGISTRY_ADD: 'registry.add', + ACTION_REGISTRY_UPDATE: 'registry.update', + ACTION_REGISTRY_DEL: 'registry.del', + ACTION_SERVICE_CONFIGURE: 'service.configure', ACTION_SERVICE_REBUILD: 'service.rebuild', ACTION_SERVICE_RESTART: 'service.restart', diff --git a/src/paths.js b/src/paths.js index 4c55974a8..417407fb6 100644 --- a/src/paths.js +++ b/src/paths.js @@ -26,6 +26,7 @@ exports = module.exports = { DEFAULT_BACKUP_DIR: '/var/backups', VOLUMES_MOUNT_DIR: '/mnt/volumes', MANAGED_BACKUP_MOUNT_DIR: '/mnt/cloudronbackup', + DOCKER_SOCKET_PATH: '/var/run/docker.sock', PLATFORM_DATA_DIR: path.join(baseDir(), 'platformdata'), APPS_DATA_DIR: path.join(baseDir(), 'appsdata'), diff --git a/src/routes/docker.js b/src/routes/docker.js deleted file mode 100644 index c08883c17..000000000 --- a/src/routes/docker.js +++ /dev/null @@ -1,37 +0,0 @@ -'use strict'; - -exports = module.exports = { - getRegistryConfig, - setRegistryConfig -}; - -const assert = require('assert'), - BoxError = require('../boxerror.js'), - docker = require('../docker.js'), - HttpError = require('connect-lastmile').HttpError, - HttpSuccess = require('connect-lastmile').HttpSuccess, - safe = require('safetydance'); - -async function getRegistryConfig(req, res, next) { - const [error, registryConfig] = await safe(docker.getRegistryConfig()); - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(200, docker.removePrivateFields(registryConfig))); -} - -async function setRegistryConfig(req, res, next) { - assert.strictEqual(typeof req.body, 'object'); - - if (!req.body.provider || typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required')); - if (req.body.provider !== 'noop') { - if (typeof req.body.serverAddress !== 'string') return next(new HttpError(400, 'serverAddress is required')); - if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username is required')); - if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email is required')); - if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password is required')); - } - - const [error] = await safe(docker.setRegistryConfig(req.body)); - if (error) return next(BoxError.toHttpError(error)); - - next(new HttpSuccess(200)); -} diff --git a/src/routes/dockerregistries.js b/src/routes/dockerregistries.js new file mode 100644 index 000000000..6ae41a0d4 --- /dev/null +++ b/src/routes/dockerregistries.js @@ -0,0 +1,86 @@ +'use strict'; + +exports = module.exports = { + list, + add, + get, + del, + update, + load +}; + +const assert = require('assert'), + AuditSource = require('../auditsource.js'), + BoxError = require('../boxerror.js'), + dockerRegistries = require('../dockerregistries.js'), + HttpError = require('connect-lastmile').HttpError, + HttpSuccess = require('connect-lastmile').HttpSuccess, + safe = require('safetydance'); + +async function load(req, res, next) { + assert.strictEqual(typeof req.params.id, 'string'); + + const [error, result] = await safe(dockerRegistries.get(req.params.id)); + if (error) return next(BoxError.toHttpError(error)); + if (!result) return next(new HttpError(404, 'Registry not found')); + req.resource = result; + + next(); +} + +async function list(req, res, next) { + assert.strictEqual(typeof req.user, 'object'); + + const [error, results] = await safe(dockerRegistries.list()); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(200, { registries: results.map(dockerRegistries.removePrivateFields) })); +} + +function get(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); + assert.strictEqual(typeof req.user, 'object'); + + next(new HttpSuccess(200, dockerRegistries.removePrivateFields(req.resource))); +} + +async function add(req, res, next) { + assert.strictEqual(typeof req.body, 'object'); + + if (!req.body.provider || typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required')); + if (typeof req.body.serverAddress !== 'string') return next(new HttpError(400, 'serverAddress is required')); + if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string')); + if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a string')); + if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string')); + + const [error, id] = await safe(dockerRegistries.add(req.body, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(201, { id })); +} + +async function update(req, res, next) { + assert.strictEqual(typeof req.params.id, 'string'); + assert.strictEqual(typeof req.body, 'object'); + assert.strictEqual(typeof req.resource, 'object'); + + if (!req.body.provider || typeof req.body.provider !== 'string') return next(new HttpError(400, 'provider is required')); + if (typeof req.body.serverAddress !== 'string') return next(new HttpError(400, 'serverAddress is required')); + if ('username' in req.body && typeof req.body.username !== 'string') return next(new HttpError(400, 'username must be a string')); + if ('email' in req.body && typeof req.body.email !== 'string') return next(new HttpError(400, 'email must be a string')); + if ('password' in req.body && typeof req.body.password !== 'string') return next(new HttpError(400, 'password must be a string')); + + const [updateError] = await safe(dockerRegistries.update(req.resource, req.body, AuditSource.fromRequest(req))); + if (updateError) return next(BoxError.toHttpError(updateError)); + + next(new HttpSuccess(204, {})); +} + +async function del(req, res, next) { + assert.strictEqual(typeof req.resource, 'object'); + + const [error] = await safe(dockerRegistries.del(req.resource, AuditSource.fromRequest(req))); + if (error) return next(BoxError.toHttpError(error)); + + next(new HttpSuccess(204)); +} diff --git a/src/routes/index.js b/src/routes/index.js index 141a05c76..c1df63279 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -13,7 +13,7 @@ exports = module.exports = { cloudron: require('./cloudron.js'), dashboard: require('./dashboard.js'), directoryServer: require('./directoryserver.js'), - docker: require('./docker.js'), + dockerRegistries: require('./dockerregistries.js'), domains: require('./domains.js'), eventlog: require('./eventlog.js'), externalLdap: require('./externalldap.js'), diff --git a/src/routes/test/common.js b/src/routes/test/common.js index a6d1c6cff..067e5a9b3 100644 --- a/src/routes/test/common.js +++ b/src/routes/test/common.js @@ -134,7 +134,7 @@ async function setup() { await timers.setTimeout(2000); - // create admin + // create owner response = await superagent.post(`${serverUrl}/api/v1/provision/activate`) .query({ setupToken: 'somesetuptoken' }) .send({ username: owner.username, password: owner.password, email: owner.email }); diff --git a/src/routes/test/dockerregistries-test.js b/src/routes/test/dockerregistries-test.js new file mode 100644 index 000000000..6f5122962 --- /dev/null +++ b/src/routes/test/dockerregistries-test.js @@ -0,0 +1,85 @@ +/* jslint node:true */ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +const common = require('./common.js'), + constants = require('../../constants.js'), + expect = require('expect.js'), + superagent = require('../../superagent.js'); + +describe('Docker Registries', function () { + const { setup, cleanup, serverUrl, owner } = common; + + before(setup); + after(cleanup); + + it('cannot add registry with invalid token', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/docker/registries`) + .query({ access_token: owner.token + 'xx' }) + .send({ provider: 'devops', serverAddress: 'registry.devops' }) + .ok(() => true); + + expect(response.status).to.equal(401); + }); + + it('cannot add registry without provider', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/docker/registries`) + .query({ access_token: owner.token }) + .send({}) + .ok(() => true); + + expect(response.status).to.equal(400); + }); + + let id; + it('can add registry', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/docker/registries`) + .query({ access_token: owner.token }) + .send({ provider: 'devops', serverAddress: 'registry.devops', password: 'hushhush' }); + + expect(response.status).to.equal(201); + expect(response.body.id).to.be.a('string'); + id = response.body.id; + }); + + it('can list registries', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/docker/registries`) + .query({ access_token: owner.token }); + + expect(response.status).to.equal(200); + expect(response.body.registries).to.be.an(Array); + expect(response.body.registries.length).to.be(1); + expect(response.body.registries[0].provider).to.be('devops'); + expect(response.body.registries[0].serverAddress).to.be('registry.devops'); + expect(response.body.registries[0].password).to.be(constants.SECRET_PLACEHOLDER); + }); + + it('can update registry', async function () { + const response = await superagent.post(`${serverUrl}/api/v1/docker/registries/${id}`) + .query({ access_token: owner.token }) + .send({ provider: 'devops2', serverAddress: 'registry.devops2', password: 'hushhush2' }); + + expect(response.status).to.equal(204); + }); + + it('can get registry', async function () { + const response = await superagent.get(`${serverUrl}/api/v1/docker/registries/${id}`) + .query({ access_token: owner.token }); + + expect(response.status).to.equal(200); + expect(response.body.provider).to.be('devops2'); + expect(response.body.serverAddress).to.be('registry.devops2'); + expect(response.body.password).to.be(constants.SECRET_PLACEHOLDER); + }); + + it('can del registry', async function () { + const response = await superagent.del(`${serverUrl}/api/v1/docker/registries/${id}`) + .query({ access_token: owner.token }); + + expect(response.status).to.equal(204); + }); +}); diff --git a/src/server.js b/src/server.js index e5ef7022e..c7724e626 100644 --- a/src/server.js +++ b/src/server.js @@ -339,9 +339,12 @@ async function initializeExpressSync() { router.get ('/api/v1/network/ipv4', token, authorizeAdmin, routes.network.getIPv4); router.get ('/api/v1/network/ipv6', token, authorizeAdmin, routes.network.getIPv6); - // docker - router.get ('/api/v1/docker/registry_config', token, authorizeAdmin, routes.docker.getRegistryConfig); - router.post('/api/v1/docker/registry_config', json, token, authorizeAdmin, routes.docker.setRegistryConfig); + // private docker registry + router.get ('/api/v1/docker/registries', token, authorizeAdmin, routes.dockerRegistries.list); + router.post('/api/v1/docker/registries', json, token, authorizeAdmin, routes.dockerRegistries.add); + router.get ('/api/v1/docker/registries/:id', token, routes.dockerRegistries.load, authorizeAdmin, routes.dockerRegistries.get); + router.post('/api/v1/docker/registries/:id', json, token, routes.dockerRegistries.load, authorizeAdmin, routes.dockerRegistries.update); + router.del ('/api/v1/docker/registries/:id', token, routes.dockerRegistries.load, authorizeAdmin, routes.dockerRegistries.del); // email routes router.get ('/api/v1/mailserver/eventlog', token, authorizeAdmin, routes.mailserver.proxy); diff --git a/src/settings.js b/src/settings.js index 04f272b01..7561e6434 100644 --- a/src/settings.js +++ b/src/settings.js @@ -36,7 +36,6 @@ exports = module.exports = { MAIL_SUBDOMAIN_KEY: 'mail_subdomain', OIDC_COOKIE_SECRET_KEY: 'cookie_secret', PROFILE_CONFIG_KEY: 'profile_config', - REGISTRY_CONFIG_KEY: 'registry_config', REVERSE_PROXY_CONFIG_KEY: 'reverseproxy_config', SERVICES_CONFIG_KEY: 'services_config', TIME_ZONE_KEY: 'time_zone', diff --git a/src/test/dockerregistries-test.js b/src/test/dockerregistries-test.js new file mode 100644 index 000000000..3cdc9ecd6 --- /dev/null +++ b/src/test/dockerregistries-test.js @@ -0,0 +1,66 @@ +/* jslint node:true */ +/* global it:false */ +/* global describe:false */ +/* global before:false */ +/* global after:false */ + +'use strict'; + +const dockerRegistries = require('../dockerregistries.js'), + BoxError = require('../boxerror.js'), + common = require('./common.js'), + expect = require('expect.js'), + safe = require('safetydance'); + +describe('Docker Registries', function () { + const { setup, cleanup, auditSource } = common; + + before(setup); + after(cleanup); + + let id; + + it('cannot get random', async function () { + const registry = await dockerRegistries.get('srandom'); + expect(registry).to.be(null); + }); + + it('can add', async function () { + id = await dockerRegistries.add({ provider: 'devops', serverAddress: 'registry.devops.test', password: 'hushhush' }, auditSource); + expect(id).to.be.a('string'); + }); + + it('can get', async function () { + const registry = await dockerRegistries.get(id); + expect(registry.provider).to.be('devops'); + expect(registry.serverAddress).to.be('registry.devops.test'); + expect(registry.password).to.be('hushhush'); + }); + + it('can list', async function () { + const registries = await dockerRegistries.list(); + expect(registries.length).to.be(1); + expect(registries[0].provider).to.be('devops'); + expect(registries[0].serverAddress).to.be('registry.devops.test'); + expect(registries[0].password).to.be('hushhush'); + }); + + it('can update', async function () { + await dockerRegistries.update(await dockerRegistries.get(id), { provider: 'cicd', serverAddress: 'registry.cicd' }, auditSource); + const registry = await dockerRegistries.get(id); + expect(registry.provider).to.be('cicd'); + expect(registry.serverAddress).to.be('registry.cicd'); + expect(registry.password).to.be('hushhush'); // injected from the older one + }); + + it('can del', async function () { + await dockerRegistries.del(await dockerRegistries.get(id), auditSource); + const registries = await dockerRegistries.list(); + expect(registries.length).to.be(0); + }); + + it('cannot del random', async function () { + const [error] = await safe(dockerRegistries.del({ id: 'fake' }, auditSource)); + expect(error.reason).to.be(BoxError.NOT_FOUND); + }); +});