Skip to content

Commit 3c1fa5a

Browse files
committed
feat(plugins): add local flag to exec module type
When local is set to true, the build command, tests and tasks are run in the module directory, as opposed in the .garden/build directory. Furthermore, the source code for local exec modules does not get synced into the .garden/build directory.
1 parent 8cddab8 commit 3c1fa5a

File tree

28 files changed

+552
-118
lines changed

28 files changed

+552
-118
lines changed

docs/reference/module-types/exec.md

Lines changed: 46 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,14 @@ title: Exec
77
A simple module for executing commands in your shell. This can be a useful escape hatch if no other module
88
type fits your needs, and you just need to execute something (as opposed to deploy it, track its status etc.).
99

10+
By default, the `exec` module type executes the commands in the Garden build directory
11+
(under .garden/build/<module-name>). By setting `local: true`, the commands are executed in the module
12+
source directory instead.
13+
14+
Note that Garden does not sync the source code for local exec modules into the Garden build directory.
15+
This means that include/exclude filters and ignore files are not applied to local exec modules, as the
16+
filtering is done during the sync.
17+
1018
Below is the schema reference. For an introduction to configuring Garden modules, please look at our [Configuration
1119
guide](../../using-garden/configuration-files.md).
1220
The [first section](#configuration-keys) lists and describes the available
@@ -206,7 +214,10 @@ POSIX-style path or filename to copy the directory or file(s).
206214

207215
[build](#build) > command
208216

209-
The command to run inside the module's directory to perform the build.
217+
The command to run to perform the build.
218+
219+
By default, the command is run inside the Garden build directory (under .garden/build/<module-name>).
220+
If the top level `local` directive is set to `true`, the command runs in the module source directory instead.
210221

211222
| Type | Required | Default |
212223
| --------------- | -------- | ------- |
@@ -223,6 +234,18 @@ build:
223234
- build
224235
```
225236

237+
### `local`
238+
239+
If set to true, Garden will run the build command, tests, and tasks in the module source directory,
240+
instead of in the Garden build directory (under .garden/build/<module-name>).
241+
242+
Garden will therefore not stage the build for local exec modules. This means that include/exclude filters
243+
and ignore files are not applied to local exec modules.
244+
245+
| Type | Required | Default |
246+
| --------- | -------- | ------- |
247+
| `boolean` | No | `false` |
248+
226249
### `env`
227250

228251
Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives.
@@ -283,11 +306,24 @@ Maximum duration (in seconds) of the task's execution.
283306

284307
[tasks](#tasks) > command
285308

286-
The command to run in the module build context.
309+
The command to run.
310+
311+
By default, the command is run inside the Garden build directory (under .garden/build/<module-name>).
312+
If the top level `local` directive is set to `true`, the command runs in the module source directory instead.
287313

288314
| Type | Required |
289315
| --------------- | -------- |
290-
| `array[string]` | No |
316+
| `array[string]` | Yes |
317+
318+
### `tasks[].env`
319+
320+
[tasks](#tasks) > env
321+
322+
Key/value map of environment variables. Keys must be valid POSIX environment variable names (must not start with `GARDEN`) and values must be primitives.
323+
324+
| Type | Required | Default |
325+
| -------- | -------- | ------- |
326+
| `object` | No | `{}` |
291327

292328
### `tests`
293329

@@ -331,11 +367,14 @@ Maximum duration (in seconds) of the test run.
331367

332368
[tests](#tests) > command
333369

334-
The command to run in the module build context in order to test it.
370+
The command to run to test the module.
371+
372+
By default, the command is run inside the Garden build directory (under .garden/build/<module-name>).
373+
If the top level `local` directive is set to `true`, the command runs in the module source directory instead.
335374

336375
| Type | Required |
337376
| --------------- | -------- |
338-
| `array[string]` | No |
377+
| `array[string]` | Yes |
339378

340379
### `tests[].env`
341380

@@ -366,13 +405,15 @@ build:
366405
- source:
367406
target: <same as source path>
368407
command: []
408+
local: false
369409
env: {}
370410
tasks:
371411
- name:
372412
description:
373413
dependencies: []
374414
timeout: null
375415
command:
416+
env: {}
376417
tests:
377418
- name:
378419
dependencies: []

docs/reference/module-types/openfaas.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -288,7 +288,7 @@ The command to run in the module build context in order to test it.
288288

289289
| Type | Required |
290290
| --------------- | -------- |
291-
| `array[string]` | No |
291+
| `array[string]` | Yes |
292292

293293
### `tests[].env`
294294

examples/local-exec/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
bin

examples/local-exec/README.md

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
# Local Exec (Executing local commands with Garden)
2+
3+
> Note: You need to have Go installed to run this project.
4+
5+
This example project demonstrates how you can use the `exec` module type to run build commands, tasks and tests in the module directory, by setting `local: true` in the module config. By default the commands are executed in the `.garden/build` directory.
6+
7+
The idea is to use a local `exec` module to run pre-build commands for a container module.
8+
9+
## Project Structure
10+
11+
The project consists of a `builder` module and a `backend` module. Both modules are in the same `garden.yml` file in the `backend` directory.
12+
13+
The `backend` module is a simple `container` module that acts as a web server written in Go. The corresponding Dockerfile expects the web server binary to already be built before adding it to the image.
14+
15+
To achieve this, we add a `go build` command to the `builder` module, set `local: true`, and then declare it as a build dependency in the `backend` module. We also tell Garden to copy the built binary to the `backend` build context since we're git ignoring it. This way, it's available with rest of the `backend` build context at `./garden/build/backend`. These are the relevant parts of the config
16+
17+
```yaml
18+
# backend/garden.yml
19+
kind: Module
20+
type: exec
21+
local: true
22+
...
23+
build:
24+
command: [go, build, -o, bin/backend]
25+
---
26+
kind: Module
27+
type: container
28+
build:
29+
dependencies:
30+
- name: builder
31+
copy:
32+
- source: bin
33+
target: .
34+
...
35+
```
36+
37+
This ensures that Garden runs `go build` in the module directory before it attempts to build the Docker image for the `backend` module.
38+
39+
## Usage
40+
41+
Run `garden deploy` to deploy the project. You'll notice that Garden first builds the Go binary, before it's added to the container image.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
FROM golang:1.8.3-alpine
2+
3+
WORKDIR /server/
4+
COPY bin/backend .
5+
6+
ENTRYPOINT ./backend
7+
8+
EXPOSE 8080
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
kind: Module
2+
type: exec
3+
local: true
4+
name: builder
5+
# This runs in the module directory before the backend container that's specified below is built
6+
build:
7+
command: [go, build, -o, bin/backend]
8+
env:
9+
GOOS: linux
10+
GOARCH: amd64
11+
---
12+
# This module's Dockerfile expects the Go binary to already be built
13+
kind: Module
14+
type: container
15+
name: backend
16+
# Setting the "builder" module as a dependency ensures that the Go binary is built
17+
# before the build step for this module is executed.
18+
build:
19+
dependencies:
20+
- name: builder
21+
copy:
22+
- source: bin
23+
target: .
24+
services:
25+
- name: backend
26+
ports:
27+
- name: http
28+
containerPort: 8080
29+
# Maps service:80 -> container:8080
30+
servicePort: 80
31+
ingresses:
32+
- path: /hello-backend
33+
port: http

examples/local-exec/backend/main.go

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package main
2+
3+
import (
4+
"fmt"
5+
"net/http"
6+
)
7+
8+
func handler(w http.ResponseWriter, r *http.Request) {
9+
fmt.Fprint(w, "Hello from Go!")
10+
}
11+
12+
func main() {
13+
http.HandleFunc("/hello-backend", handler)
14+
fmt.Println("Server running...")
15+
16+
http.ListenAndServe(":8080", nil)
17+
}

examples/local-exec/garden.yml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
kind: Project
2+
name: local-exec
3+
environments:
4+
- name: local
5+
providers:
6+
- name: local-kubernetes

examples/multiple-modules/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
This project shows how you can configure several modules in a single directory.
44

5-
This is useful, for exmample, when you want to use more than one Dockerfile for the same code.
5+
This is useful, for example, when you want to use more than one Dockerfile for the same code.
66

77
```shell
88
$ garden deploy

garden-service/src/build-dir.ts

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,17 @@ import {
1919
import { emptyDir, ensureDir } from "fs-extra"
2020
import { ConfigurationError } from "./exceptions"
2121
import { FileCopySpec, Module, getModuleKey } from "./types/module"
22-
import { zip } from "lodash"
2322
import execa from "execa"
2423
import { normalizeLocalRsyncPath } from "./util/fs"
2524
import { LogEntry } from "./logger/log-entry"
25+
import { ModuleConfig } from "./config/module"
26+
import { ConfigGraph } from "./config-graph"
27+
28+
// FIXME: We don't want to keep special casing this module type so we need to think
29+
// of a better way around this.
30+
function isLocalExecModule(moduleConfig: ModuleConfig) {
31+
return moduleConfig.type === "exec" && moduleConfig.spec.local
32+
}
2633

2734
// Lazily construct a directory of modules inside which all build steps are performed.
2835

@@ -38,6 +45,12 @@ export class BuildDir {
3845
}
3946

4047
async syncFromSrc(module: Module, log: LogEntry) {
48+
// We don't sync local exec modules to the build dir
49+
if (isLocalExecModule(module)) {
50+
log.silly("Skipping syncing from source for local exec module")
51+
return
52+
}
53+
4154
const files = module.version.files
4255
// Normalize to relative POSIX-style paths
4356
.map(f => normalize(isAbsolute(f) ? relative(module.path, f) : f))
@@ -52,20 +65,20 @@ export class BuildDir {
5265
})
5366
}
5467

55-
async syncDependencyProducts(module: Module, log: LogEntry) {
56-
const buildPath = await this.buildPath(module.name)
57-
const buildDependencies = await module.build.dependencies
58-
const dependencyConfigs = module.build.dependencies || []
68+
async syncDependencyProducts(module: Module, graph: ConfigGraph, log: LogEntry) {
69+
const buildPath = await this.buildPath(module)
70+
const buildDependencies = module.build.dependencies
5971

60-
await bluebirdMap(zip(buildDependencies, dependencyConfigs), async ([sourceModule, depConfig]) => {
61-
if (!sourceModule || !depConfig || !depConfig.copy) {
72+
await bluebirdMap(buildDependencies, async (buildDepConfig) => {
73+
if (!buildDepConfig || !buildDepConfig.copy) {
6274
return
6375
}
6476

65-
const sourceBuildPath = await this.buildPath(getModuleKey(sourceModule.name, sourceModule.plugin))
77+
const sourceModule = await graph.getModule(getModuleKey(buildDepConfig.name, buildDepConfig.plugin))
78+
const sourceBuildPath = await this.buildPath(sourceModule)
6679

6780
// Sync to the module's top-level dir by default.
68-
await bluebirdMap(depConfig.copy, (copy: FileCopySpec) => {
81+
await bluebirdMap(buildDepConfig.copy, (copy: FileCopySpec) => {
6982
if (isAbsolute(copy.source)) {
7083
throw new ConfigurationError(`Source path in build dependency copy spec must be a relative path`, {
7184
copySpec: copy,
@@ -89,9 +102,18 @@ export class BuildDir {
89102
await emptyDir(this.buildDirPath)
90103
}
91104

92-
async buildPath(moduleName: string): Promise<string> {
93-
const path = resolve(this.buildDirPath, moduleName)
105+
async buildPath(moduleOrConfig: Module | ModuleConfig): Promise<string> {
106+
// We don't stage the build for local exec modules, so the module path is effectively the build path.
107+
if (isLocalExecModule(moduleOrConfig)) {
108+
return moduleOrConfig.path
109+
}
110+
111+
// This returns the same result for modules and module configs
112+
const moduleKey = getModuleKey(moduleOrConfig.name, moduleOrConfig.plugin)
113+
114+
const path = resolve(this.buildDirPath, moduleKey)
94115
await ensureDir(path)
116+
95117
return path
96118
}
97119

garden-service/src/config/config-context.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -514,7 +514,7 @@ export class ModuleConfigContext extends ProviderConfigContext {
514514
stack: [...opts.stack || [], stackKey],
515515
})
516516
const version = await garden.resolveVersion(resolvedConfig.name, resolvedConfig.build.dependencies)
517-
const buildPath = await garden.buildDir.buildPath(config.name)
517+
const buildPath = await garden.buildDir.buildPath(config)
518518

519519
return new ModuleContext(_this, resolvedConfig, buildPath, version)
520520
}],

garden-service/src/plugins/container/build.ts

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,8 @@ import { ContainerModule } from "./config"
1111
import { ConfigurationError } from "../../exceptions"
1212
import { GetBuildStatusParams } from "../../types/plugin/module/getBuildStatus"
1313
import { BuildModuleParams } from "../../types/plugin/module/build"
14-
import chalk from "chalk"
1514
import { LogLevel } from "../../logger/log-node"
16-
import split2 = require("split2")
15+
import { createOutputStream } from "../../util/util"
1716

1817
export async function getContainerBuildStatus({ module, log }: GetBuildStatusParams<ContainerModule>) {
1918
const identifier = await containerHelpers.imageExistsLocally(module)
@@ -65,14 +64,7 @@ export async function buildContainerModule({ module, log }: BuildModuleParams<Co
6564
}
6665

6766
// Stream log to a status line
68-
const outputStream = split2()
69-
const statusLine = log.placeholder(LogLevel.debug)
70-
71-
outputStream.on("error", () => { })
72-
outputStream.on("data", (line: Buffer) => {
73-
statusLine.setState(chalk.gray(" → " + line.toString().slice(0, 80)))
74-
})
75-
67+
const outputStream = createOutputStream(log.placeholder(LogLevel.debug))
7668
const timeout = module.spec.build.timeout
7769
const buildLog = await containerHelpers.dockerCli(module, [...cmdOpts, buildPath], { outputStream, timeout })
7870

0 commit comments

Comments
 (0)