Skip to content

Commit

Permalink
feat: Adding Strict Package (#3424)
Browse files Browse the repository at this point in the history
* WIP - feat: Adding strict package

* feat: Adding strict package

* feat: Removing default on/off control. It just complicates things.

* feat: Documenting Strict Mode

* fix: Addressing markdownlint errors

* fix: Addressing lints

* fix: Blindly do what Levko says to do

* fix: Incorporate review feedback

* fix: Fixing docs for redesigned implementation

* fix: Fixing bug in implementation.

* fix: Cleaning up strict implementation

* fix: Markdown linting

* fix: Revert change to README

* fix: Adding integration tests
  • Loading branch information
yhakbar authored Oct 16, 2024
1 parent a36f34b commit 2cab98f
Show file tree
Hide file tree
Showing 9 changed files with 454 additions and 17 deletions.
25 changes: 25 additions & 0 deletions cli/commands/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/gruntwork-io/go-commons/collections"
"github.com/gruntwork-io/terragrunt/internal/errors"
"github.com/gruntwork-io/terragrunt/internal/strict"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/pkg/cli"
"github.com/gruntwork-io/terragrunt/pkg/log"
Expand Down Expand Up @@ -143,6 +144,14 @@ const (
TerragruntForwardTFStdoutFlagName = "terragrunt-forward-tf-stdout"
TerragruntForwardTFStdoutEnvName = "TERRAGRUNT_FORWARD_TF_STDOUT"

// Strict Mode related flags/envs

TerragruntStrictModeFlagName = "strict-mode"
TerragruntStrictModeEnvName = "TERRAGRUNT_STRICT_MODE"

TerragruntStrictControlFlagName = "strict-control"
TerragruntStrictControlEnvName = "TERRAGRUNT_STRICT_CONTROL"

// Terragrunt Provider Cache related flags/envs

TerragruntProviderCacheFlagName = "terragrunt-provider-cache"
Expand Down Expand Up @@ -456,6 +465,22 @@ func NewGlobalFlags(opts *options.TerragruntOptions) cli.Flags {
Destination: &opts.DisableCommandValidation,
Usage: "When this flag is set, Terragrunt will not validate the terraform command.",
},
// Strict Mode flags
&cli.BoolFlag{
Name: TerragruntStrictModeFlagName,
EnvVar: TerragruntStrictModeEnvName,
Destination: &opts.StrictMode,
Usage: "Enables strict mode for Terragrunt. For more information, see https://terragrunt.gruntwork.io/docs/reference/strict-mode .",
},
&cli.SliceFlag[string]{
Name: TerragruntStrictControlFlagName,
EnvVar: TerragruntStrictControlEnvName,
Destination: &opts.StrictControls,
Usage: "Enables specific strict controls. For a list of available controls, see https://terragrunt.gruntwork.io/docs/reference/strict-mode .",
Action: func(ctx *cli.Context, val []string) error {
return strict.StrictControls.ValidateControlNames(val)
},
},
// Terragrunt Provider Cache flags
&cli.BoolFlag{
Name: TerragruntProviderCacheFlagName,
Expand Down
28 changes: 21 additions & 7 deletions cli/deprecated_commands.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

runall "github.com/gruntwork-io/terragrunt/cli/commands/run-all"
"github.com/gruntwork-io/terragrunt/internal/strict"
"github.com/gruntwork-io/terragrunt/options"
"github.com/gruntwork-io/terragrunt/pkg/cli"
"github.com/gruntwork-io/terragrunt/terraform"
Expand Down Expand Up @@ -47,13 +48,26 @@ func replaceDeprecatedCommandFunc(terragruntCommandName, terraformCommandName st
deprecatedCommandName := ctx.Command.Name
newCommandFriendly := fmt.Sprintf("terragrunt %s %s", terragruntCommandName, strings.Join(args, " "))

opts.Logger.Warnf(
"'%s' is deprecated. Running '%s' instead. Please update your workflows to use '%s', as '%s' may be removed in the future!\n",
deprecatedCommandName,
newCommandFriendly,
newCommandFriendly,
deprecatedCommandName,
)
control, ok := strict.GetStrictControl(deprecatedCommandName)
if ok {
warning, err := control.Evaluate(opts)
if err != nil {
return err //nolint:wrapcheck
}

opts.Logger.Warn(warning)

} else { //nolint:wsl,whitespace
// This else clause should never be hit, as all the commands above are accounted for.
// This might be missed accidentally in the future, so we'll keep it for safety.
opts.Logger.Warnf(
"'%s' is deprecated. Running '%s' instead. Please update your workflows to use '%s', as '%s' may be removed in the future!\n", //nolint:lll
deprecatedCommandName,
newCommandFriendly,
newCommandFriendly,
deprecatedCommandName,
)
}

err := command.Run(ctx, args)

Expand Down
116 changes: 116 additions & 0 deletions docs/_docs/04_reference/strict-mode.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
---
layout: collection-browser-doc
title: Strict Mode
category: reference
categories_url: reference
excerpt: >-
Opt-in to strict mode to avoid deprecated features and ensure your code is future-proof.
tags: ["CLI"]
order: 404
nav_title: Documentation
nav_title_link: /docs/
---

Terragrunt supports operating in a mode referred to as "Strict Mode".

Strict Mode is a set of controls that can be enabled to ensure that your Terragrunt usage is future-proof
by making deprecated features throw errors instead of warnings. This can be useful when you want to ensure
that your Terragrunt code is up-to-date with the latest conventions to avoid breaking changes in
future versions of Terragrunt.

Whenever possible, Terragrunt will initially provide you with a warning when you use a deprecated feature, without throwing an error.
However, in Strict Mode, these warnings will be converted to errors, which will cause the Terragrunt command to fail.

## Controlling Strict Mode

The simplest way to enable strict mode is to set the `TERRAGRUNT_STRICT_MODE` environment variable to `true`.

This will enable strict mode for all Terragrunt commands, for all strict mode controls.

```bash
$ terragrunt plan-all
15:26:08.585 WARN The `plan-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all plan` instead.
```

```bash
$ TERRAGRUNT_STRICT_MODE='true' tg plan-all
15:26:23.685 ERROR The `plan-all` command is no longer supported. Use `terragrunt run-all plan` instead.
```

Instead of setting this environment variable, you can also enable strict mode for specific controls by setting the `TERRAGRUNT_STRICT_CONTROL`
environment variable to a value that's specific to a particular strict control.
This can allow you to gradually increase your confidence in the future compatibility of your Terragrunt usage.

```bash
$ TERRAGRUNT_STRICT_CONTROL='apply-all' terragrunt plan-all
15:26:08.585 WARN The `plan-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all plan` instead.
```

```bash
$ TERRAGRUNT_STRICT_CONTROL='plan-all' terragrunt plan-all
15:26:23.685 ERROR The `plan-all` command is no longer supported. Use `terragrunt run-all plan` instead.
```

You can also enable multiple strict controls at once with a comma delimited list.

```bash
$ TERRAGRUNT_STRICT_CONTROL='plan-all,apply-all' bash -c 'terragrunt plan-all; terragrunt apply-all'
15:26:46.521 ERROR The `plan-all` command is no longer supported. Use `terragrunt run-all plan` instead.
15:26:46.521 ERROR Unable to determine underlying exit code, so Terragrunt will exit with error code 1
15:26:46.564 ERROR The `apply-all` command is no longer supported. Use `terragrunt run-all apply` instead.
15:26:46.564 ERROR Unable to determine underlying exit code, so Terragrunt will exit with error code 1
```

## Strict Mode Controls

The following strict mode controls are available:

- [spin-up](#spin-up)
- [tear-down](#tear-down)
- [plan-all](#plan-all)
- [apply-all](#apply-all)
- [destroy-all](#destroy-all)
- [output-all](#output-all)
- [validate-all](#validate-all)

### spin-up

Throw an error when using the `spin-up` command.

**Reason**: The `spin-up` command is deprecated and will be removed in a future version. Use `terragrunt run-all apply` instead.

### tear-down

Throw an error when using the `tear-down` command.

**Reason**: The `tear-down` command is deprecated and will be removed in a future version. Use `terragrunt run-all destroy` instead.

### plan-all

Throw an error when using the `plan-all` command.

**Reason**: The `plan-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all plan` instead.

### apply-all

Throw an error when using the `apply-all` command.

**Reason**: The `apply-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all apply` instead.

### destroy-all

Throw an error when using the `destroy-all` command.

**Reason**: The `destroy-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all destroy` instead.

### output-all

Throw an error when using the `output-all` command.

**Reason**: The `output-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all output` instead.

### validate-all

Throw an error when using the `validate-all` command.

**Reason**: The `validate-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all validate` instead.
121 changes: 121 additions & 0 deletions internal/strict/strict.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
// Package strict provides utilities used by Terragrunt to support a "strict" mode.
// By default strict mode is disabled, but when enabled, any breaking changes
// to Terragrunt behavior that is not backwards compatible will result in an error.
//
// Note that any behavior outlined here should be documented in /docs/_docs/04_reference/strict-mode.md
//
// That is how users will know what to expect when they enable strict mode, and how to customize it.
package strict

import (
"errors"
"strings"

"github.com/gruntwork-io/terragrunt/options"
)

// Control represents a control that can be enabled or disabled in strict mode.
// When the control is enabled, Terragrunt will behave in a way that is not backwards compatible.
type Control struct {
// Error is the error that will be returned when the control is enabled.
Error error
// Warning is a warning that will be logged when the control is not enabled.
Warning string
}

const (
// SpinUp is the control that prevents the deprecated `spin-up` command from being used.
SpinUp = "spin-up"
// TearDown is the control that prevents the deprecated `tear-down` command from being used.
TearDown = "tear-down"
// PlanAll is the control that prevents the deprecated `plan-all` command from being used.
PlanAll = "plan-all"
// ApplyAll is the control that prevents the deprecated `apply-all` command from being used.
ApplyAll = "apply-all"
// DestroyAll is the control that prevents the deprecated `destroy-all` command from being used.
DestroyAll = "destroy-all"
// OutputAll is the control that prevents the deprecated `output-all` command from being used.
OutputAll = "output-all"
// ValidateAll is the control that prevents the deprecated `validate-all` command from being used.
ValidateAll = "validate-all"
)

// GetStrictControl returns the strict control with the given name.
func GetStrictControl(name string) (Control, bool) {
control, ok := StrictControls[name]

return control, ok
}

// Evaluate returns a warning if the control is not enabled, and an error if the control is enabled.
func (control Control) Evaluate(opts *options.TerragruntOptions) (string, error) {
if opts.StrictMode {
return "", control.Error
}

for _, controlName := range opts.StrictControls {
strictControl, ok := StrictControls[controlName]
if !ok {
// This should never happen, but if it does, it's a bug in Terragrunt.
// The slice of StrictControls should be validated before they're used.
return "", errors.New("Invalid strict control: " + controlName)
}

if strictControl == control {
return "", control.Error
}
}

return control.Warning, nil
}

type Controls map[string]Control

//nolint:lll,gochecknoglobals,stylecheck
var StrictControls = Controls{
SpinUp: {
Error: errors.New("The `spin-up` command is no longer supported. Use `terragrunt run-all apply` instead."),
Warning: "The `spin-up` command is deprecated and will be removed in a future version. Use `terragrunt run-all apply` instead.",
},
TearDown: {
Error: errors.New("The `tear-down` command is no longer supported. Use `terragrunt run-all destroy` instead."),
Warning: "The `tear-down` command is deprecated and will be removed in a future version. Use `terragrunt run-all destroy` instead.",
},
PlanAll: {
Error: errors.New("The `plan-all` command is no longer supported. Use `terragrunt run-all plan` instead."),
Warning: "The `plan-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all plan` instead.",
},
ApplyAll: {
Error: errors.New("The `apply-all` command is no longer supported. Use `terragrunt run-all apply` instead."),
Warning: "The `apply-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all apply` instead.",
},
DestroyAll: {
Error: errors.New("The `destroy-all` command is no longer supported. Use `terragrunt run-all destroy` instead."),
Warning: "The `destroy-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all destroy` instead.",
},
OutputAll: {
Error: errors.New("The `output-all` command is no longer supported. Use `terragrunt run-all output` instead."),
Warning: "The `output-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all output` instead.",
},
ValidateAll: {
Error: errors.New("The `validate-all` command is no longer supported. Use `terragrunt run-all validate` instead."),
Warning: "The `validate-all` command is deprecated and will be removed in a future version. Use `terragrunt run-all validate` instead.",
},
}

func (controls Controls) ValidateControlNames(strictControlNames []string) error {
invalidControls := []string{}

for _, controlName := range strictControlNames {
_, ok := controls[controlName]
if !ok {
invalidControls = append(invalidControls, controlName)
}
}

if len(invalidControls) > 0 {
return errors.New("Invalid strict controls: " + strings.Join(invalidControls, ", "))
}

return nil
}
Loading

0 comments on commit 2cab98f

Please sign in to comment.