From 392d5f0acf3271ffecd7831a24e766d4ddbd79c4 Mon Sep 17 00:00:00 2001 From: kevinwang Date: Sat, 6 Sep 2025 14:15:26 -0400 Subject: [PATCH] Added docker resource --- scripts/init.sh | 2 +- src/index.ts | 2 + src/resources/docker/docker-schema.json | 18 +++++ src/resources/docker/docker.ts | 102 ++++++++++++++++++++++++ src/resources/node/npm/npm.ts | 8 +- src/utils/index.ts | 17 ++-- test/docker/docker.test.ts | 22 +++++ 7 files changed, 160 insertions(+), 11 deletions(-) create mode 100644 src/resources/docker/docker-schema.json create mode 100644 src/resources/docker/docker.ts create mode 100644 test/docker/docker.test.ts diff --git a/scripts/init.sh b/scripts/init.sh index 1f857d0..3eebf4f 100755 --- a/scripts/init.sh +++ b/scripts/init.sh @@ -1,2 +1,2 @@ tart clone ghcr.io/kevinwang5658/sonoma-codify:v0.0.3 codify-test-vm -tart clone ghcr.io/kevinwang5658/sonoma-codify:v0.0.3 codify-sonoma +# tart clone ghcr.io/kevinwang5658/sonoma-codify:v0.0.3 codify-sonoma diff --git a/src/index.ts b/src/index.ts index 6a43806..9b34b97 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,6 +35,7 @@ import { VscodeResource } from './resources/vscode/vscode.js'; import { XcodeToolsResource } from './resources/xcode-tools/xcode-tools.js'; import { MacportsResource } from './resources/macports/macports.js'; import { Npm } from './resources/node/npm/npm.js'; +import { DockerResource } from './resources/docker/docker.js'; runPlugin(Plugin.create( 'default', @@ -74,5 +75,6 @@ runPlugin(Plugin.create( new PipSync(), new MacportsResource(), new Npm(), + new DockerResource(), ]) ) diff --git a/src/resources/docker/docker-schema.json b/src/resources/docker/docker-schema.json new file mode 100644 index 0000000..75cf274 --- /dev/null +++ b/src/resources/docker/docker-schema.json @@ -0,0 +1,18 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema", + "$id": "https://www.codifycli.com/docker.json", + "title": "Docker resource", + "type": "object", + "description": "Installs docker.", + "properties": { + "acceptLicense": { + "type": "boolean", + "description": "Accepts the license agreement. Defaults to true" + }, + "useCurrentUser": { + "type": "boolean", + "description": "Use the current user to install docker. Defaults to true" + } + }, + "additionalProperties": false +} diff --git a/src/resources/docker/docker.ts b/src/resources/docker/docker.ts new file mode 100644 index 0000000..4c70218 --- /dev/null +++ b/src/resources/docker/docker.ts @@ -0,0 +1,102 @@ +import { CreatePlan, DestroyPlan, Resource, ResourceSettings, getPty } from 'codify-plugin-lib'; +import { StringIndexedObject } from 'codify-schemas'; +import fs from 'node:fs/promises'; +import os from 'node:os'; +import path from 'node:path'; + +import { SpawnStatus, codifySpawn } from '../../utils/codify-spawn.js'; +import { FileUtils } from '../../utils/file-utils.js'; +import { Utils } from '../../utils/index.js'; +import Schema from './docker-schema.json'; + +export interface DockerConfig extends StringIndexedObject { + acceptLicense?: boolean; + useCurrentUser?: boolean; +} + +const ARM_DOWNLOAD_LINK = 'https://desktop.docker.com/mac/main/arm64/Docker.dmg' +const INTEL_DOWNLOAD_LINK = 'https://desktop.docker.com/mac/main/amd64/Docker.dmg' + +export class DockerResource extends Resource { + getSettings(): ResourceSettings { + return { + id: 'docker', + schema: Schema, + parameterSettings: { + acceptLicense: { + type: 'boolean', + setting: true, + default: true, + }, + // version: { + // type: 'version' + // }, + useCurrentUser: { + type: 'boolean', + setting: true, + default: true, + } + } + }; + } + + async refresh(): Promise | Partial[] | null> { + const $ = getPty(); + + const versionResult = await $.spawnSafe('docker --version'); + if (versionResult.status === SpawnStatus.ERROR) { + return null; + } + + const result: DockerConfig = {}; + + // TODO: support versioning in the future + // const version = /Docker version (.*), build/.exec(versionResult.data)?.[1]; + // if (version && parameters.version) { + // result.version = version; + // } + + return result; + } + + /** + * References: + * Blog about docker changes: https://dazwallace.wordpress.com/2022/12/02/changes-to-docker-desktop-for-mac/ + * Path: https://stackoverflow.com/questions/64009138/docker-command-not-found-when-running-on-mac + * Issue: https://github.com/docker/for-mac/issues/6504 + * @param plan + */ + async create(plan: CreatePlan): Promise { + const downloadLink = await Utils.isArmArch() ? ARM_DOWNLOAD_LINK : INTEL_DOWNLOAD_LINK; + + const tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), 'codify-docker')) + await Utils.downloadUrlIntoFile(path.join(tmpDir, 'Docker.dmg'), downloadLink); + const user = Utils.getUser(); + + try { + await codifySpawn('hdiutil attach Docker.dmg', { cwd: tmpDir, requiresRoot: true }) + + console.log('Running Docker installer. This may take a couple of minutes to complete...') + await codifySpawn(`/Volumes/Docker/Docker.app/Contents/MacOS/install ${plan.desiredConfig.acceptLicense ? '--accept-license' : ''} ${plan.desiredConfig.useCurrentUser ? `--user ${user}` : ''}`, + { requiresRoot: true } + ) + await codifySpawn('hdiutil detach /Volumes/Docker', { cwd: tmpDir, requiresRoot: true }) + } finally { + await fs.rm(tmpDir, { recursive: true, force: true }) + } + + await codifySpawn('xattr -r -d com.apple.quarantine /Applications/Docker.app', { requiresRoot: true }); + await FileUtils.addPathToZshrc('/Applications/Docker.app/Contents/Resources/bin', false); + } + + async destroy(plan: DestroyPlan): Promise { + await codifySpawn('/Applications/Docker.app/Contents/MacOS/uninstall', { throws: false }) + await fs.rm(path.join(os.homedir(), 'Library/Group\\ Containers/group.com.docker'), { recursive: true, force: true }); + await fs.rm(path.join(os.homedir(), 'Library/Containers/com.docker.docker/Data'), { recursive: true, force: true }); + await fs.rm(path.join(os.homedir(), '.docker'), { recursive: true, force: true }); + await codifySpawn('rm -rf /Applications/Docker.app', { requiresRoot: true }) + + await FileUtils.removeLineFromZshrc('/Applications/Docker.app/Contents/Resources/bin') + } + +} diff --git a/src/resources/node/npm/npm.ts b/src/resources/node/npm/npm.ts index 2b3bd8e..5bcaf0c 100644 --- a/src/resources/node/npm/npm.ts +++ b/src/resources/node/npm/npm.ts @@ -1,4 +1,4 @@ -import { CreatePlan, DestroyPlan, RefreshContext, Resource, ResourceSettings, getPty } from 'codify-plugin-lib'; +import { Resource, ResourceSettings, getPty } from 'codify-plugin-lib'; import { ResourceConfig } from 'codify-schemas'; import { NpmGlobalInstallParameter, NpmPackage } from './global-install.js'; @@ -22,7 +22,7 @@ export class Npm extends Resource { } } - async refresh(parameters: Partial, context: RefreshContext): Promise | Partial[] | null> { + async refresh(parameters: Partial): Promise | Partial[] | null> { const pty = getPty(); const { status } = await pty.spawnSafe('which npm') @@ -34,9 +34,9 @@ export class Npm extends Resource { } // Npm gets created with NodeJS - async create(plan: CreatePlan): Promise {} + async create(): Promise {} // Npm is destroyed with NodeJS - destroy(plan: DestroyPlan): Promise {} + async destroy(): Promise {} } diff --git a/src/utils/index.ts b/src/utils/index.ts index 0004292..a5cb3bf 100644 --- a/src/utils/index.ts +++ b/src/utils/index.ts @@ -1,11 +1,12 @@ -import * as fs from 'node:fs/promises'; import * as fsSync from 'node:fs'; - -import { codifySpawn, SpawnStatus } from './codify-spawn.js'; -import { SpotlightKind, SpotlightUtils } from './spotlight-search.js'; +import * as fs from 'node:fs/promises'; +import os from 'node:os'; import path from 'node:path'; -import { finished } from 'node:stream/promises'; import { Readable } from 'node:stream'; +import { finished } from 'node:stream/promises'; + +import { SpawnStatus, codifySpawn } from './codify-spawn.js'; +import { SpotlightKind, SpotlightUtils } from './spotlight-search.js'; export const Utils = { async findApplication(name: string): Promise { @@ -73,7 +74,7 @@ export const Utils = { async isArmArch(): Promise { const query = await codifySpawn('sysctl -n machdep.cpu.brand_string'); - return /M([0-9])/.test(query.data); + return /M(\d)/.test(query.data); }, async isDirectoryOnPath(directory: string): Promise { @@ -108,4 +109,8 @@ export const Utils = { // Different type definitions here for readable stream (NodeJS vs DOM). Small hack to fix that await finished(Readable.fromWeb(body as never).pipe(ws)); }, + + getUser(): string { + return os.userInfo().username; + } }; diff --git a/test/docker/docker.test.ts b/test/docker/docker.test.ts new file mode 100644 index 0000000..1be5929 --- /dev/null +++ b/test/docker/docker.test.ts @@ -0,0 +1,22 @@ +import { beforeEach, describe, expect, it } from 'vitest'; +import { PluginTester } from 'codify-plugin-test'; +import * as path from 'node:path'; +import cp from 'child_process'; + +describe('Test docker', async () => { + const pluginPath = path.resolve('./src/index.ts'); + + it('Can install docker', { timeout: 300000 }, async () => { + await PluginTester.fullTest(pluginPath, [ + { type: 'docker' }, + ], { + validateApply: async () => { + expect(() => cp.execSync('source ~/.zshrc; which aws;')).to.not.throw; + + }, + validateDestroy: async () => { + expect(() => cp.execSync('source ~/.zshrc; which aws;')).to.throw; + } + }) + }) +})