diff --git a/.changeset/ten-llamas-smile.md b/.changeset/ten-llamas-smile.md new file mode 100644 index 00000000..754f19ad --- /dev/null +++ b/.changeset/ten-llamas-smile.md @@ -0,0 +1,5 @@ +--- +"@sunodo/cli": patch +--- + +fixed checking docker and docker compose in sunodo doctor diff --git a/apps/cli/src/commands/doctor.ts b/apps/cli/src/commands/doctor.ts index 865e152f..b1869852 100644 --- a/apps/cli/src/commands/doctor.ts +++ b/apps/cli/src/commands/doctor.ts @@ -9,75 +9,131 @@ export default class DoctorCommand extends BaseCommand { private static MINIMUM_DOCKER_VERSION = "23.0.0"; // Replace with our minimum required Docker version private static MINIMUM_DOCKER_COMPOSE_VERSION = "2.21.0"; // Replace with our minimum required Docker Compose version + private static MINIMUM_BUILDX_VERSION = "0.13.0"; // Replace with our minimum required Buildx version - private static isDockerVersionValid(version: string): boolean { - return semver.gte(version, DoctorCommand.MINIMUM_DOCKER_VERSION); - } + private async checkDocker(): Promise { + try { + const { stdout: dockerVersion } = await execa("docker", [ + "version", + "--format", + "{{json .Client.Version}}", + ]); - private static isDockerComposeVersionValid(version: string): boolean { - const v = semver.coerce(version); - return ( - v !== null && - semver.gte(v, DoctorCommand.MINIMUM_DOCKER_COMPOSE_VERSION) - ); - } + const v = semver.coerce(dockerVersion); + if ( + v !== null && + !semver.gte(v, DoctorCommand.MINIMUM_DOCKER_VERSION) + ) { + throw new Error( + `Unsupported Docker version. Minimum required version is ${DoctorCommand.MINIMUM_DOCKER_VERSION}. Installed version is ${v}.`, + ); + } + } catch (e: unknown) { + if ( + e instanceof Error && + (e as NodeJS.ErrnoException).code === "ENOENT" + ) { + throw new Error("Docker is required but not installed."); + } else { + throw e; + } + } - private static isBuildxRiscv64Supported(buildxOutput: string): boolean { - return buildxOutput.includes("riscv64"); + return true; } - public async run() { + private async checkCompose(): Promise { try { - // Check Docker version - const { stdout: dockerVersionOutput } = await execa("docker", [ + const { stdout: dockerComposeVersion } = await execa("docker", [ + "compose", "version", - "--format", - "{{json .Client.Version}}", + "--short", ]); - const dockerVersion = JSON.parse(dockerVersionOutput); - if (!DoctorCommand.isDockerVersionValid(dockerVersion)) { + const v = semver.coerce(dockerComposeVersion); + if ( + v !== null && + !semver.gte(v, DoctorCommand.MINIMUM_DOCKER_COMPOSE_VERSION) + ) { + throw new Error( + `Unsupported Docker Compose version. Minimum required version is ${DoctorCommand.MINIMUM_DOCKER_COMPOSE_VERSION}. Installed version is ${v}.`, + ); + } + } catch (e: unknown) { + if ( + e instanceof Error && + (e as Error & { exitCode?: number }).exitCode === 125 + ) { throw new Error( - `Unsupported Docker version. Minimum required version is ${DoctorCommand.MINIMUM_DOCKER_VERSION}. Installed version is ${dockerVersion}.`, + "Docker Compose is required but not installed or the command execution failed. Please refer to the Docker Compose documentation for installation instructions: https://docs.docker.com/compose/install/", ); + } else { + throw e; } + } - // Check Docker Compose version - const { stdout: dockerComposeVersionOutput } = await execa( - "docker", - ["compose", "version", "--short"], - ); - const dockerComposeVersion = dockerComposeVersionOutput.trim(); + return true; + } + private async checkBuildx(): Promise { + try { + const { stdout: buildxOutput } = await execa("docker", [ + "buildx", + "version", + ]); + + const v = semver.coerce(buildxOutput); if ( - !DoctorCommand.isDockerComposeVersionValid(dockerComposeVersion) + v !== null && + !semver.gte(v, DoctorCommand.MINIMUM_BUILDX_VERSION) ) { throw new Error( - `Unsupported Docker Compose version. Minimum required version is ${DoctorCommand.MINIMUM_DOCKER_COMPOSE_VERSION}. Installed version is ${dockerComposeVersion}.`, + `Unsupported Docker Buildx version. Minimum required version is ${DoctorCommand.MINIMUM_BUILDX_VERSION}. Installed version is ${v}.`, ); } - // Check Docker Buildx version and riscv64 support - const { stdout: buildxOutput } = await execa("docker", [ + const { stdout: platformsOutput } = await execa("docker", [ "buildx", "ls", + "--format", + "{{.Platforms}}", ]); - const buildxRiscv64Supported = - DoctorCommand.isBuildxRiscv64Supported(buildxOutput); - if (!buildxRiscv64Supported) { + const buildxPlatforms: string[] = platformsOutput + .split(",") + .map((platform) => platform.trim()); + + if (!buildxPlatforms.includes("linux/riscv64")) { throw new Error( - "Your system does not support riscv64 architecture.", + "Your system does not support riscv64 architecture. Run `docker run --privileged --rm tonistiigi/binfmt:riscv` to enable riscv64 support.", ); } - - this.log(`Your system is ready for ${this.config.bin}.`); - } catch (error: unknown) { - if (error instanceof Error) { - this.error(error.message); + } catch (e: unknown) { + if ( + e instanceof Error && + (e as Error & { exitCode?: number }).exitCode === 125 + ) { + throw new Error( + "Docker Buildx is required but not installed. Please refer to the Docker Desktop documentation for installation instructions: https://docs.docker.com/desktop/", + ); } else { - this.error(String(error)); + throw e; + } + } + + return true; + } + + public async run(): Promise { + try { + if (await this.checkDocker()) { + await this.checkCompose(); + await this.checkBuildx(); } + } catch (e: unknown) { + this.error(e as Error); } + + this.log("Your system is ready for sunodo."); } }