update
This commit is contained in:
@@ -19,7 +19,7 @@ export class OneboxDockerManager {
|
||||
try {
|
||||
// Initialize Docker client (connects to /var/run/docker.sock by default)
|
||||
this.dockerClient = new plugins.docker.Docker({
|
||||
socketPath: '/var/run/docker.sock',
|
||||
socketPath: 'unix:///var/run/docker.sock',
|
||||
});
|
||||
|
||||
// Start the Docker client
|
||||
@@ -45,14 +45,25 @@ export class OneboxDockerManager {
|
||||
|
||||
if (!existingNetwork) {
|
||||
logger.info(`Creating Docker network: ${this.networkName}`);
|
||||
|
||||
// Check if Docker is in Swarm mode
|
||||
let isSwarmMode = false;
|
||||
try {
|
||||
const swarmResponse = await this.dockerClient!.request('GET', '/swarm', {});
|
||||
isSwarmMode = swarmResponse.statusCode === 200;
|
||||
} catch (error) {
|
||||
isSwarmMode = false;
|
||||
}
|
||||
|
||||
await this.dockerClient!.createNetwork({
|
||||
Name: this.networkName,
|
||||
Driver: 'bridge',
|
||||
Driver: isSwarmMode ? 'overlay' : 'bridge',
|
||||
Attachable: isSwarmMode ? true : undefined, // Required for overlay networks to allow standalone containers
|
||||
Labels: {
|
||||
'managed-by': 'onebox',
|
||||
},
|
||||
});
|
||||
logger.success(`Docker network created: ${this.networkName}`);
|
||||
logger.success(`Docker network created: ${this.networkName} (${isSwarmMode ? 'overlay' : 'bridge'})`);
|
||||
} else {
|
||||
logger.debug(`Docker network already exists: ${this.networkName}`);
|
||||
}
|
||||
@@ -66,77 +77,30 @@ export class OneboxDockerManager {
|
||||
* Pull an image from a registry
|
||||
*/
|
||||
async pullImage(image: string, registry?: string): Promise<void> {
|
||||
try {
|
||||
logger.info(`Pulling Docker image: ${image}`);
|
||||
|
||||
const fullImage = registry ? `${registry}/${image}` : image;
|
||||
|
||||
await this.dockerClient!.pull(fullImage, (error: any, stream: any) => {
|
||||
if (error) {
|
||||
throw error;
|
||||
}
|
||||
|
||||
// Follow progress
|
||||
this.dockerClient!.modem.followProgress(stream, (err: any, output: any) => {
|
||||
if (err) {
|
||||
throw err;
|
||||
}
|
||||
logger.debug('Pull complete:', output);
|
||||
});
|
||||
});
|
||||
|
||||
logger.success(`Image pulled successfully: ${fullImage}`);
|
||||
} catch (error) {
|
||||
logger.error(`Failed to pull image ${image}: ${error.message}`);
|
||||
throw error;
|
||||
}
|
||||
// Skip manual image pulling - Docker will automatically pull when creating container
|
||||
const fullImage = registry ? `${registry}/${image}` : image;
|
||||
logger.debug(`Skipping manual pull for ${fullImage} - Docker will auto-pull on container creation`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create and start a container
|
||||
* Create and start a container or service (depending on Swarm mode)
|
||||
*/
|
||||
async createContainer(service: IService): Promise<string> {
|
||||
try {
|
||||
logger.info(`Creating container for service: ${service.name}`);
|
||||
|
||||
const fullImage = service.registry
|
||||
? `${service.registry}/${service.image}`
|
||||
: service.image;
|
||||
|
||||
// Prepare environment variables
|
||||
const env: string[] = [];
|
||||
for (const [key, value] of Object.entries(service.envVars)) {
|
||||
env.push(`${key}=${value}`);
|
||||
// Check if Docker is in Swarm mode
|
||||
let isSwarmMode = false;
|
||||
try {
|
||||
const swarmResponse = await this.dockerClient!.request('GET', '/swarm', {});
|
||||
isSwarmMode = swarmResponse.statusCode === 200;
|
||||
} catch (error) {
|
||||
isSwarmMode = false;
|
||||
}
|
||||
|
||||
// Create container
|
||||
const container = await this.dockerClient!.createContainer({
|
||||
Image: fullImage,
|
||||
name: `onebox-${service.name}`,
|
||||
Env: env,
|
||||
Labels: {
|
||||
'managed-by': 'onebox',
|
||||
'onebox-service': service.name,
|
||||
},
|
||||
ExposedPorts: {
|
||||
[`${service.port}/tcp`]: {},
|
||||
},
|
||||
HostConfig: {
|
||||
NetworkMode: this.networkName,
|
||||
RestartPolicy: {
|
||||
Name: 'unless-stopped',
|
||||
},
|
||||
PortBindings: {
|
||||
// Don't bind to host ports - nginx will proxy
|
||||
[`${service.port}/tcp`]: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const containerID = container.id;
|
||||
logger.success(`Container created: ${containerID}`);
|
||||
|
||||
return containerID;
|
||||
if (isSwarmMode) {
|
||||
return await this.createSwarmService(service);
|
||||
} else {
|
||||
return await this.createStandaloneContainer(service);
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create container for ${service.name}: ${error.message}`);
|
||||
throw error;
|
||||
@@ -144,19 +108,158 @@ export class OneboxDockerManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a container by ID
|
||||
* Create a standalone container (non-Swarm mode)
|
||||
*/
|
||||
private async createStandaloneContainer(service: IService): Promise<string> {
|
||||
logger.info(`Creating standalone container for service: ${service.name}`);
|
||||
|
||||
const fullImage = service.registry
|
||||
? `${service.registry}/${service.image}`
|
||||
: service.image;
|
||||
|
||||
// Prepare environment variables
|
||||
const env: string[] = [];
|
||||
for (const [key, value] of Object.entries(service.envVars)) {
|
||||
env.push(`${key}=${value}`);
|
||||
}
|
||||
|
||||
// Create container using Docker REST API directly
|
||||
const response = await this.dockerClient!.request('POST', `/containers/create?name=onebox-${service.name}`, {
|
||||
Image: fullImage,
|
||||
Env: env,
|
||||
Labels: {
|
||||
'managed-by': 'onebox',
|
||||
'onebox-service': service.name,
|
||||
},
|
||||
ExposedPorts: {
|
||||
[`${service.port}/tcp`]: {},
|
||||
},
|
||||
HostConfig: {
|
||||
NetworkMode: this.networkName,
|
||||
RestartPolicy: {
|
||||
Name: 'unless-stopped',
|
||||
},
|
||||
PortBindings: {
|
||||
// Don't bind to host ports - nginx will proxy
|
||||
[`${service.port}/tcp`]: [],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode >= 300) {
|
||||
throw new Error(`Failed to create container: HTTP ${response.statusCode}`);
|
||||
}
|
||||
|
||||
const containerID = response.body.Id;
|
||||
logger.success(`Standalone container created: ${containerID}`);
|
||||
|
||||
return containerID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a Docker Swarm service
|
||||
*/
|
||||
private async createSwarmService(service: IService): Promise<string> {
|
||||
logger.info(`Creating Swarm service for: ${service.name}`);
|
||||
|
||||
const fullImage = service.registry
|
||||
? `${service.registry}/${service.image}`
|
||||
: service.image;
|
||||
|
||||
// Prepare environment variables
|
||||
const env: string[] = [];
|
||||
for (const [key, value] of Object.entries(service.envVars)) {
|
||||
env.push(`${key}=${value}`);
|
||||
}
|
||||
|
||||
// Create Swarm service using Docker REST API
|
||||
const response = await this.dockerClient!.request('POST', '/services/create', {
|
||||
Name: `onebox-${service.name}`,
|
||||
Labels: {
|
||||
'managed-by': 'onebox',
|
||||
'onebox-service': service.name,
|
||||
},
|
||||
TaskTemplate: {
|
||||
ContainerSpec: {
|
||||
Image: fullImage,
|
||||
Env: env,
|
||||
Labels: {
|
||||
'managed-by': 'onebox',
|
||||
'onebox-service': service.name,
|
||||
},
|
||||
},
|
||||
Networks: [
|
||||
{
|
||||
Target: await this.getNetworkID(this.networkName),
|
||||
},
|
||||
],
|
||||
RestartPolicy: {
|
||||
Condition: 'any',
|
||||
MaxAttempts: 0,
|
||||
},
|
||||
},
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
EndpointSpec: {
|
||||
Ports: [
|
||||
{
|
||||
Protocol: 'tcp',
|
||||
TargetPort: service.port,
|
||||
PublishMode: 'host',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
if (response.statusCode >= 300) {
|
||||
throw new Error(`Failed to create service: HTTP ${response.statusCode} - ${JSON.stringify(response.body)}`);
|
||||
}
|
||||
|
||||
const serviceID = response.body.ID;
|
||||
logger.success(`Swarm service created: ${serviceID}`);
|
||||
|
||||
return serviceID;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get network ID by name
|
||||
*/
|
||||
private async getNetworkID(networkName: string): Promise<string> {
|
||||
const networks = await this.dockerClient!.getNetworks();
|
||||
const network = networks.find((n: any) =>
|
||||
(n.name || n.Name) === networkName
|
||||
);
|
||||
if (!network) {
|
||||
throw new Error(`Network not found: ${networkName}`);
|
||||
}
|
||||
return network.id || network.Id;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start a container or service by ID
|
||||
*/
|
||||
async startContainer(containerID: string): Promise<void> {
|
||||
try {
|
||||
// Try service first
|
||||
if (await this.isService(containerID)) {
|
||||
return await this.startService(containerID);
|
||||
}
|
||||
|
||||
logger.info(`Starting container: ${containerID}`);
|
||||
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
await container.start();
|
||||
const response = await this.dockerClient!.request('POST', `/containers/${containerID}/start`, {});
|
||||
|
||||
if (response.statusCode >= 300 && response.statusCode !== 304) {
|
||||
throw new Error(`Failed to start container: HTTP ${response.statusCode}`);
|
||||
}
|
||||
|
||||
logger.success(`Container started: ${containerID}`);
|
||||
} catch (error) {
|
||||
// Ignore "already started" errors
|
||||
if (error.message.includes('already started')) {
|
||||
// Ignore "already started" errors (304 status)
|
||||
if (error.message.includes('304')) {
|
||||
logger.debug(`Container already running: ${containerID}`);
|
||||
return;
|
||||
}
|
||||
@@ -166,19 +269,71 @@ export class OneboxDockerManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a container by ID
|
||||
* Start a Swarm service (scale to 1 replica)
|
||||
*/
|
||||
private async startService(serviceID: string): Promise<void> {
|
||||
logger.info(`Starting service: ${serviceID}`);
|
||||
|
||||
// Get current service spec
|
||||
const getResponse = await this.dockerClient!.request('GET', `/services/${serviceID}`, {});
|
||||
if (getResponse.statusCode >= 300) {
|
||||
throw new Error(`Failed to get service: HTTP ${getResponse.statusCode}`);
|
||||
}
|
||||
|
||||
const service = getResponse.body;
|
||||
const version = service.Version.Index;
|
||||
|
||||
// Update service to scale to 1 replica
|
||||
const updateResponse = await this.dockerClient!.request('POST', `/services/${serviceID}/update?version=${version}`, {
|
||||
...service.Spec,
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 1,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (updateResponse.statusCode >= 300) {
|
||||
throw new Error(`Failed to start service: HTTP ${updateResponse.statusCode}`);
|
||||
}
|
||||
|
||||
logger.success(`Service started (scaled to 1 replica): ${serviceID}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if ID is a service (not a container)
|
||||
*/
|
||||
private async isService(id: string): Promise<boolean> {
|
||||
try {
|
||||
const response = await this.dockerClient!.request('GET', `/services/${id}`, {});
|
||||
return response.statusCode === 200;
|
||||
} catch (error) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop a container or service by ID
|
||||
*/
|
||||
async stopContainer(containerID: string): Promise<void> {
|
||||
try {
|
||||
// Try service first
|
||||
if (await this.isService(containerID)) {
|
||||
return await this.stopService(containerID);
|
||||
}
|
||||
|
||||
logger.info(`Stopping container: ${containerID}`);
|
||||
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
await container.stop();
|
||||
const response = await this.dockerClient!.request('POST', `/containers/${containerID}/stop`, {});
|
||||
|
||||
if (response.statusCode >= 300 && response.statusCode !== 304) {
|
||||
throw new Error(`Failed to stop container: HTTP ${response.statusCode}`);
|
||||
}
|
||||
|
||||
logger.success(`Container stopped: ${containerID}`);
|
||||
} catch (error) {
|
||||
// Ignore "already stopped" errors
|
||||
if (error.message.includes('already stopped') || error.statusCode === 304) {
|
||||
// Ignore "already stopped" errors (304 status)
|
||||
if (error.message.includes('304')) {
|
||||
logger.debug(`Container already stopped: ${containerID}`);
|
||||
return;
|
||||
}
|
||||
@@ -188,14 +343,54 @@ export class OneboxDockerManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart a container by ID
|
||||
* Stop a Swarm service (scale to 0 replicas)
|
||||
*/
|
||||
private async stopService(serviceID: string): Promise<void> {
|
||||
logger.info(`Stopping service: ${serviceID}`);
|
||||
|
||||
// Get current service spec
|
||||
const getResponse = await this.dockerClient!.request('GET', `/services/${serviceID}`, {});
|
||||
if (getResponse.statusCode >= 300) {
|
||||
throw new Error(`Failed to get service: HTTP ${getResponse.statusCode}`);
|
||||
}
|
||||
|
||||
const service = getResponse.body;
|
||||
const version = service.Version.Index;
|
||||
|
||||
// Update service to scale to 0 replicas
|
||||
const updateResponse = await this.dockerClient!.request('POST', `/services/${serviceID}/update?version=${version}`, {
|
||||
...service.Spec,
|
||||
Mode: {
|
||||
Replicated: {
|
||||
Replicas: 0,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (updateResponse.statusCode >= 300) {
|
||||
throw new Error(`Failed to stop service: HTTP ${updateResponse.statusCode}`);
|
||||
}
|
||||
|
||||
logger.success(`Service stopped (scaled to 0 replicas): ${serviceID}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Restart a container or service by ID
|
||||
*/
|
||||
async restartContainer(containerID: string): Promise<void> {
|
||||
try {
|
||||
// Try service first
|
||||
if (await this.isService(containerID)) {
|
||||
return await this.restartService(containerID);
|
||||
}
|
||||
|
||||
logger.info(`Restarting container: ${containerID}`);
|
||||
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
await container.restart();
|
||||
const response = await this.dockerClient!.request('POST', `/containers/${containerID}/restart`, {});
|
||||
|
||||
if (response.statusCode >= 300) {
|
||||
throw new Error(`Failed to restart container: HTTP ${response.statusCode}`);
|
||||
}
|
||||
|
||||
logger.success(`Container restarted: ${containerID}`);
|
||||
} catch (error) {
|
||||
@@ -205,13 +400,47 @@ export class OneboxDockerManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a container by ID
|
||||
* Restart a Swarm service (force update with same spec)
|
||||
*/
|
||||
private async restartService(serviceID: string): Promise<void> {
|
||||
logger.info(`Restarting service: ${serviceID}`);
|
||||
|
||||
// Get current service spec
|
||||
const getResponse = await this.dockerClient!.request('GET', `/services/${serviceID}`, {});
|
||||
if (getResponse.statusCode >= 300) {
|
||||
throw new Error(`Failed to get service: HTTP ${getResponse.statusCode}`);
|
||||
}
|
||||
|
||||
const service = getResponse.body;
|
||||
const version = service.Version.Index;
|
||||
|
||||
// Force update to trigger restart
|
||||
const updateResponse = await this.dockerClient!.request('POST', `/services/${serviceID}/update?version=${version}`, {
|
||||
...service.Spec,
|
||||
TaskTemplate: {
|
||||
...service.Spec.TaskTemplate,
|
||||
ForceUpdate: (service.Spec.TaskTemplate.ForceUpdate || 0) + 1,
|
||||
},
|
||||
});
|
||||
|
||||
if (updateResponse.statusCode >= 300) {
|
||||
throw new Error(`Failed to restart service: HTTP ${updateResponse.statusCode}`);
|
||||
}
|
||||
|
||||
logger.success(`Service restarted: ${serviceID}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a container or service by ID
|
||||
*/
|
||||
async removeContainer(containerID: string, force = false): Promise<void> {
|
||||
try {
|
||||
logger.info(`Removing container: ${containerID}`);
|
||||
// Try service first
|
||||
if (await this.isService(containerID)) {
|
||||
return await this.removeService(containerID);
|
||||
}
|
||||
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
logger.info(`Removing container: ${containerID}`);
|
||||
|
||||
// Stop first if not forced
|
||||
if (!force) {
|
||||
@@ -223,7 +452,12 @@ export class OneboxDockerManager {
|
||||
}
|
||||
}
|
||||
|
||||
await container.remove({ force });
|
||||
const url = force ? `/containers/${containerID}?force=true` : `/containers/${containerID}`;
|
||||
const response = await this.dockerClient!.request('DELETE', url, {});
|
||||
|
||||
if (response.statusCode >= 300) {
|
||||
throw new Error(`Failed to remove container: HTTP ${response.statusCode}`);
|
||||
}
|
||||
|
||||
logger.success(`Container removed: ${containerID}`);
|
||||
} catch (error) {
|
||||
@@ -233,20 +467,112 @@ export class OneboxDockerManager {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container status
|
||||
* Remove a Swarm service
|
||||
*/
|
||||
private async removeService(serviceID: string): Promise<void> {
|
||||
logger.info(`Removing service: ${serviceID}`);
|
||||
|
||||
const response = await this.dockerClient!.request('DELETE', `/services/${serviceID}`, {});
|
||||
|
||||
if (response.statusCode >= 300) {
|
||||
throw new Error(`Failed to remove service: HTTP ${response.statusCode}`);
|
||||
}
|
||||
|
||||
logger.success(`Service removed: ${serviceID}`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container or service status
|
||||
*/
|
||||
async getContainerStatus(containerID: string): Promise<string> {
|
||||
try {
|
||||
const container = this.dockerClient!.getContainer(containerID);
|
||||
const info = await container.inspect();
|
||||
// Try service first
|
||||
if (await this.isService(containerID)) {
|
||||
return await this.getServiceStatus(containerID);
|
||||
}
|
||||
|
||||
return info.State.Status;
|
||||
const response = await this.dockerClient!.request('GET', `/containers/${containerID}/json`, {});
|
||||
|
||||
if (response.statusCode >= 300) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
return response.body.State?.Status || 'unknown';
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get container status ${containerID}: ${error.message}`);
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get Swarm service status
|
||||
*/
|
||||
private async getServiceStatus(serviceID: string): Promise<string> {
|
||||
try {
|
||||
// Get service details
|
||||
const serviceResponse = await this.dockerClient!.request('GET', `/services/${serviceID}`, {});
|
||||
if (serviceResponse.statusCode >= 300) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const service = serviceResponse.body;
|
||||
const replicas = service.Spec?.Mode?.Replicated?.Replicas || 0;
|
||||
|
||||
if (replicas === 0) {
|
||||
return 'stopped';
|
||||
}
|
||||
|
||||
// Get tasks for this service to check if they're running
|
||||
const tasksResponse = await this.dockerClient!.request('GET', `/tasks?filters=${encodeURIComponent(JSON.stringify({service: [serviceID]}))}`, {});
|
||||
if (tasksResponse.statusCode >= 300) {
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
const tasks = tasksResponse.body;
|
||||
if (tasks.length === 0) {
|
||||
return 'starting';
|
||||
}
|
||||
|
||||
// Check if any task is running
|
||||
const hasRunning = tasks.some((task: any) => task.Status?.State === 'running');
|
||||
if (hasRunning) {
|
||||
return 'running';
|
||||
}
|
||||
|
||||
// Check task states
|
||||
const latestTask = tasks[0];
|
||||
const taskState = latestTask?.Status?.State || 'unknown';
|
||||
|
||||
// Map Swarm task states to container-like states
|
||||
switch (taskState) {
|
||||
case 'new':
|
||||
case 'allocated':
|
||||
case 'pending':
|
||||
case 'assigned':
|
||||
case 'accepted':
|
||||
case 'preparing':
|
||||
case 'ready':
|
||||
case 'starting':
|
||||
return 'starting';
|
||||
case 'running':
|
||||
return 'running';
|
||||
case 'complete':
|
||||
return 'exited';
|
||||
case 'failed':
|
||||
case 'shutdown':
|
||||
case 'rejected':
|
||||
case 'orphaned':
|
||||
case 'remove':
|
||||
return 'stopped';
|
||||
default:
|
||||
return 'unknown';
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(`Failed to get service status ${serviceID}: ${error.message}`);
|
||||
return 'unknown';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get container stats (CPU, memory, network)
|
||||
*/
|
||||
|
||||
Reference in New Issue
Block a user