Skip to content

Commit e07ab13

Browse files
gurusai-voletirileykarson
authored andcommitted
feat: add HNS support for storage bucket (GoogleCloudPlatform#11852)
Co-authored-by: Riley Karson <[email protected]>
1 parent 61c876c commit e07ab13

File tree

3 files changed

+232
-0
lines changed

3 files changed

+232
-0
lines changed

mmv1/third_party/terraform/services/storage/resource_storage_bucket.go.tmpl

+68
Original file line numberDiff line numberDiff line change
@@ -549,6 +549,24 @@ func ResourceStorageBucket() *schema.Resource {
549549
},
550550
},
551551
},
552+
"hierarchical_namespace": {
553+
Type: schema.TypeList,
554+
MaxItems: 1,
555+
Optional: true,
556+
ForceNew: true,
557+
DiffSuppressFunc: hierachicalNamespaceDiffSuppress,
558+
Description: `The bucket's HNS support, which defines bucket can organize folders in logical file system structure`,
559+
Elem: &schema.Resource{
560+
Schema: map[string]*schema.Schema{
561+
"enabled": {
562+
Type: schema.TypeBool,
563+
Required: true,
564+
ForceNew: true,
565+
Description: `Set this enabled flag to true when folders with logical files structure. Default value is false.`,
566+
},
567+
},
568+
},
569+
},
552570
},
553571
UseJSONNumber: true,
554572
}
@@ -696,6 +714,10 @@ func resourceStorageBucketCreate(d *schema.ResourceData, meta interface{}) error
696714
sb.SoftDeletePolicy = expandBucketSoftDeletePolicy(v.([]interface{}))
697715
}
698716

717+
if v, ok := d.GetOk("hierarchical_namespace"); ok {
718+
sb.HierarchicalNamespace = expandBucketHierachicalNamespace(v.([]interface{}))
719+
}
720+
699721
var res *storage.Bucket
700722

