Skip to content

Commit

Permalink
feat(k8s): allow owner/perm tweaks on dev mode syncs
Browse files Browse the repository at this point in the history
Also updated our docs a bit.

Closes #2519
  • Loading branch information
edvald authored and thsig committed Aug 20, 2021
1 parent 7a01e41 commit eb4be42
Show file tree
Hide file tree
Showing 10 changed files with 445 additions and 77 deletions.
37 changes: 35 additions & 2 deletions core/src/plugins/container/config.ts
Expand Up @@ -186,8 +186,18 @@ export interface DevModeSyncSpec {
target: string
mode: SyncMode
exclude?: string[]
defaultFileMode?: number
defaultDirectoryMode?: number
defaultOwner?: number | string
defaultGroup?: number | string
}

const permissionsDocs =
"See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#permissions) for more information."

const ownerDocs =
"Specify either an integer ID or a string name. See the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions#owners-and-groups) for more information."

const devModeSyncSchema = () =>
hotReloadSyncSchema().keys({
exclude: joi
Expand All @@ -198,10 +208,33 @@ const devModeSyncSchema = () =>
mode: joi
.string()
.allow("one-way", "one-way-replica", "two-way")
.only()
.default("one-way")
.description(
"The sync mode to use for the given paths. Allowed options: `one-way`, `one-way-replica`, `two-way`."
),
defaultFileMode: joi
.number()
.min(0)
.max(0o777)
.description(
"The default permission bits, specified as an octal, to set on files at the sync target. Defaults to 0600 (user read/write). " +
permissionsDocs
),
defaultDirectoryMode: joi
.number()
.min(0)
.max(0o777)
.description(
"The default permission bits, specified as an octal, to set on directories at the sync target. Defaults to 0700 (user read/write). " +
permissionsDocs
),
defaultOwner: joi
.alternatives(joi.number().integer(), joi.string())
.description("Set the default owner of files and directories at the target. " + ownerDocs),
defaultGroup: joi
.alternatives(joi.number().integer(), joi.string())
.description("Set the default group on files and directories at the target. " + ownerDocs),
})

export interface ContainerDevModeSpec {
Expand All @@ -222,11 +255,11 @@ export const containerDevModeSchema = () =>
.items(devModeSyncSchema())
.description("Specify one or more source files or directories to automatically sync with the running container."),
}).description(dedent`
**EXPERIMENTAL**
Specifies which files or directories to sync to which paths inside the running containers of the service when it's in dev mode, and overrides for the container command and/or arguments.
Dev mode is enabled when running the \`garden dev\` command, and by setting the \`--dev\` flag on the \`garden deploy\` command.
See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more information.
`)

