Skip to content

Commit b529b8c

Browse files
authored
feat(storage): support Bucket custom placement config (#9481)
1 parent 3753156 commit b529b8c

8 files changed

Lines changed: 237 additions & 3 deletions

google/cloud/storage/bucket_metadata.cc

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,12 @@ std::ostream& operator<<(std::ostream& os, BucketRetentionPolicy const& rhs) {
133133
<< ", locked=" << rhs.is_locked << "}";
134134
}
135135

136+
std::ostream& operator<<(std::ostream& os,
137+
BucketCustomPlacementConfig const& rhs) {
138+
return os << "BucketCustomPlacementConfig=["
139+
<< absl::StrJoin(rhs.data_locations, ", ") << "]";
140+
}
141+
136142
bool operator==(BucketMetadata const& lhs, BucketMetadata const& rhs) {
137143
return static_cast<internal::CommonMetadata<BucketMetadata> const&>(lhs) ==
138144
rhs &&
@@ -148,7 +154,8 @@ bool operator==(BucketMetadata const& lhs, BucketMetadata const& rhs) {
148154
lhs.logging_ == rhs.logging_ && lhs.labels_ == rhs.labels_ &&
149155
lhs.retention_policy_ == rhs.retention_policy_ &&
150156
lhs.rpo_ == rhs.rpo_ && lhs.versioning_ == rhs.versioning_ &&
151-
lhs.website_ == rhs.website_;
157+
lhs.website_ == rhs.website_ &&
158+
lhs.custom_placement_config_ == rhs.custom_placement_config_;
152159
}
153160

154161
std::ostream& operator<<(std::ostream& os, BucketMetadata const& rhs) {
@@ -245,6 +252,12 @@ std::ostream& operator<<(std::ostream& os, BucketMetadata const& rhs) {
245252
<< ", website.not_found_page=" << rhs.website().not_found_page;
246253
}
247254

255+
if (rhs.has_custom_placement_config()) {
256+
os << ", custom_placement_config.data_locations=["
257+
<< absl::StrJoin(rhs.custom_placement_config().data_locations, ", ")
258+
<< "]";
259+
}
260+
248261
return os << "}";
249262
}
250263

google/cloud/storage/bucket_metadata.h

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,56 @@ inline bool operator>=(BucketWebsite const& lhs, BucketWebsite const& rhs) {
521521
return std::rel_ops::operator>=(lhs, rhs);
522522
}
523523

524+
/**
525+
* Configuration for Custom Dual Regions.
526+
*
527+
* It should specify precisely two eligible regions within the same Multiregion.
528+
*
529+
* @see Additional information on custom dual regions in the
530+
* [feature documentation][cdr-link].
531+
*
532+
* [cdr-link]: https://cloud.google.com/storage/docs/locations
533+
*/
534+
struct BucketCustomPlacementConfig {
535+
std::vector<std::string> data_locations;
536+
};
537+
538+
//@{
539+
/// @name Comparison operators for BucketCustomPlacementConfig.
540+
inline bool operator==(BucketCustomPlacementConfig const& lhs,
541+
BucketCustomPlacementConfig const& rhs) {
542+
return lhs.data_locations == rhs.data_locations;
543+
}
544+
545+
inline bool operator<(BucketCustomPlacementConfig const& lhs,
546+
BucketCustomPlacementConfig const& rhs) {
547+
return lhs.data_locations < rhs.data_locations;
548+
}
549+
550+
inline bool operator!=(BucketCustomPlacementConfig const& lhs,
551+
BucketCustomPlacementConfig const& rhs) {
552+
return std::rel_ops::operator!=(lhs, rhs);
553+
}
554+
555+
inline bool operator>(BucketCustomPlacementConfig const& lhs,
556+
BucketCustomPlacementConfig const& rhs) {
557+
return std::rel_ops::operator>(lhs, rhs);
558+
}
559+
560+
inline bool operator<=(BucketCustomPlacementConfig const& lhs,
561+
BucketCustomPlacementConfig const& rhs) {
562+
return std::rel_ops::operator<=(lhs, rhs);
563+
}
564+
565+
inline bool operator>=(BucketCustomPlacementConfig const& lhs,
566+
BucketCustomPlacementConfig const& rhs) {
567+
return std::rel_ops::operator>=(lhs, rhs);
568+
}
569+
//@}
570+
571+
std::ostream& operator<<(std::ostream& os,
572+
BucketCustomPlacementConfig const& rhs);
573+
524574
/**
525575
* Represents a Google Cloud Storage Bucket Metadata object.
526576
*/
@@ -912,6 +962,30 @@ class BucketMetadata : private internal::CommonMetadata<BucketMetadata> {
912962
}
913963
///@}
914964

965+
/// @name Accessors and modifiers for custom placement configuration.
966+
///@{
967+
bool has_custom_placement_config() const {
968+
return custom_placement_config_.has_value();
969+
}
970+
BucketCustomPlacementConfig const& custom_placement_config() const {
971+
return *custom_placement_config_;
972+
}
973+
absl::optional<BucketCustomPlacementConfig> const&
974+
custom_placement_config_as_optional() const {
975+
return custom_placement_config_;
976+
}
977+
/// Placement configuration can only be set when the bucket is created.
978+
BucketMetadata& set_custom_placement_config(BucketCustomPlacementConfig v) {
979+
custom_placement_config_ = std::move(v);
980+
return *this;
981+
}
982+
/// Placement configuration can only be set when the bucket is created.
983+
BucketMetadata& reset_custom_placement_config() {
984+
custom_placement_config_.reset();
985+
return *this;
986+
}
987+
///@}
988+
915989
friend bool operator==(BucketMetadata const& lhs, BucketMetadata const& rhs);
916990
friend bool operator!=(BucketMetadata const& lhs, BucketMetadata const& rhs) {
917991
return !(lhs == rhs);
@@ -940,6 +1014,7 @@ class BucketMetadata : private internal::CommonMetadata<BucketMetadata> {
9401014
std::string rpo_;
9411015
absl::optional<BucketVersioning> versioning_;
9421016
absl::optional<BucketWebsite> website_;
1017+
absl::optional<BucketCustomPlacementConfig> custom_placement_config_;
9431018
};
9441019

9451020
std::ostream& operator<<(std::ostream& os, BucketMetadata const& rhs);

google/cloud/storage/bucket_metadata_test.cc

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,9 @@ BucketMetadata CreateBucketMetadataForTest() {
163163
"website": {
164164
"mainPageSuffix": "index.html",
165165
"notFoundPage": "404.html"
166+
},
167+
"customPlacementConfig": {
168+
"dataLocations": ["us-central1", "us-east1"]
166169
}
167170
})""";
168171
return internal::BucketMetadataParser::FromString(text).value();
@@ -277,6 +280,14 @@ TEST(BucketMetadataTest, Parse) {
277280
ASSERT_TRUE(actual.has_website());
278281
EXPECT_EQ("index.html", actual.website().main_page_suffix);
279282
EXPECT_EQ("404.html", actual.website().not_found_page);
283+
284+
// custom placement config
285+
ASSERT_TRUE(actual.has_custom_placement_config());
286+
EXPECT_THAT(actual.custom_placement_config().data_locations,
287+
ElementsAre("us-central1", "us-east1"));
288+
ASSERT_TRUE(actual.custom_placement_config_as_optional().has_value());
289+
EXPECT_THAT(actual.custom_placement_config_as_optional()->data_locations,
290+
ElementsAre("us-central1", "us-east1"));
280291
}
281292

282293
/// @test Verify that the IOStream operator works as expected.
@@ -350,6 +361,12 @@ TEST(BucketMetadataTest, IOStream) {
350361
// website()
351362
EXPECT_THAT(actual, HasSubstr("index.html"));
352363
EXPECT_THAT(actual, HasSubstr("404.html"));
364+
365+
// custom_placement_config()
366+
EXPECT_THAT(
367+
actual,
368+
HasSubstr(
369+
"custom_placement_config.data_locations=[us-central1, us-east1]"));
353370
}
354371

355372
/// @test Verify we can convert a BucketMetadata object to a JSON string.
@@ -476,6 +493,13 @@ TEST(BucketMetadataTest, ToJsonString) {
476493
ASSERT_TRUE(actual["website"].is_object()) << actual;
477494
EXPECT_EQ("index.html", actual["website"].value("mainPageSuffix", ""));
478495
EXPECT_EQ("404.html", actual["website"].value("notFoundPage", ""));
496+
497+
// custom_placement_config()
498+
ASSERT_TRUE(actual.contains("customPlacementConfig")) << actual;
499+
auto expected_custom_placement_config = nlohmann::json{
500+
{"dataLocations", std::vector<std::string>{"us-central1", "us-east1"}},
501+
};
502+
EXPECT_EQ(actual["customPlacementConfig"], expected_custom_placement_config);
479503
}
480504

481505
TEST(BucketMetadataTest, ToJsonLifecycleRoundtrip) {
@@ -863,6 +887,39 @@ TEST(BucketMetadataTest, ResetWebsite) {
863887
EXPECT_THAT(os.str(), Not(HasSubstr("website.")));
864888
}
865889

890+
/// @test Verify we can set the custom_placement_config field in BucketMetadata.
891+
TEST(BucketMetadataTest, SetCustomPlacementConfig) {
892+
auto expected = CreateBucketMetadataForTest();
893+
auto copy = expected;
894+
copy.set_custom_placement_config(
895+
BucketCustomPlacementConfig{{"test-location-1", "test-location-2"}});
896+
ASSERT_TRUE(copy.has_custom_placement_config());
897+
EXPECT_THAT(copy.custom_placement_config().data_locations,
898+
ElementsAre("test-location-1", "test-location-2"));
899+
ASSERT_TRUE(copy.custom_placement_config_as_optional().has_value());
900+
EXPECT_THAT(copy.custom_placement_config_as_optional()->data_locations,
901+
ElementsAre("test-location-1", "test-location-2"));
902+
EXPECT_NE(copy, expected);
903+
std::ostringstream os;
904+
os << copy;
905+
EXPECT_THAT(os.str(), HasSubstr("custom_placement_config"));
906+
}
907+
908+
/// @test Verify we can set the custom_placement_config field in BucketMetadata.
909+
TEST(BucketMetadataTest, ResetCustomPlacementConfig) {
910+
auto expected = CreateBucketMetadataForTest();
911+
EXPECT_TRUE(expected.has_custom_placement_config());
912+
EXPECT_TRUE(expected.custom_placement_config_as_optional().has_value());
913+
auto copy = expected;
914+
copy.reset_custom_placement_config();
915+
EXPECT_FALSE(copy.has_custom_placement_config());
916+
EXPECT_FALSE(copy.custom_placement_config_as_optional().has_value());
917+
EXPECT_NE(copy, expected);
918+
std::ostringstream os;
919+
os << copy;
920+
EXPECT_THAT(os.str(), Not(HasSubstr("custom_placement_config")));
921+
}
922+
866923
TEST(BucketMetadataPatchBuilder, SetAcl) {
867924
BucketMetadataPatchBuilder builder;
868925
builder.SetAcl({internal::BucketAccessControlParser::FromString(

google/cloud/storage/examples/storage_bucket_samples.cc

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,8 @@ void CreateBucketDualRegion(google::cloud::storage::Client client,
146146
std::string const& region_a, std::string const& region_b) {
147147
auto metadata = client.CreateBucket(
148148
bucket_name,
149-
gcs::BucketMetadata().set_location(region_a + '+' + region_b));
149+
gcs::BucketMetadata().set_custom_placement_config(
150+
gcs::BucketCustomPlacementConfig{{region_a, region_b}}));
150151
if (!metadata) throw std::runtime_error(metadata.status().message());
151152

152153
std::cout << "Bucket " << metadata->name() << " created."
@@ -711,7 +712,7 @@ void RunAll(std::vector<std::string> const& argv) {
711712
DeleteBucket(client, {bucket_name});
712713

713714
std::cout << "\nRunning CreateBucketDualRegion example" << std::endl;
714-
CreateBucketDualRegion(client, {dual_bucket_name, "us-east1", "us-central1"});
715+
CreateBucketDualRegion(client, {dual_bucket_name, "us-east4", "us-central1"});
715716

716717
(void)client.DeleteBucket(dual_bucket_name);
717718

google/cloud/storage/internal/bucket_metadata_parser.cc

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,29 @@ Status ParseWebsite(absl::optional<BucketWebsite>& website,
235235
return Status{};
236236
}
237237

238+
Status ParseCustomPlacementConfig(
239+
absl::optional<BucketCustomPlacementConfig>& lhs,
240+
nlohmann::json const& json) {
241+
if (!json.contains("customPlacementConfig")) return Status{};
242+
auto const& field = json["customPlacementConfig"];
243+
auto error = [] {
244+
return Status{StatusCode::kInvalidArgument,
245+
"malformed customPlacementConfig"};
246+
};
247+
if (!field.is_object()) return error();
248+
if (!field.contains("dataLocations")) return Status{};
249+
auto const& locations = field["dataLocations"];
250+
if (!locations.is_array()) return error();
251+
252+
BucketCustomPlacementConfig value;
253+
for (auto const& i : locations.items()) {
254+
if (!i.value().is_string()) return error();
255+
value.data_locations.push_back(i.value().get<std::string>());
256+
}
257+
lhs = std::move(value);
258+
return Status{};
259+
}
260+
238261
void ToJsonAcl(nlohmann::json& json, BucketMetadata const& meta) {
239262
if (meta.acl().empty()) return;
240263
nlohmann::json value;
@@ -416,6 +439,14 @@ void ToJsonWebsite(nlohmann::json& json, BucketMetadata const& meta) {
416439
json["website"] = std::move(value);
417440
}
418441

442+
void ToJsonCustomPlacementConfig(nlohmann::json& json,
443+
BucketMetadata const& meta) {
444+
if (!meta.has_custom_placement_config()) return;
445+
json["customPlacementConfig"] = nlohmann::json{
446+
{"dataLocations", meta.custom_placement_config().data_locations},
447+
};
448+
}
449+
419450
} // namespace
420451

421452
StatusOr<BucketMetadata> BucketMetadataParser::FromJson(
@@ -484,6 +515,9 @@ StatusOr<BucketMetadata> BucketMetadataParser::FromJson(
484515
[](BucketMetadata& meta, nlohmann::json const& json) {
485516
return ParseWebsite(meta.website_, json);
486517
},
518+
[](BucketMetadata& meta, nlohmann::json const& json) {
519+
return ParseCustomPlacementConfig(meta.custom_placement_config_, json);
520+
},
487521
};
488522

489523
BucketMetadata meta{};
@@ -525,6 +559,7 @@ std::string BucketMetadataToJsonString(BucketMetadata const& meta) {
525559
ToJsonStorageClass(json, meta);
526560
ToJsonVersioning(json, meta);
527561
ToJsonWebsite(json, meta);
562+
ToJsonCustomPlacementConfig(json, meta);
528563

529564
return json.dump();
530565
}

google/cloud/storage/internal/grpc_bucket_metadata_parser.cc

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,10 @@ google::storage::v2::Bucket GrpcBucketMetadataParser::ToProto(
100100
if (rhs.has_iam_configuration()) {
101101
*result.mutable_iam_config() = ToProto(rhs.iam_configuration());
102102
}
103+
if (rhs.has_custom_placement_config()) {
104+
*result.mutable_custom_placement_config() =
105+
ToProto(rhs.custom_placement_config());
106+
}
103107
return result;
104108
}
105109

@@ -169,6 +173,10 @@ BucketMetadata GrpcBucketMetadataParser::FromProto(
169173
}
170174
if (rhs.has_versioning()) metadata.versioning_ = FromProto(rhs.versioning());
171175
if (rhs.has_website()) metadata.website_ = FromProto(rhs.website());
176+
if (rhs.has_custom_placement_config()) {
177+
metadata.custom_placement_config_ =
178+
FromProto(rhs.custom_placement_config());
179+
}
172180

173181
return metadata;
174182
}
@@ -479,6 +487,24 @@ BucketWebsite GrpcBucketMetadataParser::FromProto(
479487
return result;
480488
}
481489

490+
google::storage::v2::Bucket::CustomPlacementConfig
491+
GrpcBucketMetadataParser::ToProto(BucketCustomPlacementConfig rhs) {
492+
google::storage::v2::Bucket::CustomPlacementConfig result;
493+
for (auto& l : rhs.data_locations) {
494+
*result.add_data_locations() = std::move(l);
495+
}
496+
return result;
497+
}
498+
499+
BucketCustomPlacementConfig GrpcBucketMetadataParser::FromProto(
500+
google::storage::v2::Bucket::CustomPlacementConfig rhs) {
501+
BucketCustomPlacementConfig result;
502+
for (auto& l : *rhs.mutable_data_locations()) {
503+
result.data_locations.push_back(std::move(l));
504+
}
505+
return result;
506+
}
507+
482508
} // namespace internal
483509
GOOGLE_CLOUD_CPP_INLINE_NAMESPACE_END
484510
} // namespace storage

google/cloud/storage/internal/grpc_bucket_metadata_parser.h

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,11 @@ struct GrpcBucketMetadataParser {
8080

8181
static google::storage::v2::Bucket::Website ToProto(BucketWebsite rhs);
8282
static BucketWebsite FromProto(google::storage::v2::Bucket::Website rhs);
83+
84+
static google::storage::v2::Bucket::CustomPlacementConfig ToProto(
85+
BucketCustomPlacementConfig rhs);
86+
static BucketCustomPlacementConfig FromProto(
87+
google::storage::v2::Bucket::CustomPlacementConfig rhs);
8388
};
8489

8590
} // namespace internal

0 commit comments

Comments
 (0)