Compare commits

..
19 Commits
Author SHA1 Message Date
tf-release-bot 02445d2192 v1.0.6 2022-01-21 00:58:42 +00:00
Louis Ruch 123284e88f chore(release): Update changelog 2022-01-20 16:56:47 -08:00
6b28ea22a8 Add dynamic host plugin catalog/set (#159)
Co-authored-by: Todd <T.Alan.Knight@gmail.com>
Co-authored-by: Louis Ruch <louisruch@gmail.com>
2022-01-19 16:19:09 -08:00
Peter Lindsten 649b6a54f5 Update incorrect docs (#161)
Update example resource variable name
2021-12-20 07:44:49 -08:00
Louis Ruch 93471e3b28 chore: Update deps 2021-12-17 21:39:22 -08:00
Louis Ruch 452fd1c764 chore: make docs 2021-12-17 21:19:27 -08:00
Olivier Boukili cfe7d47286 Support insecure TLS communication to API endpoint (#163) 2021-12-17 20:24:09 -08:00
Louis Ruch af98aaa9b2 chore(targets): remove deprecated fields (#150) 2021-11-15 13:42:00 -05:00
Louis Ruch 10b421cec8 chore(deps): update dependancies (#149) 2021-10-13 18:10:22 -07:00
dependabot[bot] 98d768f8c8 build(deps): bump github.com/hashicorp/boundary from 0.6.0 to 0.6.1
Bumps [github.com/hashicorp/boundary](https://github.com/hashicorp/boundary) from 0.6.0 to 0.6.1.
- [Release notes](https://github.com/hashicorp/boundary/releases)
- [Changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md)
- [Commits](https://github.com/hashicorp/boundary/compare/v0.6.0...v0.6.1)

---
updated-dependencies:
- dependency-name: github.com/hashicorp/boundary
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-21 09:29:34 -04:00
Timothy Messier c869dfb367 ci: Use same docker image from boundary for acceptance tests
Boundary changes how the test harness is used in order to speed up its
tests. It now requires that a postgres docker container is started prior
to running the tests. This adds some make targets to start up the docker
image, using the same image as boundary.
2021-09-21 09:29:34 -04:00
tf-release-bot 5f4fa20ca8 v1.0.5 2021-09-09 15:26:00 +00:00
Louis Ruch 6374cb307b 1.0.5 release update dependencies (#140)
* chore: bump dependencies

* chore: update changelog for release 1.0.5
2021-09-09 08:22:25 -07:00
DevOps RobandLouis Ruch 9c054c1ac8 updating examples to use new grants_strings syntax (#132)
* updating examples to use new grants_strings syntax

* Make docs

Co-authored-by: Louis Ruch <louisruch@gmail.com>
2021-09-08 13:02:12 -07:00
Louis Ruchandgrantorchard 725f2d8fb9 Deprecate target host_set_ids and credential_library_ids (#134)
* Deprecate target host_set_ids and credential_library_ids

* Update changelog

* Include the 'type' attribute in the boundary_target documentation example. (#88)

* Include the 'type' attribute in the example.

* run tfplugindocs to generate the new markdown with type attribute change included

Co-authored-by: grantorchard <go@hashicorp.com>
2021-09-08 11:15:10 -07:00
Louis Ruch 7372487f84 Fix white space 2021-09-01 14:37:22 -07:00
Edgaras f202fbc06d docs: fix examples (#96) 2021-09-01 14:36:33 -07:00
grantorchard 8bb1490ebd Include the 'type' attribute in the boundary_target documentation example. (#88)
* Include the 'type' attribute in the example.

* run tfplugindocs to generate the new markdown with type attribute change included
2021-09-01 14:15:36 -07:00
Louis Ruch 45c1a8c5cc Make docs 2021-08-19 21:13:33 +02:00
45 changed files with 3283 additions and 892 deletions

No files matched your search

+10 -1
View File
@@ -1,8 +1,9 @@
version: 2.1
jobs:
build:
resource_class: large
machine:
image: 'ubuntu-1604:201903-01'
image: 'ubuntu-2004:202107-02'
working_directory: ~/terraform-provider-watchtower
steps:
- add_ssh_keys:
@@ -10,7 +11,15 @@ jobs:
# hc-github-team-secure-boundary github user,TF_ACC=1 ~/.go/bin/go test ./... -v $(TESTARGS) -timeout 120m to read private modules
- "d4:7f:b2:f7:6f:4d:e7:21:cc:d2:b7:6a:8b:68:e6:d4"
- checkout
- run:
name: "Install go"
command: |
unset GOPATH # ci image has done strange things with GOPATH, unset it so install-go can fix it
make install-go
source ~/.bashrc
go mod download
- run:
name: "Run Acceptance Tests"
command: |
source ~/.bashrc
make testacc-ci
+1 -1
View File
@@ -1 +1 @@
1.16.3
1.17.5
+23
View File
@@ -1,3 +1,26 @@
## 1.0.6 (January 21, 2022)
### New and Improved
* Adds dynamic host plugin catalog/set
([PR](https://github.com/hashicorp/terraform-provider-boundary/pull/159)).
* Adds support for insecure TLS to Boundary
([PR](https://github.com/hashicorp/terraform-provider-boundary/pull/163)).
### Deprecations/Changes
* Removes fields `host_set_ids` and `application_credential_library_ids` of the
`target` resource, which were deprecated in 1.0.5
([PR](https://github.com/hashicorp/terraform-provider-boundary/pull/150)).
## 1.0.5 (September 09, 2021)
### Deprecations/Changes
* Deprecate fields `host_set_ids` and `application_credential_library_ids` of the
`target` resource. See boundary 0.5.0 [changelog](https://github.com/hashicorp/boundary/blob/main/CHANGELOG.md#deprecationschanges) for more detail on the deprecation.
([PR](https://github.com/hashicorp/terraform-provider-boundary/pull/134)).
## 1.0.4 (August 19, 2021)
### New and Improved
+1 -1
View File
@@ -1,4 +1,4 @@
FROM golang:1.16
FROM golang:1.17
WORKDIR /go/src/terraform-provider-boundary
COPY . .
+43 -6
View File
@@ -1,4 +1,4 @@
default: testacc
default: testacc
GOOS=$(shell go env GOOS)
GOARCH=$(shell go env GOARCH)
INSTALL_PATH=~/.local/share/terraform/plugins/localhost/providers/boundary/0.0.1/linux_$(GOARCH)
@@ -11,6 +11,16 @@ ifeq ($(GOOS), "windows")
INSTALL_PATH=%APPDATA%/HashiCorp/Terraform/plugins/localhost/providers/boundary/0.0.1/windows_$(GOARCH)
endif
REGISTRY_NAME?=docker.io/hashicorpboundary
IMAGE_NAME=postgres
IMAGE_TAG ?= $(REGISTRY_NAME)/$(IMAGE_NAME):11-alpine
DOCKER_ARGS ?= -d
PG_OPTS ?=
TEST_DB_PORT ?= 5432
BOUNDARY_VERSION = $(shell go mod edit -json | jq -r '.["Require"][] | select(.Path=="github.com/hashicorp/boundary") | .["Version"]')
GOPATH ?= ~/go
GOMODCACHE ?= $(GOPATH)/pkg/mod
tools:
go generate -tags tools tools/tools.go
@@ -24,15 +34,17 @@ test:
testacc:
TF_ACC=1 go test ./... -v $(TESTARGS) -timeout 120m
testacc-ci: install-go
testacc-ci:
git config --global --add url."git@github.com:".insteadOf "https://github.com/"
TF_ACC=1 ~/.go/bin/go test ./... -v $(TESTARGS) -timeout 120m
install-go:
install-go: ~/.go/bin/go
~/.go/bin/go:
./ci/goinstall.sh
dev:
mkdir -p $(INSTALL_PATH)
mkdir -p $(INSTALL_PATH)
go build -o $(INSTALL_PATH)/terraform-provider-boundary main.go
all:
@@ -41,10 +53,35 @@ all:
GOOS=windows go build -o $(BUILD_ALL_PATH)/terraform-provider-boundary_windows-amd64 main.go
GOOS=linux go build -o $(BUILD_ALL_PATH)/terraform-provider-boundary_linux-amd64 main.go
docs:
docs:
go run github.com/hashicorp/terraform-plugin-docs/cmd/tfplugindocs
rm-id-flag-from-docs:
find docs/ -name "*.md" -type f | xargs sed -i -e '/- \*\*id\*\*/d'
.PHONY: testacc tools docs
test-database-up:
@echo "Using image: $(IMAGE_TAG)"
@echo "Additional postgres configuration: $(PG_OPTS)"
@echo "Using volume: $(GOMODCACHE)/github.com/hashicorp/boundary@$(BOUNDARY_VERSION)/internal/db/schema/migrations:/migrations"
@docker run \
$(DOCKER_ARGS) \
--name boundary-sql-tests \
-p $(TEST_DB_PORT):5432 \
-e POSTGRES_PASSWORD=boundary \
-e POSTGRES_USER=boundary \
-e POSTGRES_DB=boundary \
-e PGDATA=/pgdata \
--mount type=tmpfs,destination=/pgdata \
-v "$(GOMODCACHE)/github.com/hashicorp/boundary@$(BOUNDARY_VERSION)/internal/db/schema/migrations":/migrations \
$(IMAGE_TAG) \
-c 'config_file=/etc/postgresql/postgresql.conf' \
$(PG_OPTS) 1> /dev/null
@echo "Test database available at: 127.0.0.1:$(TEST_DB_PORT)"
@echo "For database logs run:"
@echo " docker logs boundary-sql-tests"
test-database-down:
docker stop boundary-sql-tests || true
docker rm -v boundary-sql-tests || true
.PHONY: testacc tools docs test-database-up test-database-down
+13 -2
View File
@@ -9,7 +9,7 @@ Requirements
------------
- [Terraform](https://www.terraform.io/downloads.html) >= 0.12.x
- [Go](https://golang.org/doc/install) >= 1.16
- [Go](https://golang.org/doc/install) >= 1.17
Building The Provider
---------------------
@@ -53,7 +53,15 @@ If you wish to work on the provider, you'll first need [Go](http://www.golang.or
To compile the provider, run `go install`. This will build the provider and put the provider binary in the `$GOPATH/bin` directory.
In order to run the full suite of Acceptance tests, run `make testacc`.
In order to run the full suite of Acceptance tests,
a postgres docker container must be started first:
```sh
$ go mod download # ensure boundary is installed, files are used by the docker image
$ make test-database-up
```
Once the test database is ready the tests can be run using `make testacc`.
*Note:* Acceptance tests create real resources, and often cost money to run.
@@ -61,6 +69,9 @@ In order to run the full suite of Acceptance tests, run `make testacc`.
$ make testacc
```
For more details on the docker image and troubleshooting see the
[boundary testing doc](https://github.com/hashicorp/boundary/blob/main/CONTRIBUTING.md#testing).
Generating Docs
----------------------
+1 -1
View File
@@ -1,7 +1,7 @@
#!/bin/bash
set -e
VERSION="1.16"
VERSION="1.17.5"
[ -z "$GOROOT" ] && GOROOT="$HOME/.go"
[ -z "$GOPATH" ] && GOPATH="$HOME/go"
+1
View File
@@ -35,4 +35,5 @@ provider "boundary" {
- **password_auth_method_login_name** (String) The auth method login name for password-style auth methods
- **password_auth_method_password** (String) The auth method password for password-style auth methods
- **recovery_kms_hcl** (String) Can be a heredoc string or a path on disk. If set, the string/file will be parsed as HCL and used with the recovery KMS mechanism. While this is set, it will override any other authentication information; the KMS mechanism will always be used. See Boundary's KMS docs for examples: https://boundaryproject.io/docs/configuration/kms
- **tls_insecure** (Boolean) When set to true, does not validate the Boundary API endpoint certificate
- **token** (String) The Boundary token to use, as a string or path on disk containing just the string. If set, the token read here will be used in place of authenticating with the auth method specified in "auth_method_id", although the recovery KMS mechanism will still override this. Can also be set with the BOUNDARY_TOKEN environment variable.
+1 -1
View File
@@ -50,7 +50,7 @@ resource "boundary_credential_library_vault" "bar" {
credential_store_id = boundary_credential_store_vault.foo.id
path = "my/secret/bar" # change to Vault backend path
http_method = "POST"
request_body = <<EOT
http_request_body = <<EOT
{
"key": "Value",
}
+14 -7
View File
@@ -3,12 +3,12 @@
page_title: "boundary_host Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The host resource allows you to configure a Boundary static host. Hosts are always part of a project, so a project resource should be used inline or you should have the project ID in hand to successfully configure a host.
Deprecated: use resource_host_static instead.
---
# boundary_host (Resource)
The host resource allows you to configure a Boundary static host. Hosts are always part of a project, so a project resource should be used inline or you should have the project ID in hand to successfully configure a host.
Deprecated: use `resource_host_static` instead.
## Example Usage
@@ -28,12 +28,19 @@ resource "boundary_scope" "project" {
auto_create_admin_role = true
}
resource "boundary_host" "example" {
name = "example_host"
description = "My first host!"
address = "10.0.0.1"
resource "boundary_host_catalog" "static" {
name = "My catalog"
description = "My first host catalog!"
type = "static"
scope_id = boundary_scope.project.id
}
resource "boundary_host" "example" {
name = "example_host"
description = "My first host!"
address = "10.0.0.1"
host_catalog_id = boundary_host_catalog.static.id
}
```
<!-- schema generated by tfplugindocs -->
@@ -42,7 +49,7 @@ resource "boundary_host" "example" {
### Required
- **host_catalog_id** (String)
- **type** (String)
- **type** (String) The type of host
### Optional
+2 -2
View File
@@ -3,12 +3,12 @@
page_title: "boundary_host_catalog Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The host catalog resource allows you to configure a Boundary host catalog. Host catalogs are always part of a project, so a project resource should be used inline or you should have the project ID in hand to successfully configure a host catalog.
Deprecated: use resource_host_catalog_static instead.
---
# boundary_host_catalog (Resource)
The host catalog resource allows you to configure a Boundary host catalog. Host catalogs are always part of a project, so a project resource should be used inline or you should have the project ID in hand to successfully configure a host catalog.
Deprecated: use `resource_host_catalog_static` instead.
## Example Usage
+39
View File
@@ -0,0 +1,39 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "boundary_host_catalog_plugin Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The host catalog resource allows you to configure a Boundary plugin-type host catalog. Host catalogs are always part of a project, so a project resource should be used inline or you should have the project ID in hand to successfully configure a host catalog.
---
# boundary_host_catalog_plugin (Resource)
The host catalog resource allows you to configure a Boundary plugin-type host catalog. Host catalogs are always part of a project, so a project resource should be used inline or you should have the project ID in hand to successfully configure a host catalog.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- **scope_id** (String) The scope ID in which the resource is created.
### Optional
- **attributes_json** (String) The attributes for the host catalog. Either values encoded with the "jsonencode" function, pre-escaped JSON string, or a file:// or env:// path. Set to a string "null" or remove the block to clear all attributes in the host catalog.
- **description** (String) The host catalog description.
- **internal_force_update** (String) Internal only. Used to force update so that we can always check the value of secrets.
- **internal_hmac_used_for_secrets_config_hmac** (String) Internal only. The Boundary-provided HMAC used to calculate the current value of the HMAC'd config. Used for drift detection.
- **internal_secrets_config_hmac** (String) Internal only. HMAC of (serverSecretsHmac + config secrets). Used for proper secrets handling.
- **name** (String) The host catalog name. Defaults to the resource name.
- **plugin_id** (String) The ID of the plugin that should back the resource. This or plugin_name must be defined.
- **plugin_name** (String) The name of the plugin that should back the resource. This or plugin_id must be defined.
- **secrets_hmac** (String) The HMAC'd secrets value returned from the server.
- **secrets_json** (String, Sensitive) The secrets for the host catalog. Either values encoded with the "jsonencode" function, pre-escaped JSON string, or a file:// or env:// path. Set to a string "null" to clear any existing values. NOTE: Unlike "attributes_json", removing this block will NOT clear secrets from the host catalog; this allows injecting secrets for one call, then removing them for storage.
### Read-Only
- **id** (String) The ID of the host catalog.
+31
View File
@@ -0,0 +1,31 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "boundary_host_catalog_static Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The static host catalog resource allows you to configure a Boundary static-type host catalog. Host catalogs are always part of a project, so a project resource should be used inline or you should have the project ID in hand to successfully configure a host catalog.
---
# boundary_host_catalog_static (Resource)
The static host catalog resource allows you to configure a Boundary static-type host catalog. Host catalogs are always part of a project, so a project resource should be used inline or you should have the project ID in hand to successfully configure a host catalog.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- **scope_id** (String) The scope ID in which the resource is created.
### Optional
- **description** (String) The host catalog description.
- **name** (String) The host catalog name. Defaults to the resource name.
### Read-Only
- **id** (String) The ID of the host catalog.
+8 -7
View File
@@ -3,12 +3,12 @@
page_title: "boundary_host_set Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The host_set resource allows you to configure a Boundary host set. Host sets are always part of a host catalog, so a host catalog resource should be used inline or you should have the host catalog ID in hand to successfully configure a host set.
Deprecated: use resource_host_set_static instead.
---
# boundary_host_set (Resource)
The host_set resource allows you to configure a Boundary host set. Host sets are always part of a host catalog, so a host catalog resource should be used inline or you should have the host catalog ID in hand to successfully configure a host set.
Deprecated: use `resource_host_set_static` instead.
## Example Usage
@@ -50,6 +50,7 @@ resource "boundary_host" "2" {
resource "boundary_host_set" "web" {
host_catalog_id = boundary_host_catalog.static.id
type = "static"
host_ids = [
boundary_host.1.id,
boundary_host.2.id,
@@ -62,18 +63,18 @@ resource "boundary_host_set" "web" {
### Required
- **host_catalog_id** (String) The catalog for the hostset.
- **type** (String)
- **host_catalog_id** (String) The catalog for the host set.
- **type** (String) The type of host set
### Optional
- **description** (String) The hostset description.
- **description** (String) The host set description.
- **host_ids** (Set of String) The list of host IDs contained in this set.
- **name** (String) The hostset name. Defaults to the resource name.
- **name** (String) The host set name. Defaults to the resource name.
### Read-Only
- **id** (String) The ID of the hostset.
- **id** (String) The ID of the host set.
## Import
+35
View File
@@ -0,0 +1,35 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "boundary_host_set_plugin Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The hostsetplugin resource allows you to configure a Boundary host set. Host sets are always part of a host catalog, so a host catalog resource should be used inline or you should have the host catalog ID in hand to successfully configure a host set.
---
# boundary_host_set_plugin (Resource)
The host_set_plugin resource allows you to configure a Boundary host set. Host sets are always part of a host catalog, so a host catalog resource should be used inline or you should have the host catalog ID in hand to successfully configure a host set.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- **host_catalog_id** (String) The catalog for the host set.
### Optional
- **attributes_json** (String) The attributes for the host set. Either values encoded with the "jsonencode" function, pre-escaped JSON string, or a file:// or env:// path. Set to a string "null" or remove the block to clear all attributes in the host set.
- **description** (String) The host set description.
- **name** (String) The host set name. Defaults to the resource name.
- **preferred_endpoints** (List of String) The ordered list of preferred endpoints.
- **sync_interval_seconds** (Number) The value to set for the sync interval seconds.
- **type** (String) The type of host set
### Read-Only
- **id** (String) The ID of the host set.
+33
View File
@@ -0,0 +1,33 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "boundary_host_set_static Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The hostsetstatic resource allows you to configure a Boundary host set. Host sets are always part of a host catalog, so a host catalog resource should be used inline or you should have the host catalog ID in hand to successfully configure a host set.
---
# boundary_host_set_static (Resource)
The host_set_static resource allows you to configure a Boundary host set. Host sets are always part of a host catalog, so a host catalog resource should be used inline or you should have the host catalog ID in hand to successfully configure a host set.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- **host_catalog_id** (String) The catalog for the host set.
### Optional
- **description** (String) The host set description.
- **host_ids** (Set of String) The list of host IDs contained in this set.
- **name** (String) The host set name. Defaults to the resource name.
- **type** (String) The type of host set
### Read-Only
- **id** (String) The ID of the host set.
+33
View File
@@ -0,0 +1,33 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "boundary_host_static Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The static host resource allows you to configure a Boundary static host. Hosts are always part of a project, so a project resource should be used inline or you should have the project ID in hand to successfully configure a host.
---
# boundary_host_static (Resource)
The static host resource allows you to configure a Boundary static host. Hosts are always part of a project, so a project resource should be used inline or you should have the project ID in hand to successfully configure a host.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- **host_catalog_id** (String)
### Optional
- **address** (String) The static address of the host resource as `<IP>` (note: port assignment occurs in the target resource definition, do not add :port here) or a domain name.
- **description** (String) The host description.
- **name** (String) The host name. Defaults to the resource name.
- **type** (String) The type of host
### Read-Only
- **id** (String) The ID of the host.
+32
View File
@@ -0,0 +1,32 @@
---
# generated by https://github.com/hashicorp/terraform-plugin-docs
page_title: "boundary_managed_group Resource - terraform-provider-boundary"
subcategory: ""
description: |-
The managed group resource allows you to configure a Boundary group.
---
# boundary_managed_group (Resource)
The managed group resource allows you to configure a Boundary group.
<!-- schema generated by tfplugindocs -->
## Schema
### Required
- **auth_method_id** (String) The resource ID for the auth method.
- **filter** (String) Boolean expression to filter the workers for this managed group.
### Optional
- **description** (String) The managed group description.
- **name** (String) The managed group name. Defaults to the resource name.
### Read-Only
- **id** (String) The ID of the group.
+14 -14
View File
@@ -51,10 +51,10 @@ resource "boundary_user" "bar" {
}
resource "boundary_role" "example" {
name = "My role"
description = "My first role!"
principals = [boundary_user.foo.id, boundary_user.bar.id]
scope_id = boundary_scope.org.id
name = "My role"
description = "My first role!"
principal_ids = [boundary_user.foo.id, boundary_user.bar.id]
scope_id = boundary_scope.org.id
}
```
@@ -76,11 +76,11 @@ resource "boundary_user" "readonly" {
}
resource "boundary_role" "readonly" {
name = "readonly"
description = "A readonly role"
principals = [boundary_user.readonly.id]
grants = ["id=*;action=read"]
scope_id = boundary_scope.org.id
name = "readonly"
description = "A readonly role"
principal_ids = [boundary_user.readonly.id]
grant_strings = ["id=*;type=*;actions=read"]
scope_id = boundary_scope.org.id
}
```
@@ -109,11 +109,11 @@ resource "boundary_user" "readonly" {
}
resource "boundary_role" "readonly" {
name = "readonly"
description = "A readonly role"
principals = [boundary_user.readonly.id]
grants = ["id=*;action=read"]
scope_id = boundary_scope.project.id
name = "readonly"
description = "A readonly role"
principal_ids = [boundary_user.readonly.id]
grant_strings = ["id=*;type=*;actions=read"]
scope_id = boundary_scope.project.id
}
```
+5 -4
View File
@@ -83,12 +83,13 @@ resource "boundary_host_set" "foo" {
resource "boundary_target" "foo" {
name = "foo"
description = "Foo target"
type = "tcp"
default_port = "22"
scope_id = boundary_scope.project.id
host_set_ids = [
host_source_ids = [
boundary_host_set.foo.id
]
application_credential_library_ids = [
application_credential_source_ids = [
boundary_credential_library_vault.foo.id
]
}
@@ -104,10 +105,10 @@ resource "boundary_target" "foo" {
### Optional
- **application_credential_library_ids** (Set of String) A list of application credential library ID's.
- **application_credential_source_ids** (Set of String) A list of application credential source ID's.
- **default_port** (Number) The default port for this target.
- **description** (String) The target description.
- **host_set_ids** (Set of String) A list of host set ID's.
- **host_source_ids** (Set of String) A list of host source ID's.
- **name** (String) The target name. Defaults to the resource name.
- **session_connection_limit** (Number)
- **session_max_seconds** (Number)
@@ -35,7 +35,7 @@ resource "boundary_credential_library_vault" "bar" {
credential_store_id = boundary_credential_store_vault.foo.id
path = "my/secret/bar" # change to Vault backend path
http_method = "POST"
request_body = <<EOT
http_request_body = <<EOT
{
"key": "Value",
}
+11 -4
View File
@@ -13,9 +13,16 @@ resource "boundary_scope" "project" {
auto_create_admin_role = true
}
resource "boundary_host" "example" {
name = "example_host"
description = "My first host!"
address = "10.0.0.1"
resource "boundary_host_catalog" "static" {
name = "My catalog"
description = "My first host catalog!"
type = "static"
scope_id = boundary_scope.project.id
}
resource "boundary_host" "example" {
name = "example_host"
description = "My first host!"
address = "10.0.0.1"
host_catalog_id = boundary_host_catalog.static.id
}
@@ -35,6 +35,7 @@ resource "boundary_host" "2" {
resource "boundary_host_set" "web" {
host_catalog_id = boundary_host_catalog.static.id
type = "static"
host_ids = [
boundary_host.1.id,
boundary_host.2.id,
@@ -20,9 +20,9 @@ resource "boundary_user" "readonly" {
}
resource "boundary_role" "readonly" {
name = "readonly"
description = "A readonly role"
principals = [boundary_user.readonly.id]
grants = ["id=*;action=read"]
scope_id = boundary_scope.project.id
name = "readonly"
description = "A readonly role"
principal_ids = [boundary_user.readonly.id]
grant_strings = ["id=*;type=*;actions=read"]
scope_id = boundary_scope.project.id
}
@@ -13,9 +13,9 @@ resource "boundary_user" "readonly" {
}
resource "boundary_role" "readonly" {
name = "readonly"
description = "A readonly role"
principals = [boundary_user.readonly.id]
grants = ["id=*;action=read"]
scope_id = boundary_scope.org.id
name = "readonly"
description = "A readonly role"
principal_ids = [boundary_user.readonly.id]
grant_strings = ["id=*;type=*;actions=read"]
scope_id = boundary_scope.org.id
}
@@ -17,8 +17,8 @@ resource "boundary_user" "bar" {
}
resource "boundary_role" "example" {
name = "My role"
description = "My first role!"
principals = [boundary_user.foo.id, boundary_user.bar.id]
scope_id = boundary_scope.org.id
name = "My role"
description = "My first role!"
principal_ids = [boundary_user.foo.id, boundary_user.bar.id]
scope_id = boundary_scope.org.id
}
@@ -68,12 +68,13 @@ resource "boundary_host_set" "foo" {
resource "boundary_target" "foo" {
name = "foo"
description = "Foo target"
type = "tcp"
default_port = "22"
scope_id = boundary_scope.project.id
host_set_ids = [
host_source_ids = [
boundary_host_set.foo.id
]
application_credential_library_ids = [
application_credential_source_ids = [
boundary_credential_library_vault.foo.id
]
}
+199 -28
View File
@@ -1,35 +1,206 @@
module github.com/hashicorp/terraform-provider-boundary
go 1.16
go 1.17
require (
cloud.google.com/go v0.68.0 // indirect
cloud.google.com/go/storage v1.11.0 // indirect
github.com/Azure/azure-sdk-for-go v46.4.0+incompatible // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.3 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v1.61.545 // indirect
github.com/aws/aws-sdk-go v1.35.4 // indirect
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe // indirect
github.com/golang/snappy v0.0.2 // indirect
github.com/hashicorp/boundary v0.5.1
github.com/hashicorp/boundary/api v0.0.15
github.com/hashicorp/boundary/sdk v0.0.7
github.com/hashicorp/boundary v0.7.4
github.com/hashicorp/boundary/api v0.0.21
github.com/hashicorp/boundary/sdk v0.0.13
github.com/hashicorp/cap v0.1.1
github.com/hashicorp/go-immutable-radix v1.3.0 // indirect
github.com/hashicorp/go-kms-wrapping v0.6.5
github.com/hashicorp/golang-lru v0.5.4 // indirect
github.com/hashicorp/hcl/v2 v2.6.0 // indirect
github.com/hashicorp/terraform-plugin-docs v0.4.0
github.com/hashicorp/terraform-plugin-sdk/v2 v2.7.0
github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce // indirect
github.com/hashicorp/go-cty v1.4.1-0.20200723130312-85980079f637
github.com/hashicorp/go-kms-wrapping v0.6.8
github.com/hashicorp/go-secure-stdlib/parseutil v0.1.2
github.com/hashicorp/terraform-plugin-docs v0.5.1
github.com/hashicorp/terraform-plugin-sdk/v2 v2.10.1
github.com/kr/pretty v0.3.0
github.com/mitchellh/go-homedir v1.1.0
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/oklog/run v1.1.0 // indirect
github.com/oracle/oci-go-sdk v24.3.0+incompatible // indirect
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97
google.golang.org/appengine v1.6.7 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
github.com/stretchr/testify v1.7.0
golang.org/x/crypto v0.0.0-20211215153901-e495a2d5b3d3
)
require (
cloud.google.com/go v0.81.0 // indirect
cloud.google.com/go/storage v1.10.0 // indirect
github.com/Azure/azure-sdk-for-go v58.0.0+incompatible // indirect
github.com/Azure/go-ansiterm v0.0.0-20170929234023-d6e3b3328b78 // indirect
github.com/Azure/go-autorest v14.2.0+incompatible // indirect
github.com/Azure/go-autorest/autorest v0.11.21 // indirect
github.com/Azure/go-autorest/autorest/adal v0.9.14 // indirect
github.com/Azure/go-autorest/autorest/azure/auth v0.5.8 // indirect
github.com/Azure/go-autorest/autorest/azure/cli v0.4.2 // indirect
github.com/Azure/go-autorest/autorest/date v0.3.0 // indirect
github.com/Azure/go-autorest/autorest/to v0.4.0 // indirect
github.com/Azure/go-autorest/autorest/validation v0.3.1 // indirect
github.com/Azure/go-autorest/logger v0.2.1 // indirect
github.com/Azure/go-autorest/tracing v0.6.0 // indirect
github.com/Masterminds/goutils v1.1.1 // indirect
github.com/Masterminds/semver v1.5.0 // indirect
github.com/Masterminds/sprig v2.22.0+incompatible // indirect
github.com/Microsoft/go-winio v0.4.16 // indirect
github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect
github.com/agext/levenshtein v1.2.2 // indirect
github.com/aliyun/alibaba-cloud-sdk-go v0.0.0-20190620160927-9418d7b0cd0f // indirect
github.com/apparentlymart/go-textseg v1.0.0 // indirect
github.com/apparentlymart/go-textseg/v13 v13.0.0 // indirect
github.com/armon/go-metrics v0.3.9 // indirect
github.com/armon/go-radix v1.0.0 // indirect
github.com/aws/aws-sdk-go v1.40.55 // indirect
github.com/bgentry/go-netrc v0.0.0-20140422174119-9fd32a8b3d3d // indirect
github.com/bgentry/speakeasy v0.1.0 // indirect
github.com/cenkalti/backoff/v3 v3.0.0 // indirect
github.com/cenkalti/backoff/v4 v4.1.0 // indirect
github.com/containerd/containerd v1.4.3 // indirect
github.com/containerd/continuity v0.0.0-20200709052629-daa8e1ccc0bc // indirect
github.com/coreos/go-oidc/v3 v3.0.0 // indirect
github.com/danieljoos/wincred v1.1.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/dimchansky/utfbom v1.1.1 // indirect
github.com/docker/cli v20.10.7+incompatible // indirect
github.com/docker/docker v20.10.7+incompatible // indirect
github.com/docker/go-connections v0.4.0 // indirect
github.com/docker/go-units v0.4.0 // indirect
github.com/dvsekhvalnov/jose2go v0.0.0-20200901110807-248326c1351b // indirect
github.com/fatih/color v1.13.0 // indirect
github.com/fatih/structs v1.1.0 // indirect
github.com/form3tech-oss/jwt-go v3.2.2+incompatible // indirect
github.com/godbus/dbus/v5 v5.0.4 // indirect
github.com/gogo/protobuf v1.3.2 // indirect
github.com/golang-migrate/migrate/v4 v4.14.1 // indirect
github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e // indirect
github.com/golang/protobuf v1.5.2 // indirect
github.com/golang/snappy v0.0.1 // indirect
github.com/google/go-cmp v0.5.6 // indirect
github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 // indirect
github.com/google/uuid v1.3.0 // indirect
github.com/googleapis/gax-go/v2 v2.0.5 // indirect
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.6.0 // indirect
github.com/hashicorp/dbassert v0.0.0-20210708202608-ecf920cf1ed8 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/eventlogger v0.1.1-0.20211106154408-4ff8da3a890c // indirect
github.com/hashicorp/eventlogger/filters/encrypt v0.1.6-0.20211027211326-5db60a48f239 // indirect
github.com/hashicorp/go-bexpr v0.1.10 // indirect
github.com/hashicorp/go-checkpoint v0.5.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-dbw v0.0.0-20211215222256-2ff0d37184ff // indirect
github.com/hashicorp/go-getter v1.5.3 // indirect
github.com/hashicorp/go-hclog v1.0.0 // indirect
github.com/hashicorp/go-immutable-radix v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-plugin v1.4.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.0 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-safetemp v1.0.0 // indirect
github.com/hashicorp/go-secure-stdlib/awsutil v0.1.6 // indirect
github.com/hashicorp/go-secure-stdlib/base62 v0.1.2 // indirect
github.com/hashicorp/go-secure-stdlib/configutil v0.1.2 // indirect
github.com/hashicorp/go-secure-stdlib/gatedwriter v0.1.1 // indirect
github.com/hashicorp/go-secure-stdlib/kv-builder v0.1.1 // indirect
github.com/hashicorp/go-secure-stdlib/listenerutil v0.1.3-0.20211129134915-8f7e6fb52622 // indirect
github.com/hashicorp/go-secure-stdlib/mlock v0.1.1 // indirect
github.com/hashicorp/go-secure-stdlib/reloadutil v0.1.1 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-secure-stdlib/tlsutil v0.1.1 // indirect
github.com/hashicorp/go-sockaddr v1.0.2 // indirect
github.com/hashicorp/go-uuid v1.0.2 // indirect
github.com/hashicorp/go-version v1.3.0 // indirect
github.com/hashicorp/golang-lru v0.5.3 // indirect
github.com/hashicorp/hc-install v0.3.1 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hashicorp/hcl/v2 v2.3.0 // indirect
github.com/hashicorp/logutils v1.0.0 // indirect
github.com/hashicorp/terraform-exec v0.15.0 // indirect
github.com/hashicorp/terraform-json v0.13.0 // indirect
github.com/hashicorp/terraform-plugin-go v0.5.0 // indirect
github.com/hashicorp/terraform-plugin-log v0.2.0 // indirect
github.com/hashicorp/terraform-registry-address v0.0.0-20210412075316-9b2996cce896 // indirect
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect
github.com/hashicorp/vault/api v1.1.1 // indirect
github.com/hashicorp/vault/sdk v0.2.1 // indirect
github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect
github.com/huandu/xstrings v1.3.2 // indirect
github.com/imdario/mergo v0.3.12 // indirect
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
github.com/jackc/pgconn v1.10.1 // indirect
github.com/jackc/pgio v1.0.0 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgproto3/v2 v2.2.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/pgtype v1.9.0 // indirect
github.com/jackc/pgx/v4 v4.14.0 // indirect
github.com/jefferai/go-libsecret v0.0.0-20210105015933-d08a58b018bc // indirect
github.com/jefferai/isbadcipher v0.0.0-20190226160619-51d2077c035f // indirect
github.com/jefferai/keyring v1.1.7-0.20210105022822-8749b3d9ce79 // indirect
github.com/jinzhu/gorm v1.9.12 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.3 // indirect
github.com/jmespath/go-jmespath v0.4.0 // indirect
github.com/json-iterator/go v1.1.11 // indirect
github.com/jstemmer/go-junit-report v0.9.1 // indirect
github.com/keybase/go-keychain v0.0.0-20190712205309-48d3d31d256d // indirect
github.com/klauspost/compress v1.13.5 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/lib/pq v1.10.2 // indirect
github.com/mattn/go-colorable v0.1.12 // indirect
github.com/mattn/go-isatty v0.0.14 // indirect
github.com/mattn/go-sqlite3 v2.0.1+incompatible // indirect
github.com/mitchellh/cli v1.1.2 // indirect
github.com/mitchellh/copystructure v1.2.0 // indirect
github.com/mitchellh/go-testing-interface v1.14.1 // indirect
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
github.com/mitchellh/mapstructure v1.4.2 // indirect
github.com/mitchellh/pointerstructure v1.2.0 // indirect
github.com/mitchellh/reflectwalk v1.0.2 // indirect
github.com/moby/term v0.0.0-20201216013528-df9cb8a40635 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.1 // indirect
github.com/mr-tron/base58 v1.2.0 // indirect
github.com/mtibben/percent v0.2.1 // indirect
github.com/oklog/run v1.0.0 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect
github.com/opencontainers/image-spec v1.0.1 // indirect
github.com/opencontainers/runc v1.0.0-rc9 // indirect
github.com/oracle/oci-go-sdk v13.1.0+incompatible // indirect
github.com/ory/dockertest/v3 v3.7.0 // indirect
github.com/pierrec/lz4 v2.5.2+incompatible // indirect
github.com/pires/go-proxyproto v0.6.1 // indirect
github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/posener/complete v1.2.3 // indirect
github.com/rogpeppe/go-internal v1.8.1-0.20211023094830-115ce09fd6b4 // indirect
github.com/russross/blackfriday v1.6.0 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/sirupsen/logrus v1.7.0 // indirect
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect
github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect
github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect
github.com/xeipuuv/gojsonschema v1.2.0 // indirect
github.com/xo/dburl v0.9.0 // indirect
github.com/zalando/go-keyring v0.1.1 // indirect
github.com/zclconf/go-cty v1.10.0 // indirect
go.opencensus.io v0.23.0 // indirect
go.uber.org/atomic v1.9.0 // indirect
golang.org/x/lint v0.0.0-20210508222113-6edffad5e616 // indirect
golang.org/x/mod v0.5.1 // indirect
golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2 // indirect
golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f // indirect
golang.org/x/sys v0.0.0-20211210111614-af8b64212486 // indirect
golang.org/x/term v0.0.0-20210916214954-140adaaadfaf // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/time v0.0.0-20200630173020-3af7569d3a1e // indirect
golang.org/x/tools v0.1.8-0.20211102182255-bb4add04ddef // indirect
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 // indirect
google.golang.org/api v0.44.0 // indirect
google.golang.org/appengine v1.6.7 // indirect
google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83 // indirect
google.golang.org/grpc v1.41.0-dev.0.20210907181116-2f3355d2244e // indirect
google.golang.org/protobuf v1.27.1 // indirect
gopkg.in/ini.v1 v1.62.0 // indirect
gopkg.in/square/go-jose.v2 v2.5.1 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
gorm.io/driver/postgres v1.2.2 // indirect
gorm.io/driver/sqlite v1.2.4 // indirect
gorm.io/gorm v1.22.3 // indirect
)
+292 -300
View File
File diff suppressed because it is too large. Load diff
+26
View File
@@ -15,4 +15,30 @@ const (
HostCatalogIdKey = "host_catalog_id"
// AuthMethodIdKey is used for common "auth_method_id" resource attribute
AuthMethodIdKey = "auth_method_id"
// PluginIdKey is used for common "plugin_id" resource attribute
PluginIdKey = "plugin_id"
// PluginNameKey is used for common "plugin_name" resource attribute
PluginNameKey = "plugin_name"
// AttributesJsonKey is used for setting attributes and corresponds to the
// API "attributes" key
AttributesJsonKey = "attributes_json"
// SecretsJsonKey is used for setting secrets and corresponds to the API
// "secrets" key
SecretsJsonKey = "secrets_json"
// SecretsHmacKey is a read-only key used for ensuring we detect if secrets
// have changed
SecretsHmacKey = "secrets_hmac"
// PreferredEndpointsKey is used for setting preferred endpoints
PreferredEndpointsKey = "preferred_endpoints"
// SyncIntervalSecondsKey is used for setting the interval seconds
SyncIntervalSecondsKey = "sync_interval_seconds"
// internalSecretsConfigHmacKey is used for storing an hmac of hmac from server +
// config string
internalSecretsConfigHmacKey = "internal_secrets_config_hmac"
// internalHmacUsedForSecretsConfigHmacKey is used for storing the server-provided
// hmac used when calculating the current value of secretsConfigHmacKey
internalHmacUsedForSecretsConfigHmacKey = "internal_hmac_used_for_secrets_config_hmac"
// internalForceUpdateKey is used to force updates so we can always check
// the value of secrets
internalForceUpdateKey = "internal_force_update"
)
+17 -1
View File
@@ -52,6 +52,11 @@ func New() *schema.Provider {
Optional: true,
Description: "The auth method password for password-style auth methods",
},
"tls_insecure": {
Type: schema.TypeBool,
Optional: true,
Description: "When set to true, does not validate the Boundary API endpoint certificate",
},
},
ResourcesMap: map[string]*schema.Resource{
"boundary_account": resourceAccount(),
@@ -65,8 +70,13 @@ func New() *schema.Provider {
"boundary_managed_group": resourceManagedGroup(),
"boundary_group": resourceGroup(),
"boundary_host": resourceHost(),
"boundary_host_static": resourceHostStatic(),
"boundary_host_catalog": resourceHostCatalog(),
"boundary_host_set": resourceHostset(),
"boundary_host_catalog_static": resourceHostCatalogStatic(),
"boundary_host_catalog_plugin": resourceHostCatalogPlugin(),
"boundary_host_set": resourceHostSet(),
"boundary_host_set_static": resourceHostSetStatic(),
"boundary_host_set_plugin": resourceHostSetPlugin(),
"boundary_role": resourceRole(),
"boundary_scope": resourceScope(),
"boundary_target": resourceTarget(),
@@ -169,6 +179,12 @@ func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc {
return nil, diag.Errorf(`"no valid address could be determined from "addr" or "BOUNDARY_ADDR" env var`)
}
if tlsInsecure, ok := d.GetOk("tls_insecure"); ok {
if client.SetTLSConfig(&api.TLSConfig{Insecure: tlsInsecure.(bool)}) != nil {
return nil, diag.Errorf("could not set insecure tls")
}
}
client.SetLimiter(5, 5)
md := &metaData{
-216
View File
@@ -1,216 +0,0 @@
package provider
import (
"context"
"net/http"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/hostcatalogs"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
hostCatalogTypeStatic = "static"
)
func resourceHostCatalog() *schema.Resource {
return &schema.Resource{
Description: "The host catalog resource allows you to configure a Boundary host catalog. Host " +
"catalogs are always part of a project, so a project resource should be used inline or you " +
"should have the project ID in hand to successfully configure a host catalog.",
CreateContext: resourceHostCatalogCreate,
ReadContext: resourceHostCatalogRead,
UpdateContext: resourceHostCatalogUpdate,
DeleteContext: resourceHostCatalogDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of the host catalog.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The host catalog name. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The host catalog description.",
Type: schema.TypeString,
Optional: true,
},
ScopeIdKey: {
Description: "The scope ID in which the resource is created.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
TypeKey: {
Description: "The host catalog type. Only `static` is supported.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func setFromHostCatalogResponseMap(d *schema.ResourceData, raw map[string]interface{}) error {
if err := d.Set(NameKey, raw["name"]); err != nil {
return err
}
if err := d.Set(DescriptionKey, raw["description"]); err != nil {
return err
}
if err := d.Set(ScopeIdKey, raw["scope_id"]); err != nil {
return err
}
if err := d.Set(TypeKey, raw["type"]); err != nil {
return err
}
d.SetId(raw["id"].(string))
return nil
}
func resourceHostCatalogCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
var scopeId string
if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok {
scopeId = scopeIdVal.(string)
} else {
return diag.Errorf("no scope ID provided")
}
var typeStr string
if typeVal, ok := d.GetOk(TypeKey); ok {
typeStr = typeVal.(string)
} else {
return diag.Errorf("no type provided")
}
switch typeStr {
case hostCatalogTypeStatic:
default:
return diag.Errorf("invalid type provided")
}
opts := []hostcatalogs.Option{}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, hostcatalogs.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, hostcatalogs.WithDescription(descStr))
}
hcClient := hostcatalogs.NewClient(md.client)
hccr, err := hcClient.Create(ctx, typeStr, scopeId, opts...)
if err != nil {
return diag.Errorf("error creating host catalog: %v", err)
}
if hccr == nil {
return diag.Errorf("nil host catalog after create")
}
if err := setFromHostCatalogResponseMap(d, hccr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
return nil
}
func resourceHostCatalogRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
hcrr, err := hcClient.Read(ctx, d.Id())
if err != nil {
if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound {
d.SetId("")
return nil
}
return diag.Errorf("error reading host catalog: %v", err)
}
if hcrr == nil {
return diag.Errorf("host catalog nil after read")
}
if err := setFromHostCatalogResponseMap(d, hcrr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
return nil
}
func resourceHostCatalogUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
opts := []hostcatalogs.Option{}
var name *string
if d.HasChange(NameKey) {
opts = append(opts, hostcatalogs.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, hostcatalogs.WithName(nameStr))
}
}
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, hostcatalogs.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, hostcatalogs.WithDescription(descStr))
}
}
if len(opts) > 0 {
opts = append(opts, hostcatalogs.WithAutomaticVersioning(true))
_, err := hcClient.Update(ctx, d.Id(), 0, opts...)
if err != nil {
return diag.Errorf("error updating host catalog: %v", err)
}
}
if d.HasChange(NameKey) {
if err := d.Set(NameKey, name); err != nil {
return diag.FromErr(err)
}
}
if d.HasChange(DescriptionKey) {
if err := d.Set(DescriptionKey, desc); err != nil {
return diag.FromErr(err)
}
}
return nil
}
func resourceHostCatalogDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
_, err := hcClient.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error deleting host catalog: %v", err)
}
return nil
}
@@ -0,0 +1,648 @@
package provider
import (
"context"
"crypto/hmac"
"crypto/sha256"
"encoding/base64"
"encoding/json"
"errors"
"fmt"
"math/rand"
"net/http"
"strconv"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/hostcatalogs"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"golang.org/x/crypto/blake2b"
)
const (
hostCatalogTypePlugin = "plugin"
)
func resourceHostCatalogPlugin() *schema.Resource {
return &schema.Resource{
Description: "The host catalog resource allows you to configure a Boundary plugin-type host catalog. Host " +
"catalogs are always part of a project, so a project resource should be used inline or you " +
"should have the project ID in hand to successfully configure a host catalog.",
CreateContext: resourceHostCatalogPluginCreate,
ReadContext: resourceHostCatalogPluginRead,
UpdateContext: resourceHostCatalogPluginUpdate,
DeleteContext: resourceHostCatalogPluginDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of the host catalog.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The host catalog name. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The host catalog description.",
Type: schema.TypeString,
Optional: true,
},
ScopeIdKey: {
Description: "The scope ID in which the resource is created.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
PluginIdKey: {
Description: "The ID of the plugin that should back the resource. This or " + PluginNameKey + " must be defined.",
Type: schema.TypeString,
ConflictsWith: []string{PluginNameKey},
Optional: true,
ForceNew: true,
Computed: true, // If name is provided this will be computed
},
PluginNameKey: {
Description: "The name of the plugin that should back the resource. This or " + PluginIdKey + " must be defined.",
Type: schema.TypeString,
ConflictsWith: []string{PluginIdKey},
Optional: true,
ForceNew: true,
Computed: true,
},
AttributesJsonKey: {
Description: `The attributes for the host catalog. Either values encoded with the "jsonencode" function, pre-escaped JSON string, ` +
`or a file:// or env:// path. Set to a string "null" or remove the block to clear all attributes in the host catalog.`,
Type: schema.TypeString,
Optional: true,
// If set to null in config and nothing comes from API, consider
// it the same. Same if config changes from empty to null.
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
sanitizedNew, err := sanitizeJson(new)
if err != nil {
return false
}
new = string(sanitizedNew)
switch {
case old == new:
return true
case old == "null" && new == "":
return true
case old == "" && new == "null":
return true
default:
return false
}
},
},
SecretsJsonKey: {
Description: `The secrets for the host catalog. Either values encoded with the "jsonencode" function, pre-escaped JSON string, ` +
`or a file:// or env:// path. Set to a string "null" to clear any existing values. NOTE: Unlike "attributes_json", removing ` +
`this block will NOT clear secrets from the host catalog; this allows injecting secrets for one call, then removing them for storage.`,
Type: schema.TypeString,
Optional: true,
Sensitive: true,
},
SecretsHmacKey: {
Description: "The HMAC'd secrets value returned from the server.",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
internalSecretsConfigHmacKey: {
Description: "Internal only. HMAC of (serverSecretsHmac + config secrets). Used for proper secrets handling.",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
internalHmacUsedForSecretsConfigHmacKey: {
Description: "Internal only. The Boundary-provided HMAC used to calculate the current value of the HMAC'd config. Used for drift detection.",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
internalForceUpdateKey: {
Description: "Internal only. Used to force update so that we can always check the value of secrets.",
Type: schema.TypeString,
Optional: true,
Computed: true,
},
},
// We want to always force an update (which itself may not actually do
// anything) so that we can properly check secrets state.
CustomizeDiff: func(_ context.Context, d *schema.ResourceDiff, _ interface{}) error {
return d.SetNewComputed(internalForceUpdateKey)
},
}
}
func sanitizeJson(in string) ([]byte, error) {
var v interface{}
if err := json.Unmarshal([]byte(in), &v); err != nil {
return nil, err
}
// Remarshal so we sanitize the order
out, err := json.Marshal(v)
if err != nil {
return nil, err
}
return out, nil
}
// calculateCurrentConfigHmac generates an HMAC'd config value using the
// server's calculated HMAC as an HMAC key. Prior to calculating this we parse
// and then re-marshal the JSON to take advantage of Go alphabetizing JSON
// output so that we can ensure we'll treat the same objects as equivalent
// regardless of initial input order.
func calculateCurrentConfigHmac(serverHmac, secretsStr string) (string, error) {
jsonBytes, err := sanitizeJson(secretsStr)
if err != nil {
return "", err
}
key := blake2b.Sum256([]byte(serverHmac))
mac := hmac.New(sha256.New, key[:])
_, err = mac.Write(jsonBytes)
if err != nil {
return "", err
}
hmac := mac.Sum(nil)
return base64.StdEncoding.EncodeToString(hmac), nil
}
// calculateConfigHmacPlan, given the schema and the current server HMAC,
// returns what to set on the server (if anything) and any diagnostics. If
// clearState is set we should nil out existing values in state. If
// sendToBoundary is set then the read secrets_json should be sent in an API
// call.
func calculateConfigHmacPlan(serverHmac, secretsJson string, d *schema.ResourceData) (clearState, sendToBoundary bool, diagWarn *diag.Diagnostic, retErr error) {
stateConfigHmac := d.Get(internalSecretsConfigHmacKey).(string)
// Iterate through possible states and handle appropriately
switch {
case serverHmac == "" && secretsJson == "":
// State 1: No configured secrets in either Boundary or TF. Clear HMAC'd
// config if present.
return true, false, nil, nil
case serverHmac == "" && secretsJson != "":
// State 2: Boundary has no secrets but TF does. Put secrets into
// Boundary. HMAC'd config will be updated after. However, if the value
// is "null" it's a no-op so just make sure HMAC'd config is cleared if
// present.
switch secretsJson {
case "null":
return true, false, nil, nil
default:
return false, true, nil, nil
}
case serverHmac != "" && secretsJson == "":
// State 3: Boundary has configured secrets but nothing in TF config. In
// this case we make a break from "normal" TF behavior; rather than
// interpret this as needing to clear secrets out of Boundary, we do
// nothing. That way the user is free to configure secrets directly in
// the API (thus never having it transport through TF or any of its
// parts or configs) or remove them from the TF config file once they're
// set. Note that we wipe knowledge of the HMAC'd config in this case,
// so if they re-add values (even the same ones) we'll hit state 4. This
// is the escape hatch for state 6a below.
return true, false, nil, nil
case serverHmac != "" && secretsJson != "" && stateConfigHmac == "":
// State 4: In this case Boundary has secrets and TF config has secrets,
// but we have no HMAC value at all in state. This almost certainly
// means that Boundary was configured before the Terraform configuration
// file was written, or at least prior to the value being added to TF's
// config, since running TF with config would generate an HMAC'd config
// value. We can assume that the TF config is new and meant as an update.
return false, true, nil, nil
case serverHmac != "" && secretsJson != "":
// At this point we need to calculated the current HMAC'd config value
// so we can see if it matches.
currentConfigHmac, err := calculateCurrentConfigHmac(serverHmac, secretsJson)
if err != nil {
return false, false, nil, err
}
if stateConfigHmac == currentConfigHmac {
// State 5: Both Boundary and TF have secrets configured and they
// are known to match, so do nothing.
return false, false, nil, nil
}
// The next bit is a little complicated. We have been storing not just
// the HMAC'd config but also the boundary HMAC value that was used to
// generate it. If that value has not changed, but the HMAC'd config
// has, it means that the Terraform configuration has changed (but
// Boundary has not been given new values since the last time we
// calculated from the TF config) and we can assume that it is new. If
// that value has changed, it is unclear whether or not Boundary's new
// values are correct or the existing config values are correct. Picking
// wrong could lead to a denial of service (e.g. if creds provided
// through TF were rotated and no longer valid) so instead we raise this
// as a warning to the user.
//
// The next question, however, is how does the user escape from this
// scenario? Simply changing TF config more won't work as we'll be right
// back here. The answer is state 3. If they remove the secrets we will
// wipe knowlege of the HMAC'd config from TF; when they add them back
// they'll hit state 4 and we'll put the new values in.
switch {
case serverHmac != d.Get(internalHmacUsedForSecretsConfigHmacKey).(string):
// State 6a: mismatch. Warn the user.
return false, false, &diag.Diagnostic{
Severity: diag.Warning,
Summary: "Mismatch in secrets state between Boundary and Terraform.",
Detail: `Boundary's secret state is out of sync with Terraform. Usually this is the result of secrets being provided ` +
`directly via Boundary's API. To suppress this warning, either remove the secrets via Boundary's API and allow Terraform to ` +
`repopulate them, or remove the secrets_json block from Terraform's configuration file (the next time you add secrets_json ` +
`back to the file, those values will be used to overwrite the current Boundary values.)`,
}, nil
default:
// State 6b: Boundary's HMAC hasn't changed from the one we used to
// generate the HMAC'd config, so we know the config has changed.
return false, true, nil, nil
}
default:
return false, false, nil, fmt.Errorf(
"unhandled secrets state; server hmac is found: %t; secrets detected in config: %t; existing hmac'd config: %t",
serverHmac != "", secretsJson != "", stateConfigHmac != "")
}
}
func setFromHostCatalogPluginResponseMap(d *schema.ResourceData, raw map[string]interface{}) error {
if err := d.Set(NameKey, raw[NameKey]); err != nil {
return err
}
if err := d.Set(DescriptionKey, raw[DescriptionKey]); err != nil {
return err
}
if err := d.Set(ScopeIdKey, raw[ScopeIdKey]); err != nil {
return err
}
// Plugin stuff
{
if err := d.Set(PluginIdKey, raw[PluginIdKey]); err != nil {
return err
}
pluginRaw, ok := raw["plugin"]
if !ok {
return fmt.Errorf("plugin field not found in response")
}
pluginInfo, ok := pluginRaw.(map[string]interface{})
if !ok {
return fmt.Errorf("plugin field in response has wrong type")
}
pluginNameRaw, ok := pluginInfo["name"]
if !ok {
return fmt.Errorf("plugin name field not found in response")
}
pluginName, ok := pluginNameRaw.(string)
if !ok {
return fmt.Errorf("plugin name field in response has wrong type")
}
if err := d.Set(PluginNameKey, pluginName); err != nil {
return err
}
}
// Attributes stuff
{
attrRaw, ok := raw["attributes"]
switch ok {
case true:
encodedAttributes, err := json.Marshal(attrRaw)
if err != nil {
return err
}
if err := d.Set(AttributesJsonKey, string(encodedAttributes)); err != nil {
return err
}
default:
d.Set(AttributesJsonKey, nil)
}
}
// Secrets stuff
{
// We do not save secrets into the state file, and they're not returned in
// the response
secretsHmacRaw, ok := raw[SecretsHmacKey]
switch ok {
case true:
if err := d.Set(SecretsHmacKey, secretsHmacRaw); err != nil {
return err
}
default:
d.Set(SecretsHmacKey, nil)
}
}
d.SetId(raw[IDKey].(string))
if err := d.Set(internalForceUpdateKey, strconv.FormatInt(rand.Int63(), 10)); err != nil {
return err
}
return nil
}
func resourceHostCatalogPluginCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
var scopeId string
if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok {
scopeId = scopeIdVal.(string)
} else {
return diag.Errorf("no scope ID provided")
}
opts := []hostcatalogs.Option{}
var foundPluginId bool
var foundPluginName bool
if pluginIdVal, ok := d.GetOk(PluginIdKey); ok {
pluginId := pluginIdVal.(string)
opts = append(opts, hostcatalogs.WithPluginId(pluginId))
foundPluginId = true
}
if pluginNameVal, ok := d.GetOk(PluginNameKey); ok {
pluginName := pluginNameVal.(string)
opts = append(opts, hostcatalogs.WithPluginName(pluginName))
foundPluginName = true
}
if !foundPluginId && !foundPluginName {
return diag.Errorf("neither plugin ID nor plugin name provided")
}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, hostcatalogs.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, hostcatalogs.WithDescription(descStr))
}
attrsVal, ok := d.GetOk(AttributesJsonKey)
if ok {
attrsStr, err := parseutil.ParsePath(attrsVal.(string))
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return diag.Errorf("error parsing path with attributes: %v", err)
}
switch attrsStr {
case "null":
opts = append(opts, hostcatalogs.DefaultAttributes())
default:
// What comes in is json-encoded but we want to set a
// map[string]interface{} so we unmarshal it and set that
var m map[string]interface{}
if err := json.Unmarshal([]byte(attrsStr), &m); err != nil {
return diag.Errorf("error unmarshaling attributes: %v", err)
}
opts = append(opts, hostcatalogs.WithAttributes(m))
}
}
secretsVal, ok := d.GetOk(SecretsJsonKey)
var secretsJson string
if ok {
var err error
secretsJson, err = parseutil.ParsePath(secretsVal.(string))
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return diag.Errorf("error parsing path with secrets: %v", err)
}
switch secretsJson {
case "null":
opts = append(opts, hostcatalogs.DefaultSecrets())
default:
// What comes in is json-encoded but we want to set a
// map[string]interface{} so we unmarshal it and set that
var m map[string]interface{}
if err := json.Unmarshal([]byte(secretsJson), &m); err != nil {
return diag.Errorf("error unmarshaling secrets: %v", err)
}
opts = append(opts, hostcatalogs.WithSecrets(m))
}
}
hcClient := hostcatalogs.NewClient(md.client)
hccr, err := hcClient.Create(ctx, hostCatalogTypePlugin, scopeId, opts...)
if err != nil {
return diag.Errorf("error creating host catalog: %v", err)
}
if hccr == nil {
return diag.Errorf("host catalog nil after create")
}
if err := setFromHostCatalogPluginResponseMap(d, hccr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
if serverHmac := d.Get(SecretsHmacKey).(string); serverHmac != "" {
configHmac, err := calculateCurrentConfigHmac(serverHmac, secretsJson)
if err != nil {
return diag.FromErr(err)
}
if err := d.Set(internalSecretsConfigHmacKey, configHmac); err != nil {
return diag.FromErr(err)
}
if err := d.Set(internalHmacUsedForSecretsConfigHmacKey, serverHmac); err != nil {
return diag.FromErr(err)
}
}
return nil
}
func resourceHostCatalogPluginRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
hcrr, err := hcClient.Read(ctx, d.Id())
if err != nil {
if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound {
d.SetId("")
return nil
}
return diag.Errorf("error reading host catalog: %v", err)
}
if hcrr == nil {
return diag.Errorf("host catalog nil after read")
}
if err := setFromHostCatalogPluginResponseMap(d, hcrr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
return nil
}
func resourceHostCatalogPluginUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
// We need to refresh the current server hmac value to figure out what to do
// next around secrets handling
var clearStateSecrets, sendSecretsToBoundary bool
var secretsJson string
var currentDiagnostics diag.Diagnostics
{
hcrr, err := hcClient.Read(ctx, d.Id())
if err != nil {
if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound {
d.SetId("")
return nil
}
return diag.Errorf("error reading host catalog in update: %v", err)
}
if hcrr == nil {
return diag.Errorf("host catalog nil after read in update")
}
var serverSecretsHmac string
if secretsHmacRaw, ok := hcrr.GetResponse().Map[SecretsHmacKey]; ok {
serverSecretsHmac = secretsHmacRaw.(string)
}
// Get current secrets_json value
secretsJson, err = parseutil.ParsePath(d.Get(SecretsJsonKey).(string))
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return diag.Errorf("error parsing path with secrets: %v", err)
}
// Now that we have the value from the server, see if anything needs to be
// done
var diagWarning *diag.Diagnostic
clearStateSecrets, sendSecretsToBoundary, diagWarning, err = calculateConfigHmacPlan(serverSecretsHmac, secretsJson, d)
if err != nil {
return diag.FromErr(err)
}
if diagWarning != nil {
currentDiagnostics = append(currentDiagnostics, *diagWarning)
}
}
opts := []hostcatalogs.Option{}
if d.HasChange(NameKey) {
opts = append(opts, hostcatalogs.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, hostcatalogs.WithName(nameStr))
}
}
if d.HasChange(DescriptionKey) {
opts = append(opts, hostcatalogs.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, hostcatalogs.WithDescription(descStr))
}
}
if d.HasChange(AttributesJsonKey) {
attrsVal, ok := d.GetOk(AttributesJsonKey)
if ok {
attrsStr, err := parseutil.ParsePath(attrsVal.(string))
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return append(currentDiagnostics, diag.Errorf("error parsing path with attributes: %v", err)...)
}
switch attrsStr {
case "null", "":
opts = append(opts, hostcatalogs.DefaultAttributes())
default:
// What comes in is json-encoded but we want to set a
// map[string]interface{} so we unmarshal it and set that
var m map[string]interface{}
if err := json.Unmarshal([]byte(attrsStr), &m); err != nil {
return append(currentDiagnostics, diag.Errorf("error unmarshaling attributes: %v", err)...)
}
opts = append(opts, hostcatalogs.WithAttributes(m))
}
} else {
opts = append(opts, hostcatalogs.DefaultAttributes())
}
}
if sendSecretsToBoundary {
switch secretsJson {
case "null":
opts = append(opts, hostcatalogs.DefaultSecrets())
default:
// What comes in is json-encoded but we want to set a
// map[string]interface{} so we unmarshal it and set that
var m map[string]interface{}
if err := json.Unmarshal([]byte(secretsJson), &m); err != nil {
return append(currentDiagnostics, diag.Errorf("error unmarshaling secrets: %v", err)...)
}
opts = append(opts, hostcatalogs.WithSecrets(m))
}
}
if len(opts) > 0 {
opts = append(opts, hostcatalogs.WithAutomaticVersioning(true))
hcur, err := hcClient.Update(ctx, d.Id(), 0, opts...)
if err != nil {
return append(currentDiagnostics, diag.Errorf("error updating host catalog: %v", err)...)
}
if hcur == nil {
return append(currentDiagnostics, diag.Errorf("host catalog nil after update")...)
}
if err := setFromHostCatalogPluginResponseMap(d, hcur.GetResponse().Map); err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
}
// Save any updated secrets information if needed
switch {
case clearStateSecrets:
if err := d.Set(internalSecretsConfigHmacKey, nil); err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
if err := d.Set(internalHmacUsedForSecretsConfigHmacKey, nil); err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
case sendSecretsToBoundary:
if serverHmac := d.Get(SecretsHmacKey).(string); serverHmac != "" {
configHmac, err := calculateCurrentConfigHmac(serverHmac, secretsJson)
if err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
if err := d.Set(internalSecretsConfigHmacKey, configHmac); err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
if err := d.Set(internalHmacUsedForSecretsConfigHmacKey, serverHmac); err != nil {
return append(currentDiagnostics, diag.FromErr(err)...)
}
}
}
return nil
}
func resourceHostCatalogPluginDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
_, err := hcClient.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error deleting host catalog: %v", err)
}
return nil
}
@@ -0,0 +1,391 @@
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/hostcatalogs"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
_ "github.com/kr/pretty" // So I don't have to keep adding to/removing from go.mod :-)
)
const (
testPluginHostCatalogDescription = "bar"
testPluginHostCatalogDescriptionUpdate = "bar foo"
testPluginHostCatalogDescriptionUpdate2 = "bar foo foo"
)
// expectedAttributesState is used here and in plugin host sets to control how
// we expect attributes to behave compared to the previous step.
type expectedAttributesState uint
const (
expectedAttributesStatePreviouslyEmptyNowSet expectedAttributesState = iota
expectedAttributesStatePreviouslySetNoChange
expectedAttributesStatePreviouslySetButChanged
expectedAttributesStatePreviouslySetNowEmpty
)
var projPluginHostCatalogBase = `
resource "boundary_host_catalog_plugin" "foo" {
name = "foo"
scope_id = boundary_scope.proj1.id
plugin_name = "loopback"
%s
depends_on = [boundary_role.proj1_admin]
}`
var (
currentPluginHostCatalogSecretsHmacValue string
currentPluginHostCatalogAttributesValue string
testStep = 1
)
// NOTE: In the test below, secrets and attributes change in the same manner at
// the same time; the eventual result is the same even if the JSON looks
// different. Thus expectedAttributesState also controls expectations for
// secrets.
func TestAccPluginHostCatalog(t *testing.T) {
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]
resName := "boundary_host_catalog_plugin.foo"
initialValuesStr := fmt.Sprintf(`
description = "%s"
attributes_json = jsonencode({
foo = "bar"
zip = "zap"
})
secrets_json = jsonencode({
hush = "puppies"
})
`,
testPluginHostCatalogDescription,
)
// Changed description and secrets
valuesStrUpdate1 := fmt.Sprintf(`
description = "%s"
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
secrets_json = jsonencode({
flush = "fluppies"
})
`,
testPluginHostCatalogDescriptionUpdate,
)
// Changed description, no secrets update
valuesStrUpdate2 := fmt.Sprintf(`
description = "%s"
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
secrets_json = jsonencode({
flush = "fluppies"
})
`,
testPluginHostCatalogDescriptionUpdate2,
)
// Same description, now empty secrets
valuesStrUpdate3 := fmt.Sprintf(`
description = "%s"
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
`,
testPluginHostCatalogDescriptionUpdate2,
)
// Same description, now explicitly unset secrets and blankify attrs
valuesStrUpdate4 := fmt.Sprintf(`
description = "%s"
secrets_json = "null"
`,
testPluginHostCatalogDescriptionUpdate2,
)
// Set values again
valuesStrUpdate5 := fmt.Sprintf(`
description = "%s"
attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})
secrets_json = jsonencode({
flush = "fluppies"
})
`,
testPluginHostCatalogDescriptionUpdate2,
)
// Explicitly set both secrets and attributes to null
valuesStrUpdate6 := fmt.Sprintf(`
description = "%s"
attributes_json = "null"
secrets_json = "null"
`,
testPluginHostCatalogDescriptionUpdate2,
)
initialHcl := fmt.Sprintf(projPluginHostCatalogBase, initialValuesStr)
update1Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate1)
update2Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate2)
update3Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate3)
update4Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate4)
update5Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate5)
update6Hcl := fmt.Sprintf(projPluginHostCatalogBase, valuesStrUpdate6)
var provider *schema.Provider
resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories(&provider),
CheckDestroy: testAccCheckPluginHostCatalogResourceDestroy(t, provider),
Steps: []resource.TestStep{
{
// test create
Config: testConfig(url, fooOrg, firstProjectFoo, initialHcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckScopeResourceExists(provider, "boundary_scope.org1"),
testAccCheckScopeResourceExists(provider, "boundary_scope.proj1"),
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslyEmptyNowSet),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescription),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update1Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetButChanged),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update2Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetNoChange),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// this runs the same HCL; mostly used in some manual checking
// to ensure update is still called even when nothing has
// changed (as we need for secrets)
Config: testConfig(url, fooOrg, firstProjectFoo, update2Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetNoChange),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update3Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetNoChange),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update4Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetNowEmpty),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update5Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslyEmptyNowSet),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, update6Hcl),
Check: resource.ComposeTestCheckFunc(
testAccCheckPluginHostCatalogResourceExists(provider, resName, expectedAttributesStatePreviouslySetNowEmpty),
resource.TestCheckResourceAttr(resName, DescriptionKey, testPluginHostCatalogDescriptionUpdate2),
),
ExpectNonEmptyPlan: true,
},
importStep(resName, SecretsJsonKey, internalHmacUsedForSecretsConfigHmacKey, internalForceUpdateKey, internalSecretsConfigHmacKey),
},
})
}
func testAccCheckPluginHostCatalogResourceExists(testProvider *schema.Provider, name string, expAttrState expectedAttributesState) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
id := rs.Primary.ID
if id == "" {
return fmt.Errorf("No ID is set")
}
md := testProvider.Meta().(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
val, err := hcClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading host catalog %q: %v", id, err)
}
if val == nil {
return errors.New("empty val returned")
}
secretsHmac := val.GetResponse().Map[SecretsHmacKey]
attrs := val.GetResponse().Map["attributes"]
switch expAttrState {
case expectedAttributesStatePreviouslyEmptyNowSet:
if currentPluginHostCatalogSecretsHmacValue != "" {
return errors.New("expected no previous secrets hmac value")
}
val := secretsHmac.(string)
if val == "" {
return errors.New("expected non-empty new secrets hmac value")
}
currentPluginHostCatalogSecretsHmacValue = val
if currentPluginHostCatalogAttributesValue != "" {
return fmt.Errorf("expected no previous attributes value, got %s", currentPluginHostCatalogAttributesValue)
}
if attrs == nil {
return errors.New("expected non-empty new attributes value")
}
attrsVal, err := json.Marshal(attrs)
if err != nil {
return fmt.Errorf("error marshaling attrs: %w", err)
}
currentPluginHostCatalogAttributesValue = string(attrsVal)
case expectedAttributesStatePreviouslySetButChanged:
if currentPluginHostCatalogSecretsHmacValue == "" {
return errors.New("expected previous secrets hmac value")
}
val := secretsHmac.(string)
if val == "" {
return errors.New("expected non-empty new secrets hmac value")
}
if val == currentPluginHostCatalogSecretsHmacValue {
return errors.New("expected changed secrets hmac value")
}
currentPluginHostCatalogSecretsHmacValue = val
if currentPluginHostCatalogAttributesValue == "" {
return errors.New("expected previous attributes value")
}
if attrs == nil {
return errors.New("expected non-empty new attributes value")
}
attrsVal, err := json.Marshal(attrs)
if err != nil {
return fmt.Errorf("error marshaling attrs: %w", err)
}
if string(attrsVal) == currentPluginHostCatalogAttributesValue {
return errors.New("expected changed attrs value")
}
currentPluginHostCatalogAttributesValue = string(attrsVal)
case expectedAttributesStatePreviouslySetNoChange:
if currentPluginHostCatalogSecretsHmacValue == "" {
return errors.New("expected previous secrets hmac value")
}
val := secretsHmac.(string)
if val == "" {
return errors.New("expected non-empty new secrets hmac value")
}
if val != currentPluginHostCatalogSecretsHmacValue {
return errors.New("expected same secrets hmac value")
}
if currentPluginHostCatalogAttributesValue == "" {
return errors.New("expected previous attributes value")
}
if attrs == nil {
return errors.New("expected non-empty new attributes value")
}
attrsVal, err := json.Marshal(attrs)
if err != nil {
return fmt.Errorf("error marshaling attrs: %w", err)
}
if string(attrsVal) == "" {
return errors.New("expected non-empty new attributes value")
}
if string(attrsVal) != currentPluginHostCatalogAttributesValue {
return errors.New("expected same attributes value")
}
case expectedAttributesStatePreviouslySetNowEmpty:
if currentPluginHostCatalogSecretsHmacValue == "" {
return errors.New("expected previous secrets hmac value")
}
if secretsHmac != nil {
return fmt.Errorf("expected empty new secrets hmac value, got %s", secretsHmac.(string))
}
currentPluginHostCatalogSecretsHmacValue = ""
if currentPluginHostCatalogAttributesValue == "" {
return errors.New("expected previous attributes value")
}
if attrs != nil {
return fmt.Errorf("expected empty new attributes value, got %s", attrs)
}
currentPluginHostCatalogAttributesValue = ""
}
testStep++
return nil
}
}
func testAccCheckPluginHostCatalogResourceDestroy(t *testing.T, testProvider *schema.Provider) resource.TestCheckFunc {
return func(s *terraform.State) error {
md := testProvider.Meta().(*metaData)
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "boundary_host_catalog":
id := rs.Primary.ID
hcClient := hostcatalogs.NewClient(md.client)
_, err := hcClient.Read(context.Background(), id)
if apiErr := api.AsServerError(err); apiErr == nil || apiErr.Response().StatusCode() != http.StatusNotFound {
return fmt.Errorf("didn't get a 404 when reading destroyed host catalog %q: %v", id, err)
}
default:
continue
}
}
return nil
}
}
@@ -0,0 +1,257 @@
package provider
import (
"context"
"net/http"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/hostcatalogs"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
hostCatalogTypeStatic = "static"
)
func resourceHostCatalog() *schema.Resource {
return &schema.Resource{
DeprecationMessage: "Deprecated: use `resource_host_catalog_static` instead.",
Description: "Deprecated: use `resource_host_catalog_static` instead.",
CreateContext: resourceHostCatalogStaticCreate(true),
ReadContext: resourceHostCatalogStaticRead(true),
UpdateContext: resourceHostCatalogStaticUpdate(true),
DeleteContext: resourceHostCatalogStaticDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of the host catalog.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The host catalog name. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The host catalog description.",
Type: schema.TypeString,
Optional: true,
},
ScopeIdKey: {
Description: "The scope ID in which the resource is created.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
TypeKey: {
Description: "The host catalog type. Only `static` is supported.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func resourceHostCatalogStatic() *schema.Resource {
return &schema.Resource{
Description: "The static host catalog resource allows you to configure a Boundary static-type host catalog. Host " +
"catalogs are always part of a project, so a project resource should be used inline or you " +
"should have the project ID in hand to successfully configure a host catalog.",
CreateContext: resourceHostCatalogStaticCreate(false),
ReadContext: resourceHostCatalogStaticRead(false),
UpdateContext: resourceHostCatalogStaticUpdate(false),
DeleteContext: resourceHostCatalogStaticDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of the host catalog.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The host catalog name. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The host catalog description.",
Type: schema.TypeString,
Optional: true,
},
ScopeIdKey: {
Description: "The scope ID in which the resource is created.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func setFromHostCatalogStaticResponseMap(d *schema.ResourceData, raw map[string]interface{}, hasTypeKey bool) error {
if err := d.Set(NameKey, raw["name"]); err != nil {
return err
}
if err := d.Set(DescriptionKey, raw["description"]); err != nil {
return err
}
if err := d.Set(ScopeIdKey, raw["scope_id"]); err != nil {
return err
}
if hasTypeKey {
if err := d.Set(TypeKey, raw["type"]); err != nil {
return err
}
}
d.SetId(raw["id"].(string))
return nil
}
func resourceHostCatalogStaticCreate(hasTypeKey bool) schema.CreateContextFunc {
return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
var scopeId string
if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok {
scopeId = scopeIdVal.(string)
} else {
return diag.Errorf("no scope ID provided")
}
typeStr := hostCatalogTypeStatic
if hasTypeKey {
// Perform backwards-compat validation
if typeVal, ok := d.GetOk(TypeKey); ok {
typeStr = typeVal.(string)
} else {
return diag.Errorf("no type provided")
}
switch typeStr {
case hostCatalogTypeStatic:
default:
return diag.Errorf("invalid type provided")
}
}
opts := []hostcatalogs.Option{}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, hostcatalogs.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, hostcatalogs.WithDescription(descStr))
}
hcClient := hostcatalogs.NewClient(md.client)
hccr, err := hcClient.Create(ctx, typeStr, scopeId, opts...)
if err != nil {
return diag.Errorf("error creating host catalog: %v", err)
}
if hccr == nil {
return diag.Errorf("nil host catalog after create")
}
if err := setFromHostCatalogStaticResponseMap(d, hccr.GetResponse().Map, hasTypeKey); err != nil {
return diag.FromErr(err)
}
return nil
}
}
func resourceHostCatalogStaticRead(hasTypeKey bool) schema.ReadContextFunc {
return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
hcrr, err := hcClient.Read(ctx, d.Id())
if err != nil {
if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound {
d.SetId("")
return nil
}
return diag.Errorf("error reading host catalog: %v", err)
}
if hcrr == nil {
return diag.Errorf("host catalog nil after read")
}
if err := setFromHostCatalogStaticResponseMap(d, hcrr.GetResponse().Map, hasTypeKey); err != nil {
return diag.FromErr(err)
}
return nil
}
}
func resourceHostCatalogStaticUpdate(hasTypeKey bool) schema.UpdateContextFunc {
return func(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
opts := []hostcatalogs.Option{}
if d.HasChange(NameKey) {
opts = append(opts, hostcatalogs.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, hostcatalogs.WithName(nameStr))
}
}
if d.HasChange(DescriptionKey) {
opts = append(opts, hostcatalogs.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, hostcatalogs.WithDescription(descStr))
}
}
if len(opts) > 0 {
opts = append(opts, hostcatalogs.WithAutomaticVersioning(true))
hcrr, err := hcClient.Update(ctx, d.Id(), 0, opts...)
if err != nil {
return diag.Errorf("error updating host catalog: %v", err)
}
if hcrr == nil {
return diag.Errorf("host catalog nil after update")
}
if err := setFromHostCatalogStaticResponseMap(d, hcrr.GetResponse().Map, hasTypeKey); err != nil {
return diag.FromErr(err)
}
}
return nil
}
}
func resourceHostCatalogStaticDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
_, err := hcClient.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error deleting host catalog: %v", err)
}
return nil
}
@@ -20,30 +20,50 @@ const (
)
var (
projHostCatalog = fmt.Sprintf(`
resource "boundary_host_catalog" "foo" {
projHostCatalog = `
resource "%s" "foo" {
name = "foo"
description = "%s"
description = "bar"
scope_id = boundary_scope.proj1.id
type = "static"
%s
depends_on = [boundary_role.proj1_admin]
}`, fooHostCatalogDescription)
}`
projHostCatalogUpdate = fmt.Sprintf(`
resource "boundary_host_catalog" "foo" {
projHostCatalogUpdate = `
resource "%s" "foo" {
name = "foo"
description = "%s"
description = "foo bar"
scope_id = boundary_scope.proj1.id
type = "static"
%s
depends_on = [boundary_role.proj1_admin]
}`, fooHostCatalogDescriptionUpdate)
}`
)
func TestAccHostCatalogCreate(t *testing.T) {
t.Run("non-static", func(t *testing.T) {
t.Parallel()
testAccHostCatalog(t, false)
})
t.Run("static", func(t *testing.T) {
t.Parallel()
testAccHostCatalog(t, true)
})
}
func testAccHostCatalog(t *testing.T, static bool) {
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]
resName := "boundary_host_catalog"
typeStr := `type = "static"`
if static {
resName = "boundary_host_catalog_static"
typeStr = ""
}
hc, hcu := fmt.Sprintf(projHostCatalog, resName, typeStr), fmt.Sprintf(projHostCatalogUpdate, resName, typeStr)
fooName := fmt.Sprintf("%s.foo", resName)
var provider *schema.Provider
resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories(&provider),
@@ -51,24 +71,24 @@ func TestAccHostCatalogCreate(t *testing.T) {
Steps: []resource.TestStep{
{
// test create
Config: testConfig(url, fooOrg, firstProjectFoo, projHostCatalog),
Config: testConfig(url, fooOrg, firstProjectFoo, hc),
Check: resource.ComposeTestCheckFunc(
testAccCheckScopeResourceExists(provider, "boundary_scope.org1"),
testAccCheckScopeResourceExists(provider, "boundary_scope.proj1"),
testAccCheckHostCatalogResourceExists(provider, "boundary_host_catalog.foo"),
resource.TestCheckResourceAttr("boundary_host_catalog.foo", DescriptionKey, fooHostCatalogDescription),
testAccCheckHostCatalogResourceExists(provider, fooName),
resource.TestCheckResourceAttr(fooName, DescriptionKey, fooHostCatalogDescription),
),
},
importStep("boundary_host_catalog.foo"),
importStep(fooName),
{
// test update
Config: testConfig(url, fooOrg, firstProjectFoo, projHostCatalogUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, hcu),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostCatalogResourceExists(provider, "boundary_host_catalog.foo"),
resource.TestCheckResourceAttr("boundary_host_catalog.foo", DescriptionKey, fooHostCatalogDescriptionUpdate),
testAccCheckHostCatalogResourceExists(provider, fooName),
resource.TestCheckResourceAttr(fooName, DescriptionKey, fooHostCatalogDescriptionUpdate),
),
},
importStep("boundary_host_catalog.foo"),
importStep(fooName),
},
})
}
@@ -102,7 +122,7 @@ func testAccCheckHostCatalogResourceDestroy(t *testing.T, testProvider *schema.P
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "boundary_host_catalog":
case "boundary_host_catalog", "boundary_host_catalog_static":
id := rs.Primary.ID
hcClient := hostcatalogs.NewClient(md.client)
@@ -0,0 +1,356 @@
package provider
import (
"context"
"encoding/json"
"errors"
"net/http"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/hostsets"
"github.com/hashicorp/go-cty/cty"
"github.com/hashicorp/go-secure-stdlib/parseutil"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
hostSetTypePlugin = "plugin"
)
func resourceHostSetPlugin() *schema.Resource {
return &schema.Resource{
Description: "The host_set_plugin resource allows you to configure a Boundary host set. Host sets are " +
"always part of a host catalog, so a host catalog resource should be used inline or you " +
"should have the host catalog ID in hand to successfully configure a host set.",
CreateContext: resourceHostSetPluginCreate,
ReadContext: resourceHostSetPluginRead,
UpdateContext: resourceHostSetPluginUpdate,
DeleteContext: resourceHostSetPluginDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of the host set.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The host set name. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The host set description.",
Type: schema.TypeString,
Optional: true,
},
TypeKey: {
Description: "The type of host set",
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: hostSetTypePlugin,
},
HostCatalogIdKey: {
Description: "The catalog for the host set.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
PreferredEndpointsKey: {
Description: "The ordered list of preferred endpoints.",
Type: schema.TypeList,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
SyncIntervalSecondsKey: {
Description: "The value to set for the sync interval seconds.",
Type: schema.TypeInt,
Optional: true,
ValidateDiagFunc: func(in interface{}, _ cty.Path) diag.Diagnostics {
val := in.(int)
switch {
case val >= -1:
return nil
default:
return diag.Errorf("invalid value for sync_interval_seconds")
}
},
},
AttributesJsonKey: {
Description: `The attributes for the host set. Either values encoded with the "jsonencode" function, pre-escaped JSON string, or a file:// or env:// path. Set to a string "null" or remove the block to clear all attributes in the host set.`,
Type: schema.TypeString,
Optional: true,
// If set to null in config and nothing comes from API, consider
// it the same. Same if config changes from empty to null.
DiffSuppressFunc: func(k, old, new string, d *schema.ResourceData) bool {
switch {
case old == new:
return true
case old == "null" && new == "":
return true
case old == "" && new == "null":
return true
default:
return false
}
},
},
},
}
}
func setFromHostSetPluginResponseMap(d *schema.ResourceData, raw map[string]interface{}) error {
if err := d.Set(NameKey, raw[NameKey]); err != nil {
return err
}
if err := d.Set(DescriptionKey, raw[DescriptionKey]); err != nil {
return err
}
if err := d.Set(HostCatalogIdKey, raw[HostCatalogIdKey]); err != nil {
return err
}
if err := d.Set(TypeKey, raw[TypeKey]); err != nil {
return err
}
if err := d.Set(SyncIntervalSecondsKey, raw[SyncIntervalSecondsKey]); err != nil {
return err
}
if err := d.Set(PreferredEndpointsKey, raw[PreferredEndpointsKey]); err != nil {
return err
}
// Attributes stuff
{
attrRaw, ok := raw["attributes"]
switch ok {
case true:
encodedAttributes, err := json.Marshal(attrRaw)
if err != nil {
return err
}
if err := d.Set(AttributesJsonKey, string(encodedAttributes)); err != nil {
return err
}
default:
d.Set(AttributesJsonKey, nil)
}
}
d.SetId(raw[IDKey].(string))
return nil
}
func resourceHostSetPluginCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
var hostsetHostCatalogId string
if hostsetHostCatalogIdVal, ok := d.GetOk(HostCatalogIdKey); ok {
hostsetHostCatalogId = hostsetHostCatalogIdVal.(string)
} else {
return diag.Errorf("no host catalog ID provided")
}
opts := []hostsets.Option{}
var typeStr string
if typeVal, ok := d.GetOk(TypeKey); ok {
typeStr = typeVal.(string)
} else {
return diag.Errorf("no type provided")
}
switch typeStr {
// NOTE: When other types are added, ensure they don't accept hostSetIds if
// it's not allowed
case hostSetTypePlugin:
default:
return diag.Errorf("invalid type provided")
}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, hostsets.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, hostsets.WithDescription(descStr))
}
syncIntervalSecondsVal, ok := d.GetOk(SyncIntervalSecondsKey)
if ok {
syncIntervalSecondsInt := syncIntervalSecondsVal.(int)
opts = append(opts, hostsets.WithSyncIntervalSeconds(int32(syncIntervalSecondsInt)))
}
var preferredEndpoints []string
if preferredEndpointsVal, ok := d.GetOk(PreferredEndpointsKey); ok {
list := preferredEndpointsVal.([]interface{})
preferredEndpoints = make([]string, 0, len(list))
for _, i := range list {
preferredEndpoints = append(preferredEndpoints, i.(string))
}
opts = append(opts, hostsets.WithPreferredEndpoints(preferredEndpoints))
}
attrsVal, ok := d.GetOk(AttributesJsonKey)
if ok {
attrsStr, err := parseutil.ParsePath(attrsVal.(string))
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return diag.Errorf("error parsing path with attributes: %v", err)
}
switch attrsStr {
case "null":
opts = append(opts, hostsets.DefaultAttributes())
default:
// What comes in is json-encoded but we want to set a
// map[string]interface{} so we unmarshal it and set that
var m map[string]interface{}
if err := json.Unmarshal([]byte(attrsStr), &m); err != nil {
return diag.Errorf("error unmarshaling attributes: %v", err)
}
opts = append(opts, hostsets.WithAttributes(m))
}
}
hsClient := hostsets.NewClient(md.client)
hscr, err := hsClient.Create(ctx, hostsetHostCatalogId, opts...)
if err != nil {
return diag.Errorf("error creating host set: %v", err)
}
if hscr == nil {
return diag.Errorf("nil host set after create")
}
if err := setFromHostSetPluginResponseMap(d, hscr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
return nil
}
func resourceHostSetPluginRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hsClient := hostsets.NewClient(md.client)
hsrr, err := hsClient.Read(ctx, d.Id())
if err != nil {
if apiErr := api.AsServerError(err); apiErr != nil && apiErr.Response().StatusCode() == http.StatusNotFound {
d.SetId("")
return nil
}
return diag.Errorf("error reading host set: %v", err)
}
if hsrr == nil {
return diag.Errorf("host set nil after read")
}
if err := setFromHostSetPluginResponseMap(d, hsrr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
return nil
}
func resourceHostSetPluginUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hsClient := hostsets.NewClient(md.client)
opts := []hostsets.Option{}
if d.HasChange(NameKey) {
opts = append(opts, hostsets.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, hostsets.WithName(nameStr))
}
}
if d.HasChange(DescriptionKey) {
opts = append(opts, hostsets.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, hostsets.WithDescription(descStr))
}
}
if d.HasChange(SyncIntervalSecondsKey) {
opts = append(opts, hostsets.DefaultSyncIntervalSeconds())
syncIntervalSecondsVal, ok := d.GetOk(SyncIntervalSecondsKey)
if ok {
syncIntervalSecondsInt := syncIntervalSecondsVal.(int)
opts = append(opts, hostsets.WithSyncIntervalSeconds(int32(syncIntervalSecondsInt)))
}
}
if d.HasChange(PreferredEndpointsKey) {
opts = append(opts, hostsets.DefaultPreferredEndpoints())
var preferredEndpoints []string
if preferredEndpointsVal, ok := d.GetOk(PreferredEndpointsKey); ok {
list := preferredEndpointsVal.([]interface{})
preferredEndpoints = make([]string, 0, len(list))
for _, i := range list {
preferredEndpoints = append(preferredEndpoints, i.(string))
}
opts = append(opts, hostsets.WithPreferredEndpoints(preferredEndpoints))
}
}
if d.HasChange(AttributesJsonKey) {
attrsVal, ok := d.GetOk(AttributesJsonKey)
if ok {
attrsStr, err := parseutil.ParsePath(attrsVal.(string))
if err != nil && !errors.Is(err, parseutil.ErrNotAUrl) {
return diag.Errorf("error parsing path with attributes: %v", err)
}
switch attrsStr {
case "null", "":
opts = append(opts, hostsets.DefaultAttributes())
default:
// What comes in is json-encoded but we want to set a
// map[string]interface{} so we unmarshal it and set that
var m map[string]interface{}
if err := json.Unmarshal([]byte(attrsStr), &m); err != nil {
return diag.Errorf("error unmarshaling attributes: %v", err)
}
opts = append(opts, hostsets.WithAttributes(m))
}
} else {
opts = append(opts, hostsets.DefaultAttributes())
}
}
if len(opts) > 0 {
opts = append(opts, hostsets.WithAutomaticVersioning(true))
hsrr, err := hsClient.Update(ctx, d.Id(), 0, opts...)
if err != nil {
return diag.Errorf("error updating host set: %v", err)
}
if hsrr == nil {
return diag.Errorf("host set nil after update")
}
if err := setFromHostSetPluginResponseMap(d, hsrr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
}
return nil
}
func resourceHostSetPluginDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hsClient := hostsets.NewClient(md.client)
_, err := hsClient.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error deleting host set: %s", err.Error())
}
return nil
}
@@ -0,0 +1,311 @@
package provider
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"testing"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/hostsets"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
var currentPluginHostSetAttributesValue string
func TestAccHostSetPlugin(t *testing.T) {
t.Parallel()
initialPreferredEndpoints := []string{"cidr:1.2.3.4/32", "dns:bar.foo.com"}
initialPreferredEndpointsRaw, err := json.Marshal(initialPreferredEndpoints)
require.NoError(t, err)
initialPreferredEndpointsStr := string(initialPreferredEndpointsRaw)
updatePreferredEndpoints := []string{"dns:bar.foo.com", "cidr:4.3.2.0/24"}
updatePreferredEndpointsRaw, err := json.Marshal(updatePreferredEndpoints)
require.NoError(t, err)
updatePreferredEndpointsStr := string(updatePreferredEndpointsRaw)
initialSyncIntervalSeconds := -1
updateSyncIntervalSeconds := 60
catalogBlock := `
resource "boundary_host_catalog_plugin" "foo" {
scope_id = boundary_scope.proj1.id
depends_on = [boundary_role.proj1_admin]
plugin_name = "loopback"
}`
hostSetBlock := `
resource "boundary_host_set_plugin" "foo" {
host_catalog_id = boundary_host_catalog_plugin.foo.id
name = "test"
description = "test hostset"
preferred_endpoints = %s
sync_interval_seconds = %d
%s
}`
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]
fooSetName := "boundary_host_set_plugin.foo"
attrBlock1 := `attributes_json = jsonencode({
foo = "bar"
zip = "zap"
})`
attrBlock2 := `attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})`
attrBlock3 := `attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})`
attrBlock4 := ``
attrBlock5 := `attributes_json = jsonencode({
foo = "bar"
zip = "zoop"
})`
attrBlock6 := `attributes_json = "null"`
hsBlock := fmt.Sprintf(hostSetBlock, initialPreferredEndpointsStr, initialSyncIntervalSeconds, attrBlock1)
hsUpdate1Block := fmt.Sprintf(hostSetBlock, updatePreferredEndpointsStr, updateSyncIntervalSeconds, attrBlock2)
hsUpdate2Block := fmt.Sprintf(hostSetBlock, updatePreferredEndpointsStr, updateSyncIntervalSeconds, attrBlock3)
hsUpdate3Block := fmt.Sprintf(hostSetBlock, updatePreferredEndpointsStr, updateSyncIntervalSeconds, attrBlock4)
hsUpdate4Block := fmt.Sprintf(hostSetBlock, updatePreferredEndpointsStr, updateSyncIntervalSeconds, attrBlock5)
hsUpdate5Block := fmt.Sprintf(hostSetBlock, updatePreferredEndpointsStr, updateSyncIntervalSeconds, attrBlock6)
var provider *schema.Provider
resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories(&provider),
CheckDestroy: testAccCheckHostSetPluginResourceDestroy(t, provider),
Steps: []resource.TestStep{
{
// test project hostset create
Config: testConfig(url, fooOrg, firstProjectFoo, catalogBlock, hsBlock),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostSetPluginResourceExists(provider, fooSetName, expectedAttributesStatePreviouslyEmptyNowSet),
resource.TestCheckResourceAttr(fooSetName, NameKey, "test"),
resource.TestCheckResourceAttr(fooSetName, DescriptionKey, "test hostset"),
resource.TestCheckResourceAttr(fooSetName, SyncIntervalSecondsKey, fmt.Sprintf("%d", initialSyncIntervalSeconds)),
testAccCheckHostSetPluginPreferredEndpoints(t, provider, fooSetName, initialPreferredEndpoints),
),
ExpectNonEmptyPlan: true,
},
importStep(fooSetName),
{
// test project hostset update
Config: testConfig(url, fooOrg, firstProjectFoo, catalogBlock, hsUpdate1Block),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostSetPluginResourceExists(provider, fooSetName, expectedAttributesStatePreviouslySetButChanged),
resource.TestCheckResourceAttr(fooSetName, NameKey, "test"),
resource.TestCheckResourceAttr(fooSetName, DescriptionKey, "test hostset"),
resource.TestCheckResourceAttr(fooSetName, SyncIntervalSecondsKey, fmt.Sprintf("%d", updateSyncIntervalSeconds)),
testAccCheckHostSetPluginPreferredEndpoints(t, provider, fooSetName, updatePreferredEndpoints),
),
ExpectNonEmptyPlan: true,
},
importStep(fooSetName),
{
// test project hostset update
Config: testConfig(url, fooOrg, firstProjectFoo, catalogBlock, hsUpdate2Block),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostSetPluginResourceExists(provider, fooSetName, expectedAttributesStatePreviouslySetNoChange),
resource.TestCheckResourceAttr(fooSetName, NameKey, "test"),
resource.TestCheckResourceAttr(fooSetName, DescriptionKey, "test hostset"),
resource.TestCheckResourceAttr(fooSetName, SyncIntervalSecondsKey, fmt.Sprintf("%d", updateSyncIntervalSeconds)),
testAccCheckHostSetPluginPreferredEndpoints(t, provider, fooSetName, updatePreferredEndpoints),
),
ExpectNonEmptyPlan: true,
},
importStep(fooSetName),
{
// test project hostset update
Config: testConfig(url, fooOrg, firstProjectFoo, catalogBlock, hsUpdate3Block),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostSetPluginResourceExists(provider, fooSetName, expectedAttributesStatePreviouslySetNowEmpty),
resource.TestCheckResourceAttr(fooSetName, NameKey, "test"),
resource.TestCheckResourceAttr(fooSetName, DescriptionKey, "test hostset"),
resource.TestCheckResourceAttr(fooSetName, SyncIntervalSecondsKey, fmt.Sprintf("%d", updateSyncIntervalSeconds)),
testAccCheckHostSetPluginPreferredEndpoints(t, provider, fooSetName, updatePreferredEndpoints),
),
ExpectNonEmptyPlan: true,
},
importStep(fooSetName),
{
// test project hostset update
Config: testConfig(url, fooOrg, firstProjectFoo, catalogBlock, hsUpdate4Block),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostSetPluginResourceExists(provider, fooSetName, expectedAttributesStatePreviouslyEmptyNowSet),
resource.TestCheckResourceAttr(fooSetName, NameKey, "test"),
resource.TestCheckResourceAttr(fooSetName, DescriptionKey, "test hostset"),
resource.TestCheckResourceAttr(fooSetName, SyncIntervalSecondsKey, fmt.Sprintf("%d", updateSyncIntervalSeconds)),
testAccCheckHostSetPluginPreferredEndpoints(t, provider, fooSetName, updatePreferredEndpoints),
),
ExpectNonEmptyPlan: true,
},
importStep(fooSetName),
{
// test project hostset update
Config: testConfig(url, fooOrg, firstProjectFoo, catalogBlock, hsUpdate5Block),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostSetPluginResourceExists(provider, fooSetName, expectedAttributesStatePreviouslySetNowEmpty),
resource.TestCheckResourceAttr(fooSetName, NameKey, "test"),
resource.TestCheckResourceAttr(fooSetName, DescriptionKey, "test hostset"),
resource.TestCheckResourceAttr(fooSetName, SyncIntervalSecondsKey, fmt.Sprintf("%d", updateSyncIntervalSeconds)),
testAccCheckHostSetPluginPreferredEndpoints(t, provider, fooSetName, updatePreferredEndpoints),
),
ExpectNonEmptyPlan: true,
},
importStep(fooSetName),
},
})
}
func testAccCheckHostSetPluginResourceExists(testProvider *schema.Provider, name string, expAttrs expectedAttributesState) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("Not found: %s", name)
}
id := rs.Primary.ID
if id == "" {
return fmt.Errorf("No ID is set")
}
md := testProvider.Meta().(*metaData)
hostsetsClient := hostsets.NewClient(md.client)
hsrr, err := hostsetsClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading hostset %q: %v", id, err)
}
attrs := hsrr.GetResponse().Map["attributes"]
switch expAttrs {
case expectedAttributesStatePreviouslyEmptyNowSet:
if currentPluginHostSetAttributesValue != "" {
return fmt.Errorf("expected no previous attributes value, got %s", currentPluginHostSetAttributesValue)
}
if attrs == nil {
return errors.New("expected non-empty new attributes value")
}
attrsVal, err := json.Marshal(attrs)
if err != nil {
return fmt.Errorf("error marshaling attrs: %w", err)
}
currentPluginHostSetAttributesValue = string(attrsVal)
case expectedAttributesStatePreviouslySetButChanged:
if currentPluginHostSetAttributesValue == "" {
return errors.New("expected previous attributes value")
}
if attrs == nil {
return errors.New("expected non-empty new attributes value")
}
attrsVal, err := json.Marshal(attrs)
if err != nil {
return fmt.Errorf("error marshaling attrs: %w", err)
}
if string(attrsVal) == currentPluginHostSetAttributesValue {
return errors.New("expected changed attrs value")
}
currentPluginHostSetAttributesValue = string(attrsVal)
case expectedAttributesStatePreviouslySetNoChange:
if currentPluginHostSetAttributesValue == "" {
return errors.New("expected previous attributes value")
}
if attrs == nil {
return errors.New("expected non-empty new attributes value")
}
attrsVal, err := json.Marshal(attrs)
if err != nil {
return fmt.Errorf("error marshaling attrs: %w", err)
}
if string(attrsVal) == "" {
return errors.New("expected non-empty new attributes value")
}
if string(attrsVal) != currentPluginHostSetAttributesValue {
return errors.New("expected same attributes value")
}
case expectedAttributesStatePreviouslySetNowEmpty:
if currentPluginHostSetAttributesValue == "" {
return errors.New("expected previous attributes value")
}
if attrs != nil {
return fmt.Errorf("expected empty new attributes value, got %s", attrs)
}
currentPluginHostSetAttributesValue = ""
}
return nil
}
}
func testAccCheckHostSetPluginResourceDestroy(t *testing.T, testProvider *schema.Provider) resource.TestCheckFunc {
return func(s *terraform.State) error {
if testProvider.Meta() == nil {
t.Fatal("got nil provider metadata")
}
md := testProvider.Meta().(*metaData)
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "boundary_host_set":
id := rs.Primary.ID
hostsetsClient := hostsets.NewClient(md.client)
_, err := hostsetsClient.Read(context.Background(), id)
if apiErr := api.AsServerError(err); apiErr == nil || apiErr.Response().StatusCode() != http.StatusNotFound {
return fmt.Errorf("didn't get a 404 when reading destroyed host set %q: %v", id, apiErr)
}
default:
continue
}
}
return nil
}
}
func testAccCheckHostSetPluginPreferredEndpoints(t *testing.T, testProvider *schema.Provider, name string, wantPreferredEndpoints []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("host set resource not found: %s", name)
}
id := rs.Primary.ID
if id == "" {
return fmt.Errorf("host set resource ID is not set")
}
// check boundary to ensure it matches
md := testProvider.Meta().(*metaData)
client := md.client.Clone()
hstClient := hostsets.NewClient(client)
hs, err := hstClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading hostset %q: %v", id, err)
}
assert.Equal(t, wantPreferredEndpoints, hs.Item.PreferredEndpoints)
return nil
}
}
@@ -11,51 +11,52 @@ import (
)
const (
hostsetHostIdsKey = "host_ids"
hostsetTypeStatic = "static"
hostSetHostIdsKey = "host_ids"
hostSetTypeStatic = "static"
)
func resourceHostset() *schema.Resource {
func resourceHostSet() *schema.Resource {
return &schema.Resource{
Description: "The host_set resource allows you to configure a Boundary host set. Host sets are " +
"always part of a host catalog, so a host catalog resource should be used inline or you " +
"should have the host catalog ID in hand to successfully configure a host set.",
DeprecationMessage: "Deprecated: use `resource_host_set_static` instead.",
Description: "Deprecated: use `resource_host_set_static` instead.",
CreateContext: resourceHostsetCreate,
ReadContext: resourceHostsetRead,
UpdateContext: resourceHostsetUpdate,
DeleteContext: resourceHostsetDelete,
CreateContext: resourceHostSetStaticCreate,
ReadContext: resourceHostSetStaticRead,
UpdateContext: resourceHostSetStaticUpdate,
DeleteContext: resourceHostSetStaticDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of the hostset.",
Description: "The ID of the host set.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The hostset name. Defaults to the resource name.",
Description: "The host set name. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The hostset description.",
Description: "The host set description.",
Type: schema.TypeString,
Optional: true,
},
TypeKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
HostCatalogIdKey: {
Description: "The catalog for the hostset.",
Description: "The type of host set",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
hostsetHostIdsKey: {
HostCatalogIdKey: {
Description: "The catalog for the host set.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
hostSetHostIdsKey: {
Description: "The list of host IDs contained in this set.",
Type: schema.TypeSet,
Optional: true,
@@ -65,7 +66,60 @@ func resourceHostset() *schema.Resource {
}
}
func setFromHostSetResponseMap(d *schema.ResourceData, raw map[string]interface{}) error {
func resourceHostSetStatic() *schema.Resource {
return &schema.Resource{
Description: "The host_set_static resource allows you to configure a Boundary host set. Host sets are " +
"always part of a host catalog, so a host catalog resource should be used inline or you " +
"should have the host catalog ID in hand to successfully configure a host set.",
CreateContext: resourceHostSetStaticCreate,
ReadContext: resourceHostSetStaticRead,
UpdateContext: resourceHostSetStaticUpdate,
DeleteContext: resourceHostSetStaticDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of the host set.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The host set name. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The host set description.",
Type: schema.TypeString,
Optional: true,
},
TypeKey: {
Description: "The type of host set",
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: hostSetTypeStatic,
},
HostCatalogIdKey: {
Description: "The catalog for the host set.",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
hostSetHostIdsKey: {
Description: "The list of host IDs contained in this set.",
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
},
}
}
func setFromHostSetStaticResponseMap(d *schema.ResourceData, raw map[string]interface{}) error {
if err := d.Set(NameKey, raw["name"]); err != nil {
return err
}
@@ -78,14 +132,14 @@ func setFromHostSetResponseMap(d *schema.ResourceData, raw map[string]interface{
if err := d.Set(TypeKey, raw["type"]); err != nil {
return err
}
if err := d.Set(hostsetHostIdsKey, raw["host_ids"]); err != nil {
if err := d.Set(hostSetHostIdsKey, raw["host_ids"]); err != nil {
return err
}
d.SetId(raw["id"].(string))
return nil
}
func resourceHostsetCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func resourceHostSetStaticCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
var hostsetHostCatalogId string
@@ -96,7 +150,7 @@ func resourceHostsetCreate(ctx context.Context, d *schema.ResourceData, meta int
}
var hostIds []string
if hostIdsVal, ok := d.GetOk(hostsetHostIdsKey); ok {
if hostIdsVal, ok := d.GetOk(hostSetHostIdsKey); ok {
list := hostIdsVal.(*schema.Set).List()
hostIds = make([]string, 0, len(list))
for _, i := range list {
@@ -115,7 +169,7 @@ func resourceHostsetCreate(ctx context.Context, d *schema.ResourceData, meta int
switch typeStr {
// NOTE: When other types are added, ensure they don't accept hostSetIds if
// it's not allowed
case hostsetTypeStatic:
case hostSetTypeStatic:
default:
return diag.Errorf("invalid type provided")
}
@@ -154,14 +208,14 @@ func resourceHostsetCreate(ctx context.Context, d *schema.ResourceData, meta int
raw = hsshr.GetResponse().Map
}
if err := setFromHostSetResponseMap(d, raw); err != nil {
if err := setFromHostSetStaticResponseMap(d, raw); err != nil {
return diag.FromErr(err)
}
return nil
}
func resourceHostsetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func resourceHostSetStaticRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hsClient := hostsets.NewClient(md.client)
@@ -177,14 +231,14 @@ func resourceHostsetRead(ctx context.Context, d *schema.ResourceData, meta inter
return diag.Errorf("host set nil after read")
}
if err := setFromHostSetResponseMap(d, hsrr.GetResponse().Map); err != nil {
if err := setFromHostSetStaticResponseMap(d, hsrr.GetResponse().Map); err != nil {
return diag.FromErr(err)
}
return nil
}
func resourceHostsetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func resourceHostSetStaticUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hsClient := hostsets.NewClient(md.client)
@@ -233,9 +287,9 @@ func resourceHostsetUpdate(ctx context.Context, d *schema.ResourceData, meta int
// The above call may not actually happen, so we use d.Id() and automatic
// versioning here
if d.HasChange(hostsetHostIdsKey) {
if d.HasChange(hostSetHostIdsKey) {
var hostIds []string
if hostIdsVal, ok := d.GetOk(hostsetHostIdsKey); ok {
if hostIdsVal, ok := d.GetOk(hostSetHostIdsKey); ok {
hosts := hostIdsVal.(*schema.Set).List()
for _, host := range hosts {
hostIds = append(hostIds, host.(string))
@@ -245,7 +299,7 @@ func resourceHostsetUpdate(ctx context.Context, d *schema.ResourceData, meta int
if err != nil {
return diag.Errorf("error updating hosts in host set: %v", err)
}
if err := d.Set(hostsetHostIdsKey, hostIds); err != nil {
if err := d.Set(hostSetHostIdsKey, hostIds); err != nil {
return diag.FromErr(err)
}
}
@@ -253,7 +307,7 @@ func resourceHostsetUpdate(ctx context.Context, d *schema.ResourceData, meta int
return nil
}
func resourceHostsetDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func resourceHostSetStaticDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hsClient := hostsets.NewClient(md.client)
@@ -14,97 +14,104 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
var (
fooHostset = `
resource "boundary_host_catalog" "foo" {
scope_id = boundary_scope.proj1.id
type = "static"
depends_on = [boundary_role.proj1_admin]
func TestAccHostSetStatic(t *testing.T) {
t.Run("non-static", func(t *testing.T) {
t.Parallel()
testAccHostSetStatic(t, false)
})
t.Run("static", func(t *testing.T) {
t.Parallel()
testAccHostSetStatic(t, true)
})
}
resource "boundary_host" "foo" {
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
address = "10.0.0.1"
}
func testAccHostSetStatic(t *testing.T, static bool) {
catalogBlock := `
resource "%s" "foo" {
%s
scope_id = boundary_scope.proj1.id
depends_on = [boundary_role.proj1_admin]
}`
resource "boundary_host_set" "foo" {
type = "static"
name = "test"
description = "test hostset"
host_catalog_id = boundary_host_catalog.foo.id
host_ids = [boundary_host.foo.id]
}`
hostBlock := `
resource "%s" "foo" {
host_catalog_id = %s.foo.id
%s
address = "10.0.0.1"
}`
fooHostsetUpdate = `
resource "boundary_host_catalog" "foo" {
scope_id = boundary_scope.proj1.id
type = "static"
depends_on = [boundary_role.proj1_admin]
}
hostSetBlock := `
resource "%s" "foo" {
host_catalog_id = %s.foo.id
name = "test"
description = "test hostset"
%s
host_ids = [%s.foo.id%s]
}`
resource "boundary_host" "foo" {
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
address = "10.0.0.1"
}
host2Block := `
resource "%s" "bar" {
host_catalog_id = %s.foo.id
%s
address = "10.0.0.2"
}`
resource "boundary_host" "bar" {
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
address = "10.0.0.2"
}
resource "boundary_host_set" "foo" {
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
name = "test"
description = "test hostset"
host_ids = [
boundary_host.foo.id,
boundary_host.bar.id,
]
}`
)
func TestAccHostset(t *testing.T) {
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
// org := iam.TestOrg(t, tc.IamRepo())
url := tc.ApiAddrs()[0]
catalogName := "boundary_host_catalog"
hostName := "boundary_host"
setName := "boundary_host_set"
typeStr := `type = "static"`
if static {
catalogName = "boundary_host_catalog_static"
hostName = "boundary_host_static"
setName = "boundary_host_set_static"
typeStr = ""
}
fooSetName := fmt.Sprintf("%s.foo", setName)
fooHostName := fmt.Sprintf("%s.foo", hostName)
barHostName := fmt.Sprintf("%s.bar", hostName)
hcBlock := fmt.Sprintf(catalogBlock, catalogName, typeStr)
hBlock := fmt.Sprintf(hostBlock, hostName, catalogName, typeStr)
h2Block := fmt.Sprintf(host2Block, hostName, catalogName, typeStr)
hsBlock := fmt.Sprintf(hostSetBlock, setName, catalogName, typeStr, hostName, "")
hsUpdateBlock := fmt.Sprintf(hostSetBlock, setName, catalogName, typeStr, hostName, fmt.Sprintf(`, %s.bar.id`, hostName))
var provider *schema.Provider
resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories(&provider),
CheckDestroy: testAccCheckHostsetResourceDestroy(t, provider),
CheckDestroy: testAccCheckHostSetStaticResourceDestroy(t, provider),
Steps: []resource.TestStep{
{
// test project hostset create
Config: testConfig(url, fooOrg, firstProjectFoo, fooHostset),
Config: testConfig(url, fooOrg, firstProjectFoo, hcBlock, hBlock, hsBlock),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostsetResourceExists(provider, "boundary_host_set.foo"),
testAccCheckHostsetHostIDsSet(provider, "boundary_host_set.foo", []string{"boundary_host.foo"}),
resource.TestCheckResourceAttr("boundary_host_set.foo", "name", "test"),
resource.TestCheckResourceAttr("boundary_host_set.foo", "description", "test hostset"),
testAccCheckHostSetStaticResourceExists(provider, fooSetName),
testAccCheckHostSetStaticHostIDsSet(provider, fooSetName, []string{fooHostName}),
resource.TestCheckResourceAttr(fooSetName, "name", "test"),
resource.TestCheckResourceAttr(fooSetName, "description", "test hostset"),
),
},
importStep("boundary_host_set.foo"),
importStep(fooSetName),
{
// test project hostset update
Config: testConfig(url, fooOrg, firstProjectFoo, fooHostsetUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, hcBlock, hBlock, h2Block, hsUpdateBlock),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostsetResourceExists(provider, "boundary_host_set.foo"),
testAccCheckHostsetHostIDsSet(provider, "boundary_host_set.foo", []string{"boundary_host.foo", "boundary_host.bar"}),
resource.TestCheckResourceAttr("boundary_host_set.foo", "name", "test"),
resource.TestCheckResourceAttr("boundary_host_set.foo", "description", "test hostset"),
testAccCheckHostSetStaticResourceExists(provider, fooSetName),
testAccCheckHostSetStaticHostIDsSet(provider, fooSetName, []string{fooHostName, barHostName}),
resource.TestCheckResourceAttr(fooSetName, "name", "test"),
resource.TestCheckResourceAttr(fooSetName, "description", "test hostset"),
),
},
importStep("boundary_host_set.foo"),
importStep(fooSetName),
},
})
}
func testAccCheckHostsetResourceExists(testProvider *schema.Provider, name string) resource.TestCheckFunc {
func testAccCheckHostSetStaticResourceExists(testProvider *schema.Provider, name string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
@@ -127,7 +134,7 @@ func testAccCheckHostsetResourceExists(testProvider *schema.Provider, name strin
}
}
func testAccCheckHostsetHostIDsSet(testProvider *schema.Provider, name string, wantHostIDs []string) resource.TestCheckFunc {
func testAccCheckHostSetStaticHostIDsSet(testProvider *schema.Provider, name string, wantHostIDs []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
@@ -185,7 +192,7 @@ func testAccCheckHostsetHostIDsSet(testProvider *schema.Provider, name string, w
}
}
func testAccCheckHostsetResourceDestroy(t *testing.T, testProvider *schema.Provider) resource.TestCheckFunc {
func testAccCheckHostSetStaticResourceDestroy(t *testing.T, testProvider *schema.Provider) resource.TestCheckFunc {
return func(s *terraform.State) error {
if testProvider.Meta() == nil {
t.Fatal("got nil provider metadata")
@@ -17,14 +17,13 @@ const (
func resourceHost() *schema.Resource {
return &schema.Resource{
Description: "The host resource allows you to configure a Boundary static host. Hosts are " +
"always part of a project, so a project resource should be used inline or you should have " +
"the project ID in hand to successfully configure a host.",
DeprecationMessage: "Deprecated: use `resource_host_static` instead.",
Description: "Deprecated: use `resource_host_static` instead.",
CreateContext: resourceHostCreate,
ReadContext: resourceHostRead,
UpdateContext: resourceHostUpdate,
DeleteContext: resourceHostDelete,
CreateContext: resourceHostStaticCreate,
ReadContext: resourceHostStaticRead,
UpdateContext: resourceHostStaticUpdate,
DeleteContext: resourceHostStaticDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
@@ -46,9 +45,60 @@ func resourceHost() *schema.Resource {
Optional: true,
},
TypeKey: {
Description: "The type of host",
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
HostCatalogIdKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
hostAddressKey: {
Description: "The static address of the host resource as `<IP>` (note: port assignment occurs in the target resource definition, do not add :port here) or a domain name.",
Type: schema.TypeString,
Optional: true,
},
},
}
}
func resourceHostStatic() *schema.Resource {
return &schema.Resource{
Description: "The static host resource allows you to configure a Boundary static host. Hosts are " +
"always part of a project, so a project resource should be used inline or you should have " +
"the project ID in hand to successfully configure a host.",
CreateContext: resourceHostStaticCreate,
ReadContext: resourceHostStaticRead,
UpdateContext: resourceHostStaticUpdate,
DeleteContext: resourceHostStaticDelete,
Importer: &schema.ResourceImporter{
StateContext: schema.ImportStatePassthroughContext,
},
Schema: map[string]*schema.Schema{
IDKey: {
Description: "The ID of the host.",
Type: schema.TypeString,
Computed: true,
},
NameKey: {
Description: "The host name. Defaults to the resource name.",
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Description: "The host description.",
Type: schema.TypeString,
Optional: true,
},
TypeKey: {
Description: "The type of host",
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Default: "static",
},
HostCatalogIdKey: {
Type: schema.TypeString,
@@ -91,7 +141,7 @@ func setFromHostResponseMap(d *schema.ResourceData, raw map[string]interface{})
return nil
}
func resourceHostCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func resourceHostStaticCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
var hostCatalogId string
@@ -158,7 +208,7 @@ func resourceHostCreate(ctx context.Context, d *schema.ResourceData, meta interf
return nil
}
func resourceHostRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func resourceHostStaticRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hClient := hosts.NewClient(md.client)
@@ -181,7 +231,7 @@ func resourceHostRead(ctx context.Context, d *schema.ResourceData, meta interfac
return nil
}
func resourceHostUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func resourceHostStaticUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hClient := hosts.NewClient(md.client)
@@ -252,7 +302,7 @@ func resourceHostUpdate(ctx context.Context, d *schema.ResourceData, meta interf
return nil
}
func resourceHostDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
func resourceHostStaticDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hClient := hosts.NewClient(md.client)
@@ -15,53 +15,59 @@ import (
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
fooHostAddress = "10.0.0.1"
fooHostAddressUpdate = "10.10.0.0"
)
var (
projHost = fmt.Sprintf(`
resource "boundary_host_catalog" "foo" {
name = "test"
description = "test catalog"
scope_id = boundary_scope.proj1.id
type = "static"
depends_on = [boundary_role.proj1_admin]
}
resource "boundary_host" "foo" {
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
name = "test"
description = "test host"
address = "%s"
}`, fooHostAddress)
projHostUpdate = fmt.Sprintf(`
resource "boundary_host_catalog" "foo" {
name = "test"
description = "test catalog"
scope_id = boundary_scope.proj1.id
type = "static"
depends_on = [boundary_role.proj1_admin]
}
resource "boundary_host" "foo" {
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
name = "test"
description = "test host"
address = "%s"
}`, fooHostAddressUpdate)
)
func TestAccHost(t *testing.T) {
t.Run("non-static", func(t *testing.T) {
t.Parallel()
testAccHost(t, false)
})
t.Run("static", func(t *testing.T) {
t.Parallel()
testAccHost(t, true)
})
}
func testAccHost(t *testing.T, static bool) {
const (
fooHostAddress = "10.0.0.1"
fooHostAddressUpdate = "10.10.0.0"
)
projCatalog := `
resource "%s" "foo" {
name = "test"
description = "test catalog"
%s
scope_id = boundary_scope.proj1.id
depends_on = [boundary_role.proj1_admin]
}`
projHost := `
resource "%s" "foo" {
host_catalog_id = %s.foo.id
name = "test"
description = "test host"
%s
address = "%s"
}`
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
// org := iam.TestOrg(t, tc.IamRepo())
url := tc.ApiAddrs()[0]
catalogName := "boundary_host_catalog"
hostName := "boundary_host"
typeStr := `type = "static"`
if static {
catalogName = "boundary_host_catalog_static"
hostName = "boundary_host_static"
typeStr = ""
}
fooHostName := fmt.Sprintf("%s.foo", hostName)
hcBlock := fmt.Sprintf(projCatalog, catalogName, typeStr)
hostBlock := fmt.Sprintf(projHost, hostName, catalogName, typeStr, fooHostAddress)
hostUpdateBlock := fmt.Sprintf(projHost, hostName, catalogName, typeStr, fooHostAddressUpdate)
var provider *schema.Provider
resource.Test(t, resource.TestCase{
ProviderFactories: providerFactories(&provider),
@@ -69,28 +75,28 @@ func TestAccHost(t *testing.T) {
Steps: []resource.TestStep{
{
// test project host create
Config: testConfig(url, fooOrg, firstProjectFoo, projHost),
Config: testConfig(url, fooOrg, firstProjectFoo, hcBlock, hostBlock),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostAttributeSet(provider, "boundary_host.foo", "address", fooHostAddress),
testAccCheckHostResourceExists(provider, "boundary_host.foo"),
resource.TestCheckResourceAttr("boundary_host.foo", "name", "test"),
resource.TestCheckResourceAttr("boundary_host.foo", "description", "test host"),
resource.TestCheckResourceAttr("boundary_host.foo", "address", fooHostAddress),
testAccCheckHostAttributeSet(provider, fooHostName, "address", fooHostAddress),
testAccCheckHostResourceExists(provider, fooHostName),
resource.TestCheckResourceAttr(fooHostName, "name", "test"),
resource.TestCheckResourceAttr(fooHostName, "description", "test host"),
resource.TestCheckResourceAttr(fooHostName, "address", fooHostAddress),
),
},
importStep("boundary_host.foo"),
importStep(fooHostName),
{
// test project host update
Config: testConfig(url, fooOrg, firstProjectFoo, projHostUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, hcBlock, hostUpdateBlock),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostAttributeSet(provider, "boundary_host.foo", "address", fooHostAddressUpdate),
testAccCheckHostResourceExists(provider, "boundary_host.foo"),
resource.TestCheckResourceAttr("boundary_host.foo", "name", "test"),
resource.TestCheckResourceAttr("boundary_host.foo", "description", "test host"),
resource.TestCheckResourceAttr("boundary_host.foo", "address", fooHostAddressUpdate),
testAccCheckHostAttributeSet(provider, fooHostName, "address", fooHostAddressUpdate),
testAccCheckHostResourceExists(provider, fooHostName),
resource.TestCheckResourceAttr(fooHostName, "name", "test"),
resource.TestCheckResourceAttr(fooHostName, "description", "test host"),
resource.TestCheckResourceAttr(fooHostName, "address", fooHostAddressUpdate),
),
},
importStep("boundary_host.foo"),
importStep(fooHostName),
},
})
}
+1 -1
View File
@@ -51,7 +51,7 @@ func resourceManagedGroup() *schema.Resource {
managedGroupFilterKey: {
Description: "Boolean expression to filter the workers for this managed group.",
Type: schema.TypeString,
Optional: true,
Required: true,
},
},
}
+49 -49
View File
@@ -12,12 +12,12 @@ import (
)
const (
targetHostSetIdsKey = "host_set_ids"
targetApplicationCredentialLibraryIdsKey = "application_credential_library_ids"
targetDefaultPortKey = "default_port"
targetSessionMaxSecondsKey = "session_max_seconds"
targetSessionConnectionLimitKey = "session_connection_limit"
targetWorkerFilterKey = "worker_filter"
targetHostSourceIdsKey = "host_source_ids"
targetApplicationCredentialSourceIdsKey = "application_credential_source_ids"
targetDefaultPortKey = "default_port"
targetSessionMaxSecondsKey = "session_max_seconds"
targetSessionConnectionLimitKey = "session_connection_limit"
targetWorkerFilterKey = "worker_filter"
targetTypeTcp = "tcp"
)
@@ -67,14 +67,14 @@ func resourceTarget() *schema.Resource {
Type: schema.TypeInt,
Optional: true,
},
targetHostSetIdsKey: {
Description: "A list of host set ID's.",
targetHostSourceIdsKey: {
Description: "A list of host source ID's.",
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
targetApplicationCredentialLibraryIdsKey: {
Description: "A list of application credential library ID's.",
targetApplicationCredentialSourceIdsKey: {
Description: "A list of application credential source ID's.",
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
@@ -111,10 +111,10 @@ func setFromTargetResponseMap(d *schema.ResourceData, raw map[string]interface{}
if err := d.Set(TypeKey, raw["type"]); err != nil {
return err
}
if err := d.Set(targetHostSetIdsKey, raw["host_set_ids"]); err != nil {
if err := d.Set(targetHostSourceIdsKey, raw["host_source_ids"]); err != nil {
return err
}
if err := d.Set(targetApplicationCredentialLibraryIdsKey, raw["application_credential_library_ids"]); err != nil {
if err := d.Set(targetApplicationCredentialSourceIdsKey, raw["application_credential_source_ids"]); err != nil {
return err
}
if err := d.Set(targetSessionMaxSecondsKey, raw["session_max_seconds"]); err != nil {
@@ -206,21 +206,21 @@ func resourceTargetCreate(ctx context.Context, d *schema.ResourceData, meta inte
opts = append(opts, targets.WithSessionConnectionLimit(int32(sessionConnectionLimitInt)))
}
var hostSetIds []string
if hostSetIdsVal, ok := d.GetOk(targetHostSetIdsKey); ok {
list := hostSetIdsVal.(*schema.Set).List()
hostSetIds = make([]string, 0, len(list))
var hostSourceIds []string
if hostSourceIdsVal, ok := d.GetOk(targetHostSourceIdsKey); ok {
list := hostSourceIdsVal.(*schema.Set).List()
hostSourceIds = make([]string, 0, len(list))
for _, i := range list {
hostSetIds = append(hostSetIds, i.(string))
hostSourceIds = append(hostSourceIds, i.(string))
}
}
var credentialLibraryIds []string
if credentialLibraryIdsVal, ok := d.GetOk(targetApplicationCredentialLibraryIdsKey); ok {
list := credentialLibraryIdsVal.(*schema.Set).List()
credentialLibraryIds = make([]string, 0, len(list))
var credentialSourceIds []string
if credentialSourceIdsVal, ok := d.GetOk(targetApplicationCredentialSourceIdsKey); ok {
list := credentialSourceIdsVal.(*schema.Set).List()
credentialSourceIds = make([]string, 0, len(list))
for _, i := range list {
credentialLibraryIds = append(credentialLibraryIds, i.(string))
credentialSourceIds = append(credentialSourceIds, i.(string))
}
}
@@ -242,19 +242,19 @@ func resourceTargetCreate(ctx context.Context, d *schema.ResourceData, meta inte
raw := tcr.GetResponse().Map
version := tcr.Item.Version
if hostSetIds != nil {
tur, err := tc.SetHostSets(ctx, tcr.Item.Id, version, hostSetIds)
if hostSourceIds != nil {
tur, err := tc.SetHostSources(ctx, tcr.Item.Id, version, hostSourceIds)
if err != nil {
return diag.Errorf("error setting host sets on target: %v", err)
return diag.Errorf("error setting host sources on target: %v", err)
}
raw = tur.GetResponse().Map
version = tur.Item.Version
}
if credentialLibraryIds != nil {
tur, err := tc.SetCredentialLibraries(ctx, tcr.Item.Id, version, targets.WithApplicationCredentialLibraryIds(credentialLibraryIds))
if credentialSourceIds != nil {
tur, err := tc.SetCredentialSources(ctx, tcr.Item.Id, version, targets.WithApplicationCredentialSourceIds(credentialSourceIds))
if err != nil {
return diag.Errorf("error setting credential libraries on target: %v", err)
return diag.Errorf("error setting credential sources on target: %v", err)
}
raw = tur.GetResponse().Map
}
@@ -411,47 +411,47 @@ func resourceTargetUpdate(ctx context.Context, d *schema.ResourceData, meta inte
// The above call may not actually happen, so we use d.Id() and automatic
// versioning here
if d.HasChange(targetHostSetIdsKey) {
var hostSetIds []string
if hostSetIdsVal, ok := d.GetOk(targetHostSetIdsKey); ok {
hostSets := hostSetIdsVal.(*schema.Set).List()
for _, hostSet := range hostSets {
hostSetIds = append(hostSetIds, hostSet.(string))
if d.HasChange(targetHostSourceIdsKey) {
var hostSourceIds []string
if hostSourceIdsVal, ok := d.GetOk(targetHostSourceIdsKey); ok {
hostSources := hostSourceIdsVal.(*schema.Set).List()
for _, hostSource := range hostSources {
hostSourceIds = append(hostSourceIds, hostSource.(string))
}
}
_, err := tc.SetHostSets(ctx, d.Id(), 0, hostSetIds, targets.WithAutomaticVersioning(true))
_, err := tc.SetHostSources(ctx, d.Id(), 0, hostSourceIds, targets.WithAutomaticVersioning(true))
if err != nil {
return diag.Errorf("error updating host sets in target: %v", err)
return diag.Errorf("error updating host sources in target: %v", err)
}
if err := d.Set(targetHostSetIdsKey, hostSetIds); err != nil {
if err := d.Set(targetHostSourceIdsKey, hostSourceIds); err != nil {
return diag.FromErr(err)
}
}
// The above calls may not actually happen, so we use d.Id() and automatic
// versioning here
if d.HasChange(targetApplicationCredentialLibraryIdsKey) {
var credentialLibraryIds []string
if credentialLibraryIdsVal, ok := d.GetOk(targetApplicationCredentialLibraryIdsKey); ok {
credLibsIds := credentialLibraryIdsVal.(*schema.Set).List()
for _, credLibId := range credLibsIds {
credentialLibraryIds = append(credentialLibraryIds, credLibId.(string))
if d.HasChange(targetApplicationCredentialSourceIdsKey) {
var credentialSourceIds []string
if credentialSourceIdsVal, ok := d.GetOk(targetApplicationCredentialSourceIdsKey); ok {
credSourceIds := credentialSourceIdsVal.(*schema.Set).List()
for _, credSourceId := range credSourceIds {
credentialSourceIds = append(credentialSourceIds, credSourceId.(string))
}
}
opts := []targets.Option{
targets.WithAutomaticVersioning(true),
targets.DefaultApplicationCredentialLibraryIds(),
targets.DefaultApplicationCredentialSourceIds(),
}
if len(credentialLibraryIds) > 0 {
opts = append(opts, targets.WithApplicationCredentialLibraryIds(credentialLibraryIds))
if len(credentialSourceIds) > 0 {
opts = append(opts, targets.WithApplicationCredentialSourceIds(credentialSourceIds))
}
_, err := tc.SetCredentialLibraries(ctx, d.Id(), 0, opts...)
_, err := tc.SetCredentialSources(ctx, d.Id(), 0, opts...)
if err != nil {
return diag.Errorf("error updating credential libraries in target: %v", err)
return diag.Errorf("error updating credential sources in target: %v", err)
}
if err := d.Set(targetApplicationCredentialLibraryIdsKey, credentialLibraryIds); err != nil {
if err := d.Set(targetApplicationCredentialSourceIdsKey, credentialSourceIds); err != nil {
return diag.FromErr(err)
}
}
+46 -46
View File
@@ -6,11 +6,10 @@ import (
"net/http"
"testing"
"github.com/hashicorp/boundary/testing/vault"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/boundary/testing/vault"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
@@ -87,10 +86,10 @@ resource "boundary_target" "foo" {
description = "%s"
type = "tcp"
scope_id = boundary_scope.proj1.id
host_set_ids = [
host_source_ids = [
boundary_host_set.foo.id
]
application_credential_library_ids = [
application_credential_source_ids = [
boundary_credential_library_vault.foo.id
]
default_port = 22
@@ -106,10 +105,10 @@ resource "boundary_target" "foo" {
description = "%s"
type = "tcp"
scope_id = boundary_scope.proj1.id
host_set_ids = [
host_source_ids = [
boundary_host_set.bar.id
]
application_credential_library_ids = [
application_credential_source_ids = [
boundary_credential_library_vault.bar.id
]
default_port = 80
@@ -119,7 +118,7 @@ resource "boundary_target" "foo" {
worker_filter = "type == \"bar\""
}`, fooTargetDescriptionUpdate)
fooTargetUpdateUnsetHostAndCredLibs = fmt.Sprintf(`
fooTargetUpdateUnsetHostAndCredSources = fmt.Sprintf(`
resource "boundary_target" "foo" {
name = "test"
description = "%s"
@@ -151,6 +150,7 @@ func TestAccTarget(t *testing.T) {
var provider *schema.Provider
resource.Test(t, resource.TestCase{
IsUnitTest: true,
ProviderFactories: providerFactories(&provider),
CheckDestroy: testAccCheckTargetResourceDestroy(t, provider),
Steps: []resource.TestStep{
@@ -165,8 +165,8 @@ func TestAccTarget(t *testing.T) {
resource.TestCheckResourceAttr("boundary_target.foo", targetSessionMaxSecondsKey, "6000"),
resource.TestCheckResourceAttr("boundary_target.foo", targetSessionConnectionLimitKey, "6"),
resource.TestCheckResourceAttr("boundary_target.foo", targetWorkerFilterKey, `type == "foo"`),
testAccCheckTargetResourceHostSet(provider, "boundary_target.foo", []string{"boundary_host_set.foo"}),
testAccCheckTargetResourceAppCredLibs(provider, "boundary_target.foo", []string{"boundary_credential_library_vault.foo"}),
testAccCheckTargetResourceHostSource(provider, "boundary_target.foo", []string{"boundary_host_set.foo"}),
testAccCheckTargetResourceAppCredSources(provider, "boundary_target.foo", []string{"boundary_credential_library_vault.foo"}),
),
},
importStep("boundary_target.foo"),
@@ -180,14 +180,14 @@ func TestAccTarget(t *testing.T) {
resource.TestCheckResourceAttr("boundary_target.foo", targetSessionMaxSecondsKey, "7000"),
resource.TestCheckResourceAttr("boundary_target.foo", targetSessionConnectionLimitKey, "7"),
resource.TestCheckResourceAttr("boundary_target.foo", targetWorkerFilterKey, `type == "bar"`),
testAccCheckTargetResourceHostSet(provider, "boundary_target.foo", []string{"boundary_host_set.bar"}),
testAccCheckTargetResourceAppCredLibs(provider, "boundary_target.foo", []string{"boundary_credential_library_vault.bar"}),
testAccCheckTargetResourceHostSource(provider, "boundary_target.foo", []string{"boundary_host_set.bar"}),
testAccCheckTargetResourceAppCredSources(provider, "boundary_target.foo", []string{"boundary_credential_library_vault.bar"}),
),
},
importStep("boundary_target.foo"),
{
// test unset hosts and cred libs
Config: testConfig(url, fooOrg, firstProjectFoo, credStoreRes, fooBarCredLibs, fooBarHostSet, fooTargetUpdateUnsetHostAndCredLibs),
// test unset hosts and cred sources
Config: testConfig(url, fooOrg, firstProjectFoo, credStoreRes, fooBarCredLibs, fooBarHostSet, fooTargetUpdateUnsetHostAndCredSources),
Check: resource.ComposeTestCheckFunc(
testAccCheckTargetResourceExists(provider, "boundary_target.foo"),
resource.TestCheckResourceAttr("boundary_target.foo", DescriptionKey, fooTargetDescriptionUpdate),
@@ -195,8 +195,8 @@ func TestAccTarget(t *testing.T) {
resource.TestCheckResourceAttr("boundary_target.foo", targetSessionMaxSecondsKey, "7000"),
resource.TestCheckResourceAttr("boundary_target.foo", targetSessionConnectionLimitKey, "7"),
resource.TestCheckResourceAttr("boundary_target.foo", targetWorkerFilterKey, `type == "bar"`),
testAccCheckTargetResourceHostSet(provider, "boundary_target.foo", nil),
testAccCheckTargetResourceAppCredLibs(provider, "boundary_target.foo", nil),
testAccCheckTargetResourceHostSource(provider, "boundary_target.foo", nil),
testAccCheckTargetResourceAppCredSources(provider, "boundary_target.foo", nil),
),
},
importStep("boundary_target.foo"),
@@ -204,7 +204,7 @@ func TestAccTarget(t *testing.T) {
})
}
func testAccCheckTargetResourceHostSet(testProvider *schema.Provider, name string, hostSets []string) resource.TestCheckFunc {
func testAccCheckTargetResourceHostSource(testProvider *schema.Provider, name string, hostSources []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
@@ -216,20 +216,20 @@ func testAccCheckTargetResourceHostSet(testProvider *schema.Provider, name strin
return fmt.Errorf("target resource ID is not set")
}
// ensure host sets are declared in state
var hostSetIDs []string
for _, hostSetResourceID := range hostSets {
hs, ok := s.RootModule().Resources[hostSetResourceID]
// ensure host sources are declared in state
var hostSourceIDs []string
for _, hostSourceResourceID := range hostSources {
hs, ok := s.RootModule().Resources[hostSourceResourceID]
if !ok {
return fmt.Errorf("host set resource not found: %s", hostSetResourceID)
return fmt.Errorf("host source resource not found: %s", hostSourceResourceID)
}
hostSetID := hs.Primary.ID
hostSourceID := hs.Primary.ID
if id == "" {
return fmt.Errorf("host set resource ID not set")
return fmt.Errorf("host source resource ID not set")
}
hostSetIDs = append(hostSetIDs, hostSetID)
hostSourceIDs = append(hostSourceIDs, hostSourceID)
}
// check boundary to ensure it matches
@@ -241,19 +241,19 @@ func testAccCheckTargetResourceHostSet(testProvider *schema.Provider, name strin
return fmt.Errorf("Got an error when reading target %q: %v", id, err)
}
if len(t.Item.HostSetIds) != len(hostSetIDs) {
return fmt.Errorf("tf state and boundary have different number of host sets")
if len(t.Item.HostSourceIds) != len(hostSourceIDs) {
return fmt.Errorf("tf state and boundary have different number of host sources")
}
for _, stateHostSetId := range t.Item.HostSetIds {
for _, stateHostSourceId := range t.Item.HostSourceIds {
ok := false
for _, gotHostSetID := range hostSetIDs {
if gotHostSetID == stateHostSetId {
for _, gotHostSourceID := range hostSourceIDs {
if gotHostSourceID == stateHostSourceId {
ok = true
}
}
if !ok {
return fmt.Errorf("host set id in state not set in boundary: %s", stateHostSetId)
return fmt.Errorf("host source id in state not set in boundary: %s", stateHostSourceId)
}
}
@@ -261,7 +261,7 @@ func testAccCheckTargetResourceHostSet(testProvider *schema.Provider, name strin
}
}
func testAccCheckTargetResourceAppCredLibs(testProvider *schema.Provider, name string, credLibs []string) resource.TestCheckFunc {
func testAccCheckTargetResourceAppCredSources(testProvider *schema.Provider, name string, credSources []string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
@@ -273,20 +273,20 @@ func testAccCheckTargetResourceAppCredLibs(testProvider *schema.Provider, name s
return fmt.Errorf("target resource ID is not set")
}
// ensure host sets are declared in state
var credLibIDs []string
for _, credLibResourceID := range credLibs {
cl, ok := s.RootModule().Resources[credLibResourceID]
// ensure cred sources are declared in state
var credSourceIDs []string
for _, credSourceResourceID := range credSources {
cl, ok := s.RootModule().Resources[credSourceResourceID]
if !ok {
return fmt.Errorf("credential library resource not found: %s", credLibResourceID)
return fmt.Errorf("credential source resource not found: %s", credSourceResourceID)
}
credLibID := cl.Primary.ID
credSourceID := cl.Primary.ID
if id == "" {
return fmt.Errorf("credential library resource ID not set")
return fmt.Errorf("credential source resource ID not set")
}
credLibIDs = append(credLibIDs, credLibID)
credSourceIDs = append(credSourceIDs, credSourceID)
}
// check boundary to ensure it matches
@@ -295,22 +295,22 @@ func testAccCheckTargetResourceAppCredLibs(testProvider *schema.Provider, name s
t, err := tgtsClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading target %q: %v", id, err)
return fmt.Errorf("got an error when reading target %q: %w", id, err)
}
if len(t.Item.ApplicationCredentialLibraryIds) != len(credLibIDs) {
return fmt.Errorf("tf state and boundary have different number of application credential libraries")
if len(t.Item.ApplicationCredentialSourceIds) != len(credSourceIDs) {
return fmt.Errorf("tf state and boundary have different number of application credential sources")
}
for _, stateCredLibId := range t.Item.ApplicationCredentialLibraryIds {
for _, stateCredSourceId := range t.Item.ApplicationCredentialSourceIds {
ok := false
for _, gotCredLibID := range credLibIDs {
if gotCredLibID == stateCredLibId {
for _, gotCredSourceID := range credSourceIDs {
if gotCredSourceID == stateCredSourceId {
ok = true
}
}
if !ok {
return fmt.Errorf("application credential library id in state not set in boundary: %s", stateCredLibId)
return fmt.Errorf("application credential source id in state not set in boundary: %s", stateCredSourceId)
}
}