From 88946e1088454ed413848395b81499be21f5df8b Mon Sep 17 00:00:00 2001 From: Jeff Mitchell Date: Tue, 15 Sep 2020 19:56:43 -0400 Subject: [PATCH] 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 --- .vscode/settings.json | 5 + Makefile | 1 - README.md | 8 +- go.mod | 18 +- go.sum | 71 +-- internal/provider/const.go | 16 + internal/provider/pathorcontents.go | 43 ++ internal/provider/provider.go | 182 ++++--- internal/provider/provider_test.go | 96 +++- internal/provider/resource_account.go | 271 ++++++++++ internal/provider/resource_auth_method.go | 279 ++++++++++ internal/provider/resource_group.go | 357 ++++++------- internal/provider/resource_group_test.go | 244 ++++----- internal/provider/resource_host.go | 334 ++++++------ internal/provider/resource_host_catalog.go | 302 +++++------ .../provider/resource_host_catalog_test.go | 75 +-- internal/provider/resource_host_set.go | 358 ++++++------- internal/provider/resource_host_set_test.go | 51 +- internal/provider/resource_host_test.go | 93 ++-- internal/provider/resource_organization.go | 167 ------ .../provider/resource_organization_test.go | 114 ---- internal/provider/resource_project.go | 189 ------- internal/provider/resource_project_test.go | 253 --------- internal/provider/resource_role.go | 496 +++++++++--------- internal/provider/resource_role_test.go | 212 ++++---- internal/provider/resource_scope.go | 174 ++++++ internal/provider/resource_scope_test.go | 130 +++++ internal/provider/resource_target.go | 415 ++++++++------- internal/provider/resource_target_test.go | 96 ++-- internal/provider/resource_user.go | 246 ++++----- internal/provider/resource_user_test.go | 77 ++- main.go | 2 +- website/docs/index.html.markdown | 19 +- 33 files changed, 2772 insertions(+), 2622 deletions(-) create mode 100644 .vscode/settings.json create mode 100644 internal/provider/const.go create mode 100644 internal/provider/pathorcontents.go create mode 100644 internal/provider/resource_account.go create mode 100644 internal/provider/resource_auth_method.go delete mode 100644 internal/provider/resource_organization.go delete mode 100644 internal/provider/resource_organization_test.go delete mode 100644 internal/provider/resource_project.go delete mode 100644 internal/provider/resource_project_test.go create mode 100644 internal/provider/resource_scope.go create mode 100644 internal/provider/resource_scope_test.go diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..359dd236 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,5 @@ +{ + "go.testEnvVars": { + "TF_ACC": "1" + } +} \ No newline at end of file diff --git a/Makefile b/Makefile index 792ed5d9..e45414ef 100644 --- a/Makefile +++ b/Makefile @@ -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 diff --git a/README.md b/README.md index 367c91e1..bcc6ba19 100644 --- a/README.md +++ b/README.md @@ -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" { diff --git a/go.mod b/go.mod index 23848a0e..ca4840ea 100644 --- a/go.mod +++ b/go.mod @@ -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 ) diff --git a/go.sum b/go.sum index d0c1b11e..2e168aa9 100644 --- a/go.sum +++ b/go.sum @@ -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= diff --git a/internal/provider/const.go b/internal/provider/const.go new file mode 100644 index 00000000..a90b1734 --- /dev/null +++ b/internal/provider/const.go @@ -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" +) diff --git a/internal/provider/pathorcontents.go b/internal/provider/pathorcontents.go new file mode 100644 index 00000000..754b3119 --- /dev/null +++ b/internal/provider/pathorcontents.go @@ -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 +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 1cdaf773..db492e07 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -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 } } diff --git a/internal/provider/provider_test.go b/internal/provider/provider_test.go index 67423e34..e9b19522 100644 --- a/internal/provider/provider_test.go +++ b/internal/provider/provider_test.go @@ -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 = < 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 +} diff --git a/internal/provider/resource_auth_method.go b/internal/provider/resource_auth_method.go new file mode 100644 index 00000000..c00f543e --- /dev/null +++ b/internal/provider/resource_auth_method.go @@ -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 +} diff --git a/internal/provider/resource_group.go b/internal/provider/resource_group.go index a08d2368..6ac90829 100644 --- a/internal/provider/resource_group.go +++ b/internal/provider/resource_group.go @@ -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 diff --git a/internal/provider/resource_group_test.go b/internal/provider/resource_group_test.go index 060edb55..2d6cf3dc 100644 --- a/internal/provider/resource_group_test.go +++ b/internal/provider/resource_group_test.go @@ -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: diff --git a/internal/provider/resource_host.go b/internal/provider/resource_host.go index b3b5681c..263f255b 100644 --- a/internal/provider/resource_host.go +++ b/internal/provider/resource_host.go @@ -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 diff --git a/internal/provider/resource_host_catalog.go b/internal/provider/resource_host_catalog.go index 7a2c1758..56c27f18 100644 --- a/internal/provider/resource_host_catalog.go +++ b/internal/provider/resource_host_catalog.go @@ -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 diff --git a/internal/provider/resource_host_catalog_test.go b/internal/provider/resource_host_catalog_test.go index 417ce461..fe106d7b 100644 --- a/internal/provider/resource_host_catalog_test.go +++ b/internal/provider/resource_host_catalog_test.go @@ -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) - }) - } -} diff --git a/internal/provider/resource_host_set.go b/internal/provider/resource_host_set.go index c973cb91..230b78d2 100644 --- a/internal/provider/resource_host_set.go +++ b/internal/provider/resource_host_set.go @@ -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 diff --git a/internal/provider/resource_host_set_test.go b/internal/provider/resource_host_set_test.go index 2405a2f9..f5e4c12c 100644 --- a/internal/provider/resource_host_set_test.go +++ b/internal/provider/resource_host_set_test.go @@ -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: diff --git a/internal/provider/resource_host_test.go b/internal/provider/resource_host_test.go index 27ebb2ef..dba3ba1f 100644 --- a/internal/provider/resource_host_test.go +++ b/internal/provider/resource_host_test.go @@ -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: diff --git a/internal/provider/resource_organization.go b/internal/provider/resource_organization.go deleted file mode 100644 index d37733e5..00000000 --- a/internal/provider/resource_organization.go +++ /dev/null @@ -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 -} diff --git a/internal/provider/resource_organization_test.go b/internal/provider/resource_organization_test.go deleted file mode 100644 index a1be0000..00000000 --- a/internal/provider/resource_organization_test.go +++ /dev/null @@ -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 - } -} diff --git a/internal/provider/resource_project.go b/internal/provider/resource_project.go deleted file mode 100644 index e616e773..00000000 --- a/internal/provider/resource_project.go +++ /dev/null @@ -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 -} diff --git a/internal/provider/resource_project_test.go b/internal/provider/resource_project_test.go deleted file mode 100644 index 59837afa..00000000 --- a/internal/provider/resource_project_test.go +++ /dev/null @@ -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) - }) - } -} diff --git a/internal/provider/resource_role.go b/internal/provider/resource_role.go index fc50459a..e7234ccc 100644 --- a/internal/provider/resource_role.go +++ b/internal/provider/resource_role.go @@ -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 diff --git a/internal/provider/resource_role_test.go b/internal/provider/resource_role_test.go index 8e79c0d2..0ae96cb9 100644 --- a/internal/provider/resource_role_test.go +++ b/internal/provider/resource_role_test.go @@ -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: diff --git a/internal/provider/resource_scope.go b/internal/provider/resource_scope.go new file mode 100644 index 00000000..d9228f97 --- /dev/null +++ b/internal/provider/resource_scope.go @@ -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 +} diff --git a/internal/provider/resource_scope_test.go b/internal/provider/resource_scope_test.go new file mode 100644 index 00000000..a310f113 --- /dev/null +++ b/internal/provider/resource_scope_test.go @@ -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 + } +} diff --git a/internal/provider/resource_target.go b/internal/provider/resource_target.go index 428c4478..4442ba80 100644 --- a/internal/provider/resource_target.go +++ b/internal/provider/resource_target.go @@ -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 diff --git a/internal/provider/resource_target_test.go b/internal/provider/resource_target_test.go index 6643a168..ad81b296 100644 --- a/internal/provider/resource_target_test.go +++ b/internal/provider/resource_target_test.go @@ -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: diff --git a/internal/provider/resource_user.go b/internal/provider/resource_user.go index 98fb0e32..f0dddfb7 100644 --- a/internal/provider/resource_user.go +++ b/internal/provider/resource_user.go @@ -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 diff --git a/internal/provider/resource_user_test.go b/internal/provider/resource_user_test.go index 11a9a189..0b303249 100644 --- a/internal/provider/resource_user_test.go +++ b/internal/provider/resource_user_test.go @@ -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: diff --git a/main.go b/main.go index 48ce3856..0b10a3db 100644 --- a/main.go +++ b/main.go @@ -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" ) diff --git a/website/docs/index.html.markdown b/website/docs/index.html.markdown index c12d38af..121e5d6b 100644 --- a/website/docs/index.html.markdown +++ b/website/docs/index.html.markdown @@ -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 = [