github.com/rstandt/terraform@v0.12.32-0.20230710220336-b1063613405c/configs/configupgrade/upgrade_test.go (about) 1 package configupgrade 2 3 import ( 4 "bytes" 5 "flag" 6 "io" 7 "io/ioutil" 8 "log" 9 "os" 10 "os/exec" 11 "path/filepath" 12 "testing" 13 14 "github.com/davecgh/go-spew/spew" 15 "github.com/zclconf/go-cty/cty" 16 17 "github.com/hashicorp/terraform/addrs" 18 backendinit "github.com/hashicorp/terraform/backend/init" 19 "github.com/hashicorp/terraform/configs/configschema" 20 "github.com/hashicorp/terraform/helper/logging" 21 "github.com/hashicorp/terraform/providers" 22 "github.com/hashicorp/terraform/provisioners" 23 "github.com/hashicorp/terraform/terraform" 24 ) 25 26 func TestUpgradeValid(t *testing.T) { 27 // This test uses the contents of the testdata/valid directory as 28 // a table of tests. Every directory there must have both "input" and 29 // "want" subdirectories, where "input" is the configuration to be 30 // upgraded and "want" is the expected result. 31 fixtureDir := "testdata/valid" 32 testDirs, err := ioutil.ReadDir(fixtureDir) 33 if err != nil { 34 t.Fatal(err) 35 } 36 37 for _, entry := range testDirs { 38 if !entry.IsDir() { 39 continue 40 } 41 t.Run(entry.Name(), func(t *testing.T) { 42 inputDir := filepath.Join(fixtureDir, entry.Name(), "input") 43 wantDir := filepath.Join(fixtureDir, entry.Name(), "want") 44 u := &Upgrader{ 45 Providers: providers.ResolverFixed(testProviders), 46 Provisioners: testProvisioners, 47 } 48 49 inputSrc, err := LoadModule(inputDir) 50 if err != nil { 51 t.Fatal(err) 52 } 53 wantSrc, err := LoadModule(wantDir) 54 if err != nil { 55 t.Fatal(err) 56 } 57 58 gotSrc, diags := u.Upgrade(inputSrc, inputDir) 59 if diags.HasErrors() { 60 t.Error(diags.Err()) 61 } 62 63 // Upgrade uses a nil entry as a signal to delete a file, which 64 // we can't test here because we aren't modifying an existing 65 // dir in place, so we'll just ignore those and leave that mechanism 66 // to be tested elsewhere. 67 68 for name, got := range gotSrc { 69 if gotSrc[name] == nil { 70 delete(gotSrc, name) 71 continue 72 } 73 want, wanted := wantSrc[name] 74 if !wanted { 75 t.Errorf("unexpected extra output file %q\n=== GOT ===\n%s", name, got) 76 continue 77 } 78 79 got = bytes.TrimSpace(got) 80 want = bytes.TrimSpace(want) 81 if !bytes.Equal(got, want) { 82 diff := diffSourceFiles(got, want) 83 t.Errorf("wrong content in %q\n%s", name, diff) 84 } 85 } 86 87 for name, want := range wantSrc { 88 if _, present := gotSrc[name]; !present { 89 t.Errorf("missing output file %q\n=== WANT ===\n%s", name, want) 90 } 91 } 92 }) 93 } 94 } 95 96 func TestUpgradeRenameJSON(t *testing.T) { 97 inputDir := filepath.Join("testdata/valid/rename-json/input") 98 inputSrc, err := LoadModule(inputDir) 99 if err != nil { 100 t.Fatal(err) 101 } 102 103 u := &Upgrader{ 104 Providers: providers.ResolverFixed(testProviders), 105 } 106 gotSrc, diags := u.Upgrade(inputSrc, inputDir) 107 if diags.HasErrors() { 108 t.Error(diags.Err()) 109 } 110 111 // This test fixture is also fully covered by TestUpgradeValid, so 112 // we're just testing that the file was renamed here. 113 src, exists := gotSrc["misnamed-json.tf"] 114 if src != nil { 115 t.Errorf("misnamed-json.tf still has content") 116 } else if !exists { 117 t.Errorf("misnamed-json.tf not marked for deletion") 118 } 119 120 src, exists = gotSrc["misnamed-json.tf.json"] 121 if src == nil || !exists { 122 t.Errorf("misnamed-json.tf.json was not created") 123 } 124 } 125 126 func diffSourceFiles(got, want []byte) []byte { 127 // We'll try to run "diff -u" here to get nice output, but if that fails 128 // (e.g. because we're running on a machine without diff installed) then 129 // we'll fall back on just printing out the before and after in full. 130 gotR, gotW, err := os.Pipe() 131 if err != nil { 132 return diffSourceFilesFallback(got, want) 133 } 134 defer gotR.Close() 135 defer gotW.Close() 136 wantR, wantW, err := os.Pipe() 137 if err != nil { 138 return diffSourceFilesFallback(got, want) 139 } 140 defer wantR.Close() 141 defer wantW.Close() 142 143 cmd := exec.Command("diff", "-u", "--label=GOT", "--label=WANT", "/dev/fd/3", "/dev/fd/4") 144 cmd.ExtraFiles = []*os.File{gotR, wantR} 145 stdout, err := cmd.StdoutPipe() 146 stderr, err := cmd.StderrPipe() 147 if err != nil { 148 return diffSourceFilesFallback(got, want) 149 } 150 151 go func() { 152 wantW.Write(want) 153 wantW.Close() 154 }() 155 go func() { 156 gotW.Write(got) 157 gotW.Close() 158 }() 159 160 err = cmd.Start() 161 if err != nil { 162 return diffSourceFilesFallback(got, want) 163 } 164 165 outR := io.MultiReader(stdout, stderr) 166 out, err := ioutil.ReadAll(outR) 167 if err != nil { 168 return diffSourceFilesFallback(got, want) 169 } 170 171 cmd.Wait() // not checking errors here because on failure we'll have stderr captured to return 172 173 const noNewline = "\\ No newline at end of file\n" 174 if bytes.HasSuffix(out, []byte(noNewline)) { 175 out = out[:len(out)-len(noNewline)] 176 } 177 return out 178 } 179 180 func diffSourceFilesFallback(got, want []byte) []byte { 181 var buf bytes.Buffer 182 buf.WriteString("=== GOT ===\n") 183 buf.Write(got) 184 buf.WriteString("\n=== WANT ===\n") 185 buf.Write(want) 186 buf.WriteString("\n") 187 return buf.Bytes() 188 } 189 190 var testProviders = map[addrs.Provider]providers.Factory{ 191 addrs.NewLegacyProvider("test"): providers.Factory(func() (providers.Interface, error) { 192 p := &terraform.MockProvider{} 193 p.GetSchemaReturn = &terraform.ProviderSchema{ 194 ResourceTypes: map[string]*configschema.Block{ 195 "test_instance": { 196 Attributes: map[string]*configschema.Attribute{ 197 "id": {Type: cty.String, Computed: true}, 198 "type": {Type: cty.String, Optional: true}, 199 "image": {Type: cty.String, Optional: true}, 200 "tags": {Type: cty.Map(cty.String), Optional: true}, 201 "security_groups": {Type: cty.List(cty.String), Optional: true}, 202 "subnet_ids": {Type: cty.Set(cty.String), Optional: true}, 203 "list_of_obj": {Type: cty.List(cty.EmptyObject), Optional: true}, 204 }, 205 BlockTypes: map[string]*configschema.NestedBlock{ 206 "network": { 207 Nesting: configschema.NestingSet, 208 Block: configschema.Block{ 209 Attributes: map[string]*configschema.Attribute{ 210 "cidr_block": {Type: cty.String, Optional: true}, 211 "subnet_cidrs": {Type: cty.Map(cty.String), Computed: true}, 212 }, 213 BlockTypes: map[string]*configschema.NestedBlock{ 214 "subnet": { 215 Nesting: configschema.NestingSet, 216 Block: configschema.Block{ 217 Attributes: map[string]*configschema.Attribute{ 218 "number": {Type: cty.Number, Required: true}, 219 }, 220 }, 221 }, 222 }, 223 }, 224 }, 225 "addresses": { 226 Nesting: configschema.NestingSingle, 227 Block: configschema.Block{ 228 Attributes: map[string]*configschema.Attribute{ 229 "ipv4": {Type: cty.String, Computed: true}, 230 "ipv6": {Type: cty.String, Computed: true}, 231 }, 232 }, 233 }, 234 }, 235 }, 236 }, 237 } 238 return p, nil 239 }), 240 addrs.NewLegacyProvider("terraform"): providers.Factory(func() (providers.Interface, error) { 241 p := &terraform.MockProvider{} 242 p.GetSchemaReturn = &terraform.ProviderSchema{ 243 DataSources: map[string]*configschema.Block{ 244 "terraform_remote_state": { 245 // This is just enough an approximation of the remote state 246 // schema to check out reference upgrade logic. It is 247 // intentionally not fully-comprehensive. 248 Attributes: map[string]*configschema.Attribute{ 249 "backend": {Type: cty.String, Optional: true}, 250 }, 251 }, 252 }, 253 } 254 return p, nil 255 }), 256 addrs.NewLegacyProvider("aws"): providers.Factory(func() (providers.Interface, error) { 257 // This is here only so we can test the provisioner connection info 258 // migration behavior, which is resource-type specific. Do not use 259 // it in any other tests. 260 p := &terraform.MockProvider{} 261 p.GetSchemaReturn = &terraform.ProviderSchema{ 262 ResourceTypes: map[string]*configschema.Block{ 263 "aws_instance": {}, 264 }, 265 } 266 return p, nil 267 }), 268 } 269 270 var testProvisioners = map[string]provisioners.Factory{ 271 "test": provisioners.Factory(func() (provisioners.Interface, error) { 272 p := &terraform.MockProvisioner{} 273 p.GetSchemaResponse = provisioners.GetSchemaResponse{ 274 Provisioner: &configschema.Block{ 275 Attributes: map[string]*configschema.Attribute{ 276 "commands": {Type: cty.List(cty.String), Optional: true}, 277 "interpreter": {Type: cty.String, Optional: true}, 278 }, 279 }, 280 } 281 return p, nil 282 }), 283 } 284 285 func init() { 286 // Initialize the backends 287 backendinit.Init(nil) 288 } 289 290 func TestMain(m *testing.M) { 291 flag.Parse() 292 if testing.Verbose() { 293 // if we're verbose, use the logging requested by TF_LOG 294 logging.SetOutput() 295 } else { 296 // otherwise silence all logs 297 log.SetOutput(ioutil.Discard) 298 } 299 300 // We have fmt.Stringer implementations on lots of objects that hide 301 // details that we very often want to see in tests, so we just disable 302 // spew's use of String methods globally on the assumption that spew 303 // usage implies an intent to see the raw values and ignore any 304 // abstractions. 305 spew.Config.DisableMethods = true 306 307 os.Exit(m.Run()) 308 }