Update to the new SDK/API (#32)

* Begin API updating

* Update scope and user providers

* Update group resource

* Update to TF plugin SDK v2

* Finish recovery KMS workflow

This also modifies groups tests to use it. It's easy to verify from the
controller output that it's being used since it causes warnings to be
printed.

* Finish porting group resource to proposed new paradigm

* Minor cleanup

* Allow specifying a particular token for auth

* base_url -> addr

* Add test using token

* Add set calls to Update

* Update SDK

* Update SDK

* Update user and scope

* Half of host catalog and some consolidation

* Finish host catalog

* Add host resource

* Add host set resource

* Update targets

* Some updates to newer API

* Start updating to response map standards

* Finish updating already ported resources to new paradigm

* Finish existing resource porting

* Update target to exercise default port

* Adapt target to using json.Number

* Add auth method and account resources
This commit is contained in:
Jeff Mitchell authored and GitHub committed 2020-09-15 16:56:43 -07:00
1 parent 84eabe083f
commit 88946e1088
33 files changed
+2772 -2622

No files matched your search

+5
View File
@@ -0,0 +1,5 @@
{
"go.testEnvVars": {
"TF_ACC": "1"
}
}
-1
View File
@@ -9,7 +9,6 @@ testacc-ci: install-go
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:
./ci/goinstall.sh
+4 -4
View File
@@ -39,10 +39,10 @@ Please see our detailed docs for individual resource usage. Below is a complex e
```hcl
provider "boundary" {
base_url = "http://127.0.0.1:9200"
auth_method_id = "ampw_0000000000"
auth_method_username = "foo"
auth_method_password = "foofoofoo"
addr = "http://127.0.0.1:9200"
auth_method_id = "ampw_1234567890"
password_auth_method_username = "admin"
password_auth_method_password = "passpass"
}
variable "backend_team" {
+4 -14
View File
@@ -3,36 +3,26 @@ module github.com/hashicorp/terraform-provider-boundary
go 1.13
require (
cloud.google.com/go v0.65.0 // indirect
cloud.google.com/go/storage v1.11.0 // indirect
github.com/agext/levenshtein v1.2.3 // indirect
github.com/apparentlymart/go-cidr v1.1.0 // indirect
github.com/aws/aws-sdk-go v1.34.16 // indirect
github.com/cenkalti/backoff/v3 v3.2.2 // indirect
github.com/containerd/continuity v0.0.0-20200710164510-efbc4488d8fe // indirect
github.com/google/go-cmp v0.5.2 // indirect
github.com/google/uuid v1.1.2 // indirect
github.com/hashicorp/boundary v0.0.0-20200903172434-23156afa116f
github.com/hashicorp/go-getter v1.4.1 // indirect
github.com/hashicorp/go-plugin v1.3.0 // indirect
github.com/hashicorp/boundary v0.0.0-20200915062024-77bcee186138
github.com/hashicorp/go-kms-wrapping v0.5.14
github.com/hashicorp/go-version v1.2.1 // indirect
github.com/hashicorp/hcl/v2 v2.6.0 // indirect
github.com/hashicorp/terraform-plugin-sdk v1.15.0
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 // indirect
github.com/hashicorp/terraform-plugin-sdk/v2 v2.0.1
github.com/hashicorp/yamux v0.0.0-20200609203250-aecfd211c9ce // indirect
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/sergi/go-diff v1.1.0 // indirect
github.com/spf13/afero v1.3.5 // indirect
github.com/stretchr/testify v1.6.1
github.com/ulikunitz/xz v0.5.8 // indirect
github.com/zclconf/go-cty v1.6.1 // indirect
github.com/zclconf/go-cty-yaml v1.0.2 // indirect
golang.org/x/crypto v0.0.0-20200820211705-5c72a883971a // indirect
golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43 // indirect
golang.org/x/sys v0.0.0-20200831180312-196b9ba8737a // indirect
golang.org/x/tools v0.0.0-20200903153655-76a6aac657c7 // indirect
google.golang.org/api v0.31.0 // indirect
google.golang.org/genproto v0.0.0-20200903010400-9bfcb5116336 // indirect
google.golang.org/grpc v1.31.1 // indirect
)
+15 -56
View File
@@ -140,8 +140,6 @@ github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb
github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ=
github.com/apparentlymart/go-cidr v1.0.1 h1:NmIwLZ/KdsjIUlhf+/Np40atNXm/+lZ5txfTJ/SpF+U=
github.com/apparentlymart/go-cidr v1.0.1/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/apparentlymart/go-cidr v1.1.0 h1:2mAhrMoF+nhXqxTzSZMUzDHkLjmIHC+Zzn4tdgBZjnU=
github.com/apparentlymart/go-cidr v1.1.0/go.mod h1:EBcsNrHc3zQeuaeCeCtQruQm+n9/YjEn/vI25Lg7Gwc=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3 h1:ZSTrOEhiM5J5RFxEaFvMZVEAM1KvT1YzbEOwB2EAGjA=
github.com/apparentlymart/go-dump v0.0.0-20180507223929-23540a00eaa3/go.mod h1:oL81AME2rN47vu18xqj1S1jPIPuN7afo62yKTNn3XMM=
github.com/apparentlymart/go-dump v0.0.0-20190214190832-042adf3cf4a0 h1:MzVXffFUye+ZcSR6opIgz9Co7WcDx6ZcY+RjfFHoA0I=
@@ -174,8 +172,6 @@ github.com/aws/aws-sdk-go v1.25.37/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpi
github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo=
github.com/aws/aws-sdk-go v1.30.27 h1:9gPjZWVDSoQrBO2AvqrWObS6KAZByfEJxQoCYo4ZfK0=
github.com/aws/aws-sdk-go v1.30.27/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.34.13 h1:wwNWSUh4FGJxXVOVVNj2lWI8wTe5hK8sGWlK7ziEcgg=
github.com/aws/aws-sdk-go v1.34.13/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go v1.34.16 h1:22jPsMe98UX/van5Ca/5jXnyNsNpJxCJ1rw/wFAlZ+4=
github.com/aws/aws-sdk-go v1.34.16/go.mod h1:5zCpMtNQVjRREroY7sYe8lOMRSxkhG6MZveU8YkpAk0=
github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g=
@@ -542,8 +538,6 @@ github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm4
github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.1 h1:Gkbcsh/GbpXz7lPftLA3P6TYMwjCLYm83jiFQZF/3gY=
github.com/google/uuid v1.1.1/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/google/uuid v1.1.2 h1:EVhdT+1Kseyi1/pUmXKaFxYsDNy9RQYkMWRH68J/W7Y=
github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg=
github.com/googleapis/gax-go/v2 v2.0.5 h1:sjZBwGj9Jlw33ImPtvFviGYvseOtDM7hkSKB7+Tv3SM=
github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk=
@@ -570,12 +564,8 @@ github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t
github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.0-beta.4.0.20200813083929-9bdca3d79b97 h1:BFGJgt5fEZhMtNE8VrTWmAMnLptKdn98qLgrSFNmDxY=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.0.0-beta.4.0.20200813083929-9bdca3d79b97/go.mod h1:wK7PFLUnfulN3kJW75Btnhb3iasamaju+L19L0bFXZk=
github.com/hailocab/go-hostpool v0.0.0-20160125115350-e80d13ce29ed/go.mod h1:tMWxXQ9wFIaZeTI9F+hmhFiGpFmhOHzyShyFUhRm0H4=
github.com/hashicorp/boundary v0.0.0-20200902173807-ee35a92f7f7e h1:qUt3V0dGpZ91Qq/1avHXuSmMZS5Y1rEY3bZFJI2PkQc=
github.com/hashicorp/boundary v0.0.0-20200902173807-ee35a92f7f7e/go.mod h1:DjIz4TeQ2zpeUXfuIqgY7DYu4lDTnQyUPrB0uoGQq+Y=
github.com/hashicorp/boundary v0.0.0-20200903150230-c55153ff3d39 h1:EfX+fjc/nHotXYhONNEE/8Qkq9g/AzNnwA+O/6xH5XE=
github.com/hashicorp/boundary v0.0.0-20200903150230-c55153ff3d39/go.mod h1:DjIz4TeQ2zpeUXfuIqgY7DYu4lDTnQyUPrB0uoGQq+Y=
github.com/hashicorp/boundary v0.0.0-20200903172434-23156afa116f h1:Pu6IigReHW58u0GtDHneA88R8PbzZJLXePiTy4eHZy4=
github.com/hashicorp/boundary v0.0.0-20200903172434-23156afa116f/go.mod h1:hBaBC+9RfOiK0UAyqSHDdzJOaKCX3gxM88RtU5ukZV0=
github.com/hashicorp/boundary v0.0.0-20200915062024-77bcee186138 h1:LJU2AZ/rmagdI26oVAJReCYA39eHrHeoyIiFwKt3czI=
github.com/hashicorp/boundary v0.0.0-20200915062024-77bcee186138/go.mod h1:hBaBC+9RfOiK0UAyqSHDdzJOaKCX3gxM88RtU5ukZV0=
github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBtguAZLlVdkD9Q=
github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE=
github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8=
@@ -584,8 +574,6 @@ github.com/hashicorp/dbassert v0.0.0-20200602142727-a6709b436ad6 h1:Nejn3Kli0VXW
github.com/hashicorp/dbassert v0.0.0-20200602142727-a6709b436ad6/go.mod h1:+B5eZq7vXqN4Gxkgjm4ub1bwG6OTcYzFL0r+bMvWorg=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-alpnmux v0.0.0-20200513011953-0293f5d23c31 h1:pxqI71/0R1WIASjQEJ9W9skCKYiREEkRoXFvHCZH1pg=
github.com/hashicorp/go-alpnmux v0.0.0-20200513011953-0293f5d23c31/go.mod h1:KvpteZzIafT4tRAuQ9vVRBgZyqeVCS2B2177fNAyEZc=
github.com/hashicorp/go-alpnmux v0.0.0-20200822022353-14527cfc58e5 h1:2YnwbJbobkTULUOhJ77seiSGqd7tD3V7zFXn7u5666k=
github.com/hashicorp/go-alpnmux v0.0.0-20200822022353-14527cfc58e5/go.mod h1:KvpteZzIafT4tRAuQ9vVRBgZyqeVCS2B2177fNAyEZc=
github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU=
@@ -593,10 +581,12 @@ github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuD
github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cleanhttp v0.5.1 h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=
github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI=
github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs=
github.com/hashicorp/go-getter v1.4.0 h1:ENHNi8494porjD0ZhIrjlAHnveSFhY7hvOJrV/fsKkw=
github.com/hashicorp/go-getter v1.4.0/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY=
github.com/hashicorp/go-getter v1.4.1 h1:3A2Mh8smGFcf5M+gmcv898mZdrxpseik45IpcyISLsA=
github.com/hashicorp/go-getter v1.4.1/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY=
github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02 h1:l1KB3bHVdvegcIf5upQ5mjcHjs2qsWnKh4Yr9xgIuu8=
github.com/hashicorp/go-getter v1.4.2-0.20200106182914-9813cbd4eb02/go.mod h1:7qxyCd8rBfcShwsvxgIguu4KbS3l8bUCwg2Umn7RjeY=
github.com/hashicorp/go-hclog v0.0.0-20180709165350-ff2cf002a8dd/go.mod h1:9bjs9uLqI8l75knNv3lV1kA55veR+WUPSiKIWcQHudI=
github.com/hashicorp/go-hclog v0.8.0/go.mod h1:5CU+agLiy3J7N7QjHK5d05KxGsuXiQLrjA0H7acj2lQ=
github.com/hashicorp/go-hclog v0.9.2 h1:CG6TE5H9/JXsFWJCfoIVpKFIkFe6ysEuHirp4DxCsHI=
@@ -657,12 +647,9 @@ github.com/hashicorp/golang-lru v0.5.1 h1:0hERBMJE1eitiLkihrMvRVBYAkpHzc/J3QdDN+
github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8=
github.com/hashicorp/golang-lru v0.5.3 h1:YPkqC67at8FYaadspW/6uE0COsBxS2656RLEr8Bppgk=
github.com/hashicorp/golang-lru v0.5.3/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4=
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f h1:UdxlrJz4JOnY8W+DbLISwf2B8WXEolNRA8BGCwI9jws=
github.com/hashicorp/hcl v0.0.0-20170504190234-a4b07c25de5f/go.mod h1:oZtUIOe8dh44I2q6ScRibXws4Ajl+d+nod3AaR9vL5w=
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
github.com/hashicorp/hcl/v2 v2.0.0 h1:efQznTz+ydmQXq3BOnRa3AXzvCeTq1P4dKj/z5GLlY8=
github.com/hashicorp/hcl/v2 v2.0.0/go.mod h1:oVVDG71tEinNGYCxinCYadcmKU9bglqW9pV3txagJ90=
github.com/hashicorp/hcl/v2 v2.3.0/go.mod h1:d+FwDBbOLvpAM3Z6J7gPj/VoAGkNe/gm352ZhjJ/Zv8=
github.com/hashicorp/hcl/v2 v2.6.0 h1:3krZOfGY6SziUXa6H9PJU6TyohHn7I+ARYnhbeNBz+o=
github.com/hashicorp/hcl/v2 v2.6.0/go.mod h1:bQTN5mpo+jewjJgh8jr0JUguIi7qPHUF6yIfAEN3jqY=
github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y=
@@ -672,20 +659,14 @@ github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2p
github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc=
github.com/hashicorp/shared-secure-libs v0.0.2 h1:+izKlB8vxpBK+K6zNmB4DZtDEl1a7fB8JmqdBNhnhjs=
github.com/hashicorp/shared-secure-libs v0.0.2/go.mod h1:xhtA0FH6AYYFOy0sir7u0O0zzdLi7ofU6oWxy+gjnuc=
github.com/hashicorp/terraform-config-inspect v0.0.0-20191115094559-17f92b0546e8 h1:+RyjwU+Gnd/aTJBPZVDNm903eXVjjqhbaR4Ypx3xYyY=
github.com/hashicorp/terraform-config-inspect v0.0.0-20191115094559-17f92b0546e8/go.mod h1:p+ivJws3dpqbp1iP84+npOyAmTTOLMgCzrXd3GSdn/A=
github.com/hashicorp/terraform-exec v0.1.1 h1:soHqDz5365aZVt9pyTT7ArOXSpMqaHGMYq4VhI+HNkE=
github.com/hashicorp/terraform-exec v0.1.1/go.mod h1:yKWvMPtkTaHpeAmllw+1qdHZ7E5u+pAZ+x8e2jQF6gM=
github.com/hashicorp/terraform-exec v0.3.0 h1:5WLBsnv9BoEUGlHJZETROZZxw+qO3/TFQEh6JMP2uaY=
github.com/hashicorp/terraform-exec v0.3.0/go.mod h1:yKWvMPtkTaHpeAmllw+1qdHZ7E5u+pAZ+x8e2jQF6gM=
github.com/hashicorp/terraform-json v0.5.0 h1:7TV3/F3y7QVSuN4r9BEXqnWqrAyeOtON8f0wvREtyzs=
github.com/hashicorp/terraform-json v0.5.0/go.mod h1:eAbqb4w0pSlRmdvl8fOyHAi/+8jnkVYN28gJkSJrLhU=
github.com/hashicorp/terraform-plugin-sdk v1.15.0 h1:bmYnTT7MqNXlUHDc7pT8E6uKT2g/upjlRLypJFK1OQU=
github.com/hashicorp/terraform-plugin-sdk v1.15.0/go.mod h1:PuFTln8urDmRM6mV0II6apOTsyG/iHkxp+5W11eJE58=
github.com/hashicorp/terraform-plugin-test v1.4.3 h1:HSOZZu2W7a9tx4QPYXhrT9oh7JptibaSkP7CK4C0OF0=
github.com/hashicorp/terraform-plugin-test v1.4.3/go.mod h1:UA7z/02pgqsRLut4DJIPm0Hjnj27uOvhi19c8kTqIfM=
github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596 h1:hjyO2JsNZUKT1ym+FAdlBEkGPevazYsmVgIMw7dVELg=
github.com/hashicorp/terraform-svchost v0.0.0-20191011084731-65d371908596/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg=
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734 h1:HKLsbzeOsfXmKNpr3GiT18XAblV0BjCbzL8KQAMZGa0=
github.com/hashicorp/terraform-svchost v0.0.0-20200729002733-f050f53b9734/go.mod h1:kNDNcF7sN4DocDLBkQYz73HGKwN1ANB1blq4lIYLYvg=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.0.1 h1:qG6EdnW2UrftQI4mBdIsWP4YWqYJXynZtl0shQYuU78=
github.com/hashicorp/terraform-plugin-sdk/v2 v2.0.1/go.mod h1:BRz6UtYmksQJU0eMfahQR8fcJf8tIe77gn7YVm6rGD4=
github.com/hashicorp/terraform-plugin-test/v2 v2.0.0 h1:fYGV3nZvs8KFGKuY2NPAJDMNfVSDHo+U2FGFl3bPv1s=
github.com/hashicorp/terraform-plugin-test/v2 v2.0.0/go.mod h1:C6VALgUlvaif+PnHyRGKWPTdQkMJK4NQ20VJolxZLI0=
github.com/hashicorp/vault/api v1.0.5-0.20200514164214-89b1987e38c2 h1:lfjJouPlC/inHjTOqe48N/BiLshU2YZqkxkUbNPj4fU=
github.com/hashicorp/vault/api v1.0.5-0.20200514164214-89b1987e38c2/go.mod h1:P8A7gn1a6j/7qQ4zOaWI6FC/Q9jQWEBRTOwZH33tC9o=
github.com/hashicorp/vault/api v1.0.5-0.20200519221902-385fac77e20f h1:PYtnlUZzFSZxPcq7mYp5oC9N+BcJ8IKYf6/EG0GHM2Y=
@@ -919,8 +900,6 @@ github.com/mitchellh/cli v1.0.0 h1:iGBIsUe3+HZ/AD/Vd7DErOt5sU9fa8Uj7A2s1aggv1Y=
github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc=
github.com/mitchellh/cli v1.1.1 h1:J64v/xD7Clql+JVKSvkYojLOXu1ibnY9ZjGLwSt/89w=
github.com/mitchellh/cli v1.1.1/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db h1:62I3jR2EmQ4l5rM/4FEfDWcRD+abF5XlKShorW5LRoQ=
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db/go.mod h1:l0dey0ia/Uv7NcFFVbCLtqEBQbrT4OCwCSKTEv6enCw=
github.com/mitchellh/copystructure v1.0.0 h1:Laisrj+bAB6b/yJwB5Bt3ITZhGJdqmxquMKeZ+mmkFQ=
github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw=
github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
@@ -929,6 +908,7 @@ github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrk
github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0=
github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.0.4/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI=
github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU=
github.com/mitchellh/go-testing-interface v1.14.1/go.mod h1:gfgS7OtZj6MA4U1UrDRp04twqAjfvlZyCfX3sDjEym8=
github.com/mitchellh/go-wordwrap v0.0.0-20150314170334-ad45545899c7/go.mod h1:ZXFpozHsX6DPmq2I0TCekCxypsnAUbP2oI0UX1GXzOo=
@@ -1041,8 +1021,6 @@ github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZ
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI=
github.com/posener/complete v1.2.1 h1:LrvDIY//XNo65Lq84G/akBuMGlawHvGBABv8f/ZN6DI=
github.com/posener/complete v1.2.1/go.mod h1:6gapUrK/U1TAN7ciCoNRIdVC5sbdBTUh1DKN0g6uH7E=
github.com/posener/complete v1.2.3 h1:NP0eAhjcjImqslEwo/1hq7gpajME0fTLTezBKDqfXqo=
github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s=
github.com/pquerna/cachecontrol v0.0.0-20180517163645-1555304b9b35/go.mod h1:prYjPmNq4d1NPVmpShWobRqXY3q7Vp+80DqgxxUrUIA=
@@ -1106,8 +1084,6 @@ github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdh
github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc=
github.com/sergi/go-diff v1.0.0 h1:Kpca3qRNrduNnOQeazBd0ysaKrUJiIuISHxogkT9RPQ=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/sergi/go-diff v1.1.0 h1:we8PVUC3FE2uYfodKH/nBHMSetSfHDR6scGdBi+erh0=
github.com/sergi/go-diff v1.1.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24 h1:pntxY8Ary0t43dCZ5dqY4YTJCObLY1kIXl0uzMv+7DE=
github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4=
github.com/shopspring/decimal v0.0.0-20200227202807-02e2044944cc h1:jUIKcSPO9MoMJBbEoyE/RJoE8vz7Mb8AjvifMMwSyvY=
@@ -1132,14 +1108,8 @@ github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4k
github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY=
github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA=
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
github.com/spf13/afero v1.2.2 h1:5jhuqJyZCZf2JRofRvN/nIFgIWNzPa3/Vz8mYylgbWc=
github.com/spf13/afero v1.2.2/go.mod h1:9ZxEEn6pIJ8Rxe320qSDBk6AsU0r9pR7Q4OcevTdifk=
github.com/spf13/afero v1.3.2 h1:GDarE4TJQI52kYSbSAmLiId1Elfj+xgSDqrUZxFhxlU=
github.com/spf13/afero v1.3.2/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4=
github.com/spf13/afero v1.3.4 h1:8q6vk3hthlpb2SouZcnBVKboxWQWMDNF38bwholZrJc=
github.com/spf13/afero v1.3.4/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/afero v1.3.5 h1:AWZ/w4lcfxuh52NVL78p9Eh8j6r1mCTEGSRFBJyIHAE=
github.com/spf13/afero v1.3.5/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I=
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
github.com/spf13/cobra v0.0.2-0.20171109065643-2da4a54c5cee/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
@@ -1185,6 +1155,7 @@ github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVM
github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY=
github.com/ulikunitz/xz v0.5.5 h1:pFrO0lVpTBXLpYw+pnLj6TbvHuyjXMfjGeCwSqCVwok=
github.com/ulikunitz/xz v0.5.5/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8=
github.com/ulikunitz/xz v0.5.7/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/ulikunitz/xz v0.5.8 h1:ERv8V6GKqVi23rgu5cj9pVfVzJbOqAY2Ntl88O6c2nQ=
github.com/ulikunitz/xz v0.5.8/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14=
github.com/urfave/cli v0.0.0-20171014202726-7bc6a0acffa5/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA=
@@ -1211,19 +1182,10 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/zalando/go-keyring v0.1.0 h1:ffq972Aoa4iHNzBlUHgK5Y+k8+r/8GvcGd80/OFZb/k=
github.com/zalando/go-keyring v0.1.0/go.mod h1:RaxNwUITJaHVdQ0VC7pELPZ3tOWn13nr0gZMZEhpVU0=
github.com/zclconf/go-cty v1.0.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.1.0 h1:uJwc9HiBOCpoKIObTQaLR+tsEXx1HBHnOsOOpcdhZgw=
github.com/zclconf/go-cty v1.1.0/go.mod h1:xnAOWiHeOqg2nWS62VtQ7pbOu17FtxJNW8RLEih+O3s=
github.com/zclconf/go-cty v1.2.0/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.2.1/go.mod h1:hOPWgoHbaTUnI5k4D2ld+GRpFJSCe6bCM7m1q/N4PQ8=
github.com/zclconf/go-cty v1.6.0 h1:ZuaqX/4p43EWNzVkYOm1Xh4JNcx+j/JvQqU5IJV9uQY=
github.com/zclconf/go-cty v1.6.0/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o=
github.com/zclconf/go-cty v1.6.1 h1:wHtZ+LSSQVwUSb+XIJ5E9hgAQxyWATZsAWT+ESJ9dQ0=
github.com/zclconf/go-cty v1.6.1/go.mod h1:VDR4+I79ubFBGm1uJac1226K5yANQFHeauxPBoP54+o=
github.com/zclconf/go-cty-yaml v1.0.1 h1:up11wlgAaDvlAGENcFDnZgkn0qUJurso7k6EpURKNF8=
github.com/zclconf/go-cty-yaml v1.0.1/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
github.com/zclconf/go-cty-yaml v1.0.2 h1:dNyg4QLTrv2IfJpm7Wtxi55ed5gLGOlPrZ6kMd51hY0=
github.com/zclconf/go-cty-yaml v1.0.2/go.mod h1:IP3Ylp0wQpYm50IHK8OZWKMu6sPJIUgKa8XhiVHura0=
github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q=
gitlab.com/nyarla/go-crypt v0.0.0-20160106005555-d9a5dc2b789b/go.mod h1:T3BPAOm2cqquPa0MKWeNkmOM5RQsRhkrwMWonFMN7fE=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@@ -1362,8 +1324,6 @@ golang.org/x/net v0.0.0-20190827160401-ba9fcec4b297/go.mod h1:z5CRVTTTmAJ677TzLL
golang.org/x/net v0.0.0-20191002035440-2ec189313ef0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191003171128-d98b1b443823/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191004110552-13f9640d40b9/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191009170851-d66e71096ffb h1:TR699M2v0qoKTOHxeLgp6zPqaQNs74f01a/ob9W0qko=
golang.org/x/net v0.0.0-20191009170851-d66e71096ffb/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
@@ -1628,7 +1588,6 @@ google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfG
google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200310143817-43be25429f5a/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200323114720-3f67cca34472/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c=
+16
View File
@@ -0,0 +1,16 @@
package provider
const (
// Key used for common "name" resource attribute
NameKey = "name"
// Key used for common "description" resource attribute
DescriptionKey = "description"
// Key used for common "scope_id" resource attribute
ScopeIdKey = "scope_id"
// Key used for common "type" resource attribute
TypeKey = "type"
// Key used for common "host_catalog_id" resource attribute
HostCatalogIdKey = "host_catalog_id"
// Key used for common "auth_method_id" resource attribute
AuthMethodIdKey = "auth_method_id"
)
+43
View File
@@ -0,0 +1,43 @@
// Helpers for dealing with file paths and their contents
//
// Originally from Terraform; see
// https://www.terraform.io/docs/extend/guides/v2-upgrade-guide.html#removal-of-helper-pathorcontents-package
package provider
import (
"io/ioutil"
"os"
"github.com/mitchellh/go-homedir"
)
// If the argument is a path, Read loads it and returns the contents,
// otherwise the argument is assumed to be the desired contents and is simply
// returned.
//
// The boolean second return value can be called `wasPath` - it indicates if a
// path was detected and a file loaded.
func ReadPathOrContents(poc string) (string, bool, error) {
if len(poc) == 0 {
return poc, false, nil
}
path := poc
if path[0] == '~' {
var err error
path, err = homedir.Expand(path)
if err != nil {
return path, true, err
}
}
if _, err := os.Stat(path); err == nil {
contents, err := ioutil.ReadFile(path)
if err != nil {
return string(contents), true, err
}
return string(contents), true, nil
}
return poc, false, nil
}
+115 -67
View File
@@ -3,123 +3,171 @@ package provider
import (
"context"
"errors"
"fmt"
"strings"
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/authmethods"
"github.com/hashicorp/boundary/api/authtokens"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/hashicorp/boundary/sdk/wrapper"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func New() terraform.ResourceProvider {
func New() *schema.Provider {
p := &schema.Provider{
Schema: map[string]*schema.Schema{
"default_scope": {
Type: schema.TypeString,
Optional: true,
Description: "The Boundary scope to operate all actions in if not provided in the individual resources. This is the scope at which the provider authenticates itself.",
},
"base_url": {
"addr": {
Type: schema.TypeString,
Required: true,
Description: "The base url of the Boundary API. For example 'http://127.0.0.1/'",
Description: `The base url of the Boundary API, e.g. "http://127.0.0.1". If not set, it will be read from the "BOUNDARY_ADDR" env var.`,
},
"token": {
Type: schema.TypeString,
Optional: true,
Description: `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.`,
},
"recovery_kms_hcl": {
Type: schema.TypeString,
Optional: true,
Description: "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.",
},
"auth_method_id": {
Type: schema.TypeString,
Required: true,
Description: "The auth method ID. Example am_1234567890",
Optional: true,
Description: "The auth method ID e.g. ampw_1234567890",
},
"auth_method_username": {
"password_auth_method_login_name": {
Type: schema.TypeString,
Required: true,
Description: "The auth method username",
Optional: true,
Description: "The auth method login name for password-style auth methods",
},
"auth_method_password": {
"password_auth_method_password": {
Type: schema.TypeString,
Required: true,
Description: "The auth method password",
Optional: true,
Description: "The auth method password for password-style auth methods",
},
},
ResourcesMap: map[string]*schema.Resource{
"boundary_account": resourceAccount(),
"boundary_auth_method": resourceAuthMethod(),
"boundary_group": resourceGroup(),
"boundary_host": resourceHost(),
"boundary_host_catalog": resourceHostCatalog(),
"boundary_host_set": resourceHostset(),
"boundary_organization": resourceOrganization(),
"boundary_project": resourceProject(),
"boundary_role": resourceRole(),
"boundary_scope": resourceScope(),
"boundary_target": resourceTarget(),
"boundary_user": resourceUser(),
},
}
p.ConfigureFunc = providerConfigure(p)
p.ConfigureContextFunc = providerConfigure(p)
return p
}
type metaData struct {
client *api.Client
authToken *authtokens.AuthToken
ctx context.Context
client *api.Client
recoveryKmsWrapper wrapping.Wrapper
}
func providerAuthenticate(d *schema.ResourceData, client *api.Client) (*authtokens.AuthToken, error) {
authMethodID, ok := d.GetOk("auth_method_id")
if !ok {
return nil, errors.New("auth method ID not set, please set auth_method_id on the provider")
func providerAuthenticate(ctx context.Context, d *schema.ResourceData, md *metaData) error {
var credentials map[string]interface{}
authMethodId, authMethodIdOk := d.GetOk("auth_method_id")
recoveryKmsHcl, recoveryKmsHclOk := d.GetOk("recovery_kms_hcl")
if token, ok := d.GetOk("token"); ok {
md.client.SetToken(token.(string))
}
authMethodUser, ok := d.GetOk("auth_method_username")
if !ok {
return nil, errors.New("auth method username not set, please set auth_method_username on the provider")
switch {
case recoveryKmsHclOk:
recoveryHclStr, _, err := ReadPathOrContents(recoveryKmsHcl.(string))
if err != nil {
return fmt.Errorf(`error reading data from "recovery_kms_hcl": %v`, err)
}
wrapper, err := wrapper.GetWrapperFromHcl(recoveryHclStr, "recovery")
if err != nil {
return fmt.Errorf(`error reading wrappers from "recovery_kms_hcl": %v`, err)
}
if wrapper == nil {
return errors.New(`No "kms" block with purpose "recovery" found in "recovery_kms_hcl"`)
}
if err := wrapper.Init(ctx); err != nil {
return fmt.Errorf("error initializing recovery kms: %v", err)
}
md.recoveryKmsWrapper = wrapper
md.client.SetRecoveryKmsWrapper(wrapper)
return nil
case md.client.Token() != "":
// Use the token sourced from the conf file or env var
case authMethodIdOk:
switch {
case strings.HasPrefix(authMethodId.(string), "ampw"):
// Password-style
authMethodLoginName, ok := d.GetOk("password_auth_method_login_name")
if !ok {
return errors.New("password-style auth method login name not set, please set password_auth_method_login_name on the provider")
}
authMethodPassword, ok := d.GetOk("password_auth_method_password")
if !ok {
return errors.New("password-style auth method password not set, please set password_auth_method_password on the provider")
}
credentials = map[string]interface{}{
"login_name": authMethodLoginName,
"password": authMethodPassword,
}
default:
return errors.New("no suitable typed auth method information found")
}
am := authmethods.NewClient(md.client)
at, apiErr, err := am.Authenticate(ctx, authMethodId.(string), credentials)
if apiErr != nil {
return errors.New(apiErr.Message)
}
if err != nil {
return err
}
md.client.SetToken(at.Item.Token)
default:
return errors.New("no suitable auth method information found")
}
authMethodPass, ok := d.GetOk("auth_method_password")
if !ok {
return nil, errors.New("auth method password not set, please set the auth_method_password on the provider")
}
am := authmethods.NewClient(client)
ctx := context.Background()
// note: Authenticate() calls SetToken() under the hood to set the
// auth bearer on the client so we do not need to do anything with the
// returned token after this call, so we ignore it
at, apiErr, err := am.Authenticate(ctx, authMethodID.(string), authMethodUser.(string), authMethodPass.(string))
if apiErr != nil {
return at, errors.New(apiErr.Message)
}
if err != nil {
return at, err
}
return at, nil
return nil
}
func providerConfigure(p *schema.Provider) schema.ConfigureFunc {
return func(d *schema.ResourceData) (interface{}, error) {
func providerConfigure(p *schema.Provider) schema.ConfigureContextFunc {
return func(ctx context.Context, d *schema.ResourceData) (interface{}, diag.Diagnostics) {
client, err := api.NewClient(nil)
if err != nil {
return nil, err
return nil, diag.FromErr(err)
}
if err := client.SetAddr(d.Get("base_url").(string)); err != nil {
return nil, err
if url, ok := d.GetOk("addr"); ok {
client.SetAddr(url.(string))
}
if client.Addr() == "" {
return nil, diag.Errorf(`"no valid address could be determined from "addr" or "BOUNDARY_ADDR" env var`)
}
client.SetScopeId(d.Get("default_scope").(string))
client.SetLimiter(5, 5)
at, err := providerAuthenticate(d, client)
if err != nil {
return nil, err
md := &metaData{
client: client,
}
// need to use 'at' in the provider config to get the principal ID for use
// in overriding grant scope for project level configuration
return &metaData{
client: client,
authToken: at,
ctx: p.StopContext()}, nil
if err := providerAuthenticate(ctx, d, md); err != nil {
return nil, diag.FromErr(err)
}
return md, nil
}
}
+75 -21
View File
@@ -2,6 +2,7 @@ package provider
import (
"context"
"encoding/base64"
"fmt"
"strings"
"testing"
@@ -9,60 +10,113 @@ import (
"github.com/hashicorp/boundary/api"
"github.com/hashicorp/boundary/api/roles"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
wrapping "github.com/hashicorp/go-kms-wrapping"
"github.com/hashicorp/go-kms-wrapping/wrappers/aead"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
var testProvider *schema.Provider
var testProviders map[string]terraform.ResourceProvider
var testProviders map[string]*schema.Provider
var (
tcUsername = "user"
tcPassword = "passpass"
tcPAUM = "ampw_0000000000"
tcOrg = "global"
tcConfig = []controller.Option{
tcLoginName = "user"
tcPassword = "passpass"
tcPAUM = "ampw_0000000000"
tcConfig = []controller.Option{
controller.WithDefaultAuthMethodId(tcPAUM),
controller.WithDefaultLoginName(tcUsername),
controller.WithDefaultLoginName(tcLoginName),
controller.WithDefaultPassword(tcPassword),
}
tcRecoveryKey = "7xtkEoS5EXPbgynwd+dDLHopaCqK8cq0Rpep4eooaTs="
)
func init() {
testProvider = New().(*schema.Provider)
testProviders = map[string]terraform.ResourceProvider{
testProvider = New()
testProviders = map[string]*schema.Provider{
"boundary": testProvider,
}
}
func testWrapper(t *testing.T, key string) wrapping.Wrapper {
wrapper := aead.NewWrapper(nil)
keyBytes, err := base64.StdEncoding.DecodeString(key)
if err != nil {
t.Fatal(err)
}
_, err = wrapper.SetConfig(map[string]string{
"key_id": key,
})
if err != nil {
t.Fatal(err)
}
if err := wrapper.SetAESGCMKeyBytes(keyBytes); err != nil {
t.Fatal(err)
}
return wrapper
}
func testConfig(url string, res ...string) string {
provider := fmt.Sprintf(`
provider "boundary" {
base_url = "%s"
default_scope = "%s"
addr = "%s"
auth_method_id = "%s"
auth_method_username = "%s"
auth_method_password = "%s"
}`, url, tcOrg, tcPAUM, tcUsername, tcPassword)
password_auth_method_login_name = "%s"
password_auth_method_password = "%s"
}`, url, tcPAUM, tcLoginName, tcPassword)
c := []string{provider}
c = append(c, res...)
return strings.Join(c, "\n")
}
func setGrantScopeIDonProject(projID string, principalID string, client *api.Client) error {
func testConfigWithToken(url, token string, res ...string) string {
provider := fmt.Sprintf(`
provider "boundary" {
addr = "%s"
token = "%s"
}`, url, token)
c := []string{provider}
c = append(c, res...)
return strings.Join(c, "\n")
}
func testConfigWithRecovery(url string, res ...string) string {
provider := fmt.Sprintf(`
provider "boundary" {
addr = "%s"
auth_method_id = "%s"
password_auth_method_login_name = "%s"
password_auth_method_password = "%s"
recovery_kms_hcl = <<DOC
kms "aead" {
purpose = ["recovery", "config"]
aead_type = "aes-gcm"
key = "7xtkEoS5EXPbgynwd+dDLHopaCqK8cq0Rpep4eooaTs="
key_id = "global_recovery"
}
DOC
}`, url, tcPAUM, tcLoginName, tcPassword)
c := []string{provider}
c = append(c, res...)
return strings.Join(c, "\n")
}
func setGrantScopeIdOnProject(scopeId, grantScopeId, principalId string, client *api.Client) error {
roleClient := roles.NewClient(client)
_, err, _ := roleClient.Create(
context.Background(),
roles.WithName(fmt.Sprintf("TestRole_%s", projID)),
roles.WithDescription(fmt.Sprintf("Terraform test management role for %s", projID)),
roles.WithGrantScopeId(projID))
scopeId,
roles.WithName(fmt.Sprintf("TestRole_%s", grantScopeId)),
roles.WithDescription(fmt.Sprintf("Terraform test management role for %s", grantScopeId)),
roles.WithGrantScopeId(grantScopeId))
return fmt.Errorf("%s", err.Message)
}
func TestProvider(t *testing.T) {
if err := New().(*schema.Provider).InternalValidate(); err != nil {
if err := New().InternalValidate(); err != nil {
t.Fatalf("err: %s", err)
}
}
+271
View File
@@ -0,0 +1,271 @@
package provider
import (
"context"
"github.com/hashicorp/boundary/api/accounts"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
accountTypePassword = "password"
accountLoginNameKey = "login_name"
accountPasswordKey = "password"
)
func resourceAccount() *schema.Resource {
return &schema.Resource{
CreateContext: resourceAccountCreate,
ReadContext: resourceAccountRead,
UpdateContext: resourceAccountUpdate,
DeleteContext: resourceAccountDelete,
Schema: map[string]*schema.Schema{
NameKey: {
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
AuthMethodIdKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
TypeKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
accountLoginNameKey: {
Type: schema.TypeString,
Optional: true,
},
accountPasswordKey: {
Type: schema.TypeString,
Optional: true,
},
},
}
}
func setFromAccountResponseMap(d *schema.ResourceData, raw map[string]interface{}) {
d.Set(NameKey, raw["name"])
d.Set(DescriptionKey, raw["description"])
d.Set(AuthMethodIdKey, raw["auth_method_id"])
d.Set(TypeKey, raw["type"])
switch raw["type"].(string) {
case accountTypePassword:
if attrsVal, ok := raw["attributes"]; ok {
attrs := attrsVal.(map[string]interface{})
d.Set(accountLoginNameKey, attrs["login_name"])
d.Set(accountPasswordKey, attrs["password"])
}
}
d.SetId(raw["id"].(string))
}
func resourceAccountCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
var authMethodId string
if authMethodIdVal, ok := d.GetOk(AuthMethodIdKey); ok {
authMethodId = authMethodIdVal.(string)
} else {
return diag.Errorf("no auth method ID provided")
}
var loginName *string
if keyVal, ok := d.GetOk(accountLoginNameKey); ok {
key := keyVal.(string)
loginName = &key
}
var password *string
if keyVal, ok := d.GetOk(accountPasswordKey); ok {
key := keyVal.(string)
password = &key
}
opts := []accounts.Option{}
var typeStr string
if typeVal, ok := d.GetOk(TypeKey); ok {
typeStr = typeVal.(string)
} else {
return diag.Errorf("no type provided")
}
switch typeStr {
case accountTypePassword:
if loginName != nil {
opts = append(opts, accounts.WithPasswordAccountLoginName(*loginName))
}
if password != nil {
opts = append(opts, accounts.WithPasswordAccountPassword(*password))
}
default:
return diag.Errorf("invalid type provided")
}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, accounts.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, accounts.WithDescription(descStr))
}
aClient := accounts.NewClient(md.client)
acr, apiErr, err := aClient.Create(
ctx,
authMethodId,
opts...)
if err != nil {
return diag.Errorf("error calling create account: %v", err)
}
if apiErr != nil {
return diag.Errorf("error creating account: %s", apiErr.Message)
}
if acr == nil {
return diag.Errorf("nil account after create")
}
setFromAccountResponseMap(d, acr.GetResponseMap())
return nil
}
func resourceAccountRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
aClient := accounts.NewClient(md.client)
arr, apiErr, err := aClient.Read(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling read account: %v", err)
}
if apiErr != nil {
return diag.Errorf("error reading account: %s", apiErr.Message)
}
if arr == nil {
return diag.Errorf("account nil after read")
}
setFromAccountResponseMap(d, arr.GetResponseMap())
return nil
}
func resourceAccountUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
aClient := accounts.NewClient(md.client)
opts := []accounts.Option{}
var name *string
if d.HasChange(NameKey) {
opts = append(opts, accounts.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, accounts.WithName(nameStr))
}
}
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, accounts.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, accounts.WithDescription(descStr))
}
}
var loginName *string
if d.HasChange(accountLoginNameKey) {
switch d.Get(TypeKey).(string) {
case accountTypePassword:
opts = append(opts, accounts.DefaultPasswordAccountLoginName())
keyVal, ok := d.GetOk(accountLoginNameKey)
if ok {
keyStr := keyVal.(string)
loginName = &keyStr
opts = append(opts, accounts.WithPasswordAccountLoginName(keyStr))
}
default:
return diag.Errorf(`"login_name" cannot be used with this type of account`)
}
}
var password *string
if d.HasChange(accountPasswordKey) {
switch d.Get(TypeKey).(string) {
case accountTypePassword:
opts = append(opts, accounts.DefaultPasswordAccountPassword())
keyVal, ok := d.GetOk(accountPasswordKey)
if ok {
keyStr := keyVal.(string)
password = &keyStr
opts = append(opts, accounts.WithPasswordAccountPassword(keyStr))
}
default:
return diag.Errorf(`"password" cannot be used with this type of account`)
}
}
if len(opts) > 0 {
opts = append(opts, accounts.WithAutomaticVersioning(true))
_, apiErr, err := aClient.Update(
ctx,
d.Id(),
0,
opts...)
if err != nil {
return diag.Errorf("error calling update account: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating account: %s", apiErr.Message)
}
}
if d.HasChange(NameKey) {
d.Set(NameKey, name)
}
if d.HasChange(DescriptionKey) {
d.Set(DescriptionKey, desc)
}
if d.HasChange(accountLoginNameKey) {
d.Set(accountLoginNameKey, loginName)
}
if d.HasChange(accountPasswordKey) {
d.Set(accountPasswordKey, password)
}
return nil
}
func resourceAccountDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
aClient := accounts.NewClient(md.client)
_, apiErr, err := aClient.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling delete account: %v", err)
}
if apiErr != nil {
return diag.Errorf("error deleting account: %s", apiErr.Message)
}
return nil
}
+279
View File
@@ -0,0 +1,279 @@
package provider
import (
"context"
"encoding/json"
"github.com/hashicorp/boundary/api/authmethods"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
authmethodTypePassword = "password"
authmethodMinLoginNameLengthKey = "min_login_name_length"
authmethodMinPasswordLengthKey = "min_password_length"
)
func resourceAuthMethod() *schema.Resource {
return &schema.Resource{
CreateContext: resourceAuthMethodCreate,
ReadContext: resourceAuthMethodRead,
UpdateContext: resourceAuthMethodUpdate,
DeleteContext: resourceAuthMethodDelete,
Schema: map[string]*schema.Schema{
NameKey: {
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
ScopeIdKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
TypeKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
authmethodMinLoginNameLengthKey: {
Type: schema.TypeInt,
Optional: true,
},
authmethodMinPasswordLengthKey: {
Type: schema.TypeInt,
Optional: true,
},
},
}
}
func setFromAuthMethodResponseMap(d *schema.ResourceData, raw map[string]interface{}) {
d.Set(NameKey, raw["name"])
d.Set(DescriptionKey, raw["description"])
d.Set(ScopeIdKey, raw["scope_id"])
d.Set(TypeKey, raw["type"])
switch raw["type"].(string) {
case authmethodTypePassword:
if attrsVal, ok := raw["attributes"]; ok {
attrs := attrsVal.(map[string]interface{})
minLoginNameLength := attrs["min_login_name_length"].(json.Number)
minLoginNameLengthInt, _ := minLoginNameLength.Int64()
d.Set(authmethodMinLoginNameLengthKey, int(minLoginNameLengthInt))
minPasswordLength := attrs["min_password_length"].(json.Number)
minPasswordLengthInt, _ := minPasswordLength.Int64()
d.Set(authmethodMinPasswordLengthKey, int(minPasswordLengthInt))
}
}
d.SetId(raw["id"].(string))
}
func resourceAuthMethodCreate(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 minLoginNameLength *int
if minLengthVal, ok := d.GetOk(authmethodMinLoginNameLengthKey); ok {
minLength := minLengthVal.(int)
minLoginNameLength = &minLength
}
var minPasswordLength *int
if minLengthVal, ok := d.GetOk(authmethodMinPasswordLengthKey); ok {
minLength := minLengthVal.(int)
minPasswordLength = &minLength
}
opts := []authmethods.Option{}
var typeStr string
if typeVal, ok := d.GetOk(TypeKey); ok {
typeStr = typeVal.(string)
} else {
return diag.Errorf("no type provided")
}
switch typeStr {
case authmethodTypePassword:
if minLoginNameLength != nil {
opts = append(opts, authmethods.WithPasswordAuthMethodMinLoginNameLength(uint32(*minLoginNameLength)))
}
if minPasswordLength != nil {
opts = append(opts, authmethods.WithPasswordAuthMethodMinPasswordLength(uint32(*minPasswordLength)))
}
default:
return diag.Errorf("invalid type provided")
}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, authmethods.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, authmethods.WithDescription(descStr))
}
amClient := authmethods.NewClient(md.client)
amcr, apiErr, err := amClient.Create(
ctx,
typeStr,
scopeId,
opts...)
if err != nil {
return diag.Errorf("error calling create auth method: %v", err)
}
if apiErr != nil {
return diag.Errorf("error creating auth method: %s", apiErr.Message)
}
if amcr == nil {
return diag.Errorf("nil auth method after create")
}
setFromAuthMethodResponseMap(d, amcr.GetResponseMap())
return nil
}
func resourceAuthMethodRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
amClient := authmethods.NewClient(md.client)
amrr, apiErr, err := amClient.Read(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling read auth method: %v", err)
}
if apiErr != nil {
return diag.Errorf("error reading auth method: %s", apiErr.Message)
}
if amrr == nil {
return diag.Errorf("auth method nil after read")
}
setFromAuthMethodResponseMap(d, amrr.GetResponseMap())
return nil
}
func resourceAuthMethodUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
amClient := authmethods.NewClient(md.client)
opts := []authmethods.Option{}
var name *string
if d.HasChange(NameKey) {
opts = append(opts, authmethods.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, authmethods.WithName(nameStr))
}
}
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, authmethods.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, authmethods.WithDescription(descStr))
}
}
var minLoginNameLength *int
if d.HasChange(authmethodMinLoginNameLengthKey) {
switch d.Get(TypeKey).(string) {
case authmethodTypePassword:
opts = append(opts, authmethods.DefaultPasswordAuthMethodMinLoginNameLength())
minLengthVal, ok := d.GetOk(authmethodMinLoginNameLengthKey)
if ok {
minLengthInt := minLengthVal.(int)
minLoginNameLength = &minLengthInt
opts = append(opts, authmethods.WithPasswordAuthMethodMinLoginNameLength(uint32(minLengthInt)))
}
default:
return diag.Errorf(`"min_login_name_length" cannot be used with this type of auth method`)
}
}
var minPasswordLength *int
if d.HasChange(authmethodMinPasswordLengthKey) {
switch d.Get(TypeKey).(string) {
case authmethodTypePassword:
opts = append(opts, authmethods.DefaultPasswordAuthMethodMinPasswordLength())
minLengthVal, ok := d.GetOk(authmethodMinPasswordLengthKey)
if ok {
minLengthInt := minLengthVal.(int)
minPasswordLength = &minLengthInt
opts = append(opts, authmethods.WithPasswordAuthMethodMinPasswordLength(uint32(minLengthInt)))
}
default:
return diag.Errorf(`"min_password_length" cannot be used with this type of auth method`)
}
}
if len(opts) > 0 {
opts = append(opts, authmethods.WithAutomaticVersioning(true))
_, apiErr, err := amClient.Update(
ctx,
d.Id(),
0,
opts...)
if err != nil {
return diag.Errorf("error calling update auth method: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating auth method: %s", apiErr.Message)
}
}
if d.HasChange(NameKey) {
d.Set(NameKey, name)
}
if d.HasChange(DescriptionKey) {
d.Set(DescriptionKey, desc)
}
if d.HasChange(authmethodMinLoginNameLengthKey) {
d.Set(authmethodMinLoginNameLengthKey, minLoginNameLength)
}
if d.HasChange(authmethodMinPasswordLengthKey) {
d.Set(authmethodMinPasswordLengthKey, minPasswordLength)
}
return nil
}
func resourceAuthMethodDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
amClient := authmethods.NewClient(md.client)
_, apiErr, err := amClient.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling delete auth method: %v", err)
}
if apiErr != nil {
return diag.Errorf("error deleting auth method: %s", apiErr.Message)
}
return nil
}
+173 -184
View File
@@ -1,42 +1,38 @@
package provider
import (
"fmt"
"context"
"github.com/hashicorp/boundary/api/groups"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
groupNameKey = "name"
groupDescriptionKey = "description"
groupScopeIDKey = "scope_id"
groupMemberIDsKey = "member_ids"
groupMemberIdsKey = "member_ids"
)
func resourceGroup() *schema.Resource {
return &schema.Resource{
Create: resourceGroupCreate,
Read: resourceGroupRead,
Update: resourceGroupUpdate,
Delete: resourceGroupDelete,
CreateContext: resourceGroupCreate,
ReadContext: resourceGroupRead,
UpdateContext: resourceGroupUpdate,
DeleteContext: resourceGroupDelete,
Schema: map[string]*schema.Schema{
groupNameKey: {
NameKey: {
Type: schema.TypeString,
Optional: true,
},
groupDescriptionKey: {
DescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
groupScopeIDKey: {
ScopeIdKey: {
Type: schema.TypeString,
Optional: true,
Required: true,
ForceNew: true,
Computed: true,
},
groupMemberIDsKey: {
groupMemberIdsKey: {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
@@ -45,199 +41,192 @@ func resourceGroup() *schema.Resource {
}
}
// convertGroupToResourceData creates a ResourceData type from a Group
func convertGroupToResourceData(g *groups.Group, d *schema.ResourceData) error {
if g.Name != "" {
if err := d.Set(groupNameKey, g.Name); err != nil {
return err
}
}
if g.Description != "" {
if err := d.Set(groupDescriptionKey, g.Description); err != nil {
return err
}
}
// TODO when calling SetMembers() on a group the API returns
// a nil ScopeInfo so we are checking here to ensure
// we catch it. This work around can possibly ignore
// an updated scope ID when updating members and scope
// of a group simultaniously.
if g.Scope != nil && g.Scope.Id != "" {
if err := d.Set(groupScopeIDKey, g.Scope.Id); err != nil {
return err
}
}
if g.MemberIds != nil {
if err := d.Set(groupMemberIDsKey, g.MemberIds); err != nil {
return err
}
}
d.SetId(g.Id)
return nil
func setFromGroupResponseMap(d *schema.ResourceData, raw map[string]interface{}) {
d.Set(NameKey, raw["name"])
d.Set(DescriptionKey, raw["description"])
d.Set(ScopeIdKey, raw["scope_id"])
d.Set(groupMemberIdsKey, raw["member_ids"])
d.SetId(raw["id"].(string))
}
// convertResourceDataToGroup returns a localy built Group using the values provided in the ResourceData.
func convertResourceDataToGroup(d *schema.ResourceData, meta *metaData) *groups.Group {
g := &groups.Group{Scope: &scopes.ScopeInfo{}}
if descVal, ok := d.GetOk(groupDescriptionKey); ok {
g.Description = descVal.(string)
}
if nameVal, ok := d.GetOk(groupNameKey); ok {
g.Name = nameVal.(string)
}
if scopeIDVal, ok := d.GetOk(groupScopeIDKey); ok {
g.Scope.Id = scopeIDVal.(string)
}
if val, ok := d.GetOk(groupMemberIDsKey); ok {
memberIds := val.(*schema.Set).List()
for _, i := range memberIds {
g.MemberIds = append(g.MemberIds, i.(string))
}
}
if d.Id() != "" {
g.Id = d.Id()
}
return g
}
func resourceGroupCreate(d *schema.ResourceData, meta interface{}) error {
func resourceGroupCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
g := convertResourceDataToGroup(d, md)
grps := groups.NewClient(client)
var scopeId string
if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok {
scopeId = scopeIdVal.(string)
} else {
return diag.Errorf("no scope ID provided")
}
memIDs := g.MemberIds
opts := []groups.Option{}
g, apiErr, err := grps.Create(
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, groups.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, groups.WithDescription(descStr))
}
grps := groups.NewClient(md.client)
gcr, apiErr, err := grps.Create(
ctx,
groups.WithScopeId(g.Scope.Id),
groups.WithName(g.Name),
groups.WithDescription(g.Description))
scopeId,
opts...)
if err != nil {
return fmt.Errorf("error creating group: %s", err.Error())
return diag.Errorf("error calling create group: %v", err)
}
if apiErr != nil {
return fmt.Errorf("error creating group: %s", apiErr.Message)
return diag.Errorf("error creating group: %s", apiErr.Message)
}
if gcr == nil {
return diag.Errorf("group nil after create")
}
raw := gcr.GetResponseMap()
if len(memIDs) > 0 {
g, apiErr, err = grps.SetMembers(
if val, ok := d.GetOk(groupMemberIdsKey); ok {
list := val.(*schema.Set).List()
memberIds := make([]string, 0, len(list))
for _, i := range list {
memberIds = append(memberIds, i.(string))
}
gcsmr, apiErr, err := grps.SetMembers(
ctx,
g.Id,
g.Version,
memIDs,
groups.WithScopeId(g.Scope.Id))
gcr.Item.Id,
gcr.Item.Version,
memberIds)
if apiErr != nil {
return fmt.Errorf("error setting principals on role: %s\n", apiErr.Message)
return diag.Errorf("error setting principals on role: %s", apiErr.Message)
}
if err != nil {
return fmt.Errorf("error setting principals on role: %s\n", err)
return diag.Errorf("error setting principals on role: %v", err)
}
if gcsmr == nil {
return diag.Errorf("group nil after setting members")
}
raw = gcsmr.GetResponseMap()
}
setFromGroupResponseMap(d, raw)
return nil
}
func resourceGroupRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
grps := groups.NewClient(md.client)
g, apiErr, err := grps.Read(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling read group: %v", err)
}
if apiErr != nil {
return diag.Errorf("error reading group: %s", apiErr.Message)
}
if g == nil {
return diag.Errorf("group nil after read")
}
setFromGroupResponseMap(d, g.GetResponseMap())
return nil
}
func resourceGroupUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
grps := groups.NewClient(md.client)
opts := []groups.Option{}
var name *string
if d.HasChange(NameKey) {
opts = append(opts, groups.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, groups.WithName(nameStr))
}
}
return convertGroupToResourceData(g, d)
}
func resourceGroupRead(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
g := convertResourceDataToGroup(d, md)
grps := groups.NewClient(client)
g, apiErr, err := grps.Read(ctx, g.Id, groups.WithScopeId(g.Scope.Id))
if err != nil {
return fmt.Errorf("error reading group: %s", err.Error())
}
if apiErr != nil {
return fmt.Errorf("error reading group: %s", apiErr.Message)
}
return convertGroupToResourceData(g, d)
}
func resourceGroupUpdate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
g := convertResourceDataToGroup(d, md)
grps := groups.NewClient(client)
if d.HasChange(groupNameKey) {
g.Name = d.Get(groupNameKey).(string)
}
if d.HasChange(groupDescriptionKey) {
g.Description = d.Get(groupDescriptionKey).(string)
}
g.Scope.Id = d.Get(groupScopeIDKey).(string)
g, apiErr, err := grps.Update(
ctx,
g.Id,
0,
groups.WithScopeId(g.Scope.Id),
groups.WithAutomaticVersioning(),
groups.WithName(g.Name),
groups.WithDescription(g.Description))
if err != nil {
return err
}
if apiErr != nil {
return fmt.Errorf("%+v\n", apiErr.Message)
}
if d.HasChange(groupMemberIDsKey) {
memberIds := []string{}
members := d.Get(groupMemberIDsKey).(*schema.Set).List()
for _, member := range members {
memberIds = append(memberIds, member.(string))
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, groups.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, groups.WithDescription(descStr))
}
}
g, apiErr, err = grps.SetMembers(
if len(opts) > 0 {
opts = append(opts, groups.WithAutomaticVersioning(true))
_, apiErr, err := grps.Update(
ctx,
g.Id,
g.Version,
memberIds,
groups.WithScopeId(g.Scope.Id))
if apiErr != nil || err != nil {
return fmt.Errorf("error updating members on group:\n API Err: %+v\n Err: %+v\n", *apiErr, err)
d.Id(),
0,
opts...)
if err != nil {
return diag.Errorf("error calling update group: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating group: %s", apiErr.Message)
}
}
return convertGroupToResourceData(g, d)
}
func resourceGroupDelete(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
g := convertResourceDataToGroup(d, md)
grps := groups.NewClient(client)
_, apiErr, err := grps.Delete(ctx, g.Id, groups.WithScopeId(g.Scope.Id))
if err != nil {
return fmt.Errorf("error deleting group: %s", err.Error())
if d.HasChange(NameKey) {
d.Set(NameKey, name)
}
if apiErr != nil {
return fmt.Errorf("error deleting group: %s", apiErr.Message)
if d.HasChange(DescriptionKey) {
d.Set(DescriptionKey, desc)
}
// The above call may not actually happen, so we use d.Id() and automatic
// versioning here
if d.HasChange(groupMemberIdsKey) {
var memberIds []string
if membersVal, ok := d.GetOk(groupMemberIdsKey); ok {
members := membersVal.(*schema.Set).List()
for _, member := range members {
memberIds = append(memberIds, member.(string))
}
}
_, apiErr, err := grps.SetMembers(
ctx,
d.Id(),
0,
memberIds,
groups.WithAutomaticVersioning(true))
if err != nil {
return diag.Errorf("error updating members in group: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating members in group: %s", apiErr.Message)
}
d.Set(groupMemberIdsKey, memberIds)
}
return nil
}
func resourceGroupDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
grps := groups.NewClient(md.client)
_, apiErr, err := grps.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling delete group: %s", err.Error())
}
if apiErr != nil {
return diag.Errorf("error deleting group: %s", apiErr.Message)
}
return nil
+125 -119
View File
@@ -1,14 +1,16 @@
package provider
import (
"context"
"fmt"
"net/http"
"strings"
"testing"
"github.com/hashicorp/boundary/api/groups"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
@@ -18,82 +20,88 @@ const (
var (
orgGroup = fmt.Sprintf(`
resource "boundary_group" "foo" {
name = "test"
resource "boundary_group" "org1" {
name = "test"
description = "%s"
scope_id = boundary_organization.foo.id
scope_id = boundary_scope.org1.id
}`, fooGroupDescription)
orgGroupUpdate = fmt.Sprintf(`
resource "boundary_group" "foo" {
name = "test"
resource "boundary_group" "org1" {
name = "test"
description = "%s"
scope_id = boundary_organization.foo.id
scope_id = boundary_scope.org1.id
}`, fooGroupDescriptionUpdate)
orgGroupWithMembers = `
resource "boundary_user" "foo" {
description = "foo"
scope_id = boundary_organization.foo.id
resource "boundary_user" "org1" {
description = "org1"
scope_id = boundary_scope.org1.id
}
resource "boundary_group" "with_members" {
description = "with members"
member_ids = [boundary_user.foo.id]
scope_id = boundary_organization.foo.id
member_ids = [boundary_user.org1.id]
scope_id = boundary_scope.org1.id
}`
orgGroupWithMembersUpdate = `
resource "boundary_user" "foo" {
description = "foo"
scope_id = boundary_organization.foo.id
resource "boundary_user" "org1" {
description = "org1"
scope_id = boundary_scope.org1.id
}
resource "boundary_user" "bar" {
description = "bar"
scope_id = boundary_organization.foo.id
description = "bar"
scope_id = boundary_scope.org1.id
}
resource "boundary_group" "with_members" {
description = "with members"
member_ids = [boundary_user.foo.id, boundary_user.bar.id]
scope_id = boundary_organization.foo.id
member_ids = [boundary_user.org1.id, boundary_user.bar.id]
scope_id = boundary_scope.org1.id
}`
orgToProjectGroupUpdate = fmt.Sprintf(`
resource "boundary_group" "foo" {
name = "test"
description = "%s"
scope_id = boundary_project.foo.id
}`, fooGroupDescriptionUpdate)
orgToProjectGroupUpdate = `
resource "boundary_group" "org1" {
name = "test-to-proj"
description = "org1-test-to-proj"
scope_id = boundary_scope.proj1.id
}`
projGroup = fmt.Sprintf(`
resource "boundary_group" "foo" {
name = "test"
description = "%s"
scope_id = boundary_project.foo.id
}`, fooGroupDescription)
projGroup = `
resource "boundary_group" "proj1" {
name = "test-proj"
description = "desc-test-proj"
scope_id = boundary_scope.proj1.id
}`
projGroupUpdate = fmt.Sprintf(`
resource "boundary_group" "foo" {
name = "test"
description = "%s"
scope_id = boundary_project.foo.id
}`, fooGroupDescriptionUpdate)
projGroupUpdate = `
resource "boundary_group" "proj1" {
name = "test-proj-up"
description = "desc-test-proj-up"
scope_id = boundary_scope.proj1.id
}`
// TODO When removing the scope_id the provider does not revert back to the provider
// default scope. As a workaround, you can move the resource into the org scope by
// manually setting the org scope ID as the scope_id field.
projToOrgGroupUpdate = fmt.Sprintf(`
resource "boundary_group" "foo" {
name = "test"
description = "%s"
scope_id = "%s"
}`, fooGroupDescriptionUpdate, tcOrg)
projNameRemoval = `
resource "boundary_group" "proj1" {
name = ""
description = "no-name"
scope_id = boundary_scope.proj1.id
}`
projToOrgGroupUpdate = `
resource "boundary_group" "proj1" {
name = "test-back"
description = "desc-back"
scope_id = boundary_scope.org1.id
}`
)
// NOTE: this test also tests out the recovery KMS mechanism.
func TestAccGroup(t *testing.T) {
tc := controller.NewTestController(t, tcConfig...)
wrapper := testWrapper(t, tcRecoveryKey)
tc := controller.NewTestController(t, append(tcConfig, controller.WithRecoveryKms(wrapper))...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]
@@ -104,56 +112,65 @@ func TestAccGroup(t *testing.T) {
Steps: []resource.TestStep{
{
// test create
Config: testConfig(url, fooOrg, orgGroup),
Config: testConfigWithRecovery(url, fooOrg, orgGroup),
Check: resource.ComposeTestCheckFunc(
testAccCheckGroupResourceExists("boundary_group.foo"),
resource.TestCheckResourceAttr("boundary_group.foo", groupDescriptionKey, fooGroupDescription),
resource.TestCheckResourceAttr("boundary_group.foo", groupNameKey, "test"),
testAccCheckGroupResourceExists("boundary_group.org1"),
resource.TestCheckResourceAttr("boundary_group.org1", DescriptionKey, fooGroupDescription),
resource.TestCheckResourceAttr("boundary_group.org1", NameKey, "test"),
),
},
{
// test update
Config: testConfig(url, fooOrg, orgGroupUpdate),
Config: testConfigWithRecovery(url, fooOrg, orgGroupUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckGroupResourceExists("boundary_group.foo"),
resource.TestCheckResourceAttr("boundary_group.foo", groupDescriptionKey, fooGroupDescriptionUpdate),
testAccCheckGroupResourceExists("boundary_group.org1"),
resource.TestCheckResourceAttr("boundary_group.org1", DescriptionKey, fooGroupDescriptionUpdate),
),
},
{
// test update to project scope
Config: testConfig(url, fooOrg, fooProject, orgToProjectGroupUpdate),
Config: testConfigWithRecovery(url, fooOrg, firstProjectFoo, orgToProjectGroupUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckGroupResourceExists("boundary_group.foo"),
resource.TestCheckResourceAttr("boundary_group.foo", groupDescriptionKey, fooGroupDescriptionUpdate),
testAccCheckGroupProjectScope("boundary_group.foo"),
testAccCheckGroupResourceExists("boundary_group.org1"),
resource.TestCheckResourceAttr("boundary_group.org1", DescriptionKey, "org1-test-to-proj"),
testAccCheckGroupScope("boundary_group.org1", "p_"),
),
},
{
// test create
Config: testConfig(url, fooOrg, fooProject, projGroup),
Config: testConfigWithRecovery(url, fooOrg, firstProjectFoo, projGroup),
Check: resource.ComposeTestCheckFunc(
testAccCheckGroupResourceExists("boundary_group.foo"),
resource.TestCheckResourceAttr("boundary_group.foo", groupDescriptionKey, fooGroupDescription),
resource.TestCheckResourceAttr("boundary_group.foo", groupNameKey, "test"),
testAccCheckGroupProjectScope("boundary_group.foo"),
testAccCheckGroupResourceExists("boundary_group.proj1"),
resource.TestCheckResourceAttr("boundary_group.proj1", DescriptionKey, "desc-test-proj"),
resource.TestCheckResourceAttr("boundary_group.proj1", NameKey, "test-proj"),
testAccCheckGroupScope("boundary_group.proj1", "p_"),
),
},
{
// test update
Config: testConfig(url, fooOrg, fooProject, projGroupUpdate),
Config: testConfigWithRecovery(url, fooOrg, firstProjectFoo, projGroupUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckGroupResourceExists("boundary_group.foo"),
resource.TestCheckResourceAttr("boundary_group.foo", groupDescriptionKey, fooGroupDescriptionUpdate),
testAccCheckGroupProjectScope("boundary_group.foo"),
testAccCheckGroupResourceExists("boundary_group.proj1"),
resource.TestCheckResourceAttr("boundary_group.proj1", DescriptionKey, "desc-test-proj-up"),
testAccCheckGroupScope("boundary_group.proj1", "p_"),
),
},
{
// test name removal
Config: testConfigWithRecovery(url, fooOrg, firstProjectFoo, projNameRemoval),
Check: resource.ComposeTestCheckFunc(
testAccCheckGroupResourceExists("boundary_group.proj1"),
resource.TestCheckResourceAttr("boundary_group.proj1", NameKey, ""),
testAccCheckGroupScope("boundary_group.proj1", "p_"),
),
},
{
// test update to org scope
Config: testConfig(url, fooOrg, fooProject, projToOrgGroupUpdate),
Config: testConfigWithRecovery(url, fooOrg, firstProjectFoo, projToOrgGroupUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckGroupResourceExists("boundary_group.foo"),
resource.TestCheckResourceAttr("boundary_group.foo", groupDescriptionKey, fooGroupDescriptionUpdate),
resource.TestCheckResourceAttr("boundary_group.foo", groupScopeIDKey, tcOrg),
testAccCheckGroupResourceExists("boundary_group.proj1"),
resource.TestCheckResourceAttr("boundary_group.proj1", DescriptionKey, "desc-back"),
testAccCheckGroupScope("boundary_group.proj1", "o_"),
),
},
},
@@ -175,9 +192,9 @@ func TestAccGroupWithMembers(t *testing.T) {
Config: testConfig(url, fooOrg, orgGroupWithMembers),
Check: resource.ComposeTestCheckFunc(
testAccCheckGroupResourceExists("boundary_group.with_members"),
testAccCheckGroupResourceExists("boundary_user.foo"),
resource.TestCheckResourceAttr("boundary_group.with_members", groupDescriptionKey, "with members"),
testAccCheckGroupResourceMembersSet("boundary_group.with_members", []string{"boundary_user.foo"}),
testAccCheckUserResourceExists("boundary_user.org1"),
resource.TestCheckResourceAttr("boundary_group.with_members", DescriptionKey, "with members"),
testAccCheckGroupResourceMembersSet("boundary_group.with_members", []string{"boundary_user.org1"}),
),
},
{
@@ -185,9 +202,9 @@ func TestAccGroupWithMembers(t *testing.T) {
Config: testConfig(url, fooOrg, orgGroupWithMembersUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckGroupResourceExists("boundary_group.with_members"),
testAccCheckGroupResourceExists("boundary_user.foo"),
resource.TestCheckResourceAttr("boundary_group.with_members", groupDescriptionKey, "with members"),
testAccCheckGroupResourceMembersSet("boundary_group.with_members", []string{"boundary_user.foo", "boundary_user.bar"}),
testAccCheckUserResourceExists("boundary_user.org1"),
resource.TestCheckResourceAttr("boundary_group.with_members", DescriptionKey, "with members"),
testAccCheckGroupResourceMembersSet("boundary_group.with_members", []string{"boundary_user.org1", "boundary_user.bar"}),
),
},
},
@@ -224,26 +241,23 @@ func testAccCheckGroupResourceMembersSet(name string, members []string) resource
// check boundary to ensure it matches
md := testProvider.Meta().(*metaData)
client := md.client.Clone()
grpsClient := groups.NewClient(md.client)
projID, ok := rs.Primary.Attributes["scope_id"]
if ok {
client.SetScopeId(projID)
}
grpsClient := groups.NewClient(client)
g, _, err := grpsClient.Read(md.ctx, id)
g, apiErr, err := grpsClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading role %q: %v", id, err)
return fmt.Errorf("Got an error when reading group %q: %v", id, err)
}
if apiErr != nil {
return fmt.Errorf("Got an API error when reading group %q: %v", id, apiErr.Message)
}
// for every member set as a member on the group in the state, ensure
// each group in boundary has the same setings
if len(g.MemberIds) == 0 {
if len(g.Item.MemberIds) == 0 {
return fmt.Errorf("no members found on group")
}
for _, stateMember := range g.MemberIds {
for _, stateMember := range g.Item.MemberIds {
ok := false
for _, gotMember := range memberIDs {
if gotMember == stateMember {
@@ -259,7 +273,7 @@ func testAccCheckGroupResourceMembersSet(name string, members []string) resource
}
}
func testAccCheckGroupProjectScope(name string) resource.TestCheckFunc {
func testAccCheckGroupScope(name, prefix string) resource.TestCheckFunc {
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
@@ -272,21 +286,18 @@ func testAccCheckGroupProjectScope(name string) resource.TestCheckFunc {
}
md := testProvider.Meta().(*metaData)
projClient := md.client.Clone()
grps := groups.NewClient(md.client)
stateProjID, ok := rs.Primary.Attributes["scope_id"]
if ok {
projClient.SetScopeId(stateProjID)
}
grps := groups.NewClient(projClient)
g, _, err := grps.Read(md.ctx, id)
g, apiErr, err := grps.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("could not read resource state %q: %v", id, err)
}
if apiErr != nil {
return fmt.Errorf("Got an API error when reading group %q: %v", id, apiErr.Message)
}
if g.Scope.Id != stateProjID {
return fmt.Errorf("project ID in state does not match boundary state: %s != %s", g.Scope.Id, stateProjID)
if !strings.HasPrefix(g.Item.ScopeId, prefix) {
return fmt.Errorf("Scope ID in state does not have prefix: %s != %s", g.Item.ScopeId, prefix)
}
return nil
@@ -306,16 +317,15 @@ func testAccCheckGroupResourceExists(name string) resource.TestCheckFunc {
}
md := testProvider.Meta().(*metaData)
projClient := md.client.Clone()
projID, ok := rs.Primary.Attributes["scope_id"]
if ok && projID != "" {
projClient.SetScopeId(projID)
}
grps := groups.NewClient(projClient)
grps := groups.NewClient(md.client)
if _, _, err := grps.Read(md.ctx, id); err != nil {
_, apiErr, err := grps.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading group %q: %v", id, err)
}
if apiErr != nil {
return fmt.Errorf("Got an API error when reading group %q: %v", id, apiErr.Message)
}
return nil
}
@@ -331,25 +341,21 @@ func testAccCheckGroupResourceDestroy(t *testing.T) resource.TestCheckFunc {
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "boundary_organization":
continue
case "boundary_project":
case "boundary_scope":
continue
case "boundary_user":
continue
case "boundary_group":
projClient := md.client.Clone()
projID, ok := rs.Primary.Attributes["scope_id"]
if ok {
projClient.SetScopeId(projID)
}
grps := groups.NewClient(projClient)
grps := groups.NewClient(md.client)
id := rs.Primary.ID
_, apiErr, _ := grps.Read(md.ctx, id)
if apiErr == nil || apiErr.Status != http.StatusForbidden && apiErr.Status != http.StatusNotFound {
return fmt.Errorf("Didn't get a 403 or 404 when reading destroyed resource %q: %v", id, apiErr)
_, apiErr, err := grps.Read(context.Background(), id)
if err != nil {
return err
}
if apiErr == nil || apiErr.Status != http.StatusNotFound {
return fmt.Errorf("Didn't get a 404 when reading destroyed resource %q: %v", id, apiErr)
}
default:
+169 -165
View File
@@ -1,236 +1,240 @@
package provider
import (
"fmt"
"context"
"github.com/hashicorp/boundary/api/hosts"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
hostNameKey = "name"
hostDescriptionKey = "description"
hostScopeIDKey = "scope_id"
hostCatalogIDKey = "host_catalog_id"
hostAddressKey = "address"
hostTypeStatic = "static"
hostAddressKey = "address"
)
func resourceHost() *schema.Resource {
return &schema.Resource{
Create: resourceHostCreate,
Read: resourceHostRead,
Update: resourceHostUpdate,
Delete: resourceHostDelete,
CreateContext: resourceHostCreate,
ReadContext: resourceHostRead,
UpdateContext: resourceHostUpdate,
DeleteContext: resourceHostDelete,
Schema: map[string]*schema.Schema{
hostNameKey: {
NameKey: {
Type: schema.TypeString,
Optional: true,
},
hostDescriptionKey: {
DescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
hostCatalogIDKey: {
TypeKey: {
Type: schema.TypeString,
Optional: true,
Required: true,
ForceNew: true,
},
HostCatalogIdKey: {
Type: schema.TypeString,
Required: true,
},
hostAddressKey: {
Type: schema.TypeString,
Optional: true,
},
hostScopeIDKey: {
Type: schema.TypeString,
Optional: true,
ForceNew: true,
Computed: true,
},
},
}
}
// convertHostToResourceData creates a ResourceData type from a Host
func convertHostToResourceData(h *hosts.Host, d *schema.ResourceData) error {
if h.Name != "" {
if err := d.Set(hostNameKey, h.Name); err != nil {
return err
func setFromHostResponseMap(d *schema.ResourceData, raw map[string]interface{}) {
d.Set(NameKey, raw["name"])
d.Set(DescriptionKey, raw["description"])
d.Set(HostCatalogIdKey, raw["host_catalog_id"])
d.Set(TypeKey, raw["type"])
switch raw["type"].(string) {
case hostTypeStatic:
if attrsVal, ok := raw["attributes"]; ok {
attrs := attrsVal.(map[string]interface{})
d.Set(hostAddressKey, attrs["address"])
}
}
if h.Description != "" {
if err := d.Set(hostDescriptionKey, h.Description); err != nil {
return err
}
d.SetId(raw["id"].(string))
}
func resourceHostCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
var hostCatalogId string
if hostCatalogIdVal, ok := d.GetOk(HostCatalogIdKey); ok {
hostCatalogId = hostCatalogIdVal.(string)
} else {
return diag.Errorf("no host catalog ID provided")
}
if h.Scope.Id != "" {
if err := d.Set(hostScopeIDKey, h.Scope.Id); err != nil {
return err
}
var address *string
if addressVal, ok := d.GetOk(hostAddressKey); ok {
hostAddress := addressVal.(string)
address = &hostAddress
}
if h.HostCatalogId != "" {
if err := d.Set(hostCatalogIDKey, h.HostCatalogId); err != nil {
return err
opts := []hosts.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 address if
// it's not allowed
case hostTypeStatic:
if address != nil {
opts = append(opts, hosts.WithStaticHostAddress(*address))
} else {
return diag.Errorf("no address provided")
}
default:
return diag.Errorf("invalid type provided")
}
if len(h.Attributes) != 0 {
if addr, ok := h.Attributes["address"]; ok {
if err := d.Set(hostAddressKey, addr); err != nil {
return err
}
}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, hosts.WithName(nameStr))
}
d.SetId(h.Id)
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, hosts.WithDescription(descStr))
}
hClient := hosts.NewClient(md.client)
hcr, apiErr, err := hClient.Create(
ctx,
hostCatalogId,
opts...)
if err != nil {
return diag.Errorf("error calling create host: %v", err)
}
if apiErr != nil {
return diag.Errorf("error creating host: %s", apiErr.Message)
}
if hcr == nil {
return diag.Errorf("host nil after create")
}
setFromHostResponseMap(d, hcr.GetResponseMap())
return nil
}
// convertResourceDataToHost returns a localy built Host using the values provided in the ResourceData.
func convertResourceDataToHost(d *schema.ResourceData) *hosts.Host {
// if you're manually defining the host in TF, it's always going
// to be of type "static"
h := &hosts.Host{
Scope: &scopes.ScopeInfo{},
Type: hostCatalogTypeStatic,
Attributes: map[string]interface{}{
"address": "",
},
}
if descVal, ok := d.GetOk(hostDescriptionKey); ok {
h.Description = descVal.(string)
}
if nameVal, ok := d.GetOk(hostNameKey); ok {
h.Name = nameVal.(string)
}
if scopeIDVal, ok := d.GetOk(hostScopeIDKey); ok {
h.Scope.Id = scopeIDVal.(string)
}
if hostCatalogVal, ok := d.GetOk(hostCatalogIDKey); ok {
h.HostCatalogId = hostCatalogVal.(string)
}
if hostAddrVal, ok := d.GetOk(hostAddressKey); ok {
h.Attributes["address"] = hostAddrVal.(string)
}
if d.Id() != "" {
h.Id = d.Id()
}
return h
}
func resourceHostCreate(d *schema.ResourceData, meta interface{}) error {
func resourceHostRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
hClient := hosts.NewClient(md.client)
h := convertResourceDataToHost(d)
usrs := hosts.NewClient(client)
h, apiErr, err := usrs.Create(
ctx,
h.HostCatalogId,
hosts.WithName(h.Name),
hosts.WithDescription(h.Description),
// not checking the key or the type because it's guaranteed set and string
// when calling convertResourceDataToHost()
hosts.WithStaticHostAddress(h.Attributes["address"].(string)),
hosts.WithScopeId(h.Scope.Id))
hrr, apiErr, err := hClient.Read(ctx, d.Id())
if err != nil {
return fmt.Errorf("error calling new host: %s", err.Error())
return diag.Errorf("error calling read host: %v", err)
}
if apiErr != nil {
return fmt.Errorf("error creating host: %s", apiErr.Message)
return diag.Errorf("error reading host: %s", apiErr.Message)
}
if hrr == nil {
return diag.Errorf("host nil after read")
}
d.SetId(h.Id)
setFromHostResponseMap(d, hrr.GetResponseMap())
return convertHostToResourceData(h, d)
return nil
}
func resourceHostRead(d *schema.ResourceData, meta interface{}) error {
func resourceHostUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
hClient := hosts.NewClient(md.client)
h := convertResourceDataToHost(d)
usrs := hosts.NewClient(client)
opts := []hosts.Option{}
h, apiErr, err := usrs.Read(ctx, h.HostCatalogId, h.Id, hosts.WithScopeId(h.Scope.Id))
if err != nil {
return fmt.Errorf("error reading host: %s", err.Error())
}
if apiErr != nil {
return fmt.Errorf("error reading host: %s", apiErr.Message)
var name *string
if d.HasChange(NameKey) {
opts = append(opts, hosts.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, hosts.WithName(nameStr))
}
}
return convertHostToResourceData(h, d)
}
func resourceHostUpdate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
h := convertResourceDataToHost(d)
usrs := hosts.NewClient(client)
if d.HasChange(hostNameKey) {
h.Name = d.Get(hostNameKey).(string)
}
if d.HasChange(hostDescriptionKey) {
h.Description = d.Get(hostDescriptionKey).(string)
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, hosts.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, hosts.WithDescription(descStr))
}
}
var address *string
if d.HasChange(hostAddressKey) {
h.Attributes["address"] = d.Get(hostAddressKey).(string)
switch d.Get(TypeKey).(string) {
case hostTypeStatic:
opts = append(opts, hosts.DefaultStaticHostAddress())
addrVal, ok := d.GetOk(hostAddressKey)
if ok {
addrStr := addrVal.(string)
address = &addrStr
opts = append(opts, hosts.WithStaticHostAddress(addrStr))
}
default:
return diag.Errorf("address cannot be used with this type of host")
}
}
h.Scope.Id = d.Get(hostScopeIDKey).(string)
h, apiErr, err := usrs.Update(
ctx,
h.HostCatalogId,
h.Id,
0,
hosts.WithStaticHostAddress(h.Attributes["address"].(string)),
hosts.WithAutomaticVersioning(),
hosts.WithName(h.Name),
hosts.WithDescription(h.Description),
hosts.WithScopeId(h.Scope.Id))
if err != nil {
return err
}
if apiErr != nil {
return fmt.Errorf("error updating host: %s\n Invalid request fields: %v\n", apiErr.Message, apiErr.Details.RequestFields)
if len(opts) > 0 {
opts = append(opts, hosts.WithAutomaticVersioning(true))
_, apiErr, err := hClient.Update(
ctx,
d.Id(),
0,
opts...)
if err != nil {
return diag.Errorf("error calling update host: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating host: %s", apiErr.Message)
}
}
return convertHostToResourceData(h, d)
}
func resourceHostDelete(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
h := convertResourceDataToHost(d)
usrs := hosts.NewClient(client)
_, apiErr, err := usrs.Delete(ctx, h.HostCatalogId, h.Id, hosts.WithScopeId(h.Scope.Id))
if err != nil {
return fmt.Errorf("error deleting host: %s", err.Error())
if d.HasChange(NameKey) {
d.Set(NameKey, name)
}
if apiErr != nil {
return fmt.Errorf("error deleting host: %s", apiErr.Message)
if d.HasChange(DescriptionKey) {
d.Set(DescriptionKey, desc)
}
if d.HasChange(hostAddressKey) {
d.Set(hostAddressKey, *address)
}
return nil
}
func resourceHostDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hClient := hosts.NewClient(md.client)
_, apiErr, err := hClient.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling delete host: %v", err)
}
if apiErr != nil {
return diag.Errorf("error deleting host: %s", apiErr.Message)
}
return nil
+133 -169
View File
@@ -1,231 +1,195 @@
package provider
import (
"errors"
"fmt"
"context"
"github.com/hashicorp/boundary/api/hostcatalogs"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
hostCatalogNameKey = "name"
hostCatalogDescriptionKey = "description"
hostCatalogScopeIDKey = "scope_id"
hostCatalogTypeKey = "type"
hostCatalogTypeStatic = "static"
hostCatalogTypeStatic = "static"
)
func resourceHostCatalog() *schema.Resource {
return &schema.Resource{
Create: resourceHostCatalogCreate,
Read: resourceHostCatalogRead,
Update: resourceHostCatalogUpdate,
Delete: resourceHostCatalogDelete,
CreateContext: resourceHostCatalogCreate,
ReadContext: resourceHostCatalogRead,
UpdateContext: resourceHostCatalogUpdate,
DeleteContext: resourceHostCatalogDelete,
Schema: map[string]*schema.Schema{
hostCatalogNameKey: {
NameKey: {
Type: schema.TypeString,
Optional: true,
},
hostCatalogDescriptionKey: {
DescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
hostCatalogScopeIDKey: {
ScopeIdKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
hostCatalogTypeKey: {
TypeKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func validateHostCatalogType(val interface{}, key string) (warns []string, errs []error) {
allow := []string{hostCatalogTypeStatic}
v := val.(string)
for _, a := range allow {
if a == v {
return
}
}
errs = append(errs, fmt.Errorf("%s is not a supported host catalog type, please use one of %v", v, allow))
return
func setFromHostCatalogResponseMap(d *schema.ResourceData, raw map[string]interface{}) {
d.Set(NameKey, raw["name"])
d.Set(DescriptionKey, raw["description"])
d.Set(ScopeIdKey, raw["scope_id"])
d.Set(TypeKey, raw["type"])
d.SetId(raw["id"].(string))
}
// convertHostCatalogToResourceData creates a ResourceData type from a HostCatalog
func convertHostCatalogToResourceData(h *hostcatalogs.HostCatalog, d *schema.ResourceData) error {
if h.Name != "" {
if err := d.Set(hostCatalogNameKey, h.Name); err != nil {
return err
}
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")
}
if h.Description != "" {
if err := d.Set(hostCatalogDescriptionKey, h.Description); err != nil {
return err
}
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")
}
if h.Type != "" {
if err := d.Set(hostCatalogTypeKey, h.Type); err != nil {
return err
}
opts := []hostcatalogs.Option{}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, hostcatalogs.WithName(nameStr))
}
if h.Scope.Id != "" {
if err := d.Set(hostCatalogScopeIDKey, h.Scope.Id); err != nil {
return err
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, hostcatalogs.WithDescription(descStr))
}
d.SetId(h.Id)
hcClient := hostcatalogs.NewClient(md.client)
hccr, apiErr, err := hcClient.Create(
ctx,
typeStr,
scopeId,
opts...)
if err != nil {
return diag.Errorf("error calling create host catalog: %v", err)
}
if apiErr != nil {
return diag.Errorf("error creating host catalog: %s", apiErr.Message)
}
if hccr == nil {
return diag.Errorf("nil host catalog after create")
}
setFromHostCatalogResponseMap(d, hccr.GetResponseMap())
return nil
}
// convertResourceDataToHostCatalog returns a localy built HostCatalog using the values provided in the ResourceData.
func convertResourceDataToHostCatalog(d *schema.ResourceData) *hostcatalogs.HostCatalog {
h := &hostcatalogs.HostCatalog{Scope: &scopes.ScopeInfo{}}
func resourceHostCatalogRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
if descVal, ok := d.GetOk(hostCatalogDescriptionKey); ok {
h.Description = descVal.(string)
hcrr, apiErr, err := hcClient.Read(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling read host catalog: %v", err)
}
if apiErr != nil {
return diag.Errorf("error reading host catalog: %s", apiErr.Message)
}
if hcrr == nil {
return diag.Errorf("host catalog nil after read")
}
if nameVal, ok := d.GetOk(hostCatalogNameKey); ok {
h.Name = nameVal.(string)
}
setFromHostCatalogResponseMap(d, hcrr.GetResponseMap())
if typeVal, ok := d.GetOk(hostCatalogTypeKey); ok {
h.Type = typeVal.(string)
}
return nil
}
if d.Id() != "" {
h.Id = d.Id()
}
func resourceHostCatalogUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
if scopeIDVal, ok := d.GetOk(hostCatalogScopeIDKey); ok {
// if scope_id is not set, don't override with an empty scope
if scopeIDVal.(string) != "" {
h.Scope.Id = scopeIDVal.(string)
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))
}
}
return h
}
func resourceHostCatalogCreate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
h := convertResourceDataToHostCatalog(d)
hcClient := hostcatalogs.NewClient(client)
h, apiErr, err := hcClient.Create(
ctx,
h.Type,
hostcatalogs.WithName(h.Name),
hostcatalogs.WithDescription(h.Description),
hostcatalogs.WithScopeId(h.Scope.Id))
if err != nil {
return fmt.Errorf("error calling new host catalog: %s", err.Error())
}
if apiErr != nil {
return fmt.Errorf("error creating host catalog: %s", apiErr.Message)
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))
}
}
d.SetId(h.Id)
return convertHostCatalogToResourceData(h, d)
}
func resourceHostCatalogRead(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
h := convertResourceDataToHostCatalog(d)
hcClient := hostcatalogs.NewClient(client)
h, apiErr, err := hcClient.Read(ctx, h.Id, hostcatalogs.WithScopeId(h.Scope.Id))
if err != nil {
return fmt.Errorf("error calling new host catalog: %s", err.Error())
}
if apiErr != nil {
return fmt.Errorf("error reading host catalog: %s", apiErr.Message)
if len(opts) > 0 {
opts = append(opts, hostcatalogs.WithAutomaticVersioning(true))
_, apiErr, err := hcClient.Update(
ctx,
d.Id(),
0,
opts...)
if err != nil {
return diag.Errorf("error calling update host catalog: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating host catalog: %s", apiErr.Message)
}
}
return convertHostCatalogToResourceData(h, d)
}
func resourceHostCatalogUpdate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
h := convertResourceDataToHostCatalog(d)
hcClient := hostcatalogs.NewClient(client)
if d.HasChange(hostCatalogNameKey) {
h.Name = d.Get(hostCatalogNameKey).(string)
if d.HasChange(NameKey) {
d.Set(NameKey, name)
}
if d.HasChange(hostCatalogDescriptionKey) {
h.Description = d.Get(hostCatalogDescriptionKey).(string)
}
if d.HasChange(hostCatalogScopeIDKey) {
h.Scope.Id = d.Get(hostCatalogScopeIDKey).(string)
}
if d.HasChange(hostCatalogTypeKey) {
return errors.New("error updating host catalog: A host catalog can not have its type modified.")
}
// Type is a read-only value that can not be updated. It is added in the method to convert from a
// resource to a HostCatalog type, so it needs to be unset when calling update.
h.Type = ""
h, apiErr, err := hcClient.Update(
ctx,
h.Id,
0,
hostcatalogs.WithScopeId(h.Scope.Id),
hostcatalogs.WithAutomaticVersioning(),
hostcatalogs.WithName(h.Name),
hostcatalogs.WithDescription(h.Description))
if err != nil {
return err
}
if apiErr != nil {
return fmt.Errorf("error updating host catalog: %s\n Invalid request fields: %v\n", apiErr.Message, apiErr.Details.RequestFields)
}
return convertHostCatalogToResourceData(h, d)
}
func resourceHostCatalogDelete(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
h := convertResourceDataToHostCatalog(d)
hcClient := hostcatalogs.NewClient(client)
_, apiErr, err := hcClient.Delete(ctx, h.Id, hostcatalogs.WithScopeId(h.Scope.Id))
if err != nil {
return fmt.Errorf("error calling new host catalog: %s", err.Error())
}
if apiErr != nil {
return fmt.Errorf("error reading host catalog: %s", apiErr.Message)
if d.HasChange(DescriptionKey) {
d.Set(DescriptionKey, desc)
}
return nil
}
func resourceHostCatalogDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hcClient := hostcatalogs.NewClient(md.client)
_, apiErr, err := hcClient.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling delete host catalog: %v", err)
}
if apiErr != nil {
return diag.Errorf("error deleting host catalog: %s", apiErr.Message)
}
return nil
+20 -55
View File
@@ -1,15 +1,15 @@
package provider
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/hashicorp/boundary/api/hostcatalogs"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/stretchr/testify/assert"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
@@ -20,17 +20,17 @@ const (
var (
projHostCatalog = fmt.Sprintf(`
resource "boundary_host_catalog" "foo" {
name = "foo"
name = "foo"
description = "%s"
scope_id = boundary_project.foo.id
scope_id = boundary_scope.proj1.id
type = "static"
}`, fooHostCatalogDescription)
projHostCatalogUpdate = fmt.Sprintf(`
resource "boundary_host_catalog" "foo" {
name = "foo"
name = "foo"
description = "%s"
scope_id = boundary_project.foo.id
scope_id = boundary_scope.proj1.id
type = "static"
}`, fooHostCatalogDescriptionUpdate)
)
@@ -46,20 +46,20 @@ func TestAccHostCatalogCreate(t *testing.T) {
Steps: []resource.TestStep{
{
// test create
Config: testConfig(url, fooOrg, fooProject, projHostCatalog),
Config: testConfig(url, fooOrg, firstProjectFoo, projHostCatalog),
Check: resource.ComposeTestCheckFunc(
testAccCheckOrganizationResourceExists("boundary_organization.foo"),
testAccCheckProjectResourceExists("boundary_project.foo"),
testAccCheckScopeResourceExists("boundary_scope.org1"),
testAccCheckScopeResourceExists("boundary_scope.proj1"),
testAccCheckHostCatalogResourceExists("boundary_host_catalog.foo"),
resource.TestCheckResourceAttr("boundary_host_catalog.foo", hostCatalogDescriptionKey, fooHostCatalogDescription),
resource.TestCheckResourceAttr("boundary_host_catalog.foo", DescriptionKey, fooHostCatalogDescription),
),
},
{
// test update
Config: testConfig(url, fooOrg, fooProject, projHostCatalogUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, projHostCatalogUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostCatalogResourceExists("boundary_host_catalog.foo"),
resource.TestCheckResourceAttr("boundary_host_catalog.foo", hostCatalogDescriptionKey, fooHostCatalogDescriptionUpdate),
resource.TestCheckResourceAttr("boundary_host_catalog.foo", DescriptionKey, fooHostCatalogDescriptionUpdate),
),
},
},
@@ -79,13 +79,9 @@ func testAccCheckHostCatalogResourceExists(name string) resource.TestCheckFunc {
}
md := testProvider.Meta().(*metaData)
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
hcClient := hostcatalogs.NewClient(md.client)
if _, _, err := hcClient.Read(md.ctx, id, hostcatalogs.WithScopeId(projID)); err != nil {
if _, _, err := hcClient.Read(context.Background(), id); err != nil {
return fmt.Errorf("Got an error when reading host catalog %q: %v", id, err)
}
@@ -104,16 +100,14 @@ func testAccCheckHostCatalogResourceDestroy(t *testing.T) resource.TestCheckFunc
case "boundary_host_catalog":
id := rs.Primary.ID
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
hcClient := hostcatalogs.NewClient(md.client)
_, apiErr, _ := hcClient.Read(md.ctx, id, hostcatalogs.WithScopeId(projID))
if apiErr == nil || apiErr.Status != http.StatusNotFound && apiErr.Status != http.StatusForbidden {
return fmt.Errorf("Didn't get a 404 or 403 when reading destroyed host catalog %q: %v", id, apiErr)
_, apiErr, err := hcClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Error when reading destroyed host catalog %q: %v", id, err)
}
if apiErr == nil || apiErr.Status != http.StatusNotFound {
return fmt.Errorf("Didn't get a 404 when reading destroyed host catalog %q: %v", id, apiErr)
}
default:
@@ -123,32 +117,3 @@ func testAccCheckHostCatalogResourceDestroy(t *testing.T) resource.TestCheckFunc
return nil
}
}
func TestHostCatalogValidateType(t *testing.T) {
cases := []struct {
name string
config string
numErr int
numWrn int
}{
{
name: "should validate when correct values are passed",
config: hostCatalogTypeStatic,
numErr: 0,
numWrn: 0,
},
{
name: "should error when incorrect values are passed",
config: "nope",
numErr: 1,
numWrn: 0,
}}
for _, tCase := range cases {
t.Run(tCase.name, func(t *testing.T) {
wrn, err := validateHostCatalogType(tCase.config, "")
assert.Len(t, wrn, tCase.numWrn)
assert.Len(t, err, tCase.numErr)
})
}
}
+182 -176
View File
@@ -1,40 +1,43 @@
package provider
import (
"fmt"
"context"
"github.com/hashicorp/boundary/api/hostsets"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
hostsetNameKey = "name"
hostsetDescriptionKey = "description"
hostsetCatalogIDKey = "host_catalog_id"
hostsetHostIDsKey = "host_ids"
hostsetHostIdsKey = "host_ids"
hostsetTypeStatic = "static"
)
func resourceHostset() *schema.Resource {
return &schema.Resource{
Create: resourceHostsetCreate,
Read: resourceHostsetRead,
Update: resourceHostsetUpdate,
Delete: resourceHostsetDelete,
CreateContext: resourceHostsetCreate,
ReadContext: resourceHostsetRead,
UpdateContext: resourceHostsetUpdate,
DeleteContext: resourceHostsetDelete,
Schema: map[string]*schema.Schema{
hostsetNameKey: {
NameKey: {
Type: schema.TypeString,
Optional: true,
},
hostsetDescriptionKey: {
DescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
hostsetCatalogIDKey: {
TypeKey: {
Type: schema.TypeString,
Optional: true,
Required: true,
ForceNew: true,
},
hostsetHostIDsKey: {
HostCatalogIdKey: {
Type: schema.TypeString,
Required: true,
},
hostsetHostIdsKey: {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
@@ -43,207 +46,210 @@ func resourceHostset() *schema.Resource {
}
}
// convertHostsetToResourceData creates a ResourceData type from a Host
func convertHostsetToResourceData(h *hostsets.HostSet, d *schema.ResourceData) error {
if h.Name != "" {
if err := d.Set(hostsetNameKey, h.Name); err != nil {
return err
func setFromHostSetResponseMap(d *schema.ResourceData, raw map[string]interface{}) {
d.Set(NameKey, raw["name"])
d.Set(DescriptionKey, raw["description"])
d.Set(HostCatalogIdKey, raw["host_catalog_id"])
d.Set(TypeKey, raw["type"])
d.Set(hostsetHostIdsKey, raw["host_ids"])
d.SetId(raw["id"].(string))
}
func resourceHostsetCreate(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")
}
var hostIds []string
if hostIdsVal, ok := d.GetOk(hostsetHostIdsKey); ok {
list := hostIdsVal.(*schema.Set).List()
hostIds = make([]string, 0, len(list))
for _, i := range list {
hostIds = append(hostIds, i.(string))
}
}
if h.Description != "" {
if err := d.Set(hostsetDescriptionKey, h.Description); err != nil {
return err
}
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 hostsetTypeStatic:
default:
return diag.Errorf("invalid type provided")
}
if h.HostCatalogId != "" {
if err := d.Set(hostsetCatalogIDKey, h.HostCatalogId); err != nil {
return err
}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, hostsets.WithName(nameStr))
}
if h.HostIds != nil {
if err := d.Set(hostsetHostIDsKey, h.HostIds); err != nil {
return err
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, hostsets.WithDescription(descStr))
}
d.SetId(h.Id)
hsClient := hostsets.NewClient(md.client)
hscr, apiErr, err := hsClient.Create(
ctx,
hostsetHostCatalogId,
opts...)
if err != nil {
return diag.Errorf("error calling create host set: %v", err)
}
if apiErr != nil {
return diag.Errorf("error creating host set: %s", apiErr.Message)
}
if hscr == nil {
return diag.Errorf("nil host set after create")
}
raw := hscr.GetResponseMap()
if hostIds != nil {
hsshr, apiErr, err := hsClient.SetHosts(
ctx,
hscr.Item.Id,
hscr.Item.Version,
hostIds)
if apiErr != nil {
return diag.Errorf("error setting hosts on host set: %s", apiErr.Message)
}
if err != nil {
return diag.Errorf("error setting hosts on host set: %v", err)
}
if hsshr == nil {
return diag.Errorf("nil host set after setting hosts")
}
raw = hsshr.GetResponseMap()
}
setFromHostSetResponseMap(d, raw)
return nil
}
// convertResourceDataToHostset returns a localy built Host using the values provided in the ResourceData.
func convertResourceDataToHostset(d *schema.ResourceData) *hostsets.HostSet {
// if you're manually defining the hostset in TF, it's always going
// to be of type "static"
h := &hostsets.HostSet{
Scope: &scopes.ScopeInfo{},
HostIds: []string{},
Attributes: map[string]interface{}{},
func resourceHostsetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hsClient := hostsets.NewClient(md.client)
hsrr, apiErr, err := hsClient.Read(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling read host set: %v", err)
}
if apiErr != nil {
return diag.Errorf("error reading host set: %s", apiErr.Message)
}
if hsrr == nil {
return diag.Errorf("host set nil after read")
}
if descVal, ok := d.GetOk(hostsetDescriptionKey); ok {
h.Description = descVal.(string)
}
setFromHostSetResponseMap(d, hsrr.GetResponseMap())
if nameVal, ok := d.GetOk(hostsetNameKey); ok {
h.Name = nameVal.(string)
}
return nil
}
if hostsetCatalogVal, ok := d.GetOk(hostCatalogIDKey); ok {
h.HostCatalogId = hostsetCatalogVal.(string)
}
func resourceHostsetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hsClient := hostsets.NewClient(md.client)
if val, ok := d.GetOk(hostsetHostIDsKey); ok {
hostIds := val.(*schema.Set).List()
for _, i := range hostIds {
h.HostIds = append(h.HostIds, i.(string))
opts := []hostsets.Option{}
var name *string
if d.HasChange(NameKey) {
opts = append(opts, hostsets.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, hostsets.WithName(nameStr))
}
}
if d.Id() != "" {
h.Id = d.Id()
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, hostsets.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, hostsets.WithDescription(descStr))
}
}
return h
}
func resourceHostsetCreate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
h := convertResourceDataToHostset(d)
hst := hostsets.NewClient(client)
hostIDs := h.HostIds
h, apiErr, err := hst.Create2(
ctx,
h.HostCatalogId,
hostsets.WithName(h.Name),
hostsets.WithDescription(h.Description))
if err != nil {
return fmt.Errorf("error calling new hostset: %s", err.Error())
}
if apiErr != nil {
return fmt.Errorf("error creating hostset: %s", apiErr.Message)
}
d.SetId(h.Id)
if len(hostIDs) != 0 {
h, apiErr, err = hst.SetHosts2(
if len(opts) > 0 {
opts = append(opts, hostsets.WithAutomaticVersioning(true))
_, apiErr, err := hsClient.Update(
ctx,
h.Id,
d.Id(),
0,
hostIDs,
hostsets.WithAutomaticVersioning(),
hostsets.WithName(h.Name),
hostsets.WithDescription(h.Description))
opts...)
if err != nil {
return fmt.Errorf("error setting hosts on hostset: %s", err.Error())
return diag.Errorf("error calling update host set: %v", err)
}
if apiErr != nil {
return fmt.Errorf("error setting hosts on hostset: %s", apiErr.Message)
return diag.Errorf("error updating host set: %s", apiErr.Message)
}
}
return convertHostsetToResourceData(h, d)
}
func resourceHostsetRead(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
h := convertResourceDataToHostset(d)
hst := hostsets.NewClient(client)
h, apiErr, err := hst.Read2(ctx, h.Id)
if err != nil {
return fmt.Errorf("error reading hostset: %s", err.Error())
if d.HasChange(NameKey) {
d.Set(NameKey, name)
}
if apiErr != nil {
return fmt.Errorf("error reading hostset: %s", apiErr.Message)
if d.HasChange(DescriptionKey) {
d.Set(DescriptionKey, desc)
}
return convertHostsetToResourceData(h, d)
}
func resourceHostsetUpdate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
h := convertResourceDataToHostset(d)
hst := hostsets.NewClient(client)
if d.HasChange(hostsetNameKey) {
h.Name = d.Get(hostsetNameKey).(string)
}
if d.HasChange(hostsetDescriptionKey) {
h.Description = d.Get(hostsetDescriptionKey).(string)
}
if d.HasChange(hostsetHostIDsKey) {
hostIDs := []string{}
hosts := d.Get(hostsetHostIDsKey).(*schema.Set).List()
for _, host := range hosts {
hostIDs = append(hostIDs, host.(string))
// The above call may not actually happen, so we use d.Id() and automatic
// versioning here
if d.HasChange(hostsetHostIdsKey) {
var hostIds []string
if hostIdsVal, ok := d.GetOk(hostsetHostIdsKey); ok {
hosts := hostIdsVal.(*schema.Set).List()
for _, host := range hosts {
hostIds = append(hostIds, host.(string))
}
}
_, apiErr, err := hst.SetHosts2(
_, apiErr, err := hsClient.SetHosts(
ctx,
h.Id,
d.Id(),
0,
hostIDs,
hostsets.WithAutomaticVersioning(),
hostsets.WithName(h.Name),
hostsets.WithDescription(h.Description))
hostIds,
hostsets.WithAutomaticVersioning(true))
if err != nil {
return fmt.Errorf("error setting hosts on hostset: %s", err.Error())
return diag.Errorf("error updating hosts in host set: %v", err)
}
if apiErr != nil {
return fmt.Errorf("error setting hosts on hostset: %s", apiErr.Message)
return diag.Errorf("error updating hosts in host set: %s", apiErr.Message)
}
}
h, apiErr, err := hst.Update2(
ctx,
h.Id,
0,
hostsets.WithAutomaticVersioning(),
hostsets.WithName(h.Name),
hostsets.WithDescription(h.Description))
if err != nil {
return err
}
if apiErr != nil {
return fmt.Errorf("error updating hostset: %s\n Invalid request fields: %v\n", apiErr.Message, apiErr.Details.RequestFields)
}
return convertHostsetToResourceData(h, d)
}
func resourceHostsetDelete(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
h := convertResourceDataToHostset(d)
hst := hostsets.NewClient(client)
_, apiErr, err := hst.Delete2(ctx, h.Id)
if err != nil {
return fmt.Errorf("error deleting hostset: %s", err.Error())
}
if apiErr != nil {
return fmt.Errorf("error deleting hostset: %s", apiErr.Message)
d.Set(hostsetHostIdsKey, hostIds)
}
return nil
}
func resourceHostsetDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
hsClient := hostsets.NewClient(md.client)
_, apiErr, err := hsClient.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling delete host set: %s", err.Error())
}
if apiErr != nil {
return diag.Errorf("error deleting host set: %s", apiErr.Message)
}
return nil
+27 -24
View File
@@ -1,30 +1,32 @@
package provider
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/hashicorp/boundary/api/hostsets"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
var (
fooHostset = `
resource "boundary_host_catalog" "foo" {
scope_id = boundary_project.foo.id
scope_id = boundary_scope.proj1.id
type = "static"
}
resource "boundary_host" "foo" {
scope_id = boundary_project.foo.id
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
address = "10.0.0.1:80"
}
resource "boundary_host_set" "foo" {
type = "static"
name = "test"
description = "test hostset"
host_catalog_id = boundary_host_catalog.foo.id
@@ -33,26 +35,27 @@ resource "boundary_host_set" "foo" {
fooHostsetUpdate = `
resource "boundary_host_catalog" "foo" {
scope_id = boundary_project.foo.id
scope_id = boundary_scope.proj1.id
type = "static"
}
resource "boundary_host" "foo" {
scope_id = boundary_project.foo.id
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
address = "10.0.0.1:80"
}
resource "boundary_host" "bar" {
scope_id = boundary_project.foo.id
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
address = "10.0.0.2:80"
}
resource "boundary_host_set" "foo" {
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
name = "test"
description = "test hostset"
host_catalog_id = boundary_host_catalog.foo.id
host_ids = [
boundary_host.foo.id,
boundary_host.bar.id,
@@ -72,7 +75,7 @@ func TestAccHostset(t *testing.T) {
Steps: []resource.TestStep{
{
// test project hostset create
Config: testConfig(url, fooOrg, fooProject, fooHostset),
Config: testConfig(url, fooOrg, firstProjectFoo, fooHostset),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostsetResourceExists("boundary_host_set.foo"),
testAccCheckHostsetHostIDsSet("boundary_host_set.foo", []string{"boundary_host.foo"}),
@@ -82,7 +85,7 @@ func TestAccHostset(t *testing.T) {
},
{
// test project hostset update
Config: testConfig(url, fooOrg, fooProject, fooHostsetUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, fooHostsetUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostsetResourceExists("boundary_host_set.foo"),
testAccCheckHostsetHostIDsSet("boundary_host_set.foo", []string{"boundary_host.foo", "boundary_host.bar"}),
@@ -107,10 +110,9 @@ func testAccCheckHostsetResourceExists(name string) resource.TestCheckFunc {
}
md := testProvider.Meta().(*metaData)
hostsetsClient := hostsets.NewClient(md.client)
if _, _, err := hostsetsClient.Read2(md.ctx, id); err != nil {
if _, _, err := hostsetsClient.Read(context.Background(), id); err != nil {
return fmt.Errorf("Got an error when reading hostset %q: %v", id, err)
}
@@ -122,12 +124,12 @@ func testAccCheckHostsetHostIDsSet(name string, wantHostIDs []string) resource.T
return func(s *terraform.State) error {
rs, ok := s.RootModule().Resources[name]
if !ok {
return fmt.Errorf("role resource not found: %s", name)
return fmt.Errorf("host set resource not found: %s", name)
}
id := rs.Primary.ID
if id == "" {
return fmt.Errorf("host resource ID is not set")
return fmt.Errorf("host set resource ID is not set")
}
gotHostIDs := []string{}
@@ -151,16 +153,16 @@ func testAccCheckHostsetHostIDsSet(name string, wantHostIDs []string) resource.T
hstClient := hostsets.NewClient(client)
hs, _, err := hstClient.Read2(md.ctx, id)
hs, _, err := hstClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading hostset %q: %v", id, err)
}
if len(hs.HostIds) == 0 {
return fmt.Errorf("no hosts found on hostset")
if len(hs.Item.HostIds) == 0 {
return fmt.Errorf("no hosts found on hostset %v; %v found in state; %#v in hs map", id, gotHostIDs, hs)
}
for _, stateHost := range hs.HostIds {
for _, stateHost := range hs.Item.HostIds {
ok := false
for _, gotHost := range gotHostIDs {
if gotHost == stateHost {
@@ -185,9 +187,7 @@ func testAccCheckHostsetResourceDestroy(t *testing.T) resource.TestCheckFunc {
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "boundary_project":
continue
case "boundary_organization":
case "boundary_scope":
continue
case "boundary_host_set":
@@ -195,9 +195,12 @@ func testAccCheckHostsetResourceDestroy(t *testing.T) resource.TestCheckFunc {
hostsetsClient := hostsets.NewClient(md.client)
_, apiErr, _ := hostsetsClient.Read2(md.ctx, id)
if apiErr == nil || apiErr.Status != http.StatusNotFound && apiErr.Status != http.StatusForbidden {
return fmt.Errorf("Didn't get a 404 or 403 when reading destroyed hostset %q: %v", id, apiErr)
_, apiErr, err := hostsetsClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Error when reading destroyed host set %q: %v", id, err)
}
if apiErr == nil || apiErr.Status != http.StatusNotFound {
return fmt.Errorf("Didn't get a 404 when reading destroyed host set %q: %v", id, apiErr)
}
default:
+30 -63
View File
@@ -1,6 +1,7 @@
package provider
import (
"context"
"errors"
"fmt"
"net/http"
@@ -8,8 +9,8 @@ import (
"github.com/hashicorp/boundary/api/hosts"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
@@ -20,34 +21,34 @@ const (
var (
projHost = fmt.Sprintf(`
resource "boundary_host_catalog" "foo" {
name = "test"
name = "test"
description = "test catalog"
scope_id = boundary_project.foo.id
scope_id = boundary_scope.proj1.id
type = "static"
}
resource "boundary_host" "foo" {
name = "test"
description = "test host"
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
name = "test"
description = "test host"
address = "%s"
scope_id = boundary_project.foo.id
}`, fooHostAddress)
projHostUpdate = fmt.Sprintf(`
resource "boundary_host_catalog" "foo" {
name = "test"
name = "test"
description = "test catalog"
scope_id = boundary_project.foo.id
scope_id = boundary_scope.proj1.id
type = "static"
}
resource "boundary_host" "foo" {
name = "test"
description = "test host"
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
name = "test"
description = "test host"
address = "%s"
scope_id = boundary_project.foo.id
}`, fooHostAddressUpdate)
)
@@ -63,7 +64,7 @@ func TestAccHost(t *testing.T) {
Steps: []resource.TestStep{
{
// test project host create
Config: testConfig(url, fooOrg, fooProject, projHost),
Config: testConfig(url, fooOrg, firstProjectFoo, projHost),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostAttributeSet("boundary_host.foo", "address", fooHostAddress),
testAccCheckHostResourceExists("boundary_host.foo"),
@@ -74,7 +75,7 @@ func TestAccHost(t *testing.T) {
},
{
// test project host update
Config: testConfig(url, fooOrg, fooProject, projHostUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, projHostUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckHostAttributeSet("boundary_host.foo", "address", fooHostAddressUpdate),
testAccCheckHostResourceExists("boundary_host.foo"),
@@ -101,21 +102,9 @@ func testAccCheckHostResourceExists(name string) resource.TestCheckFunc {
md := testProvider.Meta().(*metaData)
hostCatalogID, ok := rs.Primary.Attributes["host_catalog_id"]
if !ok {
return errors.New("host_catalog_id is not set")
}
hostsClient := hosts.NewClient(md.client)
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
hostsClient := hosts.NewClient(projClient)
if _, _, err := hostsClient.Read(md.ctx, hostCatalogID, id); err != nil {
if _, _, err := hostsClient.Read(context.Background(), id); err != nil {
return fmt.Errorf("Got an error when reading host %q: %v", id, err)
}
@@ -135,30 +124,19 @@ func testAccCheckHostAttributeSet(name, attrKey, wantAttrVal string) resource.Te
return fmt.Errorf("No ID is set")
}
hostCatalogID, ok := rs.Primary.Attributes["host_catalog_id"]
if !ok {
return errors.New("host_catalog_id is not set")
}
md := testProvider.Meta().(*metaData)
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
hostsClient := hosts.NewClient(projClient)
hostsClient := hosts.NewClient(md.client)
h, _, err := hostsClient.Read(md.ctx, hostCatalogID, id)
h, _, err := hostsClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading host %q: %v", id, err)
}
if len(h.Attributes) == 0 {
if len(h.Item.Attributes) == 0 {
return errors.New("no host attributes found")
}
gotAttrVal, ok := h.Attributes[attrKey]
gotAttrVal, ok := h.Item.Attributes[attrKey]
if !ok {
return fmt.Errorf("attribute not found on host: '%s'", attrKey)
}
@@ -180,30 +158,19 @@ func testAccCheckHostResourceDestroy(t *testing.T) resource.TestCheckFunc {
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "boundary_project":
continue
case "boundary_organization":
case "boundary_scope":
continue
case "boundary_host":
id := rs.Primary.ID
hostCatalogID, ok := rs.Primary.Attributes["host_catalog_id"]
if !ok {
return errors.New("host_catalog_id is not set")
hostsClient := hosts.NewClient(md.client)
_, apiErr, err := hostsClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Error when reading destroyed host %q: %v", id, err)
}
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
hostsClient := hosts.NewClient(projClient)
_, apiErr, _ := hostsClient.Read(md.ctx, hostCatalogID, id)
if apiErr == nil || apiErr.Status != http.StatusNotFound && apiErr.Status != http.StatusForbidden {
return fmt.Errorf("Didn't get a 404 or 403 when reading destroyed host %q: %v", id, apiErr)
if apiErr == nil || apiErr.Status != http.StatusNotFound {
return fmt.Errorf("Didn't get a 404 when reading destroyed host %q: %v", id, apiErr)
}
default:
-167
View File
@@ -1,167 +0,0 @@
package provider
import (
"fmt"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)
const (
organizationDescriptionKey = "description"
organizationNameKey = "name"
)
func resourceOrganization() *schema.Resource {
return &schema.Resource{
Create: resourceOrganizationCreate,
Read: resourceOrganizationRead,
Update: resourceOrganizationUpdate,
Delete: resourceOrganizationDelete,
Schema: map[string]*schema.Schema{
organizationNameKey: {
Type: schema.TypeString,
Optional: true,
},
organizationDescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
},
}
}
// convertOrganizationToResourceData populates the provided ResourceData with the appropriate values from the provided Project.
// The organization passed into thie function should be one read from the boundary API with all fields populated.
func convertOrganizationToResourceData(o *scopes.Scope, d *schema.ResourceData) error {
if o.Name != "" {
if err := d.Set(organizationNameKey, o.Name); err != nil {
return err
}
}
if o.Description != "" {
if err := d.Set(organizationDescriptionKey, o.Description); err != nil {
return err
}
}
d.SetId(o.Id)
return nil
}
// convertResourceDataToOrganization returns a localy built Project using the values provided in the ResourceData.
func convertResourceDataToOrganization(d *schema.ResourceData) (*scopes.Scope, error) {
o := &scopes.Scope{Scope: &scopes.ScopeInfo{}}
if descVal, ok := d.GetOk(organizationDescriptionKey); ok {
o.Description = descVal.(string)
}
if nameVal, ok := d.GetOk(organizationNameKey); ok {
o.Name = nameVal.(string)
}
if d.Id() != "" {
o.Id = d.Id()
}
return o, nil
}
func resourceOrganizationCreate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
scp := scopes.NewClient(client)
o, err := convertResourceDataToOrganization(d)
if err != nil {
return err
}
o, _, err = scp.Create(
ctx,
"global", //client.ScopeId(),
scopes.WithName(o.Name),
scopes.WithDescription(o.Description))
if err != nil {
return err
}
d.SetId(o.Id)
return nil
}
func resourceOrganizationRead(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
scp := scopes.NewClient(client)
o, _, err := scp.Read(ctx, d.Id())
if err != nil {
return err
}
return convertOrganizationToResourceData(o, d)
}
func resourceOrganizationUpdate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
projClient := client.Clone()
projClient.SetScopeId(d.Id())
scp := scopes.NewClient(projClient)
o, err := convertResourceDataToOrganization(d)
if err != nil {
return err
}
if d.HasChange(organizationDescriptionKey) {
desc := d.Get(organizationDescriptionKey).(string)
o.Description = desc
}
if d.HasChange(organizationNameKey) {
name := d.Get(organizationNameKey).(string)
o.Name = name
}
o, _, err = scp.Update(
ctx,
d.Id(),
0,
scopes.WithAutomaticVersioning(),
scopes.WithDescription(o.Description),
scopes.WithName(o.Name),
)
if err != nil {
return err
}
return convertOrganizationToResourceData(o, d)
}
func resourceOrganizationDelete(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
projClient := client.Clone()
projClient.SetScopeId(d.Id())
scp := scopes.NewClient(projClient)
o, err := convertResourceDataToOrganization(d)
if err != nil {
return err
}
_, _, err = scp.Delete(ctx, o.Id)
if err != nil {
return fmt.Errorf("failed deleting organization: %w", err)
}
return nil
}
@@ -1,114 +0,0 @@
package provider
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
)
const (
orgFooName = "foo"
orgFooDescription = "foo description"
orgFooDescriptionUpdate = "foo bar description"
)
var (
fooOrg = `
resource "boundary_organization" "foo" {}`
orgFoo = fmt.Sprintf(`
resource "boundary_organization" "foo" {
name = "%s"
description = "%s"
}`, orgFooName, orgFooDescription)
orgFooUpdate = fmt.Sprintf(`
resource "boundary_organization" "foo" {
name = "%s"
description = "%s"
}`, orgFooName, orgFooDescriptionUpdate)
)
func TestAccOrganizationCreation(t *testing.T) {
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]
resource.Test(t, resource.TestCase{
Providers: testProviders,
CheckDestroy: testAccCheckOrganizationResourceDestroy(t),
Steps: []resource.TestStep{
// test create
{
Config: testConfig(url, orgFoo),
Check: resource.ComposeTestCheckFunc(
testAccCheckOrganizationResourceExists("boundary_organization.foo"),
resource.TestCheckResourceAttr("boundary_organization.foo", organizationNameKey, orgFooName),
resource.TestCheckResourceAttr("boundary_organization.foo", organizationDescriptionKey, orgFooDescription),
),
},
// test update
{
Config: testConfig(url, orgFooUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckOrganizationResourceExists("boundary_organization.foo"),
resource.TestCheckResourceAttr("boundary_organization.foo", organizationNameKey, orgFooName),
resource.TestCheckResourceAttr("boundary_organization.foo", organizationDescriptionKey, orgFooDescriptionUpdate),
),
},
},
})
}
func testAccCheckOrganizationResourceExists(name string) 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")
}
if !strings.HasPrefix(id, "o_") {
return fmt.Errorf("ID not formatted as expected, expected prefix 'o_', got %s", id)
}
md := testProvider.Meta().(*metaData)
scp := scopes.NewClient(md.client)
if _, _, err := scp.Read(md.ctx, id); err != nil {
return fmt.Errorf("Got an error when reading organization %q: %v", id, err)
}
return nil
}
}
func testAccCheckOrganizationResourceDestroy(t *testing.T) resource.TestCheckFunc {
return func(s *terraform.State) error {
// retrieve the connection established in Provider configuration
md := testProvider.Meta().(*metaData)
scp := scopes.NewClient(md.client)
for _, rs := range s.RootModule().Resources {
id := rs.Primary.ID
switch rs.Type {
case "boundary_organization":
if _, apiErr, _ := scp.Read(md.ctx, id); apiErr == nil || apiErr.Status != http.StatusNotFound && apiErr.Status != http.StatusForbidden {
return fmt.Errorf("Didn't get a 404 or 403 when reading destroyed organization %q: %v", id, apiErr)
}
default:
t.Logf("Got unknown resource type %q", rs.Type)
}
}
return nil
}
}
-189
View File
@@ -1,189 +0,0 @@
package provider
import (
"fmt"
"strings"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)
const (
projectDescriptionKey = "description"
projectNameKey = "name"
projectScopeIDKey = "scope_id"
)
func resourceProject() *schema.Resource {
return &schema.Resource{
Create: resourceProjectCreate,
Read: resourceProjectRead,
Update: resourceProjectUpdate,
Delete: resourceProjectDelete,
Schema: map[string]*schema.Schema{
projectNameKey: {
Type: schema.TypeString,
Optional: true,
},
projectDescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
projectScopeIDKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
// convertProjectToResourceData populates the provided ResourceData with the appropriate values from the provided Project.
// The project passed into thie function should be one read from the boundary API with all fields populated.
func convertProjectToResourceData(p *scopes.Scope, d *schema.ResourceData) error {
if p.Name != "" {
if err := d.Set(projectNameKey, p.Name); err != nil {
return err
}
}
if p.Description != "" {
if err := d.Set(projectDescriptionKey, p.Description); err != nil {
return err
}
}
if p.Scope != nil && p.Scope.Id != "" {
if err := d.Set(groupScopeIDKey, p.Scope.Id); err != nil {
return err
}
}
d.SetId(p.Id)
return nil
}
// convertResourceDataToProject returns a localy built Project using the values provided in the ResourceData.
func convertResourceDataToProject(d *schema.ResourceData) (*scopes.Scope, error) {
p := &scopes.Scope{Scope: &scopes.ScopeInfo{}}
if descVal, ok := d.GetOk(projectDescriptionKey); ok {
p.Description = descVal.(string)
}
if nameVal, ok := d.GetOk(projectNameKey); ok {
p.Name = nameVal.(string)
}
if scopeIDVal, ok := d.GetOk(projectScopeIDKey); ok {
// boundary only knows about scope_id, and here we want to ensure
// we manage a project within an organization
if !strings.HasPrefix(scopeIDVal.(string), "o_") {
return p, fmt.Errorf("can not use scope_id '%s' for project management", scopeIDVal.(string))
}
p.Scope.Id = scopeIDVal.(string)
}
if d.Id() != "" {
p.Id = d.Id()
}
return p, nil
}
func resourceProjectCreate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
scp := scopes.NewClient(client)
p, err := convertResourceDataToProject(d)
if err != nil {
return err
}
p, _, err = scp.Create(
ctx,
p.Scope.Id,
scopes.WithName(p.Name),
scopes.WithDescription(p.Description))
if err != nil {
return err
}
d.SetId(p.Id)
return nil
}
func resourceProjectRead(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
scp := scopes.NewClient(client)
p, _, err := scp.Read(ctx, d.Id())
if err != nil {
return err
}
return convertProjectToResourceData(p, d)
}
func resourceProjectUpdate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
//projClient := client.Clone()
//projClient.SetScopeId(d.Id())
scp := scopes.NewClient(client) //projClient)
p, err := convertResourceDataToProject(d)
if err != nil {
return err
}
if d.HasChange(projectDescriptionKey) {
desc := d.Get(projectDescriptionKey).(string)
p.Description = desc
}
if d.HasChange(projectNameKey) {
name := d.Get(projectNameKey).(string)
p.Name = name
}
p, _, err = scp.Update(
ctx,
d.Id(),
0,
scopes.WithAutomaticVersioning(),
scopes.WithDescription(p.Description),
scopes.WithName(p.Name),
)
if err != nil {
return err
}
return convertProjectToResourceData(p, d)
}
func resourceProjectDelete(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
//projClient := client.Clone()
//projClient.SetScopeId(d.Id())
scp := scopes.NewClient(client) //projClient)
p, err := convertResourceDataToProject(d)
if err != nil {
return err
}
_, _, err = scp.Delete(ctx, p.Id)
if err != nil {
return fmt.Errorf("failed deleting project: %w", err)
}
return nil
}
-253
View File
@@ -1,253 +0,0 @@
package provider
import (
"fmt"
"net/http"
"strings"
"testing"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/stretchr/testify/assert"
)
const (
fooProject = `
resource "boundary_project" "foo" {
name = "test"
scope_id = boundary_organization.foo.id
}`
firstProjectFoo = `
resource "boundary_project" "project1" {
scope_id = boundary_organization.foo.id
description = "foo"
}`
firstProjectBar = `
resource "boundary_project" "project1" {
scope_id = boundary_organization.foo.id
description = "bar"
}`
secondProject = `
resource "boundary_project" "project2" {
scope_id = boundary_organization.foo.id
description = "project2"
}
`
)
func TestAccProjectCreation(t *testing.T) {
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]
resource.Test(t, resource.TestCase{
Providers: testProviders,
CheckDestroy: testAccCheckProjectResourceDestroy(t),
Steps: []resource.TestStep{
{
Config: testConfig(url, fooOrg, firstProjectBar, secondProject),
Check: resource.ComposeTestCheckFunc(
testAccCheckOrganizationResourceExists("boundary_organization.foo"),
testAccCheckProjectResourceExists("boundary_project.project1"),
resource.TestCheckResourceAttr("boundary_project.project1", projectDescriptionKey, "bar"),
resource.TestCheckResourceAttr("boundary_project.project2", projectDescriptionKey, "project2"),
),
},
// Updates the first project to have description foo
{
Config: testConfig(url, fooOrg, firstProjectFoo, secondProject),
Check: resource.ComposeTestCheckFunc(
testAccCheckProjectResourceExists("boundary_project.project1"),
resource.TestCheckResourceAttr("boundary_project.project1", projectDescriptionKey, "foo"),
resource.TestCheckResourceAttr("boundary_project.project2", projectDescriptionKey, "project2"),
),
},
// Remove second project
{
Config: testConfig(url, fooOrg, firstProjectFoo),
Check: resource.ComposeTestCheckFunc(
testAccCheckProjectResourceExists("boundary_project.project1"),
resource.TestCheckResourceAttr("boundary_project.project1", projectDescriptionKey, "foo"),
),
},
},
})
}
func testAccCheckProjectResourceExists(name string) 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")
}
if !strings.HasPrefix(id, "p_") {
return fmt.Errorf("ID not formatted as expected, expected prefix 'p_', got %s", id)
}
md := testProvider.Meta().(*metaData)
scp := scopes.NewClient(md.client)
if _, _, err := scp.Read(md.ctx, id); err != nil {
return fmt.Errorf("Got an error when reading project %q: %v", id, err)
}
return nil
}
}
func testAccCheckProjectResourceDestroy(t *testing.T) resource.TestCheckFunc {
return func(s *terraform.State) error {
// retrieve the connection established in Provider configuration
md := testProvider.Meta().(*metaData)
scp := scopes.NewClient(md.client)
for _, rs := range s.RootModule().Resources {
id := rs.Primary.ID
switch rs.Type {
case "boundary_organization":
continue
case "boundary_project":
if _, apiErr, _ := scp.Read(md.ctx, id); apiErr == nil || apiErr.Status != http.StatusNotFound && apiErr.Status != http.StatusForbidden {
return fmt.Errorf("Didn't get a 404 or 403 when reading destroyed project %q: %v", id, apiErr)
}
default:
t.Logf("Got unknown resource type %q", rs.Type)
}
}
return nil
}
}
func TestResourceDataToProject(t *testing.T) {
rp := resourceProject()
testCases := []struct {
name string
rData map[string]interface{}
expected *scopes.Scope
}{
{
name: "Fully populated",
rData: map[string]interface{}{
projectNameKey: "name",
projectDescriptionKey: "desc",
},
expected: &scopes.Scope{
Scope: &scopes.ScopeInfo{},
Name: "name",
Description: "desc",
},
},
{
name: "Name populated",
rData: map[string]interface{}{
projectNameKey: "name",
},
expected: &scopes.Scope{
Scope: &scopes.ScopeInfo{},
Name: "name",
},
},
{
name: "Description populated",
rData: map[string]interface{}{
projectDescriptionKey: "desc",
},
expected: &scopes.Scope{
Scope: &scopes.ScopeInfo{},
Description: "desc",
},
},
{
name: "Not populated",
rData: map[string]interface{}{},
expected: &scopes.Scope{Scope: &scopes.ScopeInfo{}},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
rd := rp.TestResourceData()
for k, v := range tc.rData {
err := rd.Set(k, v)
assert.NoError(t, err)
}
r, err := convertResourceDataToProject(rd)
if err != nil {
t.Error(err.Error())
}
assert.Equal(t, tc.expected, r)
})
}
}
func TestProjectToResourceData(t *testing.T) {
rp := resourceProject()
testCases := []struct {
name string
expected map[string]interface{}
proj *scopes.Scope
}{
{
name: "Fully populated",
proj: &scopes.Scope{
Id: "someid",
Name: "name",
Description: "desc",
},
expected: map[string]interface{}{
projectNameKey: "name",
projectDescriptionKey: "desc",
},
},
{
name: "Name populated",
proj: &scopes.Scope{
Id: "someid",
Name: "name",
},
expected: map[string]interface{}{
projectNameKey: "name",
},
},
{
name: "Description populated",
proj: &scopes.Scope{
Id: "someid",
Description: "desc",
},
expected: map[string]interface{}{
projectDescriptionKey: "desc",
},
},
{
name: "Not populated",
proj: &scopes.Scope{
Id: "someid",
},
expected: map[string]interface{}{},
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
expectedRd := rp.TestResourceData()
expectedRd.SetId("someid")
for k, v := range tc.expected {
err := expectedRd.Set(k, v)
assert.NoError(t, err)
}
actual := rp.TestResourceData()
err := convertProjectToResourceData(tc.proj, actual)
assert.NoError(t, err)
assert.Equal(t, expectedRd, actual)
})
}
}
+260 -236
View File
@@ -1,295 +1,319 @@
package provider
import (
"fmt"
"context"
"github.com/hashicorp/boundary/api/roles"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
roleNameKey = "name"
roleDescriptionKey = "description"
rolePrincipalsKey = "principals"
roleGrantsKey = "grants"
roleScopeIDKey = "scope_id"
roleGrantScopeIdKey = "grant_scope_id"
rolePrincipalIdsKey = "principal_ids"
roleGrantStringsKey = "grant_strings"
)
func resourceRole() *schema.Resource {
return &schema.Resource{
Create: resourceRoleCreate,
Read: resourceRoleRead,
Update: resourceRoleUpdate,
Delete: resourceRoleDelete,
CreateContext: resourceRoleCreate,
ReadContext: resourceRoleRead,
UpdateContext: resourceRoleUpdate,
DeleteContext: resourceRoleDelete,
Schema: map[string]*schema.Schema{
roleNameKey: {
NameKey: {
Type: schema.TypeString,
Optional: true,
},
roleDescriptionKey: {
DescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
rolePrincipalsKey: {
ScopeIdKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
rolePrincipalIdsKey: {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
roleGrantsKey: {
roleGrantStringsKey: {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
},
roleScopeIDKey: {
roleGrantScopeIdKey: {
Type: schema.TypeString,
Optional: true,
Computed: true,
ForceNew: true,
},
},
}
}
// convertRoleToResourceData creates a ResourceData type from a Role
func convertRoleToResourceData(r *roles.Role, d *schema.ResourceData) error {
if r.Name != "" {
if err := d.Set(roleNameKey, r.Name); err != nil {
return err
}
}
if r.Description != "" {
if err := d.Set(roleDescriptionKey, r.Description); err != nil {
return err
}
}
if r.PrincipalIds != nil {
if err := d.Set(rolePrincipalsKey, r.PrincipalIds); err != nil {
return err
}
}
if r.Grants != nil {
grants := []string{}
for _, grant := range r.Grants {
grants = append(grants, grant.Raw)
}
if err := d.Set(roleGrantsKey, grants); err != nil {
return err
}
}
if r.Scope.Id != "" {
if err := d.Set(roleScopeIDKey, r.Scope.Id); err != nil {
return err
}
}
d.SetId(r.Id)
return nil
func setFromRoleResponseMap(d *schema.ResourceData, raw map[string]interface{}) {
d.Set(NameKey, raw["name"])
d.Set(DescriptionKey, raw["description"])
d.Set(ScopeIdKey, raw["scope_id"])
d.Set(rolePrincipalIdsKey, raw["principal_ids"])
d.Set(roleGrantStringsKey, raw["grant_strings"])
d.Set(roleGrantScopeIdKey, raw["grant_scope_id"])
d.SetId(raw["id"].(string))
}
// convertResourceDataToRole returns a localy built Role using the values provided in the ResourceData.
func convertResourceDataToRole(d *schema.ResourceData) *roles.Role {
r := &roles.Role{Scope: &scopes.ScopeInfo{}}
if projIDVal, ok := d.GetOk(roleScopeIDKey); ok {
r.Scope.Id = projIDVal.(string)
}
if descVal, ok := d.GetOk(roleDescriptionKey); ok {
r.Description = descVal.(string)
}
if nameVal, ok := d.GetOk(roleNameKey); ok {
r.Name = nameVal.(string)
}
if val, ok := d.GetOk(rolePrincipalsKey); ok {
principalIds := val.(*schema.Set).List()
for _, i := range principalIds {
r.PrincipalIds = append(r.PrincipalIds, i.(string))
}
}
if val, ok := d.GetOk(roleGrantsKey); ok {
grants := val.(*schema.Set).List()
for _, i := range grants {
g := &roles.Grant{Raw: i.(string)}
r.Grants = append(r.Grants, g)
}
}
if d.Id() != "" {
r.Id = d.Id()
}
return r
}
func resourceRoleCreate(d *schema.ResourceData, meta interface{}) error {
func resourceRoleCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
r := convertResourceDataToRole(d)
rolesClient := roles.NewClient(client)
principals := r.PrincipalIds
grants := []string{}
for _, g := range r.Grants {
grants = append(grants, g.Raw)
var scopeId string
if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok {
scopeId = scopeIdVal.(string)
} else {
return diag.Errorf("no scope ID provided")
}
r, apiErr, err := rolesClient.Create(
opts := []roles.Option{}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, roles.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, roles.WithDescription(descStr))
}
grantScopeIdVal, ok := d.GetOk(roleGrantScopeIdKey)
if ok {
grantScopeIdStr := grantScopeIdVal.(string)
opts = append(opts, roles.WithGrantScopeId(grantScopeIdStr))
}
var principalIds []string
if principalIdsVal, ok := d.GetOk(rolePrincipalIdsKey); ok {
list := principalIdsVal.(*schema.Set).List()
principalIds = make([]string, 0, len(list))
for _, i := range list {
principalIds = append(principalIds, i.(string))
}
}
var grantStrings []string
if grantStringsVal, ok := d.GetOk(roleGrantStringsKey); ok {
list := grantStringsVal.(*schema.Set).List()
grantStrings = make([]string, 0, len(list))
for _, i := range list {
grantStrings = append(grantStrings, i.(string))
}
}
rc := roles.NewClient(md.client)
tcr, apiErr, err := rc.Create(
ctx,
roles.WithName(r.Name),
roles.WithDescription(r.Description),
roles.WithScopeId(r.Scope.Id))
if apiErr != nil {
return fmt.Errorf("error creating role: %s\n", apiErr.Message)
}
scopeId,
opts...)
if err != nil {
return fmt.Errorf("error creating role: %s\n", err)
}
// on first create CreateRole() returns without err but upon
// running AddGrants it claims the role is not found. This
// doesn't occur in the test case but only on a live cluster.
if len(grants) > 0 {
r, apiErr, err = rolesClient.AddGrants(
ctx,
r.Id,
r.Version,
grants,
roles.WithScopeId(r.Scope.Id))
if apiErr != nil {
return fmt.Errorf("error setting grants on role:: %s\n", apiErr.Message)
}
if err != nil {
return fmt.Errorf("error setting grants on role: %s\n", err)
}
}
if len(principals) > 0 {
r, apiErr, err = rolesClient.SetPrincipals(
ctx,
r.Id,
r.Version,
principals,
roles.WithScopeId(r.Scope.Id))
if apiErr != nil {
return fmt.Errorf("error setting principals on role: %s\n", apiErr.Message)
}
if err != nil {
return fmt.Errorf("error setting principals on role: %s\n", err)
}
}
return convertRoleToResourceData(r, d)
}
func resourceRoleRead(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
r := convertResourceDataToRole(d)
rolesClient := roles.NewClient(client)
r, apiErr, err := rolesClient.Read(ctx, r.Id, roles.WithScopeId(r.Scope.Id))
if err != nil {
return fmt.Errorf("error reading role: %s", err.Error())
return diag.Errorf("error calling create role: %v", err)
}
if apiErr != nil {
return fmt.Errorf("error reading role: %s", apiErr.Message)
return diag.Errorf("error creating role: %s", apiErr.Message)
}
return convertRoleToResourceData(r, d)
}
func resourceRoleUpdate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
r := convertResourceDataToRole(d)
rolesClient := roles.NewClient(client)
if d.HasChange(roleNameKey) {
r.Name = d.Get(roleNameKey).(string)
if tcr == nil {
return diag.Errorf("nil role after create")
}
raw := tcr.GetResponseMap()
if d.HasChange(roleDescriptionKey) {
r.Description = d.Get(roleDescriptionKey).(string)
}
r, apiErr, err := rolesClient.Update(
ctx,
r.Id,
0,
roles.WithAutomaticVersioning(),
roles.WithName(r.Name),
roles.WithDescription(r.Description),
roles.WithScopeId(r.Scope.Id))
if apiErr != nil || err != nil {
return fmt.Errorf("error updating role:\n API Err: %+v\n Err: %+v\n", *apiErr, err)
}
if d.HasChange(roleGrantsKey) {
grants := []string{}
grantSet := d.Get(roleGrantsKey).(*schema.Set).List()
for _, grant := range grantSet {
grants = append(grants, grant.(string))
}
r, apiErr, err = rolesClient.SetGrants(
if principalIds != nil {
tspr, apiErr, err := rc.SetPrincipals(
ctx,
r.Id,
r.Version,
grants,
roles.WithScopeId(r.Scope.Id))
if apiErr != nil || err != nil {
return fmt.Errorf("error setting grants on role:\n API Err: %+v\n Err: %+v\n", *apiErr, err)
}
}
if d.HasChange(rolePrincipalsKey) {
principalIds := []string{}
principals := d.Get(rolePrincipalsKey).(*schema.Set).List()
for _, principal := range principals {
principalIds = append(principalIds, principal.(string))
}
r, apiErr, err = rolesClient.SetPrincipals(
ctx,
r.Id,
r.Version,
tcr.Item.Id,
0,
principalIds,
roles.WithScopeId(r.Scope.Id))
if apiErr != nil || err != nil {
return fmt.Errorf("error updating principal on role:\n API Err: %+v\n Err: %+v\n", *apiErr, err)
roles.WithAutomaticVersioning(true))
if apiErr != nil {
return diag.Errorf("error setting principal IDs on role: %s", apiErr.Message)
}
if err != nil {
return diag.Errorf("error setting principal IDs on role: %v", err)
}
if tspr == nil {
return diag.Errorf("nil role after setting principal IDs")
}
raw = tspr.GetResponseMap()
}
if grantStrings != nil {
tsgr, apiErr, err := rc.SetGrants(
ctx,
tcr.Item.Id,
0,
grantStrings,
roles.WithAutomaticVersioning(true))
if apiErr != nil {
return diag.Errorf("error setting grant strings on role: %s", apiErr.Message)
}
if err != nil {
return diag.Errorf("error setting grant strings on role: %v", err)
}
if tsgr == nil {
return diag.Errorf("nil role after setting grant strings")
}
raw = tsgr.GetResponseMap()
}
setFromRoleResponseMap(d, raw)
return nil
}
func resourceRoleRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
rc := roles.NewClient(md.client)
trr, apiErr, err := rc.Read(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling read role: %v", err)
}
if apiErr != nil {
return diag.Errorf("error reading role: %s", apiErr.Message)
}
if trr == nil {
return diag.Errorf("role nil after read")
}
setFromRoleResponseMap(d, trr.GetResponseMap())
return nil
}
func resourceRoleUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
rc := roles.NewClient(md.client)
opts := []roles.Option{}
var name *string
if d.HasChange(NameKey) {
opts = append(opts, roles.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, roles.WithName(nameStr))
}
}
return convertRoleToResourceData(r, d)
}
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, roles.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, roles.WithDescription(descStr))
}
}
func resourceRoleDelete(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
var grantScopeId *string
if d.HasChange(roleGrantScopeIdKey) {
opts = append(opts, roles.DefaultGrantScopeId())
grantScopeIdVal, ok := d.GetOk(roleGrantScopeIdKey)
if ok {
grantScopeIdStr := grantScopeIdVal.(string)
grantScopeId = &grantScopeIdStr
opts = append(opts, roles.WithGrantScopeId(grantScopeIdStr))
}
}
r := convertResourceDataToRole(d)
rolesClient := roles.NewClient(client)
if len(opts) > 0 {
opts = append(opts, roles.WithAutomaticVersioning(true))
_, apiErr, err := rc.Update(
ctx,
d.Id(),
0,
opts...)
if err != nil {
return diag.Errorf("error calling update target: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating target: %s", apiErr.Message)
}
}
_, apiErr, err := rolesClient.Delete(ctx, r.Id, roles.WithScopeId(r.Scope.Id))
if apiErr != nil || err != nil {
return fmt.Errorf("error deleting role:\n API Err: %+v\n Err: %+v\n", *apiErr, err)
if d.HasChange(NameKey) {
d.Set(NameKey, name)
}
if d.HasChange(DescriptionKey) {
d.Set(DescriptionKey, desc)
}
if d.HasChange(roleGrantScopeIdKey) {
d.Set(roleGrantScopeIdKey, grantScopeId)
}
if d.HasChange(roleGrantStringsKey) {
var grantStrings []string
if grantStringsVal, ok := d.GetOk(roleGrantStringsKey); ok {
grants := grantStringsVal.(*schema.Set).List()
for _, grant := range grants {
grantStrings = append(grantStrings, grant.(string))
}
}
_, apiErr, err := rc.SetGrants(
ctx,
d.Id(),
0,
grantStrings,
roles.WithAutomaticVersioning(true))
if err != nil {
return diag.Errorf("error updating grant strings on role: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating grant strings on role: %s", apiErr.Message)
}
d.Set(roleGrantStringsKey, grantStrings)
}
if d.HasChange(rolePrincipalIdsKey) {
var principalIds []string
if principalIdsVal, ok := d.GetOk(rolePrincipalIdsKey); ok {
principals := principalIdsVal.(*schema.Set).List()
for _, principal := range principals {
principalIds = append(principalIds, principal.(string))
}
}
_, apiErr, err := rc.SetPrincipals(
ctx,
d.Id(),
0,
principalIds,
roles.WithAutomaticVersioning(true))
if err != nil {
return diag.Errorf("error updating grant strings on role: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating grant strings on role: %s", apiErr.Message)
}
d.Set(rolePrincipalIdsKey, principalIds)
}
return nil
}
func resourceRoleDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
rc := roles.NewClient(md.client)
_, apiErr, err := rc.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling delete role: %s", err.Error())
}
if apiErr != nil {
return diag.Errorf("error deleting role: %s", apiErr.Message)
}
return nil
+88 -124
View File
@@ -1,6 +1,7 @@
package provider
import (
"context"
"errors"
"fmt"
"net/http"
@@ -9,8 +10,8 @@ import (
"github.com/hashicorp/boundary/api/roles"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
@@ -21,96 +22,92 @@ const (
var (
orgRole = fmt.Sprintf(`
resource "boundary_role" "foo" {
name = "test"
name = "test"
description = "%s"
scope_id = boundary_organization.foo.id
scope_id = boundary_scope.org1.id
}`, fooRoleDescription)
orgRoleUpdate = fmt.Sprintf(`
resource "boundary_role" "foo" {
name = "test"
name = "test"
description = "%s"
scope_id = boundary_organization.foo.id
scope_id = boundary_scope.org1.id
}`, fooRoleDescriptionUpdate)
ProjToOrgRole = fmt.Sprintf(`
resource "boundary_role" "foo" {
name = "test"
description = "%s"
scope_id = "%s"
}`, fooRoleDescription, tcOrg)
projRole = fmt.Sprintf(`
resource "boundary_role" "foo" {
name = "test"
name = "test"
description = "%s"
scope_id = boundary_project.foo.id
scope_id = boundary_scope.proj1.id
}`, fooRoleDescription)
projRoleUpdate = fmt.Sprintf(`
resource "boundary_role" "foo" {
name = "test"
name = "test"
description = "%s"
scope_id = boundary_project.foo.id
scope_id = boundary_scope.proj1.id
}`, fooRoleDescriptionUpdate)
projRoleWithPrincipal = `
resource "boundary_user" "foo" {
name = "foo"
name = "foo"
scope_id = boundary_scope.org1.id
}
resource "boundary_role" "with_principal" {
name = "with_principal"
description = "with principal"
principals = [boundary_user.foo.id]
scope_id = boundary_project.foo.id
name = "with_principal"
description = "with principal"
principal_ids = [boundary_user.foo.id]
scope_id = boundary_scope.proj1.id
}`
projRoleWithPrincipalUpdate = `
resource "boundary_user" "foo" {
name = "foo"
name = "foo"
scope_id = boundary_scope.org1.id
}
resource "boundary_user" "bar" {
name = "bar"
name = "bar"
scope_id = boundary_scope.org1.id
}
resource "boundary_role" "with_principal" {
name = "with_principal"
description = "with principal"
principals = [boundary_user.foo.id, boundary_user.bar.id]
scope_id = boundary_project.foo.id
name = "with_principal"
description = "with principal"
principal_ids = [boundary_user.foo.id, boundary_user.bar.id]
scope_id = boundary_scope.proj1.id
}`
projRoleWithGroups = `
resource "boundary_group" "foo" {
name = "foo"
scope_id = boundary_project.foo.id
name = "foo"
scope_id = boundary_scope.proj1.id
}
resource "boundary_role" "with_groups" {
name = "with_groups"
description = "with groups"
principals = [boundary_group.foo.id]
scope_id = boundary_project.foo.id
name = "with_groups"
description = "with groups"
principal_ids = [boundary_group.foo.id]
scope_id = boundary_scope.proj1.id
}`
projRoleWithGroupsUpdate = `
resource "boundary_group" "foo" {
name = "foo"
scope_id = boundary_project.foo.id
name = "foo"
scope_id = boundary_scope.proj1.id
}
resource "boundary_group" "bar" {
name = "bar"
scope_id = boundary_project.foo.id
name = "bar"
scope_id = boundary_scope.proj1.id
}
resource "boundary_role" "with_groups" {
name = "with_groups"
description = "with groups"
principals = [boundary_group.foo.id, boundary_group.bar.id]
scope_id = boundary_project.foo.id
name = "with_groups"
description = "with groups"
principal_ids = [boundary_group.foo.id, boundary_group.bar.id]
scope_id = boundary_scope.proj1.id
}`
readonlyGrant = "id=*;actions=read"
@@ -118,18 +115,18 @@ resource "boundary_role" "with_groups" {
projRoleWithGrants = fmt.Sprintf(`
resource "boundary_role" "with_grants" {
name = "with_grants"
description = "with grants"
grants = ["%s"]
scope_id = boundary_project.foo.id
name = "with_grants"
description = "with grants"
grant_strings = ["%s"]
scope_id = boundary_scope.proj1.id
}`, readonlyGrant)
projRoleWithGrantsUpdate = fmt.Sprintf(`
resource "boundary_role" "with_grants" {
name = "with_grants"
description = "with grants"
grants = ["%s", "%s"]
scope_id = boundary_project.foo.id
name = "with_grants"
description = "with grants"
grant_strings = ["%s", "%s"]
scope_id = boundary_scope.proj1.id
}`, readonlyGrant, readonlyGrantUpdate)
)
@@ -162,7 +159,7 @@ func TestAccRoleToOrgToProject(t *testing.T) {
},
{
// test org to project role create
Config: testConfig(url, fooOrg, fooProject, projRole),
Config: testConfig(url, fooOrg, firstProjectFoo, projRole),
Check: resource.ComposeTestCheckFunc(
testAccCheckRoleResourceExists("boundary_role.foo"),
resource.TestCheckResourceAttr("boundary_role.foo", "name", "test"),
@@ -171,7 +168,7 @@ func TestAccRoleToOrgToProject(t *testing.T) {
},
{
// test project role update
Config: testConfig(url, fooOrg, fooProject, projRoleUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, projRoleUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckRoleResourceExists("boundary_role.foo"),
resource.TestCheckResourceAttr("boundary_role.foo", "name", "test"),
@@ -193,7 +190,7 @@ func TestAccRoleWithGrants(t *testing.T) {
Steps: []resource.TestStep{
{
// test project role create with grants
Config: testConfig(url, fooOrg, fooProject, projRoleWithGrants),
Config: testConfig(url, fooOrg, firstProjectFoo, projRoleWithGrants),
Check: resource.ComposeTestCheckFunc(
testAccCheckRoleResourceExists("boundary_role.with_grants"),
testAccCheckRoleResourceGrantsSet("boundary_role.with_grants", []string{readonlyGrant}),
@@ -203,7 +200,7 @@ func TestAccRoleWithGrants(t *testing.T) {
},
{
// test project role update with grants
Config: testConfig(url, fooOrg, fooProject, projRoleWithGrantsUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, projRoleWithGrantsUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckRoleResourceExists("boundary_role.with_grants"),
@@ -227,19 +224,19 @@ func TestAccRoleWithPrincipals(t *testing.T) {
Steps: []resource.TestStep{
{
// test create
Config: testConfig(url, fooOrg, fooProject, projRoleWithPrincipal),
Config: testConfig(url, fooOrg, firstProjectFoo, projRoleWithPrincipal),
Check: resource.ComposeTestCheckFunc(
testAccCheckRoleResourceExists("boundary_role.with_principal"),
testAccCheckRoleResourcePrincipalsSet("boundary_role.with_principal", []string{"boundary_user.foo"}),
testAccCheckUserResourceExists("boundary_user.foo"),
resource.TestCheckResourceAttr("boundary_role.with_principal", roleDescriptionKey, "with principal"),
resource.TestCheckResourceAttr("boundary_role.with_principal", roleNameKey, "with_principal"),
resource.TestCheckResourceAttr("boundary_role.with_principal", DescriptionKey, "with principal"),
resource.TestCheckResourceAttr("boundary_role.with_principal", NameKey, "with_principal"),
resource.TestCheckResourceAttr("boundary_user.foo", "name", "foo"),
),
},
{
// test update
Config: testConfig(url, fooOrg, fooProject, projRoleWithPrincipalUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, projRoleWithPrincipalUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckRoleResourceExists("boundary_role.with_principal"),
testAccCheckUserResourceExists("boundary_user.foo"),
@@ -262,23 +259,23 @@ func TestAccRoleWithGroups(t *testing.T) {
Steps: []resource.TestStep{
{
// test create
Config: testConfig(url, fooOrg, fooProject, projRoleWithGroups),
Config: testConfig(url, fooOrg, firstProjectFoo, projRoleWithGroups),
Check: resource.ComposeTestCheckFunc(
testAccCheckRoleResourceExists("boundary_role.with_groups"),
testAccCheckRoleResourceGroupsSet("boundary_role.with_groups", []string{"boundary_group.foo"}),
testAccCheckUserResourceExists("boundary_group.foo"),
resource.TestCheckResourceAttr("boundary_role.with_groups", roleDescriptionKey, "with groups"),
resource.TestCheckResourceAttr("boundary_role.with_groups", roleNameKey, "with_groups"),
testAccCheckGroupResourceExists("boundary_group.foo"),
resource.TestCheckResourceAttr("boundary_role.with_groups", DescriptionKey, "with groups"),
resource.TestCheckResourceAttr("boundary_role.with_groups", NameKey, "with_groups"),
resource.TestCheckResourceAttr("boundary_group.foo", "name", "foo"),
),
},
{
// test update
Config: testConfig(url, fooOrg, fooProject, projRoleWithGroupsUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, projRoleWithGroupsUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckRoleResourceExists("boundary_role.with_groups"),
testAccCheckUserResourceExists("boundary_group.foo"),
testAccCheckUserResourceExists("boundary_group.bar"),
testAccCheckGroupResourceExists("boundary_group.foo"),
testAccCheckGroupResourceExists("boundary_group.bar"),
testAccCheckRoleResourceGroupsSet("boundary_role.with_groups", []string{"boundary_group.foo", "boundary_group.bar"}),
),
},
@@ -314,15 +311,9 @@ func testAccCheckRoleDestroyed(name string) resource.TestCheckFunc {
}
md := testProvider.Meta().(*metaData)
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
rolesClient := roles.NewClient(projClient)
rolesClient := roles.NewClient(md.client)
if _, apiErr, _ := rolesClient.Read(md.ctx, id); apiErr == nil || apiErr.Status != http.StatusNotFound {
if _, apiErr, _ := rolesClient.Read(context.Background(), id); apiErr == nil || apiErr.Status != http.StatusNotFound {
errs = append(errs, fmt.Sprintf("Role not destroyed %q: %v", id, apiErr))
}
@@ -343,15 +334,9 @@ func testAccCheckRoleResourceExists(name string) resource.TestCheckFunc {
}
md := testProvider.Meta().(*metaData)
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
rolesClient := roles.NewClient(projClient)
rolesClient := roles.NewClient(md.client)
if _, _, err := rolesClient.Read(md.ctx, id); err != nil {
if _, _, err := rolesClient.Read(context.Background(), id); err != nil {
return fmt.Errorf("Got an error when reading role %q: %v", id, err)
}
@@ -387,26 +372,20 @@ func testAccCheckRoleResourcePrincipalsSet(name string, principals []string) res
}
md := testProvider.Meta().(*metaData)
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
rolesClient := roles.NewClient(projClient)
rolesClient := roles.NewClient(md.client)
r, _, err := rolesClient.Read(md.ctx, id)
rr, _, err := rolesClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading role %q: %v", id, err)
}
// for every principal set as a principal on the role in the state, ensure
// each role in boundary has the same setings
if len(r.PrincipalIds) == 0 {
if len(rr.Item.PrincipalIds) == 0 {
return fmt.Errorf("no principals found in boundary")
}
for _, statePrincipal := range r.PrincipalIds {
for _, statePrincipal := range rr.Item.PrincipalIds {
ok := false
for _, gotPrincipal := range userIDs {
if gotPrincipal == statePrincipal {
@@ -450,26 +429,20 @@ func testAccCheckRoleResourceGroupsSet(name string, groups []string) resource.Te
}
md := testProvider.Meta().(*metaData)
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
rolesClient := roles.NewClient(projClient)
rolesClient := roles.NewClient(md.client)
r, _, err := rolesClient.Read(md.ctx, id)
rr, _, err := rolesClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading role %q: %v", id, err)
}
// for every principal set as a principal on the role in the state, ensure
// each role in boundary has the same setings
if len(r.PrincipalIds) == 0 {
if len(rr.Item.PrincipalIds) == 0 {
return fmt.Errorf("no groups found in boundary")
}
for _, stateGroup := range r.PrincipalIds {
for _, stateGroup := range rr.Item.PrincipalIds {
ok := false
for _, gotGroup := range groupIDs {
if gotGroup == stateGroup {
@@ -498,33 +471,27 @@ func testAccCheckRoleResourceGrantsSet(name string, expectedGrants []string) res
}
md := testProvider.Meta().(*metaData)
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
rolesClient := roles.NewClient(projClient)
rolesClient := roles.NewClient(md.client)
r, _, err := rolesClient.Read(md.ctx, id)
rr, _, err := rolesClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading role %q: %v", id, err)
}
// for each expected grant, ensure they're set on the role
if len(r.Grants) == 0 {
return fmt.Errorf("no grants found on role, %+v\n", r)
if len(rr.Item.Grants) == 0 {
return fmt.Errorf("no grants found on role, %+v\n", rr)
}
for _, grant := range expectedGrants {
ok = false
for _, gotGrant := range r.Grants {
for _, gotGrant := range rr.Item.Grants {
if gotGrant.Raw == grant {
ok = true
}
}
if !ok {
return fmt.Errorf("expected grant not found on role, %s: %s\n Have: %#v\n", r.Name, grant, r)
return fmt.Errorf("expected grant not found on role, %s: %s\n Have: %#v\n", rr.Item.Name, grant, rr.Item)
}
}
@@ -541,7 +508,7 @@ func testAccCheckRoleResourceDestroy(t *testing.T) resource.TestCheckFunc {
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "boundary_project":
case "boundary_scope":
continue
case "boundary_user":
continue
@@ -550,17 +517,14 @@ func testAccCheckRoleResourceDestroy(t *testing.T) resource.TestCheckFunc {
case "boundary_role":
id := rs.Primary.ID
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
rolesClient := roles.NewClient(projClient)
rolesClient := roles.NewClient(md.client)
_, apiErr, _ := rolesClient.Read(md.ctx, id)
if apiErr == nil || apiErr.Status != http.StatusNotFound && apiErr.Status != http.StatusForbidden {
return fmt.Errorf("Didn't get a 404 or 403 when reading destroyed role %q: %v", id, apiErr)
_, apiErr, err := rolesClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Error when reading destroyed role %q: %v", id, err)
}
if apiErr == nil || apiErr.Status != http.StatusNotFound {
return fmt.Errorf("Didn't get a 404 when reading destroyed role %q: %v", id, apiErr)
}
default:
+174
View File
@@ -0,0 +1,174 @@
package provider
import (
"context"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func resourceScope() *schema.Resource {
return &schema.Resource{
CreateContext: resourceScopeCreate,
ReadContext: resourceScopeRead,
UpdateContext: resourceScopeUpdate,
DeleteContext: resourceScopeDelete,
Schema: map[string]*schema.Schema{
NameKey: {
Type: schema.TypeString,
Optional: true,
},
DescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
ScopeIdKey: {
Type: schema.TypeString,
Required: true,
ForceNew: true,
},
},
}
}
func setFromScopeResponseMap(d *schema.ResourceData, raw map[string]interface{}) {
d.Set(NameKey, raw["name"])
d.Set(DescriptionKey, raw["description"])
d.Set(ScopeIdKey, raw["scope_id"])
d.SetId(raw["id"].(string))
}
func resourceScopeCreate(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 := []scopes.Option{}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, scopes.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, scopes.WithDescription(descStr))
}
scp := scopes.NewClient(md.client)
scr, apiErr, err := scp.Create(
ctx,
scopeId,
opts...)
if err != nil {
return diag.Errorf("error calling create scope: %v", err)
}
if apiErr != nil {
return diag.Errorf("error creating scope: %s", apiErr.Message)
}
if scr == nil {
return diag.Errorf("scope nil after create")
}
setFromScopeResponseMap(d, scr.GetResponseMap())
return nil
}
func resourceScopeRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
scp := scopes.NewClient(md.client)
srr, apiErr, err := scp.Read(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling read scope: %v", err)
}
if apiErr != nil {
return diag.Errorf("error reading scope: %s", apiErr.Message)
}
if srr == nil {
return diag.Errorf("scope nil after read")
}
setFromScopeResponseMap(d, srr.GetResponseMap())
return nil
}
func resourceScopeUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
scp := scopes.NewClient(md.client)
opts := []scopes.Option{}
var name *string
if d.HasChange(NameKey) {
opts = append(opts, scopes.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, scopes.WithName(nameStr))
}
}
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, scopes.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, scopes.WithDescription(descStr))
}
}
if len(opts) > 0 {
opts = append(opts, scopes.WithAutomaticVersioning(true))
_, apiErr, err := scp.Update(
ctx,
d.Id(),
0,
opts...)
if err != nil {
return diag.Errorf("error calling update scope: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating scope: %s", apiErr.Message)
}
}
if d.HasChange(NameKey) {
d.Set(NameKey, name)
}
if d.HasChange(DescriptionKey) {
d.Set(DescriptionKey, desc)
}
return nil
}
func resourceScopeDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
scp := scopes.NewClient(md.client)
_, apiErr, err := scp.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling delete scope: %v", err)
}
if apiErr != nil {
return diag.Errorf("error deleting scope: %s", apiErr.Message)
}
return nil
}
+130
View File
@@ -0,0 +1,130 @@
package provider
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
fooOrg = `
resource "boundary_scope" "org1" {
name = "test"
scope_id = "global"
}`
firstProjectFoo = `
resource "boundary_scope" "proj1" {
scope_id = boundary_scope.org1.id
description = "foo"
}`
firstProjectBar = `
resource "boundary_scope" "proj1" {
scope_id = boundary_scope.org1.id
description = "bar"
}`
secondProject = `
resource "boundary_scope" "proj2" {
scope_id = boundary_scope.org1.id
description = "project2"
}
`
)
func TestAccScopeCreation(t *testing.T) {
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]
resource.Test(t, resource.TestCase{
Providers: testProviders,
CheckDestroy: testAccCheckScopeResourceDestroy(t),
Steps: []resource.TestStep{
{
Config: testConfig(url, fooOrg, firstProjectFoo, secondProject),
Check: resource.ComposeTestCheckFunc(
testAccCheckScopeResourceExists("boundary_scope.org1"),
testAccCheckScopeResourceExists("boundary_scope.proj1"),
resource.TestCheckResourceAttr("boundary_scope.proj1", DescriptionKey, "foo"),
resource.TestCheckResourceAttr("boundary_scope.proj2", DescriptionKey, "project2"),
),
},
// Updates the first project to have description bar
{
Config: testConfig(url, fooOrg, firstProjectBar, secondProject),
Check: resource.ComposeTestCheckFunc(
testAccCheckScopeResourceExists("boundary_scope.proj1"),
resource.TestCheckResourceAttr("boundary_scope.proj1", DescriptionKey, "bar"),
resource.TestCheckResourceAttr("boundary_scope.proj2", DescriptionKey, "project2"),
),
},
// Remove second project
{
Config: testConfig(url, fooOrg, firstProjectBar),
Check: resource.ComposeTestCheckFunc(
testAccCheckScopeResourceExists("boundary_scope.proj1"),
resource.TestCheckResourceAttr("boundary_scope.proj1", DescriptionKey, "bar"),
),
},
},
})
}
func testAccCheckScopeResourceExists(name string) 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)
scp := scopes.NewClient(md.client)
_, apiErr, err := scp.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading scope %q: %v", id, err)
}
if apiErr != nil {
return fmt.Errorf("Got an API error when reading scope %q: %v", id, apiErr.Message)
}
return nil
}
}
func testAccCheckScopeResourceDestroy(t *testing.T) resource.TestCheckFunc {
return func(s *terraform.State) error {
// retrieve the connection established in Provider configuration
md := testProvider.Meta().(*metaData)
scp := scopes.NewClient(md.client)
for _, rs := range s.RootModule().Resources {
id := rs.Primary.ID
switch rs.Type {
case "boundary_scope":
_, apiErr, err := scp.Read(context.Background(), id)
if err != nil {
return err
}
if apiErr == nil || apiErr.Status != http.StatusNotFound {
return fmt.Errorf("Didn't get a 404 when reading destroyed resource %q: %v", id, apiErr)
}
default:
t.Logf("Got unknown resource type %q", rs.Type)
}
}
return nil
}
}
+226 -189
View File
@@ -1,51 +1,51 @@
package provider
import (
"fmt"
"context"
"encoding/json"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
const (
targetNameKey = "name"
targetDescriptionKey = "description"
targetScopeIDKey = "scope_id"
targetHostSetIDsKey = "host_set_ids"
targetProtoKey = "proto"
targetHostSetIdsKey = "host_set_ids"
targetDefaultPortKey = "default_port"
targetProtoTCP = "tcp"
targetTypeTcp = "tcp"
)
func resourceTarget() *schema.Resource {
return &schema.Resource{
Create: resourceTargetCreate,
Read: resourceTargetRead,
Update: resourceTargetUpdate,
Delete: resourceTargetDelete,
CreateContext: resourceTargetCreate,
ReadContext: resourceTargetRead,
UpdateContext: resourceTargetUpdate,
DeleteContext: resourceTargetDelete,
Schema: map[string]*schema.Schema{
targetNameKey: {
NameKey: {
Type: schema.TypeString,
Optional: true,
},
targetDescriptionKey: {
DescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
targetProtoKey: {
TypeKey: {
Type: schema.TypeString,
Optional: true,
Required: true,
ForceNew: true,
},
targetScopeIDKey: {
ScopeIdKey: {
Type: schema.TypeString,
Optional: true,
Required: true,
ForceNew: true,
Computed: true,
},
targetHostSetIDsKey: {
targetDefaultPortKey: {
Type: schema.TypeInt,
Optional: true,
},
targetHostSetIdsKey: {
Type: schema.TypeSet,
Optional: true,
Elem: &schema.Schema{Type: schema.TypeString},
@@ -54,200 +54,237 @@ func resourceTarget() *schema.Resource {
}
}
// convertTargetToResourceData creates a ResourceData type from a Group
func convertTargetToResourceData(t *targets.Target, d *schema.ResourceData) error {
if t.Name != "" {
if err := d.Set(targetNameKey, t.Name); err != nil {
return err
}
}
if t.Description != "" {
if err := d.Set(targetDescriptionKey, t.Description); err != nil {
return err
}
}
if t.Scope != nil && t.Scope.Id != "" {
if err := d.Set(targetScopeIDKey, t.Scope.Id); err != nil {
return err
}
}
if t.HostSetIds != nil {
if err := d.Set(targetHostSetIDsKey, t.HostSetIds); err != nil {
return err
}
}
d.SetId(t.Id)
return nil
func setFromTargetResponseMap(d *schema.ResourceData, raw map[string]interface{}) {
d.Set(NameKey, raw["name"])
d.Set(DescriptionKey, raw["description"])
d.Set(ScopeIdKey, raw["scope_id"])
d.Set(TypeKey, raw["type"])
d.Set(targetHostSetIdsKey, raw["host_set_ids"])
defPort := raw["default_port"].(json.Number)
defPortInt, _ := defPort.Int64()
d.Set(targetDefaultPortKey, int(defPortInt))
d.SetId(raw["id"].(string))
}
// convertResourceDataToTarget returns a localy built Group using the values provided in the ResourceData.
func convertResourceDataToTarget(d *schema.ResourceData, meta *metaData) (*targets.Target, string) {
t := &targets.Target{Scope: &scopes.ScopeInfo{}}
proto := targetProtoTCP
if descVal, ok := d.GetOk(targetDescriptionKey); ok {
t.Description = descVal.(string)
}
if nameVal, ok := d.GetOk(targetNameKey); ok {
t.Name = nameVal.(string)
}
if scopeIDVal, ok := d.GetOk(targetScopeIDKey); ok {
t.Scope.Id = scopeIDVal.(string)
}
if protoVal, ok := d.GetOk(targetProtoKey); ok {
proto = protoVal.(string)
}
if val, ok := d.GetOk(targetHostSetIDsKey); ok {
hostSetIds := val.(*schema.Set).List()
for _, i := range hostSetIds {
t.HostSetIds = append(t.HostSetIds, i.(string))
}
}
if d.Id() != "" {
t.Id = d.Id()
}
return t, proto
}
func resourceTargetCreate(d *schema.ResourceData, meta interface{}) error {
func resourceTargetCreate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
t, proto := convertResourceDataToTarget(d, md)
tgts := targets.NewClient(client)
var scopeId string
if scopeIdVal, ok := d.GetOk(ScopeIdKey); ok {
scopeId = scopeIdVal.(string)
} else {
return diag.Errorf("no scope ID provided")
}
hostSetIDs := t.HostSetIds
var typeStr string
if typeVal, ok := d.GetOk(TypeKey); ok {
typeStr = typeVal.(string)
} else {
return diag.Errorf("no type provided")
}
switch typeStr {
case targetTypeTcp:
default:
return diag.Errorf("invalid type provided")
}
t, apiErr, err := tgts.Create(
opts := []targets.Option{}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, targets.WithName(nameStr))
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, targets.WithDescription(descStr))
}
var defaultPort *int
defaultPortVal, ok := d.GetOk(targetDefaultPortKey)
if ok {
defaultPortInt := defaultPortVal.(int)
if defaultPortInt < 0 {
return diag.Errorf(`"default_port" cannot be less than zero`)
}
defaultPort = &defaultPortInt
opts = append(opts, targets.WithDefaultPort(uint32(*defaultPort)))
}
var hostSetIds []string
if hostSetIdsVal, ok := d.GetOk(targetHostSetIdsKey); ok {
list := hostSetIdsVal.(*schema.Set).List()
hostSetIds = make([]string, 0, len(list))
for _, i := range list {
hostSetIds = append(hostSetIds, i.(string))
}
}
tc := targets.NewClient(md.client)
tcr, apiErr, err := tc.Create(
ctx,
proto,
t.Scope.Id,
targets.WithName(t.Name),
targets.WithDescription(t.Description))
typeStr,
scopeId,
opts...)
if err != nil {
return fmt.Errorf("error creating target: %s", err.Error())
return diag.Errorf("error calling create target: %v", err)
}
if apiErr != nil {
return fmt.Errorf("error creating target: %s", apiErr.Message)
return diag.Errorf("error creating target: %s", apiErr.Message)
}
if tcr == nil {
return diag.Errorf("target nil after create")
}
raw := tcr.GetResponseMap()
if len(hostSetIDs) > 0 {
t, apiErr, err = tgts.SetHostSets(
if hostSetIds != nil {
tur, apiErr, err := tc.SetHostSets(
ctx,
t.Id,
t.Version,
hostSetIDs,
targets.WithScopeId(t.Scope.Id))
tcr.Item.Id,
tcr.Item.Version,
hostSetIds)
if apiErr != nil {
return fmt.Errorf("error setting host sets on target: %s\n", apiErr.Message)
return diag.Errorf("error setting host sets on target: %s", apiErr.Message)
}
if err != nil {
return fmt.Errorf("error setting host sets on target: %s\n", err)
return diag.Errorf("error setting host sets on target: %v", err)
}
raw = tur.GetResponseMap()
}
setFromTargetResponseMap(d, raw)
return nil
}
func resourceTargetRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
tc := targets.NewClient(md.client)
trr, apiErr, err := tc.Read(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling read target: %v", err)
}
if apiErr != nil {
return diag.Errorf("error reading target: %s", apiErr.Message)
}
if trr == nil {
return diag.Errorf("target nil after read")
}
setFromTargetResponseMap(d, trr.GetResponseMap())
return nil
}
func resourceTargetUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
tc := targets.NewClient(md.client)
opts := []targets.Option{}
var name *string
if d.HasChange(NameKey) {
opts = append(opts, targets.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, targets.WithName(nameStr))
}
}
return convertTargetToResourceData(t, d)
}
func resourceTargetRead(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
t, _ := convertResourceDataToTarget(d, md)
tgts := targets.NewClient(client)
t, apiErr, err := tgts.Read(ctx, t.Id, targets.WithScopeId(t.Scope.Id))
if err != nil {
return fmt.Errorf("error reading target: %s", err.Error())
}
if apiErr != nil {
return fmt.Errorf("error reading target: %s", apiErr.Message)
}
return convertTargetToResourceData(t, d)
}
func resourceTargetUpdate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
t, _ := convertResourceDataToTarget(d, md)
tgts := targets.NewClient(client)
if d.HasChange(targetNameKey) {
t.Name = d.Get(targetNameKey).(string)
}
if d.HasChange(targetDescriptionKey) {
t.Description = d.Get(targetDescriptionKey).(string)
}
t.Scope.Id = d.Get(targetScopeIDKey).(string)
t, apiErr, err := tgts.Update(
ctx,
t.Id,
0,
targets.WithScopeId(t.Scope.Id),
targets.WithAutomaticVersioning(),
targets.WithName(t.Name),
targets.WithDescription(t.Description))
if err != nil {
return err
}
if apiErr != nil {
return fmt.Errorf("%+v\n", apiErr.Message)
}
if d.HasChange(targetHostSetIDsKey) {
hostSetIds := []string{}
hostSets := d.Get(targetHostSetIDsKey).(*schema.Set).List()
for _, hostSet := range hostSets {
hostSetIds = append(hostSetIds, hostSet.(string))
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, targets.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, targets.WithDescription(descStr))
}
}
t, apiErr, err = tgts.SetHostSets(
var defaultPort *int
if d.HasChange(targetDefaultPortKey) {
opts = append(opts, targets.DefaultDefaultPort())
defaultPortVal, ok := d.GetOk(targetDefaultPortKey)
if ok {
defaultPortInt := defaultPortVal.(int)
if defaultPortInt < 0 {
return diag.Errorf(`"default_port" cannot be less than zero`)
}
defaultPort = &defaultPortInt
opts = append(opts, targets.WithDefaultPort(uint32(defaultPortInt)))
}
}
if len(opts) > 0 {
opts = append(opts, targets.WithAutomaticVersioning(true))
_, apiErr, err := tc.Update(
ctx,
t.Id,
t.Version,
hostSetIds,
targets.WithScopeId(t.Scope.Id))
if apiErr != nil || err != nil {
return fmt.Errorf("error updating hostSets on target:\n API Err: %+v\n Err: %+v\n", *apiErr, err)
d.Id(),
0,
opts...)
if err != nil {
return diag.Errorf("error calling update target: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating target: %s", apiErr.Message)
}
}
return convertTargetToResourceData(t, d)
}
func resourceTargetDelete(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
t, _ := convertResourceDataToTarget(d, md)
tgts := targets.NewClient(client)
_, apiErr, err := tgts.Delete(ctx, t.Id, targets.WithScopeId(t.Scope.Id))
if err != nil {
return fmt.Errorf("error deleting target: %s", err.Error())
if d.HasChange(NameKey) {
d.Set(NameKey, name)
}
if apiErr != nil {
return fmt.Errorf("error deleting target: %s", apiErr.Message)
if d.HasChange(DescriptionKey) {
d.Set(DescriptionKey, desc)
}
if d.HasChange(targetDefaultPortKey) {
d.Set(targetDefaultPortKey, defaultPort)
}
// 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))
}
}
_, apiErr, err := tc.SetHostSets(
ctx,
d.Id(),
0,
hostSetIds,
targets.WithAutomaticVersioning(true))
if err != nil {
return diag.Errorf("error updating host sets in target: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating host sets in target: %s", apiErr.Message)
}
d.Set(targetHostSetIdsKey, hostSetIds)
}
return nil
}
func resourceTargetDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
tc := targets.NewClient(md.client)
_, apiErr, err := tc.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling delete target: %s", err.Error())
}
if apiErr != nil {
return diag.Errorf("error deleting target: %s", apiErr.Message)
}
return nil
+45 -51
View File
@@ -1,14 +1,15 @@
package provider
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/hashicorp/boundary/api/targets"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
@@ -19,54 +20,58 @@ const (
var (
fooHostSet = `
resource "boundary_host_catalog" "foo" {
name = "test"
description = "test catalog"
scope_id = boundary_project.foo.id
type = "static"
name = "test"
description = "test catalog"
scope_id = boundary_scope.proj1.id
}
resource "boundary_host" "foo" {
name = "foo"
name = "foo"
host_catalog_id = boundary_host_catalog.foo.id
scope_id = boundary_project.foo.id
type = "static"
address = "10.0.0.1:80"
}
resource "boundary_host" "bar" {
name = "bar"
name = "bar"
host_catalog_id = boundary_host_catalog.foo.id
scope_id = boundary_project.foo.id
type = "static"
address = "10.0.0.1:80"
}
resource "boundary_host_set" "foo" {
name = "foo"
host_catalog_id = boundary_host_catalog.foo.id
host_ids = [
boundary_host.foo.id,
name = "foo"
type = "static"
host_catalog_id = boundary_host_catalog.foo.id
host_ids = [
boundary_host.foo.id,
boundary_host.bar.id,
]
}`
fooTarget = fmt.Sprintf(`
resource "boundary_target" "foo" {
name = "test"
name = "test"
description = "%s"
scope_id = boundary_project.foo.id
type = "tcp"
scope_id = boundary_scope.proj1.id
host_set_ids = [
boundary_host_set.foo.id
boundary_host_set.foo.id
]
default_port = 22
}`, fooTargetDescription)
fooTargetUpdate = fmt.Sprintf(`
resource "boundary_target" "foo" {
name = "test"
name = "test"
description = "%s"
scope_id = boundary_project.foo.id
type = "tcp"
scope_id = boundary_scope.proj1.id
host_set_ids = [
boundary_host_set.foo.id
boundary_host_set.foo.id
]
default_port = 80
}`, fooTargetDescriptionUpdate)
)
@@ -82,19 +87,21 @@ func TestAccTarget(t *testing.T) {
Steps: []resource.TestStep{
{
// test create
Config: testConfig(url, fooOrg, fooProject, fooHostSet, fooTarget),
Config: testConfig(url, fooOrg, firstProjectFoo, fooHostSet, fooTarget),
Check: resource.ComposeTestCheckFunc(
testAccCheckTargetResourceExists("boundary_target.foo"),
resource.TestCheckResourceAttr("boundary_target.foo", targetDescriptionKey, fooTargetDescription),
resource.TestCheckResourceAttr("boundary_target.foo", targetNameKey, "test"),
resource.TestCheckResourceAttr("boundary_target.foo", DescriptionKey, fooTargetDescription),
resource.TestCheckResourceAttr("boundary_target.foo", NameKey, "test"),
resource.TestCheckResourceAttr("boundary_target.foo", targetDefaultPortKey, "22"),
),
},
{
// test update
Config: testConfig(url, fooOrg, fooProject, fooHostSet, fooTargetUpdate),
Config: testConfig(url, fooOrg, firstProjectFoo, fooHostSet, fooTargetUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckTargetResourceExists("boundary_target.foo"),
resource.TestCheckResourceAttr("boundary_target.foo", targetDescriptionKey, fooTargetDescriptionUpdate),
resource.TestCheckResourceAttr("boundary_target.foo", DescriptionKey, fooTargetDescriptionUpdate),
resource.TestCheckResourceAttr("boundary_target.foo", targetDefaultPortKey, "80"),
),
},
},
@@ -131,24 +138,18 @@ func testAccCheckTargetResourceMembersSet(name string, hostSets []string) resour
// check boundary to ensure it matches
md := testProvider.Meta().(*metaData)
client := md.client.Clone()
tgtsClient := targets.NewClient(md.client)
projID, ok := rs.Primary.Attributes["scope_id"]
if ok {
client.SetScopeId(projID)
}
tgtsClient := targets.NewClient(client)
t, _, err := tgtsClient.Read(md.ctx, id)
t, _, err := tgtsClient.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading target %q: %v", id, err)
}
if len(t.HostSetIds) == 0 {
if len(t.Item.HostSetIds) == 0 {
return fmt.Errorf("no hostSets found on target")
}
for _, stateHostSet := range t.HostSetIds {
for _, stateHostSet := range t.Item.HostSetIds {
ok := false
for _, gotHostSetID := range hostSetIDs {
if gotHostSetID == stateHostSet {
@@ -177,14 +178,9 @@ func testAccCheckTargetResourceExists(name string) resource.TestCheckFunc {
}
md := testProvider.Meta().(*metaData)
projClient := md.client.Clone()
projID, ok := rs.Primary.Attributes["scope_id"]
if ok && projID != "" {
projClient.SetScopeId(projID)
}
tgts := targets.NewClient(projClient)
tgts := targets.NewClient(md.client)
if _, _, err := tgts.Read(md.ctx, id); err != nil {
if _, _, err := tgts.Read(context.Background(), id); err != nil {
return fmt.Errorf("Got an error when reading target %q: %v", id, err)
}
@@ -203,18 +199,16 @@ func testAccCheckTargetResourceDestroy(t *testing.T) resource.TestCheckFunc {
for _, rs := range s.RootModule().Resources {
switch rs.Type {
case "boundary_target":
projClient := md.client.Clone()
projID, ok := rs.Primary.Attributes["scope_id"]
if ok {
projClient.SetScopeId(projID)
}
tgts := targets.NewClient(projClient)
tgts := targets.NewClient(md.client)
id := rs.Primary.ID
_, apiErr, _ := tgts.Read(md.ctx, id)
if apiErr == nil || apiErr.Status != http.StatusForbidden && apiErr.Status != http.StatusNotFound {
return fmt.Errorf("Didn't get a 403 or 404 when reading destroyed resource %q: %v", id, apiErr)
_, apiErr, err := tgts.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Error when reading destroyed target %q: %v", id, err)
}
if apiErr == nil || apiErr.Status != http.StatusNotFound {
return fmt.Errorf("Didn't get a 404 when reading destroyed target %q: %v", id, apiErr)
}
default:
+116 -130
View File
@@ -1,186 +1,172 @@
package provider
import (
"fmt"
"context"
"github.com/hashicorp/boundary/api/scopes"
"github.com/hashicorp/boundary/api/users"
"github.com/hashicorp/terraform-plugin-sdk/helper/schema"
)
const (
userNameKey = "name"
userDescriptionKey = "description"
userScopeIDKey = "scope_id"
"github.com/hashicorp/terraform-plugin-sdk/v2/diag"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema"
)
func resourceUser() *schema.Resource {
return &schema.Resource{
Create: resourceUserCreate,
Read: resourceUserRead,
Update: resourceUserUpdate,
Delete: resourceUserDelete,
CreateContext: resourceUserCreate,
ReadContext: resourceUserRead,
UpdateContext: resourceUserUpdate,
DeleteContext: resourceUserDelete,
Schema: map[string]*schema.Schema{
userNameKey: {
NameKey: {
Type: schema.TypeString,
Optional: true,
},
userDescriptionKey: {
DescriptionKey: {
Type: schema.TypeString,
Optional: true,
},
userScopeIDKey: {
ScopeIdKey: {
Type: schema.TypeString,
Optional: true,
Required: true,
ForceNew: true,
Computed: true,
},
},
}
}
// convertUserToResourceData creates a ResourceData type from a User
func convertUserToResourceData(u *users.User, d *schema.ResourceData) error {
if u.Name != "" {
if err := d.Set(userNameKey, u.Name); err != nil {
return err
}
func setFromUserResponseMap(d *schema.ResourceData, raw map[string]interface{}) {
d.Set(NameKey, raw["name"])
d.Set(DescriptionKey, raw["description"])
d.Set(ScopeIdKey, raw["scope_id"])
d.SetId(raw["id"].(string))
}
func resourceUserCreate(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")
}
if u.Description != "" {
if err := d.Set(userDescriptionKey, u.Description); err != nil {
return err
}
opts := []users.Option{}
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
opts = append(opts, users.WithName(nameStr))
}
if u.Scope.Id != "" {
if err := d.Set(userScopeIDKey, u.Scope.Id); err != nil {
return err
}
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
opts = append(opts, users.WithDescription(descStr))
}
d.SetId(u.Id)
usrs := users.NewClient(md.client)
ucr, apiErr, err := usrs.Create(
ctx,
scopeId,
opts...)
if err != nil {
return diag.Errorf("error calling create user: %v", err)
}
if apiErr != nil {
return diag.Errorf("error creating user: %s", apiErr.Message)
}
if ucr == nil {
return diag.Errorf("user nil after create")
}
setFromUserResponseMap(d, ucr.GetResponseMap())
return nil
}
// convertResourceDataToUser returns a localy built User using the values provided in the ResourceData.
func convertResourceDataToUser(d *schema.ResourceData) *users.User {
u := &users.User{Scope: &scopes.ScopeInfo{}}
if descVal, ok := d.GetOk(userDescriptionKey); ok {
u.Description = descVal.(string)
}
if nameVal, ok := d.GetOk(userNameKey); ok {
u.Name = nameVal.(string)
}
if scopeIDVal, ok := d.GetOk(userScopeIDKey); ok {
u.Scope.Id = scopeIDVal.(string)
}
if d.Id() != "" {
u.Id = d.Id()
}
return u
}
func resourceUserCreate(d *schema.ResourceData, meta interface{}) error {
func resourceUserRead(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
usrs := users.NewClient(md.client)
u := convertResourceDataToUser(d)
usrs := users.NewClient(client)
u, apiErr, err := usrs.Create(
ctx,
users.WithName(u.Name),
users.WithDescription(u.Description),
users.WithScopeId(u.Scope.Id))
urr, apiErr, err := usrs.Read(ctx, d.Id())
if err != nil {
return fmt.Errorf("error calling new user: %s", err.Error())
return diag.Errorf("error calling read user: %v", err)
}
if apiErr != nil {
return fmt.Errorf("error creating user: %s", apiErr.Message)
return diag.Errorf("error reading user: %s", apiErr.Message)
}
if urr == nil {
return diag.Errorf("user nil after read")
}
d.SetId(u.Id)
setFromUserResponseMap(d, urr.GetResponseMap())
return convertUserToResourceData(u, d)
return nil
}
func resourceUserRead(d *schema.ResourceData, meta interface{}) error {
func resourceUserUpdate(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
usrs := users.NewClient(md.client)
u := convertResourceDataToUser(d)
usrs := users.NewClient(client)
opts := []users.Option{}
u, apiErr, err := usrs.Read(ctx, u.Id, users.WithScopeId(u.Scope.Id))
if err != nil {
return fmt.Errorf("error reading user: %s", err.Error())
}
if apiErr != nil {
return fmt.Errorf("error reading user: %s", apiErr.Message)
var name *string
if d.HasChange(NameKey) {
opts = append(opts, users.DefaultName())
nameVal, ok := d.GetOk(NameKey)
if ok {
nameStr := nameVal.(string)
name = &nameStr
opts = append(opts, users.WithName(nameStr))
}
}
return convertUserToResourceData(u, d)
}
func resourceUserUpdate(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
u := convertResourceDataToUser(d)
usrs := users.NewClient(client)
if d.HasChange(userNameKey) {
u.Name = d.Get(userNameKey).(string)
var desc *string
if d.HasChange(DescriptionKey) {
opts = append(opts, users.DefaultDescription())
descVal, ok := d.GetOk(DescriptionKey)
if ok {
descStr := descVal.(string)
desc = &descStr
opts = append(opts, users.WithDescription(descStr))
}
}
if d.HasChange(userDescriptionKey) {
u.Description = d.Get(userDescriptionKey).(string)
if len(opts) > 0 {
opts = append(opts, users.WithAutomaticVersioning(true))
_, apiErr, err := usrs.Update(
ctx,
d.Id(),
0,
opts...)
if err != nil {
return diag.Errorf("error calling update user: %v", err)
}
if apiErr != nil {
return diag.Errorf("error updating user: %s", apiErr.Message)
}
}
u.Scope.Id = d.Get(userScopeIDKey).(string)
u, apiErr, err := usrs.Update(
ctx,
u.Id,
0,
users.WithAutomaticVersioning(),
users.WithName(u.Name),
users.WithDescription(u.Description),
users.WithScopeId(u.Scope.Id))
if err != nil {
return err
if d.HasChange(NameKey) {
d.Set(NameKey, name)
}
if apiErr != nil {
return fmt.Errorf("error updating user: %s\n Invalid request fields: %v\n", apiErr.Message, apiErr.Details.RequestFields)
}
return convertUserToResourceData(u, d)
}
func resourceUserDelete(d *schema.ResourceData, meta interface{}) error {
md := meta.(*metaData)
client := md.client
ctx := md.ctx
u := convertResourceDataToUser(d)
usrs := users.NewClient(client)
_, apiErr, err := usrs.Delete(ctx, u.Id, users.WithScopeId(u.Scope.Id))
if err != nil {
return fmt.Errorf("error deleting user: %s", err.Error())
}
if apiErr != nil {
return fmt.Errorf("error deleting user: %s", apiErr.Message)
if d.HasChange(DescriptionKey) {
d.Set(DescriptionKey, desc)
}
return nil
}
func resourceUserDelete(ctx context.Context, d *schema.ResourceData, meta interface{}) diag.Diagnostics {
md := meta.(*metaData)
usrs := users.NewClient(md.client)
_, apiErr, err := usrs.Delete(ctx, d.Id())
if err != nil {
return diag.Errorf("error calling delete user: %v", err)
}
if apiErr != nil {
return diag.Errorf("error deleting user: %s", apiErr.Message)
}
return nil
+38 -39
View File
@@ -1,42 +1,46 @@
package provider
import (
"context"
"fmt"
"net/http"
"testing"
"github.com/hashicorp/boundary/api/users"
"github.com/hashicorp/boundary/testing/controller"
"github.com/hashicorp/terraform-plugin-sdk/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/terraform"
"github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource"
"github.com/hashicorp/terraform-plugin-sdk/v2/terraform"
)
const (
fooUserDescription = "bar"
fooUserDescriptionUpdate = "foo bar"
fooUserDescriptionUnset = ""
fooDescription = "bar"
fooDescriptionUpdate = "foo bar"
fooDescriptionUnset = ""
)
var (
orgUser = fmt.Sprintf(`
resource "boundary_user" "foo" {
name = "test"
resource "boundary_user" "org1" {
name = "test"
description = "%s"
scope_id = boundary_organization.foo.id
}`, fooUserDescription)
scope_id = boundary_scope.org1.id
}`, fooDescription)
orgUserUpdate = fmt.Sprintf(`
resource "boundary_user" "foo" {
name = "test"
resource "boundary_user" "org1" {
name = "test"
description = "%s"
scope_id = boundary_organization.foo.id
}`, fooUserDescriptionUpdate)
scope_id = boundary_scope.org1.id
}`, fooDescriptionUpdate)
)
// NOTE: this test also tests out the direct token auth mechanism.
func TestAccUser(t *testing.T) {
tc := controller.NewTestController(t, tcConfig...)
defer tc.Shutdown()
url := tc.ApiAddrs()[0]
token := tc.Token().Token
resource.Test(t, resource.TestCase{
Providers: testProviders,
@@ -44,20 +48,20 @@ func TestAccUser(t *testing.T) {
Steps: []resource.TestStep{
{
// test create
Config: testConfig(url, fooOrg, orgUser),
Config: testConfigWithToken(url, token, fooOrg, orgUser),
Check: resource.ComposeTestCheckFunc(
testAccCheckUserResourceExists("boundary_user.foo"),
resource.TestCheckResourceAttr("boundary_user.foo", userDescriptionKey, fooUserDescription),
resource.TestCheckResourceAttr("boundary_user.foo", userNameKey, "test"),
testAccCheckUserResourceExists("boundary_user.org1"),
resource.TestCheckResourceAttr("boundary_user.org1", DescriptionKey, fooDescription),
resource.TestCheckResourceAttr("boundary_user.org1", NameKey, "test"),
),
},
{
// test update description
Config: testConfig(url, fooOrg, orgUserUpdate),
Config: testConfigWithToken(url, token, fooOrg, orgUserUpdate),
Check: resource.ComposeTestCheckFunc(
testAccCheckUserResourceExists("boundary_user.foo"),
resource.TestCheckResourceAttr("boundary_user.foo", userDescriptionKey, fooUserDescriptionUpdate),
resource.TestCheckResourceAttr("boundary_user.foo", userNameKey, "test"),
testAccCheckUserResourceExists("boundary_user.org1"),
resource.TestCheckResourceAttr("boundary_user.org1", DescriptionKey, fooDescriptionUpdate),
resource.TestCheckResourceAttr("boundary_user.org1", NameKey, "test"),
),
},
},
@@ -77,17 +81,15 @@ func testAccCheckUserResourceExists(name string) resource.TestCheckFunc {
}
md := testProvider.Meta().(*metaData)
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
usrs := users.NewClient(projClient)
usrs := users.NewClient(md.client)
if _, _, err := usrs.Read(md.ctx, id); err != nil {
_, apiErr, err := usrs.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Got an error when reading user %q: %v", id, err)
}
if apiErr != nil {
return fmt.Errorf("Got an api error when reading user %q: %v", id, apiErr.Message)
}
return nil
}
@@ -105,17 +107,14 @@ func testAccCheckUserResourceDestroy(t *testing.T) resource.TestCheckFunc {
case "boundary_user":
id := rs.Primary.ID
projID, ok := rs.Primary.Attributes["scope_id"]
if !ok {
return fmt.Errorf("scope_id is not set")
}
projClient := md.client.Clone()
projClient.SetScopeId(projID)
usrs := users.NewClient(projClient)
usrs := users.NewClient(md.client)
_, apiErr, _ := usrs.Read(md.ctx, id)
if apiErr == nil || apiErr.Status != http.StatusNotFound && apiErr.Status != http.StatusForbidden {
return fmt.Errorf("Didn't get a 404 or 403 when reading destroyed user %q: %v", id, apiErr)
_, apiErr, err := usrs.Read(context.Background(), id)
if err != nil {
return fmt.Errorf("Error when reading destroyed user %q: %v", id, err)
}
if apiErr == nil || apiErr.Status != http.StatusNotFound {
return fmt.Errorf("Didn't get a 404 when reading destroyed user %q: %v", id, apiErr)
}
default:
+1 -1
View File
@@ -1,7 +1,7 @@
package main
import (
"github.com/hashicorp/terraform-plugin-sdk/plugin"
"github.com/hashicorp/terraform-plugin-sdk/v2/plugin"
"github.com/hashicorp/terraform-provider-boundary/internal/provider"
)
+8 -11
View File
@@ -16,11 +16,10 @@ Do not keep your authentication password in HCL for production environments, use
```hcl
provider "boundary" {
base_url = "http://127.0.0.1:9200"
default_scope = "global"
auth_method_id = "paum_1234567890" # changeme
auth_method_username = "myuser" # changeme
auth_method_password = "$uper$ecure9ass^^ord" # changeme
addr = "http://127.0.0.1:9200"
auth_method_id = "ampw_1234567890" # changeme
password_auth_method_username = "myuser" # changeme
password_auth_method_password = "$uper$ecure9ass^^ord" # changeme
}
```
@@ -28,14 +27,12 @@ provider "boundary" {
```hcl
provider "boundary" {
base_url = "http://127.0.0.1:9200"
default_scope = "global"
auth_method_id = "paum_1234567890" # changeme
auth_method_username = "myuser" # changeme
auth_method_password = "$uper$ecure9ass^^ord" # changeme
addr = "http://127.0.0.1:9200"
auth_method_id = "ampw_1234567890" # changeme
password_auth_method_username = "myuser" # changeme
password_auth_method_password = "$uper$ecure9ass^^ord" # changeme
}
variable "backend_team" {
type = set(string)
default = [