Skip to content

Commit

Permalink
feat: implement production flag
Browse files Browse the repository at this point in the history
- Implement production flag + antiAffinity.
- Add default replicas number for production.
- Add revisionHistoryLimit defaults and production.
- Implement command protection.
- Remove force flag from `kubectl`
  • Loading branch information
10ko authored and eysi09 committed Nov 14, 2019
1 parent fbfdf29 commit e0bb7be
Show file tree
Hide file tree
Showing 26 changed files with 237 additions and 53 deletions.
14 changes: 13 additions & 1 deletion docs/guides/remote-kubernetes.md
Original file line number Diff line number Diff line change
Expand Up @@ -131,7 +131,9 @@ you will expose for ingress.
_How you configure DNS and prepare the certificates will depend on how you manage DNS and certificates in general,
so we won't cover that in detail here._

Once you have the certificates in hand (the `.crt` and `.key` files), create a
If you are using cert-manager to manage your TLS certificates you can check out the [cert-manager integration](../guide/cert-manager-integration.md).

If you are manually creating or obtaining the certificates (and you have the `.crt` and `.key` files), create a
[Secret](https://kubernetes.io/docs/concepts/configuration/secret/) for each cert in the cluster so
they can be referenced when deploying services:

Expand Down Expand Up @@ -167,3 +169,13 @@ environments:
namespace: default
...
```

## Production flag

You can define a remote environment as `production` by setting the [production flag](../reference/config.md#environmentsproduction).

This will set up some defaults when deploying the container module type:

- Default number of replicas for a service will be 3 (unless specified by the user).
- A soft AntiAffinity setting will try to schedule pods based over different nodes.
- RevisionHistoryLimit is set to 10.
17 changes: 17 additions & 0 deletions docs/guides/using-helm-charts.md
Original file line number Diff line number Diff line change
Expand Up @@ -300,6 +300,21 @@ sources:
The base chart can also be any `helm` module (not just "base" charts specifically made for that purpose), so you have
a lot of flexibility in how you organize your charts.

## Production flag

You can define a remote environment as `production` by setting the [production flag](../reference/config.md#environmentsproduction).

This will set up some defaults when deploying the container module type:

- Default number of replicas for a service will be 3 (unless specified by the user).
- A soft AntiAffinity setting will try to schedule pods based over different nodes.
- RevisionHistoryLimit is set to 10.

Ultimately it won't run the following operations even if you specify the \`--force\` option:

- Replace the installed chart with \`--replace\` when force installing a Helm chart module.
- Force upgrade a Helm release when using the Helm module type.

## Next steps

Check out the full [helm module reference](../reference/module-types/helm.md) for more details, and the
Expand All @@ -308,3 +323,5 @@ Garden's Helm support.

Also check out the [kubernetes-module](https://github.com/garden-io/garden/tree/v0.10.14/examples/kubernetes-module)
example for a simpler alternative, if you don't need all the features of Helm.


1 change: 1 addition & 0 deletions docs/reference/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ quiet: suppresses all log output, same as --silent.
| `--log-level` | `-l` | `error` `warn` `info` `verbose` `debug` `silly` `0` `1` `2` `3` `4` `5` | Set logger level. Values can be either string or numeric and are prioritized from 0 to 5 (highest to lowest) as follows: error: 0, warn: 1, info: 2, verbose: 3, debug: 4, silly: 5.
| `--output` | `-o` | `json` `yaml` | Output command result in specified format (note: disables progress logging and interactive functionality).
| `--emoji` | | boolean | Enable emoji in output (defaults to true if the environment supports it).
| `--yes` | `-y` | boolean | Automatically approve any yes/no prompts during execution.

### garden build

Expand Down
17 changes: 17 additions & 0 deletions docs/reference/config.md
Original file line number Diff line number Diff line change
Expand Up @@ -429,6 +429,22 @@ The name of the environment.
| -------- | -------- |
| `string` | Yes |

### `environments[].production`

[environments](#environments) > production

Set environment as production.

Setting this flag to `true` will activate the protection on the `deploy`, `test`, `task`, `build`, `call`, `init` and `dev` commands.
A protected command will ask for a user confirmation every time is run agains an environment marked as production.
Run the command with the "--yes" flag to skip the check (e.g. when running Garden in CI).

Note: This flag will affect how certain providers behave. For more details please check the documentation for the providers in use.

| Type | Required | Default |
| --------- | -------- | ------- |
| `boolean` | No | `false` |


## Project YAML schema
```yaml
Expand Down Expand Up @@ -463,6 +479,7 @@ environments:
varfile: garden.<env-name>.env
variables: {}
name:
production: false
```

## Module configuration keys
Expand Down
10 changes: 5 additions & 5 deletions docs/reference/module-types/container.md
Original file line number Diff line number Diff line change
Expand Up @@ -777,12 +777,12 @@ This allows you to call the service from the outside by the node's IP address an

[services](#services) > replicas

The number of instances of the service to deploy.
The number of instances of the service to deploy. Defaults to 3 for environments configured with production: true, otherwise 1.
Note: This setting may be overridden or ignored in some cases. For example, when running with `daemon: true`, with hot-reloading enabled, or if the provider doesn't support multiple replicas.

| Type | Required | Default |
| -------- | -------- | ------- |
| `number` | No | `1` |
| Type | Required |
| -------- | -------- |
| `number` | No |

### `services[].volumes[]`

Expand Down Expand Up @@ -1218,7 +1218,7 @@ services:
servicePort: <same as containerPort>
hostPort:
nodePort:
replicas: 1
replicas:
volumes:
- name:
containerPath:
Expand Down
10 changes: 5 additions & 5 deletions docs/reference/module-types/maven-container.md
Original file line number Diff line number Diff line change
Expand Up @@ -782,12 +782,12 @@ This allows you to call the service from the outside by the node's IP address an

[services](#services) > replicas

The number of instances of the service to deploy.
The number of instances of the service to deploy. Defaults to 3 for environments configured with production: true, otherwise 1.
Note: This setting may be overridden or ignored in some cases. For example, when running with `daemon: true`, with hot-reloading enabled, or if the provider doesn't support multiple replicas.

| Type | Required | Default |
| -------- | -------- | ------- |
| `number` | No | `1` |
| Type | Required |
| -------- | -------- |
| `number` | No |

### `services[].volumes[]`

Expand Down Expand Up @@ -1268,7 +1268,7 @@ services:
servicePort: <same as containerPort>
hostPort:
nodePort:
replicas: 1
replicas:
volumes:
- name:
containerPath:
Expand Down
9 changes: 8 additions & 1 deletion garden-service/src/analytics/analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ import { Garden } from "../garden"
import { Logger, getLogger } from "../logger/logger"
import inquirer = require("inquirer")
import { SEGMENT_PROD_API_KEY, SEGMENT_DEV_API_KEY } from "../constants"
import { GlobalOptions } from "../cli/cli"
import { ParameterValues } from "../commands/base"

const API_KEY = process.env.ANALYTICS_DEV ? SEGMENT_DEV_API_KEY : SEGMENT_PROD_API_KEY

Expand Down Expand Up @@ -90,8 +92,9 @@ export class AnalyticsHandler {
private globalConfigStore: GlobalConfigStore
private localConfigStore: LocalConfigStore
private systemConfig: SystemInfo
private autoAccept: boolean = false

constructor(garden: Garden) {
constructor(garden: Garden, parsedOpts?: ParameterValues<GlobalOptions>) {
// { flushAt: 1 } means the client will track events as soon as they are created
// no batching is occurring: this will change once the daemon is implemented
this.segment = new segmentClient(API_KEY, { flushAt: 1 })
Expand All @@ -112,6 +115,7 @@ export class AnalyticsHandler {
platformVersion: release(),
gardenVersion: getPackageVersion().toString(),
}
this.autoAccept = !!(parsedOpts && parsedOpts.yes)
}

/**
Expand Down Expand Up @@ -286,6 +290,9 @@ export class AnalyticsHandler {
* @memberof Analytics
*/
private async promptAnalytics() {
if (this.autoAccept) {
return true
}
const defaultMessage = dedent`
Thanks for installing Garden! We work hard to provide you with the best experience we can.
It would help us a lot if we could collect some anonymous analytics while you use Garden.
Expand Down
34 changes: 23 additions & 11 deletions garden-service/src/cli/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ export const GLOBAL_OPTIONS = {
help: "Enable emoji in output (defaults to true if the environment supports it).",
defaultValue: envSupportsEmoji(),
}),
"yes": new BooleanParameter({
alias: "y",
help: "Automatically approve any yes/no prompts during execution.",
defaultValue: false,
}),
}

