From b49ecc3753d3280d1486909d50330466a3b22e0d Mon Sep 17 00:00:00 2001 From: Jon Edvald Date: Mon, 10 Jun 2019 19:09:11 +0200 Subject: [PATCH] feat(core): add persistent ID for each working copy This dynamically generates a `metadata.json` file when none previously exists in the project's `.garden` directory, and initializes it with a random UUIDv4 ID. We can later use the same file for other working-copy-specific metadata. --- .gitignore | 1 + garden-service/src/garden.ts | 67 ++++++++++++++++++------- garden-service/src/plugin-context.ts | 4 ++ garden-service/src/util/fs.ts | 30 ++++++++++- garden-service/test/helpers.ts | 27 ++-------- garden-service/test/unit/src/util/fs.ts | 20 ++++++++ 6 files changed, 106 insertions(+), 43 deletions(-) diff --git a/.gitignore b/.gitignore index f5afad2b1e..ed9b778ad1 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ node_modules # Runtime files .garden tmp/ +metadata.json # TS cache on the CI ts-node-* diff --git a/garden-service/src/garden.ts b/garden-service/src/garden.ts index 01ac1948ce..13904b7075 100644 --- a/garden-service/src/garden.ts +++ b/garden-service/src/garden.ts @@ -40,7 +40,7 @@ import { platform, arch } from "os" import { LogEntry } from "./logger/log-entry" import { EventBus } from "./events" import { Watcher } from "./watch" -import { getIgnorer, Ignorer, getModulesPathsFromPath, getConfigFilePath } from "./util/fs" +import { getIgnorer, Ignorer, getModulesPathsFromPath, getConfigFilePath, getWorkingCopyId } from "./util/fs" import { Provider, ProviderConfig, getProviderDependencies } from "./config/provider" import { ResolveProviderTask } from "./tasks/resolve-provider" import { ActionHelper } from "./actions" @@ -82,6 +82,21 @@ interface ModuleConfigResolveOpts extends ContextResolveOpts { const asyncLock = new AsyncLock() +export interface GardenParams { + buildDir: BuildDir, + environmentName: string, + gardenDirPath: string, + ignorer: Ignorer, + opts: GardenOpts, + plugins: Plugins, + projectName: string, + projectRoot: string, + projectSources?: SourceConfig[], + providerConfigs: ProviderConfig[], + variables: PrimitiveMap, + workingCopyId: string, +} + export class Garden { public readonly log: LogEntry private readonly loadedPlugins: { [key: string]: GardenPlugin } @@ -100,19 +115,31 @@ export class Garden { private actionHelper: ActionHelper public readonly events: EventBus - constructor( - public readonly projectRoot: string, - public readonly projectName: string, - public readonly environmentName: string, - public readonly variables: PrimitiveMap, - public readonly projectSources: SourceConfig[] = [], - public readonly buildDir: BuildDir, - public readonly gardenDirPath: string, - public readonly ignorer: Ignorer, - public readonly opts: GardenOpts, - plugins: Plugins, - private readonly providerConfigs: ProviderConfig[], - ) { + public readonly projectRoot: string + public readonly projectName: string + public readonly environmentName: string + public readonly variables: PrimitiveMap + public readonly projectSources: SourceConfig[] + public readonly buildDir: BuildDir + public readonly gardenDirPath: string + public readonly ignorer: Ignorer + public readonly opts: GardenOpts + private readonly providerConfigs: ProviderConfig[] + public readonly workingCopyId: string + + constructor(params: GardenParams) { + this.buildDir = params.buildDir + this.environmentName = params.environmentName + this.gardenDirPath = params.gardenDirPath + this.ignorer = params.ignorer + this.opts = params.opts + this.projectName = params.projectName + this.projectRoot = params.projectRoot + this.projectSources = params.projectSources || [] + this.providerConfigs = params.providerConfigs + this.variables = params.variables + this.workingCopyId = params.workingCopyId + // make sure we're on a supported platform const currentPlatform = platform() const currentArch = arch() @@ -126,7 +153,7 @@ export class Garden { } this.modulesScanned = false - this.log = opts.log || getLogger().placeholder() + this.log = this.opts.log || getLogger().placeholder() // TODO: Support other VCS options. this.vcs = new GitHandler(this.gardenDirPath) this.configStore = new LocalConfigStore(this.gardenDirPath) @@ -143,7 +170,7 @@ export class Garden { this.watcher = new Watcher(this, this.log) // Register plugins - for (const [name, pluginFactory] of Object.entries({ ...builtinPlugins, ...plugins })) { + for (const [name, pluginFactory] of Object.entries({ ...builtinPlugins, ...params.plugins })) { // This cast is required for the linter to accept the instance type hackery. this.registerPlugin(name, pluginFactory) } @@ -183,8 +210,9 @@ export class Garden { gardenDirPath = resolve(projectRoot, gardenDirPath || DEFAULT_GARDEN_DIR_NAME) const buildDir = await BuildDir.factory(projectRoot, gardenDirPath) const ignorer = await getIgnorer(projectRoot, gardenDirPath) + const workingCopyId = await getWorkingCopyId(gardenDirPath) - const garden = new this( + const garden = new this({ projectRoot, projectName, environmentName, @@ -195,8 +223,9 @@ export class Garden { ignorer, opts, plugins, - providers, - ) as InstanceType + providerConfigs: providers, + workingCopyId, + }) as InstanceType return garden } diff --git a/garden-service/src/plugin-context.ts b/garden-service/src/plugin-context.ts index 24b1531609..666aad3a83 100644 --- a/garden-service/src/plugin-context.ts +++ b/garden-service/src/plugin-context.ts @@ -20,6 +20,7 @@ type WrappedFromGarden = Pick { @@ -69,5 +72,6 @@ export async function createPluginContext(garden: Garden, providerName: string): projectSources: cloneDeep(garden.projectSources), configStore: garden.configStore, provider, + workingCopyId: garden.workingCopyId, } } diff --git a/garden-service/src/util/fs.ts b/garden-service/src/util/fs.ts index 35b2e64ae2..b6493a0d5e 100644 --- a/garden-service/src/util/fs.ts +++ b/garden-service/src/util/fs.ts @@ -8,16 +8,18 @@ import klaw = require("klaw") import * as _spawn from "cross-spawn" -import { pathExists, readFile } from "fs-extra" import * as Bluebird from "bluebird" +import { pathExists, readFile, writeFile } from "fs-extra" import minimatch = require("minimatch") import { some } from "lodash" +import * as uuid from "uuid" import { join, basename, win32, posix, relative, parse } from "path" import { ValidationError } from "../exceptions" // NOTE: Importing from ignore/ignore doesn't work on Windows const ignore = require("ignore") const VALID_CONFIG_FILENAMES = ["garden.yml", "garden.yaml"] +const metadataFilename = "metadata.json" /* Warning: Don't make any async calls in the loop body when using this function, since this may cause @@ -191,3 +193,29 @@ export function toCygwinPath(path: string) { export function matchGlobs(path: string, patterns: string[]): boolean { return some(patterns, pattern => minimatch(path, pattern)) } + +/** + * Gets an ID for the current working copy, given the path to the project's `.garden` directory. + * We do this by storing a `metadata` file in the directory with an ID. The file is created on demand and a new + * ID is set when it is first generated. + * + * The implication is that removing the `.garden` directory resets the ID, so any remote data attached to the ID + * will be orphaned. Which is usually not a big issue, but something to be mindful of. + */ +export async function getWorkingCopyId(gardenDirPath: string) { + const metadataPath = join(gardenDirPath, metadataFilename) + + let metadata = { + workingCopyId: uuid.v4(), + } + + // TODO: do this in a fully concurrency-safe way + if (await pathExists(metadataPath)) { + const metadataContent = await readFile(metadataPath) + metadata = JSON.parse(metadataContent.toString()) + } else { + await writeFile(metadataPath, JSON.stringify(metadata)) + } + + return metadata.workingCopyId +} diff --git a/garden-service/test/helpers.ts b/garden-service/test/helpers.ts index 5f7d350673..6249527111 100644 --- a/garden-service/test/helpers.ts +++ b/garden-service/test/helpers.ts @@ -14,7 +14,7 @@ import { remove, readdirSync, existsSync } from "fs-extra" import { containerModuleSpecSchema, containerTestSchema, containerTaskSchema } from "../src/plugins/container/config" import { testExecModule, buildExecModule, execBuildSpecSchema } from "../src/plugins/exec" import { TaskResults } from "../src/task-graph" -import { validate, PrimitiveMap, joiArray } from "../src/config/common" +import { validate, joiArray } from "../src/config/common" import { GardenPlugin, PluginActions, @@ -22,18 +22,14 @@ import { ModuleActions, Plugins, } from "../src/types/plugin/plugin" -import { Garden, GardenOpts } from "../src/garden" +import { Garden, GardenParams } from "../src/garden" import { ModuleConfig } from "../src/config/module" import { mapValues, fromPairs } from "lodash" import { ModuleVersion } from "../src/vcs/vcs" import { GARDEN_SERVICE_ROOT } from "../src/constants" import { EventBus, Events } from "../src/events" import { ValueOf } from "../src/util/util" -import { Ignorer } from "../src/util/fs" -import { SourceConfig } from "../src/config/project" -import { BuildDir } from "../src/build-dir" import { LogEntry } from "../src/logger/log-entry" -import { ProviderConfig } from "../src/config/provider" import timekeeper = require("timekeeper") import { GLOBAL_OPTIONS } from "../src/cli/cli" import { RunModuleParams } from "../src/types/plugin/module/runModule" @@ -291,23 +287,8 @@ class TestEventBus extends EventBus { export class TestGarden extends Garden { events: TestEventBus - constructor( - public readonly projectRoot: string, - public readonly projectName: string, - public readonly environmentName: string, - public readonly variables: PrimitiveMap, - public readonly projectSources: SourceConfig[] = [], - public readonly buildDir: BuildDir, - public readonly gardenDirPath: string, - public readonly ignorer: Ignorer, - public readonly opts: GardenOpts, - plugins: Plugins, - providerConfigs: ProviderConfig[], - ) { - super( - projectRoot, projectName, environmentName, variables, projectSources, - buildDir, gardenDirPath, ignorer, opts, plugins, providerConfigs, - ) + constructor(params: GardenParams) { + super(params) this.events = new TestEventBus(this.log) } } diff --git a/garden-service/test/unit/src/util/fs.ts b/garden-service/test/unit/src/util/fs.ts index 9777f715b9..19d089e56c 100644 --- a/garden-service/test/unit/src/util/fs.ts +++ b/garden-service/test/unit/src/util/fs.ts @@ -7,7 +7,9 @@ import { getChildDirNames, isConfigFilename, getConfigFilePath, + getWorkingCopyId, } from "../../../../src/util/fs" +import { withDir } from "tmp-promise" const projectYamlFileExtensions = getDataDir("test-project-yaml-file-extensions") const projectDuplicateYamlFileExtensions = getDataDir("test-project-duplicate-yaml-file-extensions") @@ -100,4 +102,22 @@ describe("util", () => { } }) }) + + describe("getWorkingCopyId", () => { + it("should generate and return a new ID for an empty directory", async () => { + return withDir(async (dir) => { + const id = await getWorkingCopyId(dir.path) + expect(id).to.be.string + }, { unsafeCleanup: true }) + }) + + it("should return the same ID after generating for the first time", async () => { + return withDir(async (dir) => { + const idA = await getWorkingCopyId(dir.path) + const idB = await getWorkingCopyId(dir.path) + + expect(idA).to.equal(idB) + }, { unsafeCleanup: true }) + }) + }) })