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 = [