├── .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 |
--------------------------------------------------------------------------------