// ***************************************************************************** // Copyright (C) 2024 Typefox and others. // // This program and the accompanying materials are made available under the // terms of the Eclipse Public License v. 2.0 which is available at // http://www.eclipse.org/legal/epl-2.0. // // This Source Code may also be made available under the following Secondary // Licenses when the conditions for such availability set forth in the Eclipse // Public License v. 2.0 are satisfied: GNU General Public License, version 2 // with the GNU Classpath Exception which is available at // https://www.gnu.org/software/classpath/license.html. // // SPDX-License-Identifier: EPL-2.0 OR GPL-2.0-only WITH Classpath-exception-2.0 // ***************************************************************************** import { ContributionProvider, MaybePromise, URI } from '@theia/core'; import { VariableContext } from './devcontainer-contributions/variable-resolver-contribution'; import { inject, injectable, named } from '@theia/core/shared/inversify'; import { WorkspaceServer } from '@theia/workspace/lib/common'; import * as fs from '@theia/core/shared/fs-extra'; import * as Docker from 'dockerode'; import { ContainerConnectionOptions } from '../electron-common/remote-container-connection-provider'; import { DevContainerConfiguration, NonComposeContainerBase } from './devcontainer-file'; import { DevContainerFileService } from './dev-container-file-service'; import { ContainerOutputProvider } from '../electron-common/container-output-provider'; import { RemoteDockerContainerConnection } from './remote-docker-container-connection'; import { DockerComposeService } from './docker-compose/compose-service'; export const ContainerCreationContribution = Symbol('ContainerCreationContributions'); export interface ContainerCreationContribution { handleContainerCreation?(createOptions: Docker.ContainerCreateOptions, containerConfig: DevContainerConfiguration, api: Docker, outputProvider?: ContainerOutputProvider): MaybePromise; /** * executed after creating and starting the container */ handlePostCreate?(containerConfig: DevContainerConfiguration, container: Docker.Container, api: Docker, outputProvider?: ContainerOutputProvider): MaybePromise; /** * executed after a connection has been established with the container and theia has been setup */ handlePostConnect?(containerConfig: DevContainerConfiguration, connection: RemoteDockerContainerConnection, outputProvider?: ContainerOutputProvider): MaybePromise; } @injectable() export class DockerContainerService { @inject(WorkspaceServer) protected readonly workspaceServer: WorkspaceServer; @inject(ContributionProvider) @named(ContainerCreationContribution) protected readonly containerCreationContributions: ContributionProvider; @inject(DevContainerFileService) protected readonly devContainerFileService: DevContainerFileService; @inject(DockerComposeService) protected readonly dockerComposeService: DockerComposeService; async getOrCreateContainer(docker: Docker, options: ContainerConnectionOptions, outputProvider?: ContainerOutputProvider): Promise { let container; const workspace = new URI(options.workspacePath ?? await this.workspaceServer.getMostRecentlyUsedWorkspace()); if (options.lastContainerInfo && fs.pathExistsSync(options.devcontainerFile) && fs.statSync(options.devcontainerFile).mtimeMs < options.lastContainerInfo.lastUsed) { try { container = docker.getContainer(options.lastContainerInfo.id); if ((await container.inspect()).State.Running) { await container.restart(); } else { await container.start(); } } catch (e) { container = undefined; console.warn('DevContainer: could not find last used container'); } } if (!container) { container = await this.buildContainer(docker, options.devcontainerFile, workspace, outputProvider); } return container; } async postConnect(devcontainerFile: string, connection: RemoteDockerContainerConnection, outputProvider?: ContainerOutputProvider, context?: VariableContext): Promise { const devcontainerConfig = await this.devContainerFileService.getConfiguration(devcontainerFile, context); for (const containerCreateContrib of this.containerCreationContributions.getContributions()) { await containerCreateContrib.handlePostConnect?.(devcontainerConfig, connection, outputProvider); } } protected async buildContainer(docker: Docker, devcontainerFile: string, workspace: URI, outputProvider?: ContainerOutputProvider): Promise { const devcontainerConfig = await this.devContainerFileService.getConfiguration(devcontainerFile); if (!devcontainerConfig) { // TODO add ability for user to create new config throw new Error('No devcontainer.json'); } const containerCreateOptions: Docker.ContainerCreateOptions = { Tty: true, ExposedPorts: {}, HostConfig: { PortBindings: {}, Mounts: [{ Source: workspace.path.toString(), Target: `/workspaces/${workspace.path.name}`, Type: 'bind' }], }, }; for (const containerCreateContrib of this.containerCreationContributions.getContributions()) { await containerCreateContrib.handleContainerCreation?.(containerCreateOptions, devcontainerConfig, docker, outputProvider); } // Per the devcontainer spec, overrideCommand defaults to true for non-compose containers. // When true, override the image's CMD with a sleep loop to keep the container alive. if (!devcontainerConfig.dockerComposeFile) { const overrideCommand = (devcontainerConfig as NonComposeContainerBase).overrideCommand; if (overrideCommand !== false) { containerCreateOptions.Cmd = ['/bin/sh', '-c', 'while sleep 1000; do :; done']; } } let container: Docker.Container; if (devcontainerConfig.dockerComposeFile) { const containerName = await this.dockerComposeService.createContainers(devcontainerConfig, containerCreateOptions, outputProvider); const services = await docker.listContainers({ filters: { label: [`com.docker.compose.service=${containerName}`] } }); if (services.length === 0) { throw new Error(`No running container found for docker compose service ${containerName}`); } container = docker.getContainer(services[0].Id); } else { container = await docker.createContainer(containerCreateOptions); await container.start(); } for (const containerCreateContrib of this.containerCreationContributions.getContributions()) { await containerCreateContrib.handlePostCreate?.(devcontainerConfig, container, docker, outputProvider); } return container; } }