Skip to content

Commit

Permalink
feat(terraform): allow setting version to null to use terraform on PATH
Browse files Browse the repository at this point in the history
  • Loading branch information
edvald committed Sep 22, 2020
1 parent 18db4ef commit 3b5a0f1
Show file tree
Hide file tree
Showing 10 changed files with 132 additions and 85 deletions.
4 changes: 3 additions & 1 deletion core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,7 @@
"unzipper": "^0.10.11",
"username": "^5.1.0",
"uuid": "^8.1.0",
"which": "^2.0.2",
"winston": "^3.2.1",
"wrap-ansi": "^7.0.0",
"ws": "^7.3.0",
Expand Down Expand Up @@ -196,6 +197,7 @@
"@types/unzip": "^0.1.1",
"@types/unzipper": "^0.10.3",
"@types/uuid": "^8.0.0",
"@types/which": "^1.3.2",
"@types/wrap-ansi": "^3.0.0",
"amplitude-js": "^6.2.0",
"chai": "^4.2.0",
Expand Down Expand Up @@ -247,4 +249,4 @@
]
},
"gitHead": "b0647221a4d2ff06952bae58000b104215aed922"
}
}
38 changes: 30 additions & 8 deletions core/src/plugins/terraform/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,23 +6,45 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
*/

import { ConfigurationError } from "../../exceptions"
import { ConfigurationError, RuntimeError } from "../../exceptions"
import { PluginToolSpec } from "../../types/plugin/tools"
import { TerraformProvider } from "./terraform"
import { PluginContext } from "../../plugin-context"
import which from "which"
import { CliWrapper } from "../../util/ext-tools"
import { LogEntry } from "../../logger/log-entry"

