diff --git a/.changelog/1003.txt b/.changelog/1003.txt new file mode 100644 index 00000000000..2a7cfbef32e --- /dev/null +++ b/.changelog/1003.txt @@ -0,0 +1,3 @@ +```release-note:note +The underlying `terraform-plugin-log` dependency has been updated to v0.6.0, which includes log filtering support and breaking changes of `With()` to `SetField()` function names. Any provider logging which calls those functions may require updates. +``` diff --git a/.changelog/972.txt b/.changelog/972.txt new file mode 100644 index 00000000000..e5660e47a82 --- /dev/null +++ b/.changelog/972.txt @@ -0,0 +1,11 @@ +```release-note:note +helper/resource: Provider references or external installation can now be handled at either the `TestCase` or `TestStep` level. Using the `TestStep` handling, advanced use cases are now enabled such as state upgrade acceptance testing. +``` + +```release-note:enhancement +helper/resource: Added `TestStep` type `ExternalProviders`, `ProtoV5ProviderFactories`, `ProtoV6ProviderFactories`, and `ProviderFactories` fields +``` + +```release-note:bug +helper/resource: Removed extraneous `terraform state show` command when not using the `TestStep` type `Taint` field +``` diff --git a/.changelog/983.txt b/.changelog/983.txt new file mode 100644 index 00000000000..5ba135110c6 --- /dev/null +++ b/.changelog/983.txt @@ -0,0 +1,3 @@ +```release-note:bug +helper/resource: Ensured errors are always logged. +``` diff --git a/.changelog/993.txt b/.changelog/993.txt new file mode 100644 index 00000000000..cfea5149006 --- /dev/null +++ b/.changelog/993.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +helper/resource: Added `TF_ACC_LOG`, `TF_LOG_CORE`, and `TF_LOG_PROVIDER` environment variable handling for Terraform versions 0.15 and later +``` diff --git a/.changelog/996.txt b/.changelog/996.txt new file mode 100644 index 00000000000..1c625fef999 --- /dev/null +++ b/.changelog/996.txt @@ -0,0 +1,3 @@ +```release-note:enhancement +helper/schema: Added sdk.proto logger request duration and response diagnostics logging +``` diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 57849472a51..922ee27f4c9 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,2 +1 @@ * @hashicorp/terraform-devex -/website/**/*.mdx @hashicorp/terraform-devex @hashicorp/team-tw-packer-and-terraform diff --git a/.github/workflows/add-content-to-project.yml b/.github/workflows/add-content-to-project.yml index f7e75133d0b..02bca6e78d6 100644 --- a/.github/workflows/add-content-to-project.yml +++ b/.github/workflows/add-content-to-project.yml @@ -30,7 +30,7 @@ jobs: custom_field_values: '[{\"name\":\"Priority\",\"type\":\"single_select\",\"value\":\"Triage Next\"}]' - name: "Set Pull Request to 'Priority = Triage Next'" uses: leonsteinhaeuser/project-beta-automations@v1.2.1 - if: github.event_name == 'pull_request' + if: github.event_name == 'pull_request_target' with: gh_token: ${{ secrets.TF_DEVEX_PROJECT_GITHUB_TOKEN }} organization: "hashicorp" diff --git a/.github/workflows/ci-github-actions.yml b/.github/workflows/ci-github-actions.yml index ba790468e53..0b6e2d3ea27 100644 --- a/.github/workflows/ci-github-actions.yml +++ b/.github/workflows/ci-github-actions.yml @@ -14,11 +14,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - id: go-version - # Reference: https://github.com/actions/setup-go/issues/23 - run: echo "::set-output name=version::$(cat ./.go-version)" - uses: actions/setup-go@v3 with: - go-version: ${{ steps.go-version.outputs.version }} + go-version-file: 'go.mod' - run: go install github.com/rhysd/actionlint/cmd/actionlint@latest - run: actionlint diff --git a/.github/workflows/ci-go.yml b/.github/workflows/ci-go.yml index 1252fc6ad44..56a997cd6e7 100644 --- a/.github/workflows/ci-go.yml +++ b/.github/workflows/ci-go.yml @@ -6,7 +6,6 @@ on: paths: - .github/workflows/ci-go.yml - .golangci.yml - - .go-version - go.mod - '**.go' @@ -18,16 +17,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - id: go-version - # Reference: https://github.com/actions/setup-go/issues/23 - run: echo "::set-output name=version::$(cat ./.go-version)" - uses: actions/setup-go@v3 with: - go-version: ${{ steps.go-version.outputs.version }} + go-version-file: 'go.mod' - run: go mod download - - uses: golangci/golangci-lint-action@v3.1.0 - with: - skip-go-installation: true + - uses: golangci/golangci-lint-action@v3.2.0 terraform-provider-corner: defaults: run: @@ -39,13 +33,9 @@ jobs: with: path: terraform-provider-corner repository: hashicorp/terraform-provider-corner - - id: go-version - # Reference: https://github.com/actions/setup-go/issues/23 - run: echo "::set-output name=version::$(cat ./.go-version)" - working-directory: . - uses: actions/setup-go@v3 with: - go-version: ${{ steps.go-version.outputs.version }} + go-version-file: 'go.mod' - run: go mod edit -replace=github.com/hashicorp/terraform-plugin-sdk/v2=../ - run: go mod tidy - run: go test -v ./... diff --git a/.github/workflows/ci-goreleaser.yml b/.github/workflows/ci-goreleaser.yml index ecf774075a8..c47009a2376 100644 --- a/.github/workflows/ci-goreleaser.yml +++ b/.github/workflows/ci-goreleaser.yml @@ -14,12 +14,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - id: go-version - # Reference: https://github.com/actions/setup-go/issues/23 - run: echo "::set-output name=version::$(cat ./.go-version)" - uses: actions/setup-go@v3 with: - go-version: ${{ steps.go-version.outputs.version }} - - uses: goreleaser/goreleaser-action@v2 + go-version-file: 'go.mod' + - uses: goreleaser/goreleaser-action@v3 with: args: check diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 9f37e2afae5..b245304fcda 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -19,16 +19,13 @@ jobs: with: # Required for release notes fetch-depth: 0 - - id: go-version - # Reference: https://github.com/actions/setup-go/issues/23 - run: echo "::set-output name=version::$(cat ./.go-version)" - uses: actions/setup-go@v3 with: - go-version: ${{ steps.go-version.outputs.version }} + go-version-file: 'go.mod' - name: Generate Release Notes # Fetch CHANGELOG.md contents up to Git tag prior to this release, skipping top two lines run: sed -n -e "1{/# /d;}" -e "2{/^$/d;}" -e "/# $(git describe --abbrev=0 --exclude="$(git describe --abbrev=0 --match='v*.*.*' --tags)" --match='v*.*.*' --tags | tr -d v)/q;p" CHANGELOG.md > /tmp/release-notes.txt - - uses: goreleaser/goreleaser-action@v2 + - uses: goreleaser/goreleaser-action@v3 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: diff --git a/.go-version b/.go-version deleted file mode 100644 index a23a1564cd4..00000000000 --- a/.go-version +++ /dev/null @@ -1 +0,0 @@ -1.17.8 diff --git a/CHANGELOG.md b/CHANGELOG.md index 988402d7aa1..e5fd68af5d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,34 @@ +# 2.19.0 (July 15, 2022) + +NOTES: + +* The underlying `terraform-plugin-log` dependency has been updated to v0.6.0, which includes log filtering support and breaking changes of `With()` to `SetField()` function names. Any provider logging which calls those functions may require updates. ([#1003](https://github.com/hashicorp/terraform-plugin-sdk/issues/1003)) + +# 2.18.0 (July 5, 2022) + +ENHANCEMENTS: + +* helper/resource: Added `TF_ACC_LOG`, `TF_LOG_CORE`, and `TF_LOG_PROVIDER` environment variable handling for Terraform versions 0.15 and later ([#993](https://github.com/hashicorp/terraform-plugin-sdk/issues/993)) +* helper/schema: Added sdk.proto logger request duration and response diagnostics logging ([#996](https://github.com/hashicorp/terraform-plugin-sdk/issues/996)) + +BUG FIXES: + +* helper/resource: Ensured errors are always logged. ([#983](https://github.com/hashicorp/terraform-plugin-sdk/issues/983)) + +# 2.17.0 (May 31, 2022) + +NOTES: + +* helper/resource: Provider references or external installation can now be handled at either the `TestCase` or `TestStep` level. Using the `TestStep` handling, advanced use cases are now enabled such as state upgrade acceptance testing. ([#972](https://github.com/hashicorp/terraform-plugin-sdk/issues/972)) + +ENHANCEMENTS: + +* helper/resource: Added `TestStep` type `ExternalProviders`, `ProtoV5ProviderFactories`, `ProtoV6ProviderFactories`, and `ProviderFactories` fields ([#972](https://github.com/hashicorp/terraform-plugin-sdk/issues/972)) + +BUG FIXES: + +* helper/resource: Removed extraneous `terraform state show` command when not using the `TestStep` type `Taint` field ([#972](https://github.com/hashicorp/terraform-plugin-sdk/issues/972)) + # 2.16.0 (May 10, 2022) ENHANCEMENTS: diff --git a/README.md b/README.md index 07a58aeb712..2cf671aa00b 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,9 @@ # Terraform Plugin SDK -This SDK enables building Terraform plugin which allows Terraform's users to manage existing and popular service providers as well as custom in-house solutions. +This SDK enables building Terraform plugin which allows Terraform's users to manage existing and popular service providers as well as custom in-house solutions. The SDK is stable and broadly used across the provider ecosystem. + +For new provider development it is recommended to investigate [`terraform-plugin-framework`](https://github.com/hashicorp/terraform-plugin-framework), which is a reimagined provider SDK that supports additional capabilities. Refer to the [Which SDK Should I Use?](https://terraform.io/docs/plugin/which-sdk.html) documentation for more information about differences between SDKs. Terraform itself is a tool for building, changing, and versioning infrastructure safely and efficiently. You can find more about Terraform on its [website](https://www.terraform.io) and [its GitHub repository](https://github.com/hashicorp/terraform). diff --git a/go.mod b/go.mod index 19c5c56cc8b..06c489ab0e6 100644 --- a/go.mod +++ b/go.mod @@ -8,53 +8,53 @@ require ( github.com/davecgh/go-spew v1.1.1 github.com/google/go-cmp v0.5.8 github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 - github.com/hashicorp/go-hclog v1.2.0 + github.com/hashicorp/go-hclog v1.2.1 github.com/hashicorp/go-multierror v1.1.1 - github.com/hashicorp/go-plugin v1.4.3 + github.com/hashicorp/go-plugin v1.4.4 github.com/hashicorp/go-uuid v1.0.3 - github.com/hashicorp/go-version v1.4.0 - github.com/hashicorp/hc-install v0.3.2 - github.com/hashicorp/hcl/v2 v2.12.0 + github.com/hashicorp/go-version v1.6.0 + github.com/hashicorp/hc-install v0.4.0 + github.com/hashicorp/hcl/v2 v2.13.0 github.com/hashicorp/logutils v1.0.0 - github.com/hashicorp/terraform-exec v0.16.1 - github.com/hashicorp/terraform-json v0.13.0 - github.com/hashicorp/terraform-plugin-go v0.9.0 - github.com/hashicorp/terraform-plugin-log v0.4.0 + github.com/hashicorp/terraform-exec v0.17.2 + github.com/hashicorp/terraform-json v0.14.0 + github.com/hashicorp/terraform-plugin-go v0.12.0 + github.com/hashicorp/terraform-plugin-log v0.6.0 github.com/mitchellh/copystructure v1.2.0 github.com/mitchellh/go-testing-interface v1.14.1 github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/reflectwalk v1.0.2 github.com/zclconf/go-cty v1.10.0 - golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e + golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 golang.org/x/tools v0.0.0-20200713011307-fd294ab11aed ) require ( github.com/agext/levenshtein v1.2.2 // indirect github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect - github.com/fatih/color v1.7.0 // indirect + github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.2 // indirect github.com/hashicorp/errwrap v1.0.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 // indirect + github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c // indirect github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect github.com/kylelemons/godebug v1.1.0 // indirect - github.com/mattn/go-colorable v0.1.4 // indirect - github.com/mattn/go-isatty v0.0.10 // indirect + github.com/mattn/go-colorable v0.1.12 // indirect + github.com/mattn/go-isatty v0.0.14 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect github.com/vmihailenco/msgpack/v4 v4.3.12 // indirect github.com/vmihailenco/tagparser v0.1.1 // indirect golang.org/x/mod v0.3.0 // indirect - golang.org/x/net v0.0.0-20210326060303-6b1517762897 // indirect - golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 // indirect - golang.org/x/text v0.3.5 // indirect + golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect + golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 // indirect + golang.org/x/text v0.3.7 // indirect golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect google.golang.org/appengine v1.6.6 // indirect google.golang.org/genproto v0.0.0-20200711021454-869866162049 // indirect - google.golang.org/grpc v1.45.0 // indirect + google.golang.org/grpc v1.48.0 // indirect google.golang.org/protobuf v1.28.0 // indirect ) diff --git a/go.sum b/go.sum index 134295fb39f..fe6c0759bc0 100644 --- a/go.sum +++ b/go.sum @@ -30,8 +30,8 @@ github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDk github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -43,10 +43,11 @@ github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymF github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= +github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys= github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= +github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= +github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= github.com/flynn/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:xEzjJPgXI435gkrCt3MPfRiAkVrwSbHsst4LCFVfpJc= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/gliderlabs/ssh v0.2.2/go.mod h1:U7qILu1NlMHj9FlMhZLlkCdDnU1DBEAqr0aevW3Awn0= @@ -86,7 +87,6 @@ github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= github.com/google/go-cmp v0.5.8 h1:e6P7q2lk1O+qJJb4BtCQXlK8vWEO8V1ZeuEdJNOqZyg= github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -102,37 +102,35 @@ github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/S github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= -github.com/hashicorp/go-hclog v1.2.0 h1:La19f8d7WIlm4ogzNHB0JGqs5AUDAZ2UfCY4sJXcJdM= -github.com/hashicorp/go-hclog v1.2.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.2.1 h1:YQsLlGDJgwhXFpucSPyVbCBviQtjlHv3jLTlp8YmtEw= +github.com/hashicorp/go-hclog v1.2.1/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= -github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= +github.com/hashicorp/go-plugin v1.4.4 h1:NVdrSdFRt3SkZtNckJ6tog7gbpRrcbOjQi/rgF7JYWQ= +github.com/hashicorp/go-plugin v1.4.4/go.mod h1:viDMjcLJuDui6pXb8U4HVfb8AamCWhHGUjr2IrTF67s= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.3.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/go-version v1.4.0 h1:aAQzgqIrRKRa7w75CKpbBxYsmUoPjzVm1W59ca1L0J4= -github.com/hashicorp/go-version v1.4.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.3.1/go.mod h1:3LCdWcCDS1gaHC9mhHCGbkYfoY6vdsKohGjugbZdZak= -github.com/hashicorp/hc-install v0.3.2 h1:oiQdJZvXmkNcRcEOOfM5n+VTsvNjWQeOjfAoO6dKSH8= -github.com/hashicorp/hc-install v0.3.2/go.mod h1:xMG6Tr8Fw1WFjlxH0A9v61cW15pFwgEGqEz0V4jisHs= -github.com/hashicorp/hcl/v2 v2.12.0 h1:PsYxySWpMD4KPaoJLnsHwtK5Qptvj/4Q6s0t4sUxZf4= -github.com/hashicorp/hcl/v2 v2.12.0/go.mod h1:FwWsfWEjyV/CMj8s/gqAuiviY72rJ1/oayI9WftqcKg= +github.com/hashicorp/go-version v1.5.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= +github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/hc-install v0.4.0 h1:cZkRFr1WVa0Ty6x5fTvL1TuO1flul231rWkGH92oYYk= +github.com/hashicorp/hc-install v0.4.0/go.mod h1:5d155H8EC5ewegao9A4PUTMNPZaq+TbOzkJJZ4vrXeI= +github.com/hashicorp/hcl/v2 v2.13.0 h1:0Apadu1w6M11dyGFxWnmhhcMjkbAiKCv7G1r/2QgCNc= +github.com/hashicorp/hcl/v2 v2.13.0/go.mod h1:e4z5nxYlWNPdDSNYX+ph14EvWYMFm3eP0zIUqPc2jr0= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.16.1 h1:NAwZFJW2L2SaCBVZoVaH8LPImLOGbPLkSHy0IYbs2uE= -github.com/hashicorp/terraform-exec v0.16.1/go.mod h1:aj0lVshy8l+MHhFNoijNHtqTJQI3Xlowv5EOsEaGO7M= -github.com/hashicorp/terraform-json v0.13.0 h1:Li9L+lKD1FO5RVFRM1mMMIBDoUHslOniyEi5CM+FWGY= -github.com/hashicorp/terraform-json v0.13.0/go.mod h1:y5OdLBCT+rxbwnpxZs9kGL7R9ExU76+cpdY8zHwoazk= -github.com/hashicorp/terraform-plugin-go v0.9.0 h1:FvLY/3z4SNVatPZdoFcyrlNbCar+WyyOTv5X4Tp+WZc= -github.com/hashicorp/terraform-plugin-go v0.9.0/go.mod h1:EawBkgjBWNf7jiKnVoyDyF39OSV+u6KUX+Y73EPj3oM= -github.com/hashicorp/terraform-plugin-log v0.3.0/go.mod h1:EjueSP/HjlyFAsDqt+okpCPjkT4NDynAe32AeDC4vps= -github.com/hashicorp/terraform-plugin-log v0.4.0 h1:F3eVnm8r2EfQCe2k9blPIiF/r2TT01SHijXnS7bujvc= -github.com/hashicorp/terraform-plugin-log v0.4.0/go.mod h1:9KclxdunFownr4pIm1jdmwKRmE4d6HVG2c9XDq47rpg= -github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 h1:1FGtlkJw87UsTMg5s8jrekrHmUPUJaMcu6ELiVhQrNw= -github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896/go.mod h1:bzBPnUIkI0RxauU8Dqo+2KrZZ28Cf48s8V6IHt3p4co= +github.com/hashicorp/terraform-exec v0.17.2 h1:EU7i3Fh7vDUI9nNRdMATCEfnm9axzTnad8zszYZ73Go= +github.com/hashicorp/terraform-exec v0.17.2/go.mod h1:tuIbsL2l4MlwwIZx9HPM+LOV9vVyEfBYu2GsO1uH3/8= +github.com/hashicorp/terraform-json v0.14.0 h1:sh9iZ1Y8IFJLx+xQiKHGud6/TSUCM0N8e17dKDpqV7s= +github.com/hashicorp/terraform-json v0.14.0/go.mod h1:5A9HIWPkk4e5aeeXIBbkcOvaZbIYnAIkEyqP2pNSckM= +github.com/hashicorp/terraform-plugin-go v0.12.0 h1:6wW9mT1dSs0Xq4LR6HXj1heQ5ovr5GxXNJwkErZzpJw= +github.com/hashicorp/terraform-plugin-go v0.12.0/go.mod h1:kwhmaWHNDvT1B3QiSJdAtrB/D4RaKSY/v3r2BuoWK4M= +github.com/hashicorp/terraform-plugin-log v0.6.0 h1:/Vq78uSIdUSZ3iqDc9PESKtwt8YqNKN6u+khD+lLjuw= +github.com/hashicorp/terraform-plugin-log v0.6.0/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4= +github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c h1:D8aRO6+mTqHfLsK/BC3j5OAoogv1WLRWzY1AaTo3rBg= +github.com/hashicorp/terraform-registry-address v0.0.0-20220623143253-7d51757b572c/go.mod h1:Wn3Na71knbXc1G8Lh+yu/dQWWJeFQEpDeJMtWMtlmNI= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0= github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg= github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= @@ -159,11 +157,15 @@ github.com/kylelemons/godebug v0.0.0-20170820004349-d65d576e9348/go.mod h1:B69LE github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= -github.com/mattn/go-colorable v0.1.4 h1:snbPLB8fVfU9iwbbo30TPtbLRzwWu6aJS6Xh4eaaviA= github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= +github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= +github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.10 h1:qxFzApOv4WsAL965uUPIsXzAKCZxN2p9UqdhFS4ZW10= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= +github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= @@ -202,8 +204,9 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= +github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= @@ -217,20 +220,19 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s= github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8= github.com/zclconf/go-cty v1.8.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= -github.com/zclconf/go-cty v1.9.1/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty v1.10.0 h1:mp9ZXQeIcN8kAwuqorjH+Q+njbJKjLrvB2yIh4q7U+0= github.com/zclconf/go-cty v1.10.0/go.mod h1:vVKLxnk3puL4qRAv72AO+W99LUD4da90g3uUAzyuvAk= github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= golang.org/x/crypto v0.0.0-20190219172222-a4c6cb3142f2/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190426145343-a29dc8fdc734/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e h1:gsTQYXdTw2Gq7RBsWvlQ91b+aEQ6bXFUngBGuR8sPpI= golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167 h1:O8uGbHCqlTp2P6QJSLmCojM4mN6UemYv8K+dCnmHmu0= +golang.org/x/crypto v0.0.0-20220517005047-85d78b3ac167/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -251,10 +253,12 @@ golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= +golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210326060303-6b1517762897 h1:KrsHThm5nFk34YtATK1LsThyGhGbGe1olrte/HInHvs= golang.org/x/net v0.0.0-20210326060303-6b1517762897/go.mod h1:uSPa2vr4CLtc/ILN5odXGNXS6mhrKVzTaCXzk9m6W3k= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 h1:CIJ76btIcR3eFI5EgSo6k1qKw9KJexJuRLI9G7Hp5wE= +golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -268,25 +272,34 @@ golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502175342-a43fa875dd82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210324051608-47abb6519492/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210502180810-71e4cd670f79/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1 h1:SrN+KX8Art/Sf4HNj6Zcz06G7VEz+7w9tdXTPOZ7+l4= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6 h1:nonptSpoQ4vQjyraW20DXPAglgQfVnM9ZC6MmNLMR60= +golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5 h1:i6eZZ+zk0SOf0xgBpEpPD18qWcJda6q1sxt3S0kzyUQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= @@ -320,8 +333,8 @@ google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8 google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.45.0 h1:NEpgUqV3Z+ZjkqMsxMg11IaDrXY4RY6CQukSGK0uI1M= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= +google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= +google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY= google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= @@ -350,7 +363,8 @@ gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= diff --git a/helper/resource/plugin.go b/helper/resource/plugin.go index d9bc172ea9a..9e52348d69d 100644 --- a/helper/resource/plugin.go +++ b/helper/resource/plugin.go @@ -19,17 +19,108 @@ import ( testing "github.com/mitchellh/go-testing-interface" ) +// protov5ProviderFactory is a function which is called to start a protocol +// version 5 provider server. +type protov5ProviderFactory func() (tfprotov5.ProviderServer, error) + +// protov5ProviderFactories is a mapping of provider addresses to provider +// factory for protocol version 5 provider servers. +type protov5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error) + +// merge combines provider factories. +// +// In case of an overlapping entry, the later entry will overwrite the previous +// value. +func (pf protov5ProviderFactories) merge(otherPfs ...protov5ProviderFactories) protov5ProviderFactories { + result := make(protov5ProviderFactories) + + for name, providerFactory := range pf { + result[name] = providerFactory + } + + for _, otherPf := range otherPfs { + for name, providerFactory := range otherPf { + result[name] = providerFactory + } + } + + return result +} + +// protov6ProviderFactory is a function which is called to start a protocol +// version 6 provider server. +type protov6ProviderFactory func() (tfprotov6.ProviderServer, error) + +// protov6ProviderFactories is a mapping of provider addresses to provider +// factory for protocol version 6 provider servers. +type protov6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error) + +// merge combines provider factories. +// +// In case of an overlapping entry, the later entry will overwrite the previous +// value. +func (pf protov6ProviderFactories) merge(otherPfs ...protov6ProviderFactories) protov6ProviderFactories { + result := make(protov6ProviderFactories) + + for name, providerFactory := range pf { + result[name] = providerFactory + } + + for _, otherPf := range otherPfs { + for name, providerFactory := range otherPf { + result[name] = providerFactory + } + } + + return result +} + +// sdkProviderFactory is a function which is called to start a SDK provider +// server. +type sdkProviderFactory func() (*schema.Provider, error) + +// protov6ProviderFactories is a mapping of provider addresses to provider +// factory for protocol version 6 provider servers. +type sdkProviderFactories map[string]func() (*schema.Provider, error) + +// merge combines provider factories. +// +// In case of an overlapping entry, the later entry will overwrite the previous +// value. +func (pf sdkProviderFactories) merge(otherPfs ...sdkProviderFactories) sdkProviderFactories { + result := make(sdkProviderFactories) + + for name, providerFactory := range pf { + result[name] = providerFactory + } + + for _, otherPf := range otherPfs { + for name, providerFactory := range otherPf { + result[name] = providerFactory + } + } + + return result +} + type providerFactories struct { - legacy map[string]func() (*schema.Provider, error) - protov5 map[string]func() (tfprotov5.ProviderServer, error) - protov6 map[string]func() (tfprotov6.ProviderServer, error) + legacy sdkProviderFactories + protov5 protov5ProviderFactories + protov6 protov6ProviderFactories } -func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *plugintest.WorkingDir, factories providerFactories) error { +func runProviderCommand(ctx context.Context, t testing.T, f func() error, wd *plugintest.WorkingDir, factories *providerFactories) error { // don't point to this as a test failure location // point to whatever called it t.Helper() + // This should not happen, but prevent panics just in case. + if factories == nil { + err := fmt.Errorf("Provider factories are missing to run Terraform command. Please report this bug in the testing framework.") + logging.HelperResourceError(ctx, err.Error()) + return err + } + // Run the providers in the same process as the test runner using the // reattach behavior in Terraform. This ensures we get test coverage // and enables the use of delve as a debugger. diff --git a/helper/resource/plugin_test.go b/helper/resource/plugin_test.go new file mode 100644 index 00000000000..c7389bdcb68 --- /dev/null +++ b/helper/resource/plugin_test.go @@ -0,0 +1,236 @@ +package resource + +import ( + "fmt" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestProtoV5ProviderFactoriesMerge(t *testing.T) { + t.Parallel() + + testProviderFactory1 := func() (tfprotov5.ProviderServer, error) { + return nil, nil + } + testProviderFactory2 := func() (tfprotov5.ProviderServer, error) { + return nil, nil + } + + // Function pointers do not play well with go-cmp, so convert these + // into their stringified address for comparison. + transformer := cmp.Transformer( + "protov5ProviderFactory", + func(pf protov5ProviderFactory) string { + return fmt.Sprintf("%v", pf) + }, + ) + + testCases := map[string]struct { + pf protov5ProviderFactories + others []protov5ProviderFactories + expected protov5ProviderFactories + }{ + "no-overlap": { + pf: protov5ProviderFactories{ + "test1": testProviderFactory1, + }, + others: []protov5ProviderFactories{ + { + "test2": testProviderFactory1, + }, + { + "test3": testProviderFactory1, + }, + }, + expected: protov5ProviderFactories{ + "test1": testProviderFactory1, + "test2": testProviderFactory1, + "test3": testProviderFactory1, + }, + }, + "overlap": { + pf: protov5ProviderFactories{ + "test": testProviderFactory1, + }, + others: []protov5ProviderFactories{ + { + "test": testProviderFactory1, + }, + { + "test": testProviderFactory2, + }, + }, + expected: protov5ProviderFactories{ + "test": testProviderFactory2, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.pf.merge(testCase.others...) + + if diff := cmp.Diff(got, testCase.expected, transformer); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestProtoV6ProviderFactoriesMerge(t *testing.T) { + t.Parallel() + + testProviderFactory1 := func() (tfprotov6.ProviderServer, error) { + return nil, nil + } + testProviderFactory2 := func() (tfprotov6.ProviderServer, error) { + return nil, nil + } + + // Function pointers do not play well with go-cmp, so convert these + // into their stringified address for comparison. + transformer := cmp.Transformer( + "protov6ProviderFactory", + func(pf protov6ProviderFactory) string { + return fmt.Sprintf("%v", pf) + }, + ) + + testCases := map[string]struct { + pf protov6ProviderFactories + others []protov6ProviderFactories + expected protov6ProviderFactories + }{ + "no-overlap": { + pf: protov6ProviderFactories{ + "test1": testProviderFactory1, + }, + others: []protov6ProviderFactories{ + { + "test2": testProviderFactory1, + }, + { + "test3": testProviderFactory1, + }, + }, + expected: protov6ProviderFactories{ + "test1": testProviderFactory1, + "test2": testProviderFactory1, + "test3": testProviderFactory1, + }, + }, + "overlap": { + pf: protov6ProviderFactories{ + "test": testProviderFactory1, + }, + others: []protov6ProviderFactories{ + { + "test": testProviderFactory1, + }, + { + "test": testProviderFactory2, + }, + }, + expected: protov6ProviderFactories{ + "test": testProviderFactory2, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.pf.merge(testCase.others...) + + if diff := cmp.Diff(got, testCase.expected, transformer); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestSdkProviderFactoriesMerge(t *testing.T) { + t.Parallel() + + testProviderFactory1 := func() (*schema.Provider, error) { + return nil, nil + } + testProviderFactory2 := func() (*schema.Provider, error) { + return nil, nil + } + + // Function pointers do not play well with go-cmp, so convert these + // into their stringified address for comparison. + transformer := cmp.Transformer( + "sdkProviderFactory", + func(pf sdkProviderFactory) string { + return fmt.Sprintf("%v", pf) + }, + ) + + testCases := map[string]struct { + pf sdkProviderFactories + others []sdkProviderFactories + expected sdkProviderFactories + }{ + "no-overlap": { + pf: sdkProviderFactories{ + "test1": testProviderFactory1, + }, + others: []sdkProviderFactories{ + { + "test2": testProviderFactory1, + }, + { + "test3": testProviderFactory1, + }, + }, + expected: sdkProviderFactories{ + "test1": testProviderFactory1, + "test2": testProviderFactory1, + "test3": testProviderFactory1, + }, + }, + "overlap": { + pf: sdkProviderFactories{ + "test": testProviderFactory1, + }, + others: []sdkProviderFactories{ + { + "test": testProviderFactory1, + }, + { + "test": testProviderFactory2, + }, + }, + expected: sdkProviderFactories{ + "test": testProviderFactory2, + }, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.pf.merge(testCase.others...) + + if diff := cmp.Diff(got, testCase.expected, transformer); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} diff --git a/helper/resource/testcase_providers.go b/helper/resource/testcase_providers.go new file mode 100644 index 00000000000..c09e4657cc2 --- /dev/null +++ b/helper/resource/testcase_providers.go @@ -0,0 +1,56 @@ +package resource + +import ( + "context" + "fmt" + "strings" +) + +// providerConfig takes the list of providers in a TestCase and returns a +// config with only empty provider blocks. This is useful for Import, where no +// config is provided, but the providers must be defined. +func (c TestCase) providerConfig(_ context.Context) string { + var providerBlocks, requiredProviderBlocks strings.Builder + + // [BF] The Providers field handling predates the logic being moved to this + // method. It's not entirely clear to me at this time why this field + // is being used and not the others, but leaving it here just in case + // it does have a special purpose that wasn't being unit tested prior. + for name := range c.Providers { + providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) + } + + for name, externalProvider := range c.ExternalProviders { + providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) + + if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { + continue + } + + requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) + + if externalProvider.Source != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) + } + + if externalProvider.VersionConstraint != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) + } + + requiredProviderBlocks.WriteString(" }\n") + } + + if requiredProviderBlocks.Len() > 0 { + return fmt.Sprintf(` +terraform { + required_providers { +%[1]s + } +} + +%[2]s +`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()) + } + + return providerBlocks.String() +} diff --git a/helper/resource/testcase_providers_test.go b/helper/resource/testcase_providers_test.go new file mode 100644 index 00000000000..706aba522d6 --- /dev/null +++ b/helper/resource/testcase_providers_test.go @@ -0,0 +1,298 @@ +package resource + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestTestCaseProviderConfig(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + testCase TestCase + expected string + }{ + "externalproviders-missing-source-and-versionconstraint": { + testCase: TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, + }, + }, + expected: `provider "test" {}`, + }, + "externalproviders-source-and-versionconstraint": { + testCase: TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + VersionConstraint: "1.2.3", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} + +provider "test" {} +`, + }, + "externalproviders-source": { + testCase: TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + +provider "test" {} +`, + }, + "externalproviders-versionconstraint": { + testCase: TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + VersionConstraint: "1.2.3", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + version = "1.2.3" + } + } +} + +provider "test" {} +`, + }, + "protov5providerfactories": { + testCase: TestCase{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": nil, + }, + }, + expected: ``, + }, + "protov6providerfactories": { + testCase: TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": nil, + }, + }, + expected: ``, + }, + "providerfactories": { + testCase: TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, + }, + }, + expected: ``, + }, + "providers": { + testCase: TestCase{ + Providers: map[string]*schema.Provider{ + "test": {}, + }, + }, + expected: `provider "test" {}`, + }, + } + + for name, test := range tests { + name, test := name, test + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.testCase.providerConfig(context.Background()) + + if diff := cmp.Diff(strings.TrimSpace(got), strings.TrimSpace(test.expected)); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestTest_TestCase_ExternalProviders(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + }, + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }) +} + +func TestTest_TestCase_ExternalProviders_Error(t *testing.T) { + t.Parallel() + + testExpectTFatal(t, func() { + Test(&mockT{}, TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "testnonexistent": { + Source: "registry.terraform.io/hashicorp/testnonexistent", + }, + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }) + }) +} + +func TestTest_TestCase_ProtoV5ProviderFactories(t *testing.T) { + t.Parallel() + + Test(&mockT{}, TestCase{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": func() (tfprotov5.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }) +} + +func TestTest_TestCase_ProtoV5ProviderFactories_Error(t *testing.T) { + t.Parallel() + + testExpectTFatal(t, func() { + Test(&mockT{}, TestCase{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": func() (tfprotov5.ProviderServer, error) { //nolint:unparam // required signature + return nil, fmt.Errorf("test") + }, + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }) + }) +} + +func TestTest_TestCase_ProtoV6ProviderFactories(t *testing.T) { + t.Parallel() + + Test(&mockT{}, TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }) +} + +func TestTest_TestCase_ProtoV6ProviderFactories_Error(t *testing.T) { + t.Parallel() + + testExpectTFatal(t, func() { + Test(&mockT{}, TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, fmt.Errorf("test") + }, + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }) + }) +} + +func TestTest_TestCase_ProviderFactories(t *testing.T) { + t.Parallel() + + Test(&mockT{}, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }) +} + +func TestTest_TestCase_ProviderFactories_Error(t *testing.T) { + t.Parallel() + + testExpectTFatal(t, func() { + Test(&mockT{}, TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return nil, fmt.Errorf("test") + }, + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }) + }) +} + +func TestTest_TestCase_Providers(t *testing.T) { + t.Parallel() + + Test(&mockT{}, TestCase{ + Providers: map[string]*schema.Provider{ + "test": {}, + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }) +} diff --git a/helper/resource/testcase_validate.go b/helper/resource/testcase_validate.go new file mode 100644 index 00000000000..39e5da46c9c --- /dev/null +++ b/helper/resource/testcase_validate.go @@ -0,0 +1,85 @@ +package resource + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" +) + +// hasProviders returns true if the TestCase has set any of the +// ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, +// ProviderFactories, or Providers fields. +func (c TestCase) hasProviders(_ context.Context) bool { + if len(c.ExternalProviders) > 0 { + return true + } + + if len(c.ProtoV5ProviderFactories) > 0 { + return true + } + + if len(c.ProtoV6ProviderFactories) > 0 { + return true + } + + if len(c.ProviderFactories) > 0 { + return true + } + + if len(c.Providers) > 0 { + return true + } + + return false +} + +// validate ensures the TestCase is valid based on the following criteria: +// +// - No overlapping ExternalProviders and Providers entries +// - No overlapping ExternalProviders and ProviderFactories entries +// - TestStep validations performed by the (TestStep).validate() method. +// +func (c TestCase) validate(ctx context.Context) error { + logging.HelperResourceTrace(ctx, "Validating TestCase") + + if len(c.Steps) == 0 { + err := fmt.Errorf("TestCase missing Steps") + logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + for name := range c.ExternalProviders { + if _, ok := c.Providers[name]; ok { + err := fmt.Errorf("TestCase provider %q set in both ExternalProviders and Providers", name) + logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if _, ok := c.ProviderFactories[name]; ok { + err := fmt.Errorf("TestCase provider %q set in both ExternalProviders and ProviderFactories", name) + logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + testCaseHasProviders := c.hasProviders(ctx) + + for stepIndex, step := range c.Steps { + stepNumber := stepIndex + 1 // Use 1-based index for humans + stepValidateReq := testStepValidateRequest{ + StepNumber: stepNumber, + TestCaseHasProviders: testCaseHasProviders, + } + + err := step.validate(ctx, stepValidateReq) + + if err != nil { + err := fmt.Errorf("TestStep %d/%d validation error: %w", stepNumber, len(c.Steps), err) + logging.HelperResourceError(ctx, "TestCase validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + return nil +} diff --git a/helper/resource/testcase_validate_test.go b/helper/resource/testcase_validate_test.go new file mode 100644 index 00000000000..97d00ac7c3f --- /dev/null +++ b/helper/resource/testcase_validate_test.go @@ -0,0 +1,170 @@ +package resource + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestTestCaseHasProviders(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + testCase TestCase + expected bool + }{ + "none": { + testCase: TestCase{}, + expected: false, + }, + "externalproviders": { + testCase: TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + }, + expected: true, + }, + "protov5providerfactories": { + testCase: TestCase{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": nil, // does not need to be real + }, + }, + expected: true, + }, + "protov6providerfactories": { + testCase: TestCase{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": nil, // does not need to be real + }, + }, + expected: true, + }, + "providers": { + testCase: TestCase{ + Providers: map[string]*schema.Provider{ + "test": nil, // does not need to be real + }, + }, + expected: true, + }, + "providerfactories": { + testCase: TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, // does not need to be real + }, + }, + expected: true, + }, + } + + for name, test := range tests { + name, test := name, test + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.testCase.hasProviders(context.Background()) + + if got != test.expected { + t.Errorf("expected %t, got %t", test.expected, got) + } + }) + } +} + +func TestTestCaseValidate(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + testCase TestCase + expectedError error + }{ + "valid": { + testCase: TestCase{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, // does not need to be real + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }, + }, + "externalproviders-overlapping-providers": { + testCase: TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + Providers: map[string]*schema.Provider{ + "test": nil, // does not need to be real + }, + Steps: []TestStep{ + { + Config: "", + }, + }, + }, + expectedError: fmt.Errorf("TestCase provider \"test\" set in both ExternalProviders and Providers"), + }, + "externalproviders-overlapping-providerfactories": { + testCase: TestCase{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, // does not need to be real + }, + Steps: []TestStep{ + { + Config: "", + }, + }, + }, + expectedError: fmt.Errorf("TestCase provider \"test\" set in both ExternalProviders and ProviderFactories"), + }, + "steps-missing": { + testCase: TestCase{}, + expectedError: fmt.Errorf("TestCase missing Steps"), + }, + "steps-validate-error": { + testCase: TestCase{ + Steps: []TestStep{ + {}, + }, + }, + expectedError: fmt.Errorf("TestStep 1/1 validation error"), + }, + } + + for name, test := range tests { + name, test := name, test + + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := test.testCase.validate(context.Background()) + + if err != nil { + if test.expectedError == nil { + t.Fatalf("unexpected error: %s", err) + } + + if !strings.Contains(err.Error(), test.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", test.expectedError, err) + } + } + + if err == nil && test.expectedError != nil { + t.Errorf("expected error: %s", test.expectedError) + } + }) + } +} diff --git a/helper/resource/testing.go b/helper/resource/testing.go index 45180d2c779..8cb4dbaa664 100644 --- a/helper/resource/testing.go +++ b/helper/resource/testing.go @@ -237,6 +237,7 @@ func runSweeperWithRegion(region string, s *Sweeper, sweepers map[string]*Sweepe depSweeper, ok := sweepers[dep] if !ok { + log.Printf("[ERROR] Sweeper (%s) has dependency (%s), but that sweeper was not found", s.Name, dep) return fmt.Errorf("sweeper (%s) has dependency (%s), but that sweeper was not found", s.Name, dep) } @@ -318,6 +319,11 @@ type TestCase struct { // ProviderFactories can be specified for the providers that are valid. // + // This can also be specified at the TestStep level to enable per-step + // differences in providers, however all provider specifications must + // be done either at the TestCase level or TestStep level, otherwise the + // testing framework will raise an error and fail the test. + // // These are the providers that can be referenced within the test. Each key // is an individually addressable provider. Typically you will only pass a // single value here for the provider you are testing. Aliases are not @@ -339,6 +345,11 @@ type TestCase struct { // ProtoV5ProviderFactories serves the same purpose as ProviderFactories, // but for protocol v5 providers defined using the terraform-plugin-go // ProviderServer interface. + // + // This can also be specified at the TestStep level to enable per-step + // differences in providers, however all provider specifications must + // be done either at the TestCase level or TestStep level, otherwise the + // testing framework will raise an error and fail the test. ProtoV5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error) // ProtoV6ProviderFactories serves the same purpose as ProviderFactories, @@ -346,6 +357,11 @@ type TestCase struct { // ProviderServer interface. // The version of Terraform used in acceptance testing must be greater // than or equal to v0.15.4 to use ProtoV6ProviderFactories. + // + // This can also be specified at the TestStep level to enable per-step + // differences in providers, however all provider specifications must + // be done either at the TestCase level or TestStep level, otherwise the + // testing framework will raise an error and fail the test. ProtoV6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error) // Providers is the ResourceProvider that will be under test. @@ -354,11 +370,18 @@ type TestCase struct { Providers map[string]*schema.Provider // ExternalProviders are providers the TestCase relies on that should - // be downloaded from the registry during init. This is only really - // necessary to set if you're using import, as providers in your config - // will be automatically retrieved during init. Import doesn't use a - // config, however, so we allow manually specifying them here to be - // downloaded for import tests. + // be downloaded from the registry during init. + // + // This can also be specified at the TestStep level to enable per-step + // differences in providers, however all provider specifications must + // be done either at the TestCase level or TestStep level, otherwise the + // testing framework will raise an error and fail the test. + // + // This is generally unnecessary to set at the TestCase level, however + // it has existing in the testing framework prior to the introduction of + // TestStep level specification and was only necessary for performing + // import testing where the configuration contained a provider outside the + // one under test. ExternalProviders map[string]ExternalProvider // PreventPostDestroyRefresh can be set to true for cases where data sources @@ -540,6 +563,74 @@ type TestStep struct { // fields that can't be refreshed and don't matter. ImportStateVerify bool ImportStateVerifyIgnore []string + + // ProviderFactories can be specified for the providers that are valid for + // this TestStep. When providers are specified at the TestStep level, all + // TestStep within a TestCase must declare providers. + // + // This can also be specified at the TestCase level for all TestStep, + // however all provider specifications must be done either at the TestCase + // level or TestStep level, otherwise the testing framework will raise an + // error and fail the test. + // + // These are the providers that can be referenced within the test. Each key + // is an individually addressable provider. Typically you will only pass a + // single value here for the provider you are testing. Aliases are not + // supported by the test framework, so to use multiple provider instances, + // you should add additional copies to this map with unique names. To set + // their configuration, you would reference them similar to the following: + // + // provider "my_factory_key" { + // # ... + // } + // + // resource "my_resource" "mr" { + // provider = my_factory_key + // + // # ... + // } + ProviderFactories map[string]func() (*schema.Provider, error) + + // ProtoV5ProviderFactories serves the same purpose as ProviderFactories, + // but for protocol v5 providers defined using the terraform-plugin-go + // ProviderServer interface. When providers are specified at the TestStep + // level, all TestStep within a TestCase must declare providers. + // + // This can also be specified at the TestCase level for all TestStep, + // however all provider specifications must be done either at the TestCase + // level or TestStep level, otherwise the testing framework will raise an + // error and fail the test. + ProtoV5ProviderFactories map[string]func() (tfprotov5.ProviderServer, error) + + // ProtoV6ProviderFactories serves the same purpose as ProviderFactories, + // but for protocol v6 providers defined using the terraform-plugin-go + // ProviderServer interface. + // The version of Terraform used in acceptance testing must be greater + // than or equal to v0.15.4 to use ProtoV6ProviderFactories. When providers + // are specified at the TestStep level, all TestStep within a TestCase must + // declare providers. + // + // This can also be specified at the TestCase level for all TestStep, + // however all provider specifications must be done either at the TestCase + // level or TestStep level, otherwise the testing framework will raise an + // error and fail the test. + ProtoV6ProviderFactories map[string]func() (tfprotov6.ProviderServer, error) + + // ExternalProviders are providers the TestStep relies on that should + // be downloaded from the registry during init. When providers are + // specified at the TestStep level, all TestStep within a TestCase must + // declare providers. + // + // This can also be specified at the TestCase level for all TestStep, + // however all provider specifications must be done either at the TestCase + // level or TestStep level, otherwise the testing framework will raise an + // error and fail the test. + // + // Outside specifying an earlier version of the provider under test, + // typically for state upgrader testing, this is generally only necessary + // for performing import testing where the prior TestStep configuration + // contained a provider outside the one under test. + ExternalProviders map[string]ExternalProvider } // ParallelTest performs an acceptance test on a resource, allowing concurrency @@ -593,6 +684,16 @@ func Test(t testing.T, c TestCase) { ctx := context.Background() ctx = logging.InitTestContext(ctx, t) + err := c.validate(ctx) + + if err != nil { + logging.HelperResourceError(ctx, + "Test validation error", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("Test validation error: %s", err) + } + // We only run acceptance tests if an env var is set because they're // slow and generally require some outside configuration. You can opt out // of this with OverrideEnvVar on individual TestCases. @@ -608,9 +709,6 @@ func Test(t testing.T, c TestCase) { c.ProviderFactories = map[string]func() (*schema.Provider, error){} for name, p := range c.Providers { - if _, ok := c.ProviderFactories[name]; ok { - t.Fatalf("ProviderFactory for %q already exists, cannot overwrite with Provider", name) - } prov := p c.ProviderFactories[name] = func() (*schema.Provider, error) { //nolint:unparam // required signature return prov, nil @@ -648,43 +746,6 @@ func Test(t testing.T, c TestCase) { logging.HelperResourceDebug(ctx, "Finished TestCase") } -// testProviderConfig takes the list of Providers in a TestCase and returns a -// config with only empty provider blocks. This is useful for Import, where no -// config is provided, but the providers must be defined. -func testProviderConfig(c TestCase) (string, error) { - var lines []string - var requiredProviders []string - for p := range c.Providers { - lines = append(lines, fmt.Sprintf("provider %q {}\n", p)) - } - for p, v := range c.ExternalProviders { - if _, ok := c.Providers[p]; ok { - return "", fmt.Errorf("Provider %q set in both Providers and ExternalProviders for TestCase. Must be set in only one.", p) - } - if _, ok := c.ProviderFactories[p]; ok { - return "", fmt.Errorf("Provider %q set in both ProviderFactories and ExternalProviders for TestCase. Must be set in only one.", p) - } - lines = append(lines, fmt.Sprintf("provider %q {}\n", p)) - var providerBlock string - if v.VersionConstraint != "" { - providerBlock = fmt.Sprintf("%s\nversion = %q", providerBlock, v.VersionConstraint) - } - if v.Source != "" { - providerBlock = fmt.Sprintf("%s\nsource = %q", providerBlock, v.Source) - } - if providerBlock != "" { - providerBlock = fmt.Sprintf("%s = {%s\n}\n", p, providerBlock) - } - requiredProviders = append(requiredProviders, providerBlock) - } - - if len(requiredProviders) > 0 { - lines = append([]string{fmt.Sprintf("terraform {\nrequired_providers {\n%s}\n}\n\n", strings.Join(requiredProviders, ""))}, lines...) - } - - return strings.Join(lines, ""), nil -} - // UnitTest is a helper to force the acceptance testing harness to run in the // normal unit test suite. This should only be used for resource that don't // have any external dependencies. @@ -698,10 +759,6 @@ func UnitTest(t testing.T, c TestCase) { } func testResource(c TestStep, state *terraform.State) (*terraform.ResourceState, error) { - if c.ResourceName == "" { - return nil, fmt.Errorf("ResourceName must be set in TestStep") - } - for _, m := range state.Modules { if len(m.Resources) > 0 { if v, ok := m.Resources[c.ResourceName]; ok { diff --git a/helper/resource/testing_new.go b/helper/resource/testing_new.go index 163bd3d668f..f1e607f8342 100644 --- a/helper/resource/testing_new.go +++ b/helper/resource/testing_new.go @@ -10,23 +10,17 @@ import ( tfjson "github.com/hashicorp/terraform-json" testing "github.com/mitchellh/go-testing-interface" - "github.com/hashicorp/terraform-plugin-go/tfprotov5" - "github.com/hashicorp/terraform-plugin-go/tfprotov6" - "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" "github.com/hashicorp/terraform-plugin-sdk/v2/internal/plugintest" "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func runPostTestDestroy(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, factories map[string]func() (*schema.Provider, error), v5factories map[string]func() (tfprotov5.ProviderServer, error), v6factories map[string]func() (tfprotov6.ProviderServer, error), statePreDestroy *terraform.State) error { +func runPostTestDestroy(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, providers *providerFactories, statePreDestroy *terraform.State) error { t.Helper() err := runProviderCommand(ctx, t, func() error { return wd.Destroy(ctx) - }, wd, providerFactories{ - legacy: factories, - protov5: v5factories, - protov6: v6factories}) + }, wd, providers) if err != nil { return err } @@ -55,6 +49,12 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest ctx = logging.TestTerraformPathContext(ctx, wd.GetHelper().TerraformExecPath()) ctx = logging.TestWorkingDirectoryContext(ctx, wd.GetHelper().WorkingDirectory()) + providers := &providerFactories{ + legacy: c.ProviderFactories, + protov5: c.ProtoV5ProviderFactories, + protov6: c.ProtoV6ProviderFactories, + } + defer func() { var statePreDestroy *terraform.State var err error @@ -64,10 +64,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest return err } return nil - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { logging.HelperResourceError(ctx, "Error retrieving state, there may be dangling resources", @@ -78,7 +75,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest } if !stateIsEmpty(statePreDestroy) { - err := runPostTestDestroy(ctx, t, c, wd, c.ProviderFactories, c.ProtoV5ProviderFactories, c.ProtoV6ProviderFactories, statePreDestroy) + err := runPostTestDestroy(ctx, t, c, wd, providers, statePreDestroy) if err != nil { logging.HelperResourceError(ctx, "Error running post-test destroy, there may be dangling resources", @@ -91,36 +88,28 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest wd.Close() }() - providerCfg, err := testProviderConfig(c) - if err != nil { - logging.HelperResourceError(ctx, - "Error creating test provider configuration", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error creating test provider configuration: %s", err.Error()) - } + if c.hasProviders(ctx) { + err := wd.SetConfig(ctx, c.providerConfig(ctx)) - err = wd.SetConfig(ctx, providerCfg) - if err != nil { - logging.HelperResourceError(ctx, - "Error setting test provider configuration", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error setting test provider configuration: %s", err) - } - err = runProviderCommand(ctx, t, func() error { - return wd.Init(ctx) - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) - if err != nil { - logging.HelperResourceError(ctx, - "Error running init", - map[string]interface{}{logging.KeyError: err}, - ) - t.Fatalf("Error running init: %s", err.Error()) - return + if err != nil { + logging.HelperResourceError(ctx, + "TestCase error setting provider configuration", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestCase error setting provider configuration: %s", err) + } + + err = runProviderCommand(ctx, t, func() error { + return wd.Init(ctx) + }, wd, providers) + + if err != nil { + logging.HelperResourceError(ctx, + "TestCase error running init", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestCase error running init: %s", err.Error()) + } } logging.HelperResourceDebug(ctx, "Starting TestSteps") @@ -129,8 +118,9 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest // acts as default for import tests var appliedCfg string - for i, step := range c.Steps { - ctx = logging.TestStepNumberContext(ctx, i+1) + for stepIndex, step := range c.Steps { + stepNumber := stepIndex + 1 // 1-based indexing for humans + ctx = logging.TestStepNumberContext(ctx, stepNumber) logging.HelperResourceDebug(ctx, "Starting TestStep") @@ -155,30 +145,103 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest logging.HelperResourceDebug(ctx, "Called TestStep SkipFunc") if skip { - t.Logf("Skipping step %d/%d due to SkipFunc", i+1, len(c.Steps)) + t.Logf("Skipping step %d/%d due to SkipFunc", stepNumber, len(c.Steps)) logging.HelperResourceWarn(ctx, "Skipping TestStep due to SkipFunc") continue } } + if step.Config != "" && !step.Destroy && len(step.Taint) > 0 { + var state *terraform.State + + err := runProviderCommand(ctx, t, func() error { + var err error + + state, err = getState(ctx, t, wd) + + if err != nil { + return err + } + + return nil + }, wd, providers) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error reading prior state before tainting resources", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error reading prior state before tainting resources: %s", stepNumber, len(c.Steps), err) + } + + err = testStepTaint(ctx, state, step) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error tainting resources", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error tainting resources: %s", stepNumber, len(c.Steps), err) + } + } + + if step.hasProviders(ctx) { + providers = &providerFactories{ + legacy: sdkProviderFactories(c.ProviderFactories).merge(step.ProviderFactories), + protov5: protov5ProviderFactories(c.ProtoV5ProviderFactories).merge(step.ProtoV5ProviderFactories), + protov6: protov6ProviderFactories(c.ProtoV6ProviderFactories).merge(step.ProtoV6ProviderFactories), + } + + providerCfg := step.providerConfig(ctx) + + err := wd.SetConfig(ctx, providerCfg) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error setting provider configuration", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d error setting test provider configuration: %s", stepNumber, len(c.Steps), err) + } + + err = runProviderCommand( + ctx, + t, + func() error { + return wd.Init(ctx) + }, + wd, + providers, + ) + + if err != nil { + logging.HelperResourceError(ctx, + "TestStep error running init", + map[string]interface{}{logging.KeyError: err}, + ) + t.Fatalf("TestStep %d/%d running init: %s", stepNumber, len(c.Steps), err.Error()) + return + } + } + if step.ImportState { logging.HelperResourceTrace(ctx, "TestStep is ImportState mode") - err := testStepNewImportState(ctx, t, c, helper, wd, step, appliedCfg) + err := testStepNewImportState(ctx, t, helper, wd, step, appliedCfg, providers) if step.ExpectError != nil { logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") if err == nil { logging.HelperResourceError(ctx, "Error running import: expected an error but got none", ) - t.Fatalf("Step %d/%d error running import: expected an error but got none", i+1, len(c.Steps)) + t.Fatalf("Step %d/%d error running import: expected an error but got none", stepNumber, len(c.Steps)) } if !step.ExpectError.MatchString(err.Error()) { logging.HelperResourceError(ctx, fmt.Sprintf("Error running import: expected an error with pattern (%s)", step.ExpectError.String()), map[string]interface{}{logging.KeyError: err}, ) - t.Fatalf("Step %d/%d error running import, expected an error with pattern (%s), no match on: %s", i+1, len(c.Steps), step.ExpectError.String(), err) + t.Fatalf("Step %d/%d error running import, expected an error with pattern (%s), no match on: %s", stepNumber, len(c.Steps), step.ExpectError.String(), err) } } else { if err != nil && c.ErrorCheck != nil { @@ -191,7 +254,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest "Error running import", map[string]interface{}{logging.KeyError: err}, ) - t.Fatalf("Step %d/%d error running import: %s", i+1, len(c.Steps), err) + t.Fatalf("Step %d/%d error running import: %s", stepNumber, len(c.Steps), err) } } @@ -203,7 +266,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest if step.Config != "" { logging.HelperResourceTrace(ctx, "TestStep is Config mode") - err := testStepNewConfig(ctx, t, c, wd, step) + err := testStepNewConfig(ctx, t, c, wd, step, providers) if step.ExpectError != nil { logging.HelperResourceDebug(ctx, "Checking TestStep ExpectError") @@ -211,14 +274,14 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest logging.HelperResourceError(ctx, "Expected an error but got none", ) - t.Fatalf("Step %d/%d, expected an error but got none", i+1, len(c.Steps)) + t.Fatalf("Step %d/%d, expected an error but got none", stepNumber, len(c.Steps)) } if !step.ExpectError.MatchString(err.Error()) { logging.HelperResourceError(ctx, fmt.Sprintf("Expected an error with pattern (%s)", step.ExpectError.String()), map[string]interface{}{logging.KeyError: err}, ) - t.Fatalf("Step %d/%d, expected an error with pattern, no match on: %s", i+1, len(c.Steps), err) + t.Fatalf("Step %d/%d, expected an error with pattern, no match on: %s", stepNumber, len(c.Steps), err) } } else { if err != nil && c.ErrorCheck != nil { @@ -233,7 +296,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest "Unexpected error", map[string]interface{}{logging.KeyError: err}, ) - t.Fatalf("Step %d/%d error: %s", i+1, len(c.Steps), err) + t.Fatalf("Step %d/%d error: %s", stepNumber, len(c.Steps), err) } } @@ -244,7 +307,7 @@ func runNewTest(ctx context.Context, t testing.T, c TestCase, helper *plugintest continue } - t.Fatalf("Step %d/%d, unsupported test mode", i+1, len(c.Steps)) + t.Fatalf("Step %d/%d, unsupported test mode", stepNumber, len(c.Steps)) } } @@ -277,7 +340,7 @@ func planIsEmpty(plan *tfjson.Plan) bool { return true } -func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, r *terraform.ResourceState) error { +func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, r *terraform.ResourceState, providers *providerFactories) error { t.Helper() spewConf := spew.NewDefaultConfig() @@ -291,11 +354,7 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest. // Temporarily set the config to a minimal provider config for the refresh // test. After the refresh we can reset it. - cfg, err := testProviderConfig(c) - if err != nil { - return err - } - err = wd.SetConfig(ctx, cfg) + err := wd.SetConfig(ctx, c.providerConfig(ctx)) if err != nil { t.Fatalf("Error setting import test config: %s", err) } @@ -317,10 +376,7 @@ func testIDRefresh(ctx context.Context, t testing.T, c TestCase, wd *plugintest. return err } return nil - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return err } diff --git a/helper/resource/testing_new_config.go b/helper/resource/testing_new_config.go index 5a9df2bf5f1..09d18c36452 100644 --- a/helper/resource/testing_new_config.go +++ b/helper/resource/testing_new_config.go @@ -13,30 +13,9 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep) error { +func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugintest.WorkingDir, step TestStep, providers *providerFactories) error { t.Helper() - if !step.Destroy { - var state *terraform.State - var err error - err = runProviderCommand(ctx, t, func() error { - state, err = getState(ctx, t, wd) - if err != nil { - return err - } - return nil - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) - if err != nil { - return err - } - if err := testStepTaint(ctx, state, step); err != nil { - return fmt.Errorf("Error when tainting resources: %s", err) - } - } - err := wd.SetConfig(ctx, step.Config) if err != nil { return fmt.Errorf("Error setting config: %w", err) @@ -46,10 +25,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // failing to do this will result in data sources not being updated err = runProviderCommand(ctx, t, func() error { return wd.Refresh(ctx) - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error running pre-apply refresh: %w", err) } @@ -66,10 +42,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint return wd.CreateDestroyPlan(ctx) } return wd.CreatePlan(ctx) - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error running pre-apply plan: %w", err) } @@ -84,10 +57,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint return err } return nil - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error retrieving pre-apply state: %w", err) } @@ -95,10 +65,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // Apply the diff, creating real resources err = runProviderCommand(ctx, t, func() error { return wd.Apply(ctx) - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { if step.Destroy { return fmt.Errorf("Error running destroy: %w", err) @@ -114,10 +81,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint return err } return nil - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error retrieving state after apply: %w", err) } @@ -148,10 +112,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint return wd.CreateDestroyPlan(ctx) } return wd.CreatePlan(ctx) - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error running post-apply plan: %w", err) } @@ -161,10 +122,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint var err error plan, err = wd.SavedPlan(ctx) return err - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error retrieving post-apply plan: %w", err) } @@ -175,10 +133,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint var err error stdout, err = wd.SavedPlanRawStdout(ctx) return err - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error retrieving formatted plan output: %w", err) } @@ -189,10 +144,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint if !step.Destroy || (step.Destroy && !step.PreventPostDestroyRefresh) { err := runProviderCommand(ctx, t, func() error { return wd.Refresh(ctx) - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error running post-apply refresh: %w", err) } @@ -204,10 +156,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint return wd.CreateDestroyPlan(ctx) } return wd.CreatePlan(ctx) - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error running second post-apply plan: %w", err) } @@ -216,10 +165,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint var err error plan, err = wd.SavedPlan(ctx) return err - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error retrieving second post-apply plan: %w", err) } @@ -231,10 +177,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint var err error stdout, err = wd.SavedPlanRawStdout(ctx) return err - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return fmt.Errorf("Error retrieving formatted second plan output: %w", err) } @@ -257,10 +200,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint return err } return nil - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { return err @@ -290,7 +230,7 @@ func testStepNewConfig(ctx context.Context, t testing.T, c TestCase, wd *plugint // this fails. If refresh isn't read-only, then this will have // caught a different bug. if idRefreshCheck != nil { - if err := testIDRefresh(ctx, t, c, wd, step, idRefreshCheck); err != nil { + if err := testIDRefresh(ctx, t, c, wd, step, idRefreshCheck, providers); err != nil { return fmt.Errorf( "[ERROR] Test: ID-only test failed: %s", err) } diff --git a/helper/resource/testing_new_import_state.go b/helper/resource/testing_new_import_state.go index 1dc98143a64..ec61b055f3a 100644 --- a/helper/resource/testing_new_import_state.go +++ b/helper/resource/testing_new_import_state.go @@ -14,7 +14,7 @@ import ( "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" ) -func testStepNewImportState(ctx context.Context, t testing.T, c TestCase, helper *plugintest.Helper, wd *plugintest.WorkingDir, step TestStep, cfg string) error { +func testStepNewImportState(ctx context.Context, t testing.T, helper *plugintest.Helper, wd *plugintest.WorkingDir, step TestStep, cfg string, providers *providerFactories) error { t.Helper() spewConf := spew.NewDefaultConfig() @@ -33,10 +33,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, c TestCase, helper return err } return nil - }, wd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, wd, providers) if err != nil { t.Fatalf("Error getting state: %s", err) } @@ -100,20 +97,14 @@ func testStepNewImportState(ctx context.Context, t testing.T, c TestCase, helper err = runProviderCommand(ctx, t, func() error { return importWd.Init(ctx) - }, importWd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, importWd, providers) if err != nil { t.Fatalf("Error running init: %s", err) } err = runProviderCommand(ctx, t, func() error { return importWd.Import(ctx, step.ResourceName, importId) - }, importWd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, importWd, providers) if err != nil { return err } @@ -125,10 +116,7 @@ func testStepNewImportState(ctx context.Context, t testing.T, c TestCase, helper return err } return nil - }, importWd, providerFactories{ - legacy: c.ProviderFactories, - protov5: c.ProtoV5ProviderFactories, - protov6: c.ProtoV6ProviderFactories}) + }, importWd, providers) if err != nil { t.Fatalf("Error getting state: %s", err) } diff --git a/helper/resource/testing_test.go b/helper/resource/testing_test.go index 90caae7681d..189ebd03eea 100644 --- a/helper/resource/testing_test.go +++ b/helper/resource/testing_test.go @@ -23,44 +23,64 @@ func init() { } } -func TestParallelTest(t *testing.T) { - mt := new(mockT) +// testExpectTFatal provides a wrapper for logic which should call +// (*testing.T).Fatal() or (*testing.T).Fatalf(). +// +// Since we do not want the wrapping test to fail when an expected test error +// occurs, it is required that the testLogic passed in uses +// github.com/mitchellh/go-testing-interface.RuntimeT instead of the real +// *testing.T. +// +// If Fatal() or Fatalf() is not called in the logic, the real (*testing.T).Fatal() will +// be called to fail the test. +func testExpectTFatal(t *testing.T, testLogic func()) { + t.Helper() + + var recoverIface interface{} - ParallelTest(mt, TestCase{IsUnitTest: true}) + func() { + defer func() { + recoverIface = recover() + }() - if !mt.ParallelCalled { - t.Fatal("Parallel() not called") + testLogic() + }() + + if recoverIface == nil { + t.Fatalf("expected t.Fatal(), got none") } -} -func TestTest_factoryError(t *testing.T) { - resourceFactoryError := fmt.Errorf("resource factory error") + recoverStr, ok := recoverIface.(string) - factory := func() (*schema.Provider, error) { //nolint:unparam // required signature - return nil, resourceFactoryError + if !ok { + t.Fatalf("expected string from recover(), got: %v (%T)", recoverIface, recoverIface) } + + // this string is hardcoded in github.com/mitchellh/go-testing-interface + if !strings.HasPrefix(recoverStr, "testing.T failed, see logs for output") { + t.Fatalf("expected t.Fatal(), got: %s", recoverStr) + } +} + +func TestParallelTest(t *testing.T) { mt := new(mockT) - recovered := false - func() { - defer func() { - r := recover() - // this string is hardcoded in github.com/mitchellh/go-testing-interface - if s, ok := r.(string); !ok || !strings.HasPrefix(s, "testing.T failed, see logs for output") { - panic(r) - } - recovered = true - }() - Test(mt, TestCase{ - ProviderFactories: map[string]func() (*schema.Provider, error){ - "test": factory, + ParallelTest(mt, TestCase{ + IsUnitTest: true, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return nil, nil }, - IsUnitTest: true, - }) - }() + }, + Steps: []TestStep{ + { + Config: "# not empty", + }, + }, + }) - if !recovered { - t.Fatalf("test should've failed fatally") + if !mt.ParallelCalled { + t.Fatal("Parallel() not called") } } diff --git a/helper/resource/teststep_providers.go b/helper/resource/teststep_providers.go new file mode 100644 index 00000000000..35d4a9bf57f --- /dev/null +++ b/helper/resource/teststep_providers.go @@ -0,0 +1,48 @@ +package resource + +import ( + "context" + "fmt" + "strings" +) + +// providerConfig takes the list of providers in a TestStep and returns a +// config with only empty provider blocks. This is useful for Import, where no +// config is provided, but the providers must be defined. +func (s TestStep) providerConfig(_ context.Context) string { + var providerBlocks, requiredProviderBlocks strings.Builder + + for name, externalProvider := range s.ExternalProviders { + providerBlocks.WriteString(fmt.Sprintf("provider %q {}\n", name)) + + if externalProvider.Source == "" && externalProvider.VersionConstraint == "" { + continue + } + + requiredProviderBlocks.WriteString(fmt.Sprintf(" %s = {\n", name)) + + if externalProvider.Source != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" source = %q\n", externalProvider.Source)) + } + + if externalProvider.VersionConstraint != "" { + requiredProviderBlocks.WriteString(fmt.Sprintf(" version = %q\n", externalProvider.VersionConstraint)) + } + + requiredProviderBlocks.WriteString(" }\n") + } + + if requiredProviderBlocks.Len() > 0 { + return fmt.Sprintf(` +terraform { + required_providers { +%[1]s + } +} + +%[2]s +`, strings.TrimSuffix(requiredProviderBlocks.String(), "\n"), providerBlocks.String()) + } + + return providerBlocks.String() +} diff --git a/helper/resource/teststep_providers_test.go b/helper/resource/teststep_providers_test.go new file mode 100644 index 00000000000..dd4c0d7e7a2 --- /dev/null +++ b/helper/resource/teststep_providers_test.go @@ -0,0 +1,499 @@ +package resource + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-cty/cty" + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-sdk/v2/diag" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestStepProviderConfig(t *testing.T) { + t.Parallel() + + testCases := map[string]struct { + testStep TestStep + expected string + }{ + "externalproviders-missing-source-and-versionconstraint": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, + }, + }, + expected: `provider "test" {}`, + }, + "externalproviders-source-and-versionconstraint": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + VersionConstraint: "1.2.3", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + version = "1.2.3" + } + } +} + +provider "test" {} +`, + }, + "externalproviders-source": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + Source: "registry.terraform.io/hashicorp/test", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + source = "registry.terraform.io/hashicorp/test" + } + } +} + +provider "test" {} +`, + }, + "externalproviders-versionconstraint": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": { + VersionConstraint: "1.2.3", + }, + }, + }, + expected: ` +terraform { + required_providers { + test = { + version = "1.2.3" + } + } +} + +provider "test" {} +`, + }, + "protov5providerfactories": { + testStep: TestStep{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": nil, + }, + }, + expected: ``, + }, + "protov6providerfactories": { + testStep: TestStep{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": nil, + }, + }, + expected: ``, + }, + "providerfactories": { + testStep: TestStep{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, + }, + }, + expected: ``, + }, + } + + for name, testCase := range testCases { + name, testCase := name, testCase + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := testCase.testStep.providerConfig(context.Background()) + + if diff := cmp.Diff(strings.TrimSpace(got), strings.TrimSpace(testCase.expected)); diff != "" { + t.Errorf("unexpected difference: %s", diff) + } + }) + } +} + +func TestTest_TestStep_ExternalProviders(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProviders_DifferentProviders(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + }, + }, + }, + { + Config: `resource "random_pet" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "random": { + Source: "registry.terraform.io/hashicorp/random", + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProviders_DifferentVersions(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + VersionConstraint: "3.1.0", + }, + }, + }, + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + VersionConstraint: "3.1.1", + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProviders_Error(t *testing.T) { + t.Parallel() + + testExpectTFatal(t, func() { + Test(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ExternalProviders: map[string]ExternalProvider{ + "testnonexistent": { + Source: "registry.terraform.io/hashicorp/testnonexistent", + }, + }, + }, + }, + }) + }) +} + +func TestTest_TestStep_ExternalProviders_To_ProviderFactories(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + VersionConstraint: "3.1.1", + }, + }, + }, + { + Config: `resource "null_resource" "test" {}`, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "null": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "null_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "triggers": { + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Type: schema.TypeMap, + }, + }, + }, + }, + }, nil + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ExternalProviders_To_ProviderFactories_StateUpgraders(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + VersionConstraint: "3.1.1", + }, + }, + }, + { + Check: TestCheckResourceAttr("null_resource.test", "id", "test-schema-version-1"), + Config: `resource "null_resource" "test" {}`, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "null": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "null_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "triggers": { + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Type: schema.TypeMap, + }, + }, + SchemaVersion: 1, // null 3.1.3 is version 0 + StateUpgraders: []schema.StateUpgrader{ + { + Type: cty.Object(map[string]cty.Type{ + "id": cty.String, + "triggers": cty.Map(cty.String), + }), + Upgrade: func(ctx context.Context, rawState map[string]interface{}, meta interface{}) (map[string]interface{}, error) { + // null 3.1.3 sets the id attribute to a stringified random integer. + // Double check that our resource wasn't created by this TestStep. + id, ok := rawState["id"].(string) + + if !ok || id == "test" { + return rawState, fmt.Errorf("unexpected rawState: %v", rawState) + } + + rawState["id"] = "test-schema-version-1" + + return rawState, nil + }, + Version: 0, + }, + }, + }, + }, + }, nil + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ProtoV5ProviderFactories(t *testing.T) { + t.Parallel() + + Test(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": func() (tfprotov5.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ProtoV5ProviderFactories_Error(t *testing.T) { + t.Parallel() + + testExpectTFatal(t, func() { + Test(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": func() (tfprotov5.ProviderServer, error) { //nolint:unparam // required signature + return nil, fmt.Errorf("test") + }, + }, + }, + }, + }) + }) +} + +func TestTest_TestStep_ProtoV6ProviderFactories(t *testing.T) { + t.Parallel() + + Test(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ProtoV6ProviderFactories_Error(t *testing.T) { + t.Parallel() + + testExpectTFatal(t, func() { + Test(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": func() (tfprotov6.ProviderServer, error) { //nolint:unparam // required signature + return nil, fmt.Errorf("test") + }, + }, + }, + }, + }) + }) +} + +func TestTest_TestStep_ProviderFactories(t *testing.T) { + t.Parallel() + + Test(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return nil, nil + }, + }, + }, + }, + }) +} + +func TestTest_TestStep_ProviderFactories_Error(t *testing.T) { + t.Parallel() + + testExpectTFatal(t, func() { + Test(&mockT{}, TestCase{ + Steps: []TestStep{ + { + Config: "# not empty", + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": func() (*schema.Provider, error) { //nolint:unparam // required signature + return nil, fmt.Errorf("test") + }, + }, + }, + }, + }) + }) +} + +func TestTest_TestStep_ProviderFactories_To_ExternalProviders(t *testing.T) { + t.Parallel() + + Test(t, TestCase{ + Steps: []TestStep{ + { + Config: `resource "null_resource" "test" {}`, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "null": func() (*schema.Provider, error) { //nolint:unparam // required signature + return &schema.Provider{ + ResourcesMap: map[string]*schema.Resource{ + "null_resource": { + CreateContext: func(_ context.Context, d *schema.ResourceData, _ interface{}) diag.Diagnostics { + d.SetId("test") + return nil + }, + DeleteContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + ReadContext: func(_ context.Context, _ *schema.ResourceData, _ interface{}) diag.Diagnostics { + return nil + }, + Schema: map[string]*schema.Schema{ + "triggers": { + Elem: &schema.Schema{Type: schema.TypeString}, + ForceNew: true, + Optional: true, + Type: schema.TypeMap, + }, + }, + }, + }, + }, nil + }, + }, + }, + { + Config: `resource "null_resource" "test" {}`, + ExternalProviders: map[string]ExternalProvider{ + "null": { + Source: "registry.terraform.io/hashicorp/null", + }, + }, + }, + }, + }) +} diff --git a/helper/resource/teststep_validate.go b/helper/resource/teststep_validate.go new file mode 100644 index 00000000000..e9239328c69 --- /dev/null +++ b/helper/resource/teststep_validate.go @@ -0,0 +1,99 @@ +package resource + +import ( + "context" + "fmt" + + "github.com/hashicorp/terraform-plugin-sdk/v2/internal/logging" +) + +// testStepValidateRequest contains data for the (TestStep).validate() method. +type testStepValidateRequest struct { + // StepNumber is the index of the TestStep in the TestCase.Steps. + StepNumber int + + // TestCaseHasProviders is enabled if the TestCase has set any of + // ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, + // or ProviderFactories. + TestCaseHasProviders bool +} + +// hasProviders returns true if the TestStep has set any of the +// ExternalProviders, ProtoV5ProviderFactories, ProtoV6ProviderFactories, or +// ProviderFactories fields. +func (s TestStep) hasProviders(_ context.Context) bool { + if len(s.ExternalProviders) > 0 { + return true + } + + if len(s.ProtoV5ProviderFactories) > 0 { + return true + } + + if len(s.ProtoV6ProviderFactories) > 0 { + return true + } + + if len(s.ProviderFactories) > 0 { + return true + } + + return false +} + +// validate ensures the TestStep is valid based on the following criteria: +// +// - Config or ImportState is set. +// - Providers are not specified (ExternalProviders, +// ProtoV5ProviderFactories, ProtoV6ProviderFactories, ProviderFactories) +// if specified at the TestCase level. +// - Providers are specified (ExternalProviders, ProtoV5ProviderFactories, +// ProtoV6ProviderFactories, ProviderFactories) if not specified at the +// TestCase level. +// - No overlapping ExternalProviders and ProviderFactories entries +// - ResourceName is not empty when ImportState is true, ImportStateIdFunc +// is not set, and ImportStateId is not set. +// +func (s TestStep) validate(ctx context.Context, req testStepValidateRequest) error { + ctx = logging.TestStepNumberContext(ctx, req.StepNumber) + + logging.HelperResourceTrace(ctx, "Validating TestStep") + + if s.Config == "" && !s.ImportState { + err := fmt.Errorf("TestStep missing Config or ImportState") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + for name := range s.ExternalProviders { + if _, ok := s.ProviderFactories[name]; ok { + err := fmt.Errorf("TestStep provider %q set in both ExternalProviders and ProviderFactories", name) + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + hasProviders := s.hasProviders(ctx) + + if req.TestCaseHasProviders && hasProviders { + err := fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if !req.TestCaseHasProviders && !hasProviders { + err := fmt.Errorf("Providers must be specified at the TestCase level or in all TestStep") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + + if s.ImportState { + if s.ImportStateId == "" && s.ImportStateIdFunc == nil && s.ResourceName == "" { + err := fmt.Errorf("TestStep ImportState must be specified with ImportStateId, ImportStateIdFunc, or ResourceName") + logging.HelperResourceError(ctx, "TestStep validation error", map[string]interface{}{logging.KeyError: err}) + return err + } + } + + return nil +} diff --git a/helper/resource/teststep_validate_test.go b/helper/resource/teststep_validate_test.go new file mode 100644 index 00000000000..8caf5c23323 --- /dev/null +++ b/helper/resource/teststep_validate_test.go @@ -0,0 +1,184 @@ +package resource + +import ( + "context" + "fmt" + "strings" + "testing" + + "github.com/hashicorp/terraform-plugin-go/tfprotov5" + "github.com/hashicorp/terraform-plugin-go/tfprotov6" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" +) + +func TestTestStepHasProviders(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + testStep TestStep + expected bool + }{ + "none": { + testStep: TestStep{}, + expected: false, + }, + "externalproviders": { + testStep: TestStep{ + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + }, + expected: true, + }, + "protov5providerfactories": { + testStep: TestStep{ + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": nil, // does not need to be real + }, + }, + expected: true, + }, + "protov6providerfactories": { + testStep: TestStep{ + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": nil, // does not need to be real + }, + }, + expected: true, + }, + "providerfactories": { + testStep: TestStep{ + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, // does not need to be real + }, + }, + expected: true, + }, + } + + for name, test := range tests { + name, test := name, test + + t.Run(name, func(t *testing.T) { + t.Parallel() + + got := test.testStep.hasProviders(context.Background()) + + if got != test.expected { + t.Errorf("expected %t, got %t", test.expected, got) + } + }) + } +} + +func TestTestStepValidate(t *testing.T) { + t.Parallel() + + tests := map[string]struct { + testStep TestStep + testStepValidateRequest testStepValidateRequest + expectedError error + }{ + "config-and-importstate-missing": { + testStep: TestStep{}, + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("TestStep missing Config or ImportState"), + }, + "externalproviders-overlapping-providerfactories": { + testStep: TestStep{ + Config: "# not empty", + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, // does not need to be real + }, + }, + testStepValidateRequest: testStepValidateRequest{}, + expectedError: fmt.Errorf("TestStep provider \"test\" set in both ExternalProviders and ProviderFactories"), + }, + "externalproviders-testcase-providers": { + testStep: TestStep{ + Config: "# not empty", + ExternalProviders: map[string]ExternalProvider{ + "test": {}, // does not need to be real + }, + }, + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level"), + }, + "importstate-missing-resourcename": { + testStep: TestStep{ + ImportState: true, + }, + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("TestStep ImportState must be specified with ImportStateId, ImportStateIdFunc, or ResourceName"), + }, + "protov5providerfactories-testcase-providers": { + testStep: TestStep{ + Config: "# not empty", + ProtoV5ProviderFactories: map[string]func() (tfprotov5.ProviderServer, error){ + "test": nil, // does not need to be real + }, + }, + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level"), + }, + "protov6providerfactories-testcase-providers": { + testStep: TestStep{ + Config: "# not empty", + ProtoV6ProviderFactories: map[string]func() (tfprotov6.ProviderServer, error){ + "test": nil, // does not need to be real + }, + }, + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level"), + }, + "providerfactories-testcase-providers": { + testStep: TestStep{ + Config: "# not empty", + ProviderFactories: map[string]func() (*schema.Provider, error){ + "test": nil, // does not need to be real + }, + }, + testStepValidateRequest: testStepValidateRequest{ + TestCaseHasProviders: true, + }, + expectedError: fmt.Errorf("Providers must only be specified either at the TestCase or TestStep level"), + }, + } + + for name, test := range tests { + name, test := name, test + + t.Run(name, func(t *testing.T) { + t.Parallel() + + err := test.testStep.validate(context.Background(), test.testStepValidateRequest) + + if err != nil { + if test.expectedError == nil { + t.Fatalf("unexpected error: %s", err) + } + + if !strings.Contains(err.Error(), test.expectedError.Error()) { + t.Fatalf("expected error %q, got: %s", test.expectedError, err) + } + } + + if err == nil && test.expectedError != nil { + t.Errorf("expected error: %s", test.expectedError) + } + }) + } +} diff --git a/internal/logging/context.go b/internal/logging/context.go index 5bce8140fdb..6036b0d0446 100644 --- a/internal/logging/context.go +++ b/internal/logging/context.go @@ -48,28 +48,28 @@ func InitTestContext(ctx context.Context, t testing.T) context.Context { // TestNameContext adds the current test name to loggers. func TestNameContext(ctx context.Context, testName string) context.Context { - ctx = tfsdklog.SubsystemWith(ctx, SubsystemHelperResource, KeyTestName, testName) + ctx = tfsdklog.SubsystemSetField(ctx, SubsystemHelperResource, KeyTestName, testName) return ctx } // TestStepNumberContext adds the current test step number to loggers. func TestStepNumberContext(ctx context.Context, stepNumber int) context.Context { - ctx = tfsdklog.SubsystemWith(ctx, SubsystemHelperResource, KeyTestStepNumber, stepNumber) + ctx = tfsdklog.SubsystemSetField(ctx, SubsystemHelperResource, KeyTestStepNumber, stepNumber) return ctx } // TestTerraformPathContext adds the current test Terraform CLI path to loggers. func TestTerraformPathContext(ctx context.Context, terraformPath string) context.Context { - ctx = tfsdklog.SubsystemWith(ctx, SubsystemHelperResource, KeyTestTerraformPath, terraformPath) + ctx = tfsdklog.SubsystemSetField(ctx, SubsystemHelperResource, KeyTestTerraformPath, terraformPath) return ctx } // TestWorkingDirectoryContext adds the current test working directory to loggers. func TestWorkingDirectoryContext(ctx context.Context, workingDirectory string) context.Context { - ctx = tfsdklog.SubsystemWith(ctx, SubsystemHelperResource, KeyTestWorkingDirectory, workingDirectory) + ctx = tfsdklog.SubsystemSetField(ctx, SubsystemHelperResource, KeyTestWorkingDirectory, workingDirectory) return ctx } diff --git a/internal/logging/context_test.go b/internal/logging/context_test.go index b193b9c019c..a92b04d8295 100644 --- a/internal/logging/context_test.go +++ b/internal/logging/context_test.go @@ -20,8 +20,8 @@ func TestInitContext(t *testing.T) { // Simulate root logger fields that would have been associated by // terraform-plugin-go prior to the InitContext() call. - ctx = tfsdklog.With(ctx, "tf_rpc", "GetProviderSchema") - ctx = tfsdklog.With(ctx, "tf_req_id", "123-testing-123") + ctx = tfsdklog.SetField(ctx, "tf_rpc", "GetProviderSchema") + ctx = tfsdklog.SetField(ctx, "tf_req_id", "123-testing-123") ctx = logging.InitContext(ctx) diff --git a/internal/logging/keys.go b/internal/logging/keys.go index 2ba548f61d5..03931b02473 100644 --- a/internal/logging/keys.go +++ b/internal/logging/keys.go @@ -31,6 +31,15 @@ const ( // The TestStep number of the test being executed. Starts at 1. KeyTestStepNumber = "test_step_number" + // The Terraform CLI logging level (TF_LOG) used for an acceptance test. + KeyTestTerraformLogLevel = "test_terraform_log_level" + + // The Terraform CLI logging level (TF_LOG_CORE) used for an acceptance test. + KeyTestTerraformLogCoreLevel = "test_terraform_log_core_level" + + // The Terraform CLI logging level (TF_LOG_PROVIDER) used for an acceptance test. + KeyTestTerraformLogProviderLevel = "test_terraform_log_provider_level" + // The path to the Terraform CLI logging file used for an acceptance test. // // This should match where the rest of the acceptance test logs are going diff --git a/internal/plugintest/environment_variables.go b/internal/plugintest/environment_variables.go index ac75151a4e8..6fd001a07d0 100644 --- a/internal/plugintest/environment_variables.go +++ b/internal/plugintest/environment_variables.go @@ -10,6 +10,24 @@ const ( // CLI installation, if installation is required. EnvTfAccTempDir = "TF_ACC_TEMP_DIR" + // Environment variable with level to filter Terraform logs during + // acceptance testing. This value sets TF_LOG in a safe manner when + // executing Terraform CLI commands, which would otherwise interfere + // with the testing framework using TF_LOG to set the Go standard library + // log package level. + // + // This value takes precedence over TF_LOG_CORE, due to precedence rules + // in the Terraform core code, so it is not possible to set this to a level + // and also TF_LOG_CORE=OFF. Use TF_LOG_CORE and TF_LOG_PROVIDER in that + // case instead. + // + // If not set, but TF_ACC_LOG_PATH or TF_LOG_PATH_MASK is set, it defaults + // to TRACE. If Terraform CLI is version 0.14 or earlier, it will have no + // separate affect from the TF_ACC_LOG_PATH or TF_LOG_PATH_MASK behavior, + // as those earlier versions of Terraform are unreliable with the logging + // level being outside TRACE. + EnvTfAccLog = "TF_ACC_LOG" + // Environment variable with path to save Terraform logs during acceptance // testing. This value sets TF_LOG_PATH in a safe manner when executing // Terraform CLI commands, which would otherwise be ignored since it could @@ -18,6 +36,17 @@ const ( // If TF_LOG_PATH_MASK is set, it takes precedence over this value. EnvTfAccLogPath = "TF_ACC_LOG_PATH" + // Environment variable with level to filter Terraform core logs during + // acceptance testing. This value sets TF_LOG_CORE separate from + // TF_LOG_PROVIDER when calling Terraform. + // + // This value has no affect when TF_ACC_LOG is set (which sets Terraform's + // TF_LOG), due to precedence rules in the Terraform core code. Use + // TF_LOG_CORE and TF_LOG_PROVIDER in that case instead. + // + // If not set, defaults to TF_ACC_LOG behaviors. + EnvTfLogCore = "TF_LOG_CORE" + // Environment variable with path containing the string %s, which is // replaced with the test name, to save separate Terraform logs during // acceptance testing. This value sets TF_LOG_PATH in a safe manner when @@ -27,6 +56,22 @@ const ( // Takes precedence over TF_ACC_LOG_PATH. EnvTfLogPathMask = "TF_LOG_PATH_MASK" + // Environment variable with level to filter Terraform provider logs during + // acceptance testing. This value sets TF_LOG_PROVIDER separate from + // TF_LOG_CORE. + // + // During testing, this only affects external providers whose logging goes + // through Terraform. The logging for the provider under test is controlled + // by the testing framework as it is running the provider code. Provider + // code using the Go standard library log package is controlled by TF_LOG + // for historical compatibility. + // + // This value takes precedence over TF_ACC_LOG for external provider logs, + // due to rules in the Terraform core code. + // + // If not set, defaults to TF_ACC_LOG behaviors. + EnvTfLogProvider = "TF_LOG_PROVIDER" + // Environment variable with acceptance testing Terraform CLI version to // download from releases.hashicorp.com, checksum verify, and install. The // value can be any valid Terraform CLI version, such as 1.1.6, with or diff --git a/internal/plugintest/helper.go b/internal/plugintest/helper.go index d5aecd87bce..0411eae0a87 100644 --- a/internal/plugintest/helper.go +++ b/internal/plugintest/helper.go @@ -139,9 +139,94 @@ func (h *Helper) NewWorkingDir(ctx context.Context, t TestControl) (*WorkingDir, return nil, fmt.Errorf("unable to disable terraform-exec provider verification: %w", err) } + tfAccLog := os.Getenv(EnvTfAccLog) + tfAccLogPath := os.Getenv(EnvTfAccLogPath) + tfLogCore := os.Getenv(EnvTfLogCore) + tfLogPathMask := os.Getenv(EnvTfLogPathMask) + tfLogProvider := os.Getenv(EnvTfLogProvider) + + if tfAccLog != "" && tfLogCore != "" { + err = fmt.Errorf( + "Invalid environment variable configuration. Cannot set both TF_ACC_LOG and TF_LOG_CORE. " + + "Use TF_LOG_CORE and TF_LOG_PROVIDER to separately control the Terraform CLI logging subsystems. " + + "To control the Go standard library log package for the provider under test, use TF_LOG.", + ) + logging.HelperResourceError(ctx, err.Error()) + return nil, err + } + + if tfAccLog != "" { + logging.HelperResourceTrace( + ctx, + fmt.Sprintf("Setting terraform-exec log level via %s environment variable, if Terraform CLI is version 0.15 or later", EnvTfAccLog), + map[string]interface{}{logging.KeyTestTerraformLogLevel: tfAccLog}, + ) + + err := tf.SetLog(tfAccLog) + + if err != nil { + if !errors.As(err, new(*tfexec.ErrVersionMismatch)) { + logging.HelperResourceError( + ctx, + "Unable to set terraform-exec log level", + map[string]interface{}{logging.KeyError: err.Error()}, + ) + return nil, fmt.Errorf("unable to set terraform-exec log level (%s): %w", tfAccLog, err) + } + + logging.HelperResourceWarn( + ctx, + fmt.Sprintf("Unable to set terraform-exec log level via %s environment variable, as Terraform CLI is version 0.14 or earlier. It will default to TRACE.", EnvTfAccLog), + map[string]interface{}{logging.KeyTestTerraformLogLevel: "TRACE"}, + ) + } + } + + if tfLogCore != "" { + logging.HelperResourceTrace( + ctx, + fmt.Sprintf("Setting terraform-exec core log level via %s environment variable, if Terraform CLI is version 0.15 or later", EnvTfLogCore), + map[string]interface{}{ + logging.KeyTestTerraformLogCoreLevel: tfLogCore, + }, + ) + + err := tf.SetLogCore(tfLogCore) + + if err != nil { + logging.HelperResourceError( + ctx, + "Unable to set terraform-exec core log level", + map[string]interface{}{logging.KeyError: err.Error()}, + ) + return nil, fmt.Errorf("unable to set terraform-exec core log level (%s): %w", tfLogCore, err) + } + } + + if tfLogProvider != "" { + logging.HelperResourceTrace( + ctx, + fmt.Sprintf("Setting terraform-exec provider log level via %s environment variable, if Terraform CLI is version 0.15 or later", EnvTfLogProvider), + map[string]interface{}{ + logging.KeyTestTerraformLogCoreLevel: tfLogProvider, + }, + ) + + err := tf.SetLogProvider(tfLogProvider) + + if err != nil { + logging.HelperResourceError( + ctx, + "Unable to set terraform-exec provider log level", + map[string]interface{}{logging.KeyError: err.Error()}, + ) + return nil, fmt.Errorf("unable to set terraform-exec provider log level (%s): %w", tfLogProvider, err) + } + } + var logPath, logPathEnvVar string - if tfAccLogPath := os.Getenv(EnvTfAccLogPath); tfAccLogPath != "" { + if tfAccLogPath != "" { logPath = tfAccLogPath logPathEnvVar = EnvTfAccLogPath } @@ -149,7 +234,7 @@ func (h *Helper) NewWorkingDir(ctx context.Context, t TestControl) (*WorkingDir, // Similar to helper/logging.LogOutput() and // terraform-plugin-log/tfsdklog.RegisterTestSink(), the TF_LOG_PATH_MASK // environment variable should take precedence over TF_ACC_LOG_PATH. - if tfLogPathMask := os.Getenv(EnvTfLogPathMask); tfLogPathMask != "" { + if tfLogPathMask != "" { // Escape special characters which may appear if we have subtests testName := strings.Replace(t.Name(), "/", "__", -1) logPath = fmt.Sprintf(tfLogPathMask, testName) diff --git a/internal/plugintest/working_dir.go b/internal/plugintest/working_dir.go index 5309fcfb0a0..1c15e6f5c19 100644 --- a/internal/plugintest/working_dir.go +++ b/internal/plugintest/working_dir.go @@ -155,7 +155,9 @@ func (wd *WorkingDir) Init(ctx context.Context) error { logging.HelperResourceTrace(ctx, "Calling Terraform CLI init command") - err := wd.tf.Init(context.Background(), tfexec.Reattach(wd.reattachInfo)) + // -upgrade=true is required for per-TestStep provider version changes + // e.g. TestTest_TestStep_ExternalProviders_DifferentVersions + err := wd.tf.Init(context.Background(), tfexec.Reattach(wd.reattachInfo), tfexec.Upgrade(true)) logging.HelperResourceTrace(ctx, "Called Terraform CLI init command") diff --git a/website/docs/plugin/sdkv2/index.mdx b/website/docs/plugin/sdkv2/index.mdx index 00c7471ebd3..0ebb9d830bb 100644 --- a/website/docs/plugin/sdkv2/index.mdx +++ b/website/docs/plugin/sdkv2/index.mdx @@ -33,5 +33,5 @@ Terraform Plugin SDKv2 is an established way to develop Terraform Plugins on [pr The [terraform-plugin-framework](/plugin/framework) is a new way to develop Terraform providers, offering improvements and new features from Terraform Plugin SDKv2. You can refactor individual resources and data sources over time with the following compatibility: -* Terraform 0.12 and later: First, [translate your provider](/plugin/mux/translating-protocol-version-6-to-5) into a protocol version 5 provider. Then [combine your provider](/plugin/mux/combining-protocol-version-5-providers) with the translated provider. You will not be able to use [protocol version 6](/plugin/how-terraform-works#protocol-version-6) features. -* Terraform 1.0 and later: First, [translate your provider](/plugin/mux/translating-protocol-version-5-to-6) into a protocol version 6 provider. Then [combine your provider](/plugin/mux/combining-protocol-version-6-providers) with the translated provider. +* Terraform 0.12 and later: [Combine your provider](/plugin/mux/combining-protocol-version-5-providers) with the framework provider. You will not be able to use [protocol version 6](/plugin/how-terraform-works#protocol-version-6) features. +* Terraform 1.0 and later: First, [translate your provider](/plugin/mux/translating-protocol-version-5-to-6) into a protocol version 6 provider. Then [combine your provider](/plugin/mux/combining-protocol-version-6-providers) with the framework provider. diff --git a/website/docs/plugin/sdkv2/testing/acceptance-tests/index.mdx b/website/docs/plugin/sdkv2/testing/acceptance-tests/index.mdx index 81e00a7d3d0..00949f1c8bf 100644 --- a/website/docs/plugin/sdkv2/testing/acceptance-tests/index.mdx +++ b/website/docs/plugin/sdkv2/testing/acceptance-tests/index.mdx @@ -238,14 +238,40 @@ A number of environment variables are available to control aspects of acceptance | Environment Variable Name | Default | Description | |---------------------------|---------|-------------| | `TF_ACC` | N/A | Set to any value to enable acceptance testing via the [`helper/resource.ParallelTest()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource#ParallelTest) and [`helper/resource.Test()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource#Test) functions. | -| `TF_ACC_LOG_PATH` | N/A | Set a path for Terraform logs during testing. Refer to `TF_LOG_PATH_MASK` to configure individual log files per test. | | `TF_ACC_PROVIDER_HOST`: | `registry.terraform.io` | Set the hostname of the provider under test, such as `example.com` in the `example.com/myorg/myprovider` provider source address. This is only required if any [`TestStep.Config`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource#TestStep.Config) specifies a provider source address, such as in the [`terraform` configuration block `required_providers` attribute](https://www.terraform.io/language/settings#specifying-provider-requirements). | | `TF_ACC_PROVIDER_NAMESPACE` | `hashicorp` | Set the namespace of the provider under test, such as `myorg` in the `registry.terraform.io/myorg/myprovider` provider source address. This is only required if any [`TestStep.Config`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource#TestStep.Config) specifies a provider source address, such as in the [`terraform` configuration block `required_providers` attribute](https://www.terraform.io/language/settings#specifying-provider-requirements). | | `TF_ACC_STATE_LINEAGE` | N/A | Set to `1` to enable state lineage debug logs, which are normally suppressed during acceptance testing. | | `TF_ACC_TEMP_DIR` | Operating system specific via [`os.TempDir()`](https://pkg.go.dev/os#TempDir) | Set a temporary directory used for testing files and installing Terraform CLI, if installation is required. | | `TF_ACC_TERRAFORM_PATH` | N/A | Set the path to a Terraform CLI binary on the local filesystem to be used during testing. It must be executable. If not found and `TF_ACC_TERRAFORM_VERSION` is not set, an error is returned. | | `TF_ACC_TERRAFORM_VERSION` | N/A | Set the exact version of Terraform CLI to automatically install into `TF_ACC_TEMP_DIR`. For example, `1.1.6` or `v1.0.11`. | -| `TF_LOG_PATH_MASK` | N/A | Set a file path containing the string `%s`, which is replaced with the test name, to write a separate log file per test. Refer to `TF_ACC_LOG_PATH` to configure a single log file for all tests. | + +### Logging Environment Variables + +A number of environment variables available to control logging aspects during acceptance test execution. Some of these modify or replace the production behaviors defined in [managing provider log output](/plugin/log/managing) and [debugging Terraform](/internals/debugging). + +#### Logging Levels + +| Environment Variable Name | Default | Description | +|---------------------------|---------|-------------| +| `TF_ACC_LOG` | N/A | Set the `TF_LOG` environment variable used by Terraform CLI while testing. If set, overrides `TF_LOG_CORE`. Use `TF_LOG_CORE` and `TF_LOG_PROVIDER` to configure separate levels for Terraform CLI logging. | +| `TF_LOG` | N/A | Set the log level for the Go standard library `log` package. If set to any level, sets the `TRACE` log level for any SDK and provider logs written by [`terraform-plugin-log`](/plugin/log/writing). Use the `TF_LOG_SDK*` and `TF_LOG_PROVIDER_*` environment variables described in [managing log output](/plugin/log/managing) to decrease or disable SDK and provider logs written by [`terraform-plugin-log`](/plugin/log/writing). Use `TF_ACC_LOG`, `TF_LOG_CORE`, or `TF_LOG_PROVIDER` environment variables to set the logging levels used by Terraform CLI while testing. | +| `TF_LOG_CORE` | `TF_ACC_LOG` | Set the `TF_LOG_CORE` environment variable used by Terraform CLI logging of graph operations and other core functionality while testing. If `TF_ACC_LOG` is set, this setting has no effect. Use `TF_LOG_PROVIDER` to configure a separate level for Terraform CLI logging of external providers while testing (e.g. defined by the `TestCase` or `TestStep` type `ExternalProviders` field). | +| `TF_LOG_PROVIDER` | `TF_ACC_LOG` | Set the `TF_LOG_PROVIDER` environment variable used by Terraform CLI logging of external providers while testing (e.g. defined by the `TestCase` or `TestStep` type `ExternalProviders` field). If set, overrides `TF_ACC_LOG`. Use `TF_LOG_CORE` to configure a separate level for Terraform CLI logging of graph operations and other core functionality while testing. | + +#### Logging Output + +By default, there is no logging output when running the `go test` command. Use one of the below environment variables to output logs to the local filesystem or use the `go test` command `-v` (verbose) flag to view logging without writing file(s). + +| Environment Variable Name | Default | Description | +|---------------------------|---------|-------------| +| `TF_ACC_LOG_PATH` | N/A | Set a file path for all logs during testing. Use `TF_LOG_PATH_MASK` to configure individual log files per test. | +| `TF_LOG_PATH_MASK` | N/A | Set a file path containing the string `%s`, which is replaced with the test name, to write a separate log file per test. Use `TF_ACC_LOG_PATH` to configure a single log file for all tests. | + +The logs associated with each test can output across incorrect files as each new test starts if the provider is using the Go standard library [`log` package](https://pkg.go.dev/log) for logging, acceptance testing that uses [`helper/resource.ParallelTest()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource#ParallelTest), and `TF_LOG_PATH_MASK`. To resolve this issue, choose one of the following approaches: + +* Use [`terraform-plugin-log`](/plugin/log/writing) based logging. Each logger will be correctly associated with each test name output. +* Wrap testing execution so that each test is individually executed with `go test`. Since each `go test` process will have its own `log` package output handling, logging will be correctly associated with each test name output. +* Replace [`helper/resource.ParallelTest()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource#ParallelTest) with [`helper/resource.Test()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource#Test) and ensure [`(*testing.T).Parallel()`](https://pkg.go.dev/testing#T.Parallel) is not called in tests. This serializes all testing so each test will be associated with each test name output. ## Troubleshooting