chore: use single string docker labels
This commit is contained in:
parent
1c88fe55cb
commit
59874ca749
@ -15,6 +15,7 @@ type T = NonNullable<HeadplaneConfig['integration']>['docker'];
|
|||||||
export default class DockerIntegration extends Integration<T> {
|
export default class DockerIntegration extends Integration<T> {
|
||||||
private maxAttempts = 10;
|
private maxAttempts = 10;
|
||||||
private client: Client | undefined;
|
private client: Client | undefined;
|
||||||
|
private containerId: string | undefined;
|
||||||
|
|
||||||
get name() {
|
get name() {
|
||||||
return 'Docker';
|
return 'Docker';
|
||||||
@ -56,22 +57,13 @@ export default class DockerIntegration extends Integration<T> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async isAvailable() {
|
async isAvailable() {
|
||||||
// Perform a basic check to see if any of the required properties are set
|
// Basic configuration check, the name overrides the container_label
|
||||||
if (
|
// selector because of legacy support.
|
||||||
this.context.container_name.length === 0 &&
|
const { container_name, container_label } = this.context;
|
||||||
!this.context.container_label
|
if (container_name.length === 0 && container_label.length === 0) {
|
||||||
) {
|
|
||||||
log.error('config', 'Docker container name and label are both empty');
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
this.context.container_name.length > 0 &&
|
|
||||||
!this.context.container_label
|
|
||||||
) {
|
|
||||||
log.error(
|
log.error(
|
||||||
'config',
|
'config',
|
||||||
'Docker container name and label are mutually exclusive',
|
'Missing a Docker `container_name` or `container_label`',
|
||||||
);
|
);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@ -127,40 +119,73 @@ export default class DockerIntegration extends Integration<T> {
|
|||||||
socketPath: url.pathname,
|
socketPath: url.pathname,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.client === undefined) {
|
if (this.client === undefined) {
|
||||||
log.error('config', 'Failed to create Docker client');
|
log.error('config', 'Failed to create Docker client');
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.context.container_name.length === 0) {
|
const qp = new URLSearchParams({
|
||||||
try {
|
filters: JSON.stringify(
|
||||||
if (this.context.container_label === undefined) {
|
container_name.length > 0
|
||||||
log.error('config', 'Docker container label is not defined');
|
? { name: container_name }
|
||||||
return false;
|
: { label: [container_label] },
|
||||||
}
|
),
|
||||||
const containerName = await this.getContainerName(
|
});
|
||||||
this.context.container_label.name,
|
|
||||||
this.context.container_label.value,
|
const res = await this.client.request({
|
||||||
);
|
method: 'GET',
|
||||||
if (containerName.length === 0) {
|
path: `/v1.30/containers/json?${qp.toString()}`,
|
||||||
log.error(
|
});
|
||||||
'config',
|
|
||||||
'No Docker containers found matching label: %s=%s',
|
if (res.statusCode !== 200) {
|
||||||
this.context.container_label.name,
|
log.error('config', 'Could not request available Docker containers');
|
||||||
this.context.container_label.value,
|
log.debug('config', 'Error Details: %o', await res.body.json());
|
||||||
);
|
return false;
|
||||||
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);
|
const data = (await res.body.json()) as DockerContainer[];
|
||||||
|
if (data.length > 1) {
|
||||||
|
if (container_name.length > 0) {
|
||||||
|
log.error(
|
||||||
|
'config',
|
||||||
|
`Found multiple containers with name ${container_name}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.error(
|
||||||
|
'config',
|
||||||
|
`Found multiple containers with label ${container_label}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return this.client !== undefined;
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
if (container_name.length > 0) {
|
||||||
|
log.error(
|
||||||
|
'config',
|
||||||
|
`No container found with the name ${container_name}`,
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
log.error(
|
||||||
|
'config',
|
||||||
|
`No container found with the label ${container_label}`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.containerId = data[0].Id;
|
||||||
|
log.info(
|
||||||
|
'config',
|
||||||
|
'Using container: %s (ID: %s)',
|
||||||
|
data[0].Names[0],
|
||||||
|
this.containerId,
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.client !== undefined && this.containerId !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
async onConfigChange(client: ApiClient) {
|
async onConfigChange(client: ApiClient) {
|
||||||
@ -175,13 +200,13 @@ export default class DockerIntegration extends Integration<T> {
|
|||||||
log.debug(
|
log.debug(
|
||||||
'config',
|
'config',
|
||||||
'Restarting container: %s (attempt %d)',
|
'Restarting container: %s (attempt %d)',
|
||||||
this.context.container_name,
|
this.containerId,
|
||||||
attempts,
|
attempts,
|
||||||
);
|
);
|
||||||
|
|
||||||
const response = await this.client.request({
|
const response = await this.client.request({
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
path: `/v1.30/containers/${this.context.container_name}/restart`,
|
path: `/v1.30/containers/${this.containerId}/restart`,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (response.statusCode !== 204) {
|
if (response.statusCode !== 204) {
|
||||||
@ -217,11 +242,7 @@ export default class DockerIntegration extends Integration<T> {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
log.error(
|
log.error('config', 'Missed restart deadline for %s', this.containerId);
|
||||||
'config',
|
|
||||||
'Missed restart deadline for %s',
|
|
||||||
this.context.container_name,
|
|
||||||
);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ export async function loadIntegration(context: HeadplaneConfig['integration']) {
|
|||||||
try {
|
try {
|
||||||
const res = await integration.isAvailable();
|
const res = await integration.isAvailable();
|
||||||
if (!res) {
|
if (!res) {
|
||||||
log.error('config', 'Integration %s is not available', integration);
|
log.error('config', 'Integration %s is not available', integration.name);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@ -46,11 +46,6 @@ const headscaleConfig = type({
|
|||||||
dns_records_path: 'string?',
|
dns_records_path: 'string?',
|
||||||
}).onDeepUndeclaredKey('reject');
|
}).onDeepUndeclaredKey('reject');
|
||||||
|
|
||||||
const containerLabel = type({
|
|
||||||
name: 'string',
|
|
||||||
value: 'string',
|
|
||||||
}).optional();
|
|
||||||
|
|
||||||
const agentConfig = type({
|
const agentConfig = type({
|
||||||
enabled: stringToBool.default(false),
|
enabled: stringToBool.default(false),
|
||||||
host_name: 'string = "headplane-agent"',
|
host_name: 'string = "headplane-agent"',
|
||||||
@ -64,8 +59,8 @@ const agentConfig = type({
|
|||||||
const dockerConfig = type({
|
const dockerConfig = type({
|
||||||
enabled: stringToBool,
|
enabled: stringToBool,
|
||||||
container_name: 'string = ""',
|
container_name: 'string = ""',
|
||||||
|
container_label: 'string = "me.tale.headplane.target=headscale"',
|
||||||
socket: 'string = "unix:///var/run/docker.sock"',
|
socket: 'string = "unix:///var/run/docker.sock"',
|
||||||
container_label: containerLabel,
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const kubernetesConfig = type({
|
const kubernetesConfig = type({
|
||||||
|
|||||||
@ -11,7 +11,7 @@ services:
|
|||||||
image: "headscale/headscale:0.25.1"
|
image: "headscale/headscale:0.25.1"
|
||||||
container_name: "headscale"
|
container_name: "headscale"
|
||||||
labels:
|
labels:
|
||||||
- com.headplane.selector=headscale
|
me.tale.headplane.target: headscale
|
||||||
restart: "unless-stopped"
|
restart: "unless-stopped"
|
||||||
command: "serve"
|
command: "serve"
|
||||||
networks:
|
networks:
|
||||||
|
|||||||
@ -84,18 +84,23 @@ integration:
|
|||||||
# at the same time as any of these and is recommended for the best experience.
|
# at the same time as any of these and is recommended for the best experience.
|
||||||
docker:
|
docker:
|
||||||
enabled: false
|
enabled: false
|
||||||
# Preferred method: use container_label to dynamically discover the Headscale container.
|
|
||||||
container_label:
|
# By default we check for the presence of a container label (see the docs)
|
||||||
name: "com.headplane.selector"
|
# to determine the container to signal when changes are made to DNS settings.
|
||||||
value: "headscale"
|
container_label: "me.tale.headplane.target=headscale"
|
||||||
# Optional fallback: directly specify the container name (or ID)
|
|
||||||
# of the container running Headscale
|
# HOWEVER, you can fallback to a container name if you desire, but this is
|
||||||
|
# not recommended as its brittle and doesn't work with orchestrators that
|
||||||
|
# automatically assign container names.
|
||||||
|
#
|
||||||
|
# If `container_name` is set, it will override any label checks.
|
||||||
# container_name: "headscale"
|
# container_name: "headscale"
|
||||||
|
|
||||||
# The path to the Docker socket (do not change this if you are unsure)
|
# 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
|
# Docker socket paths must start with unix:// or tcp:// and at the moment
|
||||||
# https connections are not supported.
|
# https connections are not supported.
|
||||||
socket: "unix:///var/run/docker.sock"
|
socket: "unix:///var/run/docker.sock"
|
||||||
|
|
||||||
# Please refer to docs/integration/Kubernetes.md for more information
|
# Please refer to docs/integration/Kubernetes.md for more information
|
||||||
# on how to configure the Kubernetes integration. There are requirements in
|
# on how to configure the Kubernetes integration. There are requirements in
|
||||||
# order to allow Headscale to be controlled by Headplane in a cluster.
|
# order to allow Headscale to be controlled by Headplane in a cluster.
|
||||||
|
|||||||
@ -58,6 +58,9 @@ services:
|
|||||||
container_name: headscale
|
container_name: headscale
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
command: serve
|
command: serve
|
||||||
|
labels:
|
||||||
|
# This is needed for Headplane to find it and signal it
|
||||||
|
me.tale.headplane.target: headscale
|
||||||
ports:
|
ports:
|
||||||
- '8080:8080'
|
- '8080:8080'
|
||||||
volumes:
|
volumes:
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user