mirror of
https://github.com/3bbbeau/tfvars-atlantis-config.git
synced 2024-11-24 06:50:51 +00:00
265 lines
8.1 KiB
Go
265 lines
8.1 KiB
Go
package cmd
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"slices"
|
|
"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,
|
|
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().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,
|
|
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)
|
|
|
|
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))
|
|
|
|
// 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)) {
|
|
parent := subpath
|
|
// 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),
|
|
)
|
|
parent = path
|
|
}
|
|
|
|
relParent, err := filepath.Rel(flags.Root, filepath.Dir(parent))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
logger.Sugar().Debugf("component parent is %s", relParent)
|
|
found := repocfg.Component{
|
|
Path: relParent,
|
|
}
|
|
|
|
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)
|
|
|
|
// Check if the component already exists in the slice
|
|
// If it does, ensure the var file is not a duplicate and append it.
|
|
exists := false
|
|
for idx, component := range discovered {
|
|
if component.Path == found.Path {
|
|
exists = true
|
|
for _, varFile := range found.VarFiles {
|
|
if !slices.Contains(component.VarFiles, varFile) {
|
|
discovered[idx].VarFiles = append(discovered[idx].VarFiles, varFile)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// If the component does not exist in the slice, then
|
|
// it can be treated as new component.
|
|
if !exists {
|
|
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
|
|
}
|