github.com/cycloidio/terraform@v1.1.10-0.20220513142504-76d5c768dc63/addrs/module_source_test.go (about) 1 package addrs 2 3 import ( 4 "testing" 5 6 "github.com/google/go-cmp/cmp" 7 svchost "github.com/hashicorp/terraform-svchost" 8 ) 9 10 func TestParseModuleSource(t *testing.T) { 11 tests := map[string]struct { 12 input string 13 want ModuleSource 14 wantErr string 15 }{ 16 // Local paths 17 "local in subdirectory": { 18 input: "./child", 19 want: ModuleSourceLocal("./child"), 20 }, 21 "local in subdirectory non-normalized": { 22 input: "./nope/../child", 23 want: ModuleSourceLocal("./child"), 24 }, 25 "local in sibling directory": { 26 input: "../sibling", 27 want: ModuleSourceLocal("../sibling"), 28 }, 29 "local in sibling directory non-normalized": { 30 input: "./nope/../../sibling", 31 want: ModuleSourceLocal("../sibling"), 32 }, 33 "Windows-style local in subdirectory": { 34 input: `.\child`, 35 want: ModuleSourceLocal("./child"), 36 }, 37 "Windows-style local in subdirectory non-normalized": { 38 input: `.\nope\..\child`, 39 want: ModuleSourceLocal("./child"), 40 }, 41 "Windows-style local in sibling directory": { 42 input: `..\sibling`, 43 want: ModuleSourceLocal("../sibling"), 44 }, 45 "Windows-style local in sibling directory non-normalized": { 46 input: `.\nope\..\..\sibling`, 47 want: ModuleSourceLocal("../sibling"), 48 }, 49 "an abominable mix of different slashes": { 50 input: `./nope\nope/why\./please\don't`, 51 want: ModuleSourceLocal("./nope/nope/why/please/don't"), 52 }, 53 54 // Registry addresses 55 // (NOTE: There is another test function TestParseModuleSourceRegistry 56 // which tests this situation more exhaustively, so this is just a 57 // token set of cases to see that we are indeed calling into the 58 // registry address parser when appropriate.) 59 "main registry implied": { 60 input: "hashicorp/subnets/cidr", 61 want: ModuleSourceRegistry{ 62 PackageAddr: ModuleRegistryPackage{ 63 Host: svchost.Hostname("registry.terraform.io"), 64 Namespace: "hashicorp", 65 Name: "subnets", 66 TargetSystem: "cidr", 67 }, 68 Subdir: "", 69 }, 70 }, 71 "main registry implied, subdir": { 72 input: "hashicorp/subnets/cidr//examples/foo", 73 want: ModuleSourceRegistry{ 74 PackageAddr: ModuleRegistryPackage{ 75 Host: svchost.Hostname("registry.terraform.io"), 76 Namespace: "hashicorp", 77 Name: "subnets", 78 TargetSystem: "cidr", 79 }, 80 Subdir: "examples/foo", 81 }, 82 }, 83 "main registry implied, escaping subdir": { 84 input: "hashicorp/subnets/cidr//../nope", 85 // NOTE: This error is actually being caught by the _remote package_ 86 // address parser, because any registry parsing failure falls back 87 // to that but both of them have the same subdir validation. This 88 // case is here to make sure that stays true, so we keep reporting 89 // a suitable error when the user writes a registry-looking thing. 90 wantErr: `subdirectory path "../nope" leads outside of the module package`, 91 }, 92 "custom registry": { 93 input: "example.com/awesomecorp/network/happycloud", 94 want: ModuleSourceRegistry{ 95 PackageAddr: ModuleRegistryPackage{ 96 Host: svchost.Hostname("example.com"), 97 Namespace: "awesomecorp", 98 Name: "network", 99 TargetSystem: "happycloud", 100 }, 101 Subdir: "", 102 }, 103 }, 104 "custom registry, subdir": { 105 input: "example.com/awesomecorp/network/happycloud//examples/foo", 106 want: ModuleSourceRegistry{ 107 PackageAddr: ModuleRegistryPackage{ 108 Host: svchost.Hostname("example.com"), 109 Namespace: "awesomecorp", 110 Name: "network", 111 TargetSystem: "happycloud", 112 }, 113 Subdir: "examples/foo", 114 }, 115 }, 116 117 // Remote package addresses 118 "github.com shorthand": { 119 input: "github.com/hashicorp/terraform-cidr-subnets", 120 want: ModuleSourceRemote{ 121 PackageAddr: ModulePackage("git::https://github.com/hashicorp/terraform-cidr-subnets.git"), 122 }, 123 }, 124 "github.com shorthand, subdir": { 125 input: "github.com/hashicorp/terraform-cidr-subnets//example/foo", 126 want: ModuleSourceRemote{ 127 PackageAddr: ModulePackage("git::https://github.com/hashicorp/terraform-cidr-subnets.git"), 128 Subdir: "example/foo", 129 }, 130 }, 131 "git protocol, URL-style": { 132 input: "git://example.com/code/baz.git", 133 want: ModuleSourceRemote{ 134 PackageAddr: ModulePackage("git://example.com/code/baz.git"), 135 }, 136 }, 137 "git protocol, URL-style, subdir": { 138 input: "git://example.com/code/baz.git//bleep/bloop", 139 want: ModuleSourceRemote{ 140 PackageAddr: ModulePackage("git://example.com/code/baz.git"), 141 Subdir: "bleep/bloop", 142 }, 143 }, 144 "git over HTTPS, URL-style": { 145 input: "git::https://example.com/code/baz.git", 146 want: ModuleSourceRemote{ 147 PackageAddr: ModulePackage("git::https://example.com/code/baz.git"), 148 }, 149 }, 150 "git over HTTPS, URL-style, subdir": { 151 input: "git::https://example.com/code/baz.git//bleep/bloop", 152 want: ModuleSourceRemote{ 153 PackageAddr: ModulePackage("git::https://example.com/code/baz.git"), 154 Subdir: "bleep/bloop", 155 }, 156 }, 157 "git over SSH, URL-style": { 158 input: "git::ssh://git@example.com/code/baz.git", 159 want: ModuleSourceRemote{ 160 PackageAddr: ModulePackage("git::ssh://git@example.com/code/baz.git"), 161 }, 162 }, 163 "git over SSH, URL-style, subdir": { 164 input: "git::ssh://git@example.com/code/baz.git//bleep/bloop", 165 want: ModuleSourceRemote{ 166 PackageAddr: ModulePackage("git::ssh://git@example.com/code/baz.git"), 167 Subdir: "bleep/bloop", 168 }, 169 }, 170 "git over SSH, scp-style": { 171 input: "git::git@example.com:code/baz.git", 172 want: ModuleSourceRemote{ 173 // Normalized to URL-style 174 PackageAddr: ModulePackage("git::ssh://git@example.com/code/baz.git"), 175 }, 176 }, 177 "git over SSH, scp-style, subdir": { 178 input: "git::git@example.com:code/baz.git//bleep/bloop", 179 want: ModuleSourceRemote{ 180 // Normalized to URL-style 181 PackageAddr: ModulePackage("git::ssh://git@example.com/code/baz.git"), 182 Subdir: "bleep/bloop", 183 }, 184 }, 185 186 // NOTE: We intentionally don't test the bitbucket.org shorthands 187 // here, because that detector makes direct HTTP tequests to the 188 // Bitbucket API and thus isn't appropriate for unit testing. 189 190 "Google Cloud Storage bucket implied, path prefix": { 191 input: "www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE", 192 want: ModuleSourceRemote{ 193 PackageAddr: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"), 194 }, 195 }, 196 "Google Cloud Storage bucket, path prefix": { 197 input: "gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE", 198 want: ModuleSourceRemote{ 199 PackageAddr: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH_TO_MODULE"), 200 }, 201 }, 202 "Google Cloud Storage bucket implied, archive object": { 203 input: "www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip", 204 want: ModuleSourceRemote{ 205 PackageAddr: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"), 206 }, 207 }, 208 "Google Cloud Storage bucket, archive object": { 209 input: "gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip", 210 want: ModuleSourceRemote{ 211 PackageAddr: ModulePackage("gcs::https://www.googleapis.com/storage/v1/BUCKET_NAME/PATH/TO/module.zip"), 212 }, 213 }, 214 215 "Amazon S3 bucket implied, archive object": { 216 input: "s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip", 217 want: ModuleSourceRemote{ 218 PackageAddr: ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"), 219 }, 220 }, 221 "Amazon S3 bucket, archive object": { 222 input: "s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip", 223 want: ModuleSourceRemote{ 224 PackageAddr: ModulePackage("s3::https://s3-eu-west-1.amazonaws.com/examplecorp-terraform-modules/vpc.zip"), 225 }, 226 }, 227 228 "HTTP URL": { 229 input: "http://example.com/module", 230 want: ModuleSourceRemote{ 231 PackageAddr: ModulePackage("http://example.com/module"), 232 }, 233 }, 234 "HTTPS URL": { 235 input: "https://example.com/module", 236 want: ModuleSourceRemote{ 237 PackageAddr: ModulePackage("https://example.com/module"), 238 }, 239 }, 240 "HTTPS URL, archive file": { 241 input: "https://example.com/module.zip", 242 want: ModuleSourceRemote{ 243 PackageAddr: ModulePackage("https://example.com/module.zip"), 244 }, 245 }, 246 "HTTPS URL, forced archive file": { 247 input: "https://example.com/module?archive=tar", 248 want: ModuleSourceRemote{ 249 PackageAddr: ModulePackage("https://example.com/module?archive=tar"), 250 }, 251 }, 252 "HTTPS URL, forced archive file and checksum": { 253 input: "https://example.com/module?archive=tar&checksum=blah", 254 want: ModuleSourceRemote{ 255 // The query string only actually gets processed when we finally 256 // do the get, so "checksum=blah" is accepted as valid up 257 // at this parsing layer. 258 PackageAddr: ModulePackage("https://example.com/module?archive=tar&checksum=blah"), 259 }, 260 }, 261 262 "absolute filesystem path": { 263 // Although a local directory isn't really "remote", we do 264 // treat it as such because we still need to do all of the same 265 // high-level steps to work with these, even though "downloading" 266 // is replaced by a deep filesystem copy instead. 267 input: "/tmp/foo/example", 268 want: ModuleSourceRemote{ 269 PackageAddr: ModulePackage("file:///tmp/foo/example"), 270 }, 271 }, 272 "absolute filesystem path, subdir": { 273 // This is a funny situation where the user wants to use a 274 // directory elsewhere on their system as a package containing 275 // multiple modules, but the entry point is not at the root 276 // of that subtree, and so they can use the usual subdir 277 // syntax to move the package root higher in the real filesystem. 278 input: "/tmp/foo//example", 279 want: ModuleSourceRemote{ 280 PackageAddr: ModulePackage("file:///tmp/foo"), 281 Subdir: "example", 282 }, 283 }, 284 285 "subdir escaping out of package": { 286 // This is general logic for all subdir regardless of installation 287 // protocol, but we're using a filesystem path here just as an 288 // easy placeholder/ 289 input: "/tmp/foo//example/../../invalid", 290 wantErr: `subdirectory path "../invalid" leads outside of the module package`, 291 }, 292 293 "relative path without the needed prefix": { 294 input: "boop/bloop", 295 // For this case we return a generic error message from the addrs 296 // layer, but using a specialized error type which our module 297 // installer checks for and produces an extra hint for users who 298 // were intending to write a local path which then got 299 // misinterpreted as a remote source due to the missing prefix. 300 // However, the main message is generic here because this is really 301 // just a general "this string doesn't match any of our source 302 // address patterns" situation, not _necessarily_ about relative 303 // local paths. 304 wantErr: `Terraform cannot detect a supported external module source type for boop/bloop`, 305 }, 306 307 "go-getter will accept all sorts of garbage": { 308 input: "dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg", 309 want: ModuleSourceRemote{ 310 // Unfortunately go-getter doesn't actually reject a totally 311 // invalid address like this until getting time, as long as 312 // it looks somewhat like a URL. 313 PackageAddr: ModulePackage("dfgdfgsd:dgfhdfghdfghdfg/dfghdfghdfg"), 314 }, 315 }, 316 } 317 318 for name, test := range tests { 319 t.Run(name, func(t *testing.T) { 320 addr, err := ParseModuleSource(test.input) 321 322 if test.wantErr != "" { 323 switch { 324 case err == nil: 325 t.Errorf("unexpected success\nwant error: %s", test.wantErr) 326 case err.Error() != test.wantErr: 327 t.Errorf("wrong error messages\ngot: %s\nwant: %s", err.Error(), test.wantErr) 328 } 329 return 330 } 331 332 if err != nil { 333 t.Fatalf("unexpected error: %s", err.Error()) 334 } 335 336 if diff := cmp.Diff(addr, test.want); diff != "" { 337 t.Errorf("wrong result\n%s", diff) 338 } 339 }) 340 } 341 342 } 343 344 func TestModuleSourceRemoteFromRegistry(t *testing.T) { 345 t.Run("both have subdir", func(t *testing.T) { 346 remote := ModuleSourceRemote{ 347 PackageAddr: ModulePackage("boop"), 348 Subdir: "foo", 349 } 350 registry := ModuleSourceRegistry{ 351 Subdir: "bar", 352 } 353 gotAddr := remote.FromRegistry(registry) 354 if remote.Subdir != "foo" { 355 t.Errorf("FromRegistry modified the reciever; should be pure function") 356 } 357 if registry.Subdir != "bar" { 358 t.Errorf("FromRegistry modified the given address; should be pure function") 359 } 360 if got, want := gotAddr.Subdir, "foo/bar"; got != want { 361 t.Errorf("wrong resolved subdir\ngot: %s\nwant: %s", got, want) 362 } 363 }) 364 t.Run("only remote has subdir", func(t *testing.T) { 365 remote := ModuleSourceRemote{ 366 PackageAddr: ModulePackage("boop"), 367 Subdir: "foo", 368 } 369 registry := ModuleSourceRegistry{ 370 Subdir: "", 371 } 372 gotAddr := remote.FromRegistry(registry) 373 if remote.Subdir != "foo" { 374 t.Errorf("FromRegistry modified the reciever; should be pure function") 375 } 376 if registry.Subdir != "" { 377 t.Errorf("FromRegistry modified the given address; should be pure function") 378 } 379 if got, want := gotAddr.Subdir, "foo"; got != want { 380 t.Errorf("wrong resolved subdir\ngot: %s\nwant: %s", got, want) 381 } 382 }) 383 t.Run("only registry has subdir", func(t *testing.T) { 384 remote := ModuleSourceRemote{ 385 PackageAddr: ModulePackage("boop"), 386 Subdir: "", 387 } 388 registry := ModuleSourceRegistry{ 389 Subdir: "bar", 390 } 391 gotAddr := remote.FromRegistry(registry) 392 if remote.Subdir != "" { 393 t.Errorf("FromRegistry modified the reciever; should be pure function") 394 } 395 if registry.Subdir != "bar" { 396 t.Errorf("FromRegistry modified the given address; should be pure function") 397 } 398 if got, want := gotAddr.Subdir, "bar"; got != want { 399 t.Errorf("wrong resolved subdir\ngot: %s\nwant: %s", got, want) 400 } 401 }) 402 } 403 404 func TestParseModuleSourceRegistry(t *testing.T) { 405 // We test parseModuleSourceRegistry alone here, in addition to testing 406 // it indirectly as part of TestParseModuleSource, because general 407 // module parsing unfortunately eats all of the error situations from 408 // registry passing by falling back to trying for a direct remote package 409 // address. 410 411 // Historical note: These test cases were originally derived from the 412 // ones in the old internal/registry/regsrc package that the 413 // ModuleSourceRegistry type is replacing. That package had the notion 414 // of "normalized" addresses as separate from the original user input, 415 // but this new implementation doesn't try to preserve the original 416 // user input at all, and so the main string output is always normalized. 417 // 418 // That package also had some behaviors to turn the namespace, name, and 419 // remote system portions into lowercase, but apparently we didn't 420 // actually make use of that in the end and were preserving the case 421 // the user provided in the input, and so for backward compatibility 422 // we're continuing to do that here, at the expense of now making the 423 // "ForDisplay" output case-preserving where its predecessor in the 424 // old package wasn't. The main Terraform Registry at registry.terraform.io 425 // is itself case-insensitive anyway, so our case-preserving here is 426 // entirely for the benefit of existing third-party registry 427 // implementations that might be case-sensitive, which we must remain 428 // compatible with now. 429 430 tests := map[string]struct { 431 input string 432 wantString string 433 wantForDisplay string 434 wantForProtocol string 435 wantErr string 436 }{ 437 "public registry": { 438 input: `hashicorp/consul/aws`, 439 wantString: `registry.terraform.io/hashicorp/consul/aws`, 440 wantForDisplay: `hashicorp/consul/aws`, 441 wantForProtocol: `hashicorp/consul/aws`, 442 }, 443 "public registry with subdir": { 444 input: `hashicorp/consul/aws//foo`, 445 wantString: `registry.terraform.io/hashicorp/consul/aws//foo`, 446 wantForDisplay: `hashicorp/consul/aws//foo`, 447 wantForProtocol: `hashicorp/consul/aws`, 448 }, 449 "public registry using explicit hostname": { 450 input: `registry.terraform.io/hashicorp/consul/aws`, 451 wantString: `registry.terraform.io/hashicorp/consul/aws`, 452 wantForDisplay: `hashicorp/consul/aws`, 453 wantForProtocol: `hashicorp/consul/aws`, 454 }, 455 "public registry with mixed case names": { 456 input: `HashiCorp/Consul/aws`, 457 wantString: `registry.terraform.io/HashiCorp/Consul/aws`, 458 wantForDisplay: `HashiCorp/Consul/aws`, 459 wantForProtocol: `HashiCorp/Consul/aws`, 460 }, 461 "private registry with non-standard port": { 462 input: `Example.com:1234/HashiCorp/Consul/aws`, 463 wantString: `example.com:1234/HashiCorp/Consul/aws`, 464 wantForDisplay: `example.com:1234/HashiCorp/Consul/aws`, 465 wantForProtocol: `HashiCorp/Consul/aws`, 466 }, 467 "private registry with IDN hostname": { 468 input: `Испытание.com/HashiCorp/Consul/aws`, 469 wantString: `испытание.com/HashiCorp/Consul/aws`, 470 wantForDisplay: `испытание.com/HashiCorp/Consul/aws`, 471 wantForProtocol: `HashiCorp/Consul/aws`, 472 }, 473 "private registry with IDN hostname and non-standard port": { 474 input: `Испытание.com:1234/HashiCorp/Consul/aws//Foo`, 475 wantString: `испытание.com:1234/HashiCorp/Consul/aws//Foo`, 476 wantForDisplay: `испытание.com:1234/HashiCorp/Consul/aws//Foo`, 477 wantForProtocol: `HashiCorp/Consul/aws`, 478 }, 479 "invalid hostname": { 480 input: `---.com/HashiCorp/Consul/aws`, 481 wantErr: `invalid module registry hostname "---.com"; internationalized domain names must be given as direct unicode characters, not in punycode`, 482 }, 483 "hostname with only one label": { 484 // This was historically forbidden in our initial implementation, 485 // so we keep it forbidden to avoid newly interpreting such 486 // addresses as registry addresses rather than remote source 487 // addresses. 488 input: `foo/var/baz/qux`, 489 wantErr: `invalid module registry hostname: must contain at least one dot`, 490 }, 491 "invalid target system": { 492 input: `foo/var/no-no-no`, 493 wantErr: `invalid target system "no-no-no": must be between one and 64 ASCII letters or digits`, 494 }, 495 "invalid namespace": { 496 input: `boop!/var/baz`, 497 wantErr: `invalid namespace "boop!": must be between one and 64 characters, including ASCII letters, digits, dashes, and underscores, where dashes and underscores may not be the prefix or suffix`, 498 }, 499 "missing part with explicit hostname": { 500 input: `foo.com/var/baz`, 501 wantErr: `source address must have three more components after the hostname: the namespace, the name, and the target system`, 502 }, 503 "errant query string": { 504 input: `foo/var/baz?otherthing`, 505 wantErr: `module registry addresses may not include a query string portion`, 506 }, 507 "github.com": { 508 // We don't allow using github.com like a module registry because 509 // that conflicts with the historically-supported shorthand for 510 // installing directly from GitHub-hosted git repositories. 511 input: `github.com/HashiCorp/Consul/aws`, 512 wantErr: `can't use "github.com" as a module registry host, because it's reserved for installing directly from version control repositories`, 513 }, 514 "bitbucket.org": { 515 // We don't allow using bitbucket.org like a module registry because 516 // that conflicts with the historically-supported shorthand for 517 // installing directly from BitBucket-hosted git repositories. 518 input: `bitbucket.org/HashiCorp/Consul/aws`, 519 wantErr: `can't use "bitbucket.org" as a module registry host, because it's reserved for installing directly from version control repositories`, 520 }, 521 } 522 523 for name, test := range tests { 524 t.Run(name, func(t *testing.T) { 525 addr, err := parseModuleSourceRegistry(test.input) 526 527 if test.wantErr != "" { 528 switch { 529 case err == nil: 530 t.Errorf("unexpected success\nwant error: %s", test.wantErr) 531 case err.Error() != test.wantErr: 532 t.Errorf("wrong error messages\ngot: %s\nwant: %s", err.Error(), test.wantErr) 533 } 534 return 535 } 536 537 if err != nil { 538 t.Fatalf("unexpected error: %s", err.Error()) 539 } 540 541 if got, want := addr.String(), test.wantString; got != want { 542 t.Errorf("wrong String() result\ngot: %s\nwant: %s", got, want) 543 } 544 if got, want := addr.ForDisplay(), test.wantForDisplay; got != want { 545 t.Errorf("wrong ForDisplay() result\ngot: %s\nwant: %s", got, want) 546 } 547 if got, want := addr.PackageAddr.ForRegistryProtocol(), test.wantForProtocol; got != want { 548 t.Errorf("wrong ForRegistryProtocol() result\ngot: %s\nwant: %s", got, want) 549 } 550 }) 551 } 552 }