From 38a96cc1d65ec719aa77393db865e47b68e1e7c2 Mon Sep 17 00:00:00 2001 From: Nohv Date: Tue, 10 Sep 2024 14:18:18 +0900 Subject: [PATCH] feat: Add Object Storage service (#439) * chore: Upgrade Go version to v1.21 chore: Upgrade Go version to v1.21 chore: upgrade Go to v1.21 ci: Set go version 1.21 in GitHub Actions ci: Upgrade actions version (checkout, setup-go, ci-lint) ci: Upgrade golangci-lint v1.59 * chore: Install AWS SDK for Object Storage API * feat: Add S3 Client configuration * feat: Add Object Storage Resources * feat: Add objectstorage_bucket resource * feat: Add objectstorage_bucket_acl resource * feat: Add objectstorage_object resource * feat: Add objectstorage_object_acl resource * fix: add "_bucket" suffix to bucket resource * feat: Add examples of object storage * docs: Add initial documentation of object storage * feat: add type casting function for nested listValue * feat: Add BucketRegion to bucket chema * fix: Add detailed schema of grants & type casting method * chore: Add error exception * feat: Add regex validator for resource id * chore: remove redundant EOL * fix: Remove BucketName, BucketRegion from schema(doesn't provided) * refactor: Add TrimForIDParsing util * feat: Add temporary file creation logic * feat: Add aclOptions for testing * fix: Removed Unavailable object acl * feat: Add Bucket data source * fix: Add new bucket creation for independant testing * fix: aws.String => ncloud.String * chore: fix typo * chore: change string value with constants * refactor: expand TrimForParsing to parse vairous strings (not only ID) * feat: use region info from initial configuration * refactor: Set bucket id same as bucket name * fix: Add detailed string validator with regex for bucket_name * feat: Add NewObjectDataSource data source * refactor: Move ObjectIDParser to object.go * refactor: Set Object Resource ID with new format * feat: set independent object testing * feat: Add Owner info & Creation Date to bucket data source * feat: Add OwnerID & OwnerDisplayname at object_acl attribute * fix: set bucket_id & object_id w/o NRN * docs: Add documentation of object storage resources * feat: Change format - make resource id with '/' in object storage - object * feat: Add objectstorage_object_copy * fix: Change to do not use pointer method * docs: Add data source documentation of bucket * docs: Add object_storage_object.md in data-source * docs: Add object_storage_object_copy.md * feat: Add CreationDate attribute to bucket * fix: fix logic to check whether output is nil * fix: Add format of creationDate * feat: Delete panic from object_copy Update * feat: Remove endpoint from Config struct * fix: Remove Optional keyword from creation_date(computed) * refactor: set bucket unique value from bucket_id to bucket_name * fix: Remove unused headbucket * feat: Add bucket ID (since it uses id for default) * feat: Add attributes based on HeadObjectOutput * feat: Add endpoint abstract logic * refactor: Move post fetch logic into refreshFromOutput * feat: Add genEndpointWithCode (region, site) * feat: Add optional endpoint configuration * feat: Add region formatting logic * feat: Add Optional variables compatible with aws sdk in object-kind resource * docs: Update docs with additional attributes * fix: Remove Object Encryption logic entirely * feat: Add site variable in example tf code * chore: Add Error with Diagnostics * refactor: remove endpoint from providerConfig * chore: add missed packages * fix: fix endpoint env to optional * fix: Parse bucket info after pulling plan data * refactor: Abstract BucketNameValidator * fix: Remove optional from creation_date * fix: Change serverless environment => platform independent * fix: change id with object_url * feat: Add attribute documentation * feat: Add update logic to object_acl * feat: Add update logic to bucket_acl * fix: Fix typo Revert "fix: Fix typo" This reverts commit 37c773743b0f01e70fd675a16f3b77592117dcf2. fix: Fix typo --- docs/data-sources/object_storage_bucket.md | 33 ++ docs/data-sources/object_storage_object.md | 46 ++ docs/resources/object_storage_bucket.md | 54 +++ docs/resources/object_storage_bucket_acl.md | 61 +++ docs/resources/object_storage_object.md | 82 ++++ docs/resources/object_storage_object_acl.md | 68 +++ docs/resources/object_storage_object_copy.md | 91 ++++ examples/object_storage/main.tf | 27 ++ examples/object_storage/variables.tf | 15 + examples/object_storage/versions.tf | 8 + go.mod | 82 ++-- go.sum | 217 +++++---- internal/conn/api_client.go | 55 +++ internal/conn/config.go | 8 +- internal/provider/fwprovider/provider.go | 8 + internal/provider/provider.go | 11 +- .../objectstorage/objectstorage_bucket.go | 289 +++++++++++ .../objectstorage/objectstorage_bucket_acl.go | 382 +++++++++++++++ .../objectstorage_bucket_acl_test.go | 87 ++++ .../objectstorage_bucket_data_source.go | 123 +++++ .../objectstorage_bucket_data_source_test.go | 43 ++ .../objectstorage_bucket_test.go | 94 ++++ .../objectstorage/objectstorage_object.go | 449 ++++++++++++++++++ .../objectstorage/objectstorage_object_acl.go | 380 +++++++++++++++ .../objectstorage_object_acl_test.go | 100 ++++ .../objectstorage_object_copy.go | 415 ++++++++++++++++ .../objectstorage_object_copy_test.go | 123 +++++ .../objectstorage_object_data_source.go | 211 ++++++++ .../objectstorage_object_data_source_test.go | 56 +++ .../objectstorage_object_test.go | 129 +++++ 30 files changed, 3631 insertions(+), 116 deletions(-) create mode 100644 docs/data-sources/object_storage_bucket.md create mode 100644 docs/data-sources/object_storage_object.md create mode 100644 docs/resources/object_storage_bucket.md create mode 100644 docs/resources/object_storage_bucket_acl.md create mode 100644 docs/resources/object_storage_object.md create mode 100644 docs/resources/object_storage_object_acl.md create mode 100644 docs/resources/object_storage_object_copy.md create mode 100644 examples/object_storage/main.tf create mode 100644 examples/object_storage/variables.tf create mode 100644 examples/object_storage/versions.tf create mode 100644 internal/conn/api_client.go create mode 100644 internal/service/objectstorage/objectstorage_bucket.go create mode 100644 internal/service/objectstorage/objectstorage_bucket_acl.go create mode 100644 internal/service/objectstorage/objectstorage_bucket_acl_test.go create mode 100644 internal/service/objectstorage/objectstorage_bucket_data_source.go create mode 100644 internal/service/objectstorage/objectstorage_bucket_data_source_test.go create mode 100644 internal/service/objectstorage/objectstorage_bucket_test.go create mode 100644 internal/service/objectstorage/objectstorage_object.go create mode 100644 internal/service/objectstorage/objectstorage_object_acl.go create mode 100644 internal/service/objectstorage/objectstorage_object_acl_test.go create mode 100644 internal/service/objectstorage/objectstorage_object_copy.go create mode 100644 internal/service/objectstorage/objectstorage_object_copy_test.go create mode 100644 internal/service/objectstorage/objectstorage_object_data_source.go create mode 100644 internal/service/objectstorage/objectstorage_object_data_source_test.go create mode 100644 internal/service/objectstorage/objectstorage_object_test.go diff --git a/docs/data-sources/object_storage_bucket.md b/docs/data-sources/object_storage_bucket.md new file mode 100644 index 000000000..38e9f6bb1 --- /dev/null +++ b/docs/data-sources/object_storage_bucket.md @@ -0,0 +1,33 @@ +--- +subcategory: "Object Storage" +--- + +# Data Source: ncloud_objectstorage_bucket + +Prvides information about a bucket. + +~> **NOTE:** This resource is platform independent. Does not need VPC configuration. + +## Example Usage + +```terraform +data "ncloud_objectstorage_bucket" "test-bucket" { + bucket_name = "your-bucket" +} +``` + +## Argument Reference + +The following arguments are required: + +* `id` - Unique ID for bucket. Since bucket name is already unique in specific region, ID is same as `bucket_name`. +* `bucket_name` - (Required) Bucket name to create. Bucket name must be between 3 and 63 characters long, can contain lowercase letters, numbers, periods, and hyphens. It must start and end with a letter or number, and cannot have consecutive periods. +* `creation_date` - Date of when this bucket created. + +## Attribute Reference + +This data source exports the following attributes in addition to the arguments above: + +* `owner_id` - ID of target bucket owner. +* `owner_displayname` - Display name of target bucket owner. +* `creation_date` - Date information of when this bucket created. \ No newline at end of file diff --git a/docs/data-sources/object_storage_object.md b/docs/data-sources/object_storage_object.md new file mode 100644 index 000000000..5c69a8b65 --- /dev/null +++ b/docs/data-sources/object_storage_object.md @@ -0,0 +1,46 @@ +--- +subcategory: "Object Storage" +--- + +# Data Source: ncloud_objectstorage_object + +Prvides information about a object. + +~> **NOTE:** This resource is platform independent. Does not need VPC configuration. + +## Example Usage + +```terraform +data "ncloud_objectstorage_object" "test" { + object_id = ncloud_objectstorage_object.testing_object.id +} +``` + +## Argument Reference + +The following arguments are required: + +* `object_id` - (Required) Object id to get. same as "\${bucket_name}/${object_key}". + +## Attribute Reference + +~> **NOTE:** Since Ncloud Object Stroage uses S3 Compatible SDK, these arguments are served as best-effort. + +This data source exports the following attributes in addition to the arguments above: + +* `bucket` - Name of bucket where object belongs. +* `key` - Key of object. +* `source` - Path that informs where does object is located in bucket. +* `content_language` - Language the content is in e.g., en-US or en-GB. +* `content_length` - How long the object is. +* `content_type` - Type of the object. +* `body` - Saved content of the object. +* `bucket_key_enabled` - Whether this resource uses Ncloud KMS Keys for SSE. +* `content_encoding` - Content encodings that have been applied to the object and thus what decoding mechanisms must be applied to obtain the media-type referenced by the Content-Type header field. Read [w3c content encoding](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11) for further information. +* `accept_ranges` - Indicates that a range of bytes was specified. +* `etag` - ETag generated for the object (an MD5 sum of the object content). For plaintext objects or objects encrypted with an AWS-managed key, the hash is an MD5 digest of the object data. For objects encrypted with a KMS key or objects created by either the Multipart Upload or Part Copy operation, the hash is not an MD5 digest, regardless of the method of encryption. More information on possible values can be found on [Common Response Headers](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html). +* `expiration` - the object expiration is configured, the response includes this header. It includes the expiry-date and rule-id key-value pairs providing object expiration information. The value of the rule-id is URL-encoded. +* `last_modified` - Date and time when the object was last modified. +* `parts_count` - The count of parts this object has. This value is only returned if you specify partNumber in your request and the object was uploaded as a multipart upload. +* `version_id` - Unique version ID value for the object, if bucket versioning is enabled. +* `website_redirect_location` - Target URL for website redirect. \ No newline at end of file diff --git a/docs/resources/object_storage_bucket.md b/docs/resources/object_storage_bucket.md new file mode 100644 index 000000000..28bb69822 --- /dev/null +++ b/docs/resources/object_storage_bucket.md @@ -0,0 +1,54 @@ +--- +subcategory: "Object Storage" +--- + + +# Resource: ncloud_objectstorage_bucket + +Provides Object Storage Bucket service resource. + +~> **NOTE:** This resource is platform independent. Does not need VPC configuration. + +## Example Usage + +```terraform +provider "ncloud" { + support_vpc = true + access_key = var.access_key + secret_key = var.secret_key + region = var.region +} + +resource "ncloud_objectstorage_bucket" "testing_bucket" { + bucket_name = "your-bucket-name" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `id` - Unique ID for bucket. Since bucket name is already unique in specific region, ID is same as `bucket_name`. +* `bucket_name` - (Required) Bucket name to create. Bucket name must be between 3 and 63 characters long, can contain lowercase letters, numbers, periods, and hyphens. It must start and end with a letter or number, and cannot have consecutive periods. +* `creation_date` - Date of when this bucket created. + +## Import + +### `terraform import` command + +* Object Storage Bucket can be imported using the `bucket_name`. For example: + +```console +$ terraform import ncloud_objectstorage_object.rsc_name bucket-name +``` + +### `import` block + +* In Terraform v1.5.0 and later, use a [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Object Storage Bucket using the `id`. For example: + +```terraform +import { + to = ncloud_objectstorage_object.rsc_name + bucket_name = "bucket-name" +} +``` \ No newline at end of file diff --git a/docs/resources/object_storage_bucket_acl.md b/docs/resources/object_storage_bucket_acl.md new file mode 100644 index 000000000..9336974c2 --- /dev/null +++ b/docs/resources/object_storage_bucket_acl.md @@ -0,0 +1,61 @@ +--- +subcategory: "Object Storage" +--- + + +# Resource: ncloud_objectstorage_bucket_acl + +Provides Object Storage Bucket ACL service resource. + +~> **NOTE:** This resource is platform independent. Does not need VPC configuration. + +## Example Usage + +```terraform +provider "ncloud" { + support_vpc = true + access_key = var.access_key + secret_key = var.secret_key + region = var.region +} + +resource "ncloud_objectstorage_bucket" "testing_bucket" { + bucket_name = "your-bucket-name" +} + +resource "ncloud_objectstorage_bucket_acl" "testing_acl" { + bucket_name = ncloud_objectstorage_bucket.testing_bucket.bucket_name + rule = "RULL_TO_APPLY" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `id` - Unique ID for bucket. Has format of `bucket_acl_${bucket_name}`. +* `bucket_name` - (Required) Target bucket id to create(same as bucket name). Bucket name must be between 3 and 63 characters long, can contain lowercase letters, numbers, periods, and hyphens. It must start and end with a letter or number, and cannot have consecutive periods. +* `rule` - (Required) Rule to apply. Value must be one of "private", "public-read", "public-read-write", "authenticated-read". +* `grants` - List of member who grants this rule. Consists of `grantee`, `permission`. Individual `grantee` has `type`, `display_name`, `email-address`, `id`, `uri` attributes. +* `owner` - Who owns this ACL. + +## Import + +### `terraform import` command + +* Object Storage Bucket ACL can be imported using the `bucket_name`. For example: + +```console +$ terraform import ncloud_objectstorage_bucket_acl.rsc_name bucket_acl_bucket-name +``` + +### `import` block + +* In Terraform v1.5.0 and later, use a [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Object Storage Bucket ACL using the `id`. For example: + +```terraform +import { + to = ncloud_objectstorage_bucket_acl.rsc_name + bucket_name = "bucket_acl_bucket-name" +} +``` \ No newline at end of file diff --git a/docs/resources/object_storage_object.md b/docs/resources/object_storage_object.md new file mode 100644 index 000000000..0af2fb2ad --- /dev/null +++ b/docs/resources/object_storage_object.md @@ -0,0 +1,82 @@ +--- +subcategory: "Object Storage" +--- + + +# Resource: ncloud_objectstorage_object + +Provides Object Storage Object service resource. + +~> **NOTE:** This resource is platform independent. Does not need VPC configuration. + +## Example Usage + +```terraform +provider "ncloud" { + support_vpc = true + access_key = var.access_key + secret_key = var.secret_key + region = var.region +} + +resource "ncloud_objectstorage_bucket" "testing_bucket" { + bucket_name = "your-bucket-name" +} + +resource "ncloud_objectstorage_object" "testing_object" { + bucket = ncloud_objectstorage_bucket.testing_bucket.bucket_name + key = "your-object-key" + source = "path/to/file" +} +``` + +## Argument Reference + +The following arguments are required: + +* `bucket` - (Required) Name of the bucket to read the object from. Bucket name must be between 3 and 63 characters long, can contain lowercase letters, numbers, periods, and hyphens. It must start and end with a letter or number, and cannot have consecutive periods. +* `key` - (Required) Full path to the object inside the bucket. +* `source` - (Required) Path to the file you want to upload. + +The following arguments are optional: + +* `bucket_key_enabled` - (Optional) Whether this resource uses Ncloud KMS Keys for SSE. +* `content_encoding` - (Optional) Content encodings that have been applied to the object and thus what decoding mechanisms must be applied to obtain the media-type referenced by the Content-Type header field. Read [w3c content encoding](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11) for further information. +* `content_language` - (Optional) Language the content is in e.g., en-US or en-GB. +* `content_type` - (Optional) Standard MIME type describing the format of the object data, e.g., application/octet-stream. All Valid MIME Types are valid for this input. +* `website_redirect_location` - (Optional) Target URL for website redirect. + +## Attribute Reference. + +~> **NOTE:** Since Ncloud Object Stroage uses S3 Compatible SDK, these arguments are served as best-effort. + +This resource exports the following attributes in addition to the arguments above: + +* `accept_ranges` - Indicates that a range of bytes was specified. +* `content_length` - Size of the body in bytes. +* `etag` - ETag generated for the object (an MD5 sum of the object content). For plaintext objects or objects encrypted with an AWS-managed key, the hash is an MD5 digest of the object data. For objects encrypted with a KMS key or objects created by either the Multipart Upload or Part Copy operation, the hash is not an MD5 digest, regardless of the method of encryption. More information on possible values can be found on [Common Response Headers](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html). +* `expiration` - the object expiration is configured, the response includes this header. It includes the expiry-date and rule-id key-value pairs providing object expiration information. The value of the rule-id is URL-encoded. +* `last_modified` - Date and time when the object was last modified. +* `parts_count` - The count of parts this object has. This value is only returned if you specify partNumber in your request and the object was uploaded as a multipart upload. +* `version_id` - Unique version ID value for the object, if bucket versioning is enabled. + +## Import + +### `terraform import` command + +* Object Storage Object can be imported using the `id`. For example: + +```console +$ terraform import ncloud_objectstorage_object.rsc_name bucket-name/key +``` + +### `import` block + +* In Terraform v1.5.0 and later, use a [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Object Storage Object using the `id`. For example: + +```terraform +import { + to = ncloud_objectstorage_object.rsc_name + id = "bucket-name/key" +} +``` \ No newline at end of file diff --git a/docs/resources/object_storage_object_acl.md b/docs/resources/object_storage_object_acl.md new file mode 100644 index 000000000..2a311671d --- /dev/null +++ b/docs/resources/object_storage_object_acl.md @@ -0,0 +1,68 @@ +--- +subcategory: "Object Storage" +--- + + +# Resource: ncloud_objectstorage_object_acl + +Provides Object Storage Object ACL service resource. + +~> **NOTE:** This resource is platform independent. Does not need VPC configuration. + +## Example Usage + +```terraform +provider "ncloud" { + support_vpc = true + access_key = var.access_key + secret_key = var.secret_key + region = var.region +} + +resource "ncloud_objectstorage_bucket" "testing_bucket" { + bucket_name = "your-bucket-name" +} + +resource "ncloud_objectstorage_object" "testing_object" { + bucket = ncloud_objectstorage_bucket.testing_bucket.bucket_name + key = "your-object-key" + source = "path/to/file" +} + +resource "ncloud_objectstorage_object_acl" "testing_acl" { + object_id = ncloud_objectstorage_object.testing_object.id + rule = "RULL_TO_APPLY" +} +``` + +## Argument Reference + +The following arguments are supported: + +* `id` - Unique ID for ACL. Has format of `object_acl_${object_id}`. +* `object_id` - (Required) Target object id to create. +* `rule` - (Required) Rule to apply. Value must be one of "private", "public-read", "public-read-write", "authenticated-read". +* `grants` - List of member who grants this rule. Consists of `grantee`, `permission`. Individual `grantee` has `type`, `display_name`, `email-address`, `id`, `uri` attributes. +* `owner_id` - ID of owner. +* `owner_displayname` - Name of owner. + +## Import + +### `terraform import` command + +* Object Storage Object ACL can be imported using the `id`. For example: + +```console +$ terraform import ncloud_objectstorage_object_acl.rsc_name object_acl_objectID +``` + +### `import` block + +* In Terraform v1.5.0 and later, use a [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Object Storage Bucket ACL using the `id`. For example: + +```terraform +import { + to = ncloud_objectstorage_object_acl.rsc_name + id = "object_acl_objectID" +} +``` \ No newline at end of file diff --git a/docs/resources/object_storage_object_copy.md b/docs/resources/object_storage_object_copy.md new file mode 100644 index 000000000..ec9406efc --- /dev/null +++ b/docs/resources/object_storage_object_copy.md @@ -0,0 +1,91 @@ +--- +subcategory: "Object Storage" +--- + + +# Resource: ncloud_objectstorage_object_copy + +Provides Object Storage Object Copy service resource. + +~> **NOTE:** This resource is platform independent. Does not need VPC configuration. + +## Example Usage + +```terraform +provider "ncloud" { + support_vpc = true + access_key = var.access_key + secret_key = var.secret_key + region = var.region +} + + resource "ncloud_objectstorage_bucket" "testing_bucket_from" { + bucket_name = "test-from" + } + + resource "ncloud_objectstorage_bucket" "testing_bucket_to" { + bucket_name = "test-to" + } + + resource "ncloud_objectstorage_object" "testing_object" { + bucket = ncloud_objectstorage_bucket.testing_bucket_from.bucket_name + key = "test-key.md" + source = "path/to/file" + } + + resource "ncloud_objectstorage_object_copy" "testing_copy" { + bucket = ncloud_objectstorage_bucket.testing_bucket_to.bucket_name + key = "test-key.md" + source = ncloud_objectstorage_object.testing_object.id + } +``` + +## Argument Reference + +The following arguments are required: + +* `bucket` - (Required) Name of the bucket to read the object from. Bucket name must be between 3 and 63 characters long, can contain lowercase letters, numbers, periods, and hyphens. It must start and end with a letter or number, and cannot have consecutive periods. +* `key` - (Required) Full path to the object inside the bucket. +* `source` - (Required) Path to the file you want to upload. + +The following arguments are supported: + +* `content_encoding` - (Optional) Content encodings that have been applied to the object and thus what decoding mechanisms must be applied to obtain the media-type referenced by the Content-Type header field. Read [w3c content encoding](https://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.11) for further information. +* `content_language` - (Optional) Language the content is in e.g., en-US or en-GB. +* `content_type` - (Optional) Standard MIME type describing the format of the object data, e.g., application/octet-stream. All Valid MIME Types are valid for this input. +* `website_redirect_location` - (Optional) Target URL for website redirect. + +## Attribute Reference. + +~> **NOTE:** Since Ncloud Object Stroage uses S3 Compatible SDK, these arguments are served as best-effort. + +This resource exports the following attributes in addition to the arguments above: + +* `accept_ranges` - Indicates that a range of bytes was specified. +* `content_length` - Size of the body in bytes. +* `etag` - ETag generated for the object (an MD5 sum of the object content). For plaintext objects or objects encrypted with an AWS-managed key, the hash is an MD5 digest of the object data. For objects encrypted with a KMS key or objects created by either the Multipart Upload or Part Copy operation, the hash is not an MD5 digest, regardless of the method of encryption. More information on possible values can be found on [Common Response Headers](https://docs.aws.amazon.com/AmazonS3/latest/API/RESTCommonResponseHeaders.html). +* `expiration` - the object expiration is configured, the response includes this header. It includes the expiry-date and rule-id key-value pairs providing object expiration information. The value of the rule-id is URL-encoded. +* `last_modified` - Date and time when the object was last modified. +* `parts_count` - The count of parts this object has. This value is only returned if you specify partNumber in your request and the object was uploaded as a multipart upload. +* `version_id` - Unique version ID value for the object, if bucket versioning is enabled. + +## Import + +### `terraform import` command + +* Object Storage Object can be imported using the `id`. For example: + +```console +$ terraform import ncloud_objectstorage_object_copy.rsc_name bucket-name/key +``` + +### `import` block + +* In Terraform v1.5.0 and later, use a [`import` block](https://developer.hashicorp.com/terraform/language/import) to import Object Storage Object using the `id`. For example: + +```terraform +import { + to = ncloud_objectstorage_object_copy.rsc_name + id = "bucket-name/key" +} +``` \ No newline at end of file diff --git a/examples/object_storage/main.tf b/examples/object_storage/main.tf new file mode 100644 index 000000000..8a43e2a03 --- /dev/null +++ b/examples/object_storage/main.tf @@ -0,0 +1,27 @@ +provider "ncloud" { + support_vpc = true + access_key = var.access_key + secret_key = var.secret_key + site = var.site # if needed + region = var.region +} + +resource "ncloud_objectstorage_bucket" "bucket" { + bucket_name = "bucket" +} + +resource "ncloud_objectstorage_object" "object" { + bucket = ncloud_objectstorage.bucket.bucket_name + key = "hello.md" + source = "path/to/file" +} + +resource "ncloud_objectstorage_bucket_acl" "bucket_acl" { + bucket_name = ncloud_objectstorage.bucket.bucket_name + rule = "private" +} + +resource "ncloud_objectstorage_object_acl" "object_acl" { + object_id = ncloud_objectstorage.object.id + rule = "public-read-write" +} \ No newline at end of file diff --git a/examples/object_storage/variables.tf b/examples/object_storage/variables.tf new file mode 100644 index 000000000..d3018a377 --- /dev/null +++ b/examples/object_storage/variables.tf @@ -0,0 +1,15 @@ +variable "access_key" { + description = "access_key, provide through environment variables" +} + +variable "secret_key" { + description = "secret_key, provide through environment variables" +} + +variable "site" { + default = "" +} + +variable "region" { + default = "KR" +} \ No newline at end of file diff --git a/examples/object_storage/versions.tf b/examples/object_storage/versions.tf new file mode 100644 index 000000000..994e0bf60 --- /dev/null +++ b/examples/object_storage/versions.tf @@ -0,0 +1,8 @@ +terraform { + required_providers { + ncloud = { + source = "navercloudplatform/ncloud" + } + } + required_version = ">= 0.13" +} \ No newline at end of file diff --git a/go.mod b/go.mod index 5cb14d795..9d9620ee6 100644 --- a/go.mod +++ b/go.mod @@ -4,46 +4,67 @@ go 1.21 require ( github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.6.11 + github.com/aws/aws-sdk-go-v2/config v1.27.27 + github.com/aws/aws-sdk-go-v2/credentials v1.17.27 + github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 github.com/hashicorp/go-multierror v1.1.1 - github.com/hashicorp/terraform-plugin-framework v1.3.2 - github.com/hashicorp/terraform-plugin-go v0.17.0 + github.com/hashicorp/terraform-plugin-framework v1.11.0 + github.com/hashicorp/terraform-plugin-go v0.23.0 github.com/hashicorp/terraform-plugin-log v0.9.0 - github.com/hashicorp/terraform-plugin-mux v0.11.1 - github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 + github.com/hashicorp/terraform-plugin-mux v0.16.0 + github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 gopkg.in/yaml.v3 v3.0.1 ) require ( - github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 // indirect + github.com/ProtonMail/go-crypto v1.1.0-alpha.2 // indirect + github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect + github.com/aws/aws-sdk-go-v2 v1.30.3 // indirect + github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 // indirect + github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 // indirect + github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 // indirect + github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 // indirect + github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 // indirect + github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 // indirect + github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 // indirect + github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 // indirect + github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 // indirect + github.com/aws/smithy-go v1.20.3 // indirect github.com/cloudflare/circl v1.3.7 // indirect github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 // indirect - golang.org/x/mod v0.10.0 // indirect + golang.org/x/mod v0.17.0 // indirect + golang.org/x/sync v0.7.0 // indirect + golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de // indirect ) require ( github.com/agext/levenshtein v1.2.2 // indirect - github.com/apparentlymart/go-textseg/v13 v13.0.0 // 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 - github.com/hashicorp/errwrap v1.0.0 // indirect + github.com/fatih/color v1.17.0 // indirect + github.com/golang/protobuf v1.5.4 // indirect + github.com/google/go-cmp v0.6.0 // indirect + github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-checkpoint v0.5.0 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect - github.com/hashicorp/go-hclog v1.5.0 // indirect - github.com/hashicorp/go-plugin v1.4.10 // indirect + github.com/hashicorp/go-hclog v1.6.3 // indirect + github.com/hashicorp/go-plugin v1.6.0 // indirect github.com/hashicorp/go-uuid v1.0.3 // indirect github.com/hashicorp/go-version v1.6.0 // indirect - github.com/hashicorp/hc-install v0.5.2 // indirect - github.com/hashicorp/hcl/v2 v2.17.0 // indirect + github.com/hashicorp/hc-install v0.6.4 // indirect + github.com/hashicorp/hcl/v2 v2.20.1 // indirect github.com/hashicorp/logutils v1.0.0 // indirect - github.com/hashicorp/terraform-exec v0.18.1 // indirect - github.com/hashicorp/terraform-json v0.17.0 // indirect + github.com/hashicorp/terraform-exec v0.21.0 // indirect + github.com/hashicorp/terraform-json v0.22.1 // indirect github.com/hashicorp/terraform-plugin-framework-validators v0.10.0 - github.com/hashicorp/terraform-registry-address v0.2.1 // indirect + github.com/hashicorp/terraform-registry-address v0.2.3 // indirect github.com/hashicorp/terraform-svchost v0.1.1 // indirect - github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d // indirect - github.com/mattn/go-colorable v0.1.12 // indirect - github.com/mattn/go-isatty v0.0.14 // indirect + github.com/hashicorp/yamux v0.1.1 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mitchellh/copystructure v1.2.0 // indirect github.com/mitchellh/go-testing-interface v1.14.1 // indirect github.com/mitchellh/go-wordwrap v1.0.0 // indirect @@ -51,15 +72,14 @@ require ( github.com/mitchellh/reflectwalk v1.0.2 // indirect github.com/oklog/run v1.0.0 // indirect github.com/vmihailenco/msgpack v4.0.4+incompatible // indirect - github.com/vmihailenco/msgpack/v5 v5.3.5 // indirect + github.com/vmihailenco/msgpack/v5 v5.4.1 // indirect github.com/vmihailenco/tagparser/v2 v2.0.0 // indirect - github.com/zclconf/go-cty v1.13.2 // indirect - golang.org/x/crypto v0.21.0 // indirect - golang.org/x/net v0.23.0 // indirect - golang.org/x/sys v0.18.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/appengine v1.6.7 // indirect - google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect - google.golang.org/grpc v1.56.3 // indirect - google.golang.org/protobuf v1.33.0 // indirect + github.com/zclconf/go-cty v1.14.4 // indirect + golang.org/x/crypto v0.24.0 // indirect + golang.org/x/net v0.26.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + google.golang.org/appengine v1.6.8 // indirect + google.golang.org/grpc v1.63.2 // indirect + google.golang.org/protobuf v1.34.0 // indirect ) diff --git a/go.sum b/go.sum index d4c3dc383..f8f29f746 100644 --- a/go.sum +++ b/go.sum @@ -1,47 +1,91 @@ -github.com/Microsoft/go-winio v0.5.2 h1:a9IhgEQBCUEk6QCdml9CiJGhAws+YwffDHEMp1VMrpA= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= +dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= +dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= +github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.6.11 h1:5+mB3hzj2gRJV12AvIW8eV8S9xdWey9F+YkuIFDLVV0= github.com/NaverCloudPlatform/ncloud-sdk-go-v2 v1.6.11/go.mod h1:jRp8KZ64MUevBWNqehghhG2oF5/JU3Dmt/Cu7dp1mQE= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8 h1:wPbRQzjjwFc0ih8puEVAOFGELsn1zoIIYdxvML7mDxA= -github.com/ProtonMail/go-crypto v0.0.0-20230217124315-7d5c6f04bbb8/go.mod h1:I0gYDMZ6Z5GRU7l58bNFSkPTFN6Yl12dsUlAZ8xy98g= +github.com/ProtonMail/go-crypto v1.1.0-alpha.2 h1:bkyFVUP+ROOARdgCiJzNQo2V2kiB97LyUpzH9P6Hrlg= +github.com/ProtonMail/go-crypto v1.1.0-alpha.2/go.mod h1:rA3QumHc/FZ8pAHreoekgiAbzpNsfQAosU5td4SnOrE= github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= github.com/agext/levenshtein v1.2.2 h1:0S/Yg6LYmFJ5stwQeRp6EeOcCbj7xiqQSdNelsXvaqE= github.com/agext/levenshtein v1.2.2/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/apparentlymart/go-textseg/v12 v12.0.0/go.mod h1:S/4uRK2UtaQttw1GenVJEynmyUenKwP++x/+DdGV/Ec= -github.com/apparentlymart/go-textseg/v13 v13.0.0 h1:Y+KvPE1NYz0xl601PVImeQfFyEy6iT90AvPUL1NNfNw= -github.com/apparentlymart/go-textseg/v13 v13.0.0/go.mod h1:ZK2fH7c4NqDTLtiYLvIkEghdlcqw7yxLeM89kiTRPUo= -github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= +github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY= +github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4= +github.com/aws/aws-sdk-go-v2 v1.30.3 h1:jUeBtG0Ih+ZIFH0F4UkmL9w3cSpaMv9tYYDbzILP8dY= +github.com/aws/aws-sdk-go-v2 v1.30.3/go.mod h1:nIQjQVp5sfpQcTc9mPSr1B0PaWK5ByX9MOoDadSN4lc= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3 h1:tW1/Rkad38LA15X4UQtjXZXNKsCgkshC3EbmcUmghTg= +github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.6.3/go.mod h1:UbnqO+zjqk3uIt9yCACHJ9IVNhyhOCnYk8yA19SAWrM= +github.com/aws/aws-sdk-go-v2/config v1.27.27 h1:HdqgGt1OAP0HkEDDShEl0oSYa9ZZBSOmKpdpsDMdO90= +github.com/aws/aws-sdk-go-v2/config v1.27.27/go.mod h1:MVYamCg76dFNINkZFu4n4RjDixhVr51HLj4ErWzrVwg= +github.com/aws/aws-sdk-go-v2/credentials v1.17.27 h1:2raNba6gr2IfA0eqqiP2XiQ0UVOpGPgDSi0I9iAP+UI= +github.com/aws/aws-sdk-go-v2/credentials v1.17.27/go.mod h1:gniiwbGahQByxan6YjQUMcW4Aov6bLC3m+evgcoN4r4= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11 h1:KreluoV8FZDEtI6Co2xuNk/UqI9iwMrOx/87PBNIKqw= +github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.16.11/go.mod h1:SeSUYBLsMYFoRvHE0Tjvn7kbxaUhl75CJi1sbfhMxkU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15 h1:SoNJ4RlFEQEbtDcCEt+QG56MY4fm4W8rYirAmq+/DdU= +github.com/aws/aws-sdk-go-v2/internal/configsources v1.3.15/go.mod h1:U9ke74k1n2bf+RIgoX1SXFed1HLs51OgUSs+Ph0KJP8= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15 h1:C6WHdGnTDIYETAm5iErQUiVNsclNx9qbJVPIt03B6bI= +github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.6.15/go.mod h1:ZQLZqhcu+JhSrA9/NXRm8SkDvsycE+JkV3WGY41e+IM= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0 h1:hT8rVHwugYE2lEfdFE0QWVo81lF7jMrYJVDWI+f+VxU= +github.com/aws/aws-sdk-go-v2/internal/ini v1.8.0/go.mod h1:8tu/lYfQfFe6IGnaOdrpVgEL2IrrDOf6/m9RQum4NkY= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15 h1:Z5r7SycxmSllHYmaAZPpmN8GviDrSGhMS6bldqtXZPw= +github.com/aws/aws-sdk-go-v2/internal/v4a v1.3.15/go.mod h1:CetW7bDE00QoGEmPUoZuRog07SGVAUVW6LFpNP0YfIg= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3 h1:dT3MqvGhSoaIhRseqw2I0yH81l7wiR2vjs57O51EAm8= +github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.11.3/go.mod h1:GlAeCkHwugxdHaueRr4nhPuY+WW+gR8UjlcqzPr1SPI= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17 h1:YPYe6ZmvUfDDDELqEKtAd6bo8zxhkm+XEFEzQisqUIE= +github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.3.17/go.mod h1:oBtcnYua/CgzCWYN7NZ5j7PotFDaFSUjCYVTtfyn7vw= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17 h1:HGErhhrxZlQ044RiM+WdoZxp0p+EGM62y3L6pwA4olE= +github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.11.17/go.mod h1:RkZEx4l0EHYDJpWppMJ3nD9wZJAa8/0lq9aVC+r2UII= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15 h1:246A4lSTXWJw/rmlQI+TT2OcqeDMKBdyjEQrafMaQdA= +github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.17.15/go.mod h1:haVfg3761/WF7YPuJOER2MP0k4UAXyHaLclKXB6usDg= +github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3 h1:hT8ZAZRIfqBqHbzKTII+CIiY8G2oC9OpLedkZ51DWl8= +github.com/aws/aws-sdk-go-v2/service/s3 v1.58.3/go.mod h1:Lcxzg5rojyVPU/0eFwLtcyTaek/6Mtic5B1gJo7e/zE= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.4 h1:BXx0ZIxvrJdSgSvKTZ+yRBeSqqgPM89VPlulEcl37tM= +github.com/aws/aws-sdk-go-v2/service/sso v1.22.4/go.mod h1:ooyCOXjvJEsUw7x+ZDHeISPMhtwI3ZCB7ggFMcFfWLU= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4 h1:yiwVzJW2ZxZTurVbYWA7QOrAaCYQR72t0wrSBfoesUE= +github.com/aws/aws-sdk-go-v2/service/ssooidc v1.26.4/go.mod h1:0oxfLkpz3rQ/CHlx5hB7H69YUpFiI1tql6Q6Ne+1bCw= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3 h1:ZsDKRLXGWHk8WdtyYMoGNO7bTudrvuKpDKgMVRlepGE= +github.com/aws/aws-sdk-go-v2/service/sts v1.30.3/go.mod h1:zwySh8fpFyXp9yOr/KVzxOl8SRqgf/IDw5aUt9UKFcQ= +github.com/aws/smithy-go v1.20.3 h1:ryHwveWzPV5BIof6fyDvor6V3iUL7nTfiTKXHiW05nE= +github.com/aws/smithy-go v1.20.3/go.mod h1:krry+ya/rV9RDcV/Q16kpu6ypI4K2czasz0NC3qS14E= +github.com/bufbuild/protocompile v0.4.0 h1:LbFKd2XowZvQ/kajzguUp2DC9UEIQhIq77fZZlaQsNA= +github.com/bufbuild/protocompile v0.4.0/go.mod h1:3v93+mbWn/v3xzN+31nwkJfrEpAUwp+BagBSZWx+TP8= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA= +github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= +github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/fatih/color v1.13.0 h1:8LOYc1KYPPmyKMuN8QV2DNRWNbLo6LZ0iLs8+mlH53w= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= -github.com/go-git/gcfg v1.5.0 h1:Q5ViNfGF8zFgyJWPqYwA7qGFoMTEiBmdlkcfRmpIMa4= -github.com/go-git/gcfg v1.5.0/go.mod h1:5m20vg6GwYabIxaOonVkTdrILxQMpEShl1xiMF4ua+E= -github.com/go-git/go-billy/v5 v5.4.1 h1:Uwp5tDRkPr+l/TnbHOQzp+tmJfLceOlbVucgpTz8ix4= -github.com/go-git/go-billy/v5 v5.4.1/go.mod h1:vjbugF6Fz7JIflbVpl1hJsGjSHNltrSw45YK/ukIvQg= -github.com/go-git/go-git/v5 v5.6.1 h1:q4ZRqQl4pR/ZJHc1L5CFjGA1a10u76aV1iC+nh+bHsk= -github.com/go-git/go-git/v5 v5.6.1/go.mod h1:mvyoL6Unz0PiTQrGQfSfiLFhBH1c1e84ylC2MDs4ee8= +github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4= +github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= +github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= +github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= +github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= +github.com/go-git/go-git/v5 v5.12.0 h1:7Md+ndsjrzZxbddRDZjF14qK+NN56sy6wkqaVrjZtys= +github.com/go-git/go-git/v5 v5.12.0/go.mod h1:FTM9VKtnI2m65hNI/TenDDDnUf2Q9FHnXYjuz9i5OEY= github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68= github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= +github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= +github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I= +github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-checkpoint v0.5.0 h1:MFYpPZCnQqQTE18jFwSII6eUQrD/oxMFp3mlgcqk5mU= github.com/hashicorp/go-checkpoint v0.5.0/go.mod h1:7nfLNL10NsxqO4iWuW6tWW0HjZuDrwkBuEQsVcpCOgg= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -49,51 +93,51 @@ github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9n github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320 h1:1/D3zfFHttUKaCaGKZ/dR2roBXv0vKbSCnssIldfQdI= github.com/hashicorp/go-cty v1.4.1-0.20200414143053-d3edf31b6320/go.mod h1:EiZBMaudVLy8fmjf9Npq1dq9RalhveqZG5w/yz3mHWs= -github.com/hashicorp/go-hclog v1.5.0 h1:bI2ocEMgcVlz55Oj1xZNBsVi900c7II+fWDyV9o+13c= -github.com/hashicorp/go-hclog v1.5.0/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= +github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k= +github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M= github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo= github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM= -github.com/hashicorp/go-plugin v1.4.10 h1:xUbmA4jC6Dq163/fWcp8P3JuHilrHHMLNRxzGQJ9hNk= -github.com/hashicorp/go-plugin v1.4.10/go.mod h1:6/1TEzT0eQznvI/gV2CM29DLSkAK/e58mUWKVsPaph0= +github.com/hashicorp/go-plugin v1.6.0 h1:wgd4KxHJTVGGqWBq4QPB1i5BZNEx9BR8+OFmHDmTk8A= +github.com/hashicorp/go-plugin v1.6.0/go.mod h1:lBS5MtSSBZk0SHc66KACcjjlU6WzEVP/8pwz68aMkCI= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.3 h1:2gKiV6YVmrJ1i2CKKa9obLvRieoRGviZFL26PcT/Co8= github.com/hashicorp/go-uuid v1.0.3/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-version v1.6.0 h1:feTTfFNnjP967rlCxM/I9g701jU+RN74YKx2mOkIeek= github.com/hashicorp/go-version v1.6.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= -github.com/hashicorp/hc-install v0.5.2 h1:SfwMFnEXVVirpwkDuSF5kymUOhrUxrTq3udEseZdOD0= -github.com/hashicorp/hc-install v0.5.2/go.mod h1:9QISwe6newMWIfEiXpzuu1k9HAGtQYgnSH8H9T8wmoI= -github.com/hashicorp/hcl/v2 v2.17.0 h1:z1XvSUyXd1HP10U4lrLg5e0JMVz6CPaJvAgxM0KNZVY= -github.com/hashicorp/hcl/v2 v2.17.0/go.mod h1:gJyW2PTShkJqQBKpAmPO3yxMxIuoXkOF2TpqXzrQyx4= +github.com/hashicorp/hc-install v0.6.4 h1:QLqlM56/+SIIGvGcfFiwMY3z5WGXT066suo/v9Km8e0= +github.com/hashicorp/hc-install v0.6.4/go.mod h1:05LWLy8TD842OtgcfBbOT0WMoInBMUSHjmDx10zuBIA= +github.com/hashicorp/hcl/v2 v2.20.1 h1:M6hgdyz7HYt1UN9e61j+qKJBqR3orTWbI1HKBJEdxtc= +github.com/hashicorp/hcl/v2 v2.20.1/go.mod h1:TZDqQ4kNKCbh1iJp99FdPiUaVDDUPivbqxZulxDYqL4= github.com/hashicorp/logutils v1.0.0 h1:dLEQVugN8vlakKOUE3ihGLTZJRB4j+M2cdTm/ORI65Y= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= -github.com/hashicorp/terraform-exec v0.18.1 h1:LAbfDvNQU1l0NOQlTuudjczVhHj061fNX5H8XZxHlH4= -github.com/hashicorp/terraform-exec v0.18.1/go.mod h1:58wg4IeuAJ6LVsLUeD2DWZZoc/bYi6dzhLHzxM41980= -github.com/hashicorp/terraform-json v0.17.0 h1:EiA1Wp07nknYQAiv+jIt4dX4Cq5crgP+TsTE45MjMmM= -github.com/hashicorp/terraform-json v0.17.0/go.mod h1:Huy6zt6euxaY9knPAFKjUITn8QxUFIe9VuSzb4zn/0o= -github.com/hashicorp/terraform-plugin-framework v1.3.2 h1:aQ6GSD0CTnvoALEWvKAkcH/d8jqSE0Qq56NYEhCexUs= -github.com/hashicorp/terraform-plugin-framework v1.3.2/go.mod h1:oimsRAPJOYkZ4kY6xIGfR0PHjpHLDLaknzuptl6AvnY= +github.com/hashicorp/terraform-exec v0.21.0 h1:uNkLAe95ey5Uux6KJdua6+cv8asgILFVWkd/RG0D2XQ= +github.com/hashicorp/terraform-exec v0.21.0/go.mod h1:1PPeMYou+KDUSSeRE9szMZ/oHf4fYUmB923Wzbq1ICg= +github.com/hashicorp/terraform-json v0.22.1 h1:xft84GZR0QzjPVWs4lRUwvTcPnegqlyS7orfb5Ltvec= +github.com/hashicorp/terraform-json v0.22.1/go.mod h1:JbWSQCLFSXFFhg42T7l9iJwdGXBYV8fmmD6o/ML4p3A= +github.com/hashicorp/terraform-plugin-framework v1.11.0 h1:M7+9zBArexHFXDx/pKTxjE6n/2UCXY6b8FIq9ZYhwfE= +github.com/hashicorp/terraform-plugin-framework v1.11.0/go.mod h1:qBXLDn69kM97NNVi/MQ9qgd1uWWsVftGSnygYG1tImM= github.com/hashicorp/terraform-plugin-framework-validators v0.10.0 h1:4L0tmy/8esP6OcvocVymw52lY0HyQ5OxB7VNl7k4bS0= github.com/hashicorp/terraform-plugin-framework-validators v0.10.0/go.mod h1:qdQJCdimB9JeX2YwOpItEu+IrfoJjWQ5PhLpAOMDQAE= -github.com/hashicorp/terraform-plugin-go v0.17.0 h1:OpqgPLvjW3vCDA9VUEmRKppCZOG/+Vkdp6ijkG8aJek= -github.com/hashicorp/terraform-plugin-go v0.17.0/go.mod h1:l7VK+2u5Kf2y+A+742GX0ouLut3gttudmvMgN0PA74Y= +github.com/hashicorp/terraform-plugin-go v0.23.0 h1:AALVuU1gD1kPb48aPQUjug9Ir/125t+AAurhqphJ2Co= +github.com/hashicorp/terraform-plugin-go v0.23.0/go.mod h1:1E3Cr9h2vMlahWMbsSEcNrOCxovCZhOOIXjFHbjc/lQ= github.com/hashicorp/terraform-plugin-log v0.9.0 h1:i7hOA+vdAItN1/7UrfBqBwvYPQ9TFvymaRGZED3FCV0= github.com/hashicorp/terraform-plugin-log v0.9.0/go.mod h1:rKL8egZQ/eXSyDqzLUuwUYLVdlYeamldAHSxjUFADow= -github.com/hashicorp/terraform-plugin-mux v0.11.1 h1:cDCrmkrNHf/c2zC1oREIMdgCYWi1QP9U/qNXeNSYoFk= -github.com/hashicorp/terraform-plugin-mux v0.11.1/go.mod h1:eMZPcv8b5y+alMeQmocgaphPj4zAnM3uXiMLd1emqMQ= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1 h1:G9WAfb8LHeCxu7Ae8nc1agZlQOSCUWsb610iAogBhCs= -github.com/hashicorp/terraform-plugin-sdk/v2 v2.26.1/go.mod h1:xcOSYlRVdPLmDUoqPhO9fiO/YCN/l6MGYeTzGt5jgkQ= -github.com/hashicorp/terraform-registry-address v0.2.1 h1:QuTf6oJ1+WSflJw6WYOHhLgwUiQ0FrROpHPYFtwTYWM= -github.com/hashicorp/terraform-registry-address v0.2.1/go.mod h1:BSE9fIFzp0qWsJUUyGquo4ldV9k2n+psif6NYkBRS3Y= +github.com/hashicorp/terraform-plugin-mux v0.16.0 h1:RCzXHGDYwUwwqfYYWJKBFaS3fQsWn/ZECEiW7p2023I= +github.com/hashicorp/terraform-plugin-mux v0.16.0/go.mod h1:PF79mAsPc8CpusXPfEVa4X8PtkB+ngWoiUClMrNZlYo= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0 h1:kJiWGx2kiQVo97Y5IOGR4EMcZ8DtMswHhUuFibsCQQE= +github.com/hashicorp/terraform-plugin-sdk/v2 v2.34.0/go.mod h1:sl/UoabMc37HA6ICVMmGO+/0wofkVIRxf+BMb/dnoIg= +github.com/hashicorp/terraform-registry-address v0.2.3 h1:2TAiKJ1A3MAkZlH1YI/aTVcLZRu7JseiXNRHbOAyoTI= +github.com/hashicorp/terraform-registry-address v0.2.3/go.mod h1:lFHA76T8jfQteVfT7caREqguFrW3c4MFSPhZB7HHgUM= github.com/hashicorp/terraform-svchost v0.1.1 h1:EZZimZ1GxdqFRinZ1tpJwVxxt49xc/S52uzrw4x0jKQ= github.com/hashicorp/terraform-svchost v0.1.1/go.mod h1:mNsjQfZyf/Jhz35v6/0LWcv26+X7JPS+buii2c9/ctc= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d h1:kJCB4vdITiW1eC1vq2e6IsrXKrZit1bv/TDYFGMp4BQ= -github.com/hashicorp/yamux v0.0.0-20181012175058-2f1d1f20f75d/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= -github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= -github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= +github.com/hashicorp/yamux v0.1.1 h1:yrQxtgseBDrq9Y652vSRDvsKCJKOUD+GzTS4Y0Y8pvE= +github.com/hashicorp/yamux v0.1.1/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= -github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= +github.com/jhump/protoreflect v1.15.1 h1:HUMERORf3I3ZdX05WaQ6MIpd/NJ434hTp5YiKgfCL6c= +github.com/jhump/protoreflect v1.15.1/go.mod h1:jD/2GMKKE6OqX8qTjhADU1e6DShO+gavG9e0Q693nKo= +github.com/imdario/mergo v0.3.13 h1:lFzP57bqS/wsqKssCGmtLAb8A0wKjLGrve2q3PPVcBk= +github.com/imdario/mergo v0.3.13/go.mod h1:4lJ1jqUDcsbIECGy0RUJAXNIhg+6ocWgb1ALK2O4oXg= github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= @@ -104,11 +148,14 @@ github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-colorable v0.1.12 h1:jF+Du6AlPIjs2BiUiQlKOX0rt3SujHxPnksPKZbaA40= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= github.com/mitchellh/go-testing-interface v1.14.1 h1:jrgshOhYAUVNMAJiKbEu7EqAwgJJ2JqpQmpLJOu07cU= @@ -125,49 +172,51 @@ github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ= -github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM= -github.com/skeema/knownhosts v1.1.0 h1:Wvr9V0MxhjRbl3f9nMnKnFfiWTJmtECJ9Njkea3ysW0= -github.com/skeema/knownhosts v1.1.0/go.mod h1:sKFq3RD6/TKZkSWn8boUbDC7Qkgcv+8XXijpFO6roag= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3 h1:n661drycOFuPLCN3Uc8sB6B/s6Z4t2xvBgU1htSHuq8= +github.com/sergi/go-diff v1.3.2-0.20230802210424-5b0b94c5c0d3/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/skeema/knownhosts v1.2.2 h1:Iug2P4fLmDw9f41PB6thxUkNUkJzB5i+1/exaj40L3A= +github.com/skeema/knownhosts v1.2.2/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= 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= -github.com/vmihailenco/msgpack/v5 v5.3.5 h1:5gO0H1iULLWGhs2H5tbAHIZTV8/cYafcFOr9znI5mJU= -github.com/vmihailenco/msgpack/v5 v5.3.5/go.mod h1:7xyJ9e+0+9SaZT0Wt1RGleJXzli6Q/V5KbhBonMG9jc= +github.com/vmihailenco/msgpack/v5 v5.4.1 h1:cQriyiUvjTwOHg8QZaPihLWeRAAVoCpE00IUPn0Bjt8= +github.com/vmihailenco/msgpack/v5 v5.4.1/go.mod h1:GaZTsDaehaPpQVyxrf5mtQlH+pc21PIudVV/E3rRQok= github.com/vmihailenco/tagparser/v2 v2.0.0 h1:y09buUbR+b5aycVFQs/g70pqKVZNBmxwAhO7/IwNM9g= github.com/vmihailenco/tagparser/v2 v2.0.0/go.mod h1:Wri+At7QHww0WTrCBeu4J6bNtoV6mEfg5OIWRZA9qds= github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zclconf/go-cty v1.13.2 h1:4GvrUxe/QUDYuJKAav4EYqdM47/kZa672LwmXFmEKT0= -github.com/zclconf/go-cty v1.13.2/go.mod h1:YKQzy/7pZ7iq2jNFzy5go57xdxdWoLLpaEp4u238AE0= +github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8= +github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b h1:FosyBZYxY34Wul7O/MSKey3txpPYyCqVO5ZyceuQJEI= +github.com/zclconf/go-cty-debug v0.0.0-20191215020915-b22d67c1ba0b/go.mod h1:ZRKQfBXbGkpdV6QMzT3rU1kSTAnfu1dO8dPKjYprgj8= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.14.0/go.mod h1:MVFd36DqK4CsrnJYDkBA3VC4m2GkXAM0PvzMCn4JQf4= -golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= -golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= +golang.org/x/crypto v0.24.0 h1:mnl8DM0o513X8fdIkmyFE/5hTYxbwYOjDS/+rK6qpRI= +golang.org/x/crypto v0.24.0/go.mod h1:Z1PMYSOR5nyMcyAVAIQSKCDwalqy85Aqn1x3Ws4L5DM= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.10.0 h1:lFO9qtOdlre5W1jxS3r/4szv2/6iXxScdzjoBMXNhYk= -golang.org/x/mod v0.10.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= +golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= +golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.17.0/go.mod h1:NxSsAGuq816PNPmqtQdLE42eU2Fs7NoRIZrHJAlaCOE= -golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= -golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg= +golang.org/x/net v0.26.0 h1:soB7SVo0PWrY4vPW/+ay0jKDNScG2X9wFeYlXIvJsOQ= +golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M= +golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -175,48 +224,52 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= -golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.13.0/go.mod h1:LTmsnFJwVN6bCy1rVCoS+qHT1HhALEFxKncY3WNNh4U= -golang.org/x/term v0.18.0 h1:FcHjZXDMxI8mM3nwhX9HlKop4C0YQvCVCdwYl2wOtE8= -golang.org/x/term v0.18.0/go.mod h1:ILwASektA3OnRv7amZ1xhE/KTR+u50pbXfZ03+6Nx58= +golang.org/x/term v0.21.0 h1:WVXCp+/EBEHOj53Rvu+7KiT/iElMrO8ACK16SMZ3jaA= +golang.org/x/term v0.21.0/go.mod h1:ooXLefLobQVslOqselCNF4SxFAaoS6KujMbsGzSDmX0= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/text v0.13.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= +golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.6.7 h1:FZR1q0exgwxzPzp/aF+VccGrSfxfPpkBqjIIEq3ru6c= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.56.3 h1:8I4C0Yq1EjstUzUJzpcRVbuYA2mODtEmpWiQoN/b2nc= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= +google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= +google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de h1:cZGRis4/ot9uVm639a+rHCUaG0JJHEsdyzSQTMX+suY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20240227224415-6ceb2ff114de/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY= +google.golang.org/grpc v1.63.2 h1:MUeiw1B2maTVZthpU5xvASfTh3LDbxHd6IJ6QQVU+xM= +google.golang.org/grpc v1.63.2/go.mod h1:WAX/8DgncnokcFUldAxq7GeB5DXHDbMF+lLvDomNkRA= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI= -google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= +google.golang.org/protobuf v1.34.0 h1:Qo/qEd2RZPCf2nKuorzksSknv0d3ERwp1vFG38gSmH4= +google.golang.org/protobuf v1.34.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/internal/conn/api_client.go b/internal/conn/api_client.go new file mode 100644 index 000000000..5ef33b128 --- /dev/null +++ b/internal/conn/api_client.go @@ -0,0 +1,55 @@ +package conn + +import ( + "context" + "fmt" + "log" + "strings" + + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" + "github.com/aws/aws-sdk-go-v2/config" + "github.com/aws/aws-sdk-go-v2/credentials" + "github.com/aws/aws-sdk-go-v2/service/s3" +) + +func NewS3Client(region string, api *ncloud.APIKey, site, endpointFromEnv string) *s3.Client { + var endpoint string + if endpointFromEnv != "" { + endpoint = endpointFromEnv + } else { + endpoint = genEndpointWithCode(region, site) + } + + if api.AccessKey == "" || api.SecretKey == "" { + log.Fatal("AccessKey and SecretKey must not be empty") + } + + cfg, err := config.LoadDefaultConfig(context.TODO(), + config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(api.AccessKey, api.SecretKey, "")), + config.WithRegion(region), + ) + + if err != nil { + log.Fatalf("unable to load SDK config, %v", err) + } + + newClient := s3.NewFromConfig(cfg, func(o *s3.Options) { + o.BaseEndpoint = ncloud.String(endpoint) + }) + + return newClient +} + +func genEndpointWithCode(region, site string) string { + var s3Endpoint string + switch site { + case "gov": + s3Endpoint = fmt.Sprintf("https://%[1]s.object.gov-ncloudstorage.com", strings.ToLower(region)) + case "fin": + s3Endpoint = fmt.Sprintf("https://%[1]s.object.fin-ncloudstorage.com", strings.ToLower(region)) + default: + s3Endpoint = fmt.Sprintf("https://%[1]s.object.ncloudstorage.com", strings.ToLower(region)) + } + + return s3Endpoint +} diff --git a/internal/conn/config.go b/internal/conn/config.go index de4acbf15..2f62e93cb 100644 --- a/internal/conn/config.go +++ b/internal/conn/config.go @@ -2,9 +2,11 @@ package conn import ( "fmt" - "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/vhadoop" "time" + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/vhadoop" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/vautoscaling" "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/vcdss" "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/services/vloadbalancer" @@ -73,9 +75,10 @@ type NcloudAPIClient struct { Vmssql *vmssql.APIClient Vhadoop *vhadoop.APIClient Vredis *vredis.APIClient + ObjectStorage *s3.Client } -func (c *Config) Client() (*NcloudAPIClient, error) { +func (c *Config) Client(site, endpoint string) (*NcloudAPIClient, error) { apiKey := &ncloud.APIKey{ AccessKey: c.AccessKey, SecretKey: c.SecretKey, @@ -105,6 +108,7 @@ func (c *Config) Client() (*NcloudAPIClient, error) { Vmssql: vmssql.NewAPIClient(vmssql.NewConfiguration(apiKey)), Vhadoop: vhadoop.NewAPIClient(vhadoop.NewConfiguration(apiKey)), Vredis: vredis.NewAPIClient(vredis.NewConfiguration(apiKey)), + ObjectStorage: NewS3Client(c.Region, apiKey, site, endpoint), }, nil } diff --git a/internal/provider/fwprovider/provider.go b/internal/provider/fwprovider/provider.go index 6519fc822..e46c81a1c 100644 --- a/internal/provider/fwprovider/provider.go +++ b/internal/provider/fwprovider/provider.go @@ -4,6 +4,7 @@ import ( "context" "github.com/terraform-providers/terraform-provider-ncloud/internal/service/hadoop" + "github.com/terraform-providers/terraform-provider-ncloud/internal/service/objectstorage" multierror "github.com/hashicorp/go-multierror" "github.com/hashicorp/terraform-plugin-framework/datasource" @@ -97,6 +98,8 @@ func (p *fwprovider) DataSources(ctx context.Context) []func() datasource.DataSo dataSources = append(dataSources, mssql.NewMssqlDataSource) dataSources = append(dataSources, mssql.NewMssqlImageProductsDataSource) dataSources = append(dataSources, mssql.NewMssqlProductsDataSource) + dataSources = append(dataSources, objectstorage.NewBucketDataSource) + dataSources = append(dataSources, objectstorage.NewObjectDataSource) if err := errs.ErrorOrNil(); err != nil { tflog.Warn(ctx, "registering resources", map[string]interface{}{ @@ -123,6 +126,11 @@ func (p *fwprovider) Resources(ctx context.Context) []func() resource.Resource { resources = append(resources, redis.NewRedisConfigGroupResource) resources = append(resources, redis.NewRedisResource) resources = append(resources, mssql.NewMssqlResource) + resources = append(resources, objectstorage.NewBucketResource) + resources = append(resources, objectstorage.NewObjectResource) + resources = append(resources, objectstorage.NewObjectACLResource) + resources = append(resources, objectstorage.NewBucketACLResource) + resources = append(resources, objectstorage.NewObjectCopyResource) if err := errs.ErrorOrNil(); err != nil { tflog.Warn(ctx, "registering resources", map[string]interface{}{ diff --git a/internal/provider/provider.go b/internal/provider/provider.go index 8f65ad5c5..c44abc59e 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -226,7 +226,16 @@ func ProviderConfigure(ctx context.Context, d *schema.ResourceData) (interface{} Region: region.(string), } - if client, err := config.Client(); err != nil { + // Set endpoint + var obs_endpoint string + + if endpoint, ok := getOrFromEnv(d, "obs_endpoint", "NCLOUD_OBS_ENDPOINT"); ok { + obs_endpoint = (endpoint).(string) + } else { + obs_endpoint = "" + } + + if client, err := config.Client(providerConfig.Site, obs_endpoint); err != nil { return nil, diag.FromErr(err) } else { providerConfig.Client = client diff --git a/internal/service/objectstorage/objectstorage_bucket.go b/internal/service/objectstorage/objectstorage_bucket.go new file mode 100644 index 000000000..022012f99 --- /dev/null +++ b/internal/service/objectstorage/objectstorage_bucket.go @@ -0,0 +1,289 @@ +package objectstorage + +import ( + "context" + "fmt" + "regexp" + "time" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/terraform-providers/terraform-provider-ncloud/internal/common" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" + "github.com/terraform-providers/terraform-provider-ncloud/internal/framework" +) + +const ( + CREATING = "creating" + CREATED = "created" + DELETING = "deleting" + DELETED = "deleted" +) + +var ( + _ resource.Resource = &bucketResource{} + _ resource.ResourceWithConfigure = &bucketResource{} + _ resource.ResourceWithImportState = &bucketResource{} +) + +func NewBucketResource() resource.Resource { + return &bucketResource{} +} + +type bucketResource struct { + config *conn.ProviderConfig +} + +func (o *bucketResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan bucketResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + reqParams := &s3.CreateBucketInput{ + Bucket: plan.BucketName.ValueStringPointer(), + } + + tflog.Info(ctx, "CreateObjectStorage reqParams="+common.MarshalUncheckedString(reqParams)) + + response, err := o.config.Client.ObjectStorage.CreateBucket(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("CREATING ERROR", err.Error()) + return + } + if response == nil { + resp.Diagnostics.AddError("CREATING ERROR", "response invalid") + return + } + + tflog.Info(ctx, "CreateObjectStorage response="+common.MarshalUncheckedString(response)) + + err = waitBucketCreated(ctx, o.config, plan.BucketName.String()) + if err != nil { + resp.Diagnostics.AddError("CREATING ERROR", err.Error()) + return + } + + plan.refreshFromOutput(ctx, o.config, plan.BucketName.ValueString()) + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (o *bucketResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var plan bucketResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + reqParams := &s3.DeleteBucketInput{ + Bucket: plan.BucketName.ValueStringPointer(), + } + + tflog.Info(ctx, "DeleteBucket reqParams="+common.MarshalUncheckedString(reqParams)) + + response, err := o.config.Client.ObjectStorage.DeleteBucket(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("DELETING ERROR", err.Error()) + return + } + + tflog.Info(ctx, "DeleteBucket response="+common.MarshalUncheckedString(response)) + + if err := waitBucketDeleted(ctx, o.config, plan.BucketName.String()); err != nil { + resp.Diagnostics.AddError("WAITING FOR DELETE ERROR", err.Error()) + } +} + +func (o *bucketResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_objectstorage_bucket" +} + +func (o *bucketResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var plan bucketResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + output, err := o.config.Client.ObjectStorage.ListBuckets(ctx, &s3.ListBucketsInput{}) + if err != nil { + resp.Diagnostics.AddError("READING ERROR", err.Error()) + return + } + + for _, bucket := range output.Buckets { + if *bucket.Name == *plan.BucketName.ValueStringPointer() { + _, err := o.config.Client.ObjectStorage.HeadBucket(ctx, &s3.HeadBucketInput{ + Bucket: plan.BucketName.ValueStringPointer(), + }) + if err != nil { + resp.Diagnostics.AddError("READING ERROR", err.Error()) + return + } + + plan = bucketResourceModel{ + BucketName: types.StringValue(*bucket.Name), + } + + break + } + } +} + +func (o *bucketResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttribute(), + "bucket_name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: BucketNameValidator(), + Description: "Bucket Name for Object Storage", + }, + "creation_date": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +func (o *bucketResource) Update(context.Context, resource.UpdateRequest, *resource.UpdateResponse) { +} + +func (o *bucketResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*conn.ProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Exprected *ProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + o.config = config +} + +func (o *bucketResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("bucket_name"), req, resp) +} + +func waitBucketCreated(ctx context.Context, config *conn.ProviderConfig, bucketName string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{CREATING}, + Target: []string{CREATED}, + Refresh: func() (interface{}, string, error) { + + // Since HeadBucket does not work when bucket created immediately, use ListBuckets instead for check bucket creation operated successfully. + output, err := config.Client.ObjectStorage.ListBuckets(ctx, &s3.ListBucketsInput{}) + if err != nil { + return 0, "", fmt.Errorf("ListBuckets is nil") + } + + for _, bucket := range output.Buckets { + if *bucket.Name == TrimForParsing(bucketName) { + return bucket, CREATED, nil + } + } + + return output.Buckets, CREATING, nil + }, + Timeout: conn.DefaultTimeout, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("error waiting for object storage (%s) to become terminating: %s", bucketName, err) + } + return nil +} + +func waitBucketDeleted(ctx context.Context, config *conn.ProviderConfig, bucketName string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{DELETING}, + Target: []string{DELETED}, + Refresh: func() (interface{}, string, error) { + output, err := config.Client.ObjectStorage.ListBuckets(ctx, &s3.ListBucketsInput{}) + if err != nil { + return 0, "", fmt.Errorf("ListBuckets is nil") + } + + for _, bucket := range output.Buckets { + if *bucket.Name == TrimForParsing(bucketName) { + return bucket, DELETING, nil + } + } + + return output.Buckets, DELETED, nil + }, + Timeout: 2 * conn.DefaultTimeout, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("error waiting for object storage (%s) to become terminating: %s", bucketName, err) + } + + return nil +} + +type bucketResourceModel struct { + ID types.String `tfsdk:"id"` + BucketName types.String `tfsdk:"bucket_name"` + CreationDate types.String `tfsdk:"creation_date"` +} + +func (o *bucketResourceModel) refreshFromOutput(ctx context.Context, config *conn.ProviderConfig, bucketName string) { + o.BucketName = types.StringValue(bucketName) + o.ID = types.StringValue(bucketName) + + output, _ := config.Client.ObjectStorage.ListBuckets(ctx, &s3.ListBucketsInput{}) + if output == nil { + return + } + + for _, bucket := range output.Buckets { + if *bucket.Name == TrimForParsing(bucketName) { + if !types.StringValue(bucket.CreationDate.GoString()).IsNull() { + o.CreationDate = types.StringValue(bucket.CreationDate.GoString()) + } + } + } +} + +func BucketNameValidator() []validator.String { + return []validator.String{ + stringvalidator.All( + stringvalidator.LengthBetween(3, 63), + stringvalidator.RegexMatches( + regexp.MustCompile(`^[a-z0-9][a-z0-9\.-]{1,61}[a-z0-9]$`), + "Bucket name must be between 3 and 63 characters long, can contain lowercase letters, numbers, periods, and hyphens. It must start and end with a letter or number, and cannot have consecutive periods.", + ), + stringvalidator.RegexMatches( + regexp.MustCompile(`^(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}$|.+)`), + "Bucket name cannot be formatted as an IP address.", + ), + ), + } +} diff --git a/internal/service/objectstorage/objectstorage_bucket_acl.go b/internal/service/objectstorage/objectstorage_bucket_acl.go new file mode 100644 index 000000000..cd249fcff --- /dev/null +++ b/internal/service/objectstorage/objectstorage_bucket_acl.go @@ -0,0 +1,382 @@ +package objectstorage + +import ( + "context" + "fmt" + "regexp" + "strings" + "time" + + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" + "github.com/aws/aws-sdk-go-v2/service/s3" + awsTypes "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/terraform-providers/terraform-provider-ncloud/internal/common" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" + "github.com/terraform-providers/terraform-provider-ncloud/internal/framework" +) + +const ( + APPLYING = "applying" + APPLIED = "applied" +) + +var ( + _ resource.Resource = &bucketACLResource{} + _ resource.ResourceWithConfigure = &bucketACLResource{} + _ resource.ResourceWithImportState = &bucketACLResource{} +) + +func NewBucketACLResource() resource.Resource { + return &bucketACLResource{} +} + +type bucketACLResource struct { + config *conn.ProviderConfig +} + +func (b *bucketACLResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttribute(), + "bucket_name": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.All( + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z0-9][a-z0-9\.-]{1,61}[a-z0-9]$`), "Requires pattern with link of target bucket"), + ), + }, + Description: "Target bucket name", + }, + "rule": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf( + string(awsTypes.BucketCannedACLPrivate), + string(awsTypes.BucketCannedACLPublicRead), + string(awsTypes.BucketCannedACLPublicReadWrite), + string(awsTypes.BucketCannedACLAuthenticatedRead), + ), + }, + }, + "grants": schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "grantee": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Computed: true, + }, + "display_name": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "email_address": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "id": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "uri": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + }, + }, + "permission": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + "owner": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +func (b *bucketACLResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan bucketACLResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + bucketName := TrimForParsing(plan.BucketName.String()) + + reqParams := &s3.PutBucketAclInput{ + Bucket: ncloud.String(bucketName), + ACL: plan.Rule, + } + + tflog.Info(ctx, "PutBucketACL reqParams="+common.MarshalUncheckedString(reqParams)) + + response, err := b.config.Client.ObjectStorage.PutBucketAcl(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("CREATING ERROR", err.Error()) + return + } + + tflog.Info(ctx, "PutBucketACL response="+common.MarshalUncheckedString(response)) + + if err := waitBucketACLApplied(ctx, b.config, bucketName); err != nil { + resp.Diagnostics.AddError("CREATING ERROR", err.Error()) + return + } + + plan.refreshFromOutput(ctx, b.config, bucketName, &resp.Diagnostics) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (b *bucketACLResource) Delete(context.Context, resource.DeleteRequest, *resource.DeleteResponse) { +} + +func (b *bucketACLResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var plan bucketACLResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + bucketName := TrimForParsing(plan.BucketName.String()) + + plan.refreshFromOutput(ctx, b.config, bucketName, &resp.Diagnostics) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (b *bucketACLResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state bucketACLResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + if plan.Rule != state.Rule { + bucketName := TrimForParsing(state.BucketName.String()) + + reqParams := &s3.PutBucketAclInput{ + Bucket: ncloud.String(bucketName), + ACL: plan.Rule, + } + + tflog.Info(ctx, "PutBucketACL update operation reqParams="+common.MarshalUncheckedString(reqParams)) + + response, err := b.config.Client.ObjectStorage.PutBucketAcl(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("UPDATING ERROR", err.Error()) + return + } + + tflog.Info(ctx, "PutBucketACL update operation response="+common.MarshalUncheckedString(response)) + + if err := waitBucketACLApplied(ctx, b.config, bucketName); err != nil { + resp.Diagnostics.AddError("UPDATING ERROR", err.Error()) + return + } + + plan.refreshFromOutput(ctx, b.config, bucketName, &resp.Diagnostics) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + } +} + +func (b *bucketACLResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_objectstorage_bucket_acl" +} + +func (b *bucketACLResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*conn.ProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Exprected *ProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + b.config = config +} + +func (b *bucketACLResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func waitBucketACLApplied(ctx context.Context, config *conn.ProviderConfig, bucketName string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{APPLYING}, + Target: []string{APPLIED}, + Refresh: func() (interface{}, string, error) { + output, err := config.Client.ObjectStorage.GetBucketAcl(ctx, &s3.GetBucketAclInput{ + Bucket: ncloud.String(bucketName), + }) + + if output != nil { + return output, APPLIED, nil + } + + if err != nil { + return output, APPLYING, nil + } + + return output, APPLYING, nil + }, + Timeout: conn.DefaultTimeout, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("error waiting for bucket acl (%s) to be applied: %s", bucketName, err) + } + return nil +} + +type bucketACLResourceModel struct { + ID types.String `tfsdk:"id"` + BucketName types.String `tfsdk:"bucket_name"` + Rule awsTypes.BucketCannedACL `tfsdk:"rule"` + Grants types.List `tfsdk:"grants"` + Owner types.String `tfsdk:"owner"` +} + +func (b *bucketACLResourceModel) refreshFromOutput(ctx context.Context, config *conn.ProviderConfig, bucketName string, diag *diag.Diagnostics) { + output, err := config.Client.ObjectStorage.GetBucketAcl(ctx, &s3.GetBucketAclInput{ + Bucket: ncloud.String(bucketName), + }) + if err != nil { + diag.AddError("GetBucketAcl ERROR", err.Error()) + return + } + if output == nil { + diag.AddError("GetBucketAcl ERROR", "output is nil") + return + } + + var grantList []awsTypes.Grant + for _, grant := range output.Grants { + var indivGrant awsTypes.Grant + + indivGrant.Grantee = &awsTypes.Grantee{} + indivGrant.Grantee.Type = grant.Grantee.Type + indivGrant.Permission = grant.Permission + + if !types.StringPointerValue(grant.Grantee.ID).IsNull() { + indivGrant.Grantee.ID = grant.Grantee.ID + } + + if !types.StringPointerValue(grant.Grantee.DisplayName).IsNull() { + indivGrant.Grantee.DisplayName = grant.Grantee.DisplayName + } + + if !types.StringPointerValue(grant.Grantee.EmailAddress).IsNull() { + indivGrant.Grantee.EmailAddress = grant.Grantee.EmailAddress + } + + if !types.StringPointerValue(grant.Grantee.URI).IsNull() { + indivGrant.Grantee.URI = grant.Grantee.URI + } + + grantList = append(grantList, indivGrant) + } + + listValueFromGrants, diagFromConverting := convertGrantsToListValueAtBucket(ctx, grantList) + if diagFromConverting.HasError() { + diag.AddError("CONVERTING ERROR", "Error from converting grants to listValue at Object") + return + } + + b.Grants = listValueFromGrants + b.ID = types.StringValue(fmt.Sprintf("bucket_acl_%s", b.BucketName)) + b.Owner = types.StringValue(*output.Owner.ID) +} + +func convertGrantsToListValueAtBucket(ctx context.Context, grants []awsTypes.Grant) (basetypes.ListValue, diag.Diagnostics) { + var grantValues []attr.Value + + for _, grant := range grants { + granteeMap := map[string]attr.Value{ + "type": types.StringValue(string(grant.Grantee.Type)), + "display_name": types.StringPointerValue(grant.Grantee.DisplayName), + "email_address": types.StringPointerValue(grant.Grantee.EmailAddress), + "id": types.StringPointerValue(grant.Grantee.ID), + "uri": types.StringPointerValue(grant.Grantee.URI), + } + + granteeObj, diags := types.ObjectValue(map[string]attr.Type{ + "type": types.StringType, + "display_name": types.StringType, + "email_address": types.StringType, + "id": types.StringType, + "uri": types.StringType, + }, granteeMap) + if diags.HasError() { + return basetypes.ListValue{}, diags + } + + grantMap := map[string]attr.Value{ + "grantee": granteeObj, + "permission": types.StringValue(string(grant.Permission)), + } + + grantObj, diags := types.ObjectValue(map[string]attr.Type{ + "grantee": granteeObj.Type(ctx), + "permission": types.StringType, + }, grantMap) + if diags.HasError() { + return basetypes.ListValue{}, diags + } + + grantValues = append(grantValues, grantObj) + } + + return types.ListValue(types.ObjectType{AttrTypes: map[string]attr.Type{ + "grantee": types.ObjectType{AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "display_name": types.StringType, + "email_address": types.StringType, + "id": types.StringType, + "uri": types.StringType, + }}, + "permission": types.StringType, + }}, grantValues) +} + +func TrimForParsing(s string) string { + s = strings.TrimSuffix(s, "\"") + s = strings.TrimPrefix(s, "\"") + + return s +} diff --git a/internal/service/objectstorage/objectstorage_bucket_acl_test.go b/internal/service/objectstorage/objectstorage_bucket_acl_test.go new file mode 100644 index 000000000..eb8f79274 --- /dev/null +++ b/internal/service/objectstorage/objectstorage_bucket_acl_test.go @@ -0,0 +1,87 @@ +package objectstorage_test + +import ( + "context" + "fmt" + "testing" + + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" + "github.com/aws/aws-sdk-go-v2/service/s3" + awsTypes "github.com/aws/aws-sdk-go-v2/service/s3/types" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + . "github.com/terraform-providers/terraform-provider-ncloud/internal/acctest" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" +) + +func TestAccResourceNcloudObjectStorage_bucket_acl_basic(t *testing.T) { + var aclOutput s3.GetBucketAclOutput + bucketName := fmt.Sprintf("tf-test-%s", acctest.RandString(5)) + aclOptions := []string{string(awsTypes.BucketCannedACLPrivate), + string(awsTypes.BucketCannedACLPublicRead), + string(awsTypes.BucketCannedACLPublicReadWrite), + string(awsTypes.BucketCannedACLAuthenticatedRead)} + acl := aclOptions[acctest.RandIntRange(0, len(aclOptions)-1)] + resourceName := "ncloud_objectstorage_bucket_acl.testing_acl" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccBucketACLConfig(bucketName, acl), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckBucketACLExists(resourceName, &aclOutput, GetTestProvider(true)), + resource.TestCheckResourceAttr(resourceName, "rule", acl), + ), + }, + }, + }) +} + +func testAccCheckBucketACLExists(n string, object *s3.GetBucketAclOutput, provider *schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found %s", n) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("no ID is set") + } + + bucketName := resource.Primary.Attributes["bucket_name"] + + config := provider.Meta().(*conn.ProviderConfig) + resp, err := config.Client.ObjectStorage.GetBucketAcl(context.Background(), &s3.GetBucketAclInput{ + Bucket: ncloud.String(bucketName), + }) + + if err != nil { + return err + } + + if resp != nil { + object = resp + return nil + } + + return fmt.Errorf("Bucket ACL not found") + } +} + +func testAccBucketACLConfig(bucketName, acl string) string { + return fmt.Sprintf(` + resource "ncloud_objectstorage_bucket" "testing_bucket" { + bucket_name = "%[1]s" + } + + resource "ncloud_objectstorage_bucket_acl" "testing_acl" { + bucket_name = ncloud_objectstorage_bucket.testing_bucket.bucket_name + rule = "%[2]s" + } + `, bucketName, acl) +} diff --git a/internal/service/objectstorage/objectstorage_bucket_data_source.go b/internal/service/objectstorage/objectstorage_bucket_data_source.go new file mode 100644 index 000000000..2dbdc1c10 --- /dev/null +++ b/internal/service/objectstorage/objectstorage_bucket_data_source.go @@ -0,0 +1,123 @@ +package objectstorage + +import ( + "context" + "fmt" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" +) + +var ( + _ datasource.DataSource = &bucketDataSource{} + _ datasource.DataSourceWithConfigure = &bucketDataSource{} +) + +func NewBucketDataSource() datasource.DataSource { + return &bucketDataSource{} +} + +type bucketDataSource struct { + config *conn.ProviderConfig +} + +func (b *bucketDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*conn.ProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *ProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + b.config = config +} + +func (b *bucketDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_objectstorage_bucket" +} + +func (b *bucketDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data bucketDataSourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + output, err := b.config.Client.ObjectStorage.ListBuckets(ctx, &s3.ListBucketsInput{}) + if err != nil { + resp.Diagnostics.AddError("READING ERROR", err.Error()) + return + } + + data.OwnerID = types.StringValue(*output.Owner.ID) + data.OwnerDisplayName = types.StringValue(*output.Owner.DisplayName) + + for _, bucket := range output.Buckets { + if *bucket.Name == *data.BucketName.ValueStringPointer() { + _, err := b.config.Client.ObjectStorage.HeadBucket(ctx, &s3.HeadBucketInput{ + Bucket: data.BucketName.ValueStringPointer(), + }) + if err != nil { + resp.Diagnostics.AddError("READING ERROR", err.Error()) + return + } + + data.ID = types.StringValue(*bucket.Name) + data.BucketName = types.StringValue(*bucket.Name) + + if !data.CreationDate.IsNull() || !data.CreationDate.IsUnknown() { + data.CreationDate = types.StringValue(bucket.CreationDate.String()) + } + + break + } + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (b *bucketDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "bucket_name": schema.StringAttribute{ + Required: true, + Validators: BucketNameValidator(), + Description: "Bucket Name for Object Storage", + }, + "owner_id": schema.StringAttribute{ + Computed: true, + }, + "owner_displayname": schema.StringAttribute{ + Computed: true, + }, + "creation_date": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +type bucketDataSourceModel struct { + ID types.String `tfsdk:"id"` + BucketName types.String `tfsdk:"bucket_name"` + OwnerID types.String `tfsdk:"owner_id"` + OwnerDisplayName types.String `tfsdk:"owner_displayname"` + CreationDate types.String `tfsdk:"creation_date"` +} diff --git a/internal/service/objectstorage/objectstorage_bucket_data_source_test.go b/internal/service/objectstorage/objectstorage_bucket_data_source_test.go new file mode 100644 index 000000000..7e0f05e4a --- /dev/null +++ b/internal/service/objectstorage/objectstorage_bucket_data_source_test.go @@ -0,0 +1,43 @@ +package objectstorage_test + +import ( + "fmt" + "testing" + + randacctest "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + + . "github.com/terraform-providers/terraform-provider-ncloud/internal/acctest" +) + +func TestAccDataSourceNcloudObjectStorage_bucket_basic(t *testing.T) { + dataName := "data.ncloud_objectstorage_bucket.by_name" + resourceName := "ncloud_objectstorage_bucket.testing_bucket" + testBucketName := fmt.Sprintf("tf-bucket-%s", randacctest.RandString(4)) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceBucketConfig(testBucketName), + Check: resource.ComposeTestCheckFunc( + resource.TestCheckResourceAttrPair(dataName, "bucket_name", resourceName, "bucket_name"), + ), + }, + }, + }) + +} + +func testAccDataSourceBucketConfig(testBucketName string) string { + return fmt.Sprintf(` + resource "ncloud_objectstorage_bucket" "testing_bucket" { + bucket_name = "%[1]s" + } + + data "ncloud_objectstorage_bucket" "by_name" { + bucket_name = ncloud_objectstorage_bucket.testing_bucket.bucket_name + } + `, testBucketName) +} diff --git a/internal/service/objectstorage/objectstorage_bucket_test.go b/internal/service/objectstorage/objectstorage_bucket_test.go new file mode 100644 index 000000000..8e6552e66 --- /dev/null +++ b/internal/service/objectstorage/objectstorage_bucket_test.go @@ -0,0 +1,94 @@ +package objectstorage_test + +import ( + "context" + "fmt" + "testing" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + . "github.com/terraform-providers/terraform-provider-ncloud/internal/acctest" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" +) + +func TestAccResourceNcloudObjectStorage_bucket_basic(t *testing.T) { + bucketName := fmt.Sprintf("tf-test-%s", acctest.RandString(5)) + resourceName := "ncloud_objectstorage_bucket.testing_bucket" + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories, + CheckDestroy: testAccCheckBucketDestroy, + Steps: []resource.TestStep{ + { + Config: testAccBucketConfig(bucketName), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckBucketExists(resourceName, GetTestProvider(true)), + resource.TestCheckResourceAttr(resourceName, "bucket_name", bucketName), + ), + }, + }, + }) +} + +func testAccCheckBucketExists(n string, provider *schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found %s", n) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("no ID is set") + } + + config := provider.Meta().(*conn.ProviderConfig) + resp, err := config.Client.ObjectStorage.ListBuckets(context.Background(), &s3.ListBucketsInput{}) + if err != nil { + return err + } + + for _, bucket := range resp.Buckets { + if *bucket.Name == resource.Primary.Attributes["bucket_name"] { + return nil + } + } + + return fmt.Errorf("Bucket not found") + + } +} + +func testAccCheckBucketDestroy(s *terraform.State) error { + + for _, rs := range s.RootModule().Resources { + if rs.Type != "ncloud_objectstorage" { + continue + } + + config := GetTestProvider(true).Meta().(*conn.ProviderConfig) + resp, err := config.Client.ObjectStorage.ListBuckets(context.Background(), &s3.ListBucketsInput{}) + if err != nil { + return err + } + + for _, bucket := range resp.Buckets { + if *bucket.Name == rs.Primary.Attributes["bucket_name"] { + return fmt.Errorf("Bucket found") + } + } + } + + return nil +} + +func testAccBucketConfig(bucketName string) string { + return fmt.Sprintf(` + resource "ncloud_objectstorage_bucket" "testing_bucket" { + bucket_name = "%[1]s" + }`, bucketName) +} diff --git a/internal/service/objectstorage/objectstorage_object.go b/internal/service/objectstorage/objectstorage_object.go new file mode 100644 index 000000000..032b81eb6 --- /dev/null +++ b/internal/service/objectstorage/objectstorage_object.go @@ -0,0 +1,449 @@ +package objectstorage + +import ( + "context" + "fmt" + "os" + "strings" + "time" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/terraform-providers/terraform-provider-ncloud/internal/common" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" + "github.com/terraform-providers/terraform-provider-ncloud/internal/framework" +) + +var ( + _ resource.Resource = &objectResource{} + _ resource.ResourceWithConfigure = &objectResource{} + _ resource.ResourceWithImportState = &objectResource{} +) + +func NewObjectResource() resource.Resource { + return &objectResource{} +} + +type objectResource struct { + config *conn.ProviderConfig +} + +func (o *objectResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan objectResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + file, err := os.Open(plan.Source.ValueString()) + if err != nil { + resp.Diagnostics.AddError("CREATING ERROR", "invalid source path") + return + } + + reqParams := &s3.PutObjectInput{ + Bucket: plan.Bucket.ValueStringPointer(), + Key: plan.Key.ValueStringPointer(), + Body: file, + } + + if !plan.ContentEncoding.IsNull() && !plan.ContentEncoding.IsUnknown() { + reqParams.ContentEncoding = plan.ContentEncoding.ValueStringPointer() + } + + if !plan.ContentLanguage.IsNull() && !plan.ContentLanguage.IsUnknown() { + reqParams.ContentLanguage = plan.ContentLanguage.ValueStringPointer() + } + + if !plan.ContentType.IsNull() && !plan.ContentType.IsUnknown() { + reqParams.ContentType = plan.ContentType.ValueStringPointer() + } + + if !plan.WebsiteRedirectLocation.IsNull() && !plan.WebsiteRedirectLocation.IsUnknown() { + reqParams.WebsiteRedirectLocation = plan.WebsiteRedirectLocation.ValueStringPointer() + } + + tflog.Info(ctx, "PutObject reqParams="+common.MarshalUncheckedString(reqParams)) + + output, err := o.config.Client.ObjectStorage.PutObject(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("CREATING ERROR", err.Error()) + return + } + if output == nil { + resp.Diagnostics.AddError("CREATING ERROR", "response invalid") + return + } + + tflog.Info(ctx, "PutObject response="+common.MarshalUncheckedString(output)) + + if err := waitObjectUploaded(ctx, o.config, plan.Bucket.ValueString(), plan.Key.ValueString()); err != nil { + resp.Diagnostics.AddError("CREATING ERROR", err.Error()) + return + } + + plan.refreshFromOutput(ctx, o.config, &resp.Diagnostics) + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (o *objectResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var plan objectResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + reqParams := &s3.DeleteObjectInput{ + Bucket: plan.Bucket.ValueStringPointer(), + Key: plan.Key.ValueStringPointer(), + } + + tflog.Info(ctx, "DeleteObject reqParams="+common.MarshalUncheckedString(reqParams)) + + response, err := o.config.Client.ObjectStorage.DeleteObject(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("DELETING ERROR", err.Error()) + return + } + + tflog.Info(ctx, "DeleteObject response="+common.MarshalUncheckedString(response)) + + if err := waitObjectDeleted(ctx, o.config, plan.Bucket.String(), plan.Key.String()); err != nil { + resp.Diagnostics.AddError("WAITING FOR DELETE ERROR", err.Error()) + } +} + +func (o *objectResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_objectstorage_object" +} + +func (o *objectResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var plan objectResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + plan.refreshFromOutput(ctx, o.config, &resp.Diagnostics) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (o *objectResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttribute(), + "bucket": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: BucketNameValidator(), + Description: "Bucket name for object", + }, + "key": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "(Required) Name of the object once it is in the bucket", + }, + "source": schema.StringAttribute{ + Required: true, + Description: "(Required) Path of the object", + }, + "accept_ranges": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "content_encoding": schema.StringAttribute{ + Optional: true, + }, + "content_language": schema.StringAttribute{ + Optional: true, + }, + "content_length": schema.Int64Attribute{ + Computed: true, + }, + "content_type": schema.StringAttribute{ + Computed: true, + }, + "etag": schema.StringAttribute{ + Computed: true, + }, + "expiration": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "parts_count": schema.Int64Attribute{ + Computed: true, + Optional: true, + }, + "version_id": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "website_redirect_location": schema.StringAttribute{ + Optional: true, + }, + "last_modified": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +func (o *objectResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state objectResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + if !plan.Source.Equal(state.Source) { + file, err := os.Open(plan.Source.ValueString()) + if err != nil { + resp.Diagnostics.AddError("UPDATING ERROR", "invalid source path") + return + } + + reqParams := &s3.PutObjectInput{ + Bucket: state.Bucket.ValueStringPointer(), + Key: state.Key.ValueStringPointer(), + Body: file, + } + + // attributes that has dependancies with source + if !plan.ContentEncoding.IsNull() && !plan.ContentEncoding.IsUnknown() { + reqParams.ContentEncoding = plan.ContentEncoding.ValueStringPointer() + } + + if !plan.ContentLanguage.IsNull() && !plan.ContentLanguage.IsUnknown() { + reqParams.ContentLanguage = plan.ContentLanguage.ValueStringPointer() + } + + if !plan.ContentType.IsNull() && !plan.ContentType.IsUnknown() { + reqParams.ContentType = plan.ContentType.ValueStringPointer() + } + + if !plan.WebsiteRedirectLocation.IsNull() && !plan.WebsiteRedirectLocation.IsUnknown() { + reqParams.WebsiteRedirectLocation = plan.WebsiteRedirectLocation.ValueStringPointer() + } + + tflog.Info(ctx, "PutObject at update operation reqParams="+common.MarshalUncheckedString(reqParams)) + + output, err := o.config.Client.ObjectStorage.PutObject(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("UPDATING ERROR", err.Error()) + return + } + if output == nil { + resp.Diagnostics.AddError("UPDATING ERROR", "response invalid") + return + } + + tflog.Info(ctx, "PutObject at update operation response="+common.MarshalUncheckedString(output)) + + if err := waitObjectUploaded(ctx, o.config, plan.Bucket.ValueString(), plan.Key.ValueString()); err != nil { + resp.Diagnostics.AddError("UPDATING ERROR", err.Error()) + return + } + + plan.refreshFromOutput(ctx, o.config, &resp.Diagnostics) + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) + } +} + +func (o *objectResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (o *objectResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*conn.ProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Exprected *ProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + o.config = config +} + +func waitObjectUploaded(ctx context.Context, config *conn.ProviderConfig, bucketName, key string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{CREATING}, + Target: []string{CREATED}, + Refresh: func() (interface{}, string, error) { + output, err := config.Client.ObjectStorage.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucketName, + Key: &key, + }) + if output != nil { + return output, CREATED, nil + } + + if err != nil { + return output, CREATING, nil + } + + return output, CREATING, nil + }, + Timeout: conn.DefaultTimeout, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("error waiting for object (%s) to be upload: %s", key, err) + } + return nil +} + +func waitObjectDeleted(ctx context.Context, config *conn.ProviderConfig, bucketName, key string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{DELETING}, + Target: []string{DELETED}, + Refresh: func() (interface{}, string, error) { + output, err := config.Client.ObjectStorage.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucketName, + Key: &key, + }) + if output != nil { + return output, DELETING, nil + } + + if err != nil { + return output, DELETED, nil + } + + return output, DELETED, nil + }, + Timeout: conn.DefaultTimeout, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("error waiting for object (%s) to be upload: %s", key, err) + } + return nil +} + +type objectResourceModel struct { + ID types.String `tfsdk:"id"` + Bucket types.String `tfsdk:"bucket"` + Key types.String `tfsdk:"key"` + Source types.String `tfsdk:"source"` + AcceptRanges types.String `tfsdk:"accept_ranges"` + ContentEncoding types.String `tfsdk:"content_encoding"` + ContentLanguage types.String `tfsdk:"content_language"` + ContentLength types.Int64 `tfsdk:"content_length"` + ContentType types.String `tfsdk:"content_type"` + ETag types.String `tfsdk:"etag"` + Expiration types.String `tfsdk:"expiration"` + LastModified types.String `tfsdk:"last_modified"` + PartsCount types.Int64 `tfsdk:"parts_count"` + VersionId types.String `tfsdk:"version_id"` + WebsiteRedirectLocation types.String `tfsdk:"website_redirect_location"` +} + +func (o *objectResourceModel) refreshFromOutput(ctx context.Context, config *conn.ProviderConfig, diag *diag.Diagnostics) { + output, err := config.Client.ObjectStorage.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: o.Bucket.ValueStringPointer(), + Key: o.Key.ValueStringPointer(), + }) + if err != nil { + diag.AddError("HeadObject ERROR", err.Error()) + return + } + + bucketName, key := TrimForParsing(o.Bucket.String()), TrimForParsing(o.Key.String()) + + o.ID = types.StringValue(ObjectIDGenerator(bucketName, key)) + if !types.StringPointerValue(output.AcceptRanges).IsNull() || !types.StringPointerValue(output.AcceptRanges).IsUnknown() { + o.AcceptRanges = types.StringPointerValue(output.AcceptRanges) + } + + if !types.StringPointerValue(output.ContentEncoding).IsNull() || !types.StringPointerValue(output.ContentEncoding).IsUnknown() { + o.ContentEncoding = types.StringPointerValue(output.ContentEncoding) + } + + if !types.StringPointerValue(output.ContentLanguage).IsNull() || !types.StringPointerValue(output.ContentLanguage).IsUnknown() { + o.ContentLanguage = types.StringPointerValue(output.ContentLanguage) + } + + if !types.Int64PointerValue(output.ContentLength).IsNull() || !types.Int64PointerValue(output.ContentLength).IsUnknown() { + o.ContentLength = types.Int64PointerValue(output.ContentLength) + } + + if !types.StringPointerValue(output.ContentType).IsNull() || !types.StringPointerValue(output.ContentType).IsUnknown() { + o.ContentType = types.StringPointerValue(output.ContentType) + } + + if !types.StringPointerValue(output.ETag).IsNull() || !types.StringPointerValue(output.ETag).IsUnknown() { + o.ETag = types.StringPointerValue(output.ETag) + } + + if !types.StringPointerValue(output.Expiration).IsNull() || !types.StringPointerValue(output.Expiration).IsUnknown() { + o.Expiration = types.StringPointerValue(output.Expiration) + } + + if !types.Int32PointerValue(output.PartsCount).IsNull() || !types.Int32PointerValue(output.PartsCount).IsUnknown() { + o.PartsCount = common.Int64ValueFromInt32(output.PartsCount) + } + + if !types.StringPointerValue(output.VersionId).IsNull() || !types.StringPointerValue(output.VersionId).IsUnknown() { + o.VersionId = types.StringPointerValue(output.VersionId) + } + + if !types.StringPointerValue(output.WebsiteRedirectLocation).IsNull() || !types.StringPointerValue(output.WebsiteRedirectLocation).IsUnknown() { + o.WebsiteRedirectLocation = types.StringPointerValue(output.WebsiteRedirectLocation) + } + + if output.LastModified != nil { + o.LastModified = types.StringValue(output.LastModified.Format(time.RFC3339)) + } +} +func ObjectIDGenerator(bucketName, key string) string { + return fmt.Sprintf("%s/%s", bucketName, key) +} + +func ObjectIDParser(id string) (bucketName, key string) { + if id == "" { + return "", "" + } + + id = strings.TrimPrefix(id, "\"") + id = strings.TrimSuffix(id, "\"") + + parts := strings.Split(id, "/") + if len(parts) < 2 { + return "", "" + } + + return parts[0], parts[1] +} diff --git a/internal/service/objectstorage/objectstorage_object_acl.go b/internal/service/objectstorage/objectstorage_object_acl.go new file mode 100644 index 000000000..a7cf5495a --- /dev/null +++ b/internal/service/objectstorage/objectstorage_object_acl.go @@ -0,0 +1,380 @@ +package objectstorage + +import ( + "context" + "fmt" + "regexp" + "time" + + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/attr" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-framework/types/basetypes" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/terraform-providers/terraform-provider-ncloud/internal/common" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" + "github.com/terraform-providers/terraform-provider-ncloud/internal/framework" + + "github.com/aws/aws-sdk-go-v2/service/s3" + awsTypes "github.com/aws/aws-sdk-go-v2/service/s3/types" +) + +var ( + _ resource.Resource = &objectACLResource{} + _ resource.ResourceWithConfigure = &objectACLResource{} + _ resource.ResourceWithImportState = &objectACLResource{} +) + +func NewObjectACLResource() resource.Resource { + return &objectACLResource{} +} + +type objectACLResource struct { + config *conn.ProviderConfig +} + +func (o *objectACLResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan objectACLResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + bucketName, key := ObjectIDParser(plan.ObjectID.String()) + + reqParams := &s3.PutObjectAclInput{ + Bucket: ncloud.String(bucketName), + Key: ncloud.String(key), + ACL: plan.Rule, + } + + tflog.Info(ctx, "PutObjectACL reqParams="+common.MarshalUncheckedString(reqParams)) + + response, err := o.config.Client.ObjectStorage.PutObjectAcl(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("CREATING ERROR", err.Error()) + return + } + + tflog.Info(ctx, "PutObjectACL response="+common.MarshalUncheckedString(response)) + + if err := waitObjectACLApplied(ctx, o.config, bucketName, key); err != nil { + resp.Diagnostics.AddError("CREATING ERROR", err.Error()) + return + } + + plan.refreshFromOutput(ctx, o.config, bucketName, key, &resp.Diagnostics) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) +} + +func (o *objectACLResource) Delete(_ context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { +} + +func (o *objectACLResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_objectstorage_object_acl" +} + +func (o *objectACLResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var plan objectACLResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + bucketName, key := ObjectIDParser(plan.ObjectID.String()) + + plan.refreshFromOutput(ctx, o.config, bucketName, key, &resp.Diagnostics) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (o *objectACLResource) Schema(_ context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttribute(), + "object_id": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: []validator.String{ + stringvalidator.All( + stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z0-9-_]+\/[a-zA-Z0-9_.-]+$`), "Requires pattern with link of target object"), + ), + }, + Description: "Target object id", + }, + "rule": schema.StringAttribute{ + Required: true, + Validators: []validator.String{ + stringvalidator.OneOf( + string(awsTypes.ObjectCannedACLPrivate), + string(awsTypes.ObjectCannedACLPublicRead), + string(awsTypes.ObjectCannedACLPublicReadWrite), + string(awsTypes.ObjectCannedACLAuthenticatedRead), + string(awsTypes.ObjectCannedACLBucketOwnerRead), + string(awsTypes.ObjectCannedACLBucketOwnerFullControl), + ), + }, + }, + "grants": schema.ListNestedAttribute{ + Computed: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "grantee": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Computed: true, + }, + "display_name": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "email_address": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "id": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "uri": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + }, + }, + "permission": schema.StringAttribute{ + Computed: true, + }, + }, + }, + }, + "owner_id": schema.StringAttribute{ + Computed: true, + }, + "owner_displayname": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +func (o *objectACLResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state objectACLResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + if plan.Rule != state.Rule { + bucketName, key := ObjectIDParser(state.ObjectID.String()) + + reqParams := &s3.PutObjectAclInput{ + Bucket: ncloud.String(bucketName), + Key: ncloud.String(key), + ACL: plan.Rule, + } + + tflog.Info(ctx, "PutObjectACL update operation reqParams="+common.MarshalUncheckedString(reqParams)) + + response, err := o.config.Client.ObjectStorage.PutObjectAcl(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("UPDATING ERROR", err.Error()) + return + } + + tflog.Info(ctx, "PutObjectACL update operation response="+common.MarshalUncheckedString(response)) + + if err := waitObjectACLApplied(ctx, o.config, bucketName, key); err != nil { + resp.Diagnostics.AddError("UPDATING ERROR", err.Error()) + return + } + + plan.refreshFromOutput(ctx, o.config, bucketName, key, &resp.Diagnostics) + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + } +} + +func (o *objectACLResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (o *objectACLResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*conn.ProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Exprected *ProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + o.config = config +} + +func waitObjectACLApplied(ctx context.Context, config *conn.ProviderConfig, bucketName, key string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{APPLYING}, + Target: []string{APPLIED}, + Refresh: func() (interface{}, string, error) { + output, err := config.Client.ObjectStorage.GetObjectAcl(ctx, &s3.GetObjectAclInput{ + Bucket: ncloud.String(bucketName), + Key: ncloud.String(key), + }) + if output != nil { + return output, APPLIED, nil + } + + if err != nil { + return output, APPLYING, nil + } + + return output, APPLYING, nil + }, + Timeout: conn.DefaultTimeout, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("error waiting for object acl (%s) to be applied: %s", key, err) + } + return nil +} + +type objectACLResourceModel struct { + ID types.String `tfsdk:"id"` + ObjectID types.String `tfsdk:"object_id"` + Rule awsTypes.ObjectCannedACL `tfsdk:"rule"` + Grants types.List `tfsdk:"grants"` + OwnerID types.String `tfsdk:"owner_id"` + OwnerDisplayName types.String `tfsdk:"owner_displayname"` +} + +func (o *objectACLResourceModel) refreshFromOutput(ctx context.Context, config *conn.ProviderConfig, bucketName, key string, diag *diag.Diagnostics) { + output, err := config.Client.ObjectStorage.GetObjectAcl(ctx, &s3.GetObjectAclInput{ + Bucket: ncloud.String(bucketName), + Key: ncloud.String(key), + }) + if err != nil { + diag.AddError("GetObjectAcl ERROR", err.Error()) + return + } + if output == nil { + diag.AddError("GetObjectAcl ERROR", "output is nil") + return + } + + var grantList []awsTypes.Grant + for _, grant := range output.Grants { + var indivGrant awsTypes.Grant + + indivGrant.Grantee = &awsTypes.Grantee{} + indivGrant.Grantee.Type = grant.Grantee.Type + indivGrant.Permission = grant.Permission + + if !types.StringPointerValue(grant.Grantee.ID).IsNull() { + indivGrant.Grantee.ID = grant.Grantee.ID + } + + if !types.StringPointerValue(grant.Grantee.DisplayName).IsNull() { + indivGrant.Grantee.DisplayName = grant.Grantee.DisplayName + } + + if !types.StringPointerValue(grant.Grantee.EmailAddress).IsNull() { + indivGrant.Grantee.EmailAddress = grant.Grantee.EmailAddress + } + + if !types.StringPointerValue(grant.Grantee.URI).IsNull() { + indivGrant.Grantee.URI = grant.Grantee.URI + } + + grantList = append(grantList, indivGrant) + } + + listValueWithGrants, diagFromConverting := convertGrantsToListValueAtObject(ctx, grantList) + if diagFromConverting.HasError() { + diag.AddError("CONVERTING ERROR", "Error from converting grants to listValue at Object") + return + } + + o.Grants = listValueWithGrants + o.ID = types.StringValue(fmt.Sprintf("object_acl_%s", o.ObjectID)) + o.OwnerID = types.StringValue(*output.Owner.ID) + o.OwnerDisplayName = types.StringValue(*output.Owner.DisplayName) +} + +func convertGrantsToListValueAtObject(ctx context.Context, grants []awsTypes.Grant) (basetypes.ListValue, diag.Diagnostics) { + var grantValues []attr.Value + + for _, grant := range grants { + granteeMap := map[string]attr.Value{ + "type": types.StringValue(string(grant.Grantee.Type)), + "display_name": types.StringPointerValue(grant.Grantee.DisplayName), + "email_address": types.StringPointerValue(grant.Grantee.EmailAddress), + "id": types.StringPointerValue(grant.Grantee.ID), + "uri": types.StringPointerValue(grant.Grantee.URI), + } + + granteeObj, diags := types.ObjectValue(map[string]attr.Type{ + "type": types.StringType, + "display_name": types.StringType, + "email_address": types.StringType, + "id": types.StringType, + "uri": types.StringType, + }, granteeMap) + if diags.HasError() { + return basetypes.ListValue{}, diags + } + + grantMap := map[string]attr.Value{ + "grantee": granteeObj, + "permission": types.StringValue(string(grant.Permission)), + } + + grantObj, diags := types.ObjectValue(map[string]attr.Type{ + "grantee": granteeObj.Type(ctx), + "permission": types.StringType, + }, grantMap) + if diags.HasError() { + return basetypes.ListValue{}, diags + } + + grantValues = append(grantValues, grantObj) + } + + return types.ListValue(types.ObjectType{AttrTypes: map[string]attr.Type{ + "grantee": types.ObjectType{AttrTypes: map[string]attr.Type{ + "type": types.StringType, + "display_name": types.StringType, + "email_address": types.StringType, + "id": types.StringType, + "uri": types.StringType, + }}, + "permission": types.StringType, + }}, grantValues) +} diff --git a/internal/service/objectstorage/objectstorage_object_acl_test.go b/internal/service/objectstorage/objectstorage_object_acl_test.go new file mode 100644 index 000000000..fca6b8aff --- /dev/null +++ b/internal/service/objectstorage/objectstorage_object_acl_test.go @@ -0,0 +1,100 @@ +package objectstorage_test + +import ( + "context" + "fmt" + "os" + "testing" + + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + awsTypes "github.com/aws/aws-sdk-go-v2/service/s3/types" + . "github.com/terraform-providers/terraform-provider-ncloud/internal/acctest" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" + "github.com/terraform-providers/terraform-provider-ncloud/internal/service/objectstorage" +) + +func TestAccResourceNcloudObjectStorage_object_acl_basic(t *testing.T) { + bucketName := fmt.Sprintf("tf-bucket-%s", acctest.RandString(5)) + key := fmt.Sprintf("%s.md", acctest.RandString(5)) + content := "content for file upload testing" + aclOptions := []string{string(awsTypes.ObjectCannedACLPrivate), + string(awsTypes.ObjectCannedACLPublicRead), + string(awsTypes.ObjectCannedACLPublicReadWrite), + string(awsTypes.ObjectCannedACLAuthenticatedRead)} + acl := aclOptions[acctest.RandIntRange(0, len(aclOptions)-1)] + resourceName := "ncloud_objectstorage_object_acl.testing_acl" + + tmpFile := CreateTempFile(t, content, key) + source := tmpFile.Name() + defer os.Remove(source) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccObjectACLConfig(bucketName, key, source, acl), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckObjectACLExists(resourceName, GetTestProvider(true)), + resource.TestCheckResourceAttr(resourceName, "rule", acl), + ), + }, + }, + }) +} + +func testAccCheckObjectACLExists(n string, provider *schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found %s", n) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("no ID is set") + } + + objectID := resource.Primary.Attributes["object_id"] + + bucketName, key := objectstorage.ObjectIDParser(objectID) + + config := provider.Meta().(*conn.ProviderConfig) + resp, err := config.Client.ObjectStorage.GetObjectAcl(context.Background(), &s3.GetObjectAclInput{ + Bucket: ncloud.String(bucketName), + Key: ncloud.String(key), + }) + if err != nil { + return err + } + + if resp != nil { + return nil + } + + return fmt.Errorf("Object ACL not found") + } +} + +func testAccObjectACLConfig(bucketName, key, source, acl string) string { + return fmt.Sprintf(` + resource "ncloud_objectstorage_bucket" "testing_bucket" { + bucket_name = "%[1]s" + } + + resource "ncloud_objectstorage_object" "testing_object" { + bucket = ncloud_objectstorage_bucket.testing_bucket.bucket_name + key = "%[2]s" + source = "%[3]s" + } + + resource "ncloud_objectstorage_object_acl" "testing_acl" { + object_id = ncloud_objectstorage_object.testing_object.id + rule = "%[4]s" + }`, bucketName, key, source, acl) +} diff --git a/internal/service/objectstorage/objectstorage_object_copy.go b/internal/service/objectstorage/objectstorage_object_copy.go new file mode 100644 index 000000000..d9a24dea8 --- /dev/null +++ b/internal/service/objectstorage/objectstorage_object_copy.go @@ -0,0 +1,415 @@ +package objectstorage + +import ( + "context" + "fmt" + "time" + + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/hashicorp/terraform-plugin-framework/diag" + "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/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/retry" + "github.com/terraform-providers/terraform-provider-ncloud/internal/common" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" + "github.com/terraform-providers/terraform-provider-ncloud/internal/framework" +) + +var ( + _ resource.Resource = &objectCopyResource{} + _ resource.ResourceWithConfigure = &objectCopyResource{} + _ resource.ResourceWithImportState = &objectCopyResource{} +) + +func NewObjectCopyResource() resource.Resource { + return &objectCopyResource{} +} + +type objectCopyResource struct { + config *conn.ProviderConfig +} + +func (o *objectCopyResource) Configure(_ context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*conn.ProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Exprected *ProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + o.config = config +} + +func (o *objectCopyResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var plan objectCopyResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + reqParams := &s3.CopyObjectInput{ + Bucket: plan.Bucket.ValueStringPointer(), + CopySource: plan.Source.ValueStringPointer(), + Key: plan.Key.ValueStringPointer(), + } + + if !plan.ContentEncoding.IsNull() && !plan.ContentEncoding.IsUnknown() { + reqParams.ContentEncoding = plan.ContentEncoding.ValueStringPointer() + } + + if !plan.ContentLanguage.IsNull() && !plan.ContentLanguage.IsUnknown() { + reqParams.ContentLanguage = plan.ContentLanguage.ValueStringPointer() + } + + if !plan.ContentType.IsNull() && !plan.ContentType.IsUnknown() { + reqParams.ContentType = plan.ContentType.ValueStringPointer() + } + + if !plan.WebsiteRedirectLocation.IsNull() && !plan.WebsiteRedirectLocation.IsUnknown() { + reqParams.WebsiteRedirectLocation = plan.WebsiteRedirectLocation.ValueStringPointer() + } + + tflog.Info(ctx, "CopyObject reqParams="+common.MarshalUncheckedString(reqParams)) + + output, err := o.config.Client.ObjectStorage.CopyObject(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("COPYING ERROR", err.Error()) + return + } + if output == nil { + resp.Diagnostics.AddError("COPYING ERROR", "response invalid") + return + } + + tflog.Info(ctx, "CopyObject response="+common.MarshalUncheckedString(output)) + + if err := waitObjectCopied(ctx, o.config, plan.Bucket.ValueString(), plan.Key.ValueString()); err != nil { + resp.Diagnostics.AddError("COPYING ERROR", err.Error()) + return + } + + plan.refreshFromOutput(ctx, o.config, &resp.Diagnostics) + resp.Diagnostics.Append(resp.State.Set(ctx, plan)...) +} + +func (o *objectCopyResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var plan objectCopyResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + reqParams := &s3.DeleteObjectInput{ + Bucket: plan.Bucket.ValueStringPointer(), + Key: plan.Key.ValueStringPointer(), + } + + tflog.Info(ctx, "DeleteObject reqParams="+common.MarshalUncheckedString(reqParams)) + + response, err := o.config.Client.ObjectStorage.DeleteObject(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("DELETING ERROR", err.Error()) + return + } + + tflog.Info(ctx, "DeleteObject response="+common.MarshalUncheckedString(response)) + + if err := waitObjectCopyDeleted(ctx, o.config, plan.Bucket.String(), plan.Key.String()); err != nil { + resp.Diagnostics.AddError("WAITING FOR DELETE ERROR", err.Error()) + } +} + +func (o *objectCopyResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} + +func (o *objectCopyResource) Metadata(_ context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_objectstorage_object_copy" +} + +func (o *objectCopyResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var plan objectCopyResourceModel + + resp.Diagnostics.Append(req.State.Get(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } + + plan.refreshFromOutput(ctx, o.config, &resp.Diagnostics) + + resp.Diagnostics.Append(resp.State.Set(ctx, &plan)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (o *objectCopyResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": framework.IDAttribute(), + "bucket": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Validators: BucketNameValidator(), + Description: "Bucket name for object", + }, + "key": schema.StringAttribute{ + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + Description: "(Required) Name of the object once it is in the bucket", + }, + "source": schema.StringAttribute{ + Required: true, + Description: "(Required) Path of the object", + }, + "accept_ranges": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "content_encoding": schema.StringAttribute{ + Optional: true, + }, + "content_language": schema.StringAttribute{ + Optional: true, + }, + "content_length": schema.Int64Attribute{ + Computed: true, + }, + "content_type": schema.StringAttribute{ + Computed: true, + }, + "etag": schema.StringAttribute{ + Computed: true, + }, + "expiration": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "parts_count": schema.Int64Attribute{ + Computed: true, + Optional: true, + }, + "version_id": schema.StringAttribute{ + Computed: true, + Optional: true, + }, + "website_redirect_location": schema.StringAttribute{ + Optional: true, + }, + "last_modified": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +func (o *objectCopyResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var plan, state objectCopyResourceModel + + resp.Diagnostics.Append(req.Plan.Get(ctx, &plan)...) + resp.Diagnostics.Append(req.State.Get(ctx, &state)...) + + if resp.Diagnostics.HasError() { + return + } + + if !plan.Source.Equal(state.Source) { + reqParams := &s3.CopyObjectInput{ + Bucket: state.Bucket.ValueStringPointer(), + CopySource: plan.Source.ValueStringPointer(), + Key: state.Key.ValueStringPointer(), + } + + // attributes that has dependancies with source + if !plan.ContentEncoding.IsNull() && !plan.ContentEncoding.IsUnknown() { + reqParams.ContentEncoding = plan.ContentEncoding.ValueStringPointer() + } + + if !plan.ContentLanguage.IsNull() && !plan.ContentLanguage.IsUnknown() { + reqParams.ContentLanguage = plan.ContentLanguage.ValueStringPointer() + } + + if !plan.ContentType.IsNull() && !plan.ContentType.IsUnknown() { + reqParams.ContentType = plan.ContentType.ValueStringPointer() + } + + if !plan.WebsiteRedirectLocation.IsNull() && !plan.WebsiteRedirectLocation.IsUnknown() { + reqParams.WebsiteRedirectLocation = plan.WebsiteRedirectLocation.ValueStringPointer() + } + + tflog.Info(ctx, "CopyObject at update operation reqParams="+common.MarshalUncheckedString(reqParams)) + + output, err := o.config.Client.ObjectStorage.CopyObject(ctx, reqParams) + if err != nil { + resp.Diagnostics.AddError("UPDATING ERROR", err.Error()) + return + } + if output == nil { + resp.Diagnostics.AddError("UPDATING ERROR", "response invalid") + return + } + + tflog.Info(ctx, "CopyObject at update operation response="+common.MarshalUncheckedString(output)) + + if err := waitObjectCopied(ctx, o.config, plan.Bucket.ValueString(), plan.Key.ValueString()); err != nil { + resp.Diagnostics.AddError("UPDATING ERROR", err.Error()) + return + } + + plan.refreshFromOutput(ctx, o.config, &resp.Diagnostics) + } +} + +func waitObjectCopied(ctx context.Context, config *conn.ProviderConfig, bucketName string, key string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{CREATING}, + Target: []string{CREATED}, + Refresh: func() (interface{}, string, error) { + output, err := config.Client.ObjectStorage.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucketName, + Key: &key, + }) + if output != nil { + return output, CREATED, nil + } + + if err != nil { + return output, CREATING, nil + } + + return output, CREATING, nil + }, + Timeout: conn.DefaultTimeout, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("error waiting for object (%s) to be upload: %s", key, err) + } + return nil +} + +func waitObjectCopyDeleted(ctx context.Context, config *conn.ProviderConfig, bucketName, key string) error { + stateConf := &retry.StateChangeConf{ + Pending: []string{DELETING}, + Target: []string{DELETED}, + Refresh: func() (interface{}, string, error) { + output, err := config.Client.ObjectStorage.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: &bucketName, + Key: &key, + }) + if output != nil { + return output, DELETING, nil + } + + if err != nil { + return output, DELETED, nil + } + + return output, DELETED, nil + }, + Timeout: conn.DefaultTimeout, + Delay: 5 * time.Second, + MinTimeout: 3 * time.Second, + } + + if _, err := stateConf.WaitForStateContext(ctx); err != nil { + return fmt.Errorf("error waiting for object (%s) to be upload: %s", key, err) + } + return nil +} + +type objectCopyResourceModel struct { + ID types.String `tfsdk:"id"` + Bucket types.String `tfsdk:"bucket"` + Key types.String `tfsdk:"key"` + Source types.String `tfsdk:"source"` + AcceptRanges types.String `tfsdk:"accept_ranges"` + ContentEncoding types.String `tfsdk:"content_encoding"` + ContentLanguage types.String `tfsdk:"content_language"` + ContentLength types.Int64 `tfsdk:"content_length"` + ContentType types.String `tfsdk:"content_type"` + ETag types.String `tfsdk:"etag"` + Expiration types.String `tfsdk:"expiration"` + LastModified types.String `tfsdk:"last_modified"` + PartsCount types.Int64 `tfsdk:"parts_count"` + VersionId types.String `tfsdk:"version_id"` + WebsiteRedirectLocation types.String `tfsdk:"website_redirect_location"` +} + +func (o *objectCopyResourceModel) refreshFromOutput(ctx context.Context, config *conn.ProviderConfig, diag *diag.Diagnostics) { + output, err := config.Client.ObjectStorage.HeadObject(ctx, &s3.HeadObjectInput{ + Bucket: o.Bucket.ValueStringPointer(), + Key: o.Key.ValueStringPointer(), + }) + if err != nil { + diag.AddError("HeadObject ERROR", err.Error()) + return + } + + bucketName, key := TrimForParsing(o.Bucket.String()), TrimForParsing(o.Key.String()) + + o.ID = types.StringValue(ObjectIDGenerator(bucketName, key)) + if !types.StringPointerValue(output.AcceptRanges).IsNull() || !types.StringPointerValue(output.AcceptRanges).IsUnknown() { + o.AcceptRanges = types.StringPointerValue(output.AcceptRanges) + } + + if !types.StringPointerValue(output.ContentEncoding).IsNull() || !types.StringPointerValue(output.ContentEncoding).IsUnknown() { + o.ContentEncoding = types.StringPointerValue(output.ContentEncoding) + } + + if !types.StringPointerValue(output.ContentLanguage).IsNull() || !types.StringPointerValue(output.ContentLanguage).IsUnknown() { + o.ContentLanguage = types.StringPointerValue(output.ContentLanguage) + } + + if !types.Int64PointerValue(output.ContentLength).IsNull() || !types.Int64PointerValue(output.ContentLength).IsUnknown() { + o.ContentLength = types.Int64PointerValue(output.ContentLength) + } + + if !types.StringPointerValue(output.ContentType).IsNull() || !types.StringPointerValue(output.ContentType).IsUnknown() { + o.ContentType = types.StringPointerValue(output.ContentType) + } + + if !types.StringPointerValue(output.ETag).IsNull() || !types.StringPointerValue(output.ETag).IsUnknown() { + o.ETag = types.StringPointerValue(output.ETag) + } + + if !types.StringPointerValue(output.Expiration).IsNull() || !types.StringPointerValue(output.Expiration).IsUnknown() { + o.Expiration = types.StringPointerValue(output.Expiration) + } + + if !types.Int32PointerValue(output.PartsCount).IsNull() || !types.Int32PointerValue(output.PartsCount).IsUnknown() { + o.PartsCount = common.Int64ValueFromInt32(output.PartsCount) + } + + if !types.StringPointerValue(output.VersionId).IsNull() || !types.StringPointerValue(output.VersionId).IsUnknown() { + o.VersionId = types.StringPointerValue(output.VersionId) + } + + if !types.StringPointerValue(output.WebsiteRedirectLocation).IsNull() || !types.StringPointerValue(output.WebsiteRedirectLocation).IsUnknown() { + o.WebsiteRedirectLocation = types.StringPointerValue(output.WebsiteRedirectLocation) + } + + if output.LastModified != nil { + o.LastModified = types.StringValue(output.LastModified.Format(time.RFC3339)) + } +} diff --git a/internal/service/objectstorage/objectstorage_object_copy_test.go b/internal/service/objectstorage/objectstorage_object_copy_test.go new file mode 100644 index 000000000..0a40c3c65 --- /dev/null +++ b/internal/service/objectstorage/objectstorage_object_copy_test.go @@ -0,0 +1,123 @@ +package objectstorage_test + +import ( + "context" + "fmt" + "os" + "regexp" + "testing" + + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + . "github.com/terraform-providers/terraform-provider-ncloud/internal/acctest" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" +) + +func TestAccResourceNcloudObjectStorage_object_copy_basic(t *testing.T) { + bucketName := fmt.Sprintf("tf-bucket-%s", acctest.RandString(5)) + key := fmt.Sprintf("%s.md", acctest.RandString(5)) + resourceName := "ncloud_objectstorage_object_copy.testing_copy" + content := "content for file upload testing" + + tmpFile := CreateTempFile(t, content, key) + source := tmpFile.Name() + defer os.Remove(source) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories, + CheckDestroy: testAccCheckObjectCopyDestroy, + Steps: []resource.TestStep{ + { + Config: testAccObjectCopyConfig(bucketName, key, source), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckObjectCopyExists(resourceName, GetTestProvider(true)), + resource.TestMatchResourceAttr(resourceName, "id", regexp.MustCompile(`^[a-z0-9-_]+\/[a-zA-Z0-9_.-]+$`)), + resource.TestCheckResourceAttr(resourceName, "bucket", bucketName+"-to"), + resource.TestCheckResourceAttr(resourceName, "key", key), + ), + }, + }, + }) +} + +func testAccCheckObjectCopyExists(n string, provider *schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found %s", n) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("no ID is set") + } + + config := provider.Meta().(*conn.ProviderConfig) + resp, err := config.Client.ObjectStorage.GetObject(context.Background(), &s3.GetObjectInput{ + Bucket: ncloud.String(resource.Primary.Attributes["bucket"]), + Key: ncloud.String(resource.Primary.Attributes["key"]), + }) + if err != nil { + return err + } + + if resp != nil { + return nil + } + + return fmt.Errorf("Object not found") + } +} + +func testAccCheckObjectCopyDestroy(s *terraform.State) error { + config := GetTestProvider(true).Meta().(*conn.ProviderConfig) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "ncloud_objectstorage_object_copy" { + continue + } + + resp, err := config.Client.ObjectStorage.GetObject(context.Background(), &s3.GetObjectInput{ + Bucket: ncloud.String(rs.Primary.Attributes["bucket"]), + Key: ncloud.String(rs.Primary.Attributes["key"]), + }) + if resp != nil { + return fmt.Errorf("Object found") + } + + if err != nil { + return nil + } + } + + return nil +} + +func testAccObjectCopyConfig(bucketName, key, source string) string { + return fmt.Sprintf(` + resource "ncloud_objectstorage_bucket" "testing_bucket_from" { + bucket_name = "%[1]s-from" + } + + resource "ncloud_objectstorage_bucket" "testing_bucket_to" { + bucket_name = "%[1]s-to" + } + + resource "ncloud_objectstorage_object" "testing_object" { + bucket = ncloud_objectstorage_bucket.testing_bucket_from.bucket_name + key = "%[2]s" + source = "%[3]s" + } + + resource "ncloud_objectstorage_object_copy" "testing_copy" { + bucket = ncloud_objectstorage_bucket.testing_bucket_to.bucket_name + key = "%[2]s" + source = ncloud_objectstorage_object.testing_object.id + } + `, bucketName, key, source) +} diff --git a/internal/service/objectstorage/objectstorage_object_data_source.go b/internal/service/objectstorage/objectstorage_object_data_source.go new file mode 100644 index 000000000..9a31645d9 --- /dev/null +++ b/internal/service/objectstorage/objectstorage_object_data_source.go @@ -0,0 +1,211 @@ +package objectstorage + +import ( + "context" + "fmt" + "io" + "time" + + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/terraform-providers/terraform-provider-ncloud/internal/common" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" +) + +var ( + _ datasource.DataSource = &objectDataSource{} + _ datasource.DataSourceWithConfigure = &objectDataSource{} +) + +func NewObjectDataSource() datasource.DataSource { + return &objectDataSource{} +} + +type objectDataSource struct { + config *conn.ProviderConfig +} + +func (o *objectDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + + config, ok := req.ProviderData.(*conn.ProviderConfig) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Data Source Configure Type", + fmt.Sprintf("Expected *ProviderConfig, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + + o.config = config +} + +func (o *objectDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_objectstorage_object" +} + +func (o *objectDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data objectDataResourceModel + + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + bucketName, key := ObjectIDParser(data.ObjectID.String()) + + output, err := o.config.Client.ObjectStorage.GetObject(ctx, &s3.GetObjectInput{ + Bucket: ncloud.String(bucketName), + Key: ncloud.String(key), + }) + if err != nil { + resp.Diagnostics.AddError("READING ERROR", err.Error()) + return + } + defer output.Body.Close() + + data.ID = types.StringValue(ObjectIDGenerator(bucketName, key)) + data.ContentLength = types.Int64PointerValue(output.ContentLength) + data.ContentType = types.StringPointerValue(output.ContentType) + + if !types.StringPointerValue(output.AcceptRanges).IsNull() || !types.StringPointerValue(output.AcceptRanges).IsUnknown() { + data.AcceptRanges = types.StringPointerValue(output.AcceptRanges) + } + + if !types.StringPointerValue(output.ContentEncoding).IsNull() || !types.StringPointerValue(output.ContentEncoding).IsUnknown() { + data.ContentEncoding = types.StringPointerValue(output.ContentEncoding) + } + + if !types.StringPointerValue(output.ContentLanguage).IsNull() || !types.StringPointerValue(output.ContentLanguage).IsUnknown() { + data.ContentLanguage = types.StringPointerValue(output.ContentLanguage) + } + + if !types.Int64PointerValue(output.ContentLength).IsNull() || !types.Int64PointerValue(output.ContentLength).IsUnknown() { + data.ContentLength = types.Int64PointerValue(output.ContentLength) + } + + if !types.StringPointerValue(output.ContentType).IsNull() || !types.StringPointerValue(output.ContentType).IsUnknown() { + data.ContentType = types.StringPointerValue(output.ContentType) + } + + if !types.StringPointerValue(output.ETag).IsNull() || !types.StringPointerValue(output.ETag).IsUnknown() { + data.ETag = types.StringPointerValue(output.ETag) + } + + if !types.StringPointerValue(output.Expiration).IsNull() || !types.StringPointerValue(output.Expiration).IsUnknown() { + data.Expiration = types.StringPointerValue(output.Expiration) + } + + if !types.Int32PointerValue(output.PartsCount).IsNull() || !types.Int32PointerValue(output.PartsCount).IsUnknown() { + data.PartsCount = common.Int64ValueFromInt32(output.PartsCount) + } + + if !types.StringPointerValue(output.VersionId).IsNull() || !types.StringPointerValue(output.VersionId).IsUnknown() { + data.VersionId = types.StringPointerValue(output.VersionId) + } + + if !types.StringPointerValue(output.WebsiteRedirectLocation).IsNull() || !types.StringPointerValue(output.WebsiteRedirectLocation).IsUnknown() { + data.WebsiteRedirectLocation = types.StringPointerValue(output.WebsiteRedirectLocation) + } + + if output.LastModified != nil { + data.LastModified = types.StringValue(output.LastModified.Format(time.RFC3339)) + } + + bodyBytes, err := io.ReadAll(output.Body) + if err != nil { + resp.Diagnostics.AddError("READING ERROR", err.Error()) + return + } + + data.Body = types.StringValue(string(bodyBytes)) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } +} + +func (o *objectDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + }, + "object_id": schema.StringAttribute{ + Required: true, + }, + "bucket": schema.StringAttribute{ + Computed: true, + }, + "key": schema.StringAttribute{ + Computed: true, + }, + "source": schema.StringAttribute{ + Computed: true, + }, + "accept_ranges": schema.StringAttribute{ + Computed: true, + }, + "content_encoding": schema.StringAttribute{ + Computed: true, + }, + "content_language": schema.StringAttribute{ + Computed: true, + }, + "content_length": schema.Int64Attribute{ + Computed: true, + }, + "content_type": schema.StringAttribute{ + Computed: true, + }, + "etag": schema.StringAttribute{ + Computed: true, + }, + "expiration": schema.StringAttribute{ + Computed: true, + }, + "parts_count": schema.Int64Attribute{ + Computed: true, + }, + "version_id": schema.StringAttribute{ + Computed: true, + }, + "website_redirect_location": schema.StringAttribute{ + Computed: true, + }, + "last_modified": schema.StringAttribute{ + Computed: true, + }, + "body": schema.StringAttribute{ + Computed: true, + }, + }, + } +} + +type objectDataResourceModel struct { + ID types.String `tfsdk:"id"` + ObjectID types.String `tfsdk:"object_id"` + Bucket types.String `tfsdk:"bucket"` + Key types.String `tfsdk:"key"` + Source types.String `tfsdk:"source"` + ContentLength types.Int64 `tfsdk:"content_length"` + AcceptRanges types.String `tfsdk:"accept_ranges"` + ContentEncoding types.String `tfsdk:"content_encoding"` + ContentLanguage types.String `tfsdk:"content_language"` + ContentType types.String `tfsdk:"content_type"` + ETag types.String `tfsdk:"etag"` + Expiration types.String `tfsdk:"expiration"` + LastModified types.String `tfsdk:"last_modified"` + PartsCount types.Int64 `tfsdk:"parts_count"` + VersionId types.String `tfsdk:"version_id"` + WebsiteRedirectLocation types.String `tfsdk:"website_redirect_location"` + Body types.String `tfsdk:"body"` +} diff --git a/internal/service/objectstorage/objectstorage_object_data_source_test.go b/internal/service/objectstorage/objectstorage_object_data_source_test.go new file mode 100644 index 000000000..dd8c7671f --- /dev/null +++ b/internal/service/objectstorage/objectstorage_object_data_source_test.go @@ -0,0 +1,56 @@ +package objectstorage_test + +import ( + "fmt" + "os" + "regexp" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + . "github.com/terraform-providers/terraform-provider-ncloud/internal/acctest" +) + +func TestAccDataSourceNcloudObjectStorage_object_basic(t *testing.T) { + bucket := fmt.Sprintf("tf-bucket-%s", acctest.RandString(5)) + key := fmt.Sprintf("%s.md", acctest.RandString(5)) + dataName := "data.ncloud_objectstorage_object.by_id" + resourceName := "ncloud_objectstorage_object.testing_object" + content := "content for file upload testing" + + tmpFile := CreateTempFile(t, content, key) + source := tmpFile.Name() + defer os.Remove(source) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories, + Steps: []resource.TestStep{ + { + Config: testAccDataSourceObjectConfig(bucket, key, source), + Check: resource.ComposeTestCheckFunc( + resource.TestMatchResourceAttr(resourceName, "id", regexp.MustCompile(`^[a-z0-9-_]+\/[a-zA-Z0-9_.-]+$`)), + resource.TestCheckResourceAttrPair(dataName, "object_id", resourceName, "id"), + ), + }, + }, + }) +} + +func testAccDataSourceObjectConfig(bucket, key, source string) string { + return fmt.Sprintf(` + resource "ncloud_objectstorage_bucket" "testing_bucket" { + bucket_name = "%[1]s" + } + + resource "ncloud_objectstorage_object" "testing_object" { + bucket = ncloud_objectstorage_bucket.testing_bucket.bucket_name + key = "%[2]s" + source = "%[3]s" + } + + data "ncloud_objectstorage_object" "by_id" { + object_id = ncloud_objectstorage_object.testing_object.id + } + `, bucket, key, source) +} diff --git a/internal/service/objectstorage/objectstorage_object_test.go b/internal/service/objectstorage/objectstorage_object_test.go new file mode 100644 index 000000000..da1b30888 --- /dev/null +++ b/internal/service/objectstorage/objectstorage_object_test.go @@ -0,0 +1,129 @@ +package objectstorage_test + +import ( + "context" + "fmt" + "os" + "regexp" + "testing" + + "github.com/NaverCloudPlatform/ncloud-sdk-go-v2/ncloud" + "github.com/aws/aws-sdk-go-v2/service/s3" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/acctest" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/schema" + "github.com/hashicorp/terraform-plugin-sdk/v2/terraform" + + . "github.com/terraform-providers/terraform-provider-ncloud/internal/acctest" + "github.com/terraform-providers/terraform-provider-ncloud/internal/conn" +) + +func TestAccResourceNcloudObjectStorage_object_basic(t *testing.T) { + bucketName := fmt.Sprintf("tf-bucket-%s", acctest.RandString(5)) + key := fmt.Sprintf("%s.md", acctest.RandString(5)) + resourceName := "ncloud_objectstorage_object.testing_object" + content := "content for file upload testing" + + tmpFile := CreateTempFile(t, content, key) + source := tmpFile.Name() + defer os.Remove(source) + + resource.Test(t, resource.TestCase{ + PreCheck: func() { TestAccPreCheck(t) }, + ProtoV6ProviderFactories: ProtoV6ProviderFactories, + CheckDestroy: testAccCheckObjectDestroy, + Steps: []resource.TestStep{ + { + Config: testAccObjectConfig(bucketName, key, source), + Check: resource.ComposeAggregateTestCheckFunc( + testAccCheckObjectExists(resourceName, GetTestProvider(true)), + resource.TestMatchResourceAttr(resourceName, "id", regexp.MustCompile(`^[a-z0-9-_]+\/[a-zA-Z0-9_.-]+$`)), + resource.TestCheckResourceAttr(resourceName, "bucket", bucketName), + resource.TestCheckResourceAttr(resourceName, "key", key), + resource.TestCheckResourceAttr(resourceName, "source", source), + ), + }, + }, + }) +} + +func testAccCheckObjectExists(n string, provider *schema.Provider) resource.TestCheckFunc { + return func(s *terraform.State) error { + resource, ok := s.RootModule().Resources[n] + if !ok { + return fmt.Errorf("not found %s", n) + } + + if resource.Primary.ID == "" { + return fmt.Errorf("no ID is set") + } + + config := provider.Meta().(*conn.ProviderConfig) + resp, err := config.Client.ObjectStorage.GetObject(context.Background(), &s3.GetObjectInput{ + Bucket: ncloud.String(resource.Primary.Attributes["bucket"]), + Key: ncloud.String(resource.Primary.Attributes["key"]), + }) + if err != nil { + return err + } + + if resp != nil { + return nil + } + + return fmt.Errorf("Object not found") + } +} + +func testAccCheckObjectDestroy(s *terraform.State) error { + config := GetTestProvider(true).Meta().(*conn.ProviderConfig) + + for _, rs := range s.RootModule().Resources { + if rs.Type != "ncloud_objectstorage_object" { + continue + } + + resp, err := config.Client.ObjectStorage.GetObject(context.Background(), &s3.GetObjectInput{ + Bucket: ncloud.String(rs.Primary.Attributes["bucket"]), + Key: ncloud.String(rs.Primary.Attributes["key"]), + }) + if resp != nil { + return fmt.Errorf("Object found") + } + + if err != nil { + return nil + } + } + + return nil +} + +func testAccObjectConfig(bucketName, key, source string) string { + return fmt.Sprintf(` + resource "ncloud_objectstorage_bucket" "testing_bucket" { + bucket_name = "%[1]s" + } + + resource "ncloud_objectstorage_object" "testing_object" { + bucket = ncloud_objectstorage_bucket.testing_bucket.bucket_name + key = "%[2]s" + source = "%[3]s" + }`, bucketName, key, source) +} + +func CreateTempFile(t *testing.T, content, key string) *os.File { + tmpFile, err := os.CreateTemp("", key) + if err != nil { + t.Error("Error Occur: ", err) + } + + if _, err := tmpFile.WriteString(content); err != nil { + t.Error("Error Occur: ", err) + } + if err := tmpFile.Close(); err != nil { + t.Error("Error Occur: ", err) + } + + return tmpFile +}