From 55d73b66ed3e1de392e833dfc3458bfa5e6c026d Mon Sep 17 00:00:00 2001 From: magodo Date: Wed, 8 Sep 2021 17:54:58 +0800 Subject: [PATCH] users can retype the mappings on import error --- go.mod | 1 + go.sum | 7 ++++ internal/meta.go | 98 ++++++++++++++++++++++++++++++++++++------------ internal/run.go | 35 +++++++++++++---- main.go | 2 +- 5 files changed, 112 insertions(+), 31 deletions(-) diff --git a/go.mod b/go.mod index d8a3f79..031b51f 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.16 require ( github.com/Azure/azure-sdk-for-go v56.1.0+incompatible github.com/Azure/go-autorest/autorest v0.11.19 + github.com/fatih/color v1.12.0 github.com/hashicorp/go-azure-helpers v0.16.5 github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/go-version v1.3.0 diff --git a/go.sum b/go.sum index fee4894..aca54d3 100644 --- a/go.sum +++ b/go.sum @@ -112,6 +112,8 @@ github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.m github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.12.0 h1:mRhaKNwANqRgUBGKmnI5ZxEk7QXmjQeCcuYFMX2bfcc= +github.com/fatih/color v1.12.0/go.mod h1:ELkj/draVOlAH/xkhN6mQ50Qd0MPOk5AAr3maGEBuJM= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/form3tech-oss/jwt-go v3.2.2+incompatible h1:TcekIExNqud5crz4xD2pavyTgWiPvpYe4Xau31I0PRk= github.com/form3tech-oss/jwt-go v3.2.2+incompatible/go.mod h1:pbq4aXjuKjdthFRnoDwaVPLA+WlJuPGy+QneDUgJi2k= @@ -264,10 +266,14 @@ github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+ github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= +github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/mitchellh/cli v1.1.2/go.mod h1:6iaV0fGdElS6dPBx0EApTxHrcWvmJphyh2n8YBLPPZ4= github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= @@ -448,6 +454,7 @@ golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= diff --git a/internal/meta.go b/internal/meta.go index 29f806f..752d459 100644 --- a/internal/meta.go +++ b/internal/meta.go @@ -11,6 +11,7 @@ import ( "path/filepath" "strings" + "github.com/fatih/color" "github.com/magodo/aztfy/internal/armtemplate" "github.com/magodo/aztfy/schema" @@ -147,9 +148,19 @@ func (meta *Meta) ExportArmTemplate(ctx context.Context) error { if err := json.Unmarshal(raw, &meta.armTemplate); err != nil { return fmt.Errorf("unmarshalling the template: %w", err) } + return nil } +func (meta *Meta) ListAzureResourceIDs() []string { + var ids []string + for _, res := range meta.armTemplate.Resources { + ids = append(ids, res.ID(meta.subscriptionId, meta.resourceGroup)) + } + ids = append(ids, armtemplate.ResourceGroupId.ID(meta.subscriptionId, meta.resourceGroup)) + return ids +} + type ImportList []ImportItem func (l ImportList) NonSkipped() ImportList { @@ -163,10 +174,42 @@ func (l ImportList) NonSkipped() ImportList { return out } +func (l ImportList) ImportErrored() ImportList { + var out ImportList + for _, item := range l { + if item.ImportError == nil { + continue + } + out = append(out, item) + } + return out +} + +func (l ImportList) Imported() ImportList { + var out ImportList + for _, item := range l.NonSkipped() { + if item.ImportError != nil { + continue + } + out = append(out, item) + } + return out +} + type ImportItem struct { - ResourceID string - Skip bool + // The azure resource id + ResourceID string + + // Wether this azure resource should be skipped from importing + Skip bool + + // Whether this azure resource failed to import into terraform (this might due to the TFResourceType doesn't match the resource) + ImportError error + + // The terraform resource type TFResourceType string + + // The terraform resource name TFResourceName string } @@ -177,12 +220,7 @@ func (item *ImportItem) TFAddr() string { return item.TFResourceType + "." + item.TFResourceName } -func (meta *Meta) ResolveImportList(ctx context.Context) (ImportList, error) { - var ids []string - for _, res := range meta.armTemplate.Resources { - ids = append(ids, res.ID(meta.subscriptionId, meta.resourceGroup)) - } - ids = append(ids, armtemplate.ResourceGroupId.ID(meta.subscriptionId, meta.resourceGroup)) +func (meta *Meta) ResolveImportList(ids []string, ctx context.Context) (ImportList, error) { // schema, err := meta.tf.ProvidersSchema(ctx) // if err != nil { @@ -196,7 +234,7 @@ func (meta *Meta) ResolveImportList(ctx context.Context) (ImportList, error) { // userResourceMap is used to track the resource types and resource names that are specified by users. userResourceMap := map[string]map[string]bool{} reader := bufio.NewReader(os.Stdin) - fmt.Println(`Please input the Terraform resource type and name for each Azure resource in form of ".. Press enter with no input will skip importing that resource.`) + color.Cyan("\nPlease input either the Terraform resource type and name in the form of \".\", or simply enter to skip\n") for idx, id := range ids { item := ImportItem{ ResourceID: id, @@ -244,7 +282,13 @@ func (meta *Meta) ResolveImportList(ctx context.Context) (ImportList, error) { return list, nil } -func (meta *Meta) Import(ctx context.Context, list ImportList) error { +func (meta *Meta) Import(ctx context.Context, list ImportList) (ImportList, error) { + if len(list.NonSkipped()) == 0 { + return nil, nil + } + + color.Cyan("\nImport Azure Resources\n") + // Generate a temp Terraform config to include the empty template for each resource. // This is required for the following importing. cfgFile := filepath.Join(meta.workspace, "main.tf") @@ -253,12 +297,12 @@ func (meta *Meta) Import(ctx context.Context, list ImportList) error { for _, item := range list.NonSkipped() { tpl, err := meta.tf.Add(ctx, item.TFAddr()) if err != nil { - return fmt.Errorf("generating resource template for %s: %w", item.TFAddr(), err) + return nil, fmt.Errorf("generating resource template for %s: %w", item.TFAddr(), err) } tpls = append(tpls, tpl) } if err := os.WriteFile(cfgFile, []byte(strings.Join(tpls, "\n")), 0644); err != nil { - return fmt.Errorf("generating resource template cfgFile file: %w", err) + return nil, fmt.Errorf("generating resource template cfgFile file: %w", err) } // Remove the temp Terraform config once resources are imported. // This is due to the fact that "terraform add" will complain the resource to be added already exist in the config, even we are outputting to stdout. @@ -266,14 +310,26 @@ func (meta *Meta) Import(ctx context.Context, list ImportList) error { defer os.Remove(cfgFile) // Import resources + out := ImportList{} for idx, item := range list.NonSkipped() { - fmt.Printf("[%d/%d] Importing %q as %s\n", idx+1, len(list.NonSkipped()), item.ResourceID, item.TFAddr()) - if err := meta.tf.Import(ctx, item.TFAddr(), item.ResourceID); err != nil { - return err + fmt.Printf("[%d/%d] Importing %q as %s...\n", idx+1, len(list.NonSkipped()), item.ResourceID, item.TFAddr()) + err := meta.tf.Import(ctx, item.TFAddr(), item.ResourceID) + if err != nil { + emsg := []string{} + for _, el := range strings.Split(err.Error(), "\n") { + el := strings.TrimSpace(el) + if el == "" { + continue + } + emsg = append(emsg, "\t"+el) + } + color.Red(strings.Join(emsg, "\n")) } + item.ImportError = err + out = append(out, item) } - return nil + return out, nil } type ConfigInfos []ConfigInfo @@ -290,10 +346,7 @@ func (cfg ConfigInfo) DumpHCL(w io.Writer) (int, error) { func (meta *Meta) StateToConfig(ctx context.Context, list ImportList) (ConfigInfos, error) { out := ConfigInfos{} - for _, item := range list.NonSkipped() { - if item.Skip { - continue - } + for _, item := range list.Imported() { tpl, err := meta.tf.Add(ctx, item.TFAddr(), tfexec.FromState(true)) if err != nil { return nil, fmt.Errorf("converting terraform state to config for resource %s: %w", item.TFAddr(), err) @@ -335,7 +388,7 @@ func (meta *Meta) ResolveDependency(ctx context.Context, configs ConfigInfos) (C out = append(out, cfg) continue } - // This should never happen + // This should never happen as we always ensure there is at least one implicit dependency on the resource group for each resource. if _, ok := depInfo[armId]; !ok { return nil, fmt.Errorf("can't find resource %q in the arm template", armId.ID(meta.subscriptionId, meta.resourceGroup)) } @@ -360,7 +413,6 @@ func (meta *Meta) GenerateConfig(cfgs ConfigInfos) error { return fmt.Errorf("generating main configuration file: %w", err) } - fmt.Printf("Please find the Terraform state and the config at: %s\n", meta.workspace) return nil } @@ -369,7 +421,7 @@ func (meta *Meta) hclBlockAppendDependency(body *hclwrite.Body, armIds []armtemp for _, armid := range armIds { cfg, ok := cfgset[armid] if !ok { - dependencies = append(dependencies, fmt.Sprintf("# Depending on %q, but it is not imported by Terraform. Please fix it manually.", armid.ID(meta.subscriptionId, meta.resourceGroup))) + dependencies = append(dependencies, fmt.Sprintf("# Depending on %q, which is not imported by Terraform.", armid.ID(meta.subscriptionId, meta.resourceGroup))) continue } dependencies = append(dependencies, cfg.TFAddr()+",") diff --git a/internal/run.go b/internal/run.go index e3ec855..1e00f07 100644 --- a/internal/run.go +++ b/internal/run.go @@ -4,6 +4,8 @@ import ( "context" "io" "log" + + "github.com/fatih/color" ) func Run(ctx context.Context, rg string) error { @@ -23,16 +25,33 @@ func Run(ctx context.Context, rg string) error { return err } - importList, err := meta.ResolveImportList(ctx) - if err != nil { - return err - } + ids := meta.ListAzureResourceIDs() - if err := meta.Import(ctx, importList); err != nil { - return err + // Repeat importing resources here to avoid the user incorrectly maps an azure resource to an incorrect terraform resource + var importedList ImportList + for len(ids) != 0 { + l, err := meta.ResolveImportList(ids, ctx) + if err != nil { + return err + } + + l, err = meta.Import(ctx, l) + if err != nil { + return err + } + + for _, item := range l.Imported() { + importedList = append(importedList, item) + } + + importErroredList := l.ImportErrored() + ids = make([]string, 0, len(importErroredList)) + for _, item := range importErroredList { + ids = append(ids, item.ResourceID) + } } - configs, err := meta.StateToConfig(ctx, importList) + configs, err := meta.StateToConfig(ctx, importedList) if err != nil { return err } @@ -46,5 +65,7 @@ func Run(ctx context.Context, rg string) error { return err } + color.Cyan("\nPlease find the Terraform state and the config at: %s\n", meta.workspace) + return nil } diff --git a/main.go b/main.go index 987590b..205fc70 100644 --- a/main.go +++ b/main.go @@ -15,7 +15,7 @@ var ( ) func init() { - flagVersion = flag.Bool("v", false, "print version") + flagVersion = flag.Bool("v", false, "Print version") } const usage = `aztfy [option]