From 5c01015cdbb170b498f1fb9829c21b6c0e81123a Mon Sep 17 00:00:00 2001 From: xescugc Date: Mon, 3 Jul 2023 12:45:01 +0200 Subject: [PATCH] azurerm: Add the 'lifecycle' to 'azurerm_virtual_machine_data_disk_attachment' and 'azurerm_virtual_machine' So it does not give out more errors on import in any of the resources --- CHANGELOG.md | 4 ++- azurerm/provider.go | 86 +++++++++++++++++++++++++++++++++++++++++++- hcl/format.go | 8 ++++- hcl/format_test.go | 12 +++++++ provider/resource.go | 2 +- state/writer.go | 14 ++++++-- state/writer_test.go | 2 +- util/cty.go | 39 ++++++++++++++++++-- 8 files changed, 157 insertions(+), 10 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 095e9dfb85..7f3b46a6a7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,8 +7,10 @@ ### Fixed - The generated HCL now has the fixed version for the provider used instead of using the latest one by default ([Issue #378](https://github.com/cycloidio/terracognita/issues/378)) -- Add resource_group scope to azurerm_storage_account +- Add `resource_group` scope to `azurerm_storage_account` ([Issue #393](https://github.com/cycloidio/terracognita/issues/393)) +- AzureRM `azurerm_virtual_machine_data_disk_attachment` was still giving issues on import so we added the `lifecycle.ignore_changes=[create_option]` to it + ([Issue #395](https://github.com/cycloidio/terracognita/issues/395)) ## [0.8.4] _2023-05-18_ diff --git a/azurerm/provider.go b/azurerm/provider.go index afa0e9d57a..f01e9c48ec 100644 --- a/azurerm/provider.go +++ b/azurerm/provider.go @@ -20,6 +20,7 @@ import ( "github.com/cycloidio/terracognita/filter" "github.com/cycloidio/terracognita/log" "github.com/cycloidio/terracognita/provider" + "github.com/cycloidio/terracognita/util" ) // version of the Terraform provider, this is automatically changed with the 'make update-terraform-provider' @@ -145,7 +146,38 @@ func (a *azurerm) TFClient() interface{} { } func (a *azurerm) TFProvider() *schema.Provider { - return a.tfProvider + tfp := a.tfProvider + + for _, v := range tfp.ResourcesMap { + // We add the `lifecycle` to all the resource on AzureRM, if we + // need it for other providers then we'll add the missing + // Metadata attributes and abstract this too + v.Schema["lifecycle"] = &schema.Schema{ + Type: schema.TypeSet, + Optional: true, + Elem: &schema.Resource{ + Schema: map[string]*schema.Schema{ + "create_before_destroy": &schema.Schema{ + Optional: true, + Type: schema.TypeBool, + }, + "prevent_destroy": &schema.Schema{ + Optional: true, + Type: schema.TypeBool, + }, + "ignore_changes": &schema.Schema{ + Optional: true, + Type: schema.TypeList, + Elem: &schema.Schema{ + Type: schema.TypeString, + }, + }, + }, + }, + } + } + + return tfp } func (a *azurerm) FixResource(t string, v cty.Value) (cty.Value, error) { @@ -156,6 +188,8 @@ func (a *azurerm) FixResource(t string, v cty.Value) (cty.Value, error) { // the unsetManageDiskID is the list of all the indexed of the storage_account_type that // have the create_option as FromImage var unsetManageDiskID = make(map[int]struct{}) + var storageOSDiskCount int + var storageDataDiskCount int err = cty.Walk(v, func(path cty.Path, val cty.Value) (bool, error) { if len(path) == 3 { if gas, ok := path[0].(cty.GetAttrStep); ok { @@ -163,6 +197,7 @@ func (a *azurerm) FixResource(t string, v cty.Value) (cty.Value, error) { switch gas.Name { case "storage_os_disk": if path[2].(cty.GetAttrStep).Name == "create_option" { + storageOSDiskCount += 1 var co string err := gocty.FromCtyValue(val, &co) if err != nil { @@ -177,6 +212,10 @@ func (a *azurerm) FixResource(t string, v cty.Value) (cty.Value, error) { unsetManageDiskID[idx] = struct{}{} } } + case "storage_data_disk": + if path[2].(cty.GetAttrStep).Name == "create_option" { + storageDataDiskCount += 1 + } } } } @@ -219,6 +258,32 @@ func (a *azurerm) FixResource(t string, v cty.Value) (cty.Value, error) { } return v, nil }) + m, err := util.CtyObjectToUnstructured(&v) + if err != nil { + return v, errors.Wrapf(err, "failed to convert cty object to map") + } + + ignoreChanges := make([]string, 0, 0) + for i := 0; i < storageDataDiskCount; i++ { + ignoreChanges = append(ignoreChanges, fmt.Sprintf("=tc_unquote=storage_data_disk[%d].create_option", i)) + } + for i := 0; i < storageOSDiskCount; i++ { + ignoreChanges = append(ignoreChanges, fmt.Sprintf("=tc_unquote=storage_os_disk[%d].create_option", i)) + } + // We need to set all the possible attributes and if they do not have + // any value then set them to nil + m["lifecycle"] = []interface{}{ + map[string]interface{}{ + "create_before_destroy": nil, + "prevent_destroy": nil, + "ignore_changes": ignoreChanges, + }, + } + nv, err := util.UnstructuredToCty(m) + if err != nil { + return v, errors.Wrapf(err, "failed to convert map to cty object") + } + return nv, nil if err != nil { return v, errors.Wrapf(err, "failed to convert CTY value to GO type") } @@ -281,6 +346,25 @@ func (a *azurerm) FixResource(t string, v cty.Value) (cty.Value, error) { if err != nil { return v, errors.Wrapf(err, "failed to convert CTY value to GO type") } + m, err := util.CtyObjectToUnstructured(&v) + if err != nil { + return v, errors.Wrapf(err, "failed to convert cty object to map") + } + + // We need to set all the possible attributes and if they do not have + // any value then set them to nil + m["lifecycle"] = []interface{}{ + map[string]interface{}{ + "create_before_destroy": nil, + "prevent_destroy": nil, + "ignore_changes": []string{"=tc_unquote=create_option"}, + }, + } + nv, err := util.UnstructuredToCty(m) + if err != nil { + return v, errors.Wrapf(err, "failed to convert map to cty object") + } + return nv, nil case "azurerm_windows_virtual_machine": v, err = cty.Transform(v, func(path cty.Path, v cty.Value) (cty.Value, error) { if len(path) > 0 { diff --git a/hcl/format.go b/hcl/format.go index 9cb02d461a..0ad358e012 100644 --- a/hcl/format.go +++ b/hcl/format.go @@ -57,7 +57,13 @@ var ( }, }, { - match: regexp.MustCompile(`=tc=`), + // Used for quoted values to unquote + // Replace all the `"key" = "=tc_unquote=value"` for `"key" = value` + match: regexp.MustCompile(`"=tc_unquote=([^"]+)"`), + replace: []byte(`$1`), + }, + { + match: regexp.MustCompile(`=tc[^=]*=`), replace: []byte{}, }, { diff --git a/hcl/format_test.go b/hcl/format_test.go index 0d52a20059..d9f0415ac3 100644 --- a/hcl/format_test.go +++ b/hcl/format_test.go @@ -154,6 +154,18 @@ func TestFormat(t *testing.T) { role = [var.isa-role,var.isa-role2,"{value}"] }`), }, + { + name: "RemoveQuotes", + in: []byte(` + "role" = value + + "env" = "=tc_unquote=value" + `), + out: []byte(` + role = value + env = value + `), + }, } for _, tt := range tests { diff --git a/provider/resource.go b/provider/resource.go index 7125cd1e51..e10d4358f7 100644 --- a/provider/resource.go +++ b/provider/resource.go @@ -327,7 +327,7 @@ func (r *resource) Read(f *filter.Filter) error { return err } - zstate, err := util.HashicorpToZclonfValue(newStateValue, r.tfResource.CoreConfigSchema().ImpliedType()) + zstate, err := util.HashicorpToZclonfValue(newStateValue, r.ImpliedType()) if err != nil { return err } diff --git a/state/writer.go b/state/writer.go index 1501176f83..16f57c00fa 100644 --- a/state/writer.go +++ b/state/writer.go @@ -1,6 +1,7 @@ package state import ( + "bytes" "fmt" "io" "regexp" @@ -122,15 +123,24 @@ func (w *Writer) Sync() error { lstate := w.state.Lock() defer w.state.Unlock() - log.Get().Log("func", "state.Sync(State)", "msg", "writting state to state file") + log.Get().Log("func", "state.Sync(State)", "msg", "writing state to state file") file := statemgr.NewStateFile() file.State = lstate - err := statefile.Write(file, w.writer) + b := bytes.Buffer{} + err := statefile.Write(file, &b) if err != nil { return err } + // Normally the formatter is only used on the HCL as is the one that has wrong format + // but now we set thing before the HCL/State are generated so those are passed through + // both of them, for now it's only the `"=tc_unquote=value"` so we remove it + m := regexp.MustCompile(`=tc[^=]*=`) + fb := m.ReplaceAll(b.Bytes(), nil) + nb := bytes.NewBuffer(fb) + io.Copy(w.writer, nb) + return nil } diff --git a/state/writer_test.go b/state/writer_test.go index 80ea094476..c08b085d25 100644 --- a/state/writer_test.go +++ b/state/writer_test.go @@ -192,7 +192,7 @@ func TestSync(t *testing.T) { tpt, err := util.HashicorpToZclonfType(aws.Provider().ResourcesMap[tp].CoreConfigSchema().ImpliedType()) require.NoError(t, err) - s, err := hcl2shim.HCL2ValueFromFlatmap(map[string]string{"name": "Pepito"}, tpt) + s, err := hcl2shim.HCL2ValueFromFlatmap(map[string]string{"name": "=tc_unquote=Pepito"}, tpt) require.NoError(t, err) res.EXPECT().Type().Return(tp) diff --git a/util/cty.go b/util/cty.go index 743d19b692..a1c8e9924d 100644 --- a/util/cty.go +++ b/util/cty.go @@ -4,7 +4,9 @@ import ( "encoding/json" hcty "github.com/hashicorp/go-cty/cty" + hctyjson "github.com/hashicorp/go-cty/cty/json" hmsgpack "github.com/hashicorp/go-cty/cty/msgpack" + "github.com/pkg/errors" zcty "github.com/zclconf/go-cty/cty" zmsgpack "github.com/zclconf/go-cty/cty/msgpack" ) @@ -27,15 +29,46 @@ func HashicorpToZclonfType(ht hcty.Type) (zcty.Type, error) { func HashicorpToZclonfValue(hv hcty.Value, ht hcty.Type) (zcty.Value, error) { sb, err := hmsgpack.Marshal(hv, ht) if err != nil { - return zcty.EmptyObjectVal, err + return zcty.EmptyObjectVal, errors.Wrapf(err, "failed to Hashicorp marshal") } ty, err := HashicorpToZclonfType(ht) if err != nil { - return zcty.EmptyObjectVal, err + return zcty.EmptyObjectVal, errors.Wrapf(err, "failed to convert from Hashiciprt to Zclon") } zvalue, err := zmsgpack.Unmarshal(sb, ty) if err != nil { - return zcty.EmptyObjectVal, err + return zcty.EmptyObjectVal, errors.Wrapf(err, "failed to Zclon unmarshal") } return zvalue, nil } + +// CtyObjectToUnstructured converts a Terraform specific cty.Object type manifest +// into a dynamic client specific unstructured object +func CtyObjectToUnstructured(in *hcty.Value) (map[string]interface{}, error) { + simple := &hctyjson.SimpleJSONValue{Value: *in} + jsonVal, err := simple.MarshalJSON() + if err != nil { + return nil, err + } + udata := map[string]interface{}{} + err = json.Unmarshal(jsonVal, &udata) + if err != nil { + return nil, err + } + return udata, nil +} + +// UnstructuredToCty converts a dynamic client specific unstructured object +// into a Terraform specific cty.Object type manifest +func UnstructuredToCty(in map[string]interface{}) (hcty.Value, error) { + jsonVal, err := json.Marshal(in) + if err != nil { + return hcty.NilVal, errors.Wrapf(err, "unable to marshal value") + } + simple := &hctyjson.SimpleJSONValue{} + err = simple.UnmarshalJSON(jsonVal) + if err != nil { + return hcty.NilVal, errors.Wrapf(err, "unable to unmarshal to simple value") + } + return simple.Value, nil +}