├── .editorconfig ├── .gitattributes ├── .github ├── dependabot.yml └── workflows │ ├── release.yml │ └── tests.yml ├── .gitignore ├── .goreleaser.yml ├── .vscode └── settings.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── docker-compose └── docker-compose.yml ├── docs ├── index.md └── resources │ ├── login.md │ └── user.md ├── examples ├── azure │ └── main.tf ├── fedauth │ └── main.tf └── local │ └── main.tf ├── go.mod ├── go.sum ├── main.go ├── mssql ├── const.go ├── model │ ├── connector_factory.go │ ├── login.go │ ├── provider.go │ └── user.go ├── provider.go ├── provider_test.go ├── resource_login.go ├── resource_login_import_test.go ├── resource_login_test.go ├── resource_user.go ├── resource_user_import_test.go ├── resource_user_test.go ├── server.go └── utils.go ├── scripts ├── aliases.sh └── versions.sh ├── sql ├── login.go ├── sql.go └── user.go ├── test-fixtures ├── all │ ├── .gitignore │ ├── main.tf │ ├── providers.tf │ ├── terraform.tfvars │ ├── variables.tf │ └── versions.tf └── local │ ├── main.tf │ ├── providers.tf │ ├── variables.tf │ └── versions.tf └── wait-for /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig is awesome: https://EditorConfig.org 2 | 3 | # Top-most EditorConfig file 4 | root = true 5 | 6 | [*] 7 | charset = utf-8 8 | end_of_line = lf 9 | indent_size = 2 10 | indent_style = space 11 | insert_final_newline = true 12 | trim_trailing_whitespace = true 13 | 14 | [*.sh] 15 | indent_size = 4 16 | 17 | [Makefile] 18 | indent_size = 8 19 | indent_style = tab 20 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Set default behavior to automatically normalize line endings. 2 | * text=auto 3 | 4 | # Force bash scripts to always use lf line endings. 5 | *.sh text eol=lf 6 | 7 | # Force go source to always use lf line endings.always 8 | *.go text eol=lf 9 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | 4 | # Maintain dependencies for GitHub Actions 5 | - package-ecosystem: "github-actions" 6 | directory: "/" 7 | schedule: 8 | interval: "weekly" 9 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This GitHub action can publish assets for release when a tag is created. 2 | # Currently its setup to run on any tag that matches the pattern "v*" (ie. v0.1.0). 3 | # 4 | # This uses an action (paultyng/ghaction-import-gpg) that assumes you set your 5 | # private key in the `GPG_PRIVATE_KEY` secret and passphrase in the `PASSPHRASE` 6 | # secret. If you would rather own your own GPG handling, please fork this action 7 | # or use an alternative one for key handling. 8 | # 9 | # You will need to pass the `--batch` flag to `gpg` in your signing step 10 | # in `goreleaser` to indicate this is being used in a non-interactive mode. 11 | # 12 | name: release 13 | on: 14 | push: 15 | tags: 16 | - 'v*' 17 | jobs: 18 | goreleaser: 19 | runs-on: ubuntu-22.04 20 | steps: 21 | - 22 | name: Checkout 23 | uses: actions/checkout@v4 24 | with: 25 | fetch-depth: 0 26 | - 27 | name: Set up Go 28 | uses: actions/setup-go@v5 29 | with: 30 | go-version-file: 'go.mod' 31 | cache: true 32 | - 33 | name: Import GPG key 34 | id: import_gpg 35 | uses: crazy-max/ghaction-import-gpg@v6 36 | with: 37 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 38 | passphrase: ${{ secrets.PASSPHRASE }} 39 | - 40 | name: Run GoReleaser 41 | uses: goreleaser/goreleaser-action@v5 42 | with: 43 | version: latest 44 | args: release --rm-dist 45 | env: 46 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 47 | GPG_FINGERPRINT: ${{ steps.import_gpg.outputs.fingerprint }} 48 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run tests 2 | 3 | on: 4 | push: 5 | branches: [ "master" ] 6 | pull_request: 7 | branches: [ "master" ] 8 | 9 | jobs: 10 | test: 11 | 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | 17 | - name: Run unit tests 18 | run: make test 19 | 20 | - name: Run acceptance tests 21 | run: | 22 | make docker-start 23 | sh -c 'TESTARGS=-count=1 ./wait-for localhost:1433 -- make testacc-local' 24 | make docker-stop 25 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Local .terraform directories 2 | **/.terraform/* 3 | 4 | # .tfstate files 5 | *.tfstate 6 | *.tfstate.* 7 | 8 | # Terraform lock files 9 | *.lock.hcl 10 | 11 | # Crash log files 12 | crash.log 13 | 14 | # Ignore any .tfvars files that are generated automatically for each Terraform run. Most 15 | # .tfvars files are managed as part of configuration and so should be included in 16 | # version control. 17 | # 18 | # example.tfvars 19 | 20 | # Ignore override files as they are usually used to override resources locally and so 21 | # are not checked in 22 | override.tf 23 | override.tf.json 24 | *_override.tf 25 | *_override.tf.json 26 | 27 | # Include override files you do wish to add to version controll using negated pattern 28 | # 29 | # !example_override.tf 30 | 31 | # Include tfplan files to ignore the plan output of command: terraform plan -out=tfplan 32 | # example: *tfplan* 33 | 34 | # Ignore CLI configuration files 35 | .terraformrc 36 | terraform.rc 37 | terraform 38 | 39 | # VS Code 40 | .vscode/* 41 | !.vscode/settings.json 42 | !.vscode/tasks.json 43 | !.vscode/launch.json 44 | !.vscode/extensions.json 45 | *.code-workspace 46 | 47 | # JetBrains 48 | .idea/* 49 | 50 | # Local ignores 51 | .DS_Store 52 | /bin/ 53 | /dist/ 54 | .local.env 55 | terraform-provider-mssql 56 | terraform-provider-mssql.log 57 | terraform-provider-mssql.exe 58 | /.devcontainer/ 59 | -------------------------------------------------------------------------------- /.goreleaser.yml: -------------------------------------------------------------------------------- 1 | # Visit https://goreleaser.com for documentation on how to customize this behavior. 2 | before: 3 | hooks: 4 | - go mod download 5 | - go mod tidy 6 | builds: 7 | - env: 8 | # goreleaser does not work with CGO, it could also complicate usage by users in CI/CD systems 9 | # like Terraform Cloud where they are unable to install libraries. 10 | - CGO_ENABLED=0 11 | mod_timestamp: '{{ .CommitTimestamp }}' 12 | flags: 13 | - -trimpath 14 | ldflags: 15 | - '-s -w -X main.version={{.Version}} -X main.commit={{.Commit}}' 16 | goos: 17 | - linux 18 | - darwin 19 | - windows 20 | - freebsd 21 | goarch: 22 | - amd64 23 | - '386' 24 | - arm 25 | - arm64 26 | ignore: 27 | - goos: darwin 28 | goarch: '386' 29 | - goos: freebsd 30 | goarch: arm 31 | - goos: freebsd 32 | goarch: arm64 33 | binary: '{{ .ProjectName }}_v{{ .Version }}' 34 | archives: 35 | - format: zip 36 | name_template: '{{ .ProjectName }}_{{ .Version }}_{{ .Os }}_{{ .Arch }}' 37 | checksum: 38 | name_template: '{{ .ProjectName }}_{{ .Version }}_SHA256SUMS' 39 | algorithm: sha256 40 | snapshot: 41 | name_template: "{{ .Tag }}-next" 42 | signs: 43 | - artifacts: checksum 44 | args: 45 | # if you are using this is a GitHub action or some other automated pipeline, you 46 | # need to pass the batch flag to indicate its not interactive. 47 | - "--batch" 48 | - "--local-user" 49 | - "{{ .Env.GPG_FINGERPRINT }}" # set this environment variable for your signing key 50 | - "--output" 51 | - "${signature}" 52 | - "--detach-sign" 53 | - "${artifact}" 54 | release: 55 | # If you want to manually examine the release before its live, uncomment this line: 56 | # draft: true 57 | changelog: 58 | # Set it to true if you wish to skip the changelog generation. 59 | # This may result in an empty release notes on GitHub/GitLab/Gitea. 60 | # skip: true 61 | # sort: asc 62 | filters: 63 | exclude: 64 | - '^docs:' 65 | - '^test:' 66 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), 6 | and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [0.3.1] - 2024-03-27 11 | 12 | ### Added 13 | 14 | - Create login with SID. [PR #74](https://github.com/betr-io/terraform-provider-mssql/pull/74). Closes [#64](https://github.com/betr-io/terraform-provider-mssql/issues/64). Thanks to [ValeruS](https://github.com/ValeruS) for the PR. 15 | 16 | ### Fixed 17 | 18 | - Support read timeout for SQL loop and better error handling. [PR #61](https://github.com/betr-io/terraform-provider-mssql/pull/61). Closese [#25](https://github.com/betr-io/terraform-provider-mssql/issues/25) and [#57](https://github.com/betr-io/terraform-provider-mssql/issues/57) with better error message. Thanks to [Sebastien Coavoux](https://github.com/sebastien-coavoux) for the PR. 19 | 20 | ## [0.3.0] - 2023-12-29 21 | 22 | ### Changed 23 | 24 | - Make minimum terraform version 1.5. Versions less than this are no longer supported ([endoflife.date](https://endoflife.date/terraform)) 25 | - Upgraded to go version 1.21. 26 | - Upgraded dependencies. 27 | - Replaced github.com/denisenkom/go-mssqldb with github.com/microsoft/go-mssqldb. 28 | - Upgraded terraform dependencies. 29 | - Improve Makefile. 30 | 31 | ## [0.2.7] - 2022-12-16 32 | 33 | ### Fixed 34 | 35 | - Fix concurrency issue on user create/update. [PR #52](https://github.com/betr-io/terraform-provider-mssql/pull/52). Closes [#31](https://github.com/betr-io/terraform-provider-mssql/issues/31). Thanks to [Isabel Andrade](https://github.com/beandrad) for the PR. 36 | - Fix role reorder update issue. [PR #53](https://github.com/betr-io/terraform-provider-mssql/pull/53). Closes [#46](https://github.com/betr-io/terraform-provider-mssql/issues/46). Thanks to [Paul Brittain](https://github.com/paulbrittain) for the PR. 37 | 38 | ## [0.2.6] - 2022-11-25 39 | 40 | ### Added 41 | 42 | - Support two of the auth forms available through the new [fedauth](https://github.com/denisenkom/go-mssqldb#azure-active-directory-authentication): `ActiveDirectoryDefault` and `ActiveDirectoryManagedIdentity` (because user-assigned identity) as these are the most useful variants. [PR #42](https://github.com/betr-io/terraform-provider-mssql/pull/42). Closes [#30](https://github.com/betr-io/terraform-provider-mssql/issues/30). Thanks to [Bittrance](https://github.com/bittrance) for the PR. 43 | - Improve docs on managed identities. [PR #39](https://github.com/betr-io/terraform-provider-mssql/pull/36). Thanks to [Alexander Guth](https://github.com/alxy) for the PR. 44 | 45 | ## [0.2.5] - 2022-06-03 46 | 47 | ### Added 48 | 49 | - Add SID as output attribute to the `mssql_user` resource. [PR #36](https://github.com/betr-io/terraform-provider-mssql/pull/36). Closes [#35](https://github.com/betr-io/terraform-provider-mssql/issues/35). Thanks to [rjbell](https://github.com/rjbell) for the PR. 50 | 51 | ### Changed 52 | 53 | - Treat `password` attribute of `mssql_user` as sensitive. Closes [#37](https://github.com/betr-io/terraform-provider-mssql/issues/37). 54 | - Fully qualify package name with Github repository. [PR #38](https://github.com/betr-io/terraform-provider-mssql/pull/38). Thanks to [Ewan Noble](https://github.com/EwanNoble) for the PR. 55 | - Upgraded to go version 1.18 56 | - Upgraded dependencies. 57 | - Upgraded dependencies in test fixtures. 58 | 59 | ### Fixed 60 | 61 | - Only get sql logins if user is not external. [PR #33](https://github.com/betr-io/terraform-provider-mssql/pull/33). Closes [#32](https://github.com/betr-io/terraform-provider-mssql/issues/32). Thanks to [Alexander Guth](https://github.com/alxy) for the PR. 62 | 63 | ## [0.2.4] - 2021-11-15 64 | 65 | Thanks to [Richard Lavey](https://github.com/rlaveycal) ([PR #24](https://github.com/betr-io/terraform-provider-mssql/pull/24)). 66 | 67 | ### Fixed 68 | 69 | - Race condition with String_Split causes failure ([#23](https://github.com/betr-io/terraform-provider-mssql/issues/23)) 70 | 71 | ## [0.2.3] - 2021-09-16 72 | 73 | Thanks to [Matthis Holleville](https://github.com/matthisholleville) ([PR #17](https://github.com/betr-io/terraform-provider-mssql/pull/17)), and [bruno-motacardoso](https://github.com/bruno-motacardoso) ([PR #14](https://github.com/betr-io/terraform-provider-mssql/pull/14)). 74 | 75 | ### Changed 76 | 77 | - Add string split function, which should allow the provider to work on SQL Server 2014 (#17). 78 | - Improved documentation (#14). 79 | 80 | ## [0.2.2] - 2021-08-24 81 | 82 | ### Changed 83 | 84 | - Upgraded to go version 1.17. 85 | - Upgraded dependencies. 86 | - Upgraded dependencies in test fixtures. 87 | 88 | ## [0.2.1] - 2021-04-30 89 | 90 | Thanks to [Anders Båtstrand](https://github.com/anderius) ([PR #8](https://github.com/betr-io/terraform-provider-mssql/pull/8), [PR #9](https://github.com/betr-io/terraform-provider-mssql/pull/9)) 91 | 92 | ### Changed 93 | 94 | - Upgrade go-mssqldb to support go version 1.16. 95 | 96 | ### Fixed 97 | 98 | - Cannot create user because of conflicting collation. ([#6](https://github.com/betr-io/terraform-provider-mssql/issues/6)) 99 | 100 | ## [0.2.0] - 2021-04-06 101 | 102 | When it is not possible to give AD role: _Directory Readers_ to the Sql Server Identity or an AD Group, use *object_id* to add external user. 103 | 104 | Thanks to [Brice Messeca](https://github.com/smag-bmesseca) ([PR #1](https://github.com/betr-io/terraform-provider-mssql/pull/1)) 105 | 106 | ### Added 107 | 108 | - Optional object_id attribute to mssql_user 109 | 110 | ## [0.1.1] - 2020-11-17 111 | 112 | Update documentation and examples. 113 | 114 | ## [0.1.0] - 2020-11-17 115 | 116 | Initial release. 117 | 118 | ### Added 119 | 120 | - Resource `mssql_login` to manipulate logins to a SQL Server. 121 | - Resource `mssql_user` to manipulate users in a SQL Server database. 122 | 123 | [Unreleased]: https://github.com/betr-io/terraform-provider-mssql/compare/v0.2.7...HEAD 124 | [0.2.7]: https://github.com/betr-io/terraform-provider-mssql/compare/v0.2.6...v0.2.7 125 | [0.2.6]: https://github.com/betr-io/terraform-provider-mssql/compare/v0.2.5...v0.2.6 126 | [0.2.5]: https://github.com/betr-io/terraform-provider-mssql/compare/v0.2.4...v0.2.5 127 | [0.2.4]: https://github.com/betr-io/terraform-provider-mssql/compare/v0.2.3...v0.2.4 128 | [0.2.3]: https://github.com/betr-io/terraform-provider-mssql/compare/v0.2.2...v0.2.3 129 | [0.2.2]: https://github.com/betr-io/terraform-provider-mssql/compare/v0.2.1...v0.2.2 130 | [0.2.1]: https://github.com/betr-io/terraform-provider-mssql/compare/v0.2.0...v0.2.1 131 | [0.2.0]: https://github.com/betr-io/terraform-provider-mssql/compare/v0.1.1...v0.2.0 132 | [0.1.1]: https://github.com/betr-io/terraform-provider-mssql/compare/v0.1.0...v0.1.1 133 | [0.1.0]: https://github.com/betr-io/terraform-provider-mssql/releases/tag/v0.1.0 134 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Betr AS 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL := /bin/bash 2 | 3 | VERSION = 0.3.1 4 | 5 | TERRAFORM = terraform 6 | TERRAFORM_VERSION = "~> 1.5" 7 | 8 | GO = go 9 | MODULE = $(shell env GO111MODULE=on $(GO) list -m) 10 | PKGS = $(shell env GO111MODULE=on $(GO) list ./... | grep -v /vendor/) 11 | TESTPKGS = $(shell env GO111MODULE=on $(GO) list -f \ 12 | '{{ if or .TestGoFiles .XTestGoFiles }}{{ .ImportPath }}{{ end }}' \ 13 | $(PKGS)) 14 | 15 | ifeq ($(OS),Windows_NT) 16 | OPERATING_SYSTEM=Windows 17 | ifeq ($(PROCESSOR_ARCHITEW6432),AMD64) 18 | OS_ARCH=windows_amd64 19 | else 20 | ifeq ($(PROCESSOR_ARCHITECTURE),AMD64) 21 | OS_ARCH=windows_amd64 22 | endif 23 | ifeq ($(PROCESSOR_ARCHITECTURE),x86) 24 | OS_ARCH=windows_386 25 | endif 26 | endif 27 | else 28 | UNAME_S := $(shell uname -s) 29 | ifeq ($(UNAME_S),Linux) 30 | OPERATING_SYSTEM=Linux 31 | _OS=linux 32 | endif 33 | ifeq ($(UNAME_S),Darwin) 34 | OPERATING_SYSTEM=MacOS 35 | _OS=darwin 36 | endif 37 | UNAME_P := $(shell uname -p) 38 | ifeq ($(UNAME_P),x86_64) 39 | OS_ARCH=$(_OS)_amd64 40 | endif 41 | ifneq ($(filter %86,$(UNAME_P)),) 42 | OS_ARCH=$(_OS)_386 43 | endif 44 | ifneq ($(filter arm%,$(UNAME_P)),) 45 | OS_ARCH=$(_OS)_arm 46 | endif 47 | endif 48 | 49 | INSTALL_PATH=~/.terraform.d/plugins/$(shell basename $(shell dirname $(MODULE)))/$(shell basename $(MODULE) | cut -d'-' -f3)/${VERSION}/${OS_ARCH} 50 | 51 | default: install 52 | 53 | build: 54 | CGO_ENABLED=0 $(GO) build -o $(shell basename $(MODULE)) 55 | 56 | release: 57 | # Runs goreleaser locally (testrun) 58 | goreleaser release --rm-dist --skip-sign --skip-publish 59 | 60 | install: build 61 | mkdir -p $(INSTALL_PATH) 62 | mv $(shell basename $(MODULE)) $(INSTALL_PATH)/ 63 | 64 | test: 65 | echo $(TESTPKGS) | xargs -t -n4 $(GO) test $(TESTARGS) -timeout=30s -parallel=4 66 | 67 | testacc: 68 | if [ -f .local.env ]; then source .local.env; fi && TF_ACC=1 TERRAFORM_VERSION=$(TERRAFORM_VERSION) $(GO) test $(TESTPKGS) -v $(TESTARGS) -timeout 120m 69 | 70 | testacc-local: 71 | if [ -f .local.env ]; then source .local.env; fi && TF_ACC_LOCAL=1 TERRAFORM_VERSION=$(TERRAFORM_VERSION) $(GO) test $(TESTPKGS) -v $(TESTARGS) -timeout 120m 72 | 73 | docker-start: 74 | cd test-fixtures/local && export TERRAFORM_VERSION=$(TERRAFORM_VERSION) && ${TERRAFORM} init && ${TERRAFORM} apply -auto-approve -var="operating_system=${OPERATING_SYSTEM}" 75 | 76 | docker-stop: 77 | cd test-fixtures/local && TERRAFORM_VERSION=$(TERRAFORM_VERSION) ${TERRAFORM} destroy -auto-approve -var="operating_system=${OPERATING_SYSTEM}" 78 | 79 | azure-create: 80 | cd test-fixtures/all && export TERRAFORM_VERSION=$(TERRAFORM_VERSION) && ${TERRAFORM} init && ${TERRAFORM} apply -auto-approve 81 | 82 | azure-destroy: 83 | cd test-fixtures/all && TERRAFORM_VERSION=$(TERRAFORM_VERSION) ${TERRAFORM} destroy -auto-approve 84 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Terraform Provider `mssql` 2 | 3 | > :warning: NOTE: Because the provider as it stands covers all of our current use cases, we will not be dedicating much time and effort to supporting it. We will, however, gladly accept pull requests. We will try to review and release those in a timely manner. Pull requests with included tests and documentation will be prioritized. 4 | 5 | ## Requirements 6 | 7 | - [Terraform](https://www.terraform.io/downloads.html) 1.5.x 8 | - [Go](https://golang.org/doc/install) 1.21 (to build the provider plugin) 9 | 10 | I recommend using [tfvm](https://github.com/cbuschka/tfvm) to manage Terraform versions. The `Makefile` assumes that `tfvm` is installed to use the correct version of Terraform when running tests. 11 | 12 | ## Usage 13 | 14 | ```hcl 15 | terraform { 16 | required_version = "~> 1.5" 17 | required_providers { 18 | mssql = { 19 | versions = "~> 0.2" 20 | source = "betr-io/mssql" 21 | } 22 | } 23 | } 24 | 25 | provider "mssql" {} 26 | ``` 27 | 28 | ## Building the provider 29 | 30 | Clone the repository 31 | 32 | ```shell 33 | git clone git@github.com:betr-io/terraform-provider-mssql 34 | ``` 35 | 36 | Enter the provider directory and build the provider 37 | 38 | ```shell 39 | cd terraform-provider-mssql 40 | make build 41 | ``` 42 | 43 | To build and install the provider locally 44 | 45 | ```shell 46 | make install 47 | ``` 48 | 49 | ## Developing the provider 50 | 51 | If you wish to work on the provider, you'll first need [Go](https://www.golang.org) installed on your machine (version 1.21+). 52 | 53 | To compile the provider, run `make build`. This will build the provider. 54 | 55 | To run the unit test, you can simply run `make test`. 56 | 57 | To run acceptance tests against a local SQL Server running in Docker, you must have [Docker](https://docs.docker.com/get-docker/) installed. You can then run the following commands 58 | 59 | ```shell 60 | make docker-start 61 | TESTARGS=-count=1 make testacc-local 62 | make docker-stop 63 | ``` 64 | 65 | This will spin up a SQL server running in a container on your local machine, run the tests that can run against a SQL Server, and destroy the container. 66 | 67 | In order to run the full suite of acceptance tests, run `make testacc`. Again, to spin up a local SQL Server container in docker, and corresponding resources in Azure, modify `test-fixtures/all/terraform.tfvars` to match your environment and run 68 | 69 | ```shell 70 | make azure-create 71 | TESTARGS=-count=1 make testacc 72 | make azure-destroy 73 | ``` 74 | 75 | > **NOTE**: This will create resources in Azure and _will_ incur costs. 76 | > 77 | > **Note to self**: Remember to set current IP address in `test-fixtures/all/terraform.tfvars`, and activate `Global Administrator` in PIM to run Azure tests. 78 | 79 | ## Release provider 80 | 81 | To create a release, do: 82 | 83 | - Update `CHANGELOG.md`. 84 | - Update `VERSION` in `Makefile` (only used for installing the provider when developing). 85 | - Push a new valid version tag (e.g. `v1.2.3`) to GitHub. 86 | - See also [Publishing Providers](https://www.terraform.io/docs/registry/providers/publishing.html). 87 | -------------------------------------------------------------------------------- /docker-compose/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.7" 2 | services: 3 | mssql: 4 | image: mcr.microsoft.com/mssql/server 5 | environment: 6 | ACCEPT_EULA: "Y" 7 | SA_PASSWORD: "!!up3R!!3cR37" 8 | ports: 9 | - 1433:1433 10 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | # Microsoft SQL Server Provider 2 | 3 | The SQL Server provider exposes resources used to manage the configuration of resources in a Microsoft SQL Server and an Azure SQL Database. It might also work for other Microsoft SQL Server products like Azure Managed SQL Server, but it has not been tested against these resources. 4 | 5 | ## Example Usage 6 | 7 | ```hcl 8 | terraform { 9 | required_providers { 10 | mssql = { 11 | source = "betr-io/mssql" 12 | version = "0.1.0" 13 | } 14 | } 15 | } 16 | 17 | provider "mssql" { 18 | debug = "false" 19 | } 20 | 21 | resource "mssql_login" "example" { 22 | server { 23 | host = "localhost" 24 | login { 25 | username = "sa" 26 | password = "MySuperSecr3t!" 27 | } 28 | } 29 | login_name = "testlogin" 30 | password = "NotSoS3cret?" 31 | } 32 | 33 | resource "mssql_user" "example" { 34 | server { 35 | host = "localhost" 36 | login { 37 | username = "sa" 38 | password = "MySuperSecr3t!" 39 | } 40 | } 41 | username = "testuser" 42 | login_name = mssql_login.example.login_name 43 | } 44 | ``` 45 | 46 | ## Argument Reference 47 | 48 | The following arguments are supported: 49 | 50 | * `debug` - (Optional) Either `false` or `true`. Defaults to `false`. If `true`, the provider will write a debug log to `terraform-provider-mssql.log`. 51 | -------------------------------------------------------------------------------- /docs/resources/login.md: -------------------------------------------------------------------------------- 1 | # mssql_login 2 | 3 | The `mssql_login` resource creates and manages a login on a SQL Server. 4 | 5 | ## Example Usage 6 | 7 | ```hcl 8 | resource "mssql_login" "example" { 9 | server { 10 | host = "example-sql-server.database.windows.net" 11 | azure_login {} 12 | } 13 | login_name = "testlogin" 14 | } 15 | ``` 16 | 17 | ## Argument Reference 18 | 19 | The following arguments are supported: 20 | 21 | * `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. 22 | * `login_name` - (Required) The name of the server login. Changing this forces a new resource to be created. 23 | * `password` - (Required) The password of the server login. 24 | * `sid` - (Optional) The security identifier (SID).Changing this forces a new resource to be created. 25 | * `default_database` - (Optional) The default database of this server login. Defaults to `master`. This argument does not apply to Azure SQL Database. 26 | * `default_language` - (Optional) The default language of this server login. Defaults to `us_english`. This argument does not apply to Azure SQL Database. 27 | 28 | The `server` block supports the following arguments: 29 | 30 | * `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. 31 | * `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. 32 | * `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. 33 | * `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. 34 | * `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. 35 | * `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. 36 | 37 | The `login` block supports the following arguments: 38 | 39 | * `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. 40 | * `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. 41 | 42 | The `azure_login` block supports the following arguments: 43 | 44 | * `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. 45 | * `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. 46 | * `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. 47 | 48 | The `azuread_managed_identity_auth` block supports the following arguments: 49 | 50 | * `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. 51 | 52 | -> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. 53 | 54 | ## Attribute Reference 55 | 56 | The following attributes are exported: 57 | 58 | * `principal_id` - The principal id of this server login. 59 | * `sid` - The security identifier (SID) of this login in String format. 60 | 61 | ## Import 62 | 63 | Before importing `mssql_login`, you must to configure the authentication to your sql server: 64 | 65 | 1. Using Azure AD authentication, you must set the following environment variables: `MSSQL_TENANT_ID`, `MSSQL_CLIENT_ID` and `MSSQL_CLIENT_SECRET`. 66 | 2. Using SQL authentication, you must set the following environment variables: `MSSQL_USERNAME` and `MSSQL_PASSWORD`. 67 | 68 | After that you can import the SQL Server login using the server URL and `login name`, e.g. 69 | 70 | ```shell 71 | terraform import mssql_login.example 'mssql://example-sql-server.database.windows.net/testlogin' 72 | ``` 73 | -------------------------------------------------------------------------------- /docs/resources/user.md: -------------------------------------------------------------------------------- 1 | # mssql_user 2 | 3 | The `mssql_user` resource creates and manages a user on a SQL Server database. 4 | 5 | ## Example Usage 6 | 7 | ### Basic usage 8 | 9 | ```hcl 10 | resource "mssql_user" "example" { 11 | server { 12 | host = "example-sql-server.database.windows.net" 13 | azure_login { 14 | tenant_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 15 | client_id = "xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx" 16 | client_secret = "terriblySecretSecret" 17 | } 18 | } 19 | username = "user@example.com" 20 | roles = [ "db_owner" ] 21 | } 22 | ``` 23 | 24 | ### Using managed identities 25 | 26 | ```hcl 27 | resource "azurerm_resource_group" "example" { 28 | name = "example-resources" 29 | location = "West Europe" 30 | } 31 | 32 | resource "azurerm_user_assigned_identity" "example" { 33 | resource_group_name = azurerm_resource_group.example.name 34 | location = azurerm_resource_group.example.location 35 | 36 | name = "my-sql-identity" 37 | } 38 | 39 | resource "mssql_user" "example" { 40 | server { 41 | host = "example-sql-server.database.windows.net" 42 | azure_login { 43 | } 44 | } 45 | 46 | database = "my-database" 47 | username = azurerm_user_assigned_identity.example.name 48 | object_id = azurerm_user_assigned_identity.example.client_id 49 | 50 | roles = ["db_datareader"] 51 | } 52 | ``` 53 | 54 | > Note that in order to create an external user referencing an Azure AD entity (user, application), the Azure SQL Server needs to be a member of an Azure AD group assigned the Azure AD role `Directory Readers`. If it is not possible to give the Azure SQL Server this role (through the group), you can use the `object id` of the Azure AD entity instead. 55 | 56 | ## Argument Reference 57 | 58 | The following arguments are supported: 59 | 60 | * `server` - (Required) Server and login details for the SQL Server. The attributes supported in the `server` block is detailed below. 61 | * `database` - (Optional) The user will be created in this database. Defaults to `master`. Changing this forces a new resource to be created. 62 | * `username` - (Required) The name of the database user. Changing this forces a new resource to be created. 63 | * `password` - (Optional) The password of the database user. Conflicts with the `login_name` argument. Changing this forces a new resource to be created. 64 | * `login_name` - (Optional) The login name of the database user. This must refer to an existing SQL Server login name. Conflicts with the `password` argument. Changing this forces a new resource to be created. 65 | * `default_schema` - (Optional) Specifies the first schema that will be searched by the server when it resolves the names of objects for this database user. Defaults to `dbo`. 66 | * `default_language` - (Optional) Specifies the default language for the user. If no default language is specified, the default language for the user will bed the default language of the database. This argument does not apply to Azure SQL Database or if the user is not a contained database user. 67 | * `roles` - (Optional) List of database roles the user has. Defaults to none. 68 | 69 | -> If only `username` is specified, an external user is created. The username must be in a format appropriate to the external user created, and will vary between SQL Server types. If `password` is specified, a user that authenticates at the database is created, and if `login_name` is specified, a user that authenticates at the server is created. 70 | 71 | The `server` block supports the following arguments: 72 | 73 | * `host` - (Required) The host of the SQL Server. Changing this forces a new resource to be created. 74 | * `port` - (Optional) The port of the SQL Server. Defaults to `1433`. Changing this forces a new resource to be created. 75 | * `login` - (Optional) SQL Server login for managing the database resources. The attributes supported in the `login` block is detailed below. 76 | * `azure_login` - (Optional) Azure AD login for managing the database resources. The attributes supported in the `azure_login` block is detailed below. 77 | * `azuread_default_chain_auth` - (Optional) Use a chain of strategies for authenticating when managing the database resources. This auth strategy is very similar to how the Azure CLI authenticates. For more information, see [DefaultAzureCredential](https://github.com/Azure/azure-sdk-for-go/wiki/Set-up-Your-Environment-for-Authentication#configure-defaultazurecredential). This block has no attributes. 78 | * `azuread_managed_identity_auth` - (Optional) Use a managed identity for authenticating when managing the database resources. This is mainly useful for specifying a user-assigned managed identity. The attributes supported in the `azuread_managed_identity_auth` block is detailed below. 79 | 80 | The `login` block supports the following arguments: 81 | 82 | * `username` - (Required) The username of the SQL Server login. Can also be sourced from the `MSSQL_USERNAME` environment variable. 83 | * `password` - (Required) The password of the SQL Server login. Can also be sourced from the `MSSQL_PASSWORD` environment variable. 84 | * `object_id` - (Optional) The object id of the external username. Only used in azure_login auth context when AAD role delegation to sql server identity is not possible. 85 | 86 | The `azure_login` block supports the following arguments: 87 | 88 | * `tenant_id` - (Required) The tenant ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_TENANT_ID` environment variable. 89 | * `client_id` - (Required) The client ID of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_ID` environment variable. 90 | * `client_secret` - (Required) The client secret of the principal used to login to the SQL Server. Can also be sourced from the `MSSQL_CLIENT_SECRET` environment variable. 91 | 92 | The `azuread_managed_identity_auth` block supports the following arguments: 93 | 94 | * `user_id` - (Optional) Id of a user-assigned managed identity to assume. Omitting this property instructs the provider to assume a system-assigned managed identity. 95 | 96 | -> Only one of `login`, `azure_login`, `azuread_default_chain_auth` and `azuread_managed_identity_auth` can be specified. 97 | 98 | ## Attribute Reference 99 | 100 | The following attributes are exported: 101 | 102 | * `principal_id` - The principal id of this database user. 103 | * `sid` - The security identifier (SID) of this database user in String format. 104 | * `authentication_type` - One of `DATABASE`, `INSTANCE`, or `EXTERNAL`. 105 | 106 | ## Import 107 | 108 | Before importing `mssql_user`, you must to configure the authentication to your sql server: 109 | 110 | 1. Using Azure AD authentication, you must set the following environment variables: `MSSQL_TENANT_ID`, `MSSQL_CLIENT_ID` and `MSSQL_CLIENT_SECRET`. 111 | 2. Using SQL authentication, you must set the following environment variables: `MSSQL_USERNAME` and `MSSQL_PASSWORD`. 112 | 113 | After that you can import the SQL Server database user using the server URL and `login name`, e.g. 114 | 115 | ```shell 116 | terraform import mssql_user.example 'mssql://example-sql-server.database.windows.net/master/user@example.com' 117 | ``` 118 | -------------------------------------------------------------------------------- /examples/azure/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.5" 3 | required_providers { 4 | azuread = { 5 | source = "hashicorp/azuread" 6 | version = "~> 2.47" 7 | } 8 | azurerm = { 9 | source = "hashicorp/azurerm" 10 | version = "~> 3.85" 11 | } 12 | mssql = { 13 | source = "betr-io/mssql" 14 | version = "~> 0.2" 15 | } 16 | random = { 17 | source = "hashicorp/random" 18 | version = "~> 3.6" 19 | } 20 | time = { 21 | source = "hashicorp/time" 22 | version = "~> 0.10" 23 | } 24 | } 25 | } 26 | 27 | provider "azuread" {} 28 | 29 | provider "azurerm" { 30 | features {} 31 | } 32 | 33 | provider "mssql" { 34 | debug = "true" 35 | } 36 | 37 | provider "random" {} 38 | 39 | variable "prefix" { 40 | description = "A prefix used when naming Azure resources" 41 | type = string 42 | } 43 | 44 | variable "sql_servers_group" { 45 | description = "The name of an Azure AD group assigned the role 'Directory Reader'. The Azure SQL Server will be added to this group to enable external logins." 46 | type = string 47 | default = "SQL Servers" 48 | } 49 | 50 | variable "location" { 51 | description = "The location of the Azure resources." 52 | type = string 53 | default = "East US" 54 | } 55 | 56 | variable "tenant_id" { 57 | description = "The tenant id of the Azure AD tenant" 58 | type = string 59 | } 60 | 61 | variable "local_ip_addresses" { 62 | description = "The external IP addresses of the machines running the acceptance tests. This is necessary to allow access to the Azure SQL Server resource." 63 | type = list(string) 64 | } 65 | 66 | # 67 | # Creates an Azure SQL Database running in a temporary resource group on Azure. 68 | # 69 | 70 | # Random names and secrets 71 | resource "random_string" "random" { 72 | length = 16 73 | upper = false 74 | special = false 75 | } 76 | 77 | locals { 78 | prefix = "${var.prefix}-${substr(random_string.random.result, 0, 4)}" 79 | } 80 | 81 | # An Azure AD group assigned the role 'Directory Readers'. The Azure SQL Server needs to be assigned to this group to enable external logins. 82 | data "azuread_group" "sql_servers" { 83 | display_name = var.sql_servers_group 84 | } 85 | 86 | # An Azure AD service principal used as Azure Administrator for the Azure SQL Server resource 87 | resource "azuread_application" "sa" { 88 | display_name = "${local.prefix}-sa" 89 | web { 90 | homepage_url = "https://test.example.com" 91 | } 92 | } 93 | 94 | resource "azuread_service_principal" "sa" { 95 | client_id = azuread_application.sa.client_id 96 | } 97 | 98 | resource "azuread_service_principal_password" "sa" { 99 | service_principal_id = azuread_service_principal.sa.object_id 100 | } 101 | 102 | # An Azure AD service principal used to test creating an external login to the Azure SQL server resource 103 | resource "azuread_application" "user" { 104 | display_name = "${local.prefix}-user" 105 | web { 106 | homepage_url = "https://test.example.com" 107 | } 108 | } 109 | 110 | resource "azuread_service_principal" "user" { 111 | client_id = azuread_application.user.client_id 112 | } 113 | 114 | resource "azuread_service_principal_password" "user" { 115 | service_principal_id = azuread_service_principal.user.id 116 | } 117 | 118 | # Temporary resource group 119 | resource "azurerm_resource_group" "rg" { 120 | name = "${lower(var.prefix)}-${random_string.random.result}" 121 | location = var.location 122 | } 123 | 124 | # An Azure SQL Server 125 | resource "azurerm_mssql_server" "sql_server" { 126 | name = "${lower(local.prefix)}-sql-server" 127 | resource_group_name = azurerm_resource_group.rg.name 128 | location = azurerm_resource_group.rg.location 129 | 130 | version = "12.0" 131 | administrator_login = "SuperAdministrator" 132 | administrator_login_password = azuread_service_principal_password.sa.value 133 | 134 | azuread_administrator { 135 | tenant_id = var.tenant_id 136 | object_id = azuread_service_principal.sa.client_id 137 | login_username = azuread_service_principal.sa.display_name 138 | } 139 | 140 | identity { 141 | type = "SystemAssigned" 142 | } 143 | } 144 | 145 | resource "azuread_group_member" "sql" { 146 | group_object_id = data.azuread_group.sql_servers.id 147 | member_object_id = azurerm_mssql_server.sql_server.identity[0].principal_id 148 | } 149 | 150 | resource "azurerm_mssql_firewall_rule" "sql_server_fw_rule" { 151 | count = length(var.local_ip_addresses) 152 | name = "AllowIP ${count.index}" 153 | server_id = azurerm_mssql_server.sql_server.id 154 | start_ip_address = var.local_ip_addresses[count.index] 155 | end_ip_address = var.local_ip_addresses[count.index] 156 | } 157 | 158 | # The Azure SQL Database used in tests 159 | resource "azurerm_mssql_database" "db" { 160 | name = "testdb" 161 | server_id = azurerm_mssql_server.sql_server.id 162 | sku_name = "Basic" 163 | } 164 | 165 | resource "time_sleep" "wait_15_seconds" { 166 | depends_on = [azurerm_mssql_database.db] 167 | 168 | create_duration = "15s" 169 | } 170 | 171 | 172 | # 173 | # Creates a login and user in the SQL Server 174 | # 175 | resource "random_password" "server" { 176 | keepers = { 177 | login_name = "testlogin" 178 | username = "testuser" 179 | } 180 | length = 32 181 | special = true 182 | } 183 | 184 | resource "mssql_login" "server" { 185 | server { 186 | host = azurerm_mssql_server.sql_server.fully_qualified_domain_name 187 | login { 188 | username = azurerm_mssql_server.sql_server.administrator_login 189 | password = azurerm_mssql_server.sql_server.administrator_login_password 190 | } 191 | } 192 | login_name = random_password.server.keepers.login_name 193 | password = random_password.server.result 194 | 195 | depends_on = [time_sleep.wait_15_seconds] 196 | } 197 | 198 | resource "mssql_user" "server" { 199 | server { 200 | host = azurerm_mssql_server.sql_server.fully_qualified_domain_name 201 | login { 202 | username = azurerm_mssql_server.sql_server.administrator_login 203 | password = azurerm_mssql_server.sql_server.administrator_login_password 204 | } 205 | } 206 | database = azurerm_mssql_database.db.name 207 | username = random_password.server.keepers.username 208 | login_name = mssql_login.server.login_name 209 | } 210 | 211 | output "instance" { 212 | value = { 213 | login_name = mssql_login.server.login_name, 214 | password = mssql_login.server.password 215 | } 216 | sensitive = true 217 | } 218 | 219 | 220 | # 221 | # Creates a user with login in the SQL Server database 222 | # 223 | 224 | resource "random_password" "database" { 225 | keepers = { 226 | username = "testuser2" 227 | } 228 | length = 32 229 | special = true 230 | } 231 | 232 | resource "mssql_user" "database" { 233 | server { 234 | host = azurerm_mssql_server.sql_server.fully_qualified_domain_name 235 | login { 236 | username = azurerm_mssql_server.sql_server.administrator_login 237 | password = azurerm_mssql_server.sql_server.administrator_login_password 238 | } 239 | } 240 | database = azurerm_mssql_database.db.name 241 | username = "${local.prefix}-user" 242 | password = random_password.database.result 243 | } 244 | 245 | output "database" { 246 | value = { 247 | username = mssql_user.database.username, 248 | password = mssql_user.database.password 249 | } 250 | sensitive = true 251 | } 252 | 253 | 254 | # 255 | # Creates a login and user from Azure AD in the SQL Server 256 | # 257 | 258 | resource "mssql_user" "external" { 259 | server { 260 | host = azurerm_mssql_server.sql_server.fully_qualified_domain_name 261 | azure_login { 262 | tenant_id = var.tenant_id 263 | client_id = azuread_service_principal.sa.client_id 264 | client_secret = azuread_service_principal_password.sa.value 265 | } 266 | } 267 | database = azurerm_mssql_database.db.name 268 | username = azuread_service_principal.user.display_name 269 | } 270 | 271 | output "external" { 272 | value = { 273 | tenant_id = var.tenant_id 274 | client_id = azuread_service_principal.user.client_id 275 | client_secret = azuread_service_principal_password.user.value 276 | } 277 | sensitive = true 278 | } 279 | -------------------------------------------------------------------------------- /examples/fedauth/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.5" 3 | required_providers { 4 | azuread = { 5 | source = "hashicorp/azuread" 6 | version = "~> 2.47" 7 | } 8 | azurerm = { 9 | source = "hashicorp/azurerm" 10 | version = "~> 3.85" 11 | } 12 | mssql = { 13 | source = "betr-io/mssql" 14 | version = "~> 0.2" 15 | } 16 | random = { 17 | source = "hashicorp/random" 18 | version = "~> 3.6" 19 | } 20 | time = { 21 | source = "hashicorp/time" 22 | version = "~> 0.10" 23 | } 24 | } 25 | } 26 | 27 | provider "azuread" {} 28 | 29 | provider "azurerm" { 30 | features {} 31 | } 32 | 33 | provider "mssql" { 34 | debug = "true" 35 | } 36 | 37 | provider "random" {} 38 | 39 | variable "prefix" { 40 | description = "A prefix used when naming Azure resources" 41 | type = string 42 | } 43 | 44 | variable "sql_servers_group" { 45 | description = "The name of an Azure AD group assigned the role 'Directory Reader'. The Azure SQL Server will be added to this group to enable external logins." 46 | type = string 47 | default = "SQL Servers" 48 | } 49 | 50 | variable "location" { 51 | description = "The location of the Azure resources." 52 | type = string 53 | default = "East US" 54 | } 55 | 56 | variable "local_ip_addresses" { 57 | description = "The external IP addresses of the machines running the acceptance tests. This is necessary to allow access to the Azure SQL Server resource." 58 | type = list(string) 59 | } 60 | 61 | # 62 | # Creates an Azure SQL Database running in a temporary resource group on Azure. 63 | # 64 | 65 | # Random names and secrets 66 | resource "random_string" "random" { 67 | length = 16 68 | upper = false 69 | special = false 70 | } 71 | 72 | locals { 73 | prefix = "${var.prefix}-${substr(random_string.random.result, 0, 4)}" 74 | } 75 | 76 | data "azuread_client_config" "current" {} 77 | 78 | # Temporary resource group 79 | resource "azurerm_resource_group" "rg" { 80 | name = "${lower(var.prefix)}-${random_string.random.result}" 81 | location = var.location 82 | } 83 | 84 | # An Azure SQL Server 85 | resource "azurerm_mssql_server" "sql_server" { 86 | name = "${lower(local.prefix)}-sql-server" 87 | resource_group_name = azurerm_resource_group.rg.name 88 | location = azurerm_resource_group.rg.location 89 | version = "12.0" 90 | 91 | azuread_administrator { 92 | tenant_id = data.azuread_client_config.current.tenant_id 93 | object_id = data.azuread_client_config.current.client_id 94 | login_username = "superuser" 95 | azuread_authentication_only = true 96 | } 97 | 98 | identity { 99 | type = "SystemAssigned" 100 | } 101 | } 102 | 103 | resource "azurerm_mssql_firewall_rule" "sql_server_fw_rule" { 104 | count = length(var.local_ip_addresses) 105 | name = "AllowIP ${count.index}" 106 | server_id = azurerm_mssql_server.sql_server.id 107 | start_ip_address = var.local_ip_addresses[count.index] 108 | end_ip_address = var.local_ip_addresses[count.index] 109 | } 110 | 111 | # The Azure SQL Database used in tests 112 | resource "azurerm_mssql_database" "db" { 113 | name = "testdb" 114 | server_id = azurerm_mssql_server.sql_server.id 115 | sku_name = "Basic" 116 | } 117 | 118 | resource "time_sleep" "wait_15_seconds" { 119 | depends_on = [azurerm_mssql_database.db] 120 | 121 | create_duration = "15s" 122 | } 123 | 124 | # 125 | # Creates a login and user from Azure AD in the SQL Server 126 | # 127 | 128 | resource "mssql_user" "external" { 129 | server { 130 | host = azurerm_mssql_server.sql_server.fully_qualified_domain_name 131 | azuread_default_chain_auth {} 132 | } 133 | database = azurerm_mssql_database.db.name 134 | username = "someone@foobar.onmicrosoft.com" 135 | } 136 | -------------------------------------------------------------------------------- /examples/local/main.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.5" 3 | required_providers { 4 | docker = { 5 | source = "kreuzwerker/docker" 6 | version = "~> 3.0" 7 | } 8 | mssql = { 9 | source = "betr-io/mssql" 10 | version = "~> 0.2" 11 | } 12 | random = { 13 | source = "hashicorp/random" 14 | version = "~> 3.6" 15 | } 16 | time = { 17 | source = "hashicorp/time" 18 | version = "~> 0.10" 19 | } 20 | } 21 | } 22 | 23 | provider "docker" {} 24 | 25 | provider "mssql" { 26 | debug = true 27 | } 28 | 29 | provider "random" {} 30 | 31 | # 32 | # Creates a SQL Server running in a docker container on the local machine. 33 | # 34 | locals { 35 | local_username = "sa" 36 | local_password = "!!up3R!!3cR37" 37 | } 38 | 39 | resource "docker_image" "mssql" { 40 | name = "mcr.microsoft.com/mssql/server" 41 | keep_locally = true 42 | } 43 | 44 | resource "docker_container" "mssql" { 45 | name = "mssql" 46 | image = docker_image.mssql.image_id 47 | env = ["ACCEPT_EULA=Y", "SA_PASSWORD=${local.local_password}"] 48 | } 49 | 50 | resource "time_sleep" "wait_5_seconds" { 51 | depends_on = [docker_container.mssql] 52 | 53 | create_duration = "5s" 54 | } 55 | 56 | # 57 | # Creates a login and user in the SQL Server 58 | # 59 | resource "random_password" "example" { 60 | keepers = { 61 | login_name = "testlogin" 62 | username = "testuser" 63 | } 64 | length = 32 65 | special = true 66 | } 67 | 68 | resource "mssql_login" "example" { 69 | server { 70 | host = docker_container.mssql.network_data[0].ip_address 71 | login { 72 | username = local.local_username 73 | password = local.local_password 74 | } 75 | } 76 | login_name = random_password.example.keepers.login_name 77 | password = random_password.example.result 78 | 79 | depends_on = [time_sleep.wait_5_seconds] 80 | } 81 | 82 | resource "mssql_login" "example" { 83 | server { 84 | host = docker_container.mssql.ip_address 85 | login { 86 | username = local.local_username 87 | password = local.local_password 88 | } 89 | } 90 | login_name = random_password.example.keepers.login_name 91 | password = random_password.example.result 92 | sid = "0xB7BDEF7990D03541BAA2AD73E4FF18E8" 93 | 94 | depends_on = [time_sleep.wait_5_seconds] 95 | } 96 | 97 | resource "mssql_user" "example" { 98 | server { 99 | host = docker_container.mssql.network_data[0].ip_address 100 | login { 101 | username = local.local_username 102 | password = local.local_password 103 | } 104 | } 105 | username = random_password.example.keepers.username 106 | login_name = mssql_login.example.login_name 107 | } 108 | 109 | output "login" { 110 | value = { 111 | login_name = mssql_login.example.login_name, 112 | password = mssql_login.example.password 113 | } 114 | sensitive = true 115 | } 116 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/betr-io/terraform-provider-mssql 2 | 3 | go 1.21 4 | 5 | require ( 6 | github.com/Azure/go-autorest/autorest v0.11.29 7 | github.com/Azure/go-autorest/autorest/adal v0.9.23 8 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0 9 | github.com/microsoft/go-mssqldb v1.6.0 10 | github.com/pkg/errors v0.9.1 11 | github.com/rs/zerolog v1.31.0 12 | ) 13 | 14 | require ( 15 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 // indirect 16 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 // indirect 17 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 // indirect 18 | github.com/Azure/go-autorest v14.2.0+incompatible // indirect 19 | github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect 20 | github.com/Azure/go-autorest/logger v0.2.1 // indirect 21 | github.com/Azure/go-autorest/tracing v0.6.0 // indirect 22 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 // indirect 23 | github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect 24 | github.com/agext/levenshtein v1.2.3 // indirect 25 | github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect 26 | github.com/cloudflare/circl v1.3.6 // indirect 27 | github.com/fatih/color v1.16.0 // indirect 28 | github.com/golang-jwt/jwt/v4 v4.5.0 // indirect 29 | github.com/golang-jwt/jwt/v5 v5.2.0 // indirect 30 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 // indirect 31 | github.com/golang-sql/sqlexp v0.1.0 // indirect 32 | github.com/golang/protobuf v1.5.3 // indirect 33 | github.com/google/go-cmp v0.6.0 // indirect 34 | github.com/google/uuid v1.5.0 // indirect 35 | github.com/hashicorp/errwrap v1.1.0 // indirect 36 | github.com/hashicorp/go-checkpoint v0.5.0 // indirect 37 | github.com/hashicorp/go-cleanhttp v0.5.2 // indirect 38 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect 39 | github.com/hashicorp/go-hclog v1.6.2 // indirect 40 | github.com/hashicorp/go-multierror v1.1.1 // indirect 41 | github.com/hashicorp/go-plugin v1.6.0 // indirect 42 | github.com/hashicorp/go-uuid v1.0.3 // indirect 43 | github.com/hashicorp/go-version v1.6.0 // indirect 44 | github.com/hashicorp/hc-install v0.6.2 // indirect 45 | github.com/hashicorp/hcl/v2 v2.19.1 // indirect 46 | github.com/hashicorp/logutils v1.0.0 // indirect 47 | github.com/hashicorp/terraform-exec v0.20.0 // indirect 48 | github.com/hashicorp/terraform-json v0.20.0 // indirect 49 | github.com/hashicorp/terraform-plugin-go v0.20.0 // indirect 50 | github.com/hashicorp/terraform-plugin-log v0.9.0 // indirect 51 | github.com/hashicorp/terraform-registry-address v0.2.3 // indirect 52 | github.com/hashicorp/terraform-svchost v0.1.1 // indirect 53 | github.com/hashicorp/yamux v0.1.1 // indirect 54 | github.com/kylelemons/godebug v1.1.0 // indirect 55 | github.com/mattn/go-colorable v0.1.13 // indirect 56 | github.com/mattn/go-isatty v0.0.20 // indirect 57 | github.com/mitchellh/copystructure v1.2.0 // indirect 58 | github.com/mitchellh/go-testing-interface v1.14.1 // indirect 59 | github.com/mitchellh/go-wordwrap v1.0.1 // indirect 60 | github.com/mitchellh/mapstructure v1.5.0 // indirect 61 | github.com/mitchellh/reflectwalk v1.0.2 // indirect 62 | github.com/oklog/run v1.1.0 // indirect 63 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 // indirect 64 | github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect 65 | github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect 66 | github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect 67 | github.com/zclconf/go-cty v1.14.1 // indirect 68 | golang.org/x/crypto v0.17.0 // indirect 69 | golang.org/x/mod v0.14.0 // indirect 70 | golang.org/x/net v0.19.0 // indirect 71 | golang.org/x/sys v0.15.0 // indirect 72 | golang.org/x/text v0.14.0 // indirect 73 | google.golang.org/appengine v1.6.8 // indirect 74 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 // indirect 75 | google.golang.org/grpc v1.60.1 // indirect 76 | google.golang.org/protobuf v1.32.0 // indirect 77 | ) 78 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= 2 | dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= 3 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1 h1:lGlwhPtrX6EVml1hO0ivjkUxsSyl4dsiw9qcA1k/3IQ= 4 | github.com/Azure/azure-sdk-for-go/sdk/azcore v1.9.1/go.mod h1:RKUqNu35KJYcVG/fqTRqmuXJZYNhYkBrnC/hX7yGbTA= 5 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0 h1:BMAjVKJM0U/CYF27gA0ZMmXGkOcvfFtD0oHVZ1TIPRI= 6 | github.com/Azure/azure-sdk-for-go/sdk/azidentity v1.4.0/go.mod h1:1fXstnBMas5kzG+S3q8UoJcmyU6nUeunJcMDHcRYHhs= 7 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1 h1:6oNBlSdi1QqM1PNW7FPA6xOGA5UNsXnkaYZz9vdPGhA= 8 | github.com/Azure/azure-sdk-for-go/sdk/internal v1.5.1/go.mod h1:s4kgfzA0covAXNicZHDMN58jExvcng2mC/DepXiF1EI= 9 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0 h1:yfJe15aSwEQ6Oo6J+gdfdulPNoZ3TEhmbhLIoxZcA+U= 10 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/azkeys v1.0.0/go.mod h1:Q28U+75mpCaSCDowNEmhIo/rmgdkqmkmzI7N6TGR4UY= 11 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0 h1:T028gtTPiYt/RMUfs8nVsAL7FDQrfLlrm/NnRG/zcC4= 12 | github.com/Azure/azure-sdk-for-go/sdk/security/keyvault/internal v0.8.0/go.mod h1:cw4zVQgBby0Z5f2v0itn6se2dDP17nTjbZFXW5uPyHA= 13 | github.com/Azure/go-autorest v14.2.0+incompatible h1:V5VMDjClD3GiElqLWO7mz2MxNAK/vTfRHdAubSIPRgs= 14 | github.com/Azure/go-autorest v14.2.0+incompatible/go.mod h1:r+4oMnoxhatjLLJ6zxSWATqVooLgysK6ZNox3g/xq24= 15 | github.com/Azure/go-autorest/autorest v0.11.29 h1:I4+HL/JDvErx2LjyzaVxllw2lRDB5/BT2Bm4g20iqYw= 16 | github.com/Azure/go-autorest/autorest v0.11.29/go.mod h1:ZtEzC4Jy2JDrZLxvWs8LrBWEBycl1hbT1eknI8MtfAs= 17 | github.com/Azure/go-autorest/autorest/adal v0.9.22/go.mod h1:XuAbAEUv2Tta//+voMI038TrJBqjKam0me7qR+L8Cmk= 18 | github.com/Azure/go-autorest/autorest/adal v0.9.23 h1:Yepx8CvFxwNKpH6ja7RZ+sKX+DWYNldbLiALMC3BTz8= 19 | github.com/Azure/go-autorest/autorest/adal v0.9.23/go.mod h1:5pcMqFkdPhviJdlEy3kC/v1ZLnQl0MH6XA5YCcMhy4c= 20 | github.com/Azure/go-autorest/autorest/date v0.3.0 h1:7gUk1U5M/CQbp9WoqinNzJar+8KY+LPI6wiWrP/myHw= 21 | github.com/Azure/go-autorest/autorest/date v0.3.0/go.mod h1:BI0uouVdmngYNUzGWeSYnokU+TrmwEsOqdt8Y6sso74= 22 | github.com/Azure/go-autorest/autorest/mocks v0.4.1/go.mod h1:LTp+uSrOhSkaKrUy935gNZuuIPPVsHlr9DSOxSayd+k= 23 | github.com/Azure/go-autorest/autorest/mocks v0.4.2 h1:PGN4EDXnuQbojHbU0UWoNvmu9AGVwYHG9/fkDYhtAfw= 24 | github.com/Azure/go-autorest/autorest/mocks v0.4.2/go.mod h1:Vy7OitM9Kei0i1Oj+LvyAWMXJHeKH1MVlzFugfVrmyU= 25 | github.com/Azure/go-autorest/logger v0.2.1 h1:IG7i4p/mDa2Ce4TRyAO8IHnVhAVF3RFU+ZtXWSmf4Tg= 26 | github.com/Azure/go-autorest/logger v0.2.1/go.mod h1:T9E3cAhj2VqvPOtCYAvby9aBXkZmbF5NWuPV8+WeEW8= 27 | github.com/Azure/go-autorest/tracing v0.6.0 h1:TYi4+3m5t6K48TGI9AUdb+IzbnSxvnvUMfuitfgcfuo= 28 | github.com/Azure/go-autorest/tracing v0.6.0/go.mod h1:+vhtPC754Xsa23ID7GlGsrdKBpUA79WCAKPPZVC2DeU= 29 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0 h1:hVeq+yCyUi+MsoO/CU95yqCIcdzra5ovzk8Q2BBpV2M= 30 | github.com/AzureAD/microsoft-authentication-library-for-go v1.2.0/go.mod h1:wP83P5OoQ5p6ip3ScPr0BAq0BvuPAvacpEuSzyouqAI= 31 | github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= 32 | github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= 33 | github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= 34 | github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= 35 | github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo= 36 | github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= 37 | github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= 38 | github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= 39 | github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= 40 | github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= 41 | github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= 42 | github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= 43 | github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 44 | github.com/cloudflare/circl v1.3.6 h1:/xbKIqSHbZXHwkhbrhrt2YOHIwYJlXH94E3tI/gDlUg= 45 | github.com/cloudflare/circl v1.3.6/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= 46 | github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= 47 | github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= 48 | github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= 49 | github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 50 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 51 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 52 | github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI= 53 | github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ= 54 | github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= 55 | github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= 56 | github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= 57 | github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM= 58 | github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE= 59 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= 60 | github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= 61 | github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= 62 | github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= 63 | github.com/go-git/go-git/v5 v5.10.1 h1:tu8/D8i+TWxgKpzQ3Vc43e+kkhXqtsZCKI/egajKnxk= 64 | github.com/go-git/go-git/v5 v5.10.1/go.mod h1:uEuHjxkHap8kAl//V5F/nNWwqIYtP/402ddd05mp0wg= 65 | github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= 66 | github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= 67 | github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= 68 | github.com/golang-jwt/jwt/v4 v4.0.0/go.mod h1:/xlHOz8bRuivTWchD4jCa+NbatV+wEUSzwAxVc6locg= 69 | github.com/golang-jwt/jwt/v4 v4.5.0 h1:7cYmW1XlMY7h7ii7UhUyChSgS5wUJEnm9uZVTGqOWzg= 70 | github.com/golang-jwt/jwt/v4 v4.5.0/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= 71 | github.com/golang-jwt/jwt/v5 v5.2.0 h1:d/ix8ftRUorsN+5eMIlF4T6J8CAt9rch3My2winC1Jw= 72 | github.com/golang-jwt/jwt/v5 v5.2.0/go.mod h1:pqrtFR0X4osieyHYxtmOUWsAWrfe1Q5UVIyoH402zdk= 73 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9 h1:au07oEsX2xN0ktxqI+Sida1w446QrXBRJ0nee3SNZlA= 74 | github.com/golang-sql/civil v0.0.0-20220223132316-b832511892a9/go.mod h1:8vg3r2VgvsThLBIFL93Qb5yWzgyZWhEmBwUJWevAkK0= 75 | github.com/golang-sql/sqlexp v0.1.0 h1:ZCD6MBpcuOVfGVqsEmY5/4FtYiKz6tSyUv9LPEDei6A= 76 | github.com/golang-sql/sqlexp v0.1.0/go.mod h1:J4ad9Vo8ZCWQ2GMrC4UCQy1JpCbwU9m3EOqtpKwwwHI= 77 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= 78 | github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= 79 | github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= 80 | github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= 81 | github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 82 | github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= 83 | github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= 84 | github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= 85 | github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= 86 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 87 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 88 | github.com/google/uuid v1.5.0 h1:1p67kYwdtXjb0gL0BPiP1Av9wiZPo5A8z2cWkTZ+eyU= 89 | github.com/google/uuid v1.5.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 90 | github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 91 | github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= 92 | github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= 93 | github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= 94 | github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= 95 | github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= 96 | github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ= 97 | github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= 98 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= 99 | github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= 100 | github.com/hashicorp/go-hclog v1.6.2 h1:NOtoftovWkDheyUM/8JW3QMiXyxJK3uHRK7wV04nD2I= 101 | github.com/hashicorp/go-hclog v1.6.2/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= 102 | github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= 103 | github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= 104 | github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= 105 | github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= 106 | github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 107 | github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= 108 | github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= 109 | github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= 110 | github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= 111 | github.com/hashicorp/hc-install v0.6.2 h1:V1k+Vraqz4olgZ9UzKiAcbman9i9scg9GgSt/U3mw/M= 112 | github.com/hashicorp/hc-install v0.6.2/go.mod h1:2JBpd+NCFKiHiu/yYCGaPyPHhZLxXTpz8oreHa/a3Ps= 113 | github.com/hashicorp/hcl/v2 v2.19.1 h1://i05Jqznmb2EXqa39Nsvyan2o5XyMowW5fnCKW5RPI= 114 | github.com/hashicorp/hcl/v2 v2.19.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE= 115 | github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= 116 | github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= 117 | github.com/hashicorp/terraform-exec v0.20.0 h1:DIZnPsqzPGuUnq6cH8jWcPunBfY+C+M8JyYF3vpnuEo= 118 | github.com/hashicorp/terraform-exec v0.20.0/go.mod h1:ckKGkJWbsNqFKV1itgMnE0hY9IYf1HoiekpuN0eWoDw= 119 | github.com/hashicorp/terraform-json v0.20.0 h1:cJcvn4gIOTi0SD7pIy+xiofV1zFA3hza+6K+fo52IX8= 120 | github.com/hashicorp/terraform-json v0.20.0/go.mod h1:qdeBs11ovMzo5puhrRibdD6d2Dq6TyE/28JiU4tIQxk= 121 | github.com/hashicorp/terraform-plugin-go v0.20.0 h1:oqvoUlL+2EUbKNsJbIt3zqqZ7wi6lzn4ufkn/UA51xQ= 122 | github.com/hashicorp/terraform-plugin-go v0.20.0/go.mod h1:Rr8LBdMlY53a3Z/HpP+ZU3/xCDqtKNCkeI9qOyT10QE= 123 | github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= 124 | github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= 125 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0 h1:Bl3e2ei2j/Z3Hc2HIS15Gal2KMKyLAZ2om1HCEvK6es= 126 | github.com/hashicorp/terraform-plugin-sdk/v2 v2.31.0/go.mod h1:i2C41tszDjiWfziPQDL5R/f3Zp0gahXe5No/MIO9rCE= 127 | github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= 128 | github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= 129 | github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= 130 | github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= 131 | github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= 132 | github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= 133 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= 134 | github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= 135 | github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= 136 | github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= 137 | github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= 138 | github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= 139 | github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= 140 | github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= 141 | github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= 142 | github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= 143 | github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= 144 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 145 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 146 | github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= 147 | github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= 148 | github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= 149 | github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= 150 | github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= 151 | github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= 152 | github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= 153 | github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 154 | github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= 155 | github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 156 | github.com/microsoft/go-mssqldb v1.6.0 h1:mM3gYdVwEPFrlg/Dvr2DNVEgYFG7L42l+dGc67NNNpc= 157 | github.com/microsoft/go-mssqldb v1.6.0/go.mod h1:00mDtPbeQCRGC1HwOOR5K/gr30P1NcEG0vx6Kbv2aJU= 158 | github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= 159 | github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= 160 | github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= 161 | github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8= 162 | github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0= 163 | github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0= 164 | github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= 165 | github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= 166 | github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= 167 | github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= 168 | github.com/oklog/run v1.1.0 h1:GEenZ1cK0+q0+wsJew9qUg/DyD8k3JzYsZAi5gYi2mA= 169 | github.com/oklog/run v1.1.0/go.mod h1:sVPdnTZT1zYwAJeCMu2Th4T21pA3FPOQRfWjQlk7DVU= 170 | github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= 171 | github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= 172 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8 h1:KoWmjvw+nsYOo29YJK9vDA65RGE3NrOnUtO7a+RF9HU= 173 | github.com/pkg/browser v0.0.0-20210911075715-681adbf594b8/go.mod h1:HKlIX3XHQyzLZPlr7++PzdhaXEj94dEiJgZDTsxEqUI= 174 | github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= 175 | github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 176 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 177 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 178 | github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= 179 | github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= 180 | github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= 181 | github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= 182 | github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= 183 | github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= 184 | github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= 185 | github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= 186 | github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 187 | github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= 188 | github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= 189 | github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 190 | github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= 191 | github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= 192 | github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= 193 | github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= 194 | github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 195 | github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= 196 | github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= 197 | github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= 198 | github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= 199 | github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= 200 | github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= 201 | github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= 202 | github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= 203 | github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= 204 | github.com/zclconf/go-cty v1.14.1 h1:t9fyA35fwjjUMcmL5hLER+e/rEPqrbCK1/OSE4SI9KA= 205 | github.com/zclconf/go-cty v1.14.1/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= 206 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 207 | golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= 208 | golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= 209 | golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= 210 | golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= 211 | golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= 212 | golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k= 213 | golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= 214 | golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= 215 | golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= 216 | golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= 217 | golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= 218 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 219 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 220 | golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= 221 | golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= 222 | golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= 223 | golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= 224 | golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= 225 | golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= 226 | golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= 227 | golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= 228 | golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 229 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 230 | golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 231 | golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 232 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 233 | golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 234 | golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 235 | golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 236 | golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 237 | golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 238 | golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 239 | golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 240 | golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 241 | golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 242 | golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 243 | golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 244 | golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 245 | golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 246 | golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 247 | golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 248 | golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 249 | golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= 250 | golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= 251 | golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= 252 | golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= 253 | golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= 254 | golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= 255 | golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= 256 | golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= 257 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 258 | golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= 259 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 260 | golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 261 | golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= 262 | golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= 263 | golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 264 | golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= 265 | golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= 266 | golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= 267 | golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= 268 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 269 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 270 | golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= 271 | golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= 272 | golang.org/x/tools v0.13.0 h1:Iey4qkscZuv0VvIt8E0neZjtPVQFSc870HQ448QgEmQ= 273 | golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58= 274 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 275 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 276 | google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= 277 | google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= 278 | google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= 279 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0 h1:/jFB8jK5R3Sq3i/lmeZO0cATSzFfZaJq1J2Euan3XKU= 280 | google.golang.org/genproto/googleapis/rpc v0.0.0-20231212172506-995d672761c0/go.mod h1:FUoWkonphQm3RhTS+kOEhF8h0iDpm4tdXolVCeZ9KKA= 281 | google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= 282 | google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= 283 | google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= 284 | google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= 285 | google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= 286 | google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= 287 | gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 288 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= 289 | gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= 290 | gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= 291 | gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= 292 | gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= 293 | gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= 294 | gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 295 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 296 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 297 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-sdk/v2/plugin" 5 | "github.com/betr-io/terraform-provider-mssql/mssql" 6 | ) 7 | 8 | // These will be set by goreleaser to appropriate values for the compiled binary 9 | var ( 10 | version string = "dev" 11 | commit string = "none" 12 | ) 13 | 14 | func main() { 15 | plugin.Serve(&plugin.ServeOpts{ 16 | ProviderFunc: mssql.New(version, commit), 17 | }) 18 | } 19 | -------------------------------------------------------------------------------- /mssql/const.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | const ( 4 | serverProp = "server" 5 | databaseProp = "database" 6 | principalIdProp = "principal_id" 7 | usernameProp = "username" 8 | objectIdProp = "object_id" 9 | passwordProp = "password" 10 | sidStrProp = "sid" 11 | clientIdProp = "client_id" 12 | authenticationTypeProp = "authentication_type" 13 | defaultSchemaProp = "default_schema" 14 | defaultSchemaPropDefault = "dbo" 15 | rolesProp = "roles" 16 | ) 17 | -------------------------------------------------------------------------------- /mssql/model/connector_factory.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 4 | 5 | type ConnectorFactory interface { 6 | GetConnector(prefix string, data *schema.ResourceData) (interface{}, error) 7 | } 8 | -------------------------------------------------------------------------------- /mssql/model/login.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type Login struct { 4 | PrincipalID int64 5 | LoginName string 6 | SIDStr string 7 | DefaultDatabase string 8 | DefaultLanguage string 9 | } 10 | -------------------------------------------------------------------------------- /mssql/model/provider.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 5 | "github.com/rs/zerolog" 6 | ) 7 | 8 | type Provider interface { 9 | GetConnector(prefix string, data *schema.ResourceData) (interface{}, error) 10 | ResourceLogger(resource, function string) zerolog.Logger 11 | DataSourceLogger(datasource, function string) zerolog.Logger 12 | } 13 | -------------------------------------------------------------------------------- /mssql/model/user.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | type User struct { 4 | PrincipalID int64 5 | Username string 6 | ObjectId string 7 | LoginName string 8 | Password string 9 | SIDStr string 10 | AuthType string 11 | DefaultSchema string 12 | DefaultLanguage string 13 | Roles []string 14 | } 15 | -------------------------------------------------------------------------------- /mssql/provider.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "context" 5 | "fmt" 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 7 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 8 | "github.com/rs/zerolog" 9 | "github.com/rs/zerolog/log" 10 | "io" 11 | "os" 12 | "github.com/betr-io/terraform-provider-mssql/mssql/model" 13 | "github.com/betr-io/terraform-provider-mssql/sql" 14 | "time" 15 | ) 16 | 17 | type mssqlProvider struct { 18 | factory model.ConnectorFactory 19 | logger *zerolog.Logger 20 | } 21 | 22 | const ( 23 | providerLogFile = "terraform-provider-mssql.log" 24 | ) 25 | 26 | var ( 27 | defaultTimeout = schema.DefaultTimeout(30 * time.Second) 28 | ) 29 | 30 | func New(version, commit string) func() *schema.Provider { 31 | return func() *schema.Provider { 32 | return Provider(sql.GetFactory()) 33 | } 34 | } 35 | 36 | func Provider(factory model.ConnectorFactory) *schema.Provider { 37 | return &schema.Provider{ 38 | Schema: map[string]*schema.Schema{ 39 | "debug": { 40 | Type: schema.TypeBool, 41 | Description: fmt.Sprintf("Enable provider debug logging (logs to file %s)", providerLogFile), 42 | Optional: true, 43 | Default: false, 44 | }, 45 | }, 46 | ResourcesMap: map[string]*schema.Resource{ 47 | "mssql_login": resourceLogin(), 48 | "mssql_user": resourceUser(), 49 | }, 50 | DataSourcesMap: map[string]*schema.Resource{}, 51 | ConfigureContextFunc: func(ctx context.Context, data *schema.ResourceData) (interface{}, diag.Diagnostics) { 52 | return providerConfigure(ctx, data, factory) 53 | }, 54 | } 55 | } 56 | 57 | func providerConfigure(ctx context.Context, data *schema.ResourceData, factory model.ConnectorFactory) (model.Provider, diag.Diagnostics) { 58 | isDebug := data.Get("debug").(bool) 59 | logger := newLogger(isDebug) 60 | 61 | logger.Info().Msg("Created provider") 62 | 63 | return mssqlProvider{factory: factory, logger: logger}, nil 64 | } 65 | 66 | func (p mssqlProvider) GetConnector(prefix string, data *schema.ResourceData) (interface{}, error) { 67 | return p.factory.GetConnector(prefix, data) 68 | } 69 | 70 | func (p mssqlProvider) ResourceLogger(resource, function string) zerolog.Logger { 71 | return p.logger.With().Str("resource", resource).Str("func", function).Logger() 72 | } 73 | 74 | func (p mssqlProvider) DataSourceLogger(datasource, function string) zerolog.Logger { 75 | return p.logger.With().Str("datasource", datasource).Str("func", function).Logger() 76 | } 77 | 78 | func newLogger(isDebug bool) *zerolog.Logger { 79 | var writer io.Writer = nil 80 | logLevel := zerolog.Disabled 81 | if isDebug { 82 | f, err := os.OpenFile(providerLogFile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 83 | if err != nil { 84 | log.Err(err).Msg("error opening file") 85 | } 86 | writer = f 87 | logLevel = zerolog.DebugLevel 88 | } 89 | logger := zerolog.New(writer).Level(logLevel).With().Timestamp().Logger() 90 | return &logger 91 | } 92 | -------------------------------------------------------------------------------- /mssql/provider_test.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "bytes" 5 | "context" 6 | sql2 "database/sql" 7 | "fmt" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 10 | "os" 11 | "strconv" 12 | "github.com/betr-io/terraform-provider-mssql/mssql/model" 13 | "github.com/betr-io/terraform-provider-mssql/sql" 14 | "testing" 15 | "text/template" 16 | "time" 17 | ) 18 | 19 | var runLocalAccTests bool 20 | var testAccProvider *schema.Provider 21 | var testAccProviders map[string]func() (*schema.Provider, error) 22 | 23 | func init() { 24 | _, runLocalAccTests = os.LookupEnv("TF_ACC_LOCAL") 25 | testAccProvider = Provider(sql.GetFactory()) 26 | testAccProviders = map[string]func() (*schema.Provider, error){ 27 | "mssql": func() (*schema.Provider, error) { 28 | return testAccProvider, nil 29 | }, 30 | } 31 | } 32 | 33 | func TestProvider(t *testing.T) { 34 | if err := testAccProvider.InternalValidate(); err != nil { 35 | t.Fatalf("err: %s", err) 36 | } 37 | } 38 | 39 | func testAccPreCheck(t *testing.T) { 40 | var keys []string 41 | _, azure := os.LookupEnv("TF_ACC") 42 | _, local := os.LookupEnv("TF_ACC_LOCAL") 43 | if local || azure { 44 | keys = append(keys, "MSSQL_USERNAME", "MSSQL_PASSWORD") 45 | } 46 | if azure { 47 | keys = append(keys, "MSSQL_TENANT_ID", "MSSQL_CLIENT_ID", "MSSQL_CLIENT_SECRET", "TF_ACC_SQL_SERVER", "TF_ACC_AZURE_USER_CLIENT_ID", "TF_ACC_AZURE_USER_CLIENT_SECRET") 48 | } 49 | for _, key := range keys { 50 | if v := os.Getenv(key); v == "" { 51 | t.Fatalf("Environment variable %s must be set for acceptance tests", key) 52 | } 53 | } 54 | } 55 | 56 | type Check struct { 57 | name, op string 58 | expected interface{} 59 | } 60 | 61 | type TestConnector interface { 62 | GetLogin(name string) (*model.Login, error) 63 | GetUser(database, name string) (*model.User, error) 64 | GetSystemUser() (string, error) 65 | GetCurrentUser(database string) (string, string, error) 66 | } 67 | 68 | type testConnector struct { 69 | c interface{} 70 | } 71 | 72 | func getTestConnector(a map[string]string) (TestConnector, error) { 73 | prefix := serverProp + ".0." 74 | 75 | connector := &sql.Connector{ 76 | Host: a[prefix+"host"], 77 | Port: a[prefix+"port"], 78 | Timeout: 60 * time.Second, 79 | } 80 | 81 | if username, ok := a[prefix+"login.0.username"]; ok { 82 | connector.Login = &sql.LoginUser{ 83 | Username: username, 84 | Password: a[prefix+"login.0.password"], 85 | } 86 | } 87 | 88 | if tenantId, ok := a[prefix+"azure_login.0.tenant_id"]; ok { 89 | connector.AzureLogin = &sql.AzureLogin{ 90 | TenantID: tenantId, 91 | ClientID: a[prefix+"azure_login.0.client_id"], 92 | ClientSecret: a[prefix+"azure_login.0.client_secret"], 93 | } 94 | } 95 | 96 | return testConnector{c: connector}, nil 97 | } 98 | 99 | func getTestLoginConnector(a map[string]string) (TestConnector, error) { 100 | prefix := serverProp + ".0." 101 | connector := &sql.Connector{ 102 | Host: a[prefix+"host"], 103 | Port: a[prefix+"port"], 104 | Timeout: 60 * time.Second, 105 | } 106 | if password, ok := a[passwordProp]; ok { 107 | connector.Login = &sql.LoginUser{ 108 | Username: a[loginNameProp], 109 | Password: password, 110 | } 111 | } 112 | 113 | return testConnector{c: connector}, nil 114 | } 115 | 116 | func getTestUserConnector(a map[string]string, username, password string) (TestConnector, error) { 117 | prefix := serverProp + ".0." 118 | connector := &sql.Connector{ 119 | Host: a[prefix+"host"], 120 | Port: a[prefix+"port"], 121 | Timeout: 60 * time.Second, 122 | } 123 | connector.Login = &sql.LoginUser{ 124 | Username: username, 125 | Password: password, 126 | } 127 | if database, ok := a[databaseProp]; ok { 128 | connector.Database = database 129 | } 130 | 131 | return testConnector{c: connector}, nil 132 | } 133 | 134 | func getTestExternalConnector(a map[string]string, tenantId, clientId, clientSecret string) (TestConnector, error) { 135 | prefix := serverProp + ".0." 136 | connector := &sql.Connector{ 137 | Host: a[prefix+"host"], 138 | Port: a[prefix+"port"], 139 | Timeout: 60 * time.Second, 140 | } 141 | connector.AzureLogin = &sql.AzureLogin{ 142 | TenantID: tenantId, 143 | ClientID: clientId, 144 | ClientSecret: clientSecret, 145 | } 146 | if database, ok := a[databaseProp]; ok { 147 | connector.Database = database 148 | } 149 | 150 | return testConnector{c: connector}, nil 151 | } 152 | 153 | func (t testConnector) GetLogin(name string) (*model.Login, error) { 154 | return t.c.(LoginConnector).GetLogin(context.Background(), name) 155 | } 156 | 157 | func (t testConnector) GetUser(database, name string) (*model.User, error) { 158 | return t.c.(UserConnector).GetUser(context.Background(), database, name) 159 | } 160 | 161 | func (t testConnector) GetSystemUser() (string, error) { 162 | var user string 163 | err := t.c.(*sql.Connector).QueryRowContext(context.Background(), "SELECT SYSTEM_USER;", func(row *sql2.Row) error { 164 | return row.Scan(&user) 165 | }) 166 | return user, err 167 | } 168 | 169 | func (t testConnector) GetCurrentUser(database string) (string, string, error) { 170 | if database == "" { 171 | database = "master" 172 | } 173 | t.c.(*sql.Connector).Database = database 174 | var current, system string 175 | err := t.c.(*sql.Connector).QueryRowContext(context.Background(), "SELECT CURRENT_USER, SYSTEM_USER;", func(row *sql2.Row) error { 176 | return row.Scan(¤t, &system) 177 | }) 178 | return current, system, err 179 | } 180 | 181 | func templateToString(name, text string, data interface{}) (string, error) { 182 | t, err := template.New(name).Parse(text) 183 | if err != nil { 184 | return "", err 185 | } 186 | var doc bytes.Buffer 187 | if err = t.Execute(&doc, data); err != nil { 188 | return "", err 189 | } 190 | return doc.String(), nil 191 | } 192 | 193 | func testAccImportStateId(resource string, azure bool) func(state *terraform.State) (string, error) { 194 | return func(state *terraform.State) (string, error) { 195 | rs, ok := state.RootModule().Resources[resource] 196 | if !ok { 197 | return "", fmt.Errorf("not found: %s", resource) 198 | } 199 | if rs.Primary.ID == "" { 200 | return "", fmt.Errorf("no record ID is set") 201 | } 202 | return rs.Primary.ID + "?azure=" + strconv.FormatBool(azure), nil 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /mssql/resource_login.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "context" 5 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 7 | "github.com/pkg/errors" 8 | "strings" 9 | "github.com/betr-io/terraform-provider-mssql/mssql/model" 10 | ) 11 | 12 | const loginNameProp = "login_name" 13 | const defaultDatabaseProp = "default_database" 14 | const defaultDatabaseDefault = "master" 15 | const defaultLanguageProp = "default_language" 16 | 17 | type LoginConnector interface { 18 | CreateLogin(ctx context.Context, name, password, sid, defaultDatabase, defaultLanguage string) error 19 | GetLogin(ctx context.Context, name string) (*model.Login, error) 20 | UpdateLogin(ctx context.Context, name, password, defaultDatabase, defaultLanguage string) error 21 | DeleteLogin(ctx context.Context, name string) error 22 | } 23 | 24 | func resourceLogin() *schema.Resource { 25 | return &schema.Resource{ 26 | CreateContext: resourceLoginCreate, 27 | ReadContext: resourceLoginRead, 28 | UpdateContext: resourceLoginUpdate, 29 | DeleteContext: resourceLoginDelete, 30 | Importer: &schema.ResourceImporter{ 31 | StateContext: resourceLoginImport, 32 | }, 33 | Schema: map[string]*schema.Schema{ 34 | serverProp: { 35 | Type: schema.TypeList, 36 | MaxItems: 1, 37 | Required: true, 38 | Elem: &schema.Resource{ 39 | Schema: getServerSchema(serverProp), 40 | }, 41 | }, 42 | loginNameProp: { 43 | Type: schema.TypeString, 44 | Required: true, 45 | ForceNew: true, 46 | }, 47 | passwordProp: { 48 | Type: schema.TypeString, 49 | Required: true, 50 | Sensitive: true, 51 | }, 52 | sidStrProp: { 53 | Type: schema.TypeString, 54 | Optional: true, 55 | ForceNew: true, 56 | Computed: true, 57 | }, 58 | defaultDatabaseProp: { 59 | Type: schema.TypeString, 60 | Optional: true, 61 | Default: defaultDatabaseDefault, 62 | DiffSuppressFunc: func(k, old, new string, data *schema.ResourceData) bool { 63 | return (old == "" && new == defaultDatabaseDefault) || (old == defaultDatabaseDefault && new == "") 64 | }, 65 | }, 66 | defaultLanguageProp: { 67 | Type: schema.TypeString, 68 | Optional: true, 69 | DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { 70 | return (old == "" && new == "us_english") || (old == "us_english" && new == "") 71 | }, 72 | }, 73 | principalIdProp: { 74 | Type: schema.TypeInt, 75 | Computed: true, 76 | }, 77 | }, 78 | Timeouts: &schema.ResourceTimeout{ 79 | Default: defaultTimeout, 80 | Read: defaultTimeout, 81 | }, 82 | } 83 | } 84 | 85 | func resourceLoginCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { 86 | logger := loggerFromMeta(meta, "login", "create") 87 | logger.Debug().Msgf("Create %s", getLoginID(data)) 88 | 89 | loginName := data.Get(loginNameProp).(string) 90 | password := data.Get(passwordProp).(string) 91 | sid := data.Get(sidStrProp).(string) 92 | defaultDatabase := data.Get(defaultDatabaseProp).(string) 93 | defaultLanguage := data.Get(defaultLanguageProp).(string) 94 | 95 | connector, err := getLoginConnector(meta, data) 96 | if err != nil { 97 | return diag.FromErr(err) 98 | } 99 | 100 | if err = connector.CreateLogin(ctx, loginName, password, sid, defaultDatabase, defaultLanguage); err != nil { 101 | return diag.FromErr(errors.Wrapf(err, "unable to create login [%s]", loginName)) 102 | } 103 | 104 | data.SetId(getLoginID(data)) 105 | 106 | logger.Info().Msgf("created login [%s]", loginName) 107 | 108 | return resourceLoginRead(ctx, data, meta) 109 | } 110 | 111 | func resourceLoginRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { 112 | logger := loggerFromMeta(meta, "login", "read") 113 | logger.Debug().Msgf("Read %s", getLoginID(data)) 114 | 115 | loginName := data.Get(loginNameProp).(string) 116 | 117 | connector, err := getLoginConnector(meta, data) 118 | if err != nil { 119 | return diag.FromErr(err) 120 | } 121 | 122 | login, err := connector.GetLogin(ctx, loginName) 123 | if err != nil { 124 | return diag.FromErr(errors.Wrapf(err, "unable to read login [%s]", loginName)) 125 | } 126 | if login == nil { 127 | logger.Info().Msgf("No login found for [%s]", loginName) 128 | data.SetId("") 129 | } else { 130 | if err = data.Set(principalIdProp, login.PrincipalID); err != nil { 131 | return diag.FromErr(err) 132 | } 133 | if err = data.Set(sidStrProp, login.SIDStr); err != nil { 134 | return diag.FromErr(err) 135 | } 136 | if err = data.Set(defaultDatabaseProp, login.DefaultDatabase); err != nil { 137 | return diag.FromErr(err) 138 | } 139 | if err = data.Set(defaultLanguageProp, login.DefaultLanguage); err != nil { 140 | return diag.FromErr(err) 141 | } 142 | } 143 | 144 | return nil 145 | } 146 | 147 | func resourceLoginUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { 148 | logger := loggerFromMeta(meta, "login", "update") 149 | logger.Debug().Msgf("Update %s", data.Id()) 150 | 151 | loginName := data.Get(loginNameProp).(string) 152 | password := data.Get(passwordProp).(string) 153 | defaultDatabase := data.Get(defaultDatabaseProp).(string) 154 | defaultLanguage := data.Get(defaultLanguageProp).(string) 155 | 156 | connector, err := getLoginConnector(meta, data) 157 | if err != nil { 158 | return diag.FromErr(err) 159 | } 160 | 161 | if err = connector.UpdateLogin(ctx, loginName, password, defaultDatabase, defaultLanguage); err != nil { 162 | return diag.FromErr(errors.Wrapf(err, "unable to update login [%s]", loginName)) 163 | } 164 | 165 | logger.Info().Msgf("updated login [%s]", loginName) 166 | 167 | return resourceLoginRead(ctx, data, meta) 168 | } 169 | 170 | func resourceLoginDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { 171 | logger := loggerFromMeta(meta, "login", "delete") 172 | logger.Debug().Msgf("Delete %s", data.Id()) 173 | 174 | loginName := data.Get(loginNameProp).(string) 175 | 176 | connector, err := getLoginConnector(meta, data) 177 | if err != nil { 178 | return diag.FromErr(err) 179 | } 180 | 181 | if err = connector.DeleteLogin(ctx, loginName); err != nil { 182 | return diag.FromErr(errors.Wrapf(err, "unable to delete login [%s]", loginName)) 183 | } 184 | 185 | logger.Info().Msgf("deleted login [%s]", loginName) 186 | 187 | // d.SetId("") is automatically called assuming delete returns no errors, but it is added here for explicitness. 188 | data.SetId("") 189 | 190 | return nil 191 | } 192 | 193 | func resourceLoginImport(ctx context.Context, data *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { 194 | logger := loggerFromMeta(meta, "login", "import") 195 | logger.Debug().Msgf("Import %s", data.Id()) 196 | 197 | server, u, err := serverFromId(data.Id()) 198 | if err != nil { 199 | return nil, err 200 | } 201 | if err = data.Set(serverProp, server); err != nil { 202 | return nil, err 203 | } 204 | 205 | parts := strings.Split(u.Path, "/") 206 | if len(parts) != 2 { 207 | return nil, errors.New("invalid ID") 208 | } 209 | if err = data.Set(loginNameProp, parts[1]); err != nil { 210 | return nil, err 211 | } 212 | 213 | data.SetId(getLoginID(data)) 214 | 215 | loginName := data.Get(loginNameProp).(string) 216 | 217 | connector, err := getLoginConnector(meta, data) 218 | if err != nil { 219 | return nil, err 220 | } 221 | 222 | login, err := connector.GetLogin(ctx, loginName) 223 | if err != nil { 224 | return nil, errors.Wrapf(err, "unable to read login [%s] for import", loginName) 225 | } 226 | 227 | if login == nil { 228 | return nil, errors.Errorf("no login [%s] found for import", loginName) 229 | } 230 | 231 | if err = data.Set(principalIdProp, login.PrincipalID); err != nil { 232 | return nil, err 233 | } 234 | if err = data.Set(sidStrProp, login.SIDStr); err != nil { 235 | return nil, err 236 | } 237 | if err = data.Set(defaultDatabaseProp, login.DefaultDatabase); err != nil { 238 | return nil, err 239 | } 240 | if err = data.Set(defaultLanguageProp, login.DefaultLanguage); err != nil { 241 | return nil, err 242 | } 243 | 244 | return []*schema.ResourceData{data}, nil 245 | } 246 | 247 | func getLoginConnector(meta interface{}, data *schema.ResourceData) (LoginConnector, error) { 248 | provider := meta.(model.Provider) 249 | connector, err := provider.GetConnector(serverProp, data) 250 | if err != nil { 251 | return nil, err 252 | } 253 | return connector.(LoginConnector), nil 254 | } 255 | -------------------------------------------------------------------------------- /mssql/resource_login_import_test.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 5 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 6 | "testing" 7 | ) 8 | 9 | func TestAccLogin_Local_BasicImport(t *testing.T) { 10 | resource.Test(t, resource.TestCase{ 11 | PreCheck: func() { testAccPreCheck(t) }, 12 | IsUnitTest: runLocalAccTests, 13 | ProviderFactories: testAccProviders, 14 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 15 | Steps: []resource.TestStep{ 16 | { 17 | Config: testAccCheckLogin(t, "test_import", false, map[string]interface{}{"login_name": "login_import", "password": "valueIsH8kd$¡"}), 18 | Check: resource.ComposeTestCheckFunc( 19 | testAccCheckLoginExists("mssql_login.test_import"), 20 | ), 21 | }, 22 | { 23 | ResourceName: "mssql_login.test_import", 24 | ImportState: true, 25 | ImportStateVerify: true, 26 | ImportStateVerifyIgnore: []string{"password"}, 27 | ImportStateIdFunc: testAccImportStateId("mssql_login.test_import", false), 28 | }, 29 | }, 30 | }) 31 | } 32 | -------------------------------------------------------------------------------- /mssql/resource_login_test.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 6 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 7 | "os" 8 | "testing" 9 | ) 10 | 11 | func TestAccLogin_Local_Basic(t *testing.T) { 12 | resource.Test(t, resource.TestCase{ 13 | PreCheck: func() { testAccPreCheck(t) }, 14 | IsUnitTest: runLocalAccTests, 15 | ProviderFactories: testAccProviders, 16 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 17 | Steps: []resource.TestStep{ 18 | { 19 | Config: testAccCheckLogin(t, "basic", false, map[string]interface{}{"login_name": "login_basic", "password": "valueIsH8kd$¡"}), 20 | Check: resource.ComposeTestCheckFunc( 21 | testAccCheckLoginExists("mssql_login.basic"), 22 | testAccCheckLoginWorks("mssql_login.basic"), 23 | resource.TestCheckResourceAttr("mssql_login.basic", "login_name", "login_basic"), 24 | resource.TestCheckResourceAttr("mssql_login.basic", "password", "valueIsH8kd$¡"), 25 | resource.TestCheckResourceAttr("mssql_login.basic", "default_database", "master"), 26 | resource.TestCheckResourceAttr("mssql_login.basic", "default_language", "us_english"), 27 | resource.TestCheckResourceAttr("mssql_login.basic", "server.#", "1"), 28 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.host", "localhost"), 29 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.port", "1433"), 30 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.#", "1"), 31 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), 32 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), 33 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.#", "0"), 34 | resource.TestCheckResourceAttrSet("mssql_login.basic", "principal_id"), 35 | ), 36 | }, 37 | }, 38 | }) 39 | } 40 | 41 | func TestAccLogin_Local_Basic_SID(t *testing.T) { 42 | resource.Test(t, resource.TestCase{ 43 | PreCheck: func() { testAccPreCheck(t) }, 44 | IsUnitTest: runLocalAccTests, 45 | ProviderFactories: testAccProviders, 46 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 47 | Steps: []resource.TestStep{ 48 | { 49 | Config: testAccCheckLogin(t, "basic", false, map[string]interface{}{"login_name": "login_basic", "password": "valueIsH8kd$¡", "sid": "0xB7BDEF7990D03541BAA2AD73E4FF18E8"}), 50 | Check: resource.ComposeTestCheckFunc( 51 | testAccCheckLoginExists("mssql_login.basic"), 52 | testAccCheckLoginWorks("mssql_login.basic"), 53 | resource.TestCheckResourceAttr("mssql_login.basic", "login_name", "login_basic"), 54 | resource.TestCheckResourceAttr("mssql_login.basic", "password", "valueIsH8kd$¡"), 55 | resource.TestCheckResourceAttr("mssql_login.basic", "sid", "0xB7BDEF7990D03541BAA2AD73E4FF18E8"), 56 | resource.TestCheckResourceAttr("mssql_login.basic", "default_database", "master"), 57 | resource.TestCheckResourceAttr("mssql_login.basic", "default_language", "us_english"), 58 | resource.TestCheckResourceAttr("mssql_login.basic", "server.#", "1"), 59 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.host", "localhost"), 60 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.port", "1433"), 61 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.#", "1"), 62 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), 63 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), 64 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.#", "0"), 65 | resource.TestCheckResourceAttrSet("mssql_login.basic", "principal_id"), 66 | ), 67 | }, 68 | }, 69 | }) 70 | } 71 | 72 | func TestAccLogin_Azure_Basic(t *testing.T) { 73 | resource.Test(t, resource.TestCase{ 74 | PreCheck: func() { testAccPreCheck(t) }, 75 | ProviderFactories: testAccProviders, 76 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 77 | Steps: []resource.TestStep{ 78 | { 79 | Config: testAccCheckLogin(t, "basic", true, map[string]interface{}{"login_name": "login_basic", "password": "valueIsH8kd$¡"}), 80 | Check: resource.ComposeTestCheckFunc( 81 | testAccCheckLoginExists("mssql_login.basic"), 82 | resource.TestCheckResourceAttr("mssql_login.basic", "login_name", "login_basic"), 83 | resource.TestCheckResourceAttr("mssql_login.basic", "password", "valueIsH8kd$¡"), 84 | resource.TestCheckResourceAttr("mssql_login.basic", "default_database", "master"), 85 | resource.TestCheckResourceAttr("mssql_login.basic", "default_language", "us_english"), 86 | resource.TestCheckResourceAttr("mssql_login.basic", "server.#", "1"), 87 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), 88 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.port", "1433"), 89 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.#", "1"), 90 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), 91 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), 92 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), 93 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.#", "0"), 94 | resource.TestCheckResourceAttrSet("mssql_login.basic", "principal_id"), 95 | ), 96 | }, 97 | }, 98 | }) 99 | } 100 | 101 | func TestAccLogin_Azure_Basic_SID(t *testing.T) { 102 | resource.Test(t, resource.TestCase{ 103 | PreCheck: func() { testAccPreCheck(t) }, 104 | ProviderFactories: testAccProviders, 105 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 106 | Steps: []resource.TestStep{ 107 | { 108 | Config: testAccCheckLogin(t, "basic", true, map[string]interface{}{"login_name": "login_basic", "password": "valueIsH8kd$¡", "sid": "0x01060000000000640000000000000000BAF5FC800B97EF49AC6FD89469C4987F"}), 109 | Check: resource.ComposeTestCheckFunc( 110 | testAccCheckLoginExists("mssql_login.basic"), 111 | resource.TestCheckResourceAttr("mssql_login.basic", "login_name", "login_basic"), 112 | resource.TestCheckResourceAttr("mssql_login.basic", "password", "valueIsH8kd$¡"), 113 | resource.TestCheckResourceAttr("mssql_login.basic", "sid", "0x01060000000000640000000000000000BAF5FC800B97EF49AC6FD89469C4987F"), 114 | resource.TestCheckResourceAttr("mssql_login.basic", "default_database", "master"), 115 | resource.TestCheckResourceAttr("mssql_login.basic", "default_language", "us_english"), 116 | resource.TestCheckResourceAttr("mssql_login.basic", "server.#", "1"), 117 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), 118 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.port", "1433"), 119 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.#", "1"), 120 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), 121 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), 122 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), 123 | resource.TestCheckResourceAttr("mssql_login.basic", "server.0.login.#", "0"), 124 | resource.TestCheckResourceAttrSet("mssql_login.basic", "principal_id"), 125 | ), 126 | }, 127 | }, 128 | }) 129 | } 130 | 131 | func TestAccLogin_Local_UpdateLoginName(t *testing.T) { 132 | resource.Test(t, resource.TestCase{ 133 | PreCheck: func() { testAccPreCheck(t) }, 134 | IsUnitTest: runLocalAccTests, 135 | ProviderFactories: testAccProviders, 136 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 137 | Steps: []resource.TestStep{ 138 | { 139 | Config: testAccCheckLogin(t, "test_update", false, map[string]interface{}{"login_name": "login_update_pre", "password": "valueIsH8kd$¡"}), 140 | Check: resource.ComposeTestCheckFunc( 141 | resource.TestCheckResourceAttr("mssql_login.test_update", "login_name", "login_update_pre"), 142 | testAccCheckLoginExists("mssql_login.test_update"), 143 | testAccCheckLoginWorks("mssql_login.test_update"), 144 | ), 145 | }, 146 | { 147 | Config: testAccCheckLogin(t, "test_update", false, map[string]interface{}{"login_name": "login_update_post", "password": "valueIsH8kd$¡"}), 148 | Check: resource.ComposeTestCheckFunc( 149 | resource.TestCheckResourceAttr("mssql_login.test_update", "login_name", "login_update_post"), 150 | testAccCheckLoginExists("mssql_login.test_update"), 151 | testAccCheckLoginWorks("mssql_login.test_update"), 152 | ), 153 | }, 154 | }}) 155 | } 156 | 157 | func TestAccLogin_Local_UpdatePassword(t *testing.T) { 158 | resource.Test(t, resource.TestCase{ 159 | PreCheck: func() { testAccPreCheck(t) }, 160 | IsUnitTest: runLocalAccTests, 161 | ProviderFactories: testAccProviders, 162 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 163 | Steps: []resource.TestStep{ 164 | { 165 | Config: testAccCheckLogin(t, "test_update", false, map[string]interface{}{"login_name": "login_update", "password": "valueIsH8kd$¡"}), 166 | Check: resource.ComposeTestCheckFunc( 167 | resource.TestCheckResourceAttr("mssql_login.test_update", "password", "valueIsH8kd$¡"), 168 | testAccCheckLoginExists("mssql_login.test_update"), 169 | testAccCheckLoginWorks("mssql_login.test_update"), 170 | ), 171 | }, 172 | { 173 | Config: testAccCheckLogin(t, "test_update", false, map[string]interface{}{"login_name": "login_update", "password": "otherIsH8kd$¡"}), 174 | Check: resource.ComposeTestCheckFunc( 175 | resource.TestCheckResourceAttr("mssql_login.test_update", "password", "otherIsH8kd$¡"), 176 | testAccCheckLoginExists("mssql_login.test_update"), 177 | testAccCheckLoginWorks("mssql_login.test_update"), 178 | ), 179 | }, 180 | }}) 181 | } 182 | 183 | func TestAccLogin_Local_UpdateDefaultDatabase(t *testing.T) { 184 | resource.Test(t, resource.TestCase{ 185 | PreCheck: func() { testAccPreCheck(t) }, 186 | IsUnitTest: runLocalAccTests, 187 | ProviderFactories: testAccProviders, 188 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 189 | Steps: []resource.TestStep{ 190 | { 191 | Config: testAccCheckLogin(t, "test_update", false, map[string]interface{}{"login_name": "login_update", "password": "valueIsH8kd$¡"}), 192 | Check: resource.ComposeTestCheckFunc( 193 | resource.TestCheckResourceAttr("mssql_login.test_update", "default_database", "master"), 194 | testAccCheckLoginExists("mssql_login.test_update", Check{"default_database", "==", "master"}), 195 | testAccCheckLoginWorks("mssql_login.test_update"), 196 | ), 197 | }, 198 | { 199 | Config: testAccCheckLogin(t, "test_update", false, map[string]interface{}{"login_name": "login_update", "password": "valueIsH8kd$¡", "default_database": "tempdb"}), 200 | Check: resource.ComposeTestCheckFunc( 201 | resource.TestCheckResourceAttr("mssql_login.test_update", "default_database", "tempdb"), 202 | testAccCheckLoginExists("mssql_login.test_update", Check{"default_database", "==", "tempdb"}), 203 | testAccCheckLoginWorks("mssql_login.test_update"), 204 | ), 205 | }, 206 | }}) 207 | } 208 | 209 | func TestAccLogin_Local_UpdateDefaultLanguage(t *testing.T) { 210 | resource.Test(t, resource.TestCase{ 211 | PreCheck: func() { testAccPreCheck(t) }, 212 | IsUnitTest: runLocalAccTests, 213 | ProviderFactories: testAccProviders, 214 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 215 | Steps: []resource.TestStep{ 216 | { 217 | Config: testAccCheckLogin(t, "test_update", false, map[string]interface{}{"login_name": "login_update", "password": "valueIsH8kd$¡"}), 218 | Check: resource.ComposeTestCheckFunc( 219 | resource.TestCheckResourceAttr("mssql_login.test_update", "default_language", "us_english"), 220 | testAccCheckLoginExists("mssql_login.test_update"), 221 | testAccCheckLoginWorks("mssql_login.test_update"), 222 | ), 223 | }, 224 | { 225 | Config: testAccCheckLogin(t, "test_update", false, map[string]interface{}{"login_name": "login_update", "password": "valueIsH8kd$¡", "default_language": "russian"}), 226 | Check: resource.ComposeTestCheckFunc( 227 | resource.TestCheckResourceAttr("mssql_login.test_update", "default_language", "russian"), 228 | testAccCheckLoginExists("mssql_login.test_update", Check{"default_language", "==", "russian"}), 229 | testAccCheckLoginWorks("mssql_login.test_update"), 230 | ), 231 | }, 232 | }}) 233 | } 234 | 235 | func TestAccLogin_Azure_UpdateLoginName(t *testing.T) { 236 | resource.Test(t, resource.TestCase{ 237 | PreCheck: func() { testAccPreCheck(t) }, 238 | ProviderFactories: testAccProviders, 239 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 240 | Steps: []resource.TestStep{ 241 | { 242 | Config: testAccCheckLogin(t, "test_update", true, map[string]interface{}{"login_name": "login_update_pre", "password": "valueIsH8kd$¡"}), 243 | Check: resource.ComposeTestCheckFunc( 244 | resource.TestCheckResourceAttr("mssql_login.test_update", "login_name", "login_update_pre"), 245 | testAccCheckLoginExists("mssql_login.test_update"), 246 | ), 247 | }, 248 | { 249 | Config: testAccCheckLogin(t, "test_update", true, map[string]interface{}{"login_name": "login_update_post", "password": "valueIsH8kd$¡"}), 250 | Check: resource.ComposeTestCheckFunc( 251 | resource.TestCheckResourceAttr("mssql_login.test_update", "login_name", "login_update_post"), 252 | testAccCheckLoginExists("mssql_login.test_update"), 253 | ), 254 | }, 255 | }}) 256 | } 257 | 258 | func TestAccLogin_Azure_UpdatePassword(t *testing.T) { 259 | resource.Test(t, resource.TestCase{ 260 | PreCheck: func() { testAccPreCheck(t) }, 261 | ProviderFactories: testAccProviders, 262 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 263 | Steps: []resource.TestStep{ 264 | { 265 | Config: testAccCheckLogin(t, "test_update", true, map[string]interface{}{"login_name": "login_update", "password": "valueIsH8kd$¡"}), 266 | Check: resource.ComposeTestCheckFunc( 267 | resource.TestCheckResourceAttr("mssql_login.test_update", "password", "valueIsH8kd$¡"), 268 | testAccCheckLoginExists("mssql_login.test_update"), 269 | ), 270 | }, 271 | { 272 | Config: testAccCheckLogin(t, "test_update", true, map[string]interface{}{"login_name": "login_update", "password": "otherIsH8kd$¡"}), 273 | Check: resource.ComposeTestCheckFunc( 274 | resource.TestCheckResourceAttr("mssql_login.test_update", "password", "otherIsH8kd$¡"), 275 | testAccCheckLoginExists("mssql_login.test_update"), 276 | ), 277 | }, 278 | }}) 279 | } 280 | 281 | func testAccCheckLogin(t *testing.T, name string, azure bool, data map[string]interface{}) string { 282 | text := `resource "mssql_login" "{{ .name }}" { 283 | server { 284 | host = "{{ .host }}" 285 | {{ if .azure }}azure_login {}{{ else }}login {}{{ end }} 286 | } 287 | login_name = "{{ .login_name }}" 288 | password = "{{ .password }}" 289 | {{ with .sid }}sid = "{{ . }}"{{ end }} 290 | {{ with .default_database }}default_database = "{{ . }}"{{ end }} 291 | {{ with .default_language }}default_language = "{{ . }}"{{ end }} 292 | }` 293 | data["name"] = name 294 | data["azure"] = azure 295 | if azure { 296 | data["host"] = os.Getenv("TF_ACC_SQL_SERVER") 297 | } else { 298 | data["host"] = "localhost" 299 | } 300 | res, err := templateToString(name, text, data) 301 | if err != nil { 302 | t.Fatalf("%s", err) 303 | } 304 | return res 305 | } 306 | 307 | func testAccCheckLoginDestroy(state *terraform.State) error { 308 | for _, rs := range state.RootModule().Resources { 309 | if rs.Type != "mssql_login" { 310 | continue 311 | } 312 | 313 | connector, err := getTestConnector(rs.Primary.Attributes) 314 | if err != nil { 315 | return err 316 | } 317 | 318 | loginName := rs.Primary.Attributes["login_name"] 319 | login, err := connector.GetLogin(loginName) 320 | if login != nil { 321 | return fmt.Errorf("login still exists") 322 | } 323 | if err != nil { 324 | return fmt.Errorf("expected no error, got %s", err) 325 | } 326 | } 327 | return nil 328 | } 329 | 330 | func testAccCheckLoginExists(resource string, checks ...Check) resource.TestCheckFunc { 331 | return func(state *terraform.State) error { 332 | rs, ok := state.RootModule().Resources[resource] 333 | if !ok { 334 | return fmt.Errorf("not found: %s", resource) 335 | } 336 | if rs.Type != "mssql_login" { 337 | return fmt.Errorf("expected resource of type %s, got %s", "mssql_login", rs.Type) 338 | } 339 | if rs.Primary.ID == "" { 340 | return fmt.Errorf("no record ID is set") 341 | } 342 | connector, err := getTestConnector(rs.Primary.Attributes) 343 | if err != nil { 344 | return err 345 | } 346 | 347 | loginName := rs.Primary.Attributes["login_name"] 348 | login, err := connector.GetLogin(loginName) 349 | if login == nil { 350 | return fmt.Errorf("login does not exist") 351 | } 352 | if err != nil { 353 | return fmt.Errorf("expected no error, got %s", err) 354 | } 355 | 356 | var actual interface{} 357 | for _, check := range checks { 358 | switch check.name { 359 | case "default_database": 360 | actual = login.DefaultDatabase 361 | case "default_language": 362 | actual = login.DefaultLanguage 363 | default: 364 | return fmt.Errorf("unknown property %s", check.name) 365 | } 366 | if (check.op == "" || check.op == "==") && check.expected != actual { 367 | return fmt.Errorf("expected %s == %s, got %s", check.name, check.expected, actual) 368 | } 369 | if check.op == "!=" && check.expected == actual { 370 | return fmt.Errorf("expected %s != %s, got %s", check.name, check.expected, actual) 371 | } 372 | } 373 | return nil 374 | } 375 | } 376 | 377 | func testAccCheckLoginWorks(resource string) resource.TestCheckFunc { 378 | return func(state *terraform.State) error { 379 | rs, ok := state.RootModule().Resources[resource] 380 | if !ok { 381 | return fmt.Errorf("not found: %s", resource) 382 | } 383 | if rs.Type != "mssql_login" { 384 | return fmt.Errorf("expected resource of type %s, got %s", "mssql_login", rs.Type) 385 | } 386 | if rs.Primary.ID == "" { 387 | return fmt.Errorf("no record ID is set") 388 | } 389 | connector, err := getTestLoginConnector(rs.Primary.Attributes) 390 | if err != nil { 391 | return err 392 | } 393 | systemUser, err := connector.GetSystemUser() 394 | if err != nil { 395 | return err 396 | } 397 | if systemUser != rs.Primary.Attributes[loginNameProp] { 398 | return fmt.Errorf("expected to log in as [%s], got [%s]", rs.Primary.Attributes[loginNameProp], systemUser) 399 | } 400 | return nil 401 | } 402 | } 403 | -------------------------------------------------------------------------------- /mssql/resource_user.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "context" 5 | "strings" 6 | 7 | "github.com/betr-io/terraform-provider-mssql/mssql/model" 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/diag" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 10 | "github.com/pkg/errors" 11 | ) 12 | 13 | func resourceUser() *schema.Resource { 14 | return &schema.Resource{ 15 | CreateContext: resourceUserCreate, 16 | ReadContext: resourceUserRead, 17 | UpdateContext: resourceUserUpdate, 18 | DeleteContext: resourceUserDelete, 19 | Importer: &schema.ResourceImporter{ 20 | StateContext: resourceUserImport, 21 | }, 22 | Schema: map[string]*schema.Schema{ 23 | serverProp: { 24 | Type: schema.TypeList, 25 | MaxItems: 1, 26 | Required: true, 27 | Elem: &schema.Resource{ 28 | Schema: getServerSchema(serverProp), 29 | }, 30 | }, 31 | databaseProp: { 32 | Type: schema.TypeString, 33 | Optional: true, 34 | ForceNew: true, 35 | Default: "master", 36 | }, 37 | usernameProp: { 38 | Type: schema.TypeString, 39 | Required: true, 40 | ForceNew: true, 41 | }, 42 | objectIdProp: { 43 | Type: schema.TypeString, 44 | Optional: true, 45 | ForceNew: true, 46 | }, 47 | loginNameProp: { 48 | Type: schema.TypeString, 49 | Optional: true, 50 | ForceNew: true, 51 | }, 52 | passwordProp: { 53 | Type: schema.TypeString, 54 | Optional: true, 55 | ForceNew: true, 56 | Sensitive: true, 57 | }, 58 | sidStrProp: { 59 | Type: schema.TypeString, 60 | Computed: true, 61 | }, 62 | authenticationTypeProp: { 63 | Type: schema.TypeString, 64 | Computed: true, 65 | }, 66 | principalIdProp: { 67 | Type: schema.TypeInt, 68 | Computed: true, 69 | }, 70 | defaultSchemaProp: { 71 | Type: schema.TypeString, 72 | Optional: true, 73 | Default: defaultSchemaPropDefault, 74 | }, 75 | defaultLanguageProp: { 76 | Type: schema.TypeString, 77 | Optional: true, 78 | DiffSuppressFunc: func(k, old, new string, data *schema.ResourceData) bool { 79 | return data.Get(authenticationTypeProp) == "INSTANCE" || old == new 80 | }, 81 | }, 82 | rolesProp: { 83 | Type: schema.TypeSet, 84 | Optional: true, 85 | Elem: &schema.Schema{ 86 | Type: schema.TypeString, 87 | }, 88 | }, 89 | }, 90 | Timeouts: &schema.ResourceTimeout{ 91 | Default: defaultTimeout, 92 | Read: defaultTimeout, 93 | }, 94 | } 95 | } 96 | 97 | type UserConnector interface { 98 | CreateUser(ctx context.Context, database string, user *model.User) error 99 | GetUser(ctx context.Context, database, username string) (*model.User, error) 100 | UpdateUser(ctx context.Context, database string, user *model.User) error 101 | DeleteUser(ctx context.Context, database, username string) error 102 | } 103 | 104 | func resourceUserCreate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { 105 | logger := loggerFromMeta(meta, "user", "create") 106 | logger.Debug().Msgf("Create %s", getUserID(data)) 107 | 108 | database := data.Get(databaseProp).(string) 109 | username := data.Get(usernameProp).(string) 110 | objectId := data.Get(objectIdProp).(string) 111 | loginName := data.Get(loginNameProp).(string) 112 | password := data.Get(passwordProp).(string) 113 | defaultSchema := data.Get(defaultSchemaProp).(string) 114 | defaultLanguage := data.Get(defaultLanguageProp).(string) 115 | roles := data.Get(rolesProp).(*schema.Set).List() 116 | 117 | if loginName != "" && password != "" { 118 | return diag.Errorf(loginNameProp + " and " + passwordProp + " cannot both be set") 119 | } 120 | var authType string 121 | if loginName != "" { 122 | authType = "INSTANCE" 123 | } else if password != "" { 124 | authType = "DATABASE" 125 | } else { 126 | authType = "EXTERNAL" 127 | } 128 | if defaultSchema == "" { 129 | return diag.Errorf(defaultSchemaProp + " cannot be empty") 130 | } 131 | 132 | connector, err := getUserConnector(meta, data) 133 | if err != nil { 134 | return diag.FromErr(err) 135 | } 136 | 137 | user := &model.User{ 138 | Username: username, 139 | ObjectId: objectId, 140 | LoginName: loginName, 141 | Password: password, 142 | AuthType: authType, 143 | DefaultSchema: defaultSchema, 144 | DefaultLanguage: defaultLanguage, 145 | Roles: toStringSlice(roles), 146 | } 147 | if err = connector.CreateUser(ctx, database, user); err != nil { 148 | return diag.FromErr(errors.Wrapf(err, "unable to create user [%s].[%s]", database, username)) 149 | } 150 | 151 | data.SetId(getUserID(data)) 152 | 153 | logger.Info().Msgf("created user [%s].[%s]", database, username) 154 | 155 | return resourceUserRead(ctx, data, meta) 156 | } 157 | 158 | func resourceUserRead(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { 159 | logger := loggerFromMeta(meta, "user", "read") 160 | logger.Debug().Msgf("Read %s", data.Id()) 161 | 162 | database := data.Get(databaseProp).(string) 163 | username := data.Get(usernameProp).(string) 164 | 165 | connector, err := getUserConnector(meta, data) 166 | if err != nil { 167 | return diag.FromErr(err) 168 | } 169 | 170 | user, err := connector.GetUser(ctx, database, username) 171 | if err != nil { 172 | return diag.FromErr(errors.Wrapf(err, "unable to read user [%s].[%s]", database, username)) 173 | } 174 | if user == nil { 175 | logger.Info().Msgf("No user found for [%s].[%s]", database, username) 176 | data.SetId("") 177 | } else { 178 | if err = data.Set(loginNameProp, user.LoginName); err != nil { 179 | return diag.FromErr(err) 180 | } 181 | if err = data.Set(sidStrProp, user.SIDStr); err != nil { 182 | return diag.FromErr(err) 183 | } 184 | if err = data.Set(authenticationTypeProp, user.AuthType); err != nil { 185 | return diag.FromErr(err) 186 | } 187 | if err = data.Set(principalIdProp, user.PrincipalID); err != nil { 188 | return diag.FromErr(err) 189 | } 190 | if err = data.Set(defaultSchemaProp, user.DefaultSchema); err != nil { 191 | return diag.FromErr(err) 192 | } 193 | if err = data.Set(defaultLanguageProp, user.DefaultLanguage); err != nil { 194 | return diag.FromErr(err) 195 | } 196 | if err = data.Set(rolesProp, user.Roles); err != nil { 197 | return diag.FromErr(err) 198 | } 199 | } 200 | 201 | return nil 202 | } 203 | 204 | func resourceUserUpdate(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { 205 | logger := loggerFromMeta(meta, "user", "update") 206 | logger.Debug().Msgf("Update %s", data.Id()) 207 | 208 | database := data.Get(databaseProp).(string) 209 | username := data.Get(usernameProp).(string) 210 | defaultSchema := data.Get(defaultSchemaProp).(string) 211 | defaultLanguage := data.Get(defaultLanguageProp).(string) 212 | roles := data.Get(rolesProp).(*schema.Set).List() 213 | 214 | connector, err := getUserConnector(meta, data) 215 | if err != nil { 216 | return diag.FromErr(err) 217 | } 218 | 219 | user := &model.User{ 220 | Username: username, 221 | DefaultSchema: defaultSchema, 222 | DefaultLanguage: defaultLanguage, 223 | Roles: toStringSlice(roles), 224 | } 225 | if err = connector.UpdateUser(ctx, database, user); err != nil { 226 | return diag.FromErr(errors.Wrapf(err, "unable to update user [%s].[%s]", database, username)) 227 | } 228 | 229 | data.SetId(getUserID(data)) 230 | 231 | logger.Info().Msgf("updated user [%s].[%s]", database, username) 232 | 233 | return resourceUserRead(ctx, data, meta) 234 | } 235 | 236 | func resourceUserDelete(ctx context.Context, data *schema.ResourceData, meta interface{}) diag.Diagnostics { 237 | logger := loggerFromMeta(meta, "user", "delete") 238 | logger.Debug().Msgf("Delete %s", data.Id()) 239 | 240 | database := data.Get(databaseProp).(string) 241 | username := data.Get(usernameProp).(string) 242 | 243 | connector, err := getUserConnector(meta, data) 244 | if err != nil { 245 | return diag.FromErr(err) 246 | } 247 | 248 | if err = connector.DeleteUser(ctx, database, username); err != nil { 249 | return diag.FromErr(errors.Wrapf(err, "unable to delete user [%s].[%s]", database, username)) 250 | } 251 | 252 | logger.Info().Msgf("deleted user [%s].[%s]", database, username) 253 | 254 | // d.SetId("") is automatically called assuming delete returns no errors, but it is added here for explicitness. 255 | data.SetId("") 256 | 257 | return nil 258 | } 259 | 260 | func resourceUserImport(ctx context.Context, data *schema.ResourceData, meta interface{}) ([]*schema.ResourceData, error) { 261 | logger := loggerFromMeta(meta, "user", "import") 262 | logger.Debug().Msgf("Import %s", data.Id()) 263 | 264 | server, u, err := serverFromId(data.Id()) 265 | if err != nil { 266 | return nil, err 267 | } 268 | if err = data.Set(serverProp, server); err != nil { 269 | return nil, err 270 | } 271 | 272 | parts := strings.Split(u.Path, "/") 273 | if len(parts) != 3 { 274 | return nil, errors.New("invalid ID") 275 | } 276 | if err = data.Set(databaseProp, parts[1]); err != nil { 277 | return nil, err 278 | } 279 | if err = data.Set(usernameProp, parts[2]); err != nil { 280 | return nil, err 281 | } 282 | 283 | data.SetId(getUserID(data)) 284 | 285 | database := data.Get(databaseProp).(string) 286 | username := data.Get(usernameProp).(string) 287 | 288 | connector, err := getUserConnector(meta, data) 289 | if err != nil { 290 | return nil, err 291 | } 292 | 293 | login, err := connector.GetUser(ctx, database, username) 294 | if err != nil { 295 | return nil, errors.Wrapf(err, "unable to read user [%s].[%s] for import", database, username) 296 | } 297 | 298 | if login == nil { 299 | return nil, errors.Errorf("no user [%s].[%s] found for import", database, username) 300 | } 301 | 302 | if err = data.Set(authenticationTypeProp, login.AuthType); err != nil { 303 | return nil, err 304 | } 305 | if err = data.Set(principalIdProp, login.PrincipalID); err != nil { 306 | return nil, err 307 | } 308 | if err = data.Set(defaultSchemaProp, login.DefaultSchema); err != nil { 309 | return nil, err 310 | } 311 | if err = data.Set(defaultLanguageProp, login.DefaultLanguage); err != nil { 312 | return nil, err 313 | } 314 | if err = data.Set(rolesProp, login.Roles); err != nil { 315 | return nil, err 316 | } 317 | 318 | return []*schema.ResourceData{data}, nil 319 | } 320 | 321 | func getUserConnector(meta interface{}, data *schema.ResourceData) (UserConnector, error) { 322 | provider := meta.(model.Provider) 323 | connector, err := provider.GetConnector(serverProp, data) 324 | if err != nil { 325 | return nil, err 326 | } 327 | return connector.(UserConnector), nil 328 | } 329 | 330 | func toStringSlice(values []interface{}) []string { 331 | result := make([]string, len(values)) 332 | for i, v := range values { 333 | result[i] = v.(string) 334 | } 335 | return result 336 | } 337 | -------------------------------------------------------------------------------- /mssql/resource_user_import_test.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 5 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 6 | "testing" 7 | ) 8 | 9 | func TestAccUser_Local_BasicImport(t *testing.T) { 10 | resource.Test(t, resource.TestCase{ 11 | PreCheck: func() { testAccPreCheck(t) }, 12 | IsUnitTest: runLocalAccTests, 13 | ProviderFactories: testAccProviders, 14 | CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, 15 | Steps: []resource.TestStep{ 16 | { 17 | Config: testAccCheckUser(t, "test_import", "login", map[string]interface{}{"username": "user_import", "login_name": "user_import", "login_password": "valueIsH8kd$¡"}), 18 | Check: resource.ComposeTestCheckFunc( 19 | testAccCheckUserExists("mssql_user.test_import"), 20 | ), 21 | }, 22 | { 23 | ResourceName: "mssql_user.test_import", 24 | ImportState: true, 25 | ImportStateVerify: true, 26 | ImportStateIdFunc: testAccImportStateId("mssql_user.test_import", false), 27 | }, 28 | }, 29 | }) 30 | } 31 | -------------------------------------------------------------------------------- /mssql/resource_user_test.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "testing" 7 | 8 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" 9 | "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" 10 | ) 11 | 12 | func TestAccUser_Local_Instance(t *testing.T) { 13 | resource.Test(t, resource.TestCase{ 14 | PreCheck: func() { testAccPreCheck(t) }, 15 | IsUnitTest: runLocalAccTests, 16 | ProviderFactories: testAccProviders, 17 | CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, 18 | Steps: []resource.TestStep{ 19 | { 20 | Config: testAccCheckUser(t, "instance", "login", map[string]interface{}{"username": "instance", "login_name": "user_instance", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), 21 | Check: resource.ComposeTestCheckFunc( 22 | testAccCheckUserExists("mssql_user.instance"), 23 | testAccCheckDatabaseUserWorks("mssql_user.instance", "user_instance", "valueIsH8kd$¡"), 24 | resource.TestCheckResourceAttr("mssql_user.instance", "database", "master"), 25 | resource.TestCheckResourceAttr("mssql_user.instance", "username", "instance"), 26 | resource.TestCheckResourceAttr("mssql_user.instance", "login_name", "user_instance"), 27 | resource.TestCheckResourceAttr("mssql_user.instance", "authentication_type", "INSTANCE"), 28 | resource.TestCheckResourceAttr("mssql_user.instance", "default_schema", "dbo"), 29 | resource.TestCheckResourceAttr("mssql_user.instance", "default_language", ""), 30 | resource.TestCheckResourceAttr("mssql_user.instance", "roles.#", "1"), 31 | resource.TestCheckResourceAttr("mssql_user.instance", "roles.0", "db_owner"), 32 | resource.TestCheckResourceAttr("mssql_user.instance", "server.#", "1"), 33 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.host", "localhost"), 34 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.port", "1433"), 35 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.login.#", "1"), 36 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), 37 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), 38 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.azure_login.#", "0"), 39 | resource.TestCheckResourceAttrSet("mssql_user.instance", "principal_id"), 40 | resource.TestCheckNoResourceAttr("mssql_user.instance", "password"), 41 | ), 42 | }, 43 | }, 44 | }) 45 | } 46 | 47 | func TestAccMultipleUsers_Local_Instance(t *testing.T) { 48 | resource.Test(t, resource.TestCase{ 49 | PreCheck: func() { testAccPreCheck(t) }, 50 | IsUnitTest: runLocalAccTests, 51 | ProviderFactories: testAccProviders, 52 | CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, 53 | Steps: []resource.TestStep{ 54 | { 55 | Config: testAccCheckMultipleUsers(t, "instance", "login", map[string]interface{}{"username": "instance", "login_name": "user_instance", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}, 4), 56 | Check: resource.ComposeTestCheckFunc(getMultipleUsersExistAccCheck(4)...), 57 | }, 58 | }, 59 | }) 60 | } 61 | 62 | func TestAccUser_Azure_Instance(t *testing.T) { 63 | resource.Test(t, resource.TestCase{ 64 | PreCheck: func() { testAccPreCheck(t) }, 65 | ProviderFactories: testAccProviders, 66 | CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, 67 | Steps: []resource.TestStep{ 68 | { 69 | Config: testAccCheckUser(t, "instance", "azure", map[string]interface{}{"database": "testdb", "username": "instance", "login_name": "user_instance", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), 70 | Check: resource.ComposeTestCheckFunc( 71 | testAccCheckUserExists("mssql_user.instance"), 72 | testAccCheckDatabaseUserWorks("mssql_user.instance", "user_instance", "valueIsH8kd$¡"), 73 | resource.TestCheckResourceAttr("mssql_user.instance", "database", "testdb"), 74 | resource.TestCheckResourceAttr("mssql_user.instance", "username", "instance"), 75 | resource.TestCheckResourceAttr("mssql_user.instance", "login_name", "user_instance"), 76 | resource.TestCheckResourceAttr("mssql_user.instance", "authentication_type", "INSTANCE"), 77 | resource.TestCheckResourceAttr("mssql_user.instance", "default_schema", "dbo"), 78 | resource.TestCheckResourceAttr("mssql_user.instance", "default_language", ""), 79 | resource.TestCheckResourceAttr("mssql_user.instance", "roles.#", "1"), 80 | resource.TestCheckResourceAttr("mssql_user.instance", "roles.0", "db_owner"), 81 | resource.TestCheckResourceAttr("mssql_user.instance", "server.#", "1"), 82 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), 83 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.port", "1433"), 84 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.azure_login.#", "1"), 85 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), 86 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), 87 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), 88 | resource.TestCheckResourceAttr("mssql_user.instance", "server.0.login.#", "0"), 89 | resource.TestCheckResourceAttrSet("mssql_user.instance", "principal_id"), 90 | resource.TestCheckNoResourceAttr("mssql_user.instance", "password"), 91 | ), 92 | }, 93 | }, 94 | }) 95 | } 96 | 97 | func TestAccUser_Azure_Database(t *testing.T) { 98 | resource.Test(t, resource.TestCase{ 99 | PreCheck: func() { testAccPreCheck(t) }, 100 | ProviderFactories: testAccProviders, 101 | CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, 102 | Steps: []resource.TestStep{ 103 | { 104 | Config: testAccCheckUser(t, "database", "azure", map[string]interface{}{"database": "testdb", "username": "database_user", "password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), 105 | Check: resource.ComposeTestCheckFunc( 106 | testAccCheckUserExists("mssql_user.database"), 107 | testAccCheckDatabaseUserWorks("mssql_user.database", "database_user", "valueIsH8kd$¡"), 108 | resource.TestCheckResourceAttr("mssql_user.database", "database", "testdb"), 109 | resource.TestCheckResourceAttr("mssql_user.database", "username", "database_user"), 110 | resource.TestCheckResourceAttr("mssql_user.database", "password", "valueIsH8kd$¡"), 111 | resource.TestCheckResourceAttr("mssql_user.database", "login_name", ""), 112 | resource.TestCheckResourceAttr("mssql_user.database", "authentication_type", "DATABASE"), 113 | resource.TestCheckResourceAttr("mssql_user.database", "default_schema", "dbo"), 114 | resource.TestCheckResourceAttr("mssql_user.database", "default_language", ""), 115 | resource.TestCheckResourceAttr("mssql_user.database", "roles.#", "1"), 116 | resource.TestCheckResourceAttr("mssql_user.database", "roles.0", "db_owner"), 117 | resource.TestCheckResourceAttr("mssql_user.database", "server.#", "1"), 118 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), 119 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.port", "1433"), 120 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.#", "1"), 121 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.0.tenant_id", os.Getenv("MSSQL_TENANT_ID")), 122 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), 123 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), 124 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azuread_default_chain_auth.#", "0"), 125 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azuread_managed_identity_auth.#", "0"), 126 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.login.#", "0"), 127 | resource.TestCheckResourceAttrSet("mssql_user.database", "principal_id"), 128 | ), 129 | }, 130 | }, 131 | }) 132 | } 133 | 134 | func TestAccUser_AzureadChain_Database(t *testing.T) { 135 | resource.Test(t, resource.TestCase{ 136 | PreCheck: func() { testAccPreCheck(t) }, 137 | ProviderFactories: testAccProviders, 138 | CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, 139 | Steps: []resource.TestStep{ 140 | { 141 | Config: testAccCheckUser(t, "database", "fedauth", map[string]interface{}{"database": "testdb", "username": "database_user", "password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), 142 | Check: resource.ComposeTestCheckFunc( 143 | testAccCheckUserExists("mssql_user.database"), 144 | testAccCheckDatabaseUserWorks("mssql_user.database", "database_user", "valueIsH8kd$¡"), 145 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.#", "0"), 146 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azuread_default_chain_auth.#", "1"), 147 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azuread_managed_identity_auth.#", "0"), 148 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.login.#", "0"), 149 | ), 150 | }, 151 | }, 152 | }) 153 | } 154 | 155 | func TestAccUser_AzureadMSI_Database(t *testing.T) { 156 | resource.Test(t, resource.TestCase{ 157 | PreCheck: func() { testAccPreCheck(t) }, 158 | ProviderFactories: testAccProviders, 159 | CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, 160 | Steps: []resource.TestStep{ 161 | { 162 | Config: testAccCheckUser(t, "database", "msi", map[string]interface{}{"database": "testdb", "username": "database_user", "password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), 163 | Check: resource.ComposeTestCheckFunc( 164 | testAccCheckUserExists("mssql_user.database"), 165 | testAccCheckDatabaseUserWorks("mssql_user.database", "database_user", "valueIsH8kd$¡"), 166 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.#", "0"), 167 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azuread_default_chain_auth.#", "0"), 168 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azuread_managed_identity_auth.#", "1"), 169 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.login.#", "0"), 170 | ), 171 | }, 172 | }, 173 | }) 174 | } 175 | 176 | func TestAccUser_Azure_External(t *testing.T) { 177 | tenantId := os.Getenv("MSSQL_TENANT_ID") 178 | clientId := os.Getenv("TF_ACC_AZURE_USER_CLIENT_ID") 179 | clientUser := os.Getenv("TF_ACC_AZURE_USER_CLIENT_USER") 180 | clientSecret := os.Getenv("TF_ACC_AZURE_USER_CLIENT_SECRET") 181 | resource.Test(t, resource.TestCase{ 182 | PreCheck: func() { testAccPreCheck(t) }, 183 | ProviderFactories: testAccProviders, 184 | CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, 185 | Steps: []resource.TestStep{ 186 | { 187 | Config: testAccCheckUser(t, "database", "azure", map[string]interface{}{"database": "testdb", "username": clientUser, "roles": "[\"db_owner\"]"}), 188 | Check: resource.ComposeTestCheckFunc( 189 | testAccCheckUserExists("mssql_user.database"), 190 | testAccCheckExternalUserWorks("mssql_user.database", tenantId, clientId, clientSecret), 191 | resource.TestCheckResourceAttr("mssql_user.database", "database", "testdb"), 192 | resource.TestCheckResourceAttr("mssql_user.database", "username", clientUser), 193 | resource.TestCheckResourceAttr("mssql_user.database", "login_name", ""), 194 | resource.TestCheckResourceAttr("mssql_user.database", "authentication_type", "EXTERNAL"), 195 | resource.TestCheckResourceAttr("mssql_user.database", "default_schema", "dbo"), 196 | resource.TestCheckResourceAttr("mssql_user.database", "default_language", ""), 197 | resource.TestCheckResourceAttr("mssql_user.database", "roles.#", "1"), 198 | resource.TestCheckResourceAttr("mssql_user.database", "roles.0", "db_owner"), 199 | resource.TestCheckResourceAttr("mssql_user.database", "server.#", "1"), 200 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.host", os.Getenv("TF_ACC_SQL_SERVER")), 201 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.port", "1433"), 202 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.#", "1"), 203 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.0.tenant_id", tenantId), 204 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.0.client_id", os.Getenv("MSSQL_CLIENT_ID")), 205 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.0.client_secret", os.Getenv("MSSQL_CLIENT_SECRET")), 206 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.login.#", "0"), 207 | resource.TestCheckResourceAttrSet("mssql_user.database", "principal_id"), 208 | resource.TestCheckNoResourceAttr("mssql_user.database", "password"), 209 | ), 210 | }, 211 | }, 212 | }) 213 | } 214 | 215 | func TestAccUser_AzureadChain_External(t *testing.T) { 216 | tenantId := os.Getenv("MSSQL_TENANT_ID") 217 | clientId := os.Getenv("TF_ACC_AZURE_USER_CLIENT_ID") 218 | clientUser := os.Getenv("TF_ACC_AZURE_USER_CLIENT_USER") 219 | clientSecret := os.Getenv("TF_ACC_AZURE_USER_CLIENT_SECRET") 220 | resource.Test(t, resource.TestCase{ 221 | PreCheck: func() { testAccPreCheck(t) }, 222 | ProviderFactories: testAccProviders, 223 | CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, 224 | Steps: []resource.TestStep{ 225 | { 226 | Config: testAccCheckUser(t, "database", "fedauth", map[string]interface{}{"database": "testdb", "username": clientUser, "roles": "[\"db_owner\"]"}), 227 | Check: resource.ComposeTestCheckFunc( 228 | testAccCheckUserExists("mssql_user.database"), 229 | testAccCheckExternalUserWorks("mssql_user.database", tenantId, clientId, clientSecret), 230 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.#", "0"), 231 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azuread_default_chain_auth.#", "1"), 232 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azuread_managed_identity_auth.#", "0"), 233 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.login.#", "0"), 234 | ), 235 | }, 236 | }, 237 | }) 238 | } 239 | 240 | func TestAccUser_AzureadMSI_External(t *testing.T) { 241 | tenantId := os.Getenv("MSSQL_TENANT_ID") 242 | clientId := os.Getenv("TF_ACC_AZURE_USER_CLIENT_ID") 243 | clientUser := os.Getenv("TF_ACC_AZURE_USER_CLIENT_USER") 244 | clientSecret := os.Getenv("TF_ACC_AZURE_USER_CLIENT_SECRET") 245 | resource.Test(t, resource.TestCase{ 246 | PreCheck: func() { testAccPreCheck(t) }, 247 | ProviderFactories: testAccProviders, 248 | CheckDestroy: func(state *terraform.State) error { return testAccCheckUserDestroy(state) }, 249 | Steps: []resource.TestStep{ 250 | { 251 | Config: testAccCheckUser(t, "database", "msi", map[string]interface{}{"database": "testdb", "username": clientUser, "roles": "[\"db_owner\"]"}), 252 | Check: resource.ComposeTestCheckFunc( 253 | testAccCheckUserExists("mssql_user.database"), 254 | testAccCheckExternalUserWorks("mssql_user.database", tenantId, clientId, clientSecret), 255 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azure_login.#", "0"), 256 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azuread_default_chain_auth.#", "0"), 257 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.azuread_managed_identity_auth.#", "1"), 258 | resource.TestCheckResourceAttr("mssql_user.database", "server.0.login.#", "0"), 259 | ), 260 | }, 261 | }, 262 | }) 263 | } 264 | 265 | func TestAccUser_Local_Update_DefaultSchema(t *testing.T) { 266 | resource.Test(t, resource.TestCase{ 267 | PreCheck: func() { testAccPreCheck(t) }, 268 | IsUnitTest: runLocalAccTests, 269 | ProviderFactories: testAccProviders, 270 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 271 | Steps: []resource.TestStep{ 272 | { 273 | Config: testAccCheckUser(t, "update", "login", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡"}), 274 | Check: resource.ComposeTestCheckFunc( 275 | resource.TestCheckResourceAttr("mssql_user.update", "default_schema", "dbo"), 276 | testAccCheckUserExists("mssql_user.update", Check{"default_schema", "==", "dbo"}), 277 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 278 | ), 279 | }, 280 | { 281 | Config: testAccCheckUser(t, "update", "login", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡", "default_schema": "sys"}), 282 | Check: resource.ComposeTestCheckFunc( 283 | resource.TestCheckResourceAttr("mssql_user.update", "default_schema", "sys"), 284 | testAccCheckUserExists("mssql_user.update", Check{"default_schema", "==", "sys"}), 285 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 286 | ), 287 | }, 288 | }, 289 | }) 290 | } 291 | 292 | func TestAccUser_Local_Update_DefaultLanguage(t *testing.T) { 293 | resource.Test(t, resource.TestCase{ 294 | PreCheck: func() { testAccPreCheck(t) }, 295 | IsUnitTest: runLocalAccTests, 296 | ProviderFactories: testAccProviders, 297 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 298 | Steps: []resource.TestStep{ 299 | { 300 | Config: testAccCheckUser(t, "update", "login", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡"}), 301 | Check: resource.ComposeTestCheckFunc( 302 | resource.TestCheckResourceAttr("mssql_user.update", "default_language", ""), 303 | testAccCheckUserExists("mssql_user.update", Check{"default_language", "==", ""}), 304 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 305 | ), 306 | }, 307 | { 308 | Config: testAccCheckUser(t, "update", "login", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡", "default_language": "russian"}), 309 | Check: resource.ComposeTestCheckFunc( 310 | resource.TestCheckResourceAttr("mssql_user.update", "default_language", ""), 311 | testAccCheckUserExists("mssql_user.update", Check{"default_language", "==", ""}), 312 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 313 | ), 314 | }, 315 | }, 316 | }) 317 | } 318 | 319 | func TestAccUser_Local_Update_Roles(t *testing.T) { 320 | resource.Test(t, resource.TestCase{ 321 | PreCheck: func() { testAccPreCheck(t) }, 322 | IsUnitTest: runLocalAccTests, 323 | ProviderFactories: testAccProviders, 324 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 325 | Steps: []resource.TestStep{ 326 | { 327 | Config: testAccCheckUser(t, "update", "login", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡"}), 328 | Check: resource.ComposeTestCheckFunc( 329 | resource.TestCheckResourceAttr("mssql_user.update", "roles.#", "0"), 330 | testAccCheckUserExists("mssql_user.update", Check{"roles", "==", []string{}}), 331 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 332 | ), 333 | }, 334 | { 335 | Config: testAccCheckUser(t, "update", "login", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\",\"db_datawriter\"]"}), 336 | Check: resource.ComposeTestCheckFunc( 337 | resource.TestCheckResourceAttr("mssql_user.update", "roles.#", "2"), 338 | resource.TestCheckResourceAttr("mssql_user.update", "roles.0", "db_datawriter"), 339 | resource.TestCheckResourceAttr("mssql_user.update", "roles.1", "db_owner"), 340 | testAccCheckUserExists("mssql_user.update", Check{"roles", "==", []string{"db_owner", "db_datawriter"}}), 341 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 342 | ), 343 | }, 344 | { 345 | Config: testAccCheckUser(t, "update", "login", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡", "roles": "[\"db_datawriter\",\"db_owner\"]"}), 346 | Check: resource.ComposeTestCheckFunc( 347 | resource.TestCheckResourceAttr("mssql_user.update", "roles.#", "2"), 348 | resource.TestCheckResourceAttr("mssql_user.update", "roles.0", "db_datawriter"), 349 | resource.TestCheckResourceAttr("mssql_user.update", "roles.1", "db_owner"), 350 | testAccCheckUserExists("mssql_user.update", Check{"roles", "==", []string{"db_owner", "db_datawriter"}}), 351 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 352 | ), 353 | }, 354 | { 355 | Config: testAccCheckUser(t, "update", "login", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), 356 | Check: resource.ComposeTestCheckFunc( 357 | resource.TestCheckResourceAttr("mssql_user.update", "roles.#", "1"), 358 | resource.TestCheckResourceAttr("mssql_user.update", "roles.0", "db_owner"), 359 | testAccCheckUserExists("mssql_user.update", Check{"roles", "==", []string{"db_owner"}}), 360 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 361 | ), 362 | }, 363 | }, 364 | }) 365 | } 366 | 367 | func TestAccUser_Azure_Update_DefaultSchema(t *testing.T) { 368 | resource.Test(t, resource.TestCase{ 369 | PreCheck: func() { testAccPreCheck(t) }, 370 | ProviderFactories: testAccProviders, 371 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 372 | Steps: []resource.TestStep{ 373 | { 374 | Config: testAccCheckUser(t, "update", "azure", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡"}), 375 | Check: resource.ComposeTestCheckFunc( 376 | resource.TestCheckResourceAttr("mssql_user.update", "default_schema", "dbo"), 377 | testAccCheckUserExists("mssql_user.update", Check{"default_schema", "==", "dbo"}), 378 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 379 | ), 380 | }, 381 | { 382 | Config: testAccCheckUser(t, "update", "azure", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡", "default_schema": "sys"}), 383 | Check: resource.ComposeTestCheckFunc( 384 | resource.TestCheckResourceAttr("mssql_user.update", "default_schema", "sys"), 385 | testAccCheckUserExists("mssql_user.update", Check{"default_schema", "==", "sys"}), 386 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 387 | ), 388 | }, 389 | }, 390 | }) 391 | } 392 | 393 | func TestAccUser_Azure_Update_DefaultLanguage(t *testing.T) { 394 | resource.Test(t, resource.TestCase{ 395 | PreCheck: func() { testAccPreCheck(t) }, 396 | ProviderFactories: testAccProviders, 397 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 398 | Steps: []resource.TestStep{ 399 | { 400 | Config: testAccCheckUser(t, "update", "azure", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡"}), 401 | Check: resource.ComposeTestCheckFunc( 402 | resource.TestCheckResourceAttr("mssql_user.update", "default_language", ""), 403 | testAccCheckUserExists("mssql_user.update", Check{"default_language", "==", ""}), 404 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 405 | ), 406 | }, 407 | { 408 | Config: testAccCheckUser(t, "update", "azure", map[string]interface{}{"username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡", "default_language": "russian"}), 409 | Check: resource.ComposeTestCheckFunc( 410 | resource.TestCheckResourceAttr("mssql_user.update", "default_language", ""), 411 | testAccCheckUserExists("mssql_user.update", Check{"default_language", "==", ""}), 412 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 413 | ), 414 | }, 415 | }, 416 | }) 417 | } 418 | 419 | func TestAccUser_Azure_Update_Roles(t *testing.T) { 420 | resource.Test(t, resource.TestCase{ 421 | PreCheck: func() { testAccPreCheck(t) }, 422 | ProviderFactories: testAccProviders, 423 | CheckDestroy: func(state *terraform.State) error { return testAccCheckLoginDestroy(state) }, 424 | Steps: []resource.TestStep{ 425 | { 426 | Config: testAccCheckUser(t, "update", "azure", map[string]interface{}{"database": "testdb", "username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡"}), 427 | Check: resource.ComposeTestCheckFunc( 428 | resource.TestCheckResourceAttr("mssql_user.update", "roles.#", "0"), 429 | testAccCheckUserExists("mssql_user.update", Check{"roles", "==", []string{}}), 430 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 431 | ), 432 | }, 433 | { 434 | Config: testAccCheckUser(t, "update", "azure", map[string]interface{}{"database": "testdb", "username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\",\"db_datawriter\"]"}), 435 | Check: resource.ComposeTestCheckFunc( 436 | resource.TestCheckResourceAttr("mssql_user.update", "roles.#", "2"), 437 | resource.TestCheckResourceAttr("mssql_user.update", "roles.0", "db_datawriter"), 438 | resource.TestCheckResourceAttr("mssql_user.update", "roles.1", "db_owner"), 439 | testAccCheckUserExists("mssql_user.update", Check{"roles", "==", []string{"db_owner", "db_datawriter"}}), 440 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 441 | ), 442 | }, 443 | { 444 | Config: testAccCheckUser(t, "update", "azure", map[string]interface{}{"database": "testdb", "username": "test_update", "login_name": "user_update", "login_password": "valueIsH8kd$¡", "roles": "[\"db_owner\"]"}), 445 | Check: resource.ComposeTestCheckFunc( 446 | resource.TestCheckResourceAttr("mssql_user.update", "roles.#", "1"), 447 | resource.TestCheckResourceAttr("mssql_user.update", "roles.0", "db_owner"), 448 | testAccCheckUserExists("mssql_user.update", Check{"roles", "==", []string{"db_owner"}}), 449 | testAccCheckDatabaseUserWorks("mssql_user.update", "user_update", "valueIsH8kd$¡"), 450 | ), 451 | }, 452 | }, 453 | }) 454 | } 455 | 456 | func testAccCheckUser(t *testing.T, name string, login string, data map[string]interface{}) string { 457 | text := `{{ if .login_name }} 458 | resource "mssql_login" "{{ .name }}" { 459 | server { 460 | host = "{{ .host }}" 461 | {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} 462 | } 463 | login_name = "{{ .login_name }}" 464 | password = "{{ .login_password }}" 465 | } 466 | {{ end }} 467 | resource "mssql_user" "{{ .name }}" { 468 | server { 469 | host = "{{ .host }}" 470 | {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} 471 | } 472 | {{ with .database }}database = "{{ . }}"{{ end }} 473 | username = "{{ .username }}" 474 | {{ with .password }}password = "{{ . }}"{{ end }} 475 | {{ with .login_name }}login_name = "{{ . }}"{{ end }} 476 | {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} 477 | {{ with .default_language }}default_language = "{{ . }}"{{ end }} 478 | {{ with .roles }}roles = {{ . }}{{ end }} 479 | }` 480 | data["name"] = name 481 | data["login"] = login 482 | if login == "fedauth" || login == "msi" || login == "azure" { 483 | data["host"] = os.Getenv("TF_ACC_SQL_SERVER") 484 | } else if login == "login" { 485 | data["host"] = "localhost" 486 | } else { 487 | t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) 488 | } 489 | res, err := templateToString(name, text, data) 490 | if err != nil { 491 | t.Fatalf("%s", err) 492 | } 493 | return res 494 | } 495 | 496 | func testAccCheckMultipleUsers(t *testing.T, name string, login string, data map[string]interface{}, count int) string { 497 | text := `{{ if .login_name }} 498 | resource "mssql_login" "{{ .name }}" { 499 | count = {{ .count }} 500 | server { 501 | host = "{{ .host }}" 502 | {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} 503 | } 504 | login_name = "{{ .login_name }}-${count.index}" 505 | password = "{{ .login_password }}" 506 | } 507 | {{ end }} 508 | resource "mssql_user" "{{ .name }}" { 509 | count = {{ .count }} 510 | server { 511 | host = "{{ .host }}" 512 | {{if eq .login "fedauth"}}azuread_default_chain_auth {}{{ else if eq .login "msi"}}azuread_managed_identity_auth {}{{ else if eq .login "azure" }}azure_login {}{{ else }}login {}{{ end }} 513 | } 514 | {{ with .database }}database = "{{ . }}"{{ end }} 515 | username = "{{ .username }}-${count.index}" 516 | {{ with .password }}password = "{{ . }}"{{ end }} 517 | {{ with .login_name }}login_name = "{{ . }}-${count.index}"{{ end }} 518 | {{ with .default_schema }}default_schema = "{{ . }}"{{ end }} 519 | {{ with .default_language }}default_language = "{{ . }}"{{ end }} 520 | {{ with .roles }}roles = {{ . }}{{ end }} 521 | }` 522 | data["name"] = name 523 | data["login"] = login 524 | data["count"] = count 525 | if login == "fedauth" || login == "msi" || login == "azure" { 526 | data["host"] = os.Getenv("TF_ACC_SQL_SERVER") 527 | } else if login == "login" { 528 | data["host"] = "localhost" 529 | } else { 530 | t.Fatalf("login expected to be one of 'login', 'azure', 'msi', 'fedauth', got %s", login) 531 | } 532 | res, err := templateToString(name, text, data) 533 | if err != nil { 534 | t.Fatalf("%s", err) 535 | } 536 | return res 537 | } 538 | 539 | func testAccCheckUserDestroy(state *terraform.State) error { 540 | for _, rs := range state.RootModule().Resources { 541 | if rs.Type != "mssql_user" { 542 | continue 543 | } 544 | 545 | connector, err := getTestConnector(rs.Primary.Attributes) 546 | if err != nil { 547 | return err 548 | } 549 | 550 | database := rs.Primary.Attributes["database"] 551 | username := rs.Primary.Attributes["username"] 552 | login, err := connector.GetUser(database, username) 553 | if login != nil { 554 | return fmt.Errorf("user still exists") 555 | } 556 | if err != nil { 557 | return fmt.Errorf("expected no error, got %s", err) 558 | } 559 | } 560 | return nil 561 | } 562 | 563 | func testAccCheckUserExists(resource string, checks ...Check) resource.TestCheckFunc { 564 | return func(state *terraform.State) error { 565 | rs, ok := state.RootModule().Resources[resource] 566 | if !ok { 567 | return fmt.Errorf("not found: %s", resource) 568 | } 569 | if rs.Type != "mssql_user" { 570 | return fmt.Errorf("expected resource of type %s, got %s", "mssql_user", rs.Type) 571 | } 572 | if rs.Primary.ID == "" { 573 | return fmt.Errorf("no record ID is set") 574 | } 575 | connector, err := getTestConnector(rs.Primary.Attributes) 576 | if err != nil { 577 | return err 578 | } 579 | 580 | database := rs.Primary.Attributes["database"] 581 | username := rs.Primary.Attributes["username"] 582 | user, err := connector.GetUser(database, username) 583 | if user == nil { 584 | return fmt.Errorf("user does not exist") 585 | } 586 | if err != nil { 587 | return fmt.Errorf("expected no error, got %s", err) 588 | } 589 | 590 | var actual interface{} 591 | for _, check := range checks { 592 | switch check.name { 593 | case "password": 594 | actual = user.Password 595 | case "login_name": 596 | actual = user.LoginName 597 | case "default_schema": 598 | actual = user.DefaultSchema 599 | case "default_language": 600 | actual = user.DefaultLanguage 601 | case "roles": 602 | actual = user.Roles 603 | case "authentication_type": 604 | actual = user.AuthType 605 | default: 606 | return fmt.Errorf("unknown property %s", check.name) 607 | } 608 | if (check.op == "" || check.op == "==") && !equal(check.expected, actual) { 609 | return fmt.Errorf("expected %s == %s, got %s", check.name, check.expected, actual) 610 | } 611 | if check.op == "!=" && equal(check.expected, actual) { 612 | return fmt.Errorf("expected %s != %s, got %s", check.name, check.expected, actual) 613 | } 614 | } 615 | return nil 616 | } 617 | } 618 | 619 | func equal(a, b interface{}) bool { 620 | switch a.(type) { 621 | case []string: 622 | aa := a.([]string) 623 | bb := b.([]string) 624 | if len(aa) != len(bb) { 625 | return false 626 | } 627 | for i, v := range aa { 628 | if v != bb[i] { 629 | return false 630 | } 631 | } 632 | return true 633 | default: 634 | return a == b 635 | } 636 | } 637 | 638 | func testAccCheckDatabaseUserWorks(resource string, username, password string) resource.TestCheckFunc { 639 | return func(state *terraform.State) error { 640 | rs, ok := state.RootModule().Resources[resource] 641 | if !ok { 642 | return fmt.Errorf("not found: %s", resource) 643 | } 644 | if rs.Type != "mssql_user" { 645 | return fmt.Errorf("expected resource of type %s, got %s", "mssql_user", rs.Type) 646 | } 647 | if rs.Primary.ID == "" { 648 | return fmt.Errorf("no record ID is set") 649 | } 650 | connector, err := getTestUserConnector(rs.Primary.Attributes, username, password) 651 | if err != nil { 652 | return err 653 | } 654 | current, system, err := connector.GetCurrentUser(rs.Primary.Attributes[databaseProp]) 655 | if err != nil { 656 | return fmt.Errorf("error: %s", err) 657 | } 658 | if current != rs.Primary.Attributes[usernameProp] { 659 | return fmt.Errorf("expected to be user %s, got %s (%s)", rs.Primary.Attributes[usernameProp], current, system) 660 | } 661 | return nil 662 | } 663 | } 664 | 665 | func testAccCheckExternalUserWorks(resource string, tenantId, clientId, clientSecret string) resource.TestCheckFunc { 666 | return func(state *terraform.State) error { 667 | rs, ok := state.RootModule().Resources[resource] 668 | if !ok { 669 | return fmt.Errorf("not found: %s", resource) 670 | } 671 | if rs.Type != "mssql_user" { 672 | return fmt.Errorf("expected resource of type %s, got %s", "mssql_user", rs.Type) 673 | } 674 | if rs.Primary.ID == "" { 675 | return fmt.Errorf("no record ID is set") 676 | } 677 | connector, err := getTestExternalConnector(rs.Primary.Attributes, tenantId, clientId, clientSecret) 678 | if err != nil { 679 | return err 680 | } 681 | current, system, err := connector.GetCurrentUser(rs.Primary.Attributes[databaseProp]) 682 | if err != nil { 683 | return fmt.Errorf("error: %s", err) 684 | } 685 | if current != rs.Primary.Attributes[usernameProp] { 686 | return fmt.Errorf("expected to be user %s, got %s (%s)", rs.Primary.Attributes[usernameProp], current, system) 687 | } 688 | return nil 689 | } 690 | } 691 | 692 | func getMultipleUsersExistAccCheck(count int) []resource.TestCheckFunc { 693 | checkFuncs := []resource.TestCheckFunc{} 694 | for i := 0; i < count; i++ { 695 | checkFuncs = append(checkFuncs, []resource.TestCheckFunc{ 696 | testAccCheckUserExists(fmt.Sprintf("mssql_user.instance.%v", i)), 697 | testAccCheckDatabaseUserWorks(fmt.Sprintf("mssql_user.instance.%v", i), fmt.Sprintf("user_instance-%v", i), "valueIsH8kd$¡"), 698 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "database", "master"), 699 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "username", fmt.Sprintf("instance-%v", i)), 700 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "login_name", fmt.Sprintf("user_instance-%v", i)), 701 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "authentication_type", "INSTANCE"), 702 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "default_schema", "dbo"), 703 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "default_language", ""), 704 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "roles.#", "1"), 705 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "roles.0", "db_owner"), 706 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "server.#", "1"), 707 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "server.0.host", "localhost"), 708 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "server.0.port", "1433"), 709 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "server.0.login.#", "1"), 710 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "server.0.login.0.username", os.Getenv("MSSQL_USERNAME")), 711 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "server.0.login.0.password", os.Getenv("MSSQL_PASSWORD")), 712 | resource.TestCheckResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "server.0.azure_login.#", "0"), 713 | resource.TestCheckResourceAttrSet(fmt.Sprintf("mssql_user.instance.%v", i), "principal_id"), 714 | resource.TestCheckNoResourceAttr(fmt.Sprintf("mssql_user.instance.%v", i), "password"), 715 | }..., 716 | ) 717 | } 718 | return checkFuncs 719 | } 720 | -------------------------------------------------------------------------------- /mssql/server.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "errors" 5 | "net" 6 | "net/url" 7 | "os" 8 | "strings" 9 | 10 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 11 | ) 12 | 13 | const DefaultPort = "1433" 14 | 15 | func getServerSchema(prefix string) map[string]*schema.Schema { 16 | if len(prefix) > 0 { 17 | prefix = prefix + ".0." 18 | } 19 | var LoginMethods = []string{ 20 | prefix + "login", 21 | prefix + "azure_login", 22 | prefix + "azuread_default_chain_auth", 23 | prefix + "azuread_managed_identity_auth", 24 | } 25 | return map[string]*schema.Schema{ 26 | "host": { 27 | Type: schema.TypeString, 28 | Required: true, 29 | ForceNew: true, 30 | DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool { 31 | return strings.EqualFold(old, new) 32 | }, 33 | }, 34 | "port": { 35 | Type: schema.TypeString, 36 | Optional: true, 37 | ForceNew: true, 38 | Default: DefaultPort, 39 | }, 40 | "login": { 41 | Type: schema.TypeList, 42 | MaxItems: 1, 43 | Optional: true, 44 | ExactlyOneOf: LoginMethods, 45 | Elem: &schema.Resource{ 46 | Schema: map[string]*schema.Schema{ 47 | "username": { 48 | Type: schema.TypeString, 49 | Required: true, 50 | DefaultFunc: schema.EnvDefaultFunc("MSSQL_USERNAME", nil), 51 | }, 52 | "password": { 53 | Type: schema.TypeString, 54 | Required: true, 55 | Sensitive: true, 56 | DefaultFunc: schema.EnvDefaultFunc("MSSQL_PASSWORD", nil), 57 | }, 58 | }, 59 | }, 60 | }, 61 | "azure_login": { 62 | Type: schema.TypeList, 63 | MaxItems: 1, 64 | Optional: true, 65 | ExactlyOneOf: LoginMethods, 66 | Elem: &schema.Resource{ 67 | Schema: map[string]*schema.Schema{ 68 | "tenant_id": { 69 | Type: schema.TypeString, 70 | Required: true, 71 | DefaultFunc: schema.EnvDefaultFunc("MSSQL_TENANT_ID", nil), 72 | }, 73 | "client_id": { 74 | Type: schema.TypeString, 75 | Required: true, 76 | DefaultFunc: schema.EnvDefaultFunc("MSSQL_CLIENT_ID", nil), 77 | }, 78 | "client_secret": { 79 | Type: schema.TypeString, 80 | Required: true, 81 | Sensitive: true, 82 | DefaultFunc: schema.EnvDefaultFunc("MSSQL_CLIENT_SECRET", nil), 83 | }, 84 | }, 85 | }, 86 | }, 87 | "azuread_default_chain_auth": { 88 | Type: schema.TypeList, 89 | MaxItems: 1, 90 | Optional: true, 91 | ExactlyOneOf: LoginMethods, 92 | Elem: &schema.Resource{}, 93 | }, 94 | "azuread_managed_identity_auth": { 95 | Type: schema.TypeList, 96 | MaxItems: 1, 97 | Optional: true, 98 | ExactlyOneOf: LoginMethods, 99 | Elem: &schema.Resource{ 100 | Schema: map[string]*schema.Schema{ 101 | "user_id": { 102 | Type: schema.TypeString, 103 | Optional: true, 104 | }, 105 | }, 106 | }, 107 | }, 108 | } 109 | } 110 | 111 | func serverFromId(id string) ([]map[string]interface{}, *url.URL, error) { 112 | u, err := url.Parse(id) 113 | if err != nil { 114 | return nil, nil, err 115 | } 116 | 117 | if u.Scheme != "sqlserver" && u.Scheme != "mssql" { 118 | return nil, nil, errors.New("invalid schema in ID") 119 | } 120 | 121 | host := u.Host 122 | port := DefaultPort 123 | 124 | if strings.ContainsRune(host, ':') { 125 | var err error 126 | if host, port, err = net.SplitHostPort(u.Host); err != nil { 127 | return nil, nil, err 128 | } 129 | } 130 | 131 | values := u.Query() 132 | 133 | login, loginInValues := getLogin(values) 134 | azureLogin, azureInValues := getAzureLogin(values) 135 | if login == nil && azureLogin == nil { 136 | return nil, nil, errors.New("neither login nor azure login specified") 137 | } 138 | if loginInValues && azureInValues { 139 | return nil, nil, errors.New("both login and azure login specified in resource") 140 | } 141 | if login != nil && azureLogin != nil { 142 | // prefer azure login 143 | azure := true 144 | if v, ok := values["azure"]; ok { 145 | azure = len(v) == 0 || strings.ToLower(v[0]) == "true" 146 | } 147 | if azure { 148 | login = nil 149 | } else { 150 | azureLogin = nil 151 | } 152 | } 153 | 154 | return []map[string]interface{}{{ 155 | "host": host, 156 | "port": port, 157 | "login": login, 158 | "azure_login": azureLogin, 159 | }}, u, nil 160 | } 161 | 162 | func getLogin(values url.Values) ([]map[string]interface{}, bool) { 163 | var inValues bool 164 | 165 | username := values.Get("username") 166 | if username == "" { 167 | username = os.Getenv("MSSQL_USERNAME") 168 | } else { 169 | inValues = true 170 | } 171 | 172 | password := values.Get("password") 173 | if password == "" { 174 | password = os.Getenv("MSSQL_PASSWORD") 175 | } else { 176 | inValues = true 177 | } 178 | 179 | if username == "" || password == "" { 180 | return nil, false 181 | } 182 | 183 | return []map[string]interface{}{{ 184 | "username": username, 185 | "password": password, 186 | }}, inValues 187 | } 188 | 189 | func getAzureLogin(values url.Values) ([]map[string]interface{}, bool) { 190 | var inValues bool 191 | 192 | tenantId := values.Get("tenant_id") 193 | if tenantId == "" { 194 | tenantId = os.Getenv("MSSQL_TENANT_ID") 195 | } else { 196 | inValues = true 197 | } 198 | 199 | clientId := values.Get("client_id") 200 | if clientId == "" { 201 | clientId = os.Getenv("MSSQL_CLIENT_ID") 202 | } else { 203 | inValues = true 204 | } 205 | 206 | clientSecret := values.Get("client_secret") 207 | if clientSecret == "" { 208 | clientSecret = os.Getenv("MSSQL_CLIENT_SECRET") 209 | } else { 210 | inValues = true 211 | } 212 | 213 | if tenantId == "" || clientId == "" || clientSecret == "" { 214 | return nil, false 215 | } 216 | 217 | return []map[string]interface{}{{ 218 | "tenant_id": tenantId, 219 | "client_id": clientId, 220 | "client_secret": clientSecret, 221 | }}, inValues 222 | } 223 | -------------------------------------------------------------------------------- /mssql/utils.go: -------------------------------------------------------------------------------- 1 | package mssql 2 | 3 | import ( 4 | "fmt" 5 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 6 | "github.com/rs/zerolog" 7 | "github.com/betr-io/terraform-provider-mssql/mssql/model" 8 | ) 9 | 10 | func getLoginID(data *schema.ResourceData) string { 11 | host := data.Get(serverProp + ".0.host").(string) 12 | port := data.Get(serverProp + ".0.port").(string) 13 | loginName := data.Get(loginNameProp).(string) 14 | return fmt.Sprintf("sqlserver://%s:%s/%s", host, port, loginName) 15 | } 16 | 17 | func getUserID(data *schema.ResourceData) string { 18 | host := data.Get(serverProp + ".0.host").(string) 19 | port := data.Get(serverProp + ".0.port").(string) 20 | database := data.Get(databaseProp).(string) 21 | username := data.Get(usernameProp).(string) 22 | return fmt.Sprintf("sqlserver://%s:%s/%s/%s", host, port, database, username) 23 | } 24 | 25 | func loggerFromMeta(meta interface{}, resource, function string) zerolog.Logger { 26 | return meta.(model.Provider).ResourceLogger(resource, function) 27 | } 28 | -------------------------------------------------------------------------------- /scripts/aliases.sh: -------------------------------------------------------------------------------- 1 | alias gmo='go list -u -m -json all | go-mod-outdated' 2 | alias gmod='go list -u -m -json all | go-mod-outdated -direct' 3 | alias gmou='go list -u -m -json all | go-mod-outdated -update' 4 | alias gmodu='go list -u -m -json all | go-mod-outdated -direct -update' 5 | -------------------------------------------------------------------------------- /scripts/versions.sh: -------------------------------------------------------------------------------- 1 | TERRAFORM_DIR=${TERRAFORM_DIR:-$HOME/bin} 2 | TERRAFORM_VERSION=${TERRAFORM_VERSION:-1.0.5} 3 | -------------------------------------------------------------------------------- /sql/login.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "github.com/betr-io/terraform-provider-mssql/mssql/model" 7 | ) 8 | 9 | func (c *Connector) GetLogin(ctx context.Context, name string) (*model.Login, error) { 10 | var login model.Login 11 | err := c.QueryRowContext(ctx, 12 | "SELECT principal_id, name, CONVERT(VARCHAR(1000), [sid], 1), default_database_name, default_language_name FROM [master].[sys].[sql_logins] WHERE [name] = @name", 13 | func(r *sql.Row) error { 14 | return r.Scan(&login.PrincipalID, &login.LoginName, &login.SIDStr, &login.DefaultDatabase, &login.DefaultLanguage) 15 | }, 16 | sql.Named("name", name), 17 | ) 18 | if err != nil { 19 | if err == sql.ErrNoRows { 20 | return nil, nil 21 | } 22 | return nil, err 23 | } 24 | return &login, nil 25 | } 26 | 27 | func (c *Connector) CreateLogin(ctx context.Context, name, password, sid, defaultDatabase, defaultLanguage string) error { 28 | cmd := `DECLARE @sql nvarchar(max) 29 | SET @sql = 'CREATE LOGIN ' + QuoteName(@name) + ' ' + 30 | 'WITH PASSWORD = ' + QuoteName(@password, '''') 31 | IF NOT @sid = '' 32 | BEGIN 33 | SET @sql = @sql + ', SID = ' + CONVERT(VARCHAR(1000), @sid, 1) 34 | END 35 | IF @@VERSION NOT LIKE 'Microsoft SQL Azure%' 36 | BEGIN 37 | IF @defaultDatabase = '' SET @defaultDatabase = 'master' 38 | IF NOT @defaultDatabase = 'master' 39 | BEGIN 40 | SET @sql = @sql + ', DEFAULT_DATABASE = ' + QuoteName(@defaultDatabase) 41 | END 42 | DECLARE @serverLanguage nvarchar(max) = (SELECT lang.name FROM [sys].[configurations] c INNER JOIN [sys].[syslanguages] lang ON c.[value] = lang.langid WHERE c.name = 'default language') 43 | IF NOT @defaultLanguage IN ('', @serverLanguage) 44 | BEGIN 45 | SET @sql = @sql + ', DEFAULT_LANGUAGE = ' + QuoteName(@defaultLanguage) 46 | END 47 | END 48 | EXEC (@sql)` 49 | database := "master" 50 | return c. 51 | setDatabase(&database). 52 | ExecContext(ctx, cmd, 53 | sql.Named("name", name), 54 | sql.Named("password", password), 55 | sql.Named("sid", sid), 56 | sql.Named("defaultDatabase", defaultDatabase), 57 | sql.Named("defaultLanguage", defaultLanguage)) 58 | } 59 | 60 | func (c *Connector) UpdateLogin(ctx context.Context, name, password, defaultDatabase, defaultLanguage string) error { 61 | cmd := `DECLARE @sql nvarchar(max) 62 | SET @sql = 'ALTER LOGIN ' + QuoteName(@name) + ' ' + 63 | 'WITH PASSWORD = ' + QuoteName(@password, '''') 64 | IF @@VERSION NOT LIKE 'Microsoft SQL Azure%' 65 | BEGIN 66 | IF @defaultDatabase = '' SET @defaultDatabase = 'master' 67 | IF NOT @defaultDatabase IN (SELECT default_database_name FROM [master].[sys].[sql_logins] WHERE [name] = @name) 68 | BEGIN 69 | SET @sql = @sql + ', DEFAULT_DATABASE = ' + QuoteName(@defaultDatabase) 70 | END 71 | DECLARE @language nvarchar(max) = @defaultLanguage 72 | IF @language = '' SET @language = (SELECT lang.name FROM [sys].[configurations] c INNER JOIN [sys].[syslanguages] lang ON c.[value] = lang.langid WHERE c.name = 'default language') 73 | IF @language != (SELECT default_language_name FROM [master].[sys].[sql_logins] WHERE [name] = @name) 74 | BEGIN 75 | SET @sql = @sql + ', DEFAULT_LANGUAGE = ' + QuoteName(@language) 76 | END 77 | END 78 | EXEC (@sql)` 79 | return c.ExecContext(ctx, cmd, 80 | sql.Named("name", name), 81 | sql.Named("password", password), 82 | sql.Named("defaultDatabase", defaultDatabase), 83 | sql.Named("defaultLanguage", defaultLanguage)) 84 | } 85 | 86 | func (c *Connector) DeleteLogin(ctx context.Context, name string) error { 87 | if err := c.killSessionsForLogin(ctx, name); err != nil { 88 | return err 89 | } 90 | cmd := `DECLARE @sql nvarchar(max) 91 | SET @sql = 'IF EXISTS (SELECT 1 FROM [master].[sys].[sql_logins] WHERE [name] = ' + QuoteName(@name, '''') + ') ' + 92 | 'DROP LOGIN ' + QuoteName(@name) 93 | EXEC (@sql)` 94 | return c.ExecContext(ctx, cmd, sql.Named("name", name)) 95 | } 96 | 97 | func (c *Connector) killSessionsForLogin(ctx context.Context, name string) error { 98 | cmd := `-- adapted from https://stackoverflow.com/a/5178097/38055 99 | DECLARE sessionsToKill CURSOR FAST_FORWARD FOR 100 | SELECT session_id 101 | FROM sys.dm_exec_sessions 102 | WHERE login_name = @name 103 | OPEN sessionsToKill 104 | DECLARE @sessionId INT 105 | DECLARE @statement NVARCHAR(200) 106 | FETCH NEXT FROM sessionsToKill INTO @sessionId 107 | WHILE @@FETCH_STATUS = 0 108 | BEGIN 109 | PRINT 'Killing session ' + CAST(@sessionId AS NVARCHAR(20)) + ' for login ' + @name 110 | SET @statement = 'KILL ' + CAST(@sessionId AS NVARCHAR(20)) 111 | EXEC sp_executesql @statement 112 | FETCH NEXT FROM sessionsToKill INTO @sessionId 113 | END 114 | CLOSE sessionsToKill 115 | DEALLOCATE sessionsToKill` 116 | return c.ExecContext(ctx, cmd, sql.Named("name", name)) 117 | } 118 | -------------------------------------------------------------------------------- /sql/sql.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "database/sql/driver" 7 | "fmt" 8 | "log" 9 | "net/url" 10 | "strings" 11 | "time" 12 | 13 | "github.com/Azure/go-autorest/autorest/adal" 14 | "github.com/Azure/go-autorest/autorest/azure" 15 | "github.com/betr-io/terraform-provider-mssql/mssql/model" 16 | "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" 17 | mssql "github.com/microsoft/go-mssqldb" 18 | "github.com/microsoft/go-mssqldb/azuread" 19 | "github.com/pkg/errors" 20 | ) 21 | 22 | type factory struct{} 23 | 24 | func GetFactory() model.ConnectorFactory { 25 | return new(factory) 26 | } 27 | 28 | func (f factory) GetConnector(prefix string, data *schema.ResourceData) (interface{}, error) { 29 | if len(prefix) > 0 { 30 | prefix = prefix + ".0." 31 | } 32 | 33 | connector := &Connector{ 34 | Host: data.Get(prefix + "host").(string), 35 | Port: data.Get(prefix + "port").(string), 36 | Timeout: data.Timeout(schema.TimeoutRead), 37 | } 38 | 39 | if admin, ok := data.GetOk(prefix + "login.0"); ok { 40 | admin := admin.(map[string]interface{}) 41 | connector.Login = &LoginUser{ 42 | Username: admin["username"].(string), 43 | Password: admin["password"].(string), 44 | } 45 | } 46 | 47 | if admin, ok := data.GetOk(prefix + "azure_login.0"); ok { 48 | admin := admin.(map[string]interface{}) 49 | connector.AzureLogin = &AzureLogin{ 50 | TenantID: admin["tenant_id"].(string), 51 | ClientID: admin["client_id"].(string), 52 | ClientSecret: admin["client_secret"].(string), 53 | } 54 | } 55 | 56 | if admin, ok := data.GetOk(prefix + "azuread_managed_identity_auth.0"); ok { 57 | admin := admin.(map[string]interface{}) 58 | connector.FedauthMSI = &FedauthMSI{ 59 | UserID: admin["user_id"].(string), 60 | } 61 | } 62 | 63 | return connector, nil 64 | } 65 | 66 | type Connector struct { 67 | Host string `json:"host"` 68 | Port string `json:"port"` 69 | Database string `json:"database"` 70 | Login *LoginUser 71 | AzureLogin *AzureLogin 72 | FedauthMSI *FedauthMSI 73 | Timeout time.Duration `json:"timeout,omitempty"` 74 | Token string 75 | } 76 | 77 | type LoginUser struct { 78 | Username string `json:"username,omitempty"` 79 | Password string `json:"password,omitempty"` 80 | } 81 | 82 | type AzureLogin struct { 83 | TenantID string `json:"tenant_id,omitempty"` 84 | ClientID string `json:"client_id,omitempty"` 85 | ClientSecret string `json:"client_secret,omitempty"` 86 | } 87 | 88 | type FedauthMSI struct { 89 | UserID string `json:"user_id,omitempty"` 90 | } 91 | 92 | func (c *Connector) PingContext(ctx context.Context) error { 93 | db, err := c.db() 94 | if err != nil { 95 | return err 96 | } 97 | 98 | err = db.PingContext(ctx) 99 | if err != nil { 100 | return errors.Wrap(err, "In ping") 101 | } 102 | 103 | return nil 104 | } 105 | 106 | // Execute an SQL statement and ignore the results 107 | func (c *Connector) ExecContext(ctx context.Context, command string, args ...interface{}) error { 108 | db, err := c.db() 109 | if err != nil { 110 | return err 111 | } 112 | defer db.Close() 113 | 114 | _, err = db.ExecContext(ctx, command, args...) 115 | if err != nil { 116 | return err 117 | } 118 | 119 | return nil 120 | } 121 | 122 | func (c *Connector) QueryContext(ctx context.Context, query string, scanner func(*sql.Rows) error, args ...interface{}) error { 123 | db, err := c.db() 124 | if err != nil { 125 | return err 126 | } 127 | defer db.Close() 128 | 129 | rows, err := db.QueryContext(ctx, query, args...) 130 | if err != nil { 131 | return err 132 | } 133 | defer rows.Close() 134 | 135 | err = scanner(rows) 136 | if err != nil { 137 | return err 138 | } 139 | 140 | return nil 141 | } 142 | 143 | func (c *Connector) QueryRowContext(ctx context.Context, query string, scanner func(*sql.Row) error, args ...interface{}) error { 144 | db, err := c.db() 145 | if err != nil { 146 | return err 147 | } 148 | defer db.Close() 149 | 150 | row := db.QueryRowContext(ctx, query, args...) 151 | if row.Err() != nil { 152 | return row.Err() 153 | } 154 | 155 | return scanner(row) 156 | } 157 | 158 | func (c *Connector) db() (*sql.DB, error) { 159 | if c == nil { 160 | panic("No connector") 161 | } 162 | conn, err := c.connector() 163 | if err != nil { 164 | return nil, err 165 | } 166 | if db, err := connectLoop(conn, c.Timeout); err != nil { 167 | return nil, err 168 | } else { 169 | return db, nil 170 | } 171 | } 172 | 173 | func (c *Connector) connector() (driver.Connector, error) { 174 | query := url.Values{} 175 | host := fmt.Sprintf("%s:%s", c.Host, c.Port) 176 | if c.Database != "" { 177 | query.Set("database", c.Database) 178 | } 179 | if c.Login != nil || c.AzureLogin != nil { 180 | connectionString := (&url.URL{ 181 | Scheme: "sqlserver", 182 | User: c.userPassword(), 183 | Host: host, 184 | RawQuery: query.Encode(), 185 | }).String() 186 | if c.Login != nil { 187 | return mssql.NewConnector(connectionString) 188 | } 189 | return mssql.NewAccessTokenConnector(connectionString, func() (string, error) { return c.tokenProvider() }) 190 | } 191 | if c.FedauthMSI != nil { 192 | query.Set("fedauth", "ActiveDirectoryManagedIdentity") 193 | if c.FedauthMSI.UserID != "" { 194 | query.Set("user id", c.FedauthMSI.UserID) 195 | } 196 | } else { 197 | query.Set("fedauth", "ActiveDirectoryDefault") 198 | } 199 | connectionString := (&url.URL{ 200 | Scheme: "sqlserver", 201 | Host: host, 202 | RawQuery: query.Encode(), 203 | }).String() 204 | return azuread.NewConnector(connectionString) 205 | } 206 | 207 | func (c *Connector) userPassword() *url.Userinfo { 208 | if c.Login != nil { 209 | return url.UserPassword(c.Login.Username, c.Login.Password) 210 | } 211 | return nil 212 | } 213 | 214 | func (c *Connector) tokenProvider() (string, error) { 215 | const resourceID = "https://database.windows.net/" 216 | 217 | admin := c.AzureLogin 218 | oauthConfig, err := adal.NewOAuthConfig(azure.PublicCloud.ActiveDirectoryEndpoint, admin.TenantID) 219 | if err != nil { 220 | return "", err 221 | } 222 | 223 | spt, err := adal.NewServicePrincipalToken(*oauthConfig, admin.ClientID, admin.ClientSecret, resourceID) 224 | if err != nil { 225 | return "", err 226 | } 227 | 228 | err = spt.EnsureFresh() 229 | if err != nil { 230 | return "", err 231 | } 232 | 233 | c.Token = spt.OAuthToken() 234 | 235 | return spt.OAuthToken(), nil 236 | } 237 | 238 | func connectLoop(connector driver.Connector, timeout time.Duration) (*sql.DB, error) { 239 | ticker := time.NewTicker(250 * time.Millisecond) 240 | defer ticker.Stop() 241 | 242 | timeoutExceeded := time.After(timeout) 243 | for { 244 | select { 245 | case <-timeoutExceeded: 246 | return nil, fmt.Errorf("db connection failed after %s timeout", timeout) 247 | 248 | case <-ticker.C: 249 | db, err := connect(connector) 250 | if err == nil { 251 | return db, nil 252 | } 253 | if strings.Contains(strings.ToLower(err.Error()), "login failed") { 254 | return nil, err 255 | } 256 | if strings.Contains(strings.ToLower(err.Error()), "login error") { 257 | return nil, err 258 | } 259 | if strings.Contains(err.Error(), "error retrieving access token") { 260 | return nil, err 261 | } 262 | if strings.Contains(err.Error(), "AuthenticationFailedError") { 263 | return nil, err 264 | } 265 | if strings.Contains(err.Error(), "credential") { 266 | return nil, err 267 | } 268 | if strings.Contains(err.Error(), "request failed") { 269 | return nil, err 270 | } 271 | log.Println(errors.Wrap(err, "failed to connect to database")) 272 | } 273 | } 274 | } 275 | 276 | func connect(connector driver.Connector) (*sql.DB, error) { 277 | db := sql.OpenDB(connector) 278 | if err := db.Ping(); err != nil { 279 | db.Close() 280 | return nil, err 281 | } 282 | return db, nil 283 | } 284 | -------------------------------------------------------------------------------- /sql/user.go: -------------------------------------------------------------------------------- 1 | package sql 2 | 3 | import ( 4 | "context" 5 | "database/sql" 6 | "github.com/betr-io/terraform-provider-mssql/mssql/model" 7 | "strings" 8 | ) 9 | 10 | func (c *Connector) GetUser(ctx context.Context, database, username string) (*model.User, error) { 11 | cmd := `DECLARE @stmt nvarchar(max) 12 | IF @@VERSION LIKE 'Microsoft SQL Azure%' 13 | BEGIN 14 | SET @stmt = 'WITH CTE_Roles (principal_id, role_principal_id) AS ' + 15 | '(' + 16 | ' SELECT member_principal_id, role_principal_id FROM [sys].[database_role_members] WHERE member_principal_id = DATABASE_PRINCIPAL_ID(' + QuoteName(@username, '''') + ')' + 17 | ' UNION ALL ' + 18 | ' SELECT member_principal_id, drm.role_principal_id FROM [sys].[database_role_members] drm' + 19 | ' INNER JOIN CTE_Roles cr ON drm.member_principal_id = cr.role_principal_id' + 20 | ') ' + 21 | 'SELECT p.principal_id, p.name, p.authentication_type_desc, COALESCE(p.default_schema_name, ''''), COALESCE(p.default_language_name, ''''), p.sid, CONVERT(VARCHAR(1000), p.sid, 1) AS sidStr, '''', COALESCE(STRING_AGG(USER_NAME(r.role_principal_id), '',''), '''') ' + 22 | 'FROM [sys].[database_principals] p' + 23 | ' LEFT JOIN CTE_Roles r ON p.principal_id = r.principal_id ' + 24 | 'WHERE p.name = ' + QuoteName(@username, '''') + ' ' + 25 | 'GROUP BY p.principal_id, p.name, p.authentication_type_desc, p.default_schema_name, p.default_language_name, p.sid' 26 | END 27 | ELSE 28 | BEGIN 29 | SET @stmt = 'WITH CTE_Roles (principal_id, role_principal_id) AS ' + 30 | '(' + 31 | ' SELECT member_principal_id, role_principal_id FROM ' + QuoteName(@database) + '.[sys].[database_role_members] WHERE member_principal_id = DATABASE_PRINCIPAL_ID(' + QuoteName(@username, '''') + ')' + 32 | ' UNION ALL ' + 33 | ' SELECT member_principal_id, drm.role_principal_id FROM ' + QuoteName(@database) + '.[sys].[database_role_members] drm' + 34 | ' INNER JOIN CTE_Roles cr ON drm.member_principal_id = cr.role_principal_id' + 35 | ') ' + 36 | 'SELECT p.principal_id, p.name, p.authentication_type_desc, COALESCE(p.default_schema_name, ''''), COALESCE(p.default_language_name, ''''), p.sid, CONVERT(VARCHAR(1000), p.sid, 1) AS sidStr, COALESCE(sl.name, ''''), COALESCE(STRING_AGG(USER_NAME(r.role_principal_id), '',''), '''') ' + 37 | 'FROM ' + QuoteName(@database) + '.[sys].[database_principals] p' + 38 | ' LEFT JOIN CTE_Roles r ON p.principal_id = r.principal_id ' + 39 | ' LEFT JOIN [master].[sys].[sql_logins] sl ON p.sid = sl.sid ' + 40 | 'WHERE p.name = ' + QuoteName(@username, '''') + ' ' + 41 | 'GROUP BY p.principal_id, p.name, p.authentication_type_desc, p.default_schema_name, p.default_language_name, p.sid, sl.name' 42 | END 43 | EXEC (@stmt)` 44 | var ( 45 | user model.User 46 | sid []byte 47 | roles string 48 | ) 49 | err := c. 50 | setDatabase(&database). 51 | QueryRowContext(ctx, cmd, 52 | func(r *sql.Row) error { 53 | return r.Scan(&user.PrincipalID, &user.Username, &user.AuthType, &user.DefaultSchema, &user.DefaultLanguage, &sid, &user.SIDStr, &user.LoginName, &roles) 54 | }, 55 | sql.Named("database", database), 56 | sql.Named("username", username), 57 | ) 58 | if err != nil { 59 | if err == sql.ErrNoRows { 60 | return nil, nil 61 | } 62 | return nil, err 63 | } 64 | if user.AuthType == "INSTANCE" && user.LoginName == "" { 65 | cmd = "SELECT name FROM [sys].[sql_logins] WHERE sid = @sid" 66 | c.Database = "master" 67 | err = c.QueryRowContext(ctx, cmd, 68 | func(r *sql.Row) error { 69 | return r.Scan(&user.LoginName) 70 | }, 71 | sql.Named("sid", sid), 72 | ) 73 | if err != nil { 74 | return nil, err 75 | } 76 | } 77 | if roles == "" { 78 | user.Roles = make([]string, 0) 79 | } else { 80 | user.Roles = strings.Split(roles, ",") 81 | } 82 | return &user, nil 83 | } 84 | 85 | func (c *Connector) CreateUser(ctx context.Context, database string, user *model.User) error { 86 | cmd := `DECLARE @stmt nvarchar(max) 87 | DECLARE @language nvarchar(max) = @defaultLanguage 88 | IF @language = '' SET @language = NULL 89 | IF @authType = 'INSTANCE' 90 | BEGIN 91 | SET @stmt = 'CREATE USER ' + QuoteName(@username) + ' FOR LOGIN ' + QuoteName(@loginName) + ' ' + 92 | 'WITH DEFAULT_SCHEMA = ' + QuoteName(@defaultSchema) 93 | END 94 | IF @authType = 'DATABASE' 95 | BEGIN 96 | SET @stmt = 'CREATE USER ' + QuoteName(@username) + ' WITH PASSWORD = ' + QuoteName(@password, '''') + ', ' + 97 | 'DEFAULT_SCHEMA = ' + QuoteName(@defaultSchema) 98 | IF NOT @@VERSION LIKE 'Microsoft SQL Azure%' 99 | BEGIN 100 | SET @stmt = @stmt + ', DEFAULT_LANGUAGE = ' + Coalesce(QuoteName(@language), 'NONE') 101 | END 102 | END 103 | IF @authType = 'EXTERNAL' 104 | BEGIN 105 | IF @@VERSION LIKE 'Microsoft SQL Azure%' 106 | BEGIN 107 | IF @objectId != '' 108 | BEGIN 109 | SET @stmt = 'CREATE USER ' + QuoteName(@username) + ' WITH SID=' + CONVERT(varchar(64), CAST(CAST(@objectId AS UNIQUEIDENTIFIER) AS VARBINARY(16)), 1) + ', TYPE=E' 110 | END 111 | ELSE 112 | BEGIN 113 | SET @stmt = 'CREATE USER ' + QuoteName(@username) + ' FROM EXTERNAL PROVIDER' 114 | END 115 | END 116 | ELSE 117 | BEGIN 118 | SET @stmt = 'CREATE USER ' + QuoteName(@username) + ' FOR LOGIN ' + QuoteName(@username) + ' FROM EXTERNAL PROVIDER ' + 119 | 'WITH DEFAULT_SCHEMA = ' + QuoteName(@defaultSchema) + ', ' + 120 | 'DEFAULT_LANGUAGE = ' + Coalesce(QuoteName(@language), 'NONE') 121 | END 122 | END 123 | 124 | BEGIN TRANSACTION; 125 | EXEC sp_getapplock @Resource = 'create_func', @LockMode = 'Exclusive'; 126 | IF exists (select compatibility_level FROM sys.databases where name = db_name() and compatibility_level < 130) AND objectproperty(object_id('String_Split'), 'isProcedure') IS NULL 127 | BEGIN 128 | DECLARE @sql NVARCHAR(MAX); 129 | SET @sql = N'Create FUNCTION [dbo].[String_Split] 130 | ( 131 | @string nvarchar(max), 132 | @delimiter nvarchar(max) 133 | ) 134 | /* 135 | The same as STRING_SPLIT for compatibility level < 130 136 | https://docs.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql?view=sql-server-ver15 137 | */ 138 | RETURNS TABLE AS RETURN 139 | ( 140 | SELECT 141 | --ROW_NUMBER ( ) over(order by (select 0)) AS id -- intuitive, but not correect 142 | Split.a.value(''let $n := . return count(../*[. << $n]) + 1'', ''int'') AS id 143 | , Split.a.value(''.'', ''NVARCHAR(MAX)'') AS value 144 | FROM 145 | ( 146 | SELECT CAST(''''+REPLACE(@string, @delimiter, '''')+'''' AS XML) AS String 147 | ) AS a 148 | CROSS APPLY String.nodes(''/X'') AS Split(a) 149 | )'; 150 | EXEC sp_executesql @sql; 151 | END 152 | EXEC sp_releaseapplock @Resource = 'create_func'; 153 | COMMIT TRANSACTION; 154 | SET @stmt = @stmt + '; ' + 155 | 'DECLARE role_cur CURSOR FOR SELECT name FROM ' + QuoteName(@database) + '.[sys].[database_principals] WHERE type = ''R'' AND name != ''public'' AND name COLLATE SQL_Latin1_General_CP1_CI_AS IN (SELECT value FROM String_Split(' + QuoteName(@roles, '''') + ', '',''));' + 156 | 'DECLARE @role nvarchar(max);' + 157 | 'OPEN role_cur;' + 158 | 'FETCH NEXT FROM role_cur INTO @role;' + 159 | 'WHILE @@FETCH_STATUS = 0' + 160 | ' BEGIN' + 161 | ' DECLARE @sql nvarchar(max);' + 162 | ' SET @sql = ''ALTER ROLE '' + QuoteName(@role) + '' ADD MEMBER ' + QuoteName(@username) + ''';' + 163 | ' EXEC (@sql);' + 164 | ' FETCH NEXT FROM role_cur INTO @role;' + 165 | ' END;' + 166 | 'CLOSE role_cur;' + 167 | 'DEALLOCATE role_cur;' 168 | EXEC (@stmt)` 169 | if user.AuthType != "EXTERNAL" { 170 | // External users do not have a server login 171 | _, err := c.GetLogin(ctx, user.LoginName) 172 | if err != nil { 173 | return err 174 | } 175 | } 176 | return c. 177 | setDatabase(&database). 178 | ExecContext(ctx, cmd, 179 | sql.Named("database", database), 180 | sql.Named("username", user.Username), 181 | sql.Named("objectId", user.ObjectId), 182 | sql.Named("loginName", user.LoginName), 183 | sql.Named("password", user.Password), 184 | sql.Named("authType", user.AuthType), 185 | sql.Named("defaultSchema", user.DefaultSchema), 186 | sql.Named("defaultLanguage", user.DefaultLanguage), 187 | sql.Named("roles", strings.Join(user.Roles, ",")), 188 | ) 189 | } 190 | 191 | func (c *Connector) UpdateUser(ctx context.Context, database string, user *model.User) error { 192 | cmd := `DECLARE @stmt nvarchar(max) 193 | SET @stmt = 'ALTER USER ' + QuoteName(@username) + ' ' 194 | DECLARE @language nvarchar(max) = @defaultLanguage 195 | IF @language = '' SET @language = NULL 196 | SET @stmt = @stmt + 'WITH DEFAULT_SCHEMA = ' + QuoteName(@defaultSchema) 197 | DECLARE @auth_type nvarchar(max) = (SELECT authentication_type_desc FROM [sys].[database_principals] WHERE name = @username) 198 | IF NOT @@VERSION LIKE 'Microsoft SQL Azure%' AND @auth_type != 'INSTANCE' 199 | BEGIN 200 | SET @stmt = @stmt + ', DEFAULT_LANGUAGE = ' + Coalesce(QuoteName(@language), 'NONE') 201 | END 202 | 203 | BEGIN TRANSACTION; 204 | EXEC sp_getapplock @Resource = 'create_func', @LockMode = 'Exclusive'; 205 | IF exists (select compatibility_level FROM sys.databases where name = db_name() and compatibility_level < 130) AND objectproperty(object_id('String_Split'), 'isProcedure') IS NULL 206 | BEGIN 207 | DECLARE @sql NVARCHAR(MAX); 208 | SET @sql = N'Create FUNCTION [dbo].[String_Split] 209 | ( 210 | @string nvarchar(max), 211 | @delimiter nvarchar(max) 212 | ) 213 | /* 214 | The same as STRING_SPLIT for compatibility level < 130 215 | https://docs.microsoft.com/en-us/sql/t-sql/functions/string-split-transact-sql?view=sql-server-ver15 216 | */ 217 | RETURNS TABLE AS RETURN 218 | ( 219 | SELECT 220 | --ROW_NUMBER ( ) over(order by (select 0)) AS id -- intuitive, but not correect 221 | Split.a.value(''let $n := . return count(../*[. << $n]) + 1'', ''int'') AS id 222 | , Split.a.value(''.'', ''NVARCHAR(MAX)'') AS value 223 | FROM 224 | ( 225 | SELECT CAST(''''+REPLACE(@string, @delimiter, '''')+'''' AS XML) AS String 226 | ) AS a 227 | CROSS APPLY String.nodes(''/X'') AS Split(a) 228 | )'; 229 | EXEC sp_executesql @sql; 230 | END 231 | EXEC sp_releaseapplock @Resource = 'create_func'; 232 | COMMIT TRANSACTION; 233 | SET @stmt = @stmt + '; ' + 234 | 'DECLARE @sql nvarchar(max);' + 235 | 'DECLARE @role nvarchar(max);' + 236 | 'DECLARE del_role_cur CURSOR FOR SELECT name FROM ' + QuoteName(@database) + '.[sys].[database_principals] WHERE type = ''R'' AND name != ''public'' AND name IN (SELECT name FROM ' + QuoteName(@database) + '.[sys].[database_role_members] drm, ' + QuoteName(@database) + '.[sys].[database_principals] db WHERE drm.member_principal_id = DATABASE_PRINCIPAL_ID(' + QuoteName(@username, '''') + ') AND drm.role_principal_id = db.principal_id) AND name COLLATE SQL_Latin1_General_CP1_CI_AS NOT IN(SELECT value FROM String_Split(' + QuoteName(@roles, '''') + ', '',''));' + 237 | 'DECLARE add_role_cur CURSOR FOR SELECT name FROM ' + QuoteName(@database) + '.[sys].[database_principals] WHERE type = ''R'' AND name != ''public'' AND name NOT IN (SELECT name FROM ' + QuoteName(@database) + '.[sys].[database_role_members] drm, ' + QuoteName(@database) + '.[sys].[database_principals] db WHERE drm.member_principal_id = DATABASE_PRINCIPAL_ID(' + QuoteName(@username, '''') + ') AND drm.role_principal_id = db.principal_id) AND name COLLATE SQL_Latin1_General_CP1_CI_AS IN(SELECT value FROM String_Split(' + QuoteName(@roles, '''') + ', '',''));' + 238 | 'OPEN del_role_cur;' + 239 | 'FETCH NEXT FROM del_role_cur INTO @role;' + 240 | 'WHILE @@FETCH_STATUS = 0' + 241 | ' BEGIN' + 242 | ' SET @sql = ''ALTER ROLE '' + QuoteName(@role) + '' DROP MEMBER ' + QuoteName(@username) + ''';' + 243 | ' EXEC (@sql);' + 244 | ' FETCH NEXT FROM del_role_cur INTO @role;' + 245 | ' END;' + 246 | 'CLOSE del_role_cur;' + 247 | 'DEALLOCATE del_role_cur;' + 248 | 'OPEN add_role_cur;' + 249 | 'FETCH NEXT FROM add_role_cur INTO @role;' + 250 | 'WHILE @@FETCH_STATUS = 0' + 251 | ' BEGIN' + 252 | ' SET @sql = ''ALTER ROLE '' + QuoteName(@role) + '' ADD MEMBER ' + QuoteName(@username) + ''';' + 253 | ' EXEC (@sql);' + 254 | ' FETCH NEXT FROM add_role_cur INTO @role;' + 255 | ' END;' + 256 | 'CLOSE add_role_cur;' + 257 | 'DEALLOCATE add_role_cur;' 258 | EXEC (@stmt)` 259 | return c. 260 | setDatabase(&database). 261 | ExecContext(ctx, cmd, 262 | sql.Named("database", database), 263 | sql.Named("username", user.Username), 264 | sql.Named("defaultSchema", user.DefaultSchema), 265 | sql.Named("defaultLanguage", user.DefaultLanguage), 266 | sql.Named("roles", strings.Join(user.Roles, ",")), 267 | ) 268 | } 269 | 270 | func (c *Connector) DeleteUser(ctx context.Context, database, username string) error { 271 | cmd := `DECLARE @stmt nvarchar(max) 272 | SET @stmt = 'IF EXISTS (SELECT 1 FROM ' + QuoteName(@database) + '.[sys].[database_principals] WHERE [name] = ' + QuoteName(@username, '''') + ') ' + 273 | 'DROP USER ' + QuoteName(@username) 274 | EXEC (@stmt)` 275 | return c. 276 | setDatabase(&database). 277 | ExecContext(ctx, cmd, sql.Named("database", database), sql.Named("username", username)) 278 | } 279 | 280 | func (c *Connector) setDatabase(database *string) *Connector { 281 | if *database == "" { 282 | *database = "master" 283 | } 284 | c.Database = *database 285 | return c 286 | } 287 | -------------------------------------------------------------------------------- /test-fixtures/all/.gitignore: -------------------------------------------------------------------------------- 1 | terraform.tfvars 2 | -------------------------------------------------------------------------------- /test-fixtures/all/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Creates a SQL Server running in a docker container on the local machine. 3 | # 4 | locals { 5 | local_username = "sa" 6 | local_password = "!!up3R!!3cR37" 7 | } 8 | 9 | resource "docker_image" "mssql" { 10 | name = "mcr.microsoft.com/mssql/server" 11 | keep_locally = true 12 | } 13 | 14 | resource "docker_container" "mssql" { 15 | name = "mssql" 16 | image = docker_image.mssql.image_id 17 | ports { 18 | internal = 1433 19 | external = 1433 20 | } 21 | env = ["ACCEPT_EULA=Y", "SA_PASSWORD=${local.local_password}"] 22 | } 23 | 24 | 25 | # 26 | # Creates an Azure SQL Database running in a temporary resource group on Azure. 27 | # 28 | 29 | # Random names and secrets 30 | resource "random_string" "random" { 31 | length = 16 32 | upper = false 33 | special = false 34 | } 35 | 36 | locals { 37 | prefix = "${var.prefix}-${substr(random_string.random.result, 0, 4)}" 38 | } 39 | 40 | # An Azure AD group assigned the role 'Directory Readers'. The Azure SQL Server needs to be assigned to this group to enable external logins. 41 | data "azuread_group" "sql_servers" { 42 | display_name = var.sql_servers_group 43 | } 44 | 45 | # An Azure AD service principal used as Azure Administrator for the Azure SQL Server resource 46 | resource "azuread_application" "sa" { 47 | display_name = "${local.prefix}-sa" 48 | web { 49 | homepage_url = "https://test.example.com" 50 | } 51 | } 52 | 53 | resource "azuread_service_principal" "sa" { 54 | client_id = azuread_application.sa.client_id 55 | } 56 | 57 | resource "azuread_service_principal_password" "sa" { 58 | service_principal_id = azuread_service_principal.sa.id 59 | } 60 | 61 | # An Azure AD service principal used to test creating an external login to the Azure SQL server resource 62 | resource "azuread_application" "user" { 63 | display_name = "${local.prefix}-user" 64 | web { 65 | homepage_url = "https://test.example.com" 66 | } 67 | } 68 | 69 | resource "azuread_service_principal" "user" { 70 | client_id = azuread_application.user.client_id 71 | } 72 | 73 | resource "azuread_service_principal_password" "user" { 74 | service_principal_id = azuread_service_principal.user.id 75 | } 76 | 77 | # Temporary resource group 78 | resource "azurerm_resource_group" "rg" { 79 | name = "${lower(var.prefix)}-${random_string.random.result}" 80 | location = var.location 81 | } 82 | 83 | # An Azure SQL Server 84 | resource "azurerm_mssql_server" "sql_server" { 85 | name = "${lower(local.prefix)}-sql-server" 86 | resource_group_name = azurerm_resource_group.rg.name 87 | location = azurerm_resource_group.rg.location 88 | 89 | version = "12.0" 90 | administrator_login = "SuperAdministrator" 91 | administrator_login_password = azuread_service_principal_password.sa.value 92 | 93 | azuread_administrator { 94 | tenant_id = var.tenant_id 95 | object_id = azuread_service_principal.sa.client_id 96 | login_username = azuread_service_principal.sa.display_name 97 | } 98 | 99 | identity { 100 | type = "SystemAssigned" 101 | } 102 | } 103 | 104 | resource "azuread_group_member" "sql" { 105 | group_object_id = data.azuread_group.sql_servers.id 106 | member_object_id = azurerm_mssql_server.sql_server.identity[0].principal_id 107 | } 108 | 109 | resource "azurerm_mssql_firewall_rule" "sql_server_fw_rule" { 110 | count = length(var.local_ip_addresses) 111 | name = "AllowIP ${count.index}" 112 | server_id = azurerm_mssql_server.sql_server.id 113 | start_ip_address = var.local_ip_addresses[count.index] 114 | end_ip_address = var.local_ip_addresses[count.index] 115 | } 116 | 117 | # The Azure SQL Database used in tests 118 | resource "azurerm_mssql_database" "db" { 119 | name = "testdb" 120 | server_id = azurerm_mssql_server.sql_server.id 121 | sku_name = "Basic" 122 | } 123 | 124 | 125 | # 126 | # Writes information necessary to log in to the SQL Server to file. This file is used by the Makefile when running acceptance tests. 127 | # 128 | resource "local_sensitive_file" "local_env" { 129 | filename = "${path.root}/../../.local.env" 130 | directory_permission = "0755" 131 | file_permission = "0600" 132 | content = <<-EOT 133 | export TF_ACC=1 134 | export MSSQL_USERNAME='${local.local_username}' 135 | export MSSQL_PASSWORD='${local.local_password}' 136 | export MSSQL_TENANT_ID='${var.tenant_id}' 137 | export MSSQL_CLIENT_ID='${azuread_service_principal.sa.client_id}' 138 | export MSSQL_CLIENT_SECRET='${azuread_service_principal_password.sa.value}' 139 | export TF_ACC_SQL_SERVER='${azurerm_mssql_server.sql_server.fully_qualified_domain_name}' 140 | export TF_ACC_AZURE_MSSQL_USERNAME='${azurerm_mssql_server.sql_server.administrator_login}' 141 | export TF_ACC_AZURE_MSSQL_PASSWORD='${azurerm_mssql_server.sql_server.administrator_login_password}' 142 | export TF_ACC_AZURE_USER_CLIENT_ID='${azuread_service_principal.user.client_id}' 143 | export TF_ACC_AZURE_USER_CLIENT_USER='${azuread_service_principal.user.display_name}' 144 | export TF_ACC_AZURE_USER_CLIENT_SECRET='${azuread_service_principal_password.user.value}' 145 | # Configuration for fedauth which uses env vars via DefaultAzureCredential 146 | export AZURE_TENANT_ID='${var.tenant_id}' 147 | export AZURE_CLIENT_ID='${azuread_service_principal.sa.client_id}' 148 | export AZURE_CLIENT_SECRET='${azuread_service_principal_password.sa.value}' 149 | EOT 150 | } 151 | -------------------------------------------------------------------------------- /test-fixtures/all/providers.tf: -------------------------------------------------------------------------------- 1 | provider "azuread" {} 2 | 3 | provider "azurerm" { 4 | features {} 5 | } 6 | 7 | provider "docker" {} 8 | 9 | provider "local" {} 10 | 11 | provider "random" {} 12 | -------------------------------------------------------------------------------- /test-fixtures/all/terraform.tfvars: -------------------------------------------------------------------------------- 1 | prefix = "Betr" 2 | location = "Norway East" 3 | tenant_id = "30ddf688-b59c-470c-b8e1-143ccbb6ce33" 4 | 5 | local_ip_addresses = ["193.214.250.243"] 6 | # local_ip_addresses = ["193.214.69.94"] 7 | -------------------------------------------------------------------------------- /test-fixtures/all/variables.tf: -------------------------------------------------------------------------------- 1 | variable "prefix" { 2 | description = "A prefix used when naming Azure resources" 3 | type = string 4 | } 5 | 6 | variable "sql_servers_group" { 7 | description = "The name of an Azure AD group assigned the role 'Directory Reader'. The Azure SQL Server will be added to this group to enable external logins." 8 | type = string 9 | default = "SQL Servers" 10 | } 11 | 12 | variable "location" { 13 | description = "The location of the Azure resources." 14 | type = string 15 | default = "East US" 16 | } 17 | 18 | variable "tenant_id" { 19 | description = "The tenant id of the Azure AD tenant" 20 | type = string 21 | } 22 | 23 | variable "local_ip_addresses" { 24 | description = "The external IP addresses of the machines running the acceptance tests. This is necessary to allow access to the Azure SQL Server resource." 25 | type = list(string) 26 | } 27 | -------------------------------------------------------------------------------- /test-fixtures/all/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.5" 3 | required_providers { 4 | azuread = { 5 | source = "hashicorp/azuread" 6 | version = "~> 2.47" 7 | } 8 | azurerm = { 9 | source = "hashicorp/azurerm" 10 | version = "~> 3.85" 11 | } 12 | docker = { 13 | source = "kreuzwerker/docker" 14 | version = "~> 3.0" 15 | } 16 | local = { 17 | source = "hashicorp/local" 18 | version = "~> 2.4" 19 | } 20 | random = { 21 | source = "hashicorp/random" 22 | version = "~> 3.6" 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test-fixtures/local/main.tf: -------------------------------------------------------------------------------- 1 | # 2 | # Creates a SQL Server running in a docker container on the local machine. 3 | # 4 | locals { 5 | local_username = "sa" 6 | local_password = "!!up3R!!3cR37" 7 | } 8 | 9 | resource "docker_image" "mssql" { 10 | name = "mcr.microsoft.com/mssql/server" 11 | keep_locally = true 12 | } 13 | 14 | resource "docker_container" "mssql" { 15 | name = "mssql" 16 | image = docker_image.mssql.image_id 17 | ports { 18 | internal = 1433 19 | external = 1433 20 | } 21 | env = ["ACCEPT_EULA=Y", "SA_PASSWORD=${local.local_password}"] 22 | } 23 | 24 | 25 | # 26 | # Writes information necessary to log in to the SQL Server to file. This file is used by the Makefile when running acceptance tests. 27 | # 28 | resource "local_sensitive_file" "local_env" { 29 | filename = "${path.root}/../../.local.env" 30 | directory_permission = "0755" 31 | file_permission = "0600" 32 | content = <<-EOT 33 | export TF_ACC_LOCAL=1 34 | export MSSQL_USERNAME='${local.local_username}' 35 | export MSSQL_PASSWORD='${local.local_password}' 36 | EOT 37 | } 38 | -------------------------------------------------------------------------------- /test-fixtures/local/providers.tf: -------------------------------------------------------------------------------- 1 | provider "docker" { 2 | host = var.operating_system == "Windows" ? "npipe:////.//pipe//docker_engine" : "unix:///var/run/docker.sock" 3 | } 4 | 5 | provider "local" {} 6 | -------------------------------------------------------------------------------- /test-fixtures/local/variables.tf: -------------------------------------------------------------------------------- 1 | variable "operating_system" { 2 | description = "On which operating system is Docker running?" 3 | default = "Linux" 4 | 5 | validation { 6 | condition = contains(["MacOS", "Windows", "Linux"], var.operating_system) 7 | error_message = "Value must be MacOS, Windows, or Linux." 8 | } 9 | } -------------------------------------------------------------------------------- /test-fixtures/local/versions.tf: -------------------------------------------------------------------------------- 1 | terraform { 2 | required_version = "~> 1.5" 3 | required_providers { 4 | docker = { 5 | source = "kreuzwerker/docker" 6 | version = "~> 3.0" 7 | } 8 | local = { 9 | source = "hashicorp/local" 10 | version = "~> 2.4" 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /wait-for: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # The MIT License (MIT) 4 | # 5 | # Copyright (c) 2017 Eficode Oy 6 | # 7 | # Permission is hereby granted, free of charge, to any person obtaining a copy 8 | # of this software and associated documentation files (the "Software"), to deal 9 | # in the Software without restriction, including without limitation the rights 10 | # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 11 | # copies of the Software, and to permit persons to whom the Software is 12 | # furnished to do so, subject to the following conditions: 13 | # 14 | # The above copyright notice and this permission notice shall be included in all 15 | # copies or substantial portions of the Software. 16 | # 17 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 22 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 23 | # SOFTWARE. 24 | 25 | VERSION="2.2.4" 26 | 27 | set -- "$@" -- "$TIMEOUT" "$QUIET" "$PROTOCOL" "$HOST" "$PORT" "$result" 28 | TIMEOUT=15 29 | QUIET=0 30 | # The protocol to make the request with, either "tcp" or "http" 31 | PROTOCOL="tcp" 32 | 33 | echoerr() { 34 | if [ "$QUIET" -ne 1 ]; then printf "%s\n" "$*" 1>&2; fi 35 | } 36 | 37 | usage() { 38 | exitcode="$1" 39 | cat << USAGE >&2 40 | Usage: 41 | $0 host:port|url [-t timeout] [-- command args] 42 | -q | --quiet Do not output any status messages 43 | -t TIMEOUT | --timeout=timeout Timeout in seconds, zero for no timeout 44 | Defaults to 15 seconds 45 | -v | --version Show the version of this tool 46 | -- COMMAND ARGS Execute command with args after the test finishes 47 | USAGE 48 | exit "$exitcode" 49 | } 50 | 51 | wait_for() { 52 | case "$PROTOCOL" in 53 | tcp) 54 | if ! command -v nc >/dev/null; then 55 | echoerr 'nc command is missing!' 56 | exit 1 57 | fi 58 | ;; 59 | http) 60 | if ! command -v wget >/dev/null; then 61 | echoerr 'wget command is missing!' 62 | exit 1 63 | fi 64 | ;; 65 | esac 66 | 67 | TIMEOUT_END=$(($(date +%s) + TIMEOUT)) 68 | 69 | while :; do 70 | case "$PROTOCOL" in 71 | tcp) 72 | nc -w 1 -z "$HOST" "$PORT" > /dev/null 2>&1 73 | ;; 74 | http) 75 | wget --timeout=1 --tries=1 -q "$HOST" -O /dev/null > /dev/null 2>&1 76 | ;; 77 | *) 78 | echoerr "Unknown protocol '$PROTOCOL'" 79 | exit 1 80 | ;; 81 | esac 82 | 83 | result=$? 84 | 85 | if [ $result -eq 0 ] ; then 86 | if [ $# -gt 7 ] ; then 87 | for result in $(seq $(($# - 7))); do 88 | result=$1 89 | shift 90 | set -- "$@" "$result" 91 | done 92 | 93 | TIMEOUT=$2 QUIET=$3 PROTOCOL=$4 HOST=$5 PORT=$6 result=$7 94 | shift 7 95 | exec "$@" 96 | fi 97 | exit 0 98 | fi 99 | 100 | if [ $TIMEOUT -ne 0 -a $(date +%s) -ge $TIMEOUT_END ]; then 101 | echo "Operation timed out" >&2 102 | exit 1 103 | fi 104 | 105 | sleep 1 106 | done 107 | } 108 | 109 | while :; do 110 | case "$1" in 111 | http://*|https://*) 112 | HOST="$1" 113 | PROTOCOL="http" 114 | shift 1 115 | ;; 116 | *:* ) 117 | HOST=$(printf "%s\n" "$1"| cut -d : -f 1) 118 | PORT=$(printf "%s\n" "$1"| cut -d : -f 2) 119 | shift 1 120 | ;; 121 | -v | --version) 122 | echo $VERSION 123 | exit 124 | ;; 125 | -q | --quiet) 126 | QUIET=1 127 | shift 1 128 | ;; 129 | -q-*) 130 | QUIET=0 131 | echoerr "Unknown option: $1" 132 | usage 1 133 | ;; 134 | -q*) 135 | QUIET=1 136 | result=$1 137 | shift 1 138 | set -- -"${result#-q}" "$@" 139 | ;; 140 | -t | --timeout) 141 | TIMEOUT="$2" 142 | shift 2 143 | ;; 144 | -t*) 145 | TIMEOUT="${1#-t}" 146 | shift 1 147 | ;; 148 | --timeout=*) 149 | TIMEOUT="${1#*=}" 150 | shift 1 151 | ;; 152 | --) 153 | shift 154 | break 155 | ;; 156 | --help) 157 | usage 0 158 | ;; 159 | -*) 160 | QUIET=0 161 | echoerr "Unknown option: $1" 162 | usage 1 163 | ;; 164 | *) 165 | QUIET=0 166 | echoerr "Unknown argument: $1" 167 | usage 1 168 | ;; 169 | esac 170 | done 171 | 172 | if ! [ "$TIMEOUT" -ge 0 ] 2>/dev/null; then 173 | echoerr "Error: invalid timeout '$TIMEOUT'" 174 | usage 3 175 | fi 176 | 177 | case "$PROTOCOL" in 178 | tcp) 179 | if [ "$HOST" = "" ] || [ "$PORT" = "" ]; then 180 | echoerr "Error: you need to provide a host and port to test." 181 | usage 2 182 | fi 183 | ;; 184 | http) 185 | if [ "$HOST" = "" ]; then 186 | echoerr "Error: you need to provide a host to test." 187 | usage 2 188 | fi 189 | ;; 190 | esac 191 | 192 | wait_for "$@" 193 | --------------------------------------------------------------------------------