Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

disable loading page feature #58

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The following top-level properties can be configured:
Property | Meaning
---------|--------|
`proxyListeningPort` | The port ContainerNursery should listen on for new http connections. Defaults to `80`.
`disableDefaultLoadingPage` | Boolean for disable the default loading page. By disabling the default loading page the request waits for the container and responds when the container is ready. Defaults to `false`.

The virtual hosts the proxy should handle can be configured by adding an object to the `proxyHosts` key.

Expand All @@ -68,6 +69,8 @@ The following properties are optional:
| `proxyUseHttps` | Boolean indicating if the proxy should use HTTPS to connect to the container. Defaults to `false`. This should only be used if the container only accepts HTTPS requests. It provides no additional security. |
| `stopOnTimeoutIfCpuUsageBelow` | If set, prevents the container from stopping when reaching the configured timeout if the averaged CPU usage (percentage between 0 and 100*core count) of the **main** container (first in the list of container names) is above this value. This is great for containers that should remain running while their doing intensive work even when nobody is doing any http requests, for example handbrake. |
| `proxyUseCustomMethod` | Can be set to a HTTP method (`HEAD`,`GET`, ...) which should be used for the ready check. Some services only respond to certain HTTP methods correctly. |
| `enableDefaultLoadingPage` | Boolean for enable the default loading page when `disableDefaultLoadingPage` is set to `true`. |
| `customHttpStatusReadyChecking` | Custom Http status number use to check if the container is ready and running. |