export type GlobalOptions = typeof GLOBAL_OPTIONS
Expand Down Expand Up @@ -304,24 +309,31 @@ export class GardenCli {
await this.initFileWriters(logger, garden.projectRoot, garden.gardenDirPath)

// Init Analytics, track command if user opted-in
const analytics = await new AnalyticsHandler(garden).init()
const analytics = await new AnalyticsHandler(garden, parsedOpts).init()
analytics.trackCommand(command.getFullName())

// tslint:disable-next-line: no-floating-promises
checkForUpdates(garden.globalConfigStore, headerLog)

await checkForStaticDir()

// TODO: enforce that commands always output DeepPrimitiveMap
result = await command.action({
garden,
log,
footerLog,
headerLog,
args: parsedArgs,
opts: parsedOpts,
})

// Check if the command is protected and ask for confirmation to proceed if production flag is "true".
if (await command.isAllowedToRun(garden, log, parsedOpts)) {
// TODO: enforce that commands always output DeepPrimitiveMap

result = await command.action({
garden,
log,
footerLog,
headerLog,
args: parsedArgs,
opts: parsedOpts,
})
} else {
// The command is protected and the user decided to not continue with the exectution.
log.setState("\nCommand aborted.")
result = {}
}
await garden.close()
} catch (err) {
// Generate a basic report in case Garden.factory(...) fails and command is "get debug-info".
Expand Down
41 changes: 41 additions & 0 deletions garden-service/src/commands/base.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,9 @@ import { LogEntry } from "../logger/log-entry"
import { printFooter } from "../logger/util"
import { GlobalOptions } from "../cli/cli"
import { joi } from "../config/common"
import inquirer = require("inquirer")
import dedent = require("dedent")
import chalk from "chalk"

export interface ParameterConstructor<T> {
help: string
Expand Down Expand Up @@ -268,6 +271,8 @@ export abstract class Command<T extends Parameters = {}, U extends Parameters =
noProject: boolean = false
hidden: boolean = false

protected: boolean = false

subCommands: CommandConstructor[] = []

constructor(private parent?: Command) {
Expand Down Expand Up @@ -327,6 +332,42 @@ export abstract class Command<T extends Parameters = {}, U extends Parameters =
// can't enforce the types of `args` and `opts` automatically at the abstract class level and have to specify
// the types explicitly on the subclassed methods.
abstract async action(params: CommandParams<T, U>): Promise<CommandResult>

/**
* Called on all commands and checks if the command is protected.
* If it's a protected command, the environment is "production" and the user hasn't specified the "--yes/-y" option
* it asks for confirmation to proceed.
*
* @param {Garden} garden
* @param {LogEntry} log
* @param {GlobalOptions} opts
* @returns {Promise<Boolean>}
* @memberof Command
*/
async isAllowedToRun(garden: Garden, log: LogEntry, opts: GlobalOptions): Promise<Boolean> {
log.root.stop()
if (!opts.yes && this.protected && garden.production) {
const defaultMessage = chalk.yellow(dedent`
Warning: you are trying to run "garden ${this.getFullName()}" against a production environment ([${
garden.environmentName
}])!
Are you sure you want to continue? (run the command with the "--yes" flag to skip this check).
`)
const answer: any = await inquirer.prompt({
name: "continue",
message: defaultMessage,
type: "confirm",
default: false,
})

log.info("")

return answer.continue
}

return true
}
}

export async function handleTaskResults(
Expand Down
1 change: 1 addition & 0 deletions garden-service/src/commands/build.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ type Opts = typeof buildOptions
export class BuildCommand extends Command<Args, Opts> {
name = "build"
help = "Build your modules."
protected = true

description = dedent`
Builds all or specified modules, taking into account build dependency order.
Expand Down
1 change: 1 addition & 0 deletions garden-service/src/commands/deploy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ type Opts = typeof deployOpts
export class DeployCommand extends Command<Args, Opts> {
name = "deploy"
help = "Deploy service(s) to your environment."
protected = true

description = dedent`
Deploys all or specified services, taking into account service dependency order.
Expand Down
1 change: 1 addition & 0 deletions garden-service/src/commands/dev.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ type Opts = typeof devOpts
export class DevCommand extends Command<Args, Opts> {
name = "dev"
help = "Starts the garden development console."
protected = true

// Currently it doesn't make sense to do file watching except in the CLI
cliOnly = true
Expand Down
1 change: 1 addition & 0 deletions garden-service/src/commands/init.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ type Opts = typeof initOpts
export class InitCommand extends Command {
name = "init"
help = "Initialize system, environment or other runtime components."
protected = true

// This command is generally only used when user input is needed, which will need to happen via the CLI
cliOnly = true
Expand Down
1 change: 1 addition & 0 deletions garden-service/src/commands/test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ type Opts = typeof testOpts
export class TestCommand extends Command<Args, Opts> {
name = "test"
help = "Test all or specified modules."
protected = true

description = dedent`
Runs all or specified tests defined in the project. Also builds modules and dependencies,
Expand Down
17 changes: 12 additions & 5 deletions garden-service/src/config/project.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ import { findByName, getNames } from "../util/util"
import { ConfigurationError, ParameterError } from "../exceptions"
import { PrimitiveMap } from "./common"
import { cloneDeep, omit } from "lodash"
import { providerConfigBaseSchema, Provider, ProviderConfig } from "./provider"
import { providerConfigBaseSchema, ProviderConfig } from "./provider"
import { DEFAULT_API_VERSION } from "../constants"
import { defaultDotIgnoreFiles } from "../util/fs"
import { pathExists, readFile } from "fs-extra"
Expand Down Expand Up @@ -72,10 +72,7 @@ export const environmentConfigSchema = joi.object().keys({
export interface EnvironmentConfig extends CommonEnvironmentConfig {
name: string
varfile?: string
}

export interface Environment extends EnvironmentConfig {
providers: Provider[]
production?: boolean
}

export const environmentNameSchema = joiUserIdentifier()
Expand All @@ -84,6 +81,15 @@ export const environmentNameSchema = joiUserIdentifier()

export const environmentSchema = environmentConfigSchema.keys({
name: environmentNameSchema,
production: joi.boolean().default(false).description(dedent`
Set environment as production.
Setting this flag to \`true\` will activate the protection on the \`deploy\`, \`test\`, \`task\`, \`build\`, \`init\` and \`dev\` commands.
A protected command will ask for a user confirmation every time is run agains an environment marked as production.
Run the command with the "--yes" flag to skip the check (e.g. when running Garden in CI).
Note: This flag will affect how certain providers behave. For more details please check the documentation for the providers in use.
`),
})

const environmentsSchema = joi.alternatives(
Expand Down Expand Up @@ -438,6 +444,7 @@ export async function pickEnvironment(config: ProjectConfig, environmentName: st
return {
providers: Object.values(mergedProviders),
variables,
production: !!environmentConfig.production,
}
}

Expand Down
Loading

0 comments on commit e0bb7be

Please sign in to comment.