mirror of
https://github.com/3bbbeau/tfvars-atlantis-config.git
synced 2025-12-05 22:01:43 +00:00
v0.0.1
This commit is contained in:
249
cmd/generate.go
Normal file
249
cmd/generate.go
Normal file
@@ -0,0 +1,249 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/3bbbeau/tfvars-atlantis-config/logger"
|
||||
"github.com/3bbbeau/tfvars-atlantis-config/repocfg"
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
"gopkg.in/yaml.v2"
|
||||
)
|
||||
|
||||
// Flags represents the flags for the `generate` command
|
||||
type Flags struct {
|
||||
AutoMerge bool
|
||||
AutoPlan bool
|
||||
DefaultTerraformVersion string
|
||||
MultiEnv bool
|
||||
Output string
|
||||
Parallel bool
|
||||
Root string
|
||||
UseWorkspaces bool
|
||||
}
|
||||
|
||||
// NewFlags returns a default Flags struct
|
||||
func NewFlags() (*Flags, error) {
|
||||
pwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("default flags for git root: %w", err)
|
||||
}
|
||||
|
||||
return &Flags{
|
||||
AutoMerge: false,
|
||||
AutoPlan: false,
|
||||
DefaultTerraformVersion: "",
|
||||
Root: pwd,
|
||||
MultiEnv: false,
|
||||
Output: "",
|
||||
Parallel: false,
|
||||
UseWorkspaces: false,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// AddFlags registers flags for the `generate` command
|
||||
func (flags *Flags) AddFlags(cmd *cobra.Command) {
|
||||
cmd.Flags().BoolVar(&flags.AutoPlan, "autoplan", flags.AutoPlan, "Enable auto plan. Default is disabled")
|
||||
cmd.Flags().BoolVar(&flags.AutoMerge, "automerge", flags.AutoMerge, "Enable auto merge. Default is disabled")
|
||||
cmd.Flags().BoolVar(&flags.Parallel, "parallel", flags.Parallel, "Enables plans and applys to happen in parallel. Default is disabled")
|
||||
cmd.Flags().StringVar(&flags.Output, "output", flags.Output, "Path of the file where configuration will be generated. Default is stdout")
|
||||
cmd.Flags().StringVar(&flags.Root, "root", flags.Root, "Path to the root directory of the git repo you want to build config for. Default is current dir")
|
||||
cmd.Flags().BoolVar(&flags.MultiEnv, "multienv", flags.MultiEnv, "Enable injection of environment specific environment variables to each workflow. Default is disabled")
|
||||
cmd.Flags().StringVar(&flags.DefaultTerraformVersion, "terraform-version", flags.DefaultTerraformVersion, "Default terraform version to run for Atlantis. Default is determined by the Terraform version constraints.")
|
||||
cmd.Flags().BoolVar(&flags.UseWorkspaces, "use-workspaces", flags.UseWorkspaces, "Use workspaces for projects. Default is disabled")
|
||||
|
||||
cmd.Flags().VisitAll(func(f *pflag.Flag) {
|
||||
logger.FromContext(cmd.Context()).Sugar().Debugf("Set flag: %s = %v", f.Name, f.Value)
|
||||
})
|
||||
}
|
||||
|
||||
// toOptions converts the flags provided for usage to Options within the repocfg package
|
||||
func (flags *Flags) toOptions() repocfg.Options {
|
||||
return repocfg.Options{
|
||||
Automerge: flags.AutoMerge,
|
||||
Autoplan: flags.AutoPlan,
|
||||
DefaultTerraformVersion: flags.DefaultTerraformVersion,
|
||||
MultiEnv: flags.MultiEnv,
|
||||
Parallel: flags.Parallel,
|
||||
UseWorkspaces: flags.UseWorkspaces,
|
||||
}
|
||||
}
|
||||
|
||||
// NewGenerateCmd creates a new `generate` command, while applying all flags
|
||||
// with their defaults overlayed by the flags passed in by the caller.
|
||||
func NewGenerateCmd() (*cobra.Command, error) {
|
||||
flags, err := NewFlags()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("new flags: %w", err)
|
||||
}
|
||||
|
||||
cmd := &cobra.Command{
|
||||
Use: "generate",
|
||||
Short: "Makes Atlantis config for tfvar backed Terraform projects",
|
||||
Long: "Logs Yaml representing Atlantis config to stderr",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
err := generate(cmd, flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
flags.AddFlags(cmd)
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// generate is used to generate an Atlantis config, called from the `generate`
|
||||
// command, creating an Atlantis configuration written to flags.OutputPath
|
||||
func generate(cmd *cobra.Command, flags *Flags) error {
|
||||
logger := logger.FromContext(cmd.Context())
|
||||
|
||||
tree, err := discover(cmd.Context(), flags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
opts := flags.toOptions()
|
||||
cfg, err := repocfg.NewRepoCfg(tree, opts)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
cfgBytes, err := yaml.Marshal(&cfg)
|
||||
if err != nil {
|
||||
return fmt.Errorf("repocfg: %w", err)
|
||||
}
|
||||
|
||||
switch flags.Output {
|
||||
case "":
|
||||
fmt.Println(string(cfgBytes))
|
||||
default:
|
||||
logger.Sugar().Debugf("writing config to %s", flags.Output)
|
||||
err := os.WriteFile(flags.Output, cfgBytes, 0o644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const (
|
||||
TF_EXT = ".tf"
|
||||
TFVARS_EXT = ".tfvars"
|
||||
TFVARS_JSON_EXT = ".tfvars.json"
|
||||
)
|
||||
|
||||
// discover walks the directory tree and creates a slice of Terraform components
|
||||
// and their dependencies based on the .tfvars files in the directory and
|
||||
// subdirectories.
|
||||
//
|
||||
// However, if nested directories contain a .tf file, the .tfvars files in the
|
||||
// subdirectories are ignored, as they are assumed to be part of a different
|
||||
// parent node (Terraform component).
|
||||
//
|
||||
// For example, if the function is invoked at the path defined by the `root` flag:
|
||||
//
|
||||
// .
|
||||
// ├── components
|
||||
// │ ├── component1
|
||||
// │ │ ├── main.tf
|
||||
// │ │ └── dev.tfvars
|
||||
// │ └── component2
|
||||
// │ ├── extraVars
|
||||
// │ │ └── stg.tfvars
|
||||
// │ ├── main.tf
|
||||
// │ └── dev.tfvars
|
||||
//
|
||||
// Then, the resulting slice of components would look like:
|
||||
//
|
||||
// Path: "components/component1"
|
||||
// VarFiles: "components/component1/dev.tfvars"
|
||||
//
|
||||
// Path: "components/component2"
|
||||
// VarFiles: []string{"components/component2/dev.tfvars", "components/component2/extraVars/stg.tfvars"}
|
||||
func discover(ctx context.Context, flags *Flags) ([]repocfg.Component, error) {
|
||||
logger := logger.FromContext(ctx)
|
||||
|
||||
var discovered []repocfg.Component
|
||||
|
||||
// Walk the directory tree from the root
|
||||
err := filepath.Walk(flags.Root, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Each parent should be a path containing a Terraform component
|
||||
if !info.IsDir() && strings.HasSuffix(info.Name(), TF_EXT) {
|
||||
logger.Debug(fmt.Sprintf("found .tf file: %s", path))
|
||||
|
||||
// Add the directory of the Terraform component as the parent
|
||||
parent, err := filepath.Rel(flags.Root, filepath.Dir(path))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Each edge should be a path containing Terraform variables files
|
||||
// relative to the Terraform component
|
||||
err = filepath.Walk(filepath.Dir(path), func(subpath string, subinfo os.FileInfo, suberr error) error {
|
||||
if suberr != nil {
|
||||
return suberr
|
||||
}
|
||||
|
||||
// Filter out Terraform variable files
|
||||
if !subinfo.IsDir() && (strings.HasSuffix(subinfo.Name(), TFVARS_EXT) || strings.HasSuffix(subinfo.Name(), TFVARS_JSON_EXT)) {
|
||||
// Ignore nested Terraform variable files that might belong to nested components
|
||||
if filepath.Dir(subpath) != filepath.Dir(path) && tfExistsInDir(filepath.Dir(subpath)) {
|
||||
logger.Sugar().Debugf(`ignoring nested %s: %s
|
||||
which belongs to the component at: %s
|
||||
not: %s`, filepath.Ext(subpath), subpath, filepath.Dir(subpath), filepath.Dir(path),
|
||||
)
|
||||
|
||||
return filepath.SkipDir
|
||||
}
|
||||
|
||||
logger.Sugar().Debugf("component parent is %s", parent)
|
||||
found := repocfg.Component{
|
||||
Path: parent,
|
||||
}
|
||||
|
||||
logger.Sugar().Debugf("found %s file: %s", filepath.Ext(subpath), subpath)
|
||||
child, err := filepath.Rel(flags.Root, subpath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// VarFile is a member of this component
|
||||
logger.Sugar().Debugf("component %s has var file %s", parent, child)
|
||||
found.VarFiles = append(found.VarFiles, child)
|
||||
|
||||
discovered = append(discovered, found)
|
||||
}
|
||||
return err
|
||||
})
|
||||
return filepath.SkipDir
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return discovered, nil
|
||||
}
|
||||
|
||||
// tfExistsInDir returns true if a directory contains a Terraform file
|
||||
func tfExistsInDir(dir string) bool {
|
||||
files, err := filepath.Glob(filepath.Join(dir, "*"+TF_EXT))
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return len(files) > 0
|
||||
}
|
||||
56
cmd/root.go
Normal file
56
cmd/root.go
Normal file
@@ -0,0 +1,56 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/3bbbeau/tfvars-atlantis-config/logger"
|
||||
"github.com/spf13/cobra"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// New returns the new root command for this utility.
|
||||
//
|
||||
// It initializes the root command and adds all subcommands.
|
||||
func New() (*cobra.Command, error) {
|
||||
cmd := &cobra.Command{
|
||||
Use: "tfvars-atlantis-config",
|
||||
Short: "Generates Atlantis Config for Terraform projects",
|
||||
Long: `tfvars-atlantis-config is a utility that generates Atlantis configurations for Terraform projects
|
||||
that use tfvars files per environment.`,
|
||||
PersistentPreRun: func(cmd *cobra.Command, args []string) {
|
||||
setLogger(cmd)
|
||||
},
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
err := cmd.Help()
|
||||
if err != nil {
|
||||
logger.FromContext(cmd.Context()).Error("help cmd", zap.Error(err))
|
||||
}
|
||||
},
|
||||
}
|
||||
cmd.PersistentFlags().Bool("debug", false, "Enable debug mode")
|
||||
|
||||
cmd.AddCommand(NewVersionCmd())
|
||||
gCmd, err := NewGenerateCmd()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("creating generate command: %w", err)
|
||||
}
|
||||
cmd.AddCommand(gCmd)
|
||||
|
||||
return cmd, nil
|
||||
}
|
||||
|
||||
// setLogger sets the logger for the command's lifespan, based on the debug flag.
|
||||
func setLogger(cmd *cobra.Command) {
|
||||
ctx := cmd.Context()
|
||||
|
||||
debug, err := cmd.Flags().GetBool("debug")
|
||||
if err != nil || !debug {
|
||||
z := zap.Must(zap.NewProductionConfig().Build())
|
||||
ctx = logger.WithContext(ctx, z)
|
||||
} else {
|
||||
z := zap.Must(zap.NewDevelopmentConfig().Build())
|
||||
ctx = logger.WithContext(ctx, z)
|
||||
}
|
||||
|
||||
cmd.SetContext(ctx)
|
||||
}
|
||||
22
cmd/version.go
Normal file
22
cmd/version.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
// Build-time version string, overriden by ldflags.
|
||||
var v string = "devel"
|
||||
|
||||
// NewVersionCmd returns the command for retrieving the version of this utility.
|
||||
func NewVersionCmd() *cobra.Command {
|
||||
cmd := &cobra.Command{
|
||||
Use: "version",
|
||||
Short: "Version of terragrunt-atlantis-config",
|
||||
Long: "Version of terragrunt-atlantis-config",
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
cmd.Println(v)
|
||||
},
|
||||
}
|
||||
|
||||
return cmd
|
||||
}
|
||||
24
cmd/version_test.go
Normal file
24
cmd/version_test.go
Normal file
@@ -0,0 +1,24 @@
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Tests the NewVersionCmd function returns the version of the utility.
|
||||
func Test_NewVersionCmd(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
got := new(bytes.Buffer)
|
||||
cmd, _ := New()
|
||||
cmd.SetArgs([]string{"version"})
|
||||
cmd.SetOutput(got)
|
||||
cmd.Execute() // nolint:errcheck
|
||||
want := "devel\n"
|
||||
|
||||
if got.String() != want {
|
||||
t.Errorf(`NewVersionCmd()
|
||||
got %s
|
||||
want %v`, got.String(), want)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user