Skip to content

Commit

Permalink
feat: added command to locally validate terraform configuration (#449)
Browse files Browse the repository at this point in the history
<!--

Describe in detail the changes you are proposing, and the rationale.

-->

<!--

Link all GitHub issues fixed by this PR, and add references to prior
related PRs.

-->

Fixes #

### NEW FEATURES | UPGRADE NOTES | ENHANCEMENTS | BUG FIXES |
EXPERIMENTS

<!--

Write a short description of your changes. Examples:

- Adds a command to run a validation on the generated configuration.
This is useful to run locally or in the pipeline

--> 

-
  • Loading branch information
demeyerthom authored Sep 16, 2024
2 parents f27f228 + e350fb8 commit 54b9e6d
Show file tree
Hide file tree
Showing 12 changed files with 286 additions and 5 deletions.
3 changes: 3 additions & 0 deletions .changes/unreleased/Added-20240913-113045.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
kind: Added
body: Added command to validate terraform configurations
time: 2024-09-13T11:30:45.83129735+02:00
1 change: 1 addition & 0 deletions docs/src/reference/cli/mach-composer.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,5 +28,6 @@ MACH composer is a framework that you use to orchestrate and extend modern digit
* [mach-composer sites](mach-composer_sites.md) - List all sites.
* [mach-composer terraform](mach-composer_terraform.md) - Execute terraform commands directly
* [mach-composer update](mach-composer_update.md) - Update all (or a given) component.
* [mach-composer validate](mach-composer_validate.md) - Validate the generated terraform configuration.
* [mach-composer version](mach-composer_version.md) - Return version information of the mach-composer cli

40 changes: 40 additions & 0 deletions docs/src/reference/cli/mach-composer_validate.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
## mach-composer validate

Validate the generated terraform configuration.

### Synopsis

This command validates the generated terraform configuration. It will check the provided configuration file for any errors, and will run `terraform validate` on the generated configuration. This will check for any syntax errors in the generated configuration without accessing the actual infrastructure.

By default, the generated configuration is stored in the `validations` directory in the current working directory. This can be changed by providing the `--validation-path` flag.

See [the terraform validation docs](https://www.terraform.io/docs/commands/validate.html) for more information on `terraform validate`.

```
mach-composer validate [flags]
```

### Options

```
-f, --file string YAML file to parse. (default "main.yml")
-h, --help help for validate
--ignore-version Skip MACH composer version check
--output-path string Outputs path to store the generated files. (default "deployments")
-s, --site string Site to parse. If not set parse all sites.
--validation-path string Directory path to store files required for configuration validation. (default "validations")
--var-file string Use a variable file to parse the configuration with.
-w, --workers int The number of workers to use (default 1)
```

### Options inherited from parent commands

```
-q, --quiet Quiet output. This is equal to setting log levels to error and higher
-v, --verbose Verbose output. This is equal to setting log levels to debug and higher
```

### SEE ALSO

* [mach-composer](mach-composer.md) - MACH composer is an orchestration tool for modern MACH ecosystems

1 change: 1 addition & 0 deletions internal/cmd/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,4 +80,5 @@ func init() {
RootCmd.AddCommand(terraformCmd)
RootCmd.AddCommand(versionCmd)
RootCmd.AddCommand(graphCmd)
RootCmd.AddCommand(validateCmd)
}
1 change: 1 addition & 0 deletions internal/cmd/testdata/cases/.gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
**/deployments
**/validations
**/states
33 changes: 33 additions & 0 deletions internal/cmd/testdata/cases/validate/invalid/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
mach_composer:
version: 1
deployment:
type: "site-component"
plugins:
aws:
source: mach-composer/aws
version: 0.1.0

global:
cloud: "aws"
environment: test
terraform_config:
remote_state:
plugin: local
path: ./states

sites:
- identifier: test-1
aws:
account_id: "12345"
region: eu-west-1
components:
- name: component-1

components:
- name: component-1
source: ./testdata/modules/application
version: "test"
branch: main
integrations:
- aws

33 changes: 33 additions & 0 deletions internal/cmd/testdata/cases/validate/valid/main.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
mach_composer:
version: 1
plugins:
aws:
source: mach-composer/aws
version: 0.1.0

global:
cloud: ""
environment: test
terraform_config:
remote_state:
plugin: local
path: ./states

sites:
- identifier: test-1
aws:
account_id: "12345"
region: eu-west-1
components:
- name: component-1
variables:
parent_names: [ ]

components:
- name: component-1
source: ./testdata/modules/application
version: "test"
branch: main
integrations:
- aws

77 changes: 77 additions & 0 deletions internal/cmd/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package cmd

import (
"github.com/mach-composer/mach-composer-cli/internal/batcher"
"github.com/mach-composer/mach-composer-cli/internal/cli"
"github.com/mach-composer/mach-composer-cli/internal/graph"
"github.com/mach-composer/mach-composer-cli/internal/hash"
"github.com/spf13/cobra"
"os"
"path"
"path/filepath"

"github.com/mach-composer/mach-composer-cli/internal/generator"
"github.com/mach-composer/mach-composer-cli/internal/runner"
)

var validateFlags struct {
validationPath string
}

var validateCmd = &cobra.Command{
Use: "validate",
Short: "Validate the generated terraform configuration.",
Long: "This command validates the generated terraform configuration. It will check the provided configuration file " +
"for any errors, and will run `terraform validate` on the generated configuration. This will check for any " +
"syntax errors in the generated configuration without accessing the actual infrastructure.\n\n" +
"By default, the generated configuration is stored in the `validations` directory in the current " +
"working directory. This can be changed by providing the `--validation-path` flag.\n\n" +
"See [the terraform validation docs](https://www.terraform.io/docs/commands/validate.html) for more " +
"information on `terraform validate`.",
PreRun: func(cmd *cobra.Command, args []string) {
preprocessCommonFlags(cmd)
},

RunE: func(cmd *cobra.Command, args []string) error {
return validateFunc(cmd, args)
},
}

func init() {
registerCommonFlags(validateCmd)
validateCmd.Flags().StringVarP(&validateFlags.validationPath, "validation-path", "", "validations",
"Directory path to store files required for configuration validation.")

if path.IsAbs(validateFlags.validationPath) == false {
var err error
value, err := os.Getwd()
if err != nil {
cli.PrintExitError("failed to get current working directory")
}
validateFlags.validationPath = filepath.Join(value, validateFlags.validationPath)
}
}

func validateFunc(cmd *cobra.Command, _ []string) error {
cfg := loadConfig(cmd, true)
defer cfg.Close()
ctx := cmd.Context()

dg, err := graph.ToDeploymentGraph(cfg, validateFlags.validationPath)
if err != nil {
return err
}

err = generator.Write(ctx, cfg, dg, nil)
if err != nil {
return err
}

r := runner.NewGraphRunner(
batcher.NaiveBatchFunc(),
hash.Factory(cfg),
commonFlags.workers,
)

return r.TerraformValidate(ctx, dg)
}
64 changes: 64 additions & 0 deletions internal/cmd/validate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package cmd

import (
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/suite"
"os"
"os/exec"
"path"
"testing"
)

type ValidateTestSuite struct {
suite.Suite
tempDir string
}

func TestValidateTestSuite(t *testing.T) {
suite.Run(t, new(ValidateTestSuite))
}

func (s *ValidateTestSuite) SetupSuite() {
_, err := exec.LookPath("terraform")
if err != nil {
s.T().Fatal("terraform command not found")
}

tmpDir, _ := os.MkdirTemp("mach-composer", "test")
_ = os.Setenv("TF_PLUGIN_CACHE_DIR", tmpDir)
_ = os.Setenv("TF_PLUGIN_CACHE_MAY_BREAK_DEPENDENCY_LOCK_FILE", "1")

s.tempDir = tmpDir
}

func (s *ValidateTestSuite) TearDownSuite() {
_ = os.RemoveAll(s.tempDir)
}

func (s *ValidateTestSuite) TestValidateInvalid() {
pwd, _ := os.Getwd()
workdir := path.Join(pwd, "testdata/cases/validate/invalid")

cmd := RootCmd
cmd.SetArgs([]string{
"validate",
"--file", path.Join(workdir, "main.yaml"),
"--validation-path", path.Join(workdir, "validations"),
})
err := cmd.Execute()
assert.Error(s.T(), err)
}

func (s *ValidateTestSuite) TestValidateValid() {
pwd, _ := os.Getwd()
workdir := path.Join(pwd, "testdata/cases/validate/valid")

cmd := RootCmd
cmd.SetArgs([]string{
"validate",
"--file", path.Join(workdir, "main.yaml"),
"--validation-path", path.Join(workdir, "validations"),
})
err := cmd.Execute()
assert.NoError(s.T(), err)
}
20 changes: 16 additions & 4 deletions internal/runner/graph.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ func (gr *GraphRunner) TerraformApply(ctx context.Context, dg *graph.Graph, opts
if err := gr.run(ctx, dg, func(ctx context.Context, n graph.Node) (string, error) {
if !terraformIsInitialized(n.Path()) || opts.ForceInit {
log.Info().Msgf("Running terraform init for %s", n.Path())
if out, err := terraform.Init(ctx, n.Path()); err != nil {
if out, err := terraform.Init(ctx, n.Path(), true); err != nil {
return out, err
}
} else {
Expand All @@ -126,11 +126,23 @@ func (gr *GraphRunner) TerraformApply(ctx context.Context, dg *graph.Graph, opts
return nil
}

func (gr *GraphRunner) TerraformValidate(ctx context.Context, dg *graph.Graph) error {
return gr.run(ctx, dg, func(ctx context.Context, n graph.Node) (string, error) {
log.Info().Msgf("Running terraform init without backend for %s", n.Path())
if out, err := terraform.Init(ctx, n.Path(), false); err != nil {
return out, err
}

log.Info().Msgf("Running terraform validate for %s", n.Path())
return terraform.Validate(ctx, n.Path())
}, true)
}

func (gr *GraphRunner) TerraformPlan(ctx context.Context, dg *graph.Graph, opts *PlanOptions) error {
if err := gr.run(ctx, dg, func(ctx context.Context, n graph.Node) (string, error) {
if !terraformIsInitialized(n.Path()) || opts.ForceInit {
log.Info().Msgf("Running terraform init for %s", n.Path())
if out, err := terraform.Init(ctx, n.Path()); err != nil {
if out, err := terraform.Init(ctx, n.Path(), true); err != nil {
return out, err
}
} else {
Expand Down Expand Up @@ -173,7 +185,7 @@ func (gr *GraphRunner) TerraformShow(ctx context.Context, dg *graph.Graph, opts
if err := gr.run(ctx, dg, func(ctx context.Context, n graph.Node) (string, error) {
if !terraformIsInitialized(n.Path()) || opts.ForceInit {
log.Info().Msgf("Running terraform init for %s", n.Path())
if out, err := terraform.Init(ctx, n.Path()); err != nil {
if out, err := terraform.Init(ctx, n.Path(), true); err != nil {
return out, err
}
} else {
Expand All @@ -190,7 +202,7 @@ func (gr *GraphRunner) TerraformShow(ctx context.Context, dg *graph.Graph, opts

func (gr *GraphRunner) TerraformInit(ctx context.Context, dg *graph.Graph) error {
if err := gr.run(ctx, dg, func(ctx context.Context, n graph.Node) (string, error) {
return terraform.Init(ctx, n.Path())
return terraform.Init(ctx, n.Path(), true)
}, true); err != nil {
return err
}
Expand Down
6 changes: 5 additions & 1 deletion internal/terraform/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@ import (
"github.com/mach-composer/mach-composer-cli/internal/utils"
)

func Init(ctx context.Context, path string) (string, error) {
func Init(ctx context.Context, path string, withBackend bool) (string, error) {
args := []string{"init"}

if !withBackend {
args = append(args, "-backend=false")
}

return utils.RunTerraform(ctx, path, false, args...)
}
12 changes: 12 additions & 0 deletions internal/terraform/validate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package terraform

import (
"context"
"github.com/mach-composer/mach-composer-cli/internal/utils"
)

func Validate(ctx context.Context, path string) (string, error) {
cmd := []string{"validate"}

return utils.RunTerraform(ctx, path, false, cmd...)
}

0 comments on commit 54b9e6d

Please sign in to comment.