Add filtering by container labels
This commit adds the ability to filter containers by their labels in the Docker integration. The new functionality allows users to specify container labels in the configuration, instead of container names, to allow for dynamic container name resolution. This is particularly useful in environments where container names may change frequently or are not defined in advance. Fixes #193 Signed-off-by: Georgios Ntoutsos <gntouts@nubificus.co.uk>
This commit is contained in:
parent
6b63fe209f
commit
5b3b4eb240
@ -6,6 +6,11 @@ import log from '~/utils/log';
|
||||
import type { HeadplaneConfig } from '../schema';
|
||||
import { Integration } from './abstract';
|
||||
|
||||
interface DockerContainer {
|
||||
Id: string;
|
||||
Names: string[];
|
||||
}
|
||||
|
||||
type T = NonNullable<HeadplaneConfig['integration']>['docker'];
|
||||
export default class DockerIntegration extends Integration<T> {
|
||||
private maxAttempts = 10;
|
||||
@ -15,13 +20,63 @@ export default class DockerIntegration extends Integration<T> {
|
||||
return 'Docker';
|
||||
}
|
||||
|
||||
async getContainerName(label: string, value: string): Promise<string> {
|
||||
if (!this.client) {
|
||||
throw new Error('Docker client is not initialized');
|
||||
}
|
||||
|
||||
const filters = encodeURIComponent(
|
||||
JSON.stringify({
|
||||
label: [`${label}=${value}`],
|
||||
}),
|
||||
);
|
||||
const { body } = await this.client.request({
|
||||
method: 'GET',
|
||||
path: `/containers/json?filters=${filters}`,
|
||||
});
|
||||
const containers: DockerContainer[] =
|
||||
(await body.json()) as DockerContainer[];
|
||||
if (containers.length > 1) {
|
||||
throw new Error(
|
||||
`Found multiple Docker containers matching label ${label}=${value}. Please specify a container name.`,
|
||||
);
|
||||
}
|
||||
if (containers.length === 0) {
|
||||
throw new Error(
|
||||
`No Docker containers found matching label: ${label}=${value}`,
|
||||
);
|
||||
}
|
||||
log.info(
|
||||
'config',
|
||||
'Found Docker container matching label: %s=%s',
|
||||
label,
|
||||
value,
|
||||
);
|
||||
return containers[0].Id;
|
||||
}
|
||||
|
||||
async isAvailable() {
|
||||
if (this.context.container_name.length === 0) {
|
||||
log.error('config', 'Docker container name is empty');
|
||||
// Perform a basic check to see if any of the required properties are set
|
||||
if (
|
||||
this.context.container_name.length === 0 &&
|
||||
!this.context.container_label
|
||||
) {
|
||||
log.error('config', 'Docker container name and label are both empty');
|
||||
return false;
|
||||
}
|
||||
|
||||
log.info('config', 'Using container: %s', this.context.container_name);
|
||||
if (
|
||||
this.context.container_name.length > 0 &&
|
||||
!this.context.container_label
|
||||
) {
|
||||
log.error(
|
||||
'config',
|
||||
'Docker container name and label are mutually exclusive',
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Verify that Docker socket is reachable
|
||||
let url: URL | undefined;
|
||||
try {
|
||||
url = new URL(this.context.socket);
|
||||
@ -72,6 +127,38 @@ export default class DockerIntegration extends Integration<T> {
|
||||
socketPath: url.pathname,
|
||||
});
|
||||
}
|
||||
if (this.client === undefined) {
|
||||
log.error('config', 'Failed to create Docker client');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (this.context.container_name.length === 0) {
|
||||
try {
|
||||
if (this.context.container_label === undefined) {
|
||||
log.error('config', 'Docker container label is not defined');
|
||||
return false;
|
||||
}
|
||||
const containerName = await this.getContainerName(
|
||||
this.context.container_label.name,
|
||||
this.context.container_label.value,
|
||||
);
|
||||
if (containerName.length === 0) {
|
||||
log.error(
|
||||
'config',
|
||||
'No Docker containers found matching label: %s=%s',
|
||||
this.context.container_label.name,
|
||||
this.context.container_label.value,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
this.context.container_name = containerName;
|
||||
} catch (error) {
|
||||
log.error('config', 'Failed to get Docker container name: %s', error);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
log.info('config', 'Using container: %s', this.context.container_name);
|
||||
|
||||
return this.client !== undefined;
|
||||
}
|
||||
|
||||
@ -41,10 +41,16 @@ const headscaleConfig = type({
|
||||
config_strict: stringToBool,
|
||||
}).onDeepUndeclaredKey('reject');
|
||||
|
||||
const containerLabel = type({
|
||||
name: 'string',
|
||||
value: 'string',
|
||||
}).optional();
|
||||
|
||||
const dockerConfig = type({
|
||||
enabled: stringToBool,
|
||||
container_name: 'string',
|
||||
socket: 'string = "unix:///var/run/docker.sock"',
|
||||
container_label: containerLabel,
|
||||
});
|
||||
|
||||
const kubernetesConfig = type({
|
||||
|
||||
@ -49,6 +49,12 @@ integration:
|
||||
enabled: false
|
||||
# The name (or ID) of the container running Headscale
|
||||
container_name: "headscale"
|
||||
# The label that will be used to identify the container running Headscale.
|
||||
# This can be omitted if the container name is known in advance and is not
|
||||
# subject to changes.
|
||||
container_label:
|
||||
name: "workload.headscale.io/name"
|
||||
value: "headscale"
|
||||
# The path to the Docker socket (do not change this if you are unsure)
|
||||
# Docker socket paths must start with unix:// or tcp:// and at the moment
|
||||
# https connections are not supported.
|
||||
|
||||
@ -74,7 +74,10 @@ The Docker integration is the easiest to setup, as it only requires the Docker s
|
||||
to be mounted into the container along with some configuration. As long as Headplane
|
||||
has access to the Docker socket and the name of the Headscale container, it will
|
||||
automatically propagate config and DNS changes to Headscale without any additional
|
||||
configuration.
|
||||
configuration. Additionally, instead of specifying the name of the Headscale
|
||||
container, it is possible to use a label to dynamically deduce the container
|
||||
name. This can be useful if the container name changes frequently, or is not
|
||||
known in advance.
|
||||
|
||||
## Native Linux (/proc) Integration
|
||||
The `proc` integration is used when you are running Headscale and Headplane on
|
||||
|
||||
Loading…
Reference in New Issue
Block a user