Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Terraform backend #39

Merged
merged 1 commit into from
Feb 28, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ Then you can go ahead and run `aztfy`:
aztfy [option] <resource group name>

-b Batch mode (i.e. Non-interactive mode)
-backend-config value
The Terraform backend config
-backend-type string
The Terraform backend used to store the state (default "local")
-f Whether to overwrite the out dir if it is not empty, use with caution
-k Whether continue on import error (batch mode only)
-m string
Expand Down
20 changes: 11 additions & 9 deletions internal/config/config.go
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
package config

type Config struct {
ResourceGroupName string // specified via CLI
Logfile string `env:"AZTFY_LOGFILE" default:""`
Debug bool `env:"AZTFY_DEBUG" default:"false"`
MockClient bool `env:"AZTFY_MOCK_CLIENT" default:"false"`
OutputDir string // specified via CLI option
ResourceMappingFile string // specified via CLI option
ResourceNamePattern string // specified via CLI option
Overwrite bool // specified via CLI option
BatchMode bool // specified via CLI option
ResourceGroupName string // specified via CLI
Logfile string `env:"AZTFY_LOGFILE" default:""`
Debug bool `env:"AZTFY_DEBUG" default:"false"`
MockClient bool `env:"AZTFY_MOCK_CLIENT" default:"false"`
OutputDir string // specified via CLI option
ResourceMappingFile string // specified via CLI option
ResourceNamePattern string // specified via CLI option
Overwrite bool // specified via CLI option
BatchMode bool // specified via CLI option
BackendType string // specified via CLI option
BackendConfig []string // specified via CLI option
}
4 changes: 0 additions & 4 deletions internal/meta/config_info.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package meta

