Skip to content

Commit

Permalink
fix(objectStorage): Fix invalid key format (#446)
Browse files Browse the repository at this point in the history
* fix: Fix invalid key format (now can cover longer path seperated with "/")

* fix: Fix invalid regex format

* chore: Remvoe redundant log

* fix: Set fin endpoint to do not parse regionCode

* fix: Fix object source update logic for proper acl apply

* test: Add update test code

* chore: Fix regex typo

* feat: Remove redundant Update logic (update with planmodifier)

* Revert "fix: Set fin endpoint to do not parse regionCode"

This reverts commit d42390e.

* fix: Remove unavailable optional properties

* feat: Add copy object update logic & testing

* feat: Add object update logic & testing

* fix: Modify optional input attributes

* feat: integrate duplicated PutObject logic

* fix: Remove unprovided property from docs

* feat: Add Close() contrast to Open()

* refactor: Remove dependency of local source path at update logic

* fix: Modify to operate GetObject only if source is not changed
  • Loading branch information
Geun-Oh authored Sep 23, 2024
1 parent e0330ab commit 64e4747
Show file tree
Hide file tree
Showing 12 changed files with 475 additions and 73 deletions.
1 change: 0 additions & 1 deletion docs/data-sources/object_storage_object.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,6 @@ This data source exports the following attributes in addition to the arguments a
* `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).
Expand Down
7 changes: 3 additions & 4 deletions docs/resources/object_storage_object.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,11 +40,7 @@ The following arguments are required:

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.

Expand All @@ -54,10 +50,13 @@ This resource exports the following attributes in addition to the arguments abov

* `accept_ranges` - Indicates that a range of bytes was specified.
* `content_length` - Size of the body in bytes.
* `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.
* `content_language` - Language the content is in e.g., en-US or en-GB.
* `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.
* `website_redirect_location` - Target URL for website redirect.
* `version_id` - Unique version ID value for the object, if bucket versioning is enabled.

## Import
Expand Down
8 changes: 4 additions & 4 deletions docs/resources/object_storage_object_copy.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,10 +50,7 @@ The following arguments are required:

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.
* `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. This attribute is only available in update operation.

## Attribute Reference.

Expand All @@ -62,11 +59,14 @@ The following arguments are supported:
This resource exports the following attributes in addition to the arguments above:

* `accept_ranges` - Indicates that a range of bytes was specified.
* `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.
* `content_language` - Language the content is in e.g., en-US or en-GB.
* `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.
* `website_redirect_location` - Target URL for website redirect.
* `version_id` - Unique version ID value for the object, if bucket versioning is enabled.

## Import
Expand Down
4 changes: 0 additions & 4 deletions internal/service/objectstorage/objectstorage_bucket_acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,19 +86,15 @@ func (b *bucketACLResource) Schema(_ context.Context, req resource.SchemaRequest
},
"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,
},
},
},
Expand Down
30 changes: 30 additions & 0 deletions internal/service/objectstorage/objectstorage_bucket_acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,36 @@ func TestAccResourceNcloudObjectStorage_bucket_acl_basic(t *testing.T) {
})
}

func TestAccResourceNcloudObjectStorage_bucket_acl_update(t *testing.T) {
var aclOutput s3.GetBucketAclOutput
bucketName := fmt.Sprintf("tf-test-%s", acctest.RandString(5))

acl := "public-read"
newACL := "private"
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),
),
},
{
Config: testAccBucketACLConfig(bucketName, newACL),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckBucketACLExists(resourceName, &aclOutput, GetTestProvider(true)),
resource.TestCheckResourceAttr(resourceName, "rule", newACL),
),
},
},
})
}