export type ContainerServiceConfig = ServiceConfig<ContainerServiceSpec>
Expand Down
4 changes: 3 additions & 1 deletion core/src/plugins/kubernetes/dev-mode.ts
Expand Up @@ -45,7 +45,9 @@ export const kubernetesDevModeSchema = () =>
Note that \`serviceResource\` must also be specified to enable dev mode.
Dev mode is enabled when running the \`garden dev\` command, and by setting the \`--dev\` flag on the \`garden deploy\` command.
`)
See the [Code Synchronization guide](https://docs.garden.io/guides/code-synchronization-dev-mode) for more information.
`)

/**
* Configures the specified Deployment, DaemonSet or StatefulSet for dev mode.
Expand Down
35 changes: 34 additions & 1 deletion core/src/plugins/kubernetes/mutagen.ts
Expand Up @@ -36,6 +36,10 @@ interface SyncConfig {
beta: string
mode: keyof typeof mutagenModeMap
ignore: string[]
defaultOwner?: number | string
defaultGroup?: number | string
defaultFileMode?: number
defaultDirectoryMode?: number
}

interface ActiveSync {
Expand All @@ -47,6 +51,7 @@ interface ActiveSync {
config: SyncConfig
lastProblems: string[]
lastSyncCount: number
mutagenParameters: string[]
}

let activeSyncs: { [key: string]: ActiveSync } = {}
Expand Down Expand Up @@ -400,7 +405,27 @@ export async function ensureMutagenSync({
const existing = active.find((s: any) => s.name === key)

if (!existing) {
await execMutagenCommand(log, ["sync", "create", config.alpha, config.beta, "--name", key, "--auto-start=false"])
const { alpha, beta, ignore, mode, defaultOwner, defaultGroup, defaultDirectoryMode, defaultFileMode } = config

const ignoreFlags = ignore.flatMap((i) => ["-i", i])
const syncMode = mutagenModeMap[mode]
const params = [alpha, beta, "--name", key, "--auto-start=false", "--sync-mode", syncMode, ...ignoreFlags]

if (defaultOwner) {
params.push("--default-owner", defaultOwner.toString())
}
if (defaultGroup) {
params.push("--default-group", defaultGroup.toString())
}
if (defaultFileMode) {
params.push("--default-file-mode", modeToString(defaultFileMode))
}
if (defaultDirectoryMode) {
params.push("--default-directory-mode", modeToString(defaultDirectoryMode))
}

await execMutagenCommand(log, ["sync", "create", ...params])

activeSyncs[key] = {
sourceDescription,
targetDescription,
Expand All @@ -410,6 +435,7 @@ export async function ensureMutagenSync({
config,
lastProblems: [],
lastSyncCount: 0,
mutagenParameters: params,
}
}
})
Expand Down Expand Up @@ -481,3 +507,10 @@ const mutagen = new PluginTool(mutagenCliSpec)
async function isValidLocalPath(syncPoint: string) {
return pathExists(syncPoint)
}

/**
* Converts an octal permission mask to string.
*/
function modeToString(mode: number) {
return "0" + mode.toString(8)
}
1 change: 1 addition & 0 deletions docs/README.md
Expand Up @@ -30,6 +30,7 @@
## 🌿 Guides

* [Cloud Provider Set-up](./guides/cloud-provider-setup.md)
* [Code Synchronization (Dev Mode)](./guides/code-synchronization-dev-mode.md)
* [Container Modules](./guides/container-modules.md)
* [Helm Charts](./guides/using-helm-charts.md)
* [Hot Reload](./guides/hot-reload.md)
Expand Down
105 changes: 105 additions & 0 deletions docs/guides/code-synchronization-dev-mode.md
@@ -0,0 +1,105 @@
# Code Synchronization (Dev Mode)

You can synchronize your code (and other files) to and from running containers using _dev mode_.

Dev mode works similarly to the older [hot reloading functionality](./hot-reload.md), but is much faster and more reliable. It also supports bidirectional syncing, which enables you to sync new/changed files from your containers to your local machine.

This new sync mode uses [Mutagen](https://mutagen.io/) under the hood. Garden automatically takes care of fetching Mutagen, so you don't need to install any dependencies yourself to make use of dev mode.

Dev mode sync is not affected by includes/excludes, which makes it more flexible than hot reloading. For example, you can use it to sync your `build`/`dist` directory into your container while running local, incremental builds (without having to remove those directories from your ignorefiles).

{% hint style="warning" %}
Please make sure to specify any paths that should not be synced, by setting the `exclude` field on each configured sync! Otherwise you may end up syncing large directories and even run into application errors.
{% endhint %}

## Configuration

To configure a service for dev mode, add `devMode` to your module/service configuration to specify your sync targets:

### Configuring dev mode for `container` modules

```yaml
kind: Module
description: Node greeting service
name: node-service
type: container
services:
- name: node-service
args: [npm, start]
devMode:
command: [npm, run, dev] # Overrides the container's default when the service is deployed in dev mode
sync:
# Source/target configuration for dev mode is the same as for hot reloading.
- target: /app
# Make sure to specify any paths that should not be synced!
exclude: [node_modules]
# You can use several sync specs for the same service.
- source: /tmp/somedir
target: /somedir
...
```

### Configuring dev mode for `kubernetes` and `helm` modules

```yaml
kind: Module
type: kubernetes # this example looks the same for helm modules (i.e. with `type: helm`)
name: node-service
# For `kubernetes` and `helm` modules, the `devMode` field is located at the top level.
devMode:
command: [npm, run, dev]
sync:
- target: /app
- source: /tmp/somedir
target: /somedir
serviceResource:
kind: Deployment
name: node-service-deployment
containerModule: node-service-image
containerName: node-service
...
```

## Deploying with dev mode

To deploy your services with dev mode enabled, you can use the `deploy` or `dev` commands:

```sh
# Deploy specific services in dev mode:
garden deploy --dev myservice
garden deploy --dev myservice,my-other-service

# Deploy all applicable services in dev mode:
garden deploy --dev=*

# The dev command deploys services in dev mode by default:
garden dev myservice
```

## Permissions and ownership

In certain cases you may need to set a specific owner/group or permission bits on the synced files and directories at the target.

To do this, you can set a few options on each sync:

```yaml
kind: Module
description: Node greeting service
name: node-service
type: container
services:
- name: node-service
args: [npm, start]
devMode:
command: [npm, run, dev]
sync:
- target: /app
exclude: [node_modules]
defaultOwner: 1000 # <- set an integer user ID or a string name
defaultGroup: 1000 # <- set an integer group ID or a string name
defaultFileMode: 0666 # <- set the permission bits (as octals) for synced files
defaultDirectoryMode: 0777 # <- set the permission bits (as octals) for synced directories
...
```

These options are passed directly to Mutagen. For more information, please see the [Mutagen docs](https://mutagen.io/documentation/synchronization/permissions).
68 changes: 3 additions & 65 deletions docs/guides/hot-reload.md
@@ -1,7 +1,7 @@
# Hot Reload

{% hint style="info" %}
Check out the section below on the brand-new, faster (and still experimental) dev mode—which includes bidirectional sync!
{% hint style="warning" %}
We've now deprecated the older "hot reload" mechanism and replaced it with the new _dev mode_. See the [Code Synchronization guide](./code-synchronization-dev-mode.md) for details.
{% endhint %}

When the `local-kubernetes` or `kubernetes` provider is used, `container` modules can be configured to hot-reload their running services when the module's sources change (i.e. without redeploying). In essence, hot-reloading copies syncs files into the appropriate running containers (local or remote) when code is changed by the user, and optionally runs a post-sync command inside the container.
Expand Down Expand Up @@ -57,7 +57,7 @@ Lastly, `hotReloadArgs` specifies the arguments to use to run the container (whe

## Adding a `postSyncCommand`

A `postSyncCommand` can also be added to a module's hot reload configuration. This command is executed inside the running container during each hot reload, after syncing is completed (as the name suggests).
A `postSyncCommand` can also be added to a module's hot reload configuration. This command is executed inside the running container during each hot reload, after syncing is completed (as the name suggests).

Following is a snippet from the `hot-reload-post-sync-command` example project. Here, a `postSyncCommand` is used to `touch` a file, updating its modification time. This way, `nodemon` only has to watch one file to keep the running application up to date. See the `hot-reload-post-sync-command` example for more details and a fuller discussion.

Expand All @@ -76,65 +76,3 @@ services:
hotReloadArgs: [npm, run, dev] # Runs modemon main.js --watch hotreloadfile
...
```

## Dev mode (experimental)

Dev mode works similarly to hot reloading, but is much faster and more reliable. It also supports bidirectional syncing, which enables you to sync new/changed files from your containers to your local machine.

This new sync mode uses [Mutagen](https://mutagen.io/) under the hood. Garden automatically takes care of fetching Mutagen, so you don't need to install any dependencies yourself to make use of dev mode.

Dev mode sync is not affected by includes/excludes, which makes it more flexible than hot reloading. For example, you can use it to sync your `build`/`dist` directory into your container while running local, incremental builds (without having to remove those directories from your ignorefiles).

Eventually, the plan is to deprecate hot reloading in favor of dev mode.

Dev mode opens up exciting, productive new ways to set up your inner dev loop with Garden. Happy hacking!

Dev mode is currently supported for `container`, `kubernetes` and `helm` modules.

To configure a service for dev mode, add `devMode` to your module/service configuration:

### `container` module example
```yaml
kind: Module
description: Node greeting service
name: node-service
type: container
services:
- name: node-service
args: [npm, start]
devMode:
command: [npm, run, dev] # Overrides the container's default when the service is deployed in dev mode
sync:
# Source/target configuration for dev mode is the same as for hot reloading.
- target: /app
# You can use several sync specs for the same service.
- source: /tmp/somedir
target: /somedir
...
```

### Configuring dev mode for `kubernetes` and `helm` modules
```yaml
kind: Module
type: kubernetes # this example looks the same for helm modules (i.e. with `type: helm`)
name: node-service
# For `kubernetes` and `helm` modules, the `devMode` field is located at the top level.
devMode:
command: [npm, run, dev]
sync:
- target: /app
- source: /tmp/somedir
target: /somedir
serviceResource:
kind: Deployment
name: node-service-deployment
containerModule: node-service-image
containerName: node-service
...
```
To deploy your services with dev mode enabled, you can use the `deploy` or `dev` commands:
```
garden deploy --dev myservice
garden deploy --dev myservice,my-other-service
garden dev myservice # the dev command deploys services in dev mode by default
```

0 comments on commit eb4be42

Please sign in to comment.