github.com/pix4d/terravalet@v0.8.1-0.20240131132849-abcd6a79eeeb/cmdimport.go (about) 1 package main 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "io" 7 "os" 8 "strings" 9 ) 10 11 type ResourcesBundle struct { 12 ResourceChanges []ResourceChange `json:"resource_changes"` 13 } 14 15 type ResourceChange struct { 16 Address string `json:"address"` 17 Type string `json:"type"` 18 ProviderName string `json:"provider_name"` 19 Change struct { 20 Actions []string `json:"actions"` 21 After interface{} `json:"after"` 22 } `json:"change"` 23 } 24 25 type Definitions struct { 26 Separator string `json:"separator"` 27 Priority int `json:"priority"` 28 Variables []string `json:"variables"` 29 } 30 31 // Keep track of the asymmetry of import subcommand. 32 // When importing, the up direction wants two parameters: 33 // 34 // terraform import res-address res-id 35 // 36 // while the down direction wants only one parameter: 37 // 38 // terraform state rm res-address 39 type ImportElement struct { 40 Addr string 41 ID string 42 } 43 44 func doImport(upPath, downPath, srcPlanPath, resourcesDefinitions string) error { 45 definitionsFile, err := os.Open(resourcesDefinitions) 46 if err != nil { 47 return fmt.Errorf("opening the definitions file: %v", err) 48 } 49 defer definitionsFile.Close() 50 51 srcPlanFile, err := os.Open(srcPlanPath) 52 if err != nil { 53 return fmt.Errorf("opening the terraform plan file: %v", err) 54 } 55 defer srcPlanFile.Close() 56 57 upFile, err := os.Create(upPath) 58 if err != nil { 59 return fmt.Errorf("creating the up file: %v", err) 60 } 61 defer upFile.Close() 62 63 downFile, err := os.Create(downPath) 64 if err != nil { 65 return fmt.Errorf("creating the down file: %v", err) 66 } 67 defer downFile.Close() 68 69 imports, removals, err := Import(srcPlanFile, definitionsFile) 70 if err != nil { 71 return fmt.Errorf("parse src-plan: %v", err) 72 } 73 74 if err := importUpScript(imports, upFile); err != nil { 75 return fmt.Errorf("writing the up script: %v", err) 76 } 77 if err := importDownScript(removals, downFile); err != nil { 78 return fmt.Errorf("writing the down script: %v", err) 79 } 80 81 return nil 82 } 83 84 func Import(rd, definitionsFile io.Reader) ([]ImportElement, []ImportElement, error) { 85 var imports []ImportElement 86 var removals []ImportElement 87 var configs map[string]Definitions 88 var resourcesBundle ResourcesBundle 89 var filteredResources []ResourceChange 90 91 plan, err := io.ReadAll(rd) 92 if err != nil { 93 return imports, removals, 94 fmt.Errorf("reading the plan file: %s", err) 95 } 96 if err = json.Unmarshal(plan, &resourcesBundle); err != nil { 97 return imports, removals, 98 fmt.Errorf("parsing the plan: %s", err) 99 } 100 101 defs, err := io.ReadAll(definitionsFile) 102 if err != nil { 103 return imports, removals, 104 fmt.Errorf("reading the definitions file: %s", err) 105 } 106 if err = json.Unmarshal(defs, &configs); err != nil { 107 return imports, removals, 108 fmt.Errorf("parsing resources definitions: %s", err) 109 } 110 111 // Return objects in the correct order if 'priority' parameter is set in provider configuration. 112 // The remove order is reversed (LIFO logic). 113 114 // Filter all "create" resources before going further 115 for _, resource := range resourcesBundle.ResourceChanges { 116 if resource.Change.Actions[0] == "create" { 117 filteredResources = append(filteredResources, resource) 118 } 119 } 120 121 if len(filteredResources) == 0 { 122 return imports, removals, 123 fmt.Errorf("src-plan doesn't contains resources to create") 124 } 125 126 for _, resource := range filteredResources { 127 // Proceed only if type is declared in resources definitions 128 if _, ok := configs[resource.Type]; !ok { 129 msg := fmt.Sprintf("Warning: resource %s is not defined. Check %s documentation\n", 130 resource.Type, resource.ProviderName) 131 fmt.Printf("\033[1;33m%s\033[0m", msg) 132 continue 133 } 134 resourceParams := configs[resource.Type] 135 var resID []string 136 after := resource.Change.After.(map[string]interface{}) 137 for _, field := range resourceParams.Variables { 138 if _, ok := after[field]; !ok { 139 return imports, removals, 140 fmt.Errorf( 141 "error in resources definition %s: field '%s' doesn't exist in plan", 142 resource.Type, field) 143 } 144 subID, ok := after[field].(string) 145 if !ok { 146 return imports, removals, 147 fmt.Errorf("resource_changes: after: %s: type is %T; want: string", 148 field, after[field]) 149 } 150 resID = append(resID, subID) 151 } 152 153 elem := ImportElement{ 154 Addr: resource.Address, 155 ID: strings.Join(resID, resourceParams.Separator)} 156 157 if resourceParams.Priority == 1 { 158 // Prepend 159 imports = append([]ImportElement{elem}, imports...) 160 } else { 161 // Append 162 imports = append(imports, elem) 163 } 164 } 165 166 if len(imports) == 0 { 167 return imports, removals, 168 fmt.Errorf("src-plan contains only undefined resources") 169 } 170 171 // The removals are the reverse of the imports. 172 removals = make([]ImportElement, 0, len(imports)) 173 for i := len(imports) - 1; i >= 0; i-- { 174 removals = append(removals, imports[i]) 175 } 176 177 return imports, removals, nil 178 } 179 180 func importUpScript(elements []ImportElement, out io.Writer) error { 181 cmd := "terraform import" 182 fmt.Fprintf(out, importScriptHeader, cmd, len(elements)) 183 for _, elem := range elements { 184 fmt.Fprintf(out, "%s \\\n %q %q\n\n", cmd, elem.Addr, elem.ID) 185 } 186 return nil 187 } 188 189 func importDownScript(elements []ImportElement, out io.Writer) error { 190 cmd := "terraform state rm" 191 fmt.Fprintf(out, importScriptHeader, cmd, len(elements)) 192 for _, elem := range elements { 193 fmt.Fprintf(out, "%s \\\n %q\n\n", cmd, elem.Addr) 194 } 195 return nil 196 } 197 198 const importScriptHeader = `#! /bin/sh 199 # DO NOT EDIT. Generated by terravalet. 200 # WARNING: check the order of resources before running this script. 201 # 202 # This script will %q %d items. 203 204 # Uncomment this if you want to stop the script at first error 205 # set -e 206 set -x 207 208 `