func testAccCheckBucketACLExists(n string, object *s3.GetBucketAclOutput, provider *schema.Provider) resource.TestCheckFunc {
return func(s *terraform.State) error {
resource, ok := s.RootModule().Resources[n]
Expand Down
103 changes: 64 additions & 39 deletions internal/service/objectstorage/objectstorage_object.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ func (o *objectResource) Create(ctx context.Context, req resource.CreateRequest,
resp.Diagnostics.AddError("CREATING ERROR", "invalid source path")
return
}
defer file.Close()

reqParams := &s3.PutObjectInput{
Bucket: plan.Bucket.ValueStringPointer(),
Expand Down Expand Up @@ -163,42 +164,42 @@ func (o *objectResource) Schema(_ context.Context, req resource.SchemaRequest, r
Description: "(Required) Name of the object once it is in the bucket",
},
"source": schema.StringAttribute{
Required: true,
Required: true,
PlanModifiers: []planmodifier.String{
stringplanmodifier.RequiresReplace(),
},
Description: "(Required) Path of the object",
},
"accept_ranges": schema.StringAttribute{
Computed: true,
Optional: true,
},
"content_encoding": schema.StringAttribute{
Optional: true,
Computed: true,
},
"content_language": schema.StringAttribute{
Optional: true,
Computed: true,
},
"content_length": schema.Int64Attribute{
Computed: true,
},
"content_type": schema.StringAttribute{
Computed: true,
Optional: 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,
Computed: true,
},
"last_modified": schema.StringAttribute{
Computed: true,
Expand All @@ -217,58 +218,82 @@ func (o *objectResource) Update(ctx context.Context, req resource.UpdateRequest,
return
}

reqParams := &s3.PutObjectInput{
Bucket: state.Bucket.ValueStringPointer(),
Key: state.Key.ValueStringPointer(),
}

// get body from plan with source path or existing object
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
}
defer file.Close()

reqParams := &s3.PutObjectInput{
reqParams.Body = file
} else {
getReqParams := &s3.GetObjectInput{
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()
}
tflog.Info(ctx, "GetObject at update operation reqParams="+common.MarshalUncheckedString(getReqParams))

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)
getOutput, err := o.config.Client.ObjectStorage.GetObject(ctx, getReqParams)
if err != nil {
resp.Diagnostics.AddError("UPDATING ERROR", err.Error())
return
}
if output == nil {
resp.Diagnostics.AddError("UPDATING ERROR", "response invalid")
if getOutput == nil {
resp.Diagnostics.AddError("UPDATING ERROR", "response invalid at get object")
return
}

tflog.Info(ctx, "PutObject at update operation response="+common.MarshalUncheckedString(output))
tflog.Info(ctx, "GetObject at update operation response="+common.MarshalUncheckedString(getOutput))

if err := waitObjectUploaded(ctx, o.config, plan.Bucket.ValueString(), plan.Key.ValueString()); err != nil {
resp.Diagnostics.AddError("UPDATING ERROR", err.Error())
return
}
reqParams.Body = getOutput.Body
}

plan.refreshFromOutput(ctx, o.config, &resp.Diagnostics)
resp.Diagnostics.Append(resp.State.Set(ctx, plan)...)
// 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.WebsiteRedirectLocation.IsNull() && !plan.WebsiteRedirectLocation.IsUnknown() {
reqParams.WebsiteRedirectLocation = plan.WebsiteRedirectLocation.ValueStringPointer()
}

if !plan.ContentType.Equal(state.ContentType) && !plan.ContentType.IsNull() && !plan.ContentType.IsUnknown() {
reqParams.ContentType = plan.ContentType.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 at put object")
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) {
Expand Down Expand Up @@ -445,5 +470,5 @@ func ObjectIDParser(id string) (bucketName, key string) {
return "", ""
}

return parts[0], parts[1]
return parts[0], strings.Join(parts[1:], "/")
}
6 changes: 1 addition & 5 deletions internal/service/objectstorage/objectstorage_object_acl.go
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ func (o *objectACLResource) Schema(_ context.Context, req resource.SchemaRequest
},
Validators: []validator.String{
stringvalidator.All(
stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z0-9-_]+\/[a-zA-Z0-9_.-]+$`), "Requires pattern with link of target object"),
stringvalidator.RegexMatches(regexp.MustCompile(`^[a-z0-9-_.-]+(\/[a-z0-9-_.-]+)+$`), "Requires pattern with link of target object"),
),
},
Description: "Target object id",
Expand Down Expand Up @@ -143,19 +143,15 @@ func (o *objectACLResource) Schema(_ context.Context, req resource.SchemaRequest
},
"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,
},
},
},
Expand Down
41 changes: 39 additions & 2 deletions internal/service/objectstorage/objectstorage_object_acl_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ import (

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))
sourceName := fmt.Sprintf("%s.md", acctest.RandString(5))
key := "test/key/path" + sourceName
content := "content for file upload testing"
aclOptions := []string{string(awsTypes.ObjectCannedACLPrivate),
string(awsTypes.ObjectCannedACLPublicRead),
Expand All @@ -30,7 +31,7 @@ func TestAccResourceNcloudObjectStorage_object_acl_basic(t *testing.T) {
acl := aclOptions[acctest.RandIntRange(0, len(aclOptions)-1)]
resourceName := "ncloud_objectstorage_object_acl.testing_acl"

tmpFile := CreateTempFile(t, content, key)
tmpFile := CreateTempFile(t, content, sourceName)
source := tmpFile.Name()
defer os.Remove(source)

Expand All @@ -49,6 +50,42 @@ func TestAccResourceNcloudObjectStorage_object_acl_basic(t *testing.T) {
})
}

func TestAccResourceNcloudObjectStorage_object_acl_update(t *testing.T) {
bucketName := fmt.Sprintf("tf-bucket-%s", acctest.RandString(5))
sourceName := fmt.Sprintf("%s.md", acctest.RandString(5))
key := "test/key/path" + sourceName
content := "content for file upload testing"

acl := "public-read"
newACL := "private"
resourceName := "ncloud_objectstorage_object_acl.testing_acl"

tmpFile := CreateTempFile(t, content, sourceName)
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),
),
},
{
Config: testAccObjectACLConfig(bucketName, key, source, newACL),
Check: resource.ComposeAggregateTestCheckFunc(
testAccCheckObjectACLExists(resourceName, GetTestProvider(true)),
resource.TestCheckResourceAttr(resourceName, "rule", newACL),
),
},
},
})
}

func testAccCheckObjectACLExists(n string, provider *schema.Provider) resource.TestCheckFunc {
return func(s *terraform.State) error {
resource, ok := s.RootModule().Resources[n]
Expand Down
Loading

0 comments on commit 64e4747

Please sign in to comment.