### Example Configuration
```yaml
Expand Down
2 changes: 2 additions & 0 deletions config/config.yml.example
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
proxyListeningPort: 80
disableDefaultLoadingPage: true
proxyHosts:
- domain: handbrake.yourdomain.io
containerName: handbrake
Expand All @@ -7,6 +8,7 @@ proxyHosts:
proxyPort: 5800
timeoutSeconds: 15
stopOnTimeoutIfCpuUsageBelow: 50
enableDefaultLoadingPage: true
- domain:
- wordpress.yourdomain.io
- wordpress.otherdomain.io
Expand Down
21 changes: 21 additions & 0 deletions src/ConfigManager.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,21 +18,27 @@ type ProxyHostConfig = {
proxyUseCustomMethod: string
timeoutSeconds: number
stopOnTimeoutIfCpuUsageBelow?: number
disableDefaultLoadingPage?: boolean
enableDefaultLoadingPage?: boolean
customHttpStatusReadyChecking?: number
gi4no marked this conversation as resolved.
Show resolved Hide resolved
}
type ApplicationConfig = {
proxyListeningPort: number,
disableDefaultLoadingPage: boolean,
proxyHosts: ProxyHostConfig[]
}

export default class ConfigManager {
private configFile = 'config/config.yml';
private proxyHosts: Map<string, ProxyHost>;
private proxyListeningPort: number | null;
public disableDefaultLoadingPage: boolean | undefined;
private eventEmitter: EventEmitter;

constructor(proxyHosts: Map<string, ProxyHost>) {
this.proxyHosts = proxyHosts;
this.proxyListeningPort = null;
this.disableDefaultLoadingPage = false;
this.eventEmitter = new EventEmitter();
this.createIfNotExist();
this.parseConfig();
Expand Down Expand Up @@ -96,6 +102,9 @@ export default class ConfigManager {
if (!config || !config.proxyHosts) {
logger.error({ invalidProperty: 'proxyHosts' }, 'Config is invalid, missing property');
} else {
if (config.disableDefaultLoadingPage) {
this.disableDefaultLoadingPage = config.disableDefaultLoadingPage;
}
this.loadProxyHosts(config.proxyHosts);
if (config.proxyListeningPort) {
const prevPort = this.proxyListeningPort;
Expand Down Expand Up @@ -141,6 +150,18 @@ export default class ConfigManager {
proxyHost.stopOnTimeoutIfCpuUsageBelow = proxyHostConfig.stopOnTimeoutIfCpuUsageBelow as number;
}

if (this.disableDefaultLoadingPage && !proxyHostConfig.enableDefaultLoadingPage) {
proxyHost.disableDefaultLoadingPage = this.disableDefaultLoadingPage;
}

if (proxyHostConfig.disableDefaultLoadingPage) {
proxyHost.disableDefaultLoadingPage = proxyHostConfig.disableDefaultLoadingPage;
}

if (proxyHostConfig.customHttpStatusReadyChecking) {
proxyHost.customHttpStatusReadyChecking = proxyHostConfig.customHttpStatusReadyChecking;
}

if (proxyHostConfig.domain instanceof Array) {
proxyHostConfig.domain.forEach(domain => {
this.proxyHosts.set(
Expand Down
20 changes: 19 additions & 1 deletion src/ProxyHost.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@ export default class ProxyHost {
private proxyHost: string;
private proxyPort: number;
public proxyUseHttps = false;
public customHttpStatusReadyChecking: number | undefined
public proxyUseCustomMethod: string | undefined;
private timeoutSeconds: number;
public stopOnTimeoutIfCpuUsageBelow = Infinity;
public disableDefaultLoadingPage = false
public enableDefaultLoadingPage = false

private activeSockets: Set<internal.Duplex> = new Set();
private containerEventEmitter: EventEmitter | null = null;
Expand Down Expand Up @@ -137,6 +140,18 @@ export default class ProxyHost {
this.startingHost = false;
}

public async isContainerRunning():Promise<boolean> {
let interval:NodeJS.Timer;
return new Promise((resolve) => {
interval = setInterval(async () => {
if (this.containerRunning) {
resolve(true);
clearInterval(interval);
}
}, 250);
});
}
Comment on lines +143 to +153
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is not guaranteed to always work in my opinion. It might fail if the containers webserver needs some time to start and isn't ready yet to accept a connection.
This is the reason we have used requests & status code validations before redirecting.
What do you think?

Copy link
Author

@gi4no gi4no Apr 26, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I use the containerRunning because it's set to true in the checkContainerReady method (see https://github.com/ItsEcholot/ContainerNursery/blob/main/src/ProxyHost.ts#L161), so I think it's appropriate to use it. Additionally, I've been running some tests with my container, and it works fine for me. let me know!

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry I missed this reply.
containerRunning does indeed guarantee that the container is running, but this does not guarantee that the application inside the container (in our case most likely a webserver of some sorts) has started up and is ready to accept connections.
While this might work fine for a lot of containers it is in my opinion brittle.
You would need to implement an actual request check (as we already do on the waiting page) to ensure the webserver is ready for our request.


private checkContainerReady() {
if (this.containerReadyChecking) return;

Expand All @@ -155,7 +170,10 @@ export default class ProxyHost {
headers: res.headers
}, 'Checked if target is ready');

if (res.status === 200 || (res.status >= 300 && res.status <= 399)) {
if (
res.status === 200 || (res.status >= 300 && res.status <= 399)
|| (this.customHttpStatusReadyChecking && res.status === this.customHttpStatusReadyChecking)
) {
clearInterval(checkInterval);
this.containerReadyChecking = false;
this.containerRunning = true;
Expand Down
14 changes: 12 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -120,8 +120,18 @@ placeholderServer.use((_, res, next) => {
res.setHeader('x-powered-by', 'ContainerNursery');
next();
});
placeholderServer.get('*', (req, res) => {
res.render('placeholder', { containerName: req.headers['x-container-nursery-container-name'] });
placeholderServer.all('*', async (req, res) => {
const proxyHost = proxyHosts.get(req.headers.host as string);
if (proxyHost?.disableDefaultLoadingPage) {
await proxyHost?.isContainerRunning();
proxyHost?.newConnection();
proxy.web(req, res, {
target: proxyHost?.getTarget(),
headers: proxyHost?.getHeaders()
});
} else {
res.render('placeholder', { containerName: req.headers['x-container-nursery-container-name'] });
}
});
placeholderServer.listen(placeholderServerListeningPort, placeholderServerListeningHost);
logger.info({ port: placeholderServerListeningPort, host: placeholderServerListeningHost }, 'Proxy placeholder server listening');
Expand Down