From eef405224c48290ef361a702179b97836908ee09 Mon Sep 17 00:00:00 2001 From: P-Cao Date: Thu, 20 Jul 2023 14:36:45 +0800 Subject: [PATCH] Implement Resource of SMB Share --- .gitignore | 1 + client/client.go | 2 +- docs/data-sources/smb_share.md | 133 ++++ docs/resources/smb_share.md | 176 +++++ .../resources/powerscale_smb_share/import.sh | 20 + .../powerscale_smb_share/provider.tf | 36 ++ .../powerscale_smb_share/resource.tf | 32 + go.mod | 4 + go.sum | 5 + powerscale/helper/helper.go | 26 +- powerscale/helper/resource_helper.go | 536 +++++++++++++++ powerscale/helper/resource_helper_test.go | 137 ++++ powerscale/models/smb_share.go | 211 ++++++ powerscale/provider/powerscale.env | 14 +- powerscale/provider/provider.go | 2 + powerscale/provider/provider_test.go | 19 +- powerscale/provider/smb_share_data_source.go | 503 +++++++++++++++ .../provider/smb_share_data_source_test.go | 140 ++++ powerscale/provider/smb_share_resource.go | 608 ++++++++++++++++++ .../provider/smb_share_resource_test.go | 189 ++++++ 20 files changed, 2779 insertions(+), 15 deletions(-) create mode 100644 docs/data-sources/smb_share.md create mode 100644 docs/resources/smb_share.md create mode 100644 examples/resources/powerscale_smb_share/import.sh create mode 100644 examples/resources/powerscale_smb_share/provider.tf create mode 100644 examples/resources/powerscale_smb_share/resource.tf create mode 100644 powerscale/helper/resource_helper.go create mode 100644 powerscale/helper/resource_helper_test.go create mode 100644 powerscale/models/smb_share.go create mode 100644 powerscale/provider/smb_share_data_source.go create mode 100644 powerscale/provider/smb_share_data_source_test.go create mode 100644 powerscale/provider/smb_share_resource.go create mode 100644 powerscale/provider/smb_share_resource_test.go diff --git a/.gitignore b/.gitignore index 51f48b1f..8ffba871 100644 --- a/.gitignore +++ b/.gitignore @@ -34,5 +34,6 @@ website/vendor # Keep windows files with windows line endings *.winfile eol=crlf *coverage.out* + *powerscale-go-client terraform-provider-powerscale \ No newline at end of file diff --git a/client/client.go b/client/client.go index 4e4ec626..ca467e3a 100644 --- a/client/client.go +++ b/client/client.go @@ -137,7 +137,7 @@ func NewOpenAPIClient(ctx context.Context, endpoint string, insecure bool, verbo OperationServers: map[string]powerscale.ServerConfigurations{}, } cfg.DefaultHeader = getHeaders() - fmt.Printf("config %+v header %+v", cfg, cfg.DefaultHeader) + fmt.Printf("config %+v header %+v\n", cfg, cfg.DefaultHeader) cfg.AddDefaultHeader("Authorization", "Basic "+basicAuthString) apiClient := powerscale.NewAPIClient(&cfg) return apiClient, nil diff --git a/docs/data-sources/smb_share.md b/docs/data-sources/smb_share.md new file mode 100644 index 00000000..2efaa424 --- /dev/null +++ b/docs/data-sources/smb_share.md @@ -0,0 +1,133 @@ +--- +# Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. +# +# Licensed under the Mozilla Public License Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://mozilla.org/MPL/2.0/ +# +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +title: "powerscale_smb_share data source" +linkTitle: "powerscale_smb_share" +page_title: "powerscale_smb_share Data Source - terraform-provider-powerscale" +subcategory: "" +description: |- + Data source for reading SMB Shares in PowerScale array. +--- + +# powerscale_smb_share (Data Source) + +Data source for reading SMB Shares in PowerScale array. + + + + +## Schema + +### Optional + +- `filter` (Block, Optional) (see [below for nested schema](#nestedblock--filter)) + +### Read-Only + +- `id` (String) Placeholder for acc testing +- `smb_shares` (Attributes List) List of smb shares (see [below for nested schema](#nestedatt--smb_shares)) + + +### Nested Schema for `filter` + +Optional: + +- `dir` (String) The direction of the sort. +- `limit` (Number) Return no more than this many results at once (see resume). +- `names` (Set of String) Names to filter smb shares. +- `offset` (Number) The position of the first item returned for a paginated query within the full result set. +- `resolve_names` (Boolean) If true, resolve group and user names in personas. +- `resume` (String) Continue returning results from previous call using this token (token should come from the previous call, resume cannot be used with other options). +- `scope` (String) If specified as "effective" or not specified, all fields are returned. If specified as "user", only fields with non-default values are shown. If specified as "default", the original values are returned. +- `sort` (String) The field that will be used for sorting. +- `zone` (String) Specifies which access zone to use. + + + +### Nested Schema for `smb_shares` + +Read-Only: + +- `access_based_enumeration` (Boolean) Only enumerate files and folders the requesting user has access to. +- `access_based_enumeration_root_only` (Boolean) Access-based enumeration on only the root directory of the share. +- `allow_delete_readonly` (Boolean) Allow deletion of read-only files in the share. +- `allow_execute_always` (Boolean) Allows users to execute files they have read rights for. +- `allow_variable_expansion` (Boolean) Allow automatic expansion of variables for home directories. +- `auto_create_directory` (Boolean) Automatically create home directories. +- `browsable` (Boolean) Share is visible in net view and the browse list. +- `ca_timeout` (Number) Persistent open timeout for the share. +- `ca_write_integrity` (String) Specify the level of write-integrity on continuously available shares. +- `change_notify` (String) Level of change notification alerts on the share. +- `continuously_available` (Boolean) Specify if persistent opens are allowed on the share. +- `create_permissions` (String) Create permissions for new files and directories in share. +- `csc_policy` (String) Client-side caching policy for the shares. +- `description` (String) Description for this SMB share. +- `directory_create_mask` (Number) Directory create mask bits. +- `directory_create_mode` (Number) Directory create mode bits. +- `file_create_mask` (Number) File create mask bits. +- `file_create_mode` (Number) File create mode bits. +- `file_filter_extensions` (List of String) Specifies the list of file extensions. +- `file_filter_type` (String) Specifies if filter list is for deny or allow. Default is deny. +- `file_filtering_enabled` (Boolean) Enables file filtering on this zone. +- `hide_dot_files` (Boolean) Hide files and directories that begin with a period '.'. +- `host_acl` (List of String) An ACL expressing which hosts are allowed access. A deny clause must be the final entry. +- `id` (String) Share ID. +- `impersonate_guest` (String) Specify the condition in which user access is done as the guest account. +- `impersonate_user` (String) User account to be used as guest account. +- `inheritable_path_acl` (Boolean) Set the inheritable ACL on the share path. +- `mangle_byte_start` (Number) Specifies the wchar_t starting point for automatic byte mangling. +- `mangle_map` (List of String) Character mangle map. +- `name` (String) Share name. +- `ntfs_acl_support` (Boolean) Support NTFS ACLs on files and directories. +- `oplocks` (Boolean) Support oplocks. +- `path` (String) Path of share within /ifs. +- `permissions` (Attributes List) Specifies an ordered list of permission modifications. (see [below for nested schema](#nestedatt--smb_shares--permissions)) +- `run_as_root` (Attributes List) Allow account to run as root. (see [below for nested schema](#nestedatt--smb_shares--run_as_root)) +- `smb3_encryption_enabled` (Boolean) Enables SMB3 encryption for the share. +- `sparse_file` (Boolean) Enables sparse file. +- `strict_ca_lockout` (Boolean) Specifies if persistent opens would do strict lockout on the share. +- `strict_flush` (Boolean) Handle SMB flush operations. +- `strict_locking` (Boolean) Specifies whether byte range locks contend against SMB I/O. +- `zid` (Number) Numeric ID of the access zone which contains this SMB share + + +### Nested Schema for `smb_shares.permissions` + +Read-Only: + +- `permission` (String) Specifies the file system rights that are allowed or denied. +- `permission_type` (String) Determines whether the permission is allowed or denied. +- `trustee` (Attributes) Specifies the persona of the file group. (see [below for nested schema](#nestedatt--smb_shares--permissions--trustee)) + + +### Nested Schema for `smb_shares.permissions.trustee` + +Read-Only: + +- `id` (String) Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'. +- `name` (String) Specifies the persona name, which must be combined with a type. +- `type` (String) Specifies the type of persona, which must be combined with a name. + + + + +### Nested Schema for `smb_shares.run_as_root` + +Read-Only: + +- `id` (String) Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'. +- `name` (String) Specifies the persona name, which must be combined with a type. +- `type` (String) Specifies the type of persona, which must be combined with a name. \ No newline at end of file diff --git a/docs/resources/smb_share.md b/docs/resources/smb_share.md new file mode 100644 index 00000000..692510ce --- /dev/null +++ b/docs/resources/smb_share.md @@ -0,0 +1,176 @@ +--- +# Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. +# +# Licensed under the Mozilla Public License Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://mozilla.org/MPL/2.0/ +# +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +title: "powerscale_smb_share resource" +linkTitle: "powerscale_smb_share" +page_title: "powerscale_smb_share Resource - terraform-provider-powerscale" +subcategory: "" +description: |- + Resource for managing SMB Shares in PowerScale array. +--- + +# powerscale_smb_share (Resource) + +Resource for managing SMB Shares in PowerScale array. + + +## Example Usage + +```terraform +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +resource "powerscale_smb_share" "share_example" { + auto_create_directory = true + name = "smb_share_example" + path = "/ifs/smb_share_example" + permissions = [ + { + permission = "full" + permission_type = "allow" + trustee = { + id = "SID:S-1-1-0", + name = "Everyone", + type = "wellknown" + } + } + ] +} +``` + + +## Schema + +### Required + +- `path` (String) Path of share within /ifs. +- `permissions` (Attributes List) Specifies an ordered list of permission modifications. (see [below for nested schema](#nestedatt--permissions)) + +### Optional + +- `access_based_enumeration` (Boolean) Only enumerate files and folders the requesting user has access to. +- `access_based_enumeration_root_only` (Boolean) Access-based enumeration on only the root directory of the share. +- `allow_delete_readonly` (Boolean) Allow deletion of read-only files in the share. +- `allow_execute_always` (Boolean) Allows users to execute files they have read rights for. +- `allow_variable_expansion` (Boolean) Allow automatic expansion of variables for home directories. +- `auto_create_directory` (Boolean) Automatically create home directories. +- `browsable` (Boolean) Share is visible in net view and the browse list. +- `ca_timeout` (Number) Persistent open timeout for the share. +- `ca_write_integrity` (String) Specify the level of write-integrity on continuously available shares. +- `change_notify` (String) Level of change notification alerts on the share. +- `create_path` (Boolean) Create path if does not exist. +- `create_permissions` (String) Create permissions for new files and directories in share. +- `csc_policy` (String) Client-side caching policy for the shares. +- `description` (String) Description for this SMB share. +- `directory_create_mask` (Number) Directory create mask bits. +- `directory_create_mode` (Number) Directory create mode bits. +- `file_create_mask` (Number) File create mask bits. +- `file_create_mode` (Number) File create mode bits. +- `file_filter_extensions` (List of String) Specifies the list of file extensions. +- `file_filter_type` (String) Specifies if filter list is for deny or allow. Default is deny. +- `file_filtering_enabled` (Boolean) Enables file filtering on this zone. +- `hide_dot_files` (Boolean) Hide files and directories that begin with a period '.'. +- `host_acl` (List of String) An ACL expressing which hosts are allowed access. A deny clause must be the final entry. +- `impersonate_guest` (String) Specify the condition in which user access is done as the guest account. +- `impersonate_user` (String) User account to be used as guest account. +- `inheritable_path_acl` (Boolean) Set the inheritable ACL on the share path. +- `mangle_byte_start` (Number) Specifies the wchar_t starting point for automatic byte mangling. +- `mangle_map` (List of String) Character mangle map. +- `name` (String) Share name. +- `ntfs_acl_support` (Boolean) Support NTFS ACLs on files and directories. +- `oplocks` (Boolean) Support oplocks. +- `run_as_root` (Attributes List) Allow account to run as root. (see [below for nested schema](#nestedatt--run_as_root)) +- `smb3_encryption_enabled` (Boolean) Enables SMB3 encryption for the share. +- `sparse_file` (Boolean) Enables sparse file. +- `strict_ca_lockout` (Boolean) Specifies if persistent opens would do strict lockout on the share. +- `strict_flush` (Boolean) Handle SMB flush operations. +- `strict_locking` (Boolean) Specifies whether byte range locks contend against SMB I/O. +- `zid` (Number) Numeric ID of the access zone which contains this SMB share. +- `zone` (String) Name of the access zone to which to move this SMB share. + +### Read-Only + +- `continuously_available` (Boolean) Specify if persistent opens are allowed on the share. +- `id` (String) The ID of the smb share. + + +### Nested Schema for `permissions` + +Required: + +- `permission` (String) Specifies the file system rights that are allowed or denied. +- `permission_type` (String) Determines whether the permission is allowed or denied. +- `trustee` (Attributes) Specifies the persona of the file group. (see [below for nested schema](#nestedatt--permissions--trustee)) + + +### Nested Schema for `permissions.trustee` + +Optional: + +- `id` (String) Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'. +- `name` (String) Specifies the persona name, which must be combined with a type. +- `type` (String) Specifies the type of persona, which must be combined with a name. + + + + +### Nested Schema for `run_as_root` + +Optional: + +- `id` (String) Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'. +- `name` (String) Specifies the persona name, which must be combined with a type. +- `type` (String) Specifies the type of persona, which must be combined with a name. + +## Import + +Import is supported using the following syntax: + +```shell +# Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +# Licensed under the Mozilla Public License Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://mozilla.org/MPL/2.0/ + + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The command is +# terraform import powermax_host.example_share +# Example: +terraform import powerscale_smb_share.example_share example_share +# after running this command, populate the name field in the config file to start managing this resource +``` \ No newline at end of file diff --git a/examples/resources/powerscale_smb_share/import.sh b/examples/resources/powerscale_smb_share/import.sh new file mode 100644 index 00000000..c94f7dc7 --- /dev/null +++ b/examples/resources/powerscale_smb_share/import.sh @@ -0,0 +1,20 @@ +# Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +# Licensed under the Mozilla Public License Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://mozilla.org/MPL/2.0/ + + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The command is +# terraform import powermax_host.example_share +# Example: +terraform import powerscale_smb_share.example_share example_share +# after running this command, populate the name field in the config file to start managing this resource diff --git a/examples/resources/powerscale_smb_share/provider.tf b/examples/resources/powerscale_smb_share/provider.tf new file mode 100644 index 00000000..3a44c97f --- /dev/null +++ b/examples/resources/powerscale_smb_share/provider.tf @@ -0,0 +1,36 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +terraform { + required_providers { + powerscale = { + source = "registry.terraform.io/dell/powerscale" + } + } +} + +provider "powerscale" { + username = var.username + password = var.password + endpoint = var.endpoint + insecure = var.insecure + group = var.group + volumes_path = var.volumes_path + volumes_path_permissions = var.volumes_path_permissions + ignore_unresolvable_hosts = var.ignore_unresolvable_hosts + auth_type = var.auth_type + verbose_logging = var.verbose_logging +} \ No newline at end of file diff --git a/examples/resources/powerscale_smb_share/resource.tf b/examples/resources/powerscale_smb_share/resource.tf new file mode 100644 index 00000000..264c6bca --- /dev/null +++ b/examples/resources/powerscale_smb_share/resource.tf @@ -0,0 +1,32 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ +resource "powerscale_smb_share" "share_example" { + auto_create_directory = true + name = "smb_share_example" + path = "/ifs/smb_share_example" + permissions = [ + { + permission = "full" + permission_type = "allow" + trustee = { + id = "SID:S-1-1-0", + name = "Everyone", + type = "wellknown" + } + } + ] +} \ No newline at end of file diff --git a/go.mod b/go.mod index ea0e071c..defe90d3 100644 --- a/go.mod +++ b/go.mod @@ -13,6 +13,7 @@ require ( github.com/hashicorp/terraform-plugin-log v0.9.0 github.com/hashicorp/terraform-plugin-testing v1.3.0 github.com/joho/godotenv v1.5.1 + github.com/stretchr/testify v1.8.2 ) require ( @@ -35,6 +36,7 @@ require ( github.com/bgentry/speakeasy v0.1.0 // indirect github.com/bytedance/mockey v1.2.4 github.com/cloudflare/circl v1.3.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect github.com/fatih/color v1.13.0 // indirect github.com/golang/protobuf v1.5.3 // indirect github.com/google/go-cmp v0.5.9 // indirect @@ -70,6 +72,7 @@ require ( github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect github.com/posener/complete v1.2.3 // indirect github.com/russross/blackfriday v1.6.0 // indirect github.com/shopspring/decimal v1.3.1 // indirect @@ -91,6 +94,7 @@ require ( google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect google.golang.org/grpc v1.56.0 // indirect google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) replace dell/powerscale-go-client => ./powerscale-go-client diff --git a/go.sum b/go.sum index 88a9f4b0..41922234 100644 --- a/go.sum +++ b/go.sum @@ -177,13 +177,18 @@ github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/vmihailenco/msgpack v3.3.3+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= github.com/vmihailenco/msgpack v4.0.4+incompatible h1:dSLoQfGFAo3F6OoNhwUmLwVgaUXK79GlxNBwueZn0xI= github.com/vmihailenco/msgpack v4.0.4+incompatible/go.mod h1:fy3FlTQTDXWkZ7Bh6AcGMlsjHatGryHQYUTf1ShIgkk= diff --git a/powerscale/helper/helper.go b/powerscale/helper/helper.go index 0413e762..6579f2af 100644 --- a/powerscale/helper/helper.go +++ b/powerscale/helper/helper.go @@ -57,10 +57,10 @@ func CopyFields(ctx context.Context, source, destination interface{}) error { // Get the type of the destination struct //destinationType := destinationValue.Elem().Type() for i := 0; i < sourceValue.NumField(); i++ { - sourceFieldName := sourceValue.Type().Field(i).Name + sourceFieldTag := getFieldJSONTag(sourceValue, i) tflog.Debug(ctx, "Converting source field", map[string]interface{}{ - "sourceFieldName": sourceFieldName, + "sourceFieldTag": sourceFieldTag, "sourceFieldKind": sourceValue.Field(i).Kind().String(), }) @@ -70,13 +70,13 @@ func CopyFields(ctx context.Context, source, destination interface{}) error { } if !sourceField.IsValid() { tflog.Error(ctx, "source field is not valid", map[string]interface{}{ - "sourceFieldName": sourceFieldName, - "sourceField": sourceField, + "sourceFieldTag": sourceFieldTag, + "sourceField": sourceField, }) continue } - destinationField := destinationValue.Elem().FieldByName(sourceFieldName) + destinationField := getFieldByTfTag(destinationValue.Elem(), sourceFieldTag) if destinationField.IsValid() && destinationField.CanSet() { tflog.Debug(ctx, "debugging source field", map[string]interface{}{ @@ -239,3 +239,19 @@ func GetErrorString(err error, errStr string) string { } return msgStr } + +func getFieldJSONTag(sourceValue reflect.Value, i int) string { + sourceFieldTag := sourceValue.Type().Field(i).Tag.Get("json") + sourceFieldTag = strings.TrimSuffix(sourceFieldTag, ",omitempty") + return sourceFieldTag +} + +func getFieldByTfTag(destinationValue reflect.Value, tagValue string) reflect.Value { + for j := 0; j < destinationValue.NumField(); j++ { + field := destinationValue.Type().Field(j) + if field.Tag.Get("tfsdk") == tagValue { + return destinationValue.Field(j) + } + } + return reflect.Value{} +} diff --git a/powerscale/helper/resource_helper.go b/powerscale/helper/resource_helper.go new file mode 100644 index 00000000..bdd334a0 --- /dev/null +++ b/powerscale/helper/resource_helper.go @@ -0,0 +1,536 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helper + +import ( + "context" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/tfsdk" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "math/big" + "reflect" + "strings" +) + +// CopyFieldsToNonNestedModel copy OpenAPI struct source to destination of struct with terraform types. +// use this function when model struct contains only types.List/Object +func CopyFieldsToNonNestedModel(ctx context.Context, source, destination interface{}) error { + tflog.Debug(ctx, "Copy fields", map[string]interface{}{ + "source": source, + "destination": destination, + }) + var err error + sourceValue := reflect.ValueOf(source) + destinationValue := reflect.ValueOf(destination) + + // Check if destination is a pointer to a struct + if destinationValue.Kind() != reflect.Ptr || destinationValue.Elem().Kind() != reflect.Struct { + return fmt.Errorf("destination is not a pointer to a struct") + } + + // if source is a pointer, use the Elem() method to get the value that the pointer points to + if sourceValue.Kind() == reflect.Ptr { + sourceValue = sourceValue.Elem() + } + + if sourceValue.Kind() != reflect.Struct { + return fmt.Errorf("source is not a struct") + } + + // Get the type of the destination struct + //destinationType := destinationValue.Elem().Type() + for i := 0; i < sourceValue.NumField(); i++ { + sourceFieldTag := getFieldJSONTag(sourceValue, i) + + tflog.Debug(ctx, "Converting source field", map[string]interface{}{ + "sourceFieldTag": sourceFieldTag, + "sourceFieldKind": sourceValue.Field(i).Kind().String(), + }) + + sourceField := sourceValue.Field(i) + if sourceField.Kind() == reflect.Ptr { + sourceField = sourceField.Elem() + } + if !sourceField.IsValid() { + tflog.Error(ctx, "source field is not valid", map[string]interface{}{ + "sourceFieldTag": sourceFieldTag, + "sourceField": sourceField, + }) + continue + } + + destinationField := getFieldByTfTag(destinationValue.Elem(), sourceFieldTag) + if destinationField.IsValid() && destinationField.CanSet() { + tflog.Debug(ctx, "debugging source field", map[string]interface{}{ + "sourceField Interface": sourceField.Interface(), + }) + // Convert the source value to the type of the destination field dynamically + var destinationFieldValue attr.Value + + switch sourceField.Kind() { + case reflect.String: + destinationFieldValue = types.StringValue(sourceField.String()) + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + destinationFieldValue = types.Int64Value(sourceField.Int()) + case reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64: + destinationFieldValue = types.Int64Value(sourceField.Int()) + case reflect.Float32, reflect.Float64: + //destinationFieldValue = types.Float64Value(sourceField.Float()) + destinationFieldValue = types.NumberValue(big.NewFloat(sourceField.Float())) + case reflect.Bool: + destinationFieldValue = types.BoolValue(sourceField.Bool()) + case reflect.Array, reflect.Slice: + destinationFieldValue, err = getSliceAttrValue(ctx, sourceField.Interface()) + if err != nil { + return err + } + case reflect.Struct: + destinationFieldValue, err = getStructValue(ctx, sourceField.Interface()) + if err != nil { + return err + } + default: + tflog.Error(ctx, "unsupported source field type", map[string]interface{}{ + "sourceField": sourceField, + }) + continue + } + if destinationField.Type() == reflect.TypeOf(destinationFieldValue) { + destinationField.Set(reflect.ValueOf(destinationFieldValue)) + } + } + } + + return nil +} + +func getStructValue(ctx context.Context, structObj interface{}) (basetypes.ObjectValue, error) { + elem := reflect.ValueOf(structObj) + attrType, err := getStructAttrType(ctx, structObj) + if err != nil { + return types.ObjectNull(nil), err + } + valueMap := make(map[string]attr.Value) + // iterate the listObject + for fieldIndex := 0; fieldIndex < elem.NumField(); fieldIndex++ { + tag := elem.Type().Field(fieldIndex).Tag.Get("json") + tag = strings.TrimSuffix(tag, ",omitempty") + elemFieldVal := elem.Field(fieldIndex) + elemFieldType := elemFieldVal.Type() + if elemFieldType.Kind() == reflect.Ptr { + elemFieldVal = elemFieldVal.Elem() + elemFieldType = elemFieldType.Elem() + } + switch elemFieldType.Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + valueMap[tag] = types.Int64Value(elemFieldVal.Int()) + case reflect.String: + valueMap[tag] = types.StringValue(elemFieldVal.String()) + case reflect.Bool: + valueMap[tag] = types.BoolValue(elemFieldVal.Bool()) + case reflect.Struct: + valueMap[tag], err = getStructValue(ctx, elemFieldVal.Interface()) + if err != nil { + return types.ObjectNull(nil), err + } + case reflect.Array, reflect.Slice: + valueMap[tag], err = getSliceAttrValue(ctx, elemFieldVal.Interface()) + if err != nil { + return types.ObjectNull(nil), err + } + } + } + object, _ := types.ObjectValue(attrType, valueMap) + return object, nil +} + +func getStructAttrType(ctx context.Context, structObj interface{}) (map[string]attr.Type, error) { + attrTypeMap := make(map[string]attr.Type) + structElemVal := reflect.ValueOf(structObj) + structElemType := structElemVal.Type() + if reflect.ValueOf(structObj).Kind() == reflect.Ptr { + structElemVal = reflect.ValueOf(structObj).Elem() + structElemType = structElemVal.Type() + } + if structElemType.Kind() != reflect.Struct { + return attrTypeMap, fmt.Errorf("source is not a struct") + } + for fieldIndex := 0; fieldIndex < structElemType.NumField(); fieldIndex++ { + structFieldVal := structElemVal.Field(fieldIndex) + structField := structElemType.Field(fieldIndex) + tag := structField.Tag.Get("json") + tag = strings.TrimSuffix(tag, ",omitempty") + structFieldType := structField.Type.Kind() + if structField.Type.Kind() == reflect.Ptr { + structFieldType = structField.Type.Elem().Kind() + } + switch structFieldType { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + attrTypeMap[tag] = types.Int64Type + case reflect.String: + attrTypeMap[tag] = types.StringType + case reflect.Float32, reflect.Float64: + attrTypeMap[tag] = types.NumberType + case reflect.Struct: + structAttrType, err := getStructAttrType(ctx, structFieldVal.Interface()) + if err != nil { + return nil, err + } + attrTypeMap[tag] = types.ObjectType{AttrTypes: structAttrType} + case reflect.Array, reflect.Slice: + structAttrType, err := getSliceAttrType(ctx, structFieldVal.Interface()) + if err != nil { + return nil, err + } + attrTypeMap[tag] = structAttrType + } + } + return attrTypeMap, nil +} + +func getSliceAttrValue(ctx context.Context, sliceObject interface{}) (attr.Value, error) { + sliceValue := reflect.ValueOf(sliceObject) + switch sliceValue.Type().Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + listValue, _ := types.ListValueFrom(ctx, types.Int64Type, sliceObject) + return listValue, nil + case reflect.String: + listValue, _ := types.ListValueFrom(ctx, types.StringType, sliceObject) + return listValue, nil + case reflect.Float32, reflect.Float64: + listValue, _ := types.ListValueFrom(ctx, types.NumberType, sliceObject) + return listValue, nil + case reflect.Bool: + listValue, _ := types.ListValueFrom(ctx, types.BoolType, sliceObject) + return listValue, nil + case reflect.Struct: + var values []attr.Value + sliceElemType, err := getStructAttrType(ctx, reflect.New(sliceValue.Type().Elem()).Elem().Interface()) + if err != nil { + return nil, err + } + for index := 0; index < sliceValue.Len(); index++ { + sliceElemValue, err := getStructValue(ctx, sliceValue.Index(index).Interface()) + if err != nil { + return nil, err + } + values = append(values, sliceElemValue) + } + if len(values) == 0 { + return types.ListNull(types.ObjectType{AttrTypes: sliceElemType}), nil + } + returnListValue, _ := types.ListValue(types.ObjectType{AttrTypes: sliceElemType}, values) + return returnListValue, nil + case reflect.Array, reflect.Slice: + var values []attr.Value + sliceAttrType, err := getSliceAttrType(ctx, reflect.MakeSlice(sliceValue.Type().Elem(), 0, 0).Interface()) + if err != nil { + return nil, err + } + for index := 0; index < sliceValue.Len(); index++ { + sliceElemValue, err := getSliceAttrValue(ctx, sliceValue.Index(index).Interface()) + if err != nil { + return nil, err + } + values = append(values, sliceElemValue) + } + if len(values) == 0 { + return types.ListNull(types.ListType{ElemType: sliceAttrType}), nil + } + returnListValue, _ := types.ListValue(types.ListType{ElemType: sliceAttrType}, values) + return returnListValue, nil + default: + return nil, fmt.Errorf("unknown type") + } +} + +func getSliceAttrType(ctx context.Context, sliceObject interface{}) (attr.Type, error) { + sliceValue := reflect.ValueOf(sliceObject) + switch sliceValue.Type().Elem().Kind() { + case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64: + return types.ListType{ElemType: types.Int64Type}, nil + case reflect.String: + return types.ListType{ElemType: types.StringType}, nil + case reflect.Float32, reflect.Float64: + return types.ListType{ElemType: types.NumberType}, nil + case reflect.Bool: + return types.ListType{ElemType: types.BoolType}, nil + case reflect.Struct: + structAttrType, err := getStructAttrType(ctx, reflect.New(sliceValue.Type().Elem()).Elem().Interface()) + if err != nil { + return nil, err + } + return types.ListType{ElemType: types.ObjectType{AttrTypes: structAttrType}}, nil + case reflect.Array, reflect.Slice: + sliceAttrType, err := getSliceAttrType(ctx, reflect.MakeSlice(sliceValue.Type().Elem(), 0, 0).Interface()) + if err != nil { + return nil, err + } + return types.ListType{ElemType: sliceAttrType}, nil + default: + return nil, fmt.Errorf("unknown type") + } +} + +// ReadFromState read from model to openapi struct, model should not contain nested struct +func ReadFromState(ctx context.Context, source, destination interface{}) error { + sourceValue := reflect.ValueOf(source) + destinationValue := reflect.ValueOf(destination) + if destinationValue.Kind() != reflect.Ptr || destinationValue.Elem().Kind() != reflect.Struct { + return fmt.Errorf("destination is not a pointer to a struct") + } + if sourceValue.Kind() == reflect.Ptr { + sourceValue = sourceValue.Elem() + } + if sourceValue.Kind() != reflect.Struct { + return fmt.Errorf("source is not a struct") + } + for i := 0; i < sourceValue.NumField(); i++ { + sourceFieldTag := sourceValue.Type().Field(i).Tag.Get("tfsdk") + destinationField, err := getFieldByJSONTag(destinationValue.Elem().Addr().Interface(), sourceFieldTag) + if err != nil { + // Not found, skip the field + continue + } + if destinationField.IsValid() && destinationField.CanSet() { + switch sourceValue.Field(i).Interface().(type) { + case basetypes.StringValue: + stringVal := sourceValue.Field(i).Interface().(basetypes.StringValue) + if stringVal.IsNull() || stringVal.IsUnknown() { + continue + } + targetValue := stringVal.ValueString() + if destinationField.Kind() == reflect.Ptr && destinationField.Type().Elem().Kind() == reflect.String { + destinationField.Set(reflect.ValueOf(&targetValue)) + } + if destinationField.Type().Kind() == reflect.String { + destinationField.Set(reflect.ValueOf(targetValue)) + } + case basetypes.Int64Value: + intVal := sourceValue.Field(i).Interface().(basetypes.Int64Value) + if intVal.IsNull() || intVal.IsUnknown() { + continue + } + if destinationField.Kind() == reflect.Int64 { + destinationField.Set(reflect.ValueOf(intVal.ValueInt64())) + } + if destinationField.Kind() == reflect.Ptr && destinationField.Type().Elem().Kind() == reflect.Int64 { + destinationField.Set(reflect.ValueOf(sourceValue.Field(i).Interface().(basetypes.Int64Value).ValueInt64Pointer())) + } + if destinationField.Kind() == reflect.Int32 { + destinationField.Set(reflect.ValueOf(int32(intVal.ValueInt64()))) + } + if destinationField.Kind() == reflect.Ptr && destinationField.Type().Elem().Kind() == reflect.Int32 { + val := int32(intVal.ValueInt64()) + destinationField.Set(reflect.ValueOf(&val)) + } + case basetypes.BoolValue: + boolVal := sourceValue.Field(i).Interface().(basetypes.BoolValue) + if boolVal.IsNull() || boolVal.IsUnknown() { + continue + } + if destinationField.Kind() == reflect.Ptr { + destinationField.Set(reflect.ValueOf(boolVal.ValueBoolPointer())) + } else { + destinationField.Set(reflect.ValueOf(boolVal.ValueBool())) + } + //case basetypes.ListValue: + // // TODO: Now only support []string as ListValue, need []*string and other primitive types + // var targetStringList []string + // tfsdk.ValueAs(ctx, sourceValue.Field(i).Interface().(basetypes.ListValue), &targetStringList) + // destinationField.Set(reflect.ValueOf(targetStringList)) + case basetypes.ObjectValue: + objVal := sourceValue.Field(i).Interface().(basetypes.ObjectValue) + if objVal.IsNull() || objVal.IsUnknown() { + continue + } + err := assignObjectToField(ctx, objVal, destinationField.Addr().Interface()) + if err != nil { + return err + } + case basetypes.ListValue: + listVal := sourceValue.Field(i).Interface().(basetypes.ListValue) + if listVal.IsNull() || listVal.IsUnknown() { + continue + } + err := assignListToField(ctx, listVal, destinationField.Addr().Interface()) + if err != nil { + return err + } + } + } + } + return nil +} + +func assignObjectToField(ctx context.Context, source basetypes.ObjectValue, destination interface{}) error { + destElemVal := reflect.ValueOf(destination).Elem() + destElemType := destElemVal.Type() + targetObject := reflect.New(destElemType).Elem() + // if target is pointer to a pointer + if destElemVal.Kind() == reflect.Ptr { + destElemVal = reflect.ValueOf(destination).Elem().Elem() + destElemType = destElemVal.Type() + targetObject = reflect.New(destElemType).Elem() + } + attrMap := source.Attributes() + for key, val := range attrMap { + destinationField, err := getFieldByJSONTag(targetObject.Addr().Interface(), key) + if err != nil { + // skip current field + continue + } + if destinationField.IsValid() && destinationField.CanSet() { + switch val.Type(ctx) { + case basetypes.StringType{}: + stringVal := val.(basetypes.StringValue) + if stringVal.IsNull() || stringVal.IsUnknown() { + continue + } + targetValue := stringVal.ValueString() + if destinationField.Kind() == reflect.Ptr && destinationField.Type().Elem().Kind() == reflect.String { + destinationField.Set(reflect.ValueOf(&targetValue)) + } + if destinationField.Type().Kind() == reflect.String { + destinationField.Set(reflect.ValueOf(targetValue)) + } + case basetypes.Int64Type{}: + intVal := val.(basetypes.Int64Value) + if intVal.IsNull() || intVal.IsUnknown() { + continue + } + if destinationField.Kind() == reflect.Int64 { + destinationField.Set(reflect.ValueOf(intVal.ValueInt64())) + } + if destinationField.Kind() == reflect.Ptr && destinationField.Type().Elem().Kind() == reflect.Int64 { + destinationField.Set(reflect.ValueOf(intVal.ValueInt64Pointer())) + } + if destinationField.Kind() == reflect.Int32 { + destinationField.Set(reflect.ValueOf(int32(intVal.ValueInt64()))) + } + if destinationField.Kind() == reflect.Ptr && destinationField.Type().Elem().Kind() == reflect.Int32 { + val := int32(intVal.ValueInt64()) + destinationField.Set(reflect.ValueOf(&val)) + } + case basetypes.BoolType{}: + boolVal := val.(basetypes.BoolValue) + if boolVal.IsNull() || boolVal.IsUnknown() { + continue + } + if destinationField.Kind() == reflect.Ptr { + destinationField.Set(reflect.ValueOf(boolVal.ValueBoolPointer())) + } else { + destinationField.Set(reflect.ValueOf(boolVal.ValueBool())) + } + default: + typeString := val.Type(ctx).String() + if strings.HasPrefix(typeString, "types.ObjectType") { + objVal := val.(basetypes.ObjectValue) + if objVal.IsNull() || objVal.IsUnknown() { + continue + } + err := assignObjectToField(ctx, objVal, destinationField.Addr().Interface()) + if err != nil { + return err + } + } else if strings.HasPrefix(typeString, "types.ListType") { + listVal := val.(basetypes.ListValue) + if listVal.IsNull() || listVal.IsUnknown() { + continue + } + err := assignListToField(ctx, listVal, destinationField.Addr().Interface()) + if err != nil { + return err + } + } + } + } + } + destElemVal.Set(targetObject) + return nil +} + +func assignListToField(ctx context.Context, source basetypes.ListValue, destination interface{}) error { + destVal := reflect.ValueOf(destination).Elem() + // type of element of slice + destType := destVal.Type() + // if target is pointer to a pointer + if destVal.Kind() == reflect.Ptr { + destVal = destVal.Elem() + destType = destVal.Type() + } + listLen := len(source.Elements()) + + listElemType := source.ElementType(ctx) + switch listElemType { + case basetypes.StringType{}: + tfsdk.ValueAs(ctx, source, destination) + case basetypes.Int64Type{}: + tfsdk.ValueAs(ctx, source, destination) + case basetypes.BoolType{}: + tfsdk.ValueAs(ctx, source, destination) + default: + targetList := reflect.MakeSlice(destType, listLen, listLen) + typeString := listElemType.String() + for i, listElem := range source.Elements() { + if strings.HasPrefix(typeString, "types.ListType") { + listVal := listElem.(basetypes.ListValue) + if listVal.IsNull() || listVal.IsUnknown() { + continue + } + err := assignListToField(ctx, listVal, targetList.Index(i).Addr().Interface()) + if err != nil { + return err + } + } else if strings.HasPrefix(typeString, "types.ObjectType") { + objVal := listElem.(basetypes.ObjectValue) + if objVal.IsNull() || objVal.IsUnknown() { + continue + } + err := assignObjectToField(ctx, objVal, targetList.Index(i).Addr().Interface()) + if err != nil { + return err + } + } + } + destVal.Set(targetList) + } + return nil +} + +// getFieldByJSONTag get field by tag, input destination is pointer +func getFieldByJSONTag(destination interface{}, tag string) (reflect.Value, error) { + destElemVal := reflect.ValueOf(destination).Elem() + destElemType := destElemVal.Type() + + for i := 0; i < destElemType.NumField(); i++ { + field := destElemType.Field(i) + jsonTag := field.Tag.Get("json") + if strings.Contains(jsonTag, ",") { + jsonTag = strings.TrimSuffix(jsonTag, ",omitempty") + } + if jsonTag == tag { + return destElemVal.Field(i), nil + } + } + + return reflect.Value{}, fmt.Errorf("field with tag %s not found in destination", tag) +} diff --git a/powerscale/helper/resource_helper_test.go b/powerscale/helper/resource_helper_test.go new file mode 100644 index 00000000..76ca6437 --- /dev/null +++ b/powerscale/helper/resource_helper_test.go @@ -0,0 +1,137 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package helper + +import ( + "context" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/stretchr/testify/assert" + "testing" +) + +type OpenapiStruct struct { + BoolPtr *bool `json:"bool_ptr,omitempty"` + BoolVal bool `json:"bool_val,omitempty"` + StringPtr *string `json:"string_ptr,omitempty"` + StringVal string `json:"string_val,omitempty"` + Int64Ptr *int64 `json:"int_64_ptr,omitempty"` + Int64Val int64 `json:"int_64_val,omitempty"` + NestedSlice []OpenapiChildStruct `json:"nested_slice,omitempty"` + NestedObject OpenapiChildSingleStruct `json:"nested_object,omitempty"` +} + +type OpenapiChildStruct struct { + Str string `json:"str,omitempty"` +} + +type OpenapiChildSingleStruct struct { + Strings []string `json:"strings,omitempty"` + Integers []int64 `json:"integers,omitempty"` + Structs []OpenapiChildStruct `json:"structs,omitempty"` + SingleStruct OpenapiGrandChildSingleStruct `json:"single_struct,omitempty"` +} +type OpenapiGrandChildSingleStruct struct { + String string `json:"str,omitempty"` +} + +var fakeBool = true +var fakeString = "fake_string" +var fakeInt = int64(32) + +var openapiStructObj = OpenapiStruct{ + BoolPtr: &fakeBool, + BoolVal: fakeBool, + StringPtr: &fakeString, + StringVal: fakeString, + Int64Ptr: &fakeInt, + Int64Val: fakeInt, + NestedSlice: []OpenapiChildStruct{{ + Str: "fake_child_1", + }, { + Str: "fake_child_2", + }}, + NestedObject: OpenapiChildSingleStruct{ + Strings: []string{"1", "2", "3"}, + Integers: []int64{1, 2, 3}, + Structs: []OpenapiChildStruct{{ + Str: "single_child_1", + }, { + Str: "single_child_2", + }}, + }, +} + +type TfStruct struct { + BoolPtr types.Bool `tfsdk:"bool_ptr"` + BoolVal types.Bool `tfsdk:"bool_val"` + StringPtr types.String `tfsdk:"string_ptr"` + StringVal types.String `tfsdk:"string_val"` + Int64Ptr types.Int64 `tfsdk:"int_64_ptr"` + Int64Val types.Int64 `tfsdk:"int_64_val"` + NestedSlice types.List `tfsdk:"nested_slice"` + NestedObject types.Object `tfsdk:"nested_object"` +} + +func Test_CopyFields(t *testing.T) { + testCopyTfObj := TfStruct{} + err := CopyFieldsToNonNestedModel(context.Background(), openapiStructObj, &testCopyTfObj) + assert.Equal(t, fakeBool, testCopyTfObj.BoolPtr.ValueBool()) + assert.Equal(t, fakeString, testCopyTfObj.StringPtr.ValueString()) + assert.Equal(t, fakeInt, testCopyTfObj.Int64Val.ValueInt64()) + assert.Equal(t, 2, len(testCopyTfObj.NestedSlice.Elements())) + assert.False(t, testCopyTfObj.NestedObject.IsNull()) + assert.Nil(t, err) +} + +func Test_ReadFromState(t *testing.T) { + nestedAttrMap := map[string]attr.Type{ + "strings": types.ListType{ + ElemType: types.StringType, + }, + } + nestedObj, _ := types.ListValueFrom(context.Background(), types.StringType, []string{"state1, state2, state3"}) + nestedValueMap := map[string]attr.Value{ + "strings": nestedObj, + } + obj, _ := types.ObjectValue(nestedAttrMap, nestedValueMap) + sliceAttrMap := map[string]attr.Type{ + "str": types.StringType, + } + sliceAttrVal := map[string]attr.Value{ + "str": types.StringValue("slice_1"), + } + sliceObj, _ := types.ObjectValue(sliceAttrMap, sliceAttrVal) + sliceObjs, _ := types.ListValue(types.ObjectType{ + AttrTypes: sliceAttrMap, + }, []attr.Value{sliceObj}) + tfStructObj := TfStruct{ + BoolPtr: types.BoolValue(fakeBool), + BoolVal: types.BoolValue(fakeBool), + StringPtr: types.StringValue(fakeString), + StringVal: types.StringValue(fakeString), + Int64Ptr: types.Int64Value(fakeInt), + Int64Val: types.Int64Value(fakeInt), + NestedObject: obj, + NestedSlice: sliceObjs, + } + target := OpenapiStruct{} + err := ReadFromState(context.Background(), tfStructObj, &target) + + assert.Nil(t, err) +} diff --git a/powerscale/models/smb_share.go b/powerscale/models/smb_share.go new file mode 100644 index 00000000..f510c8d3 --- /dev/null +++ b/powerscale/models/smb_share.go @@ -0,0 +1,211 @@ +package models + +import "github.com/hashicorp/terraform-plugin-framework/types" + +// SmbShareResource smb share schema attribute details. +type SmbShareResource struct { + // ID of the smb share + ID types.String `tfsdk:"id"` + // Only enumerate files and folders the requesting user has access to. + AccessBasedEnumeration types.Bool `tfsdk:"access_based_enumeration"` + // Access-based enumeration on only the root directory of the share. + AccessBasedEnumerationRootOnly types.Bool `tfsdk:"access_based_enumeration_root_only"` + // Allow deletion of read-only files in the share. + AllowDeleteReadonly types.Bool `tfsdk:"allow_delete_readonly"` + // Allows users to execute files they have read rights for. + AllowExecuteAlways types.Bool `tfsdk:"allow_execute_always"` + // Allow automatic expansion of variables for home directories. + AllowVariableExpansion types.Bool `tfsdk:"allow_variable_expansion"` + // Automatically create home directories. + AutoCreateDirectory types.Bool `tfsdk:"auto_create_directory"` + // Share is visible in net view and the browse list. + Browsable types.Bool `tfsdk:"browsable"` + // Persistent open timeout for the share. + CaTimeout types.Int64 `tfsdk:"ca_timeout"` + // Specify the level of write-integrity on continuously available shares. + CaWriteIntegrity types.String `tfsdk:"ca_write_integrity"` + // Level of change notification alerts on the share. + ChangeNotify types.String `tfsdk:"change_notify"` + // Specify if persistent opens are allowed on the share. + ContinuouslyAvailable types.Bool `tfsdk:"continuously_available"` + // Create path if does not exist. + CreatePath types.Bool `tfsdk:"create_path"` + // Create permissions for new files and directories in share. + CreatePermissions types.String `tfsdk:"create_permissions"` + // Client-side caching policy for the shares. + CscPolicy types.String `tfsdk:"csc_policy"` + // Description for this SMB share. + Description types.String `tfsdk:"description"` + // Directory create mask bits. + DirectoryCreateMask types.Int64 `tfsdk:"directory_create_mask"` + // Directory create mode bits. + DirectoryCreateMode types.Int64 `tfsdk:"directory_create_mode"` + // File create mask bits. + FileCreateMask types.Int64 `tfsdk:"file_create_mask"` + // File create mode bits. + FileCreateMode types.Int64 `tfsdk:"file_create_mode"` + // Specifies the list of file extensions. + FileFilterExtensions types.List `tfsdk:"file_filter_extensions"` + // Specifies if filter list is for deny or allow. Default is deny. + FileFilterType types.String `tfsdk:"file_filter_type"` + // Enables file filtering on this zone. + FileFilteringEnabled types.Bool `tfsdk:"file_filtering_enabled"` + // Hide files and directories that begin with a period '.'. + HideDotFiles types.Bool `tfsdk:"hide_dot_files"` + // An ACL expressing which hosts are allowed access. A deny clause must be the final entry. + HostACL types.List `tfsdk:"host_acl"` + // Specify the condition in which user access is done as the guest account. + ImpersonateGuest types.String `tfsdk:"impersonate_guest"` + // User account to be used as guest account. + ImpersonateUser types.String `tfsdk:"impersonate_user"` + // Set the inheritable ACL on the share path. + InheritablePathACL types.Bool `tfsdk:"inheritable_path_acl"` + // Specifies the wchar_t starting point for automatic byte mangling. + MangleByteStart types.Int64 `tfsdk:"mangle_byte_start"` + // Character mangle map. + MangleMap types.List `tfsdk:"mangle_map"` + // Share name. + Name types.String `tfsdk:"name"` + // Support NTFS ACLs on files and directories. + NtfsACLSupport types.Bool `tfsdk:"ntfs_acl_support"` + // Support oplocks. + Oplocks types.Bool `tfsdk:"oplocks"` + // Path of share within /ifs. + Path types.String `tfsdk:"path"` + // Specifies an ordered list of permission modifications. + Permissions types.List `tfsdk:"permissions"` + // Allow account to run as root. + RunAsRoot types.List `tfsdk:"run_as_root"` + // Enables SMB3 encryption for the share. + Smb3EncryptionEnabled types.Bool `tfsdk:"smb3_encryption_enabled"` + // Enables sparse file. + SparseFile types.Bool `tfsdk:"sparse_file"` + // Specifies if persistent opens would do strict lockout on the share. + StrictCaLockout types.Bool `tfsdk:"strict_ca_lockout"` + // Handle SMB flush operations. + StrictFlush types.Bool `tfsdk:"strict_flush"` + // Specifies whether byte range locks contend against SMB I/O. + StrictLocking types.Bool `tfsdk:"strict_locking"` + // Name of the access zone to which to move this SMB share. + Zone types.String `tfsdk:"zone"` + // Numeric ID of the access zone which contains this SMB share. + Zid types.Int64 `tfsdk:"zid"` +} + +// V1SmbSharePermission Specifies properties for an Access Control Entry. +type V1SmbSharePermission struct { + // Specifies the file system rights that are allowed or denied. + Permission types.String `tfsdk:"permission"` + // Determines whether the permission is allowed or denied. + PermissionType types.String `tfsdk:"permission_type"` + // + Trustee V1AuthAccessAccessItemFileGroup `tfsdk:"trustee"` +} + +// SmbShareDatasource holds smb share datasource schema attribute details. +type SmbShareDatasource struct { + ID types.String `tfsdk:"id"` + SmbShares []SmbShareDatasourceEntity `tfsdk:"smb_shares"` + SmbSharesFilter *SmbShareDatasourceFilter `tfsdk:"filter"` +} + +// SmbShareDatasourceFilter holds filter conditions +type SmbShareDatasourceFilter struct { + // supported by api + Sort types.String `tfsdk:"sort"` + Zone types.String `tfsdk:"zone"` + Resume types.String `tfsdk:"resume"` + ResolveNames types.Bool `tfsdk:"resolve_names"` + Limit types.Int64 `tfsdk:"limit"` + Offset types.Int64 `tfsdk:"offset"` + Scope types.String `tfsdk:"scope"` + Dir types.String `tfsdk:"dir"` + // custom name list + Names []types.String `tfsdk:"names"` +} + +// SmbShareDatasourceEntity struct for SmbShareDatasource +type SmbShareDatasourceEntity struct { + // Only enumerate files and folders the requesting user has access to. + AccessBasedEnumeration types.Bool `tfsdk:"access_based_enumeration"` + // Access-based enumeration on only the root directory of the share. + AccessBasedEnumerationRootOnly types.Bool `tfsdk:"access_based_enumeration_root_only"` + // Allow deletion of read-only files in the share. + AllowDeleteReadonly types.Bool `tfsdk:"allow_delete_readonly"` + // Allows users to execute files they have read rights for. + AllowExecuteAlways types.Bool `tfsdk:"allow_execute_always"` + // Allow automatic expansion of variables for home directories. + AllowVariableExpansion types.Bool `tfsdk:"allow_variable_expansion"` + // Automatically create home directories. + AutoCreateDirectory types.Bool `tfsdk:"auto_create_directory"` + // Share is visible in net view and the browse list. + Browsable types.Bool `tfsdk:"browsable"` + // Persistent open timeout for the share. + CaTimeout types.Int64 `tfsdk:"ca_timeout"` + // Specify the level of write-integrity on continuously available shares. + CaWriteIntegrity types.String `tfsdk:"ca_write_integrity"` + // Level of change notification alerts on the share. + ChangeNotify types.String `tfsdk:"change_notify"` + // Specify if persistent opens are allowed on the share. + ContinuouslyAvailable types.Bool `tfsdk:"continuously_available"` + // Create permissions for new files and directories in share. + CreatePermissions types.String `tfsdk:"create_permissions"` + // Client-side caching policy for the shares. + CscPolicy types.String `tfsdk:"csc_policy"` + // Description for this SMB share. + Description types.String `tfsdk:"description"` + // Directory create mask bits. + DirectoryCreateMask types.Int64 `tfsdk:"directory_create_mask"` + // Directory create mode bits. + DirectoryCreateMode types.Int64 `tfsdk:"directory_create_mode"` + // File create mask bits. + FileCreateMask types.Int64 `tfsdk:"file_create_mask"` + // File create mode bits. + FileCreateMode types.Int64 `tfsdk:"file_create_mode"` + // Specifies the list of file extensions. + FileFilterExtensions types.List `tfsdk:"file_filter_extensions"` + // Specifies if filter list is for deny or allow. Default is deny. + FileFilterType types.String `tfsdk:"file_filter_type"` + // Enables file filtering on this zone. + FileFilteringEnabled types.Bool `tfsdk:"file_filtering_enabled"` + // Hide files and directories that begin with a period '.'. + HideDotFiles types.Bool `tfsdk:"hide_dot_files"` + // An ACL expressing which hosts are allowed access. A deny clause must be the final entry. + HostACL types.List `tfsdk:"host_acl"` + // Share ID. + ID types.String `tfsdk:"id"` + // Specify the condition in which user access is done as the guest account. + ImpersonateGuest types.String `tfsdk:"impersonate_guest"` + // User account to be used as guest account. + ImpersonateUser types.String `tfsdk:"impersonate_user"` + // Set the inheritable ACL on the share path. + InheritablePathACL types.Bool `tfsdk:"inheritable_path_acl"` + // Specifies the wchar_t starting point for automatic byte mangling. + MangleByteStart types.Int64 `tfsdk:"mangle_byte_start"` + // Character mangle map. + MangleMap types.List `tfsdk:"mangle_map"` + // Share name. + Name types.String `tfsdk:"name"` + // Support NTFS ACLs on files and directories. + NtfsACLSupport types.Bool `tfsdk:"ntfs_acl_support"` + // Support oplocks. + Oplocks types.Bool `tfsdk:"oplocks"` + // Path of share within /ifs. + Path types.String `tfsdk:"path"` + // Specifies an ordered list of permission modifications. + Permissions []V1SmbSharePermission `tfsdk:"permissions"` + // Allow account to run as root. + RunAsRoot []V1AuthAccessAccessItemFileGroup `tfsdk:"run_as_root"` + // Enables SMB3 encryption for the share. + Smb3EncryptionEnabled types.Bool `tfsdk:"smb3_encryption_enabled"` + // Enables sparse file. + SparseFile types.Bool `tfsdk:"sparse_file"` + // Specifies if persistent opens would do strict lockout on the share. + StrictCaLockout types.Bool `tfsdk:"strict_ca_lockout"` + // Handle SMB flush operations. + StrictFlush types.Bool `tfsdk:"strict_flush"` + // Specifies whether byte range locks contend against SMB I/O. + StrictLocking types.Bool `tfsdk:"strict_locking"` + // Numeric ID of the access zone which contains this SMB share + Zid types.Int64 `tfsdk:"zid"` +} diff --git a/powerscale/provider/powerscale.env b/powerscale/provider/powerscale.env index 8adf7464..50fd24b7 100644 --- a/powerscale/provider/powerscale.env +++ b/powerscale/provider/powerscale.env @@ -1,11 +1,11 @@ TF_ACC=1 -POWERSCALE_ENDPOINT=https://:8080 -POWERSCALE_USERNAME= -POWERSCALE_PASSWORD= -POWERSCALE_INSECURE= +POWERSCALE_ENDPOINT=https://10.225.108.6:8080 +POWERSCALE_USERNAME=root +POWERSCALE_PASSWORD=Password123! +POWERSCALE_INSECURE=true POWERSCALE_GROUP= POWERSCALE_VOLUME_PATH= POWERSCALE_VOLUME_PATH_PERMISSIONS= -POWERSCALE_IGNORE_UNRESOLVABLE_HOSTS= -POWERSCALE_AUTH_TYPE= -POWERSCALE_VERBOSE_LOGGING= \ No newline at end of file +POWERSCALE_IGNORE_UNRESOLVABLE_HOSTS=true +POWERSCALE_AUTH_TYPE=0 +POWERSCALE_VERBOSE_LOGGING=0 \ No newline at end of file diff --git a/powerscale/provider/provider.go b/powerscale/provider/provider.go index eae99430..2b415b35 100644 --- a/powerscale/provider/provider.go +++ b/powerscale/provider/provider.go @@ -180,6 +180,7 @@ func (p *PscaleProvider) Configure(ctx context.Context, req provider.ConfigureRe func (p *PscaleProvider) Resources(ctx context.Context) []func() resource.Resource { return []func() resource.Resource{ NewAccessZoneResource, + NewSmbShareResource, } } @@ -188,6 +189,7 @@ func (p *PscaleProvider) DataSources(ctx context.Context) []func() datasource.Da return []func() datasource.DataSource{ NewAccessZoneDataSource, NewClusterDataSource, + NewSmbShareDataSource, } } diff --git a/powerscale/provider/provider_test.go b/powerscale/provider/provider_test.go index e47ab916..c0b29e5f 100644 --- a/powerscale/provider/provider_test.go +++ b/powerscale/provider/provider_test.go @@ -19,11 +19,12 @@ package provider import ( "fmt" + . "github.com/bytedance/mockey" "log" + "math/rand" "os" "testing" - - . "github.com/bytedance/mockey" + "time" "github.com/hashicorp/terraform-plugin-framework/providerserver" "github.com/hashicorp/terraform-plugin-go/tfprotov6" @@ -92,3 +93,17 @@ func testAccPreCheck(t *testing.T) { FunctionMocker.UnPatch() } } + +// for acc test, avoid conflict of existing resources. +var ResourceSuffix = RandResNameSuffix(5) + +func RandResNameSuffix(length int) string { + const charSet = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" + rand.Seed(time.Now().UnixNano()) + // generate arr of bytes for ascii characters + b := make([]byte, length) + for i := range b { + b[i] = charSet[rand.Intn(len(charSet))] + } + return string(b) +} diff --git a/powerscale/provider/smb_share_data_source.go b/powerscale/provider/smb_share_data_source.go new file mode 100644 index 00000000..12b08a02 --- /dev/null +++ b/powerscale/provider/smb_share_data_source.go @@ -0,0 +1,503 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + powerscale "dell/powerscale-go-client" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "terraform-provider-powerscale/client" + "terraform-provider-powerscale/powerscale/helper" + "terraform-provider-powerscale/powerscale/models" +) + +var ( + _ datasource.DataSource = &SmbShareDataSource{} + _ datasource.DataSourceWithConfigure = &SmbShareDataSource{} +) + +// NewSmbShareDataSource returns the SmbShare data source object. +func NewSmbShareDataSource() datasource.DataSource { + return &SmbShareDataSource{} +} + +// SmbShareDataSource defines the data source implementation. +type SmbShareDataSource struct { + client *client.Client +} + +// Metadata describes the data source arguments. +func (d *SmbShareDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_smb_share" +} + +// Schema describes the data source arguments. +func (d *SmbShareDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Data source for reading SMB Shares in PowerScale array.", + Description: "Data source for reading SMB Shares in PowerScale array.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Placeholder for acc testing", + Computed: true, + }, + "smb_shares": schema.ListNestedAttribute{ + Computed: true, + Description: "List of smb shares", + MarkdownDescription: "List of smb shares", + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "access_based_enumeration": schema.BoolAttribute{ + Description: "Only enumerate files and folders the requesting user has access to.", + MarkdownDescription: "Only enumerate files and folders the requesting user has access to.", + Computed: true, + }, + "access_based_enumeration_root_only": schema.BoolAttribute{ + Description: "Access-based enumeration on only the root directory of the share.", + MarkdownDescription: "Access-based enumeration on only the root directory of the share.", + Computed: true, + }, + "allow_delete_readonly": schema.BoolAttribute{ + Description: "Allow deletion of read-only files in the share.", + MarkdownDescription: "Allow deletion of read-only files in the share.", + Computed: true, + }, + "allow_execute_always": schema.BoolAttribute{ + Description: "Allows users to execute files they have read rights for.", + MarkdownDescription: "Allows users to execute files they have read rights for.", + Computed: true, + }, + "allow_variable_expansion": schema.BoolAttribute{ + Description: "Allow automatic expansion of variables for home directories.", + MarkdownDescription: "Allow automatic expansion of variables for home directories.", + Computed: true, + }, + "auto_create_directory": schema.BoolAttribute{ + Description: "Automatically create home directories.", + MarkdownDescription: "Automatically create home directories.", + Computed: true, + }, + "browsable": schema.BoolAttribute{ + Description: "Share is visible in net view and the browse list.", + MarkdownDescription: "Share is visible in net view and the browse list.", + Computed: true, + }, + "ca_timeout": schema.Int64Attribute{ + Description: "Persistent open timeout for the share.", + MarkdownDescription: "Persistent open timeout for the share.", + Computed: true, + }, + "ca_write_integrity": schema.StringAttribute{ + Description: "Specify the level of write-integrity on continuously available shares.", + MarkdownDescription: "Specify the level of write-integrity on continuously available shares.", + Computed: true, + }, + "change_notify": schema.StringAttribute{ + Description: "Level of change notification alerts on the share.", + MarkdownDescription: "Level of change notification alerts on the share.", + Computed: true, + }, + "continuously_available": schema.BoolAttribute{ + Description: "Specify if persistent opens are allowed on the share.", + MarkdownDescription: "Specify if persistent opens are allowed on the share.", + Computed: true, + }, + "create_permissions": schema.StringAttribute{ + Description: "Create permissions for new files and directories in share.", + MarkdownDescription: "Create permissions for new files and directories in share.", + Computed: true, + }, + "csc_policy": schema.StringAttribute{ + Description: "Client-side caching policy for the shares.", + MarkdownDescription: "Client-side caching policy for the shares.", + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "Description for this SMB share.", + MarkdownDescription: "Description for this SMB share.", + Computed: true, + }, + "directory_create_mask": schema.Int64Attribute{ + Description: "Directory create mask bits.", + MarkdownDescription: "Directory create mask bits.", + Computed: true, + }, + "directory_create_mode": schema.Int64Attribute{ + Description: "Directory create mode bits.", + MarkdownDescription: "Directory create mode bits.", + Computed: true, + }, + "file_create_mask": schema.Int64Attribute{ + Description: "File create mask bits.", + MarkdownDescription: "File create mask bits.", + Computed: true, + }, + "file_create_mode": schema.Int64Attribute{ + Description: "File create mode bits.", + MarkdownDescription: "File create mode bits.", + Computed: true, + }, + "file_filter_extensions": schema.ListAttribute{ + Description: "Specifies the list of file extensions.", + MarkdownDescription: "Specifies the list of file extensions.", + Computed: true, + ElementType: types.StringType, + }, + "file_filter_type": schema.StringAttribute{ + Description: "Specifies if filter list is for deny or allow. Default is deny.", + MarkdownDescription: "Specifies if filter list is for deny or allow. Default is deny.", + Computed: true, + }, + "file_filtering_enabled": schema.BoolAttribute{ + Description: "Enables file filtering on this zone.", + MarkdownDescription: "Enables file filtering on this zone.", + Computed: true, + }, + "hide_dot_files": schema.BoolAttribute{ + Description: "Hide files and directories that begin with a period '.'.", + MarkdownDescription: "Hide files and directories that begin with a period '.'.", + Computed: true, + }, + "host_acl": schema.ListAttribute{ + Description: "An ACL expressing which hosts are allowed access. A deny clause must be the final entry.", + MarkdownDescription: "An ACL expressing which hosts are allowed access. A deny clause must be the final entry.", + Computed: true, + ElementType: types.StringType, + }, + "id": schema.StringAttribute{ + Description: "Share ID.", + MarkdownDescription: "Share ID.", + Computed: true, + }, + "impersonate_guest": schema.StringAttribute{ + Description: "Specify the condition in which user access is done as the guest account.", + MarkdownDescription: "Specify the condition in which user access is done as the guest account.", + Computed: true, + }, + "impersonate_user": schema.StringAttribute{ + Description: "User account to be used as guest account.", + MarkdownDescription: "User account to be used as guest account.", + Computed: true, + }, + "inheritable_path_acl": schema.BoolAttribute{ + Description: "Set the inheritable ACL on the share path.", + MarkdownDescription: "Set the inheritable ACL on the share path.", + Computed: true, + }, + "mangle_byte_start": schema.Int64Attribute{ + Description: "Specifies the wchar_t starting point for automatic byte mangling.", + MarkdownDescription: "Specifies the wchar_t starting point for automatic byte mangling.", + Computed: true, + }, + "mangle_map": schema.ListAttribute{ + Description: "Character mangle map.", + MarkdownDescription: "Character mangle map.", + Computed: true, + ElementType: types.StringType, + }, + "name": schema.StringAttribute{ + Description: "Share name.", + MarkdownDescription: "Share name.", + Computed: true, + }, + "ntfs_acl_support": schema.BoolAttribute{ + Description: "Support NTFS ACLs on files and directories.", + MarkdownDescription: "Support NTFS ACLs on files and directories.", + Computed: true, + }, + "oplocks": schema.BoolAttribute{ + Description: "Support oplocks.", + MarkdownDescription: "Support oplocks.", + Computed: true, + }, + "path": schema.StringAttribute{ + Description: "Path of share within /ifs.", + MarkdownDescription: "Path of share within /ifs.", + Computed: true, + }, + "permissions": schema.ListNestedAttribute{ + Description: "Specifies an ordered list of permission modifications.", + MarkdownDescription: "Specifies an ordered list of permission modifications.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "permission": schema.StringAttribute{ + Description: "Specifies the file system rights that are allowed or denied.", + MarkdownDescription: "Specifies the file system rights that are allowed or denied.", + Computed: true, + }, + "permission_type": schema.StringAttribute{ + Description: "Determines whether the permission is allowed or denied.", + MarkdownDescription: "Determines whether the permission is allowed or denied.", + Computed: true, + }, + "trustee": schema.SingleNestedAttribute{ + Description: "Specifies the persona of the file group.", + MarkdownDescription: "Specifies the persona of the file group.", + Computed: true, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'.", + MarkdownDescription: "Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'.", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "Specifies the persona name, which must be combined with a type.", + MarkdownDescription: "Specifies the persona name, which must be combined with a type.", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Specifies the type of persona, which must be combined with a name.", + MarkdownDescription: "Specifies the type of persona, which must be combined with a name.", + Computed: true, + }, + }, + }, + }, + }, + }, + "run_as_root": schema.ListNestedAttribute{ + Description: "Allow account to run as root.", + MarkdownDescription: "Allow account to run as root.", + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'.", + MarkdownDescription: "Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'.", + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "Specifies the persona name, which must be combined with a type.", + MarkdownDescription: "Specifies the persona name, which must be combined with a type.", + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Specifies the type of persona, which must be combined with a name.", + MarkdownDescription: "Specifies the type of persona, which must be combined with a name.", + Computed: true, + }, + }, + }, + }, + "smb3_encryption_enabled": schema.BoolAttribute{ + Description: "Enables SMB3 encryption for the share.", + MarkdownDescription: "Enables SMB3 encryption for the share.", + Computed: true, + }, + "sparse_file": schema.BoolAttribute{ + Description: "Enables sparse file.", + MarkdownDescription: "Enables sparse file.", + Computed: true, + }, + "strict_ca_lockout": schema.BoolAttribute{ + Description: "Specifies if persistent opens would do strict lockout on the share.", + MarkdownDescription: "Specifies if persistent opens would do strict lockout on the share.", + Computed: true, + }, + "strict_flush": schema.BoolAttribute{ + Description: "Handle SMB flush operations.", + MarkdownDescription: "Handle SMB flush operations.", + Computed: true, + }, + "strict_locking": schema.BoolAttribute{ + Description: "Specifies whether byte range locks contend against SMB I/O.", + MarkdownDescription: "Specifies whether byte range locks contend against SMB I/O.", + Computed: true, + }, + "zid": schema.Int64Attribute{ + Description: "Numeric ID of the access zone which contains this SMB share", + MarkdownDescription: "Numeric ID of the access zone which contains this SMB share", + Computed: true, + }, + }, + }, + }, + }, + Blocks: map[string]schema.Block{ + "filter": schema.SingleNestedBlock{ + Attributes: map[string]schema.Attribute{ + "names": schema.SetAttribute{ + Description: "Names to filter smb shares.", + MarkdownDescription: "Names to filter smb shares.", + Optional: true, + ElementType: types.StringType, + }, + "sort": schema.StringAttribute{ + Description: "The field that will be used for sorting.", + MarkdownDescription: "The field that will be used for sorting.", + Optional: true, + }, + "zone": schema.StringAttribute{ + Description: "Specifies which access zone to use.", + MarkdownDescription: "Specifies which access zone to use.", + Optional: true, + }, + "resume": schema.StringAttribute{ + Description: "Continue returning results from previous call using this token " + + "(token should come from the previous call, resume cannot be used with other options).", + MarkdownDescription: "Continue returning results from previous call using this token " + + "(token should come from the previous call, resume cannot be used with other options).", + Optional: true, + }, + "resolve_names": schema.BoolAttribute{ + Description: "If true, resolve group and user names in personas.", + MarkdownDescription: "If true, resolve group and user names in personas.", + Optional: true, + }, + "limit": schema.Int64Attribute{ + Description: "Return no more than this many results at once (see resume).", + MarkdownDescription: "Return no more than this many results at once (see resume).", + Optional: true, + }, + "offset": schema.Int64Attribute{ + Description: "The position of the first item returned for a paginated query within the full result set.", + MarkdownDescription: "The position of the first item returned for a paginated query within the full result set.", + Optional: true, + }, + "scope": schema.StringAttribute{ + Description: "If specified as \"effective\" or not specified, all fields are returned. " + + "If specified as \"user\", only fields with non-default values are shown. If specified as \"default\", the original values are returned.", + MarkdownDescription: "If specified as \"effective\" or not specified, all fields are returned. " + + "If specified as \"user\", only fields with non-default values are shown. If specified as \"default\", the original values are returned.", + Optional: true, + }, + "dir": schema.StringAttribute{ + Description: "The direction of the sort.", + MarkdownDescription: "The direction of the sort.", + Optional: true, + }, + }, + }, + }, + } +} + +// Configure configures the resource. +func (d *SmbShareDataSource) Configure(ctx context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + pscaleClient, ok := req.ProviderData.(*client.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *client.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + d.client = pscaleClient +} + +// Read reads data from the data source. +func (d *SmbShareDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + tflog.Info(ctx, "Reading Smb Share data source ") + var sharesPlan models.SmbShareDatasource + var sharesState models.SmbShareDatasource + // Read Terraform configuration data into the model + resp.Diagnostics.Append(req.Config.Get(ctx, &sharesPlan)...) + + if resp.Diagnostics.HasError() { + return + } + + var shareNames []types.String + listSmbParam := d.client.PscaleOpenAPIClient.ProtocolsApi.ListProtocolsv7SmbShares(ctx) + if sharesPlan.SmbSharesFilter != nil { + shareNames = sharesPlan.SmbSharesFilter.Names + + // handle api filter + listSmbParam.Resume(sharesPlan.SmbSharesFilter.Resume.ValueString()) + listSmbParam.Zone(sharesPlan.SmbSharesFilter.Zone.ValueString()) + listSmbParam.Dir(sharesPlan.SmbSharesFilter.Dir.ValueString()) + listSmbParam.Scope(sharesPlan.SmbSharesFilter.Scope.ValueString()) + listSmbParam.Sort(sharesPlan.SmbSharesFilter.Sort.ValueString()) + if !sharesPlan.SmbSharesFilter.ResolveNames.IsNull() { + listSmbParam.ResolveNames(sharesPlan.SmbSharesFilter.ResolveNames.ValueBool()) + } + if !sharesPlan.SmbSharesFilter.Limit.IsNull() { + listSmbParam.Limit(int32(sharesPlan.SmbSharesFilter.Limit.ValueInt64())) + } + } + smbShares, _, err := listSmbParam.Execute() + if err != nil { + resp.Diagnostics.AddError("Error reading smb share datasource", + fmt.Sprintf("Could not list smb shares with error: %s", err.Error())) + return + } + totalSmbShares := smbShares.Shares + for smbShares.Resume != nil && (sharesPlan.SmbSharesFilter == nil || sharesPlan.SmbSharesFilter.Limit.IsNull()) { + resumeSmbParam := d.client.PscaleOpenAPIClient.ProtocolsApi.ListProtocolsv7SmbShares(ctx).Resume(*smbShares.Resume) + smbShares, _, err = resumeSmbParam.Execute() + if err != nil { + resp.Diagnostics.AddError("Error reading smb share datasource plan", + fmt.Sprintf("Could not list smb shares with error: %s", err.Error())) + return + } + totalSmbShares = append(totalSmbShares, smbShares.Shares...) + } + + // if names are specified filter locally + var filteredShares []models.SmbShareDatasourceEntity + if len(shareNames) > 0 { + sharesMap := make(map[string]powerscale.V7SmbShareExtended) + for _, s := range totalSmbShares { + sharesMap[s.Name] = s + } + for _, name := range shareNames { + if specifiedShare, ok := sharesMap[name.ValueString()]; ok { + entity := models.SmbShareDatasourceEntity{} + err := helper.CopyFields(ctx, specifiedShare, &entity) + if err != nil { + resp.Diagnostics.AddError("Error reading smb share datasource plan", + fmt.Sprintf("Could not list smb shares with error: %s", err.Error())) + return + } + filteredShares = append(filteredShares, entity) + } + } + } else { + entity := models.SmbShareDatasourceEntity{} + for _, share := range totalSmbShares { + err := helper.CopyFields(ctx, share, &entity) + if err != nil { + resp.Diagnostics.AddError("Error reading smb share datasource plan", + fmt.Sprintf("Could not list smb shares with error: %s", err.Error())) + return + } + filteredShares = append(filteredShares, entity) + } + } + //check if there is any error while getting the port group + sharesState.ID = types.StringValue("1") + sharesState.SmbSharesFilter = sharesPlan.SmbSharesFilter + sharesState.SmbShares = filteredShares + + // Save data into Terraform state + resp.Diagnostics.Append(resp.State.Set(ctx, &sharesState)...) +} diff --git a/powerscale/provider/smb_share_data_source_test.go b/powerscale/provider/smb_share_data_source_test.go new file mode 100644 index 00000000..da723e92 --- /dev/null +++ b/powerscale/provider/smb_share_data_source_test.go @@ -0,0 +1,140 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + powerscale "dell/powerscale-go-client" + "fmt" + . "github.com/bytedance/mockey" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "testing" +) + +func TestAccSmbShareDatasource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + //Read testing + { + Config: ProviderConfig + SmbShareDatasourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.powerscale_smb_share.share_datasource_test", "smb_shares.#", "1"), + ), + }, + }, + }) +} + +func Test_GetAll_AccSmbShareDatasource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + //Read testing + { + Config: ProviderConfig + SmbShareAllDatasourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.powerscale_smb_share.share_datasource_test_all", "filter.#", "0"), + ), + }, + }, + }) +} + +func Test_Pagination_AccSmbShareDatasource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + //Read testing + { + PreConfig: func() { + resume := "1" + shares := powerscale.V7SmbShares{ + Digest: nil, + Resume: &resume, + Shares: nil, + Total: nil, + } + if FunctionMocker != nil { + FunctionMocker.Release() + } + FunctionMocker = Mock(GetMethod(powerscale.ApiListProtocolsv7SmbSharesRequest{}, "Execute")).Return(&shares, nil, nil).Build() + FunctionMocker.When(func() bool { + shares := powerscale.V7SmbShares{ + Digest: nil, + Resume: nil, + Shares: []powerscale.V7SmbShareExtended{{Id: shareName}}, + Total: nil, + } + if FunctionMocker.MockTimes() > 0 { + FunctionMocker.UnPatch() + FunctionMocker = Mock(GetMethod(powerscale.ApiListProtocolsv7SmbSharesRequest{}, "Execute")).Return(&shares, nil, nil).Build() + } + return FunctionMocker.MockTimes() == 0 + }) + }, + Config: ProviderConfig + SmbShareAllDatasourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("data.powerscale_smb_share.share_datasource_test_all", "filter.#", "0"), + ), + }, + }, + CheckDestroy: func(_ *terraform.State) error { + if FunctionMocker != nil { + FunctionMocker = FunctionMocker.UnPatch() + } + return nil + }, + }) +} + +var SmbShareAllDatasourceConfig = fmt.Sprintf(` +data "powerscale_smb_share" "share_datasource_test_all" {} +`) + +var SmbShareDatasourceConfig = fmt.Sprintf(` +resource "powerscale_smb_share" "share_resource_test" { + auto_create_directory = true + name = "%s" + path = "/ifs/%s" + permissions = [ + { + permission = "full" + permission_type = "allow" + trustee = { + id = "SID:S-1-1-0", + name = "Everyone", + type = "wellknown" + } + } + ] +} + +data "powerscale_smb_share" "share_datasource_test" { + filter { + resolve_names = true + names = ["%s"] + } + depends_on = [ + powerscale_smb_share.share_resource_test + ] +} +`, shareName, shareName, shareName) diff --git a/powerscale/provider/smb_share_resource.go b/powerscale/provider/smb_share_resource.go new file mode 100644 index 00000000..d65ec1ac --- /dev/null +++ b/powerscale/provider/smb_share_resource.go @@ -0,0 +1,608 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + "context" + powerscale "dell/powerscale-go-client" + "fmt" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listdefault" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/listplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "terraform-provider-powerscale/client" + "terraform-provider-powerscale/powerscale/helper" + "terraform-provider-powerscale/powerscale/models" +) + +// SmbShareResource creates a new resource. +type SmbShareResource struct { + client *client.Client +} + +// Ensure the implementation satisfies the expected interfaces. +var ( + _ resource.Resource = &SmbShareResource{} + _ resource.ResourceWithConfigure = &SmbShareResource{} + _ resource.ResourceWithImportState = &SmbShareResource{} +) + +// NewSmbShareResource is a helper function to simplify the provider implementation. +func NewSmbShareResource() resource.Resource { + return &SmbShareResource{} +} + +// Metadata describes the resource arguments. +func (r SmbShareResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_smb_share" +} + +// Schema describes the resource arguments. +func (r *SmbShareResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + // This description is used by the documentation generator and the language server. + MarkdownDescription: "Resource for managing SMB Shares in PowerScale array.", + Description: "Resource for managing SMB Shares in PowerScale array.", + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "The ID of the smb share.", + MarkdownDescription: "The ID of the smb share.", + Computed: true, + }, + "access_based_enumeration": schema.BoolAttribute{ + Description: "Only enumerate files and folders the requesting user has access to.", + MarkdownDescription: "Only enumerate files and folders the requesting user has access to.", + Optional: true, + Computed: true, + }, + "access_based_enumeration_root_only": schema.BoolAttribute{ + Description: "Access-based enumeration on only the root directory of the share.", + MarkdownDescription: "Access-based enumeration on only the root directory of the share.", + Optional: true, + Computed: true, + }, + "allow_delete_readonly": schema.BoolAttribute{ + Description: "Allow deletion of read-only files in the share.", + MarkdownDescription: "Allow deletion of read-only files in the share.", + Optional: true, + Computed: true, + }, + "allow_execute_always": schema.BoolAttribute{ + Description: "Allows users to execute files they have read rights for.", + MarkdownDescription: "Allows users to execute files they have read rights for.", + Optional: true, + Computed: true, + }, + "allow_variable_expansion": schema.BoolAttribute{ + Description: "Allow automatic expansion of variables for home directories.", + MarkdownDescription: "Allow automatic expansion of variables for home directories.", + Optional: true, + Computed: true, + }, + "auto_create_directory": schema.BoolAttribute{ + Description: "Automatically create home directories.", + MarkdownDescription: "Automatically create home directories.", + Optional: true, + Computed: true, + }, + "browsable": schema.BoolAttribute{ + Description: "Share is visible in net view and the browse list.", + MarkdownDescription: "Share is visible in net view and the browse list.", + Optional: true, + Computed: true, + }, + "ca_timeout": schema.Int64Attribute{ + Description: "Persistent open timeout for the share.", + MarkdownDescription: "Persistent open timeout for the share.", + Optional: true, + Computed: true, + }, + "ca_write_integrity": schema.StringAttribute{ + Description: "Specify the level of write-integrity on continuously available shares.", + MarkdownDescription: "Specify the level of write-integrity on continuously available shares.", + Optional: true, + Computed: true, + }, + "change_notify": schema.StringAttribute{ + Description: "Level of change notification alerts on the share.", + MarkdownDescription: "Level of change notification alerts on the share.", + Optional: true, + Computed: true, + }, + "continuously_available": schema.BoolAttribute{ + Description: "Specify if persistent opens are allowed on the share.", + MarkdownDescription: "Specify if persistent opens are allowed on the share.", + Computed: true, + }, + "create_path": schema.BoolAttribute{ + Description: "Create path if does not exist.", + MarkdownDescription: "Create path if does not exist.", + Optional: true, + }, + "create_permissions": schema.StringAttribute{ + Description: "Create permissions for new files and directories in share.", + MarkdownDescription: "Create permissions for new files and directories in share.", + Optional: true, + Computed: true, + }, + "csc_policy": schema.StringAttribute{ + Description: "Client-side caching policy for the shares.", + MarkdownDescription: "Client-side caching policy for the shares.", + Optional: true, + Computed: true, + }, + "description": schema.StringAttribute{ + Description: "Description for this SMB share.", + MarkdownDescription: "Description for this SMB share.", + Optional: true, + Computed: true, + }, + "directory_create_mask": schema.Int64Attribute{ + Description: "Directory create mask bits.", + MarkdownDescription: "Directory create mask bits.", + Optional: true, + Computed: true, + }, + "directory_create_mode": schema.Int64Attribute{ + Description: "Directory create mode bits.", + MarkdownDescription: "Directory create mode bits.", + Optional: true, + Computed: true, + }, + "file_create_mask": schema.Int64Attribute{ + Description: "File create mask bits.", + MarkdownDescription: "File create mask bits.", + Optional: true, + Computed: true, + }, + "file_create_mode": schema.Int64Attribute{ + Description: "File create mode bits.", + MarkdownDescription: "File create mode bits.", + Optional: true, + Computed: true, + }, + "file_filter_extensions": schema.ListAttribute{ + Description: "Specifies the list of file extensions.", + MarkdownDescription: "Specifies the list of file extensions.", + Optional: true, + Computed: true, + ElementType: types.StringType, + }, + "file_filter_type": schema.StringAttribute{ + Description: "Specifies if filter list is for deny or allow. Default is deny.", + MarkdownDescription: "Specifies if filter list is for deny or allow. Default is deny.", + Optional: true, + Computed: true, + }, + "file_filtering_enabled": schema.BoolAttribute{ + Description: "Enables file filtering on this zone.", + MarkdownDescription: "Enables file filtering on this zone.", + Optional: true, + Computed: true, + }, + "hide_dot_files": schema.BoolAttribute{ + Description: "Hide files and directories that begin with a period '.'.", + MarkdownDescription: "Hide files and directories that begin with a period '.'.", + Optional: true, + Computed: true, + }, + "host_acl": schema.ListAttribute{ + Description: "An ACL expressing which hosts are allowed access. A deny clause must be the final entry.", + MarkdownDescription: "An ACL expressing which hosts are allowed access. A deny clause must be the final entry.", + Optional: true, + Computed: true, + ElementType: types.StringType, + }, + "impersonate_guest": schema.StringAttribute{ + Description: "Specify the condition in which user access is done as the guest account.", + MarkdownDescription: "Specify the condition in which user access is done as the guest account.", + Optional: true, + Computed: true, + }, + "impersonate_user": schema.StringAttribute{ + Description: "User account to be used as guest account.", + MarkdownDescription: "User account to be used as guest account.", + Optional: true, + Computed: true, + }, + "inheritable_path_acl": schema.BoolAttribute{ + Description: "Set the inheritable ACL on the share path.", + MarkdownDescription: "Set the inheritable ACL on the share path.", + Optional: true, + Computed: true, + }, + "mangle_byte_start": schema.Int64Attribute{ + Description: "Specifies the wchar_t starting point for automatic byte mangling.", + MarkdownDescription: "Specifies the wchar_t starting point for automatic byte mangling.", + Optional: true, + Computed: true, + }, + "mangle_map": schema.ListAttribute{ + Description: "Character mangle map.", + MarkdownDescription: "Character mangle map.", + Optional: true, + Computed: true, + ElementType: types.StringType, + }, + "name": schema.StringAttribute{ + Description: "Share name.", + MarkdownDescription: "Share name.", + Optional: true, + Computed: true, + }, + "ntfs_acl_support": schema.BoolAttribute{ + Description: "Support NTFS ACLs on files and directories.", + MarkdownDescription: "Support NTFS ACLs on files and directories.", + Optional: true, + Computed: true, + }, + "oplocks": schema.BoolAttribute{ + Description: "Support oplocks.", + MarkdownDescription: "Support oplocks.", + Optional: true, + Computed: true, + }, + "path": schema.StringAttribute{ + Description: "Path of share within /ifs.", + MarkdownDescription: "Path of share within /ifs.", + Required: true, + }, + "permissions": schema.ListNestedAttribute{ + Description: "Specifies an ordered list of permission modifications.", + MarkdownDescription: "Specifies an ordered list of permission modifications.", + Required: true, + PlanModifiers: []planmodifier.List{listplanmodifier.UseStateForUnknown()}, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "permission": schema.StringAttribute{ + Description: "Specifies the file system rights that are allowed or denied.", + MarkdownDescription: "Specifies the file system rights that are allowed or denied.", + Required: true, + }, + "permission_type": schema.StringAttribute{ + Description: "Determines whether the permission is allowed or denied.", + MarkdownDescription: "Determines whether the permission is allowed or denied.", + Required: true, + }, + "trustee": schema.SingleNestedAttribute{ + Description: "Specifies the persona of the file group.", + MarkdownDescription: "Specifies the persona of the file group.", + Required: true, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'.", + MarkdownDescription: "Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'.", + Optional: true, + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "Specifies the persona name, which must be combined with a type.", + MarkdownDescription: "Specifies the persona name, which must be combined with a type.", + Optional: true, + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Specifies the type of persona, which must be combined with a name.", + MarkdownDescription: "Specifies the type of persona, which must be combined with a name.", + Optional: true, + Computed: true, + }, + }, + }, + }, + }, + }, + "run_as_root": schema.ListNestedAttribute{ + Description: "Allow account to run as root.", + MarkdownDescription: "Allow account to run as root.", + Optional: true, + Computed: true, + Default: listdefault.StaticValue(types.ListNull(types.ObjectType{AttrTypes: map[string]attr.Type{ + "id": types.StringType, "name": types.StringType, "type": types.StringType}})), + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Description: "Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'.", + MarkdownDescription: "Specifies the serialized form of a persona, which can be 'UID:0', 'USER:name', 'GID:0', 'GROUP:wheel', or 'SID:S-1-1'.", + Optional: true, + Computed: true, + }, + "name": schema.StringAttribute{ + Description: "Specifies the persona name, which must be combined with a type.", + MarkdownDescription: "Specifies the persona name, which must be combined with a type.", + Optional: true, + Computed: true, + }, + "type": schema.StringAttribute{ + Description: "Specifies the type of persona, which must be combined with a name.", + MarkdownDescription: "Specifies the type of persona, which must be combined with a name.", + Optional: true, + Computed: true, + }, + }, + }, + }, + "smb3_encryption_enabled": schema.BoolAttribute{ + Description: "Enables SMB3 encryption for the share.", + MarkdownDescription: "Enables SMB3 encryption for the share.", + Optional: true, + Computed: true, + }, + "sparse_file": schema.BoolAttribute{ + Description: "Enables sparse file.", + MarkdownDescription: "Enables sparse file.", + Optional: true, + Computed: true, + }, + "strict_ca_lockout": schema.BoolAttribute{ + Description: "Specifies if persistent opens would do strict lockout on the share.", + MarkdownDescription: "Specifies if persistent opens would do strict lockout on the share.", + Optional: true, + Computed: true, + }, + "strict_flush": schema.BoolAttribute{ + Description: "Handle SMB flush operations.", + MarkdownDescription: "Handle SMB flush operations.", + Optional: true, + Computed: true, + }, + "strict_locking": schema.BoolAttribute{ + Description: "Specifies whether byte range locks contend against SMB I/O.", + MarkdownDescription: "Specifies whether byte range locks contend against SMB I/O.", + Optional: true, + Computed: true, + }, + "zone": schema.StringAttribute{ + Description: "Name of the access zone to which to move this SMB share.", + MarkdownDescription: "Name of the access zone to which to move this SMB share.", + Optional: true, + }, + "zid": schema.Int64Attribute{ + Description: "Numeric ID of the access zone which contains this SMB share.", + MarkdownDescription: "Numeric ID of the access zone which contains this SMB share.", + Optional: true, + Computed: true, + }, + }, + } +} + +// Configure - defines configuration for smb share resource. +func (r *SmbShareResource) Configure(ctx context.Context, req resource.ConfigureRequest, res *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + c, ok := req.ProviderData.(*client.Client) + if !ok { + res.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *c.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + r.client = c +} + +// Create allocates the resource. +func (r SmbShareResource) Create(ctx context.Context, request resource.CreateRequest, response *resource.CreateResponse) { + tflog.Info(ctx, "creating smb share") + + var sharePlan models.SmbShareResource + diags := request.Plan.Get(ctx, &sharePlan) + //cachedPermission := sharePlan.Permissions + + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + createShareParam := r.client.PscaleOpenAPIClient.ProtocolsApi.CreateProtocolsv7SmbShare(ctx) + shareToCreate := powerscale.V7SmbShare{} + // Get param from tf input + err := helper.ReadFromState(ctx, sharePlan, &shareToCreate) + if !sharePlan.Zone.IsNull() { + createShareParam = createShareParam.Zone(sharePlan.Zone.ValueString()) + } + shareResponse, _, err := createShareParam.V7SmbShare(shareToCreate).Execute() + if err != nil { + response.Diagnostics.AddError("Error creating smb share", + fmt.Sprintf("Could not create smb share of Path: %s with error: %s", sharePlan.Path.ValueString(), err.Error()), + ) + return + } + tflog.Debug(ctx, fmt.Sprintf("smb share %s created", shareResponse.Id), map[string]interface{}{ + "smbShareResponse": shareResponse, + }) + + getShareParam := r.client.PscaleOpenAPIClient.ProtocolsApi.GetProtocolsv7SmbShare(ctx, shareResponse.Id) + getShareResponse, _, err := getShareParam.Execute() + if err != nil { + response.Diagnostics.AddError( + "Error creating smb share", + fmt.Sprintf("Could not read smb share %s with error: %s", shareResponse.Id, err.Error()), + ) + return + } + + // update resource state according to response + if len(getShareResponse.Shares) <= 0 { + response.Diagnostics.AddError( + "Error creating smb share", + fmt.Sprintf("Could not get created smb share state %s with error: smb share not found", shareResponse.Id), + ) + return + } + createdShare := getShareResponse.Shares[0] + err = helper.CopyFieldsToNonNestedModel(ctx, createdShare, &sharePlan) + diags = response.State.Set(ctx, sharePlan) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "create smb share completed") +} + +// Read reads the resource state. +func (r SmbShareResource) Read(ctx context.Context, request resource.ReadRequest, response *resource.ReadResponse) { + tflog.Info(ctx, "reading smb share") + var shareState models.SmbShareResource + diags := request.State.Get(ctx, &shareState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + shareID := shareState.ID + tflog.Debug(ctx, "calling get smb share by ID", map[string]interface{}{ + "smbShareID": shareID, + }) + shareResponse, _, err := r.client.PscaleOpenAPIClient.ProtocolsApi.GetProtocolsv7SmbShare(ctx, shareID.ValueString()).Execute() + if err != nil { + response.Diagnostics.AddError( + "Error reading smb share", + fmt.Sprintf("Could not read smb share %s with error: %s", shareID, err.Error()), + ) + return + } + + if len(shareResponse.Shares) <= 0 { + response.Diagnostics.AddError( + "Error reading smb share", + fmt.Sprintf("Could not read smb share %s from pscale with error: smb share not found", shareID), + ) + return + } + tflog.Debug(ctx, "updating read smb share state", map[string]interface{}{ + "smbShareResponse": shareResponse, + "smbShareState": shareState, + }) + err = helper.CopyFieldsToNonNestedModel(ctx, shareResponse.Shares[0], &shareState) + diags = response.State.Set(ctx, shareState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "read smb share completed") +} + +// Update updates the resource state +func (r SmbShareResource) Update(ctx context.Context, request resource.UpdateRequest, response *resource.UpdateResponse) { + tflog.Info(ctx, "updating smb share") + var sharePlan models.SmbShareResource + diags := request.Plan.Get(ctx, &sharePlan) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + var shareState models.SmbShareResource + diags = response.State.Get(ctx, &shareState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + tflog.Debug(ctx, "calling update smb share", map[string]interface{}{ + "sharePlan": sharePlan, + "shareState": shareState, + }) + + shareID := shareState.ID.ValueString() + var shareToUpdate powerscale.V7SmbShareExtendedExtended + // Get param from tf input + err := helper.ReadFromState(ctx, sharePlan, &shareToUpdate) + + updateParam := r.client.PscaleOpenAPIClient.ProtocolsApi.UpdateProtocolsv7SmbShare(ctx, shareID) + _, err = updateParam.V7SmbShare(shareToUpdate).Execute() + if err != nil { + response.Diagnostics.AddError( + "Error updating smb share", + fmt.Sprintf("Could not update smb share %s with error: %s", shareID, err.Error()), + ) + return + } + + tflog.Debug(ctx, "calling get smb share by ID on pscale client", map[string]interface{}{ + "smbShareID": shareID, + }) + updatedShare, _, err := r.client.PscaleOpenAPIClient.ProtocolsApi.GetProtocolsv7SmbShare(ctx, shareID).Execute() + if err != nil { + response.Diagnostics.AddError( + "Error updating smb share", + fmt.Sprintf("Could not read smb share %s with error: %s", shareID, err.Error()), + ) + return + } + + if len(updatedShare.Shares) <= 0 { + response.Diagnostics.AddError( + "Error reading smb share", + fmt.Sprintf("Could not read smb share %s from pscale with error: %s", shareID, err.Error()), + ) + return + } + + err = helper.CopyFieldsToNonNestedModel(ctx, updatedShare.Shares[0], &shareState) + diags = response.State.Set(ctx, shareState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + tflog.Info(ctx, "update smb share completed") +} + +// Delete deletes the resource. +func (r SmbShareResource) Delete(ctx context.Context, request resource.DeleteRequest, response *resource.DeleteResponse) { + tflog.Info(ctx, "deleting smb share") + var shareState models.SmbShareResource + diags := request.State.Get(ctx, &shareState) + response.Diagnostics.Append(diags...) + if response.Diagnostics.HasError() { + return + } + + zone := shareState.Zone.ValueString() + shareID := shareState.ID.ValueString() + if diags.HasError() { + response.Diagnostics.Append(diags...) + } + + tflog.Debug(ctx, "calling delete smb share on pscale client", map[string]interface{}{ + "smbShareID": shareID, + }) + _, err := r.client.PscaleOpenAPIClient.ProtocolsApi.DeleteProtocolsv7SmbShare(ctx, shareID).Zone(zone).Execute() + if err != nil { + response.Diagnostics.AddError( + "Error deleting smb share", + fmt.Sprintf("Could not remove smb share ID: %s with error: %s ", + shareID, err.Error()), + ) + } + response.State.RemoveResource(ctx) + tflog.Info(ctx, "delete smb share completed") +} + +// ImportState imports the resource state. +func (r SmbShareResource) ImportState(ctx context.Context, request resource.ImportStateRequest, response *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), request, response) +} diff --git a/powerscale/provider/smb_share_resource_test.go b/powerscale/provider/smb_share_resource_test.go new file mode 100644 index 00000000..36a91623 --- /dev/null +++ b/powerscale/provider/smb_share_resource_test.go @@ -0,0 +1,189 @@ +/* +Copyright (c) 2023 Dell Inc., or its subsidiaries. All Rights Reserved. + +Licensed under the Mozilla Public License Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://mozilla.org/MPL/2.0/ + + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package provider + +import ( + powerscale "dell/powerscale-go-client" + "fmt" + . "github.com/bytedance/mockey" + "github.com/hashicorp/terraform-plugin-testing/helper/resource" + "github.com/hashicorp/terraform-plugin-testing/terraform" + "github.com/stretchr/testify/assert" + "regexp" + "testing" +) + +func TestAccSmbShareResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: ProviderConfig + SmbShareResourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("powerscale_smb_share.share_test", "name", shareName), + resource.TestCheckResourceAttr("powerscale_smb_share.share_test", "ca_timeout", "120"), + ), + }, + // ImportState testing + { + ResourceName: "powerscale_smb_share.share_test", + ImportState: true, + ImportStateCheck: func(states []*terraform.InstanceState) error { + assert.Equal(t, shareName, states[0].Attributes["id"]) + assert.Equal(t, "120", states[0].Attributes["ca_timeout"]) + return nil + }, + }, + // Update + { + Config: ProviderConfig + SmbShareUpdatedResourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("powerscale_smb_share.share_test", "allow_delete_readonly", "true"), + resource.TestCheckResourceAttr("powerscale_smb_share.share_test", "ca_timeout", "30"), + ), + }, + }, + }) +} + +func Test_ErrorRead_AccSmbShareResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: ProviderConfig + SmbShareResourceConfig, + Check: resource.ComposeAggregateTestCheckFunc( + resource.TestCheckResourceAttr("powerscale_smb_share.share_test", "name", shareName), + resource.TestCheckResourceAttr("powerscale_smb_share.share_test", "ca_timeout", "120"), + ), + }, + // ImportState testing + { + ResourceName: "powerscale_smb_share.share_test", + ImportState: true, + PreConfig: func() { + if FunctionMocker != nil { + FunctionMocker.Release() + } + FunctionMocker = Mock(GetMethod(powerscale.ApiGetProtocolsv7SmbShareRequest{}, "Execute")). + Return(&powerscale.V7SmbSharesExtended{}, nil, nil).Build() + }, + ExpectError: regexp.MustCompile(".not found"), + }, + // Update + { + PreConfig: func() { + if FunctionMocker != nil { + FunctionMocker.Release() + FunctionMocker = FunctionMocker.UnPatch() + } + }, + Config: ProviderConfig + SmbShareInvalidResourceConfig, + ExpectError: regexp.MustCompile(".*Bad Request*."), + }, + }, + CheckDestroy: func(_ *terraform.State) error { + if FunctionMocker != nil { + FunctionMocker = FunctionMocker.UnPatch() + } + return nil + }, + }) +} + +func Test_ErrorCreate_AccSmbShareResource(t *testing.T) { + resource.Test(t, resource.TestCase{ + PreCheck: func() { testAccPreCheck(t) }, + ProtoV6ProviderFactories: testAccProtoV6ProviderFactories, + Steps: []resource.TestStep{ + // Create and Read testing + { + Config: ProviderConfig + SmbShareInvalidResourceConfig, + ExpectError: regexp.MustCompile(".*Bad Request*."), + }, + }, + }) +} + +var shareName = "tfacc_test_smb_share" + +var SmbShareResourceConfig = fmt.Sprintf(` +resource "powerscale_smb_share" "share_test" { + auto_create_directory = true + name = "%s" + path = "/ifs/%s" + permissions = [ + { + permission = "full" + permission_type = "allow" + trustee = { + id = "SID:S-1-1-0", + name = "Everyone", + type = "wellknown" + } + } + ] + zone = "System" +} +`, shareName, shareName) + +var SmbShareInvalidResourceConfig = fmt.Sprintf(` +resource "powerscale_smb_share" "share_test" { + auto_create_directory = true + name = "%s" + path = "/ifs/%s" + zone = "System" + permissions = [ + { + permission = "full" + permission_type = "allow" + trustee = { + id = "SID:S-1-1-0", + name = "invalid", + type = "invalid" + } + } + ] +} +`, shareName, shareName) + +var SmbShareUpdatedResourceConfig = fmt.Sprintf(` +resource "powerscale_smb_share" "share_test" { + auto_create_directory = true + name = "%s" + path = "/ifs/%s" + permissions = [ + { + permission = "full" + permission_type = "allow" + trustee = { + id = "SID:S-1-1-0", + name = "Everyone", + type = "wellknown" + } + } + ] + allow_delete_readonly = true + ca_timeout = 30 + zone = "System" +} +`, shareName, shareName)