701723
err = transport_tpg.Retry(transport_tpg.RetryOptions{
@@ -1294,6 +1316,38 @@ func flattenBucketSoftDeletePolicy(softDeletePolicy *storage.BucketSoftDeletePol
12941316
return policies
12951317
}
12961318

1319+
func expandBucketHierachicalNamespace(configured interface{}) *storage.BucketHierarchicalNamespace {
1320+
configuredHierachicalNamespace := configured.([]interface{})
1321+
if len(configuredHierachicalNamespace) == 0 {
1322+
return nil
1323+
}
1324+
configuredHierachicalNamespacePolicy := configuredHierachicalNamespace[0].(map[string]interface{})
1325+
hierachicalNamespacePolicy := &storage.BucketHierarchicalNamespace{
1326+
Enabled: (configuredHierachicalNamespacePolicy["enabled"].(bool)),
1327+
}
1328+
hierachicalNamespacePolicy.ForceSendFields = append(hierachicalNamespacePolicy.ForceSendFields, "Enabled")
1329+
return hierachicalNamespacePolicy
1330+
}
1331+
1332+
func flattenBucketHierarchicalNamespacePolicy(hierachicalNamespacePolicy *storage.BucketHierarchicalNamespace) []map[string]interface{} {
1333+
policies := make([]map[string]interface{}, 0, 1)
1334+
if hierachicalNamespacePolicy == nil {
1335+
// a null object returned from the API is equivalent to a block with enabled = false
1336+
// to handle this consistently, always write a null response as a hydrated block with false
1337+
defaultPolicy :=map[string]interface{}{
1338+
"enabled": false,
1339+
}
1340+
1341+
policies = append(policies, defaultPolicy)
1342+
return policies
1343+
}
1344+
policy := map[string]interface{}{
1345+
"enabled": hierachicalNamespacePolicy.Enabled,
1346+
}
1347+
policies = append(policies, policy)
1348+
return policies
1349+
}
1350+
12971351
func expandBucketVersioning(configured interface{}) *storage.BucketVersioning {
12981352
versionings := configured.([]interface{})
12991353
if len(versionings) == 0 {
@@ -1885,6 +1939,9 @@ func setStorageBucket(d *schema.ResourceData, config *transport_tpg.Config, res
18851939
if err := d.Set("soft_delete_policy", flattenBucketSoftDeletePolicy(res.SoftDeletePolicy)); err != nil {
18861940
return fmt.Errorf("Error setting soft_delete_policy: %s", err)
18871941
}
1942+
if err := d.Set("hierarchical_namespace", flattenBucketHierarchicalNamespacePolicy(res.HierarchicalNamespace)); err != nil {
1943+
return fmt.Errorf("Error setting hierarchical namespace: %s", err)
1944+
}
18881945
if res.IamConfiguration != nil && res.IamConfiguration.UniformBucketLevelAccess != nil {
18891946
if err := d.Set("uniform_bucket_level_access", res.IamConfiguration.UniformBucketLevelAccess.Enabled); err != nil {
18901947
return fmt.Errorf("Error setting uniform_bucket_level_access: %s", err)
@@ -1914,3 +1971,14 @@ func setStorageBucket(d *schema.ResourceData, config *transport_tpg.Config, res
19141971
d.SetId(res.Id)
19151972
return nil
19161973
}
1974+
1975+
func hierachicalNamespaceDiffSuppress(k, old, new string, r *schema.ResourceData) bool {
1976+
if k == "hierarchical_namespace.#" && old == "1" && new == "0" {
1977+
o, _ := r.GetChange("hierarchical_namespace.0.enabled")
1978+
if !o.(bool) {
1979+
return true
1980+
}
1981+
}
1982+
1983+
return false
1984+
}

mmv1/third_party/terraform/services/storage/resource_storage_bucket_test.go.tmpl

+142
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
"github.com/hashicorp/terraform-plugin-testing/helper/resource"
12+
"github.com/hashicorp/terraform-plugin-testing/plancheck"
1213
"github.com/hashicorp/terraform-plugin-testing/terraform"
1314

1415
"github.com/hashicorp/terraform-provider-google/google/acctest"
@@ -1545,6 +1546,147 @@ func TestAccStorageBucket_SoftDeletePolicy(t *testing.T) {
15451546
})
15461547
}
15471548

1549+
// testcase to create HNS bucket and
1550+
// forcenew to recreate the bucket if HNS set to false
1551+
func TestAccStorageBucket_basic_hns(t *testing.T) {
1552+
t.Parallel()
1553+
1554+
bucketName := acctest.TestBucketName(t)
1555+
acctest.VcrTest(t, resource.TestCase{
1556+
PreCheck: func() { acctest.AccTestPreCheck(t) },
1557+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
1558+
CheckDestroy: testAccStorageBucketDestroyProducer(t),
1559+
Steps: []resource.TestStep{
1560+
{
1561+
Config: testAccStorageBucket_basic_hns(bucketName, true),
1562+
Check: resource.ComposeTestCheckFunc(
1563+
resource.TestCheckResourceAttr(
1564+
"google_storage_bucket.bucket", "hierarchical_namespace.0.enabled", "true"),
1565+
),
1566+
},
1567+
{
1568+
ResourceName: "google_storage_bucket.bucket",
1569+
ImportState: true,
1570+
ImportStateVerify: true,
1571+
ImportStateVerifyIgnore: []string{"force_destroy"},
1572+
},
1573+
{
1574+
Config: testAccStorageBucket_basic_hns_with_data(bucketName, false),
1575+
},
1576+
{
1577+
ResourceName: "google_storage_bucket.bucket",
1578+
ImportState: true,
1579+
ImportStateVerify: true,
1580+
ImportStateVerifyIgnore: []string{"force_destroy"},
1581+
},
1582+
{
1583+
Config: testAccStorageBucket_uniformBucketAccessOnly(bucketName, true),
1584+
ConfigPlanChecks: resource.ConfigPlanChecks{
1585+
PreApply: []plancheck.PlanCheck{
1586+
plancheck.ExpectEmptyPlan(),
1587+
},
1588+
},
1589+
},
1590+
{
1591+
ResourceName: "google_storage_bucket.bucket",
1592+
ImportState: true,
1593+
ImportStateVerify: true,
1594+
ImportStateVerifyIgnore: []string{"force_destroy"},
1595+
},
1596+
},
1597+
})
1598+
}
1599+
1600+
func TestAccStorageBucket_hns_force_destroy(t *testing.T) {
1601+
t.Parallel()
1602+
1603+
bucketName := acctest.TestBucketName(t)
1604+
1605+
acctest.VcrTest(t, resource.TestCase{
1606+
PreCheck: func() { acctest.AccTestPreCheck(t) },
1607+
ProtoV5ProviderFactories: acctest.ProtoV5ProviderFactories(t),
1608+
CheckDestroy: testAccStorageBucketDestroyProducer(t),
1609+
Steps: []resource.TestStep{
1610+
{
1611+
Config: testAccStorageBucket_basic_hns_with_data(bucketName, true),
1612+
Check: resource.ComposeTestCheckFunc(
1613+
testAccCheckStorageBucketPutFolderItem(t, bucketName),
1614+
),
1615+
},
1616+
},
1617+
})
1618+
}
1619+
1620+
func testAccCheckStorageBucketPutFolderItem(t *testing.T, bucketName string) resource.TestCheckFunc {
1621+
return func(s *terraform.State) error {
1622+
config := acctest.GoogleProviderConfig(t)
1623+
1624+
data := bytes.NewBufferString("test")
1625+
dataReader := bytes.NewReader(data.Bytes())
1626+
folderName := fmt.Sprintf("tf-test/tf-test-folder-%d/", acctest.RandInt(t))
1627+
emptyfolderName := fmt.Sprintf("tf-test/tf-test-folder-%d/", acctest.RandInt(t))
1628+
object := &storage.Object{Name: folderName + "bucketDestroyTestFile"}
1629+
1630+
folder := storage.Folder{
1631+
Bucket: bucketName,
1632+
Name: folderName,
1633+
}
1634+
1635+
emptyFolder := storage.Folder{
1636+
Bucket: bucketName,
1637+
Name: emptyfolderName,
1638+
}
1639+
1640+
if res, err := config.NewStorageClient(config.UserAgent).Folders.Insert(bucketName, &folder).Recursive(true).Do(); err == nil {
1641+
log.Printf("[INFO] Created folder %v at location %v\n\n", res.Name, res.SelfLink)
1642+
} else {
1643+
return fmt.Errorf("Folders.Insert failed: %v", err)
1644+
}
1645+
1646+
// This needs to use Media(io.Reader) call, otherwise it does not go to /upload API and fails
1647+
if res, err := config.NewStorageClient(config.UserAgent).Objects.Insert(bucketName, object).Media(dataReader).Do(); err == nil {
1648+
log.Printf("[INFO] Created object %v at location %v\n\n", res.Name, res.SelfLink)
1649+
} else {
1650+
return fmt.Errorf("Objects.Insert failed: %v", err)
1651+
}
1652+
1653+
if res, err := config.NewStorageClient(config.UserAgent).Folders.Insert(bucketName, &emptyFolder).Recursive(true).Do(); err == nil {
1654+
log.Printf("[INFO] Created folder %v at location %v\n\n", res.Name, res.SelfLink)
1655+
} else {
1656+
return fmt.Errorf("Folders.Insert failed: %v", err)
1657+
}
1658+
1659+
return nil
1660+
}
1661+
}
1662+
1663+
func testAccStorageBucket_basic_hns(bucketName string, enabled bool) string {
1664+
return fmt.Sprintf(`
1665+
resource "google_storage_bucket" "bucket" {
1666+
name = "%s"
1667+
location = "US"
1668+
uniform_bucket_level_access = true
1669+
hierarchical_namespace {
1670+
enabled = %t
1671+
}
1672+
}
1673+
`, bucketName, enabled)
1674+
}
1675+
1676+
func testAccStorageBucket_basic_hns_with_data(bucketName string, enabled bool) string {
1677+
return fmt.Sprintf(`
1678+
resource "google_storage_bucket" "bucket" {
1679+
name = "%s"
1680+
location = "US"
1681+
uniform_bucket_level_access = true
1682+
hierarchical_namespace {
1683+
enabled = %t
1684+
}
1685+
force_destroy= true
1686+
}
1687+
`, bucketName, enabled)
1688+
}
1689+
15481690
func testAccCheckStorageBucketExists(t *testing.T, n string, bucketName string, bucket *storage.Bucket) resource.TestCheckFunc {
15491691
return func(s *terraform.State) error {
15501692
rs, ok := s.RootModule().Resources[n]

mmv1/third_party/terraform/website/docs/r/storage_bucket.html.markdown

+22
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,20 @@ resource "google_storage_bucket" "auto-expire" {
103103
}
104104
```
105105

106+
## Example Usage - Enabling hierarchical namespace
107+
108+
```hcl
109+
resource "google_storage_bucket" "auto-expire" {
110+
name = "hns-enabled-bucket"
111+
location = "US"
112+
force_destroy = true
113+
114+
hierarchical_namespace = {
115+
enabled = true
116+
}
117+
}
118+
```
119+
106120
## Argument Reference
107121

108122
The following arguments are supported:
@@ -157,6 +171,8 @@ The following arguments are supported:
157171

158172
* `soft_delete_policy` - (Optional, Computed) The bucket's soft delete policy, which defines the period of time that soft-deleted objects will be retained, and cannot be permanently deleted. If the block is not provided, Server side value will be kept which means removal of block won't generate any terraform change. Structure is [documented below](#nested_soft_delete_policy).
159173

174+
* `hierarchical_namespace` - (Optional, ForceNew) The bucket's hierarchical namespace policy, which defines the bucket capability to handle folders in logical structure. Structure is [documented below](#nested_hierarchical_namespace).
175+
160176
<a name="nested_lifecycle_rule"></a>The `lifecycle_rule` block supports:
161177

162178
* `action` - (Required) The Lifecycle Rule's action configuration. A single block of this type is supported. Structure is [documented below](#nested_action).
@@ -269,6 +285,12 @@ The following arguments are supported:
269285

270286
* `effective_time` - (Computed) Server-determined value that indicates the time from which the policy, or one with a greater retention, was effective. This value is in RFC 3339 format.
271287

288+
<a name="nested_hierarchical_namespace"></a>The `hierarchical_namespace` block supports:
289+
290+
* `enabled` - (Optional) Enable hierarchical namespace for the bucket.
291+
To use this flag, you must also use --uniform-bucket-level-access
292+
293+
272294
## Attributes Reference
273295

274296
In addition to the arguments listed above, the following computed attributes are

0 commit comments

Comments
 (0)