Skip to content

Commit

Permalink
feat(core): add persistent ID for each working copy
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
edvald committed Jun 13, 2019
1 parent 51fc76a commit b49ecc3
Show file tree
Hide file tree
Showing 6 changed files with 106 additions and 43 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ node_modules
# Runtime files
.garden
tmp/
metadata.json

# TS cache on the CI
ts-node-*
Expand Down
67 changes: 48 additions & 19 deletions garden-service/src/garden.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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 }
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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)
}
Expand Down Expand Up @@ -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,
Expand All @@ -195,8 +223,9 @@ export class Garden {
ignorer,
opts,
plugins,
providers,
) as InstanceType<T>
providerConfigs: providers,
workingCopyId,
}) as InstanceType<T>

return garden
}
Expand Down
4 changes: 4 additions & 0 deletions garden-service/src/plugin-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ type WrappedFromGarden = Pick<Garden,
"projectRoot" |
"projectSources" |
"gardenDirPath" |
"workingCopyId" |
// TODO: remove this from the interface
"configStore" |
"environmentName"
Expand Down Expand Up @@ -47,6 +48,8 @@ export const pluginContextSchema = Joi.object()
environmentName: environmentNameSchema,
provider: providerSchema
.description("The provider being used for this context."),
workingCopyId: Joi.string()
.description("A unique ID assigned to the current project working copy."),
})

export async function createPluginContext(garden: Garden, providerName: string): Promise<PluginContext> {
Expand All @@ -69,5 +72,6 @@ export async function createPluginContext(garden: Garden, providerName: string):
projectSources: cloneDeep(garden.projectSources),
configStore: garden.configStore,
provider,
workingCopyId: garden.workingCopyId,
}
}
30 changes: 29 additions & 1 deletion garden-service/src/util/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
}
27 changes: 4 additions & 23 deletions garden-service/test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,26 +14,22 @@ 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,
PluginFactory,
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"
Expand Down Expand Up @@ -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)
}
}
Expand Down
20 changes: 20 additions & 0 deletions garden-service/test/unit/src/util/fs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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 })
})
})
})

0 comments on commit b49ecc3

Please sign in to comment.