export function terraform(ctx: PluginContext, provider: TerraformProvider) {
const version = provider.config.version
const cli = ctx.tools["terraform.terraform-" + version.replace(/\./g, "-")]

if (!cli) {
throw new ConfigurationError(`Unsupported Terraform version: ${version}`, {
version,
supportedVersions,
})
if (version === null) {
return new GlobalTerraform()
} else {
const cli = ctx.tools["terraform.terraform-" + version.replace(/\./g, "-")]

if (!cli) {
throw new ConfigurationError(`Unsupported Terraform version: ${version}`, {
version,
supportedVersions,
})
}

return cli
}
}

export class GlobalTerraform extends CliWrapper {
constructor() {
super("terraform", "terraform")
}

return cli
async getPath(_: LogEntry) {
try {
return await which("terraform")
} catch (err) {
throw new RuntimeError(`Terraform version is set to null, and terraform CLI could not be found on PATH`, {})
}
}
}

export const terraformCliSpecs: { [version: string]: PluginToolSpec } = {
Expand Down
4 changes: 2 additions & 2 deletions core/src/plugins/terraform/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ function makeRootCommand(commandName: string) {

const root = join(ctx.projectRoot, provider.config.initRoot)

await tfValidate({ log, ctx, provider, root, variables: provider.config.variables })
await tfValidate({ log, ctx, provider, root })

args = [commandName, ...(await prepareVariables(root, provider.config.variables)), ...args]
await terraform(ctx, provider).spawnAndWait({
Expand Down Expand Up @@ -87,7 +87,7 @@ function makeModuleCommand(commandName: string) {
const root = join(module.path, module.spec.root)

const provider = ctx.provider as TerraformProvider
await tfValidate({ log, ctx, provider, root, variables: provider.config.variables })
await tfValidate({ log, ctx, provider, root })

args = [commandName, ...(await prepareVariables(root, module.spec.variables)), ...args.slice(1)]
await terraform(ctx, provider).spawnAndWait({
Expand Down
2 changes: 1 addition & 1 deletion core/src/plugins/terraform/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ export interface TerraformBaseSpec {
autoApply: boolean
dependencies: string[]
variables: PrimitiveMap
version: string
version: string | null
}

export async function tfValidate({
Expand Down
3 changes: 2 additions & 1 deletion core/src/plugins/terraform/module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,8 +62,9 @@ export const schema = joi.object().keys({
If you specified \`variables\` in the \`terraform\` provider config, those will be included but the variables
specified here take precedence.
`),
version: joi.string().allow(...supportedVersions).description(dedent`
version: joi.string().allow(...supportedVersions, null).description(dedent`
The version of Terraform to use. Defaults to the version set in the provider config.
Set to \`null\` to use whichever version of \`terraform\` that is on your PATH.
`),
})

Expand Down
4 changes: 2 additions & 2 deletions core/src/plugins/terraform/terraform.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,9 @@ const configSchema = providerConfigBaseSchema()
// May be overridden by individual \`terraform\` modules.
version: joi
.string()
.allow(...supportedVersions)
.allow(...supportedVersions, null)
.default(defaultTerraformVersion).description(dedent`
The version of Terraform to use.
The version of Terraform to use. Set to \`null\` to use whichever version of \`terraform\` that is on your PATH.
`),
})
.unknown(false)
Expand Down
152 changes: 83 additions & 69 deletions core/src/util/ext-tools.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,79 +47,17 @@ export interface SpawnParams extends ExecParams {
rawMode?: boolean // Only used if tty = true. See also: https://nodejs.org/api/tty.html#tty_readstream_setrawmode_mode
}

interface PluginToolOpts {
platform?: string
architecture?: string
}

export interface PluginTools {
[key: string]: PluginTool
}

/**
* This helper class allows you to declare a tool dependency by providing a URL to a single-file binary,
* or an archive containing an executable, for each of our supported platforms. When executing the tool,
* the appropriate URL for the current platform will be downloaded and cached in the user's home directory
* (under .garden/tools/<name>/<url-hash>).
*
* Note: The binary or archive currently needs to be self-contained and work without further installation steps.
*/
export class PluginTool {
export class CliWrapper {
name: string
type: string
spec: PluginToolSpec
buildSpec: ToolBuildSpec

private lock: any
private toolPath: string
private versionDirname: string
protected versionPath: string
protected targetSubpath: string
private chmodDone: boolean

constructor(spec: PluginToolSpec, opts: PluginToolOpts = {}) {
const _platform = opts.platform || getPlatform()
const architecture = opts.architecture || getArchitecture()

this.buildSpec = spec.builds.find((build) => build.platform === _platform && build.architecture === architecture)!
protected toolPath: string

if (!this.buildSpec) {
throw new ConfigurationError(
`Command ${spec.name} doesn't have a spec for this platform/architecture (${platform}-${architecture})`,
{
spec,
platform,
architecture,
}
)
}

this.lock = new AsyncLock()

this.name = spec.name
this.type = spec.type
this.spec = spec
this.toolPath = join(toolsPath, this.name)
this.versionDirname = hashString(this.buildSpec.url, 16)
this.versionPath = join(this.toolPath, this.versionDirname)

this.targetSubpath = this.buildSpec.extract ? this.buildSpec.extract.targetPath : basename(this.buildSpec.url)
this.chmodDone = false
constructor(name: string, path: string) {
this.name = name
this.toolPath = path
}

async getPath(log: LogEntry) {
await this.download(log)
const path = join(this.versionPath, ...this.targetSubpath.split(posix.sep))

if (this.spec.type === "binary") {
// Make sure the target path is executable
if (!this.chmodDone) {
await chmod(path, 0o755)
this.chmodDone = true
}
}

return path
async getPath(_: LogEntry) {
return this.toolPath
}

async exec({ args, cwd, env, log, timeoutSec, input, ignoreError, stdout, stderr }: ExecParams) {
Expand Down Expand Up @@ -199,6 +137,82 @@ export class PluginTool {
tty,
})
}
}

interface PluginToolOpts {
platform?: string
architecture?: string
}

export interface PluginTools {
[key: string]: PluginTool
}

/**
* This helper class allows you to declare a tool dependency by providing a URL to a single-file binary,
* or an archive containing an executable, for each of our supported platforms. When executing the tool,
* the appropriate URL for the current platform will be downloaded and cached in the user's home directory
* (under .garden/tools/<name>/<url-hash>).
*
* Note: The binary or archive currently needs to be self-contained and work without further installation steps.
*/
export class PluginTool extends CliWrapper {
type: string
spec: PluginToolSpec
buildSpec: ToolBuildSpec

private lock: any
private versionDirname: string
protected versionPath: string
protected targetSubpath: string
private chmodDone: boolean

constructor(spec: PluginToolSpec, opts: PluginToolOpts = {}) {
super(spec.name, "")

const _platform = opts.platform || getPlatform()
const architecture = opts.architecture || getArchitecture()

this.buildSpec = spec.builds.find((build) => build.platform === _platform && build.architecture === architecture)!

if (!this.buildSpec) {
throw new ConfigurationError(
`Command ${spec.name} doesn't have a spec for this platform/architecture (${platform}-${architecture})`,
{
spec,
platform,
architecture,
}
)
}

this.lock = new AsyncLock()

this.name = spec.name
this.type = spec.type
this.spec = spec
this.toolPath = join(toolsPath, this.name)
this.versionDirname = hashString(this.buildSpec.url, 16)
this.versionPath = join(this.toolPath, this.versionDirname)

this.targetSubpath = this.buildSpec.extract ? this.buildSpec.extract.targetPath : basename(this.buildSpec.url)
this.chmodDone = false
}

async getPath(log: LogEntry) {
await this.download(log)
const path = join(this.versionPath, ...this.targetSubpath.split(posix.sep))

if (this.spec.type === "binary") {
// Make sure the target path is executable
if (!this.chmodDone) {
await chmod(path, 0o755)
this.chmodDone = true
}
}

return path
}

protected async download(log: LogEntry) {
return this.lock.acquire("download", async () => {
Expand Down
1 change: 1 addition & 0 deletions core/test/data/test-projects/terraform-provider/garden.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ providers:
allowDestroy: ${environment.name != 'prod'}
autoApply: ${environment.name != 'prod'}
initRoot: tf
version: "0.13.3"
variables:
my-variable: foo
env: ${environment.name}
Expand Down
2 changes: 2 additions & 0 deletions docs/reference/module-types/terraform.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,7 @@ root: .
variables:

# The version of Terraform to use. Defaults to the version set in the provider config.
# Set to `null` to use whichever version of `terraform` that is on your PATH.
version:
```
Expand Down Expand Up @@ -389,6 +390,7 @@ specified here take precedence.
### `version`

The version of Terraform to use. Defaults to the version set in the provider config.
Set to `null` to use whichever version of `terraform` that is on your PATH.

| Type | Required |
| -------- | -------- |
Expand Down
7 changes: 6 additions & 1 deletion yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -2931,6 +2931,11 @@
resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-8.3.0.tgz#215c231dff736d5ba92410e6d602050cce7e273f"
integrity sha512-eQ9qFW/fhfGJF8WKHGEHZEyVWfZxrT+6CLIJGBcZPfxUh/+BnEj+UCGYMlr9qZuX/2AltsvwrGqp0LhEW8D0zQ==

"@types/which@^1.3.2":
version "1.3.2"
resolved "https://registry.yarnpkg.com/@types/which/-/which-1.3.2.tgz#9c246fc0c93ded311c8512df2891fb41f6227fdf"
integrity sha512-8oDqyLC7eD4HM307boe2QWKyuzdzWBj56xI/imSl2cpL+U3tCMaTAkMJ4ee5JBZ/FsOJlvRGeIShiZDAl1qERA==

"@types/wrap-ansi@^3.0.0":
version "3.0.0"
resolved "https://registry.yarnpkg.com/@types/wrap-ansi/-/wrap-ansi-3.0.0.tgz#18b97a972f94f60a679fd5c796d96421b9abb9fd"
Expand Down Expand Up @@ -18797,7 +18802,7 @@ which@1, [email protected], which@^1.2.14, which@^1.2.9, which@^1.3.0, which@^1.3.1:
dependencies:
isexe "^2.0.0"

[email protected], which@^2.0.1:
[email protected], which@^2.0.1, which@^2.0.2:
version "2.0.2"
resolved "https://registry.yarnpkg.com/which/-/which-2.0.2.tgz#7c6a8dd0a636a0327e10b59c9286eee93f3f51b1"
integrity sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==
Expand Down

0 comments on commit 3b5a0f1

Please sign in to comment.