Skip to content

Commit 8ca67ec

Browse files
authored
feat(storage): support HNS-enabled buckets (#13753)
Add accessors and modifiers for the `hierarchical_namespace()` field in a bucket. Parse the field in bucket metadata responses for JSON and gRPC. Add support to set this field on create, update, and patch requests. Also for JSON and gRPC. Include and example (which doubles as an integration test), and the usual unit tests.
1 parent 296d56a commit 8ca67ec

11 files changed

+257
-0
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#include "google/cloud/storage/bucket_hierarchical_namespace.h"
16+
#include "google/cloud/internal/ios_flags_saver.h"
17+
#include <iomanip>
18+
#include <iostream>
19+
20+
namespace google {
21+
namespace cloud {
22+
namespace storage {
23+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
24+
25+
std::ostream& operator<<(std::ostream& os,
26+
BucketHierarchicalNamespace const& rhs) {
27+
google::cloud::internal::IosFlagsSaver flags(os);
28+
return os << "BucketHierarchicalNamespace={enabled=" << std::boolalpha
29+
<< rhs.enabled << "}";
30+
}
31+
32+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
33+
} // namespace storage
34+
} // namespace cloud
35+
} // namespace google
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
// Copyright 2024 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// https://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
#ifndef GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_BUCKET_HIERARCHICAL_NAMESPACE_H
16+
#define GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_BUCKET_HIERARCHICAL_NAMESPACE_H
17+
18+
#include "google/cloud/storage/version.h"
19+
#include <iosfwd>
20+
21+
namespace google {
22+
namespace cloud {
23+
namespace storage {
24+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_BEGIN
25+
26+
/**
27+
* Configuration for a bucket's hierarchical namespace feature.
28+
*/
29+
struct BucketHierarchicalNamespace {
30+
bool enabled;
31+
};
32+
33+
inline bool operator==(BucketHierarchicalNamespace const& lhs,
34+
BucketHierarchicalNamespace const& rhs) {
35+
return lhs.enabled == rhs.enabled;
36+
}
37+
38+
inline bool operator!=(BucketHierarchicalNamespace const& lhs,
39+
BucketHierarchicalNamespace const& rhs) {
40+
return !(lhs == rhs);
41+
}
42+
43+
std::ostream& operator<<(std::ostream& os,
44+
BucketHierarchicalNamespace const& rhs);
45+
46+
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
47+
} // namespace storage
48+
} // namespace cloud
49+
} // namespace google
50+
51+
#endif // GOOGLE_CLOUD_CPP_GOOGLE_CLOUD_STORAGE_BUCKET_HIERARCHICAL_NAMESPACE_H

google/cloud/storage/bucket_metadata.cc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ bool operator==(BucketMetadata const& lhs, BucketMetadata const& rhs) {
9191
&& lhs.default_event_based_hold_ == rhs.default_event_based_hold_ //
9292
&& lhs.encryption_ == rhs.encryption_ //
9393
&& lhs.etag_ == rhs.etag_ //
94+
&& lhs.hierarchical_namespace_ == rhs.hierarchical_namespace_ //
9495
&& lhs.iam_configuration_ == rhs.iam_configuration_ //
9596
&& lhs.id_ == rhs.id_ //
9697
&& lhs.kind_ == rhs.kind_ //
@@ -151,6 +152,10 @@ std::ostream& operator<<(std::ostream& os, BucketMetadata const& rhs) {
151152

152153
os << ", etag=" << rhs.etag();
153154

155+
if (rhs.has_hierarchical_namespace()) {
156+
os << ", hierarchical_namespace=" << rhs.hierarchical_namespace();
157+
}
158+
154159
if (rhs.has_iam_configuration()) {
155160
os << ", iam_configuration=" << rhs.iam_configuration();
156161
}
@@ -390,6 +395,21 @@ BucketMetadataPatchBuilder::ResetIamConfiguration() {
390395
return *this;
391396
}
392397

398+
BucketMetadataPatchBuilder&
399+
BucketMetadataPatchBuilder::SetHierarchicalNamespace(
400+
BucketHierarchicalNamespace const& v) {
401+
internal::PatchBuilder subpatch;
402+
subpatch.SetBoolField("enabled", v.enabled);
403+
impl_.AddSubPatch("hierarchicalNamespace", subpatch);
404+
return *this;
405+
}
406+
407+
BucketMetadataPatchBuilder&
408+
BucketMetadataPatchBuilder::ResetHierarchicalNamespace() {
409+
impl_.RemoveField("hierarchicalNamespace");
410+
return *this;
411+
}
412+
393413
BucketMetadataPatchBuilder& BucketMetadataPatchBuilder::SetLabel(
394414
std::string const& label, std::string const& value) {
395415
labels_subpatch_.SetStringField(label.c_str(), value);

google/cloud/storage/bucket_metadata.h

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
#include "google/cloud/storage/bucket_cors_entry.h"
2222
#include "google/cloud/storage/bucket_custom_placement_config.h"
2323
#include "google/cloud/storage/bucket_encryption.h"
24+
#include "google/cloud/storage/bucket_hierarchical_namespace.h"
2425
#include "google/cloud/storage/bucket_iam_configuration.h"
2526
#include "google/cloud/storage/bucket_lifecycle.h"
2627
#include "google/cloud/storage/bucket_logging.h"
@@ -212,6 +213,30 @@ class BucketMetadata {
212213
return *this;
213214
}
214215

216+
/**
217+
* @name Get and set the hierarchical namespaces configuration.
218+
*/
219+
///@{
220+
bool has_hierarchical_namespace() const {
221+
return hierarchical_namespace_.has_value();
222+
}
223+
BucketHierarchicalNamespace const& hierarchical_namespace() const {
224+
return *hierarchical_namespace_;
225+
}
226+
absl::optional<BucketHierarchicalNamespace> const&
227+
hierarchical_namespace_as_optional() const {
228+
return hierarchical_namespace_;
229+
}
230+
BucketMetadata& set_hierarchical_namespace(BucketHierarchicalNamespace v) {
231+
hierarchical_namespace_ = std::move(v);
232+
return *this;
233+
}
234+
BucketMetadata& reset_hierarchical_namespace() {
235+
hierarchical_namespace_.reset();
236+
return *this;
237+
}
238+
///@}
239+
215240
/**
216241
* @name Get and set the IAM configuration.
217242
*
@@ -612,6 +637,7 @@ class BucketMetadata {
612637
bool default_event_based_hold_ = false;
613638
absl::optional<BucketEncryption> encryption_;
614639
std::string etag_;
640+
absl::optional<BucketHierarchicalNamespace> hierarchical_namespace_;
615641
absl::optional<BucketIamConfiguration> iam_configuration_;
616642
std::string id_;
617643
std::string kind_;
@@ -690,6 +716,12 @@ class BucketMetadataPatchBuilder {
690716
BucketIamConfiguration const& v);
691717
BucketMetadataPatchBuilder& ResetIamConfiguration();
692718

719+
/// Sets a new hierarchical namespace configuration.
720+
BucketMetadataPatchBuilder& SetHierarchicalNamespace(
721+
BucketHierarchicalNamespace const& v);
722+
/// Resets the hierarchical namespace configuration
723+
BucketMetadataPatchBuilder& ResetHierarchicalNamespace();
724+
693725
BucketMetadataPatchBuilder& SetEncryption(BucketEncryption const& v);
694726
BucketMetadataPatchBuilder& ResetEncryption();
695727

google/cloud/storage/bucket_metadata_test.cc

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,9 @@ BucketMetadata CreateBucketMetadataForTest() {
107107
"defaultKmsKeyName": "projects/test-project-name/locations/us-central1/keyRings/test-keyring-name/cryptoKeys/test-key-name"
108108
},
109109
"etag": "XYZ=",
110+
"hierarchicalNamespace": {
111+
"enabled": true
112+
},
110113
"iamConfiguration": {
111114
"uniformBucketLevelAccess": {
112115
"enabled": true,
@@ -216,6 +219,10 @@ TEST(BucketMetadataTest, Parse) {
216219
"test-keyring-name/cryptoKeys/test-key-name",
217220
actual.encryption().default_kms_key_name);
218221
EXPECT_EQ("XYZ=", actual.etag());
222+
// hierarchicalNamespace
223+
ASSERT_TRUE(actual.has_hierarchical_namespace());
224+
ASSERT_TRUE(actual.hierarchical_namespace_as_optional().has_value());
225+
ASSERT_EQ(actual.hierarchical_namespace(), BucketHierarchicalNamespace{true});
219226
ASSERT_TRUE(actual.has_iam_configuration());
220227
ASSERT_TRUE(
221228
actual.iam_configuration().uniform_bucket_level_access.has_value());
@@ -361,6 +368,12 @@ TEST(BucketMetadataTest, IOStream) {
361368
HasSubstr("projects/test-project-name/locations/us-central1/"
362369
"keyRings/test-keyring-name/cryptoKeys/test-key-name"));
363370

371+
// hierarchical_namespace()
372+
EXPECT_THAT(
373+
actual,
374+
HasSubstr(
375+
"hierarchical_namespace=BucketHierarchicalNamespace={enabled=true}"));
376+
364377
// iam_policy()
365378
EXPECT_THAT(actual, HasSubstr("BucketIamConfiguration={"));
366379
EXPECT_THAT(actual, HasSubstr("locked_time=2020-01-02T03:04:05Z"));
@@ -466,6 +479,11 @@ TEST(BucketMetadataTest, ToJsonString) {
466479
"test-keyring-name/cryptoKeys/test-key-name",
467480
actual["encryption"].value("defaultKmsKeyName", ""));
468481

482+
// hierarchical_namespace()
483+
ASSERT_EQ(1, actual.count("hierarchicalNamespace"));
484+
EXPECT_EQ(actual["hierarchicalNamespace"],
485+
nlohmann::json({{"enabled", true}}));
486+
469487
// iam_configuration()
470488
ASSERT_EQ(1U, actual.count("iamConfiguration"));
471489
nlohmann::json expected_iam_configuration{
@@ -747,6 +765,25 @@ TEST(BucketMetadataTest, SetDefaultObjectAcl) {
747765
EXPECT_NE(expected, copy);
748766
}
749767

768+
/// @test Verify we can change the Hierarchical Namespace configuration.
769+
TEST(BucketMetadataTest, SetHierarchicalNamespace) {
770+
auto expected = CreateBucketMetadataForTest();
771+
auto copy = expected;
772+
copy.set_hierarchical_namespace(BucketHierarchicalNamespace{false});
773+
ASSERT_TRUE(copy.has_hierarchical_namespace());
774+
EXPECT_EQ(copy.hierarchical_namespace(), BucketHierarchicalNamespace{false});
775+
EXPECT_NE(expected, copy);
776+
}
777+
778+
/// @test Verify we can reset the Hierarchical Namespace configuration.
779+
TEST(BucketMetadataTest, ResetHierarchicalNamespace) {
780+
auto expected = CreateBucketMetadataForTest();
781+
auto copy = expected;
782+
copy.reset_hierarchical_namespace();
783+
ASSERT_FALSE(copy.has_hierarchical_namespace());
784+
EXPECT_NE(expected, copy);
785+
}
786+
750787
/// @test Verify we can change the IAM Configuration in BucketMetadata.
751788
TEST(BucketMetadataTest, SetIamConfigurationUBLA) {
752789
auto expected = CreateBucketMetadataForTest();
@@ -1226,6 +1263,26 @@ TEST(BucketMetadataPatchBuilder, ResetIamConfiguration) {
12261263
ASSERT_TRUE(json["iamConfiguration"].is_null()) << json;
12271264
}
12281265

1266+
TEST(BucketMetadataPatchBuilder, SetHierarchicalNamespace) {
1267+
BucketMetadataPatchBuilder builder;
1268+
builder.SetHierarchicalNamespace(BucketHierarchicalNamespace{true});
1269+
1270+
auto actual = builder.BuildPatch();
1271+
auto json = nlohmann::json::parse(actual);
1272+
ASSERT_EQ(1U, json.count("hierarchicalNamespace")) << json;
1273+
ASSERT_EQ(json["hierarchicalNamespace"], nlohmann::json({{"enabled", true}}));
1274+
}
1275+
1276+
TEST(BucketMetadataPatchBuilder, ResetHierarchicalNamespace) {
1277+
BucketMetadataPatchBuilder builder;
1278+
builder.ResetHierarchicalNamespace();
1279+
1280+
auto actual = builder.BuildPatch();
1281+
auto json = nlohmann::json::parse(actual);
1282+
ASSERT_EQ(1U, json.count("hierarchicalNamespace")) << json;
1283+
ASSERT_TRUE(json["hierarhicalNamespace"].is_null()) << json;
1284+
}
1285+
12291286
TEST(BucketMetadataPatchBuilder, SetEncryption) {
12301287
BucketMetadataPatchBuilder builder;
12311288
std::string expected =

google/cloud/storage/examples/storage_bucket_samples.cc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,26 @@ void CreateBucketDualRegion(google::cloud::storage::Client client,
144144
(std::move(client), argv.at(0), argv.at(1), argv.at(2));
145145
}
146146

147+
void CreateBucketWithHNS(google::cloud::storage::Client client,
148+
std::vector<std::string> const& argv) {
149+
namespace gcs = ::google::cloud::storage;
150+
using ::google::cloud::StatusOr;
151+
[](gcs::Client client, std::string const& bucket_name) {
152+
auto metadata = client.CreateBucket(
153+
bucket_name,
154+
gcs::BucketMetadata()
155+
.set_hierarchical_namespace(
156+
gcs::BucketHierarchicalNamespace{/*.enabled=*/true})
157+
.set_iam_configuration(gcs::BucketIamConfiguration{
158+
gcs::UniformBucketLevelAccess{/*.enabled=*/true, {}},
159+
absl::nullopt}));
160+
if (!metadata) throw std::move(metadata).status();
161+
162+
std::cout << "Bucket " << metadata->name() << " created."
163+
<< "\nFull Metadata: " << *metadata << "\n";
164+
}(std::move(client), argv.at(0));
165+
}
166+
147167
void GetBucketMetadata(google::cloud::storage::Client client,
148168
std::vector<std::string> const& argv) {
149169
//! [get bucket metadata]
@@ -675,6 +695,11 @@ void RunAll(std::vector<std::string> const& argv) {
675695
<< std::endl;
676696
CreateBucketWithStorageClassLocation(client, {bucket_name, "STANDARD", "US"});
677697

698+
auto const hns_bucket_name = examples::MakeRandomBucketName(generator);
699+
std::cout << "\nRunning CreateBucketWithHNS() example" << std::endl;
700+
CreateBucketWithHNS(client, {hns_bucket_name});
701+
(void)client.DeleteBucket(hns_bucket_name);
702+
678703
std::cout << "\nRunning DeleteBucket() example [3]" << std::endl;
679704
DeleteBucket(client, {bucket_name});
680705

@@ -705,6 +730,7 @@ int main(int argc, char* argv[]) {
705730
CreateBucketWithStorageClassLocation),
706731
make_entry("create-bucket-dual-region", {"<region-a>", "<region-b>"},
707732
CreateBucketDualRegion),
733+
make_entry("create-bucket-with-hns", {}, CreateBucketWithHNS),
708734
make_entry("get-bucket-metadata", {}, GetBucketMetadata),
709735
make_entry("delete-bucket", {}, DeleteBucket),
710736
make_entry("change-default-storage-class", {"<new-class>"},

google/cloud/storage/google_cloud_cpp_storage.bzl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ google_cloud_cpp_storage_hdrs = [
2424
"bucket_cors_entry.h",
2525
"bucket_custom_placement_config.h",
2626
"bucket_encryption.h",
27+
"bucket_hierarchical_namespace.h",
2728
"bucket_iam_configuration.h",
2829
"bucket_lifecycle.h",
2930
"bucket_logging.h",
@@ -158,6 +159,7 @@ google_cloud_cpp_storage_srcs = [
158159
"bucket_autoclass.cc",
159160
"bucket_cors_entry.cc",
160161
"bucket_custom_placement_config.cc",
162+
"bucket_hierarchical_namespace.cc",
161163
"bucket_iam_configuration.cc",
162164
"bucket_logging.cc",
163165
"bucket_metadata.cc",

google/cloud/storage/google_cloud_cpp_storage.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ add_library(
3232
bucket_custom_placement_config.cc
3333
bucket_custom_placement_config.h
3434
bucket_encryption.h
35+
bucket_hierarchical_namespace.cc
36+
bucket_hierarchical_namespace.h
3537
bucket_iam_configuration.cc
3638
bucket_iam_configuration.h
3739
bucket_lifecycle.h

0 commit comments

Comments
 (0)