import (
"bytes"
"io"

"github.com/hashicorp/hcl/v2/hclwrite"
Expand All @@ -16,8 +15,5 @@ type ConfigInfo struct {

func (cfg ConfigInfo) DumpHCL(w io.Writer) (int, error) {
out := hclwrite.Format(cfg.hcl.Bytes())
// Hack: removing the leading warning comments before each resource config,
// which is generated via "terraform add".
out = out[bytes.Index(out, []byte(`resource "`)):]
return w.Write(out)
}
37 changes: 33 additions & 4 deletions internal/meta/meta_impl.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,9 @@ type MetaImpl struct {

resourceNamePrefix string
resourceNameSuffix string

backendType string
backendConfig []string
}

func newMetaImpl(cfg config.Config) (Meta, error) {
Expand Down Expand Up @@ -127,6 +130,8 @@ func newMetaImpl(cfg config.Config) (Meta, error) {
outdir: outdir,
auth: auth,
resourceMapping: m,
backendType: cfg.BackendType,
backendConfig: cfg.BackendConfig,
}

if pos := strings.LastIndex(cfg.ResourceNamePattern, "*"); pos != -1 {
Expand Down Expand Up @@ -221,6 +226,7 @@ func (meta MetaImpl) Import(item *ImportItem) {
item.ImportError = fmt.Errorf("generating resource template for %s: %w", item.TFAddr, err)
return
}
tpl = meta.cleanupTerraformAdd(tpl)
if err := os.WriteFile(cfgFile, []byte(tpl), 0644); err != nil {
item.ImportError = fmt.Errorf("generating resource template file: %w", err)
return
Expand Down Expand Up @@ -266,8 +272,9 @@ func (meta MetaImpl) ExportResourceMapping(l ImportList) error {
return nil
}

func providerConfig() string {
func (meta *MetaImpl) providerConfig() string {
return fmt.Sprintf(`terraform {
backend %q {}
required_providers {
azurerm = {
source = "hashicorp/azurerm"
Expand All @@ -279,18 +286,22 @@ func providerConfig() string {
provider "azurerm" {
features {}
}
`, schema.ProviderVersion)
`, meta.backendType, schema.ProviderVersion)
}

func (meta *MetaImpl) initProvider(ctx context.Context) error {
cfgFile := filepath.Join(meta.outdir, "provider.tf")

// Always use the latest provider version here, as this is a one shot tool, which should guarantees to work with the latest version.
if err := os.WriteFile(cfgFile, []byte(providerConfig()), 0644); err != nil {
if err := os.WriteFile(cfgFile, []byte(meta.providerConfig()), 0644); err != nil {
return fmt.Errorf("error creating provider config: %w", err)
}

if err := meta.tf.Init(ctx); err != nil {
var opts []tfexec.InitOption
for _, opt := range meta.backendConfig {
opts = append(opts, tfexec.BackendConfig(opt))
}
if err := meta.tf.Init(ctx, opts...); err != nil {
return fmt.Errorf("error running terraform init: %s", err)
}

Expand Down Expand Up @@ -340,6 +351,7 @@ func (meta MetaImpl) stateToConfig(ctx context.Context, list ImportList) (Config
if err != nil {
return nil, fmt.Errorf("converting terraform state to config for resource %s: %w", item.TFAddr, err)
}
tpl = meta.cleanupTerraformAdd(tpl)
f, diag := hclwrite.ParseConfig([]byte(tpl), "", hcl.InitialPos)
if diag.HasErrors() {
return nil, fmt.Errorf("parsing the HCL generated by \"terraform add\" of %s: %s", item.TFAddr, diag.Error())
Expand Down Expand Up @@ -434,6 +446,23 @@ func (meta MetaImpl) generateConfig(cfgs ConfigInfos) error {
return nil
}

func (meta MetaImpl) cleanupTerraformAdd(tpl string) string {
segs := strings.Split(tpl, "\n")
// Removing:
// - preceding/trailing state lock related log when TF backend is used.
// - preceding warning comments.
for len(segs) != 0 && (strings.HasPrefix(segs[0], "Acquiring state lock.") || strings.HasPrefix(segs[0], "#")) {
segs = segs[1:]
}

last := len(segs) - 1
for last != -1 && (segs[last] == "" || strings.HasPrefix(segs[last], "Releasing state lock.")) {
segs = segs[:last]
last = len(segs) - 1
}
return strings.Join(segs, "\n")
}

func removeEverythingUnder(path string) error {
dir, err := os.Open(path)
if err != nil {
Expand Down
20 changes: 20 additions & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ var (
flagBatchMode *bool
flagPattern *string
flagOverwrite *bool
flagBackendType *string
)

func init() {
Expand All @@ -33,6 +34,7 @@ func init() {
flagBatchMode = flag.Bool("b", false, "Batch mode (i.e. Non-interactive mode)")
flagPattern = flag.String("p", "res-", `The pattern of the resource name. The resource name is generated by taking the pattern and adding an auto-incremental integer to the end. If pattern includes a "*", the auto-incremental integer replaces the last "*"`)
flagOverwrite = flag.Bool("f", false, "Whether to overwrite the out dir if it is not empty, use with caution")
flagBackendType = flag.String("backend-type", "local", "The Terraform backend used to store the state")
}

const usage = `aztfy [option] <resource group name>
Expand All @@ -43,11 +45,27 @@ func fatal(err error) {
os.Exit(1)
}

type strSliceFlag struct {
values *[]string
}

func (o *strSliceFlag) String() string { return "" }
func (o *strSliceFlag) Set(val string) error {
*o.values = append(*o.values, val)
return nil
}

func main() {
flag.Usage = func() {
fmt.Fprintf(flag.CommandLine.Output(), "%s\n", usage)
flag.PrintDefaults()
}

var backendConfig []string
flag.Var(&strSliceFlag{
values: &backendConfig,
}, "backend-config", "The Terraform backend config")

flag.Parse()

if *flagVersion {
Expand Down Expand Up @@ -80,6 +98,8 @@ func main() {
cfg.OutputDir = *flagOutputDir
cfg.Overwrite = *flagOverwrite
cfg.BatchMode = *flagBatchMode
cfg.BackendType = *flagBackendType
cfg.BackendConfig = backendConfig

if cfg.BatchMode {
if err := batchImport(cfg, *flagContinue); err != nil {
Expand Down