diff --git a/src/apphealthmonitor.js b/src/apphealthmonitor.js index fb3d3e8b3..5ca0c1d91 100644 --- a/src/apphealthmonitor.js +++ b/src/apphealthmonitor.js @@ -70,17 +70,25 @@ async function checkAppHealth(app, options) { const manifest = app.manifest; - const [error, data] = await safe(docker.inspect(app.containerId)); - if (error || !data || !data.State) return await setHealth(app, apps.HEALTH_ERROR); - if (data.State.Running !== true) return await setHealth(app, apps.HEALTH_DEAD); + let healthCheckUrl, host; + if (app.manifest.id === constants.RELAY_APPSTORE_ID) { + healthCheckUrl = 'http://example.com'; + host = ''; + } else { + const [error, data] = await safe(docker.inspect(app.containerId)); + if (error || !data || !data.State) return await setHealth(app, apps.HEALTH_ERROR); + if (data.State.Running !== true) return await setHealth(app, apps.HEALTH_DEAD); - // non-appstore apps may not have healthCheckPath - if (!manifest.healthCheckPath) return await setHealth(app, apps.HEALTH_HEALTHY); + // non-appstore apps may not have healthCheckPath + if (!manifest.healthCheckPath) return await setHealth(app, apps.HEALTH_HEALTHY); + + healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`; + host = app.fqdn; + } - const healthCheckUrl = `http://${app.containerIp}:${manifest.httpPort}${manifest.healthCheckPath}`; const [healthCheckError, response] = await safe(superagent .get(healthCheckUrl) - .set('Host', app.fqdn) // required for some apache configs with rewrite rules + .set('Host', host) // required for some apache configs with rewrite rules .set('User-Agent', 'Mozilla (CloudronHealth)') // required for some apps (e.g. minio) .redirects(0) .ok(() => true) diff --git a/src/apptask.js b/src/apptask.js index 2f254c273..c2f0dccea 100644 --- a/src/apptask.js +++ b/src/apptask.js @@ -74,6 +74,8 @@ async function updateApp(app, values) { async function allocateContainerIp(app) { assert.strictEqual(typeof app, 'object'); + if (app.manifest.id === constants.RELAY_APPSTORE_ID) return; + await promiseRetry({ times: 10, interval: 0, debug }, async function () { const iprange = iputils.intFromIp('172.18.20.255') - iputils.intFromIp('172.18.16.1'); let rnd = Math.floor(Math.random() * iprange); @@ -86,6 +88,8 @@ async function createContainer(app) { assert.strictEqual(typeof app, 'object'); assert(!app.containerId); // otherwise, it will trigger volumeFrom + if (app.manifest.id === constants.RELAY_APPSTORE_ID) return; + debug('createContainer: creating container'); const container = await docker.createContainer(app); @@ -290,6 +294,9 @@ async function moveDataDir(app, targetVolumeId, targetVolumePrefix) { async function downloadImage(manifest) { assert.strictEqual(typeof manifest, 'object'); + // skip for relay app + if (manifest.id === constants.RELAY_APPSTORE_ID) return; + const info = await docker.info(); const [dfError, diskUsage] = await safe(df.file(info.DockerRootDir)); if (dfError) throw new BoxError(BoxError.FS_ERROR, `Error getting file system info: ${dfError.message}`); @@ -304,6 +311,9 @@ async function startApp(app) { if (app.runState === apps.RSTATE_STOPPED) return; + // skip for relay app + if (app.manifest.id === constants.RELAY_APPSTORE_ID) return; + await docker.startContainer(app.id); } @@ -713,8 +723,10 @@ async function restart(app, args, progressCallback) { assert.strictEqual(typeof args, 'object'); assert.strictEqual(typeof progressCallback, 'function'); - await progressCallback({ percent: 20, message: 'Restarting container' }); - await docker.restartContainer(app.id); + if (app.manifest.id !== constants.RELAY_APPSTORE_ID) { + await progressCallback({ percent: 20, message: 'Restarting container' }); + await docker.restartContainer(app.id); + } await progressCallback({ percent: 100, message: 'Done' }); await updateApp(app, { installationState: apps.ISTATE_INSTALLED, error: null, health: null }); diff --git a/src/constants.js b/src/constants.js index 456f30b8e..91510fd71 100644 --- a/src/constants.js +++ b/src/constants.js @@ -49,6 +49,8 @@ exports = module.exports = { ], DEMO_APP_LIMIT: 20, + RELAY_APPSTORE_ID: 'io.cloudron.builtin.appproxy', + AUTOUPDATE_PATTERN_NEVER: 'never', // the db field is a blob so we make this explicit diff --git a/src/nginxconfig.ejs b/src/nginxconfig.ejs index de8474c57..23640285f 100644 --- a/src/nginxconfig.ejs +++ b/src/nginxconfig.ejs @@ -45,7 +45,7 @@ server { location / { <% if ( endpoint === 'dashboard' || endpoint === 'setup' ) { %> return 301 https://$host$request_uri; -<% } else if ( endpoint === 'app' ) { %> +<% } else if ( endpoint === 'app' || endpoint === 'external' ) { %> return 301 https://$host$request_uri; <% } else if ( endpoint === 'redirect' ) { %> return 301 https://<%= redirectTo %>$request_uri; @@ -175,6 +175,8 @@ server { proxy_pass http://127.0.0.1:3000; <% } else if ( endpoint === 'app' ) { %> proxy_pass http://<%= ip %>:<%= port %>; +<% } else if ( endpoint === 'external' ) { %> + proxy_pass <%= upstreamUri %>; <% } else if ( endpoint === 'redirect' ) { %> return 302 https://<%= redirectTo %>$request_uri; <% } %> @@ -326,6 +328,10 @@ server { # to clear a permanent redirect on the browser return 302 https://<%= redirectTo %>$request_uri; } +<% } else if ( endpoint === 'external' ) { %> + location / { + proxy_pass <%= upstreamUri %>; + } <% } else if ( endpoint === 'ip' ) { %> location /notfound.html { root <%= sourceDir %>/dashboard/dist; diff --git a/src/reverseproxy.js b/src/reverseproxy.js index 566c68f8d..dd9e5ce96 100644 --- a/src/reverseproxy.js +++ b/src/reverseproxy.js @@ -501,11 +501,18 @@ async function writeAppNginxConfig(app, fqdn, type, bundle) { cspQuoted: null, hideHeaders: [], proxyAuth: { enabled: false }, + upstreamUri: '', // only for endpoint === external ocsp: await isOcspEnabled(bundle.certFilePath) }; if (type === apps.LOCATION_TYPE_PRIMARY || type === apps.LOCATION_TYPE_ALIAS || type === apps.LOCATION_TYPE_SECONDARY) { data.endpoint = 'app'; + + if (app.manifest.id === constants.RELAY_APPSTORE_ID) { + data.endpoint = 'external'; + data.upstreamUri = 'http://example.com'; + } + // maybe these should become per domain at some point const reverseProxyConfig = app.reverseProxyConfig || {}; // some of our code uses fake app objects if (reverseProxyConfig.robotsTxt) data.robotsTxtQuoted = JSON.stringify(app.reverseProxyConfig.robotsTxt);