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/1006.txt b/.changelog/1006.txt new file mode 100644 index 00000000000..a988237e8d0 --- /dev/null +++ b/.changelog/1006.txt @@ -0,0 +1,7 @@ +```release-note:feature +helper/logging: New `NewLoggingHTTPTransport()` and `NewSubsystemLoggingHTTPTransport()` functions, providing `http.RoundTripper` Transport implementations that log request/response using [terraform-plugin-log](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-log) ([#546](https://github.com/hashicorp/terraform-plugin-sdk/issues/546)) +``` + +```release-note:note +helper/logging: Existing `NewTransport()` is now deprecated in favour of using the new `NewLoggingHTTPTransport()` or `NewSubsystemLoggingHTTPTransport()` +``` 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/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 6992e928cd5..bcc05a5de31 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -50,7 +50,7 @@ or submitting a patch. ## New Issue -We welcome issues of all kinds including feature requests, bug reports or documentation suggestions. Below are guidelines for well-formed issues of each type. +We welcome issues of all kinds including feature requests, bug reports or documentation contributions. Below are guidelines for well-formed issues of each type. ### Bug Reports @@ -67,6 +67,20 @@ It is possible we already fixed the bug you're experiencing. - [ ] **Include a use case description**: In addition to describing the behavior of the feature you'd like to see added, it's helpful to also lay out the reason why the feature would be important and how it would benefit the wider Terraform ecosystem. Use case in context of 1 provider is good, wider context of more providers is better. +### Documentation Contributions + + - [ ] **Search for possible duplicate suggestions**: It's helpful to keep + suggestions consolidated to one thread, so do a quick search on existing + issues to check if anybody else has suggested the same thing. You can scope + searches by the label `documentation` to help narrow things down. + + - [ ] **Describe the questions you're hoping the documentation will answer**: + It's very helpful when writing documentation to have specific questions like + "how do I implement a default value?" in mind. This helps us ensure the + documentation is targeted, specific, and framed in a useful way. + + - [ ] **Contribute**: This repository contains the markdown files that generate versioned documentation for [terraform.io/plugin/sdkv2](https://www.terraform.io/plugin/sdkv2). Please open a pull request with documentation changes. Refer to the [website README](../website/README.md) for more information. + ## New Pull Request Thank you for contributing! 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..c9bc27e363d 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,13 @@ 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 + - uses: golangci/golangci-lint-action@v3 with: - skip-go-installation: true + version: latest terraform-provider-corner: defaults: run: @@ -39,13 +35,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/.gitignore b/.gitignore index 1fa9f4e4936..637ea69288e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,6 @@ node_modules -website-preview \ No newline at end of file +website-preview + +# Jetbrains IDEs +.idea/ +*.iws 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..c97b8346aad 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,44 @@ +# 2.20.0 (July 28, 2022) + +NOTES: + +* helper/logging: Existing `NewTransport()` is now deprecated in favour of using the new `NewLoggingHTTPTransport()` or `NewSubsystemLoggingHTTPTransport()` ([#1006](https://github.com/hashicorp/terraform-plugin-sdk/issues/1006)) + +FEATURES: + +* helper/logging: New `NewLoggingHTTPTransport()` and `NewSubsystemLoggingHTTPTransport()` functions, providing `http.RoundTripper` Transport implementations that log request/response using [terraform-plugin-log](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-log) ([#546](https://github.com/hashicorp/terraform-plugin-sdk/issues/546)) ([#1006](https://github.com/hashicorp/terraform-plugin-sdk/issues/1006)) + +# 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/Makefile b/Makefile index cc49a0929cf..20cb17a7a64 100644 --- a/Makefile +++ b/Makefile @@ -22,17 +22,17 @@ WEBSITE_DOCKER_RUN_FLAGS=--interactive \ default: test -test: fmtcheck generate +test: generate go test ./... +lint: + golangci-lint run + generate: go generate ./... fmt: - gofmt -w $(GOFMT_FILES) - -fmtcheck: - @sh -c "'$(CURDIR)/scripts/gofmtcheck.sh'" + gofmt -s -w -e $(GOFMT_FILES) # Run the terraform.io website to preview local content changes website: @@ -55,4 +55,4 @@ website/build-local: @docker build https://github.com/hashicorp/terraform-website.git\#$(WEBSITE_BRANCH) \ -t $(WEBSITE_DOCKER_IMAGE_LOCAL) -.PHONY: default fmt fmtcheck generate test website website/local website/build-local \ No newline at end of file +.PHONY: default fmt lint generate test website website/local website/build-local 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..0f3f3ee7bfb 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.7.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..5c003be8d9c 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,36 @@ 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/go.mod h1:p4R1jWBXRTvL4odmEkFfDdhUjHf9zcs/BCoNHAc7IK4= +github.com/hashicorp/terraform-plugin-log v0.7.0 h1:SDxJUyT8TwN4l5b5/VkiTIaQgY6R+Y2BQ0sRZftGKQs= +github.com/hashicorp/terraform-plugin-log v0.7.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 +158,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 +205,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 +221,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 +254,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 +273,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 +334,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 +364,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/logging/logging_http_transport.go b/helper/logging/logging_http_transport.go new file mode 100644 index 00000000000..335e1784e22 --- /dev/null +++ b/helper/logging/logging_http_transport.go @@ -0,0 +1,288 @@ +package logging + +import ( + "bufio" + "bytes" + "context" + "errors" + "io" + "net/http" + "net/http/httputil" + "net/textproto" + "strings" + + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform-plugin-log/tflog" +) + +// NewLoggingHTTPTransport creates a wrapper around an *http.RoundTripper, +// designed to be used for the `Transport` field of http.Client. +// +// This logs each pair of HTTP request/response that it handles. +// The logging is done via `tflog`, that is part of the terraform-plugin-log +// library, included by this SDK. +// +// The request/response is logged via tflog.Debug, using the context.Context +// attached to the http.Request that the transport receives as input +// of http.RoundTripper RoundTrip method. +// +// It's responsibility of the developer using this transport, to ensure that each +// http.Request it handles is configured with the SDK-initialized Provider Root Logger +// context.Context, that it's passed to all resources/data-sources/provider entry-points +// (i.e. schema.Resource fields like `CreateContext`, `ReadContext`, etc.). +// +// This also gives the developer the flexibility to further configure the +// logging behaviour via the above-mentioned context: please see +// https://www.terraform.io/plugin/log/writing. +func NewLoggingHTTPTransport(t http.RoundTripper) *loggingHttpTransport { + return &loggingHttpTransport{"", t} +} + +// NewSubsystemLoggingHTTPTransport creates a wrapper around an *http.RoundTripper, +// designed to be used for the `Transport` field of http.Client. +// +// This logs each pair of HTTP request/response that it handles. +// The logging is done via `tflog`, that is part of the terraform-plugin-log +// library, included by this SDK. +// +// The request/response is logged via tflog.SubsystemDebug, using the context.Context +// attached to the http.Request that the transport receives as input +// of http.RoundTripper RoundTrip method, as well as the `subsystem` string +// provided at construction time. +// +// It's responsibility of the developer using this transport, to ensure that each +// http.Request it handles is configured with a Subsystem Logger +// context.Context that was initialized via tflog.NewSubsystem. +// +// This also gives the developer the flexibility to further configure the +// logging behaviour via the above-mentioned context: please see +// https://www.terraform.io/plugin/log/writing. +// +// Please note: setting `subsystem` to an empty string it's equivalent to +// using NewLoggingHTTPTransport. +func NewSubsystemLoggingHTTPTransport(subsystem string, t http.RoundTripper) *loggingHttpTransport { + return &loggingHttpTransport{subsystem, t} +} + +const ( + // FieldHttpOperationType is the field key used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging the type of HTTP operation via tflog. + FieldHttpOperationType = "tf_http_op_type" + + // OperationHttpRequest is the field value used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP request via tflog. + OperationHttpRequest = "request" + + // OperationHttpResponse is the field value used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP response via tflog. + OperationHttpResponse = "response" + + // FieldHttpRequestMethod is the field key used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP request method via tflog. + FieldHttpRequestMethod = "tf_http_req_method" + + // FieldHttpRequestUri is the field key used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP request URI via tflog. + FieldHttpRequestUri = "tf_http_req_uri" + + // FieldHttpRequestProtoVersion is the field key used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP request HTTP version via tflog. + FieldHttpRequestProtoVersion = "tf_http_req_version" + + // FieldHttpRequestBody is the field key used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP request body via tflog. + FieldHttpRequestBody = "tf_http_req_body" + + // FieldHttpResponseProtoVersion is the field key used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP response protocol version via tflog. + FieldHttpResponseProtoVersion = "tf_http_res_version" + + // FieldHttpResponseStatusCode is the field key used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP response status code via tflog. + FieldHttpResponseStatusCode = "tf_http_res_status_code" + + // FieldHttpResponseStatusReason is the field key used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP response status reason phrase via tflog. + FieldHttpResponseStatusReason = "tf_http_res_status_reason" + + // FieldHttpResponseBody is the field key used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP response body via tflog. + FieldHttpResponseBody = "tf_http_res_body" + + // FieldHttpTransactionId is the field key used by NewLoggingHTTPTransport + // and NewSubsystemLoggingHTTPTransport when logging an HTTP transaction via tflog. + FieldHttpTransactionId = "tf_http_trans_id" +) + +type loggingHttpTransport struct { + subsystem string + transport http.RoundTripper +} + +func (t *loggingHttpTransport) RoundTrip(req *http.Request) (*http.Response, error) { + ctx := req.Context() + ctx = t.AddTransactionIdField(ctx) + + // Decompose the request bytes in a message (HTTP body) and fields (HTTP headers), then log it + fields, err := decomposeRequestForLogging(req) + if err != nil { + t.Error(ctx, "Failed to parse request bytes for logging", map[string]interface{}{ + "error": err, + }) + } else { + t.Debug(ctx, "Sending HTTP Request", fields) + } + + // Invoke the wrapped RoundTrip now + res, err := t.transport.RoundTrip(req) + if err != nil { + return res, err + } + + // Decompose the response bytes in a message (HTTP body) and fields (HTTP headers), then log it + fields, err = decomposeResponseForLogging(res) + if err != nil { + t.Error(ctx, "Failed to parse response bytes for logging", map[string]interface{}{ + "error": err, + }) + } else { + t.Debug(ctx, "Received HTTP Response", fields) + } + + return res, nil +} + +func (t *loggingHttpTransport) Debug(ctx context.Context, msg string, fields ...map[string]interface{}) { + if t.subsystem != "" { + tflog.SubsystemDebug(ctx, t.subsystem, msg, fields...) + } else { + tflog.Debug(ctx, msg, fields...) + } +} + +func (t *loggingHttpTransport) Error(ctx context.Context, msg string, fields ...map[string]interface{}) { + if t.subsystem != "" { + tflog.SubsystemError(ctx, t.subsystem, msg, fields...) + } else { + tflog.Error(ctx, msg, fields...) + } +} + +func (t *loggingHttpTransport) AddTransactionIdField(ctx context.Context) context.Context { + tId, err := uuid.GenerateUUID() + + if err != nil { + tId = "Unable to assign Transaction ID: " + err.Error() + } + + if t.subsystem != "" { + return tflog.SubsystemSetField(ctx, t.subsystem, FieldHttpTransactionId, tId) + } else { + return tflog.SetField(ctx, FieldHttpTransactionId, tId) + + } +} + +func decomposeRequestForLogging(req *http.Request) (map[string]interface{}, error) { + fields := make(map[string]interface{}, len(req.Header)+4) + fields[FieldHttpOperationType] = OperationHttpRequest + + fields[FieldHttpRequestMethod] = req.Method + fields[FieldHttpRequestUri] = req.URL.RequestURI() + fields[FieldHttpRequestProtoVersion] = req.Proto + + // Get the full body of the request, including headers appended by http.Transport: + // this is necessary because the http.Request at this stage doesn't contain + // all the headers that will be eventually sent. + // We rely on `httputil.DumpRequestOut` to obtain the actual bytes that will be sent out. + reqBytes, err := httputil.DumpRequestOut(req, true) + if err != nil { + return nil, err + } + + // Create a reader around the request full body + reqReader := textproto.NewReader(bufio.NewReader(bytes.NewReader(reqBytes))) + + err = fieldHeadersFromRequestReader(reqReader, fields) + if err != nil { + return nil, err + } + + // Read the rest of the body content + fields[FieldHttpRequestBody] = bodyFromRestOfRequestReader(reqReader) + return fields, nil +} + +func fieldHeadersFromRequestReader(reader *textproto.Reader, fields map[string]interface{}) error { + // Ignore the first line: it contains non-header content + // that we have already captured. + // Skipping this step, would cause the following call to `ReadMIMEHeader()` + // to fail as it cannot parse the first line. + _, err := reader.ReadLine() + if err != nil { + return err + } + + // Read the MIME-style headers + mimeHeader, err := reader.ReadMIMEHeader() + if err != nil { + return err + } + + // Set the headers as fields to log + for k, v := range mimeHeader { + if len(v) == 1 { + fields[k] = v[0] + } else { + fields[k] = v + } + } + + return nil +} + +func bodyFromRestOfRequestReader(reader *textproto.Reader) string { + var builder strings.Builder + for { + line, err := reader.ReadContinuedLine() + if errors.Is(err, io.EOF) { + break + } + builder.WriteString(line) + } + + return builder.String() +} + +func decomposeResponseForLogging(res *http.Response) (map[string]interface{}, error) { + fields := make(map[string]interface{}, len(res.Header)+4) + fields[FieldHttpOperationType] = OperationHttpResponse + + fields[FieldHttpResponseProtoVersion] = res.Proto + fields[FieldHttpResponseStatusCode] = res.StatusCode + fields[FieldHttpResponseStatusReason] = res.Status + + // Set the headers as fields to log + for k, v := range res.Header { + if len(v) == 1 { + fields[k] = v[0] + } else { + fields[k] = v + } + } + + // Read the whole response body + resBody, err := io.ReadAll(res.Body) + if err != nil { + return nil, err + } + + // Wrap the bytes from the response body, back into an io.ReadCloser, + // to respect the interface of http.Response, as expected by users of the + // http.Client + res.Body = io.NopCloser(bytes.NewBuffer(resBody)) + + fields[FieldHttpResponseBody] = string(resBody) + + return fields, nil +} diff --git a/helper/logging/logging_http_transport_test.go b/helper/logging/logging_http_transport_test.go new file mode 100644 index 00000000000..b9c986fe5fc --- /dev/null +++ b/helper/logging/logging_http_transport_test.go @@ -0,0 +1,286 @@ +package logging_test + +import ( + "bytes" + "context" + "io" + "net/http" + "regexp" + "testing" + "time" + + "github.com/google/go-cmp/cmp" + "github.com/hashicorp/go-uuid" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-log/tflogtest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging" +) + +func TestNewLoggingHTTPTransport(t *testing.T) { + ctx, loggerOutput := setupRootLogger() + + transport := logging.NewLoggingHTTPTransport(http.DefaultTransport) + client := http.Client{ + Transport: transport, + Timeout: 10 * time.Second, + } + + reqBody := `An example + multiline + request body` + req, _ := http.NewRequest("GET", "https://www.terraform.io", bytes.NewBufferString(reqBody)) + res, err := client.Do(req.WithContext(ctx)) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer res.Body.Close() + + entries, err := tflogtest.MultilineJSONDecode(loggerOutput) + if err != nil { + t.Fatalf("log outtput parsing failed: %v", err) + } + + if len(entries) != 2 { + t.Fatalf("unexpected amount of logs produced; expected 2, got %d", len(entries)) + } + + if transId, ok := entries[0]["tf_http_trans_id"]; !ok || transId != entries[1]["tf_http_trans_id"] { + t.Fatalf("expected to find the same 'tf_http_trans_id' in both req/res entries, got %q", transId) + } + + transId, ok := entries[0]["tf_http_trans_id"].(string) + if !ok { + t.Fatalf("expected 'tf_http_trans_id' to be a string, got %T", transId) + } + + if _, err := uuid.ParseUUID(transId); err != nil { + t.Fatalf("expected 'tf_http_trans_id' to be contain a valid UUID, but got an error: %v", err) + } + + reqEntry := entries[0] + if diff := cmp.Diff(reqEntry, map[string]interface{}{ + "@level": "debug", + "@message": "Sending HTTP Request", + "@module": "provider", + "tf_http_op_type": "request", + "tf_http_req_method": "GET", + "tf_http_req_uri": "/", + "tf_http_req_version": "HTTP/1.1", + "tf_http_req_body": "An example multiline request body", + "tf_http_trans_id": transId, + "Accept-Encoding": "gzip", + "Host": "www.terraform.io", + "User-Agent": "Go-http-client/1.1", + "Content-Length": "37", + }); diff != "" { + t.Fatalf("unexpected difference for logging of the request:\n%s", diff) + } + + resEntry := entries[1] + expectedResEntryFields := map[string]interface{}{ + "@level": "debug", + "@module": "provider", + "@message": "Received HTTP Response", + "Content-Type": "text/html", + "tf_http_op_type": "response", + "tf_http_res_status_code": float64(200), + "tf_http_res_version": "HTTP/2.0", + "tf_http_res_status_reason": "200 OK", + "tf_http_trans_id": transId, + } + for ek, ev := range expectedResEntryFields { + if resEntry[ek] != ev { + t.Fatalf("Unexpected value for field %q; expected %q, got %q", ek, ev, resEntry[ek]) + } + } + + expectedNonEmptyEntryFields := []string{ + "tf_http_res_body", "Etag", "Date", "X-Frame-Options", "Server", + } + for _, ek := range expectedNonEmptyEntryFields { + if ev, ok := resEntry[ek]; !ok || ev == "" { + t.Fatalf("Expected field %q to contain a non-null value", ek) + } + } +} + +func TestNewSubsystemLoggingHTTPTransport(t *testing.T) { + subsys := "test-subsystem" + + ctx, loggerOutput := setupRootLogger() + ctx = tflog.NewSubsystem(ctx, subsys) + + transport := logging.NewSubsystemLoggingHTTPTransport(subsys, http.DefaultTransport) + client := http.Client{ + Transport: transport, + Timeout: 10 * time.Second, + } + + reqBody := `An example + multiline + request body` + req, _ := http.NewRequest("GET", "https://www.terraform.io", bytes.NewBufferString(reqBody)) + res, err := client.Do(req.WithContext(ctx)) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer res.Body.Close() + + entries, err := tflogtest.MultilineJSONDecode(loggerOutput) + if err != nil { + t.Fatalf("log outtput parsing failed: %v", err) + } + + if len(entries) != 2 { + t.Fatalf("unexpected amount of logs produced; expected 2, got %d", len(entries)) + } + + if transId, ok := entries[0]["tf_http_trans_id"]; !ok || transId != entries[1]["tf_http_trans_id"] { + t.Fatalf("expected to find the same 'tf_http_trans_id' in both req/res entries, got %q", transId) + } + + transId, ok := entries[0]["tf_http_trans_id"].(string) + if !ok { + t.Fatalf("expected 'tf_http_trans_id' to be a string, got %T", transId) + } + + if _, err := uuid.ParseUUID(transId); err != nil { + t.Fatalf("expected 'tf_http_trans_id' to be contain a valid UUID, but got an error: %v", err) + } + + reqEntry := entries[0] + if diff := cmp.Diff(reqEntry, map[string]interface{}{ + "@level": "debug", + "@message": "Sending HTTP Request", + "@module": "provider.test-subsystem", + "tf_http_op_type": "request", + "tf_http_req_method": "GET", + "tf_http_req_uri": "/", + "tf_http_req_version": "HTTP/1.1", + "tf_http_req_body": "An example multiline request body", + "tf_http_trans_id": transId, + "Accept-Encoding": "gzip", + "Host": "www.terraform.io", + "User-Agent": "Go-http-client/1.1", + "Content-Length": "37", + }); diff != "" { + t.Fatalf("unexpected difference for logging of the request:\n%s", diff) + } + + resEntry := entries[1] + expectedResEntryFields := map[string]interface{}{ + "@level": "debug", + "@module": "provider.test-subsystem", + "@message": "Received HTTP Response", + "Content-Type": "text/html", + "tf_http_op_type": "response", + "tf_http_res_status_code": float64(200), + "tf_http_res_version": "HTTP/2.0", + "tf_http_res_status_reason": "200 OK", + "tf_http_trans_id": transId, + } + for ek, ev := range expectedResEntryFields { + if resEntry[ek] != ev { + t.Fatalf("Unexpected value for field %q; expected %q, got %q", ek, ev, resEntry[ek]) + } + } + + expectedNonEmptyEntryFields := []string{ + "tf_http_res_body", "Etag", "Date", "X-Frame-Options", "Server", + } + for _, ek := range expectedNonEmptyEntryFields { + if ev, ok := resEntry[ek]; !ok || ev == "" { + t.Fatalf("Expected field %q to contain a non-null value", ek) + } + } +} + +func TestNewLoggingHTTPTransport_LogMasking(t *testing.T) { + ctx, loggerOutput := setupRootLogger() + ctx = tflog.MaskFieldValuesWithFieldKeys(ctx, "tf_http_op_type") + ctx = tflog.MaskAllFieldValuesRegexes(ctx, regexp.MustCompile(`.*`)) + ctx = tflog.MaskMessageStrings(ctx, "Request", "Response") + + transport := logging.NewLoggingHTTPTransport(http.DefaultTransport) + client := http.Client{ + Transport: transport, + Timeout: 10 * time.Second, + } + + req, _ := http.NewRequest("GET", "https://www.terraform.io", nil) + res, err := client.Do(req.WithContext(ctx)) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer res.Body.Close() + resBody, err := io.ReadAll(res.Body) + if err != nil { + t.Fatalf("failed to read response body: %v", err) + } + + entries, err := tflogtest.MultilineJSONDecode(loggerOutput) + if err != nil { + t.Fatalf("log outtput parsing failed: %v", err) + } + + if len(entries) != 2 { + t.Fatalf("unexpected amount of logs produced; expected 2, got %d", len(entries)) + } + + if diff := cmp.Diff(entries[0]["@message"], "Sending HTTP ***"); diff != "" { + t.Fatalf("unexpected difference for logging message of request:\n%s", diff) + } + + if diff := cmp.Diff(entries[1]["@message"], "Received HTTP ***"); diff != "" { + t.Fatalf("unexpected difference for logging message of response:\n%s", diff) + } + + expectedMaskedEntryFields := map[string]interface{}{ + "tf_http_op_type": "***", + "tf_http_res_body": "***", + } + for _, entry := range entries { + for expectedK, expectedV := range expectedMaskedEntryFields { + if entryV, ok := entry[expectedK]; ok && entryV != expectedV { + t.Fatalf("Unexpected value for field %q; expected %q, got %q", expectedK, expectedV, entry[expectedK]) + } + } + } + + if diff := cmp.Diff(entries[1]["tf_http_res_body"], string(resBody)); diff == "" { + t.Fatalf("expected HTTP response body and content of field 'tf_http_res_body' to differ, but they do not") + } +} + +func TestNewLoggingHTTPTransport_LogOmitting(t *testing.T) { + ctx, loggerOutput := setupRootLogger() + ctx = tflog.OmitLogWithMessageRegexes(ctx, regexp.MustCompile("(?i)rEsPoNsE")) + ctx = tflog.OmitLogWithFieldKeys(ctx, "tf_http_req_method") + + transport := logging.NewLoggingHTTPTransport(http.DefaultTransport) + client := http.Client{ + Transport: transport, + Timeout: 10 * time.Second, + } + + req, _ := http.NewRequest("GET", "https://www.terraform.io", nil) + res, err := client.Do(req.WithContext(ctx)) + if err != nil { + t.Fatalf("request failed: %v", err) + } + defer res.Body.Close() + + entries, err := tflogtest.MultilineJSONDecode(loggerOutput) + if err != nil { + t.Fatalf("log outtput parsing failed: %v", err) + } + + if len(entries) != 0 { + t.Fatalf("unexpected amount of logs produced; expected 0 (because they should have been omitted), got %d", len(entries)) + } +} + +func setupRootLogger() (context.Context, *bytes.Buffer) { + var output bytes.Buffer + return tflogtest.RootLogger(context.Background(), &output), &output +} diff --git a/helper/logging/transport.go b/helper/logging/transport.go index 6419605e799..bda3813d961 100644 --- a/helper/logging/transport.go +++ b/helper/logging/transport.go @@ -41,6 +41,15 @@ func (t *transport) RoundTrip(req *http.Request) (*http.Response, error) { return resp, nil } +// NewTransport creates a wrapper around a *http.RoundTripper, +// designed to be used for the `Transport` field of http.Client. +// +// This logs each pair of HTTP request/response that it handles. +// The logging is done via Go standard library `log` package. +// +// Deprecated: This will log the content of every http request/response +// at `[DEBUG]` level, without any filtering. Any sensitive information +// will appear as-is in your logs. Please use NewSubsystemLoggingHTTPTransport instead. func NewTransport(name string, t http.RoundTripper) *transport { return &transport{name, t} } 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/helper/schema/provider_test.go b/helper/schema/provider_test.go index 0445e0ef880..e791df50e43 100644 --- a/helper/schema/provider_test.go +++ b/helper/schema/provider_test.go @@ -1002,8 +1002,7 @@ func TestProviderUserAgentAppendViaEnvVar(t *testing.T) { for i, tc := range testCases { t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { - os.Unsetenv(uaEnvVar) - os.Setenv(uaEnvVar, tc.envVarValue) + t.Setenv(uaEnvVar, tc.envVarValue) p := &Provider{TerraformVersion: "4.5.6"} givenUA := p.UserAgent(tc.providerName, tc.providerVersion) if givenUA != tc.expected { diff --git a/helper/schema/schema.go b/helper/schema/schema.go index 7cbd5858917..b4a95d07530 100644 --- a/helper/schema/schema.go +++ b/helper/schema/schema.go @@ -311,11 +311,32 @@ type Schema struct { // "parent_block_name.0.child_attribute_name". RequiredWith []string - // Deprecated indicates the message to include in a warning diagnostic to - // practitioners when this attribute is configured. Typically this is used - // to signal that this attribute will be removed in the future and provide - // next steps to the practitioner, such as using a different attribute, - // different resource, or if it should just be removed. + // Deprecated defines warning diagnostic details to display to + // practitioners configuring this attribute or block. The warning + // diagnostic summary is automatically set to "Argument is deprecated" + // along with configuration source file and line information. + // + // This warning diagnostic is only displayed during Terraform's validation + // phase when this field is a non-empty string, when the attribute is + // Required or Optional, and if the practitioner configuration attempts to + // set the attribute value to a known value. It cannot detect practitioner + // configuration values that are unknown ("known after apply"). + // + // This field has no effect when the attribute is Computed-only (read-only; + // not Required or Optional) and a practitioner attempts to reference + // this attribute value in their configuration. There is a Terraform + // feature request to support this type of functionality: + // + // https://github.com/hashicorp/terraform/issues/7569 + // + // Set this field to a practitioner actionable message such as: + // + // - "Configure other_attribute instead. This attribute will be removed + // in the next major version of the provider." + // - "Remove this attribute's configuration as it no longer is used and + // the attribute will be removed in the next major version of the + // provider." + // Deprecated string // ValidateFunc allows individual fields to define arbitrary validation 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/README.md b/website/README.md new file mode 100644 index 00000000000..e25f491afdf --- /dev/null +++ b/website/README.md @@ -0,0 +1,42 @@ +# Terraform Documentation + +This directory contains the portions of [the Terraform website][terraform.io] that pertain to the Terraform Plugin SDK. + +The files in this directory are intended to be used in conjunction with +[the `terraform-website` repository](https://github.com/hashicorp/terraform-website), which brings all of the +different documentation sources together and contains the scripts for testing and building the site as +a whole. + +## Updating Sidebar Navigation + +You must update the sidebar navigation for the `terraform-plugin-sdk` documentation any time that you add or delete a documentation page. The website builds the sidebar navigation menu from the [nav-data] JSON file. For more details about how to update this file, refer to https://github.com/hashicorp/terraform-website#editing-navigation-sidebars. + +## Adding Redirects + +You must add a redirect when you move, rename, or delete documentation pages. Refer to https://github.com/hashicorp/terraform-website#redirects for details. + +## Previewing Changes + +You should preview your changes locally to ensure that the content is rendering properly before you create a pull request. The build includes content from this repository and the [`terraform-website`](https://github.com/hashicorp/terraform-website/) repository, allowing you to preview the entire Terraform documentation site. + +To preview your content, complete the following steps: + +**Set Up Local Environment** + +1. [Install Docker](https://docs.docker.com/get-docker/). +1. Restart your terminal or command line session. + +**Launch Site Locally** + +1. Navigate into your local `terraform-plugin-sdk` top-level directory and run `make website`. +1. Open `http://localhost:3000` in your web browser. While the preview is running, you can edit pages and Next.js will automatically rebuild them. +1. When you're done with the preview, press `ctrl-C` in your terminal to stop the server. + +## Deployment + +The website reads content from release tags to generate documentation for all versions of `terraform-plugin-sdk` documentation. Changes merged into `main` will be included in the documentation for the next product release. + +You cannot edit documentation for past versions of `terraform-plugin-sdk` on the site. Documentation is an artifact of a product release. We push docs fixes forward for the next release, rather than retroactively fixing older versions. + +[nav-data]: ../website/data/plugin-sdk-nav-data.json +[terraform.io]: https://www.terraform.io/ \ No newline at end of file diff --git a/website/data/plugin-sdk-nav-data.json b/website/data/plugin-sdk-nav-data.json index aa1ca1695ef..855382810cc 100644 --- a/website/data/plugin-sdk-nav-data.json +++ b/website/data/plugin-sdk-nav-data.json @@ -40,6 +40,68 @@ } ] }, + { + "title": "Logging", + "routes": [ + { + "title": "Overview", + "path": "logging" + }, + { + "title": "Writing Logs", + "href": "/plugin/log/writing" + }, + { + "title": "Filtering Logs", + "href": "/plugin/log/filtering" + }, + { + "title": "HTTP Transport", + "path": "logging/http-transport" + } + ] + }, + { + "title": "Testing", + "routes": [ + { "title": "Overview", "path": "testing" }, + { + "title": "Acceptance Testing", + "routes": [ + { + "title": "Overview", + "path": "testing/acceptance-tests" + }, + { + "title": "Test Cases", + "path": "testing/acceptance-tests/testcase" + }, + { + "title": "Test Steps", + "path": "testing/acceptance-tests/teststep" + }, + { + "title": "Sweepers", + "path": "testing/acceptance-tests/sweepers" + } + ] + }, + { + "title": "Testing API", + "path": "testing/testing-api", + "hidden": true + }, + { + "title": "Testing Patterns", + "path": "testing/testing-patterns", + "hidden": true + }, + { + "title": "Unit Testing", + "path": "testing/unit-testing" + } + ] + }, { "title": "Debugging Providers", "path": "debugging" }, { "title": "Upgrade Guides", @@ -95,46 +157,5 @@ "path": "best-practices/other-languages" } ] - }, - { - "title": "Testing", - "routes": [ - { "title": "Overview", "path": "testing" }, - { - "title": "Acceptance Testing", - "routes": [ - { - "title": "Overview", - "path": "testing/acceptance-tests" - }, - { - "title": "Test Cases", - "path": "testing/acceptance-tests/testcase" - }, - { - "title": "Test Steps", - "path": "testing/acceptance-tests/teststep" - }, - { - "title": "Sweepers", - "path": "testing/acceptance-tests/sweepers" - } - ] - }, - { - "title": "Testing API", - "path": "testing/testing-api", - "hidden": true - }, - { - "title": "Testing Patterns", - "path": "testing/testing-patterns", - "hidden": true - }, - { - "title": "Unit Testing", - "path": "testing/unit-testing" - } - ] } ] 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/logging/http-transport.mdx b/website/docs/plugin/sdkv2/logging/http-transport.mdx new file mode 100644 index 00000000000..89233ca259c --- /dev/null +++ b/website/docs/plugin/sdkv2/logging/http-transport.mdx @@ -0,0 +1,203 @@ +--- +page_title: Plugin Development - Logging HTTP Transport +description: |- + SDKv2 provides a helper to send all the HTTP transactions to structured logging. +--- + +# HTTP Transport + +Terraform's public interface has included `helper/logging`: [`NewTransport()`](https://github.com/hashicorp/terraform-plugin-sdk/blob/main/helper/logging/transport.go) since v0.9.5. This helper is an implementation of the Golang standard library [`http.RoundTripper`](https://pkg.go.dev/net/http#RoundTripper) that lets you add logging at the `DEBUG` level to your provider's HTTP transactions. + +We do not recommend using this original helper because it is designed to log the entirety of each request and response. This includes any sensitive content that may be present in the message header or body, presenting security concerns. + +Instead, we recommend using the [terraform-plugin-log](https://www.terraform.io/plugin/log) library to produce logs for your provider. This library does not present the same security concerns and provides [log filtering](https://www.terraform.io/plugin/log/filtering) functionality. This page explains how to set up the new `RoundTripper()` helper to log HTTP Transactions with `terraform-plugin-log`. + +# Setting Up Logging for HTTP Transactions + +The recommended logging helper for SDK is built on top of [terraform-plugin-log](https://www.terraform.io/plugin/log). This lets you leverage the features from our structured logging framework without having to write an entire implementation of `http.RoundTripper`. + +There are two functions inside `helper/logging` that target a specific logging setup for your provider. Refer to [“Writing Log Output”](https://www.terraform.io/plugin/log/writing) for details. + +* `NewLoggingHTTPTransport(transport http.RoundTripper)`: Use this method when you want logging against the `tflog` Provider root logger. +* `NewSubsystemLoggingHTTPTransport(subsystem string, transport http.RoundTripper)`: Use this method when you want logging against a `tflog` Provider [Subsystem logger](https://www.terraform.io/plugin/log/writing#subsystems). The `subsystem` string you use with `NewSubsystemLoggingHTTPTransport()` must match the [pre-created subsystem logger name](https://www.terraform.io/plugin/log/writing#create-subsystems). + +To set up HTTP transport, you must create the HTTP Client to use the new transport and then add logging configuration to the HTTP request context. + +### Creating the HTTP Client + +After you create the transport , you must use it to set up the `http.Client` for the provider. The following example sets up the client in `schema.Provider` `ConfigureContextFunc`. The client is identical to the default Golang `http.Client`, except it uses the new logging transport. + +```go +func New() (*schema.Provider, error) { + return &schema.Provider{ + // omitting the rest of the schema definition + + ConfigureContextFunc: func (ctx context.Context, rsc *schema.ResourceData) (interface{}, diag.Diagnostics) { + + // omitting provider-specific configuration logic + + transport := logging.NewLoggingHTTPTransport(http.DefaultTransport) + client := http.Client{ + Transport: transport, + } + + return client, diag.Diagnostics{} + } + } +} +``` + +## Adding Context to HTTP Requests + +All calls to the `tflog` package must contain an SDK provided `context.Context` that stores the logging implementation. Providers written with `terraform-plugin-sdk` must use context-aware functionality, such as the [`helper/schema.Resource` type `ReadContext` field](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema#Resource.ReadContext). + +The following example uses [`http.NewRequestWithContext()` function](https://pkg.go.dev/net/http#NewRequestWithContext) to create an HTTP request that includes the logging configuration from the `context.Context`. + +```go +// inside a context-aware Resource function +req, err := http.NewRequestWithContext(ctx, "GET", "https://www.terraform.io", nil) +if err != nil { + return fmt.Errorf("Failed to create a new request: %w", err) +} + +res, err := client.Do(req) +if err != nil { + return fmt.Errorf("Request failed: %w", err) +} +defer res.Body.Close() +``` + +Use the [`(http.Request).WithContext()` method](https://pkg.go.dev/net/http#Request.WithContext) to set the context for the `http.Request` if the request is generated separately from where the `context.Context` is available. + +## HTTP Transaction Log Format + +The logging transport produces two log entries for each HTTP transaction: one for the request and one for the response. + +### Request Example + +The following example shows a log generated from an HTTP Request to [https://terraform.io](https://terraform.io). + +```text +2022-07-26T18:54:08.880+0100 [DEBUG] provider: Sending HTTP Request: Accept-Encoding=gzip Content-Length=0 \ + Host=www.terraform.io User-Agent=Go-http-client/1.1 \ + tf_http_op_type=request tf_http_req_method=GET \ + tf_http_req_uri=/ tf_http_req_version=HTTP/1.1 tf_http_trans_id=7e80e48d-8f32-f527-1412-52a8c84359e7 +``` + +The following example shows the same logs after you enable [logging in JSON format](https://www.terraform.io/internals/debugging). + +```json +{ + "@level": "debug", + "@message": "Sending HTTP Request", + "@module": "provider", + "@timestamp": "2022-07-26T18:54:08.880+0100", + + "Accept-Encoding": "gzip", + "Content-Length": "0", + "Host": "www.terraform.io", + "User-Agent": "Go-http-client/1.1", + + "tf_http_op_type": "request", + "tf_http_req_body": "", + "tf_http_req_method": "GET", + "tf_http_req_uri": "/", + "tf_http_req_version": "HTTP/1.1", + "tf_http_trans_id": "7e80e48d-8f32-f527-1412-52a8c84359e7" +} +``` + +### Response Example + +The following example shows logs from a [https://terraform.io](https://terraform.io) HTTP response. + +```text +2022-07-26T18:54:10.734+0100 [DEBUG] provider: Received HTTP Response: Age=9 \ + Cache-Control="public, max-age=0, must-revalidate" Content-Type=text/html \ + Date="Tue, 26 Jul 2022 13:16:46 GMT" Etag="... ABCDEFGH..." Server=Vercel \ + Strict-Transport-Security="max-age=63072000" X-Frame-Options=SAMEORIGIN X-Matched-Path=/ \ + X-Nextjs-Cache=HIT X-Powered-By=Next.js X-Vercel-Cache=STALE \ + X-Vercel-Id=lhr1::iad1::lx2h8-99999999999999-fffffffffff \ + tf_http_op_type=response tf_http_res_body="... LOTS OF HTML ..." tf_http_res_status_code=200 \ + tf_http_res_status_reason="200 OK" tf_http_res_version=HTTP/2.0 \ + tf_http_trans_id=7e80e48d-8f32-f527-1412-52a8c84359e7 +``` + +the following example shows the same logs in JSON format. + +```json +{ + "@level": "debug", + "@message": "Received HTTP Response", + "@module": "provider", + "@timestamp": "2022-07-26T18:54:10.734+0100", + + "Age": "9", + "Cache-Control": "public, max-age=0, must-revalidate", + "Content-Type": "text/html", + "Date": "Tue, 26 Jul 2022 13:16:46 GMT", + "Etag": "... ABCDEFGH...", + "Server": "Vercel", + "Strict-Transport-Security": "max-age=63072000", + "X-Frame-Options": "SAMEORIGIN", + "X-Matched-Path": "/", + "X-Nextjs-Cache": "HIT", + "X-Powered-By": "Next.js", + "X-Vercel-Cache": "STALE", + "X-Vercel-Id": "lhr1::iad1::lx2h8-99999999999999-fffffffffff", + + "tf_http_op_type": "response", + "tf_http_res_body": "... LOTS OF HTML ...", + "tf_http_res_status_code": 200, + "tf_http_res_status_reason": "200 OK", + "tf_http_res_version": "HTTP/2.0", + "tf_http_trans_id": "7e80e48d-8f32-f527-1412-52a8c84359e7" +} +``` + +### Log Information + +Each log contains the following information, which is represented as [fields](https://www.terraform.io/plugin/log/writing#fields) in the JSON format. + +| Log field name | Description | Possible values | Applies to | +|---------------------------:|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------------------------------------------|:------------------:| +| `tf_http_op_type` | Which HTTP operation log refers to | [`request`, `response`] | Request / Response | +| `tf_http_trans_id` | Unique identifier used by Request and Response that belong to the same HTTP Transaction | A universally unique identifier ([UUID](https://en.wikipedia.org/wiki/Universally_unique_identifier)) | Request / Response | +| `tf_http_req_body` | Request body | | Request | +| `tf_http_req_method` | Request method | A canonical [HTTP methods](https://developer.mozilla.org/en-US/docs/Web/HTTP/Method) | Request | +| `tf_http_req_uri` | Request URI | Ex. `"/path"` | Request | +| `tf_http_req_version` | Request HTTP version | Ex. `"HTTP/1.1"` | Request | +| `tf_http_res_body` | Response body | | Response | +| `tf_http_res_status_code` | Response status code | A canonical [HTTP status code](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status) | Response | +| `tf_http_res_status_reason | Response status reason | Canonical textual description of the corresponding `tf_http_res_status_code` | Response | +| `tf_http_res_version` | Response HTTP version | Ex. `"HTTP/2.0"` | Response | +| (Other fields) | Request / Response headers. One field per header. If the header contains a single value, the log field value is set to that value. Otherwise, the field value is a slice of strings. | | Request / Response | + +## Filtering Sensitive Data + +To [filter logs](https://www.terraform.io/plugin/log/filtering), you must configure the `context.Context` before before it is added to the `http.Request`. + +The following example masks all the header values of HTTP Requests containing an `Authorization` and `Proxy-Authorization` credentials. + +```go +// inside a context-aware Resource function +ctx := tflog.SubsystemMaskFieldValuesWithFieldKeys(ctx, "my-subsystem", "Authorization") +ctx = tflog.SubsystemMaskFieldValuesWithFieldKeys(ctx, "my-subsystem", "Proxy-Authorization") + +req, err := http.NewRequestWithContext(ctx, "GET", "https://www.terraform.io", nil) +if err != nil { + return fmt.Errorf("Failed to create a new request: %w", err) +} + +res, err := client.Do(req) +if err != nil { + return fmt.Errorf("Request failed: %w", err) +} +defer res.Body.Close() +``` + +# Links + +* [Plugin Development - Logging](https://www.terraform.io/plugin/log) - Learn more about the logging framework +* [terraform-plugin-log - tflog](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-log/tflog) - Read the Golang documentation for the logging framework +* Read the Golang documentation for [`NewLoggingHTTPTransport()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging#NewLoggingHTTPTransport) and [`NewSubsystemLoggingHTTPTransport()`](https://pkg.go.dev/github.com/hashicorp/terraform-plugin-sdk/v2/helper/logging#NewSubsystemLoggingHTTPTransport) diff --git a/website/docs/plugin/sdkv2/logging/index.mdx b/website/docs/plugin/sdkv2/logging/index.mdx new file mode 100644 index 00000000000..b5f6fe4b7c7 --- /dev/null +++ b/website/docs/plugin/sdkv2/logging/index.mdx @@ -0,0 +1,25 @@ +--- +page_title: Plugin Development - Logging +description: |- + High-quality logs are important when debugging your provider. Learn to set-up logging and write meaningful logs. +--- + +# Logging + +Terraform Plugin SDKv2 integrates with the structured logging framework [terraform-plugin-log](https://www.terraform.io/plugin/log). High-quality logs are critical to quickly [debugging your provider](https://www.terraform.io/plugin/debugging). + +## Managing Log Output + +Learn how to use [environment variables and other methods](https://www.terraform.io/plugin/log/managing) to enable and filter logs. + +## Writing Log Output + +Learn how to [implement code in provider logic](https://www.terraform.io/plugin/log/writing) to output logs. + +## Filtering Log Output + +Learn how to [implement code in provider logic](https://www.terraform.io/plugin/log/filtering) to omit logs or mask specific log messages and structured log fields. + +## Log HTTP Transactions + +Learn how to [set up the Logging HTTP Transport](./logging/http-transport) to log HTTP Transactions with the [structured logging framework](https://www.terraform.io/plugin/log). diff --git a/website/docs/plugin/sdkv2/schemas/schema-methods.mdx b/website/docs/plugin/sdkv2/schemas/schema-methods.mdx index 8b21ba83bf2..f2c47dccf09 100644 --- a/website/docs/plugin/sdkv2/schemas/schema-methods.mdx +++ b/website/docs/plugin/sdkv2/schemas/schema-methods.mdx @@ -132,11 +132,22 @@ ComputedWhen []string // key. ConflictsWith []string -// When Deprecated is set, this attribute is deprecated. +// When Deprecated is set, this attribute is deprecated and a warning +// diagnostic will automatically be raised when it is configured. // // A deprecated field still works, but will probably stop working in near // future. This string is the message shown to the user with instructions on // how to address the deprecation. +// +// This warning diagnostic is only displayed during Terraform's validation +// phase when the attribute is Required or Optional and if the practitioner +// configuration attempts to set the attribute value to a known value. It +// cannot detect practitioner configuration values that are unknown ("known +// after apply"). +// +// This field has no effect when the attribute is Computed-only (read-only; +// not Required or Optional) and a practitioner attempts to reference +// this attribute value in their configuration. Deprecated string // When Removed is set, this attribute has been removed from the schema 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