1 //
2 // Copyright (C) 2022 The Android Open Source Project
3 //
4 // Licensed under the Apache License, Version 2.0 (the "License");
5 // you may not use this file except in compliance with the License.
6 // You may obtain a copy of the License at
7 //
8 // http://www.apache.org/licenses/LICENSE-2.0
9 //
10 // Unless required by applicable law or agreed to in writing, software
11 // distributed under the License is distributed on an "AS IS" BASIS,
12 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 // See the License for the specific language governing permissions and
14 // limitations under the License.
15
16 #include "host/commands/test_gce_driver/gce_api.h"
17
18 #include <uuid.h>
19
20 #include <sstream>
21 #include <string>
22
23 #include <android-base/logging.h>
24 #include <android-base/strings.h>
25
26 #include "host/libs/web/credential_source.h"
27
28 namespace cuttlefish {
29
OptStringMember(const Json::Value & jn,const std::string & name)30 std::optional<std::string> OptStringMember(const Json::Value& jn,
31 const std::string& name) {
32 if (!jn.isMember(name) || jn[name].type() != Json::ValueType::stringValue) {
33 return {};
34 }
35 return jn[name].asString();
36 }
37
OptObjMember(const Json::Value & jn,const std::string & name)38 const Json::Value* OptObjMember(const Json::Value& jn,
39 const std::string& name) {
40 if (!jn.isMember(name) || jn[name].type() != Json::ValueType::objectValue) {
41 return nullptr;
42 }
43 return &(jn[name]);
44 }
45
OptArrayMember(const Json::Value & jn,const std::string & name)46 const Json::Value* OptArrayMember(const Json::Value& jn,
47 const std::string& name) {
48 if (!jn.isMember(name) || jn[name].type() != Json::ValueType::arrayValue) {
49 return nullptr;
50 }
51 return &(jn[name]);
52 }
53
EnsureObjMember(Json::Value & jn,const std::string & name)54 Json::Value& EnsureObjMember(Json::Value& jn, const std::string& name) {
55 if (!jn.isMember(name) || jn[name].type() != Json::ValueType::objectValue) {
56 jn[name] = Json::Value(Json::ValueType::objectValue);
57 }
58 return jn[name];
59 }
60
EnsureArrayMember(Json::Value & jn,const std::string & name)61 Json::Value& EnsureArrayMember(Json::Value& jn, const std::string& name) {
62 if (!jn.isMember(name) || jn[name].type() != Json::ValueType::arrayValue) {
63 jn[name] = Json::Value(Json::ValueType::arrayValue);
64 }
65 return jn[name];
66 }
67
GceInstanceDisk(const Json::Value & json)68 GceInstanceDisk::GceInstanceDisk(const Json::Value& json) : data_(json){};
69
EphemeralBootDisk()70 GceInstanceDisk GceInstanceDisk::EphemeralBootDisk() {
71 Json::Value initial_json(Json::ValueType::objectValue);
72 initial_json["type"] = "PERSISTENT";
73 initial_json["boot"] = true;
74 initial_json["mode"] = "READ_WRITE";
75 initial_json["autoDelete"] = true;
76 return GceInstanceDisk(initial_json);
77 }
78
79 constexpr char kGceDiskInitParams[] = "initializeParams";
80 constexpr char kGceDiskName[] = "diskName";
Name() const81 std::optional<std::string> GceInstanceDisk::Name() const {
82 const auto& init_params = OptObjMember(data_, kGceDiskInitParams);
83 if (!init_params) {
84 return {};
85 }
86 return OptStringMember(*init_params, kGceDiskName);
87 }
Name(const std::string & source)88 GceInstanceDisk& GceInstanceDisk::Name(const std::string& source) & {
89 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskName] = source;
90 return *this;
91 }
Name(const std::string & source)92 GceInstanceDisk GceInstanceDisk::Name(const std::string& source) && {
93 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskName] = source;
94 return *this;
95 }
96
97 constexpr char kGceDiskSourceImage[] = "sourceImage";
SourceImage() const98 std::optional<std::string> GceInstanceDisk::SourceImage() const {
99 const auto& init_params = OptObjMember(data_, kGceDiskInitParams);
100 if (!init_params) {
101 return {};
102 }
103 return OptStringMember(*init_params, kGceDiskSourceImage);
104 }
SourceImage(const std::string & source)105 GceInstanceDisk& GceInstanceDisk::SourceImage(const std::string& source) & {
106 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSourceImage] = source;
107 return *this;
108 }
SourceImage(const std::string & source)109 GceInstanceDisk GceInstanceDisk::SourceImage(const std::string& source) && {
110 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSourceImage] = source;
111 return *this;
112 }
113
114 constexpr char kGceDiskSizeGb[] = "diskSizeGb";
SizeGb(uint64_t size)115 GceInstanceDisk& GceInstanceDisk::SizeGb(uint64_t size) & {
116 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSizeGb] = size;
117 return *this;
118 }
SizeGb(uint64_t size)119 GceInstanceDisk GceInstanceDisk::SizeGb(uint64_t size) && {
120 EnsureObjMember(data_, kGceDiskInitParams)[kGceDiskSizeGb] = size;
121 return *this;
122 }
123
AsJson() const124 const Json::Value& GceInstanceDisk::AsJson() const { return data_; }
125
GceNetworkInterface(const Json::Value & data)126 GceNetworkInterface::GceNetworkInterface(const Json::Value& data)
127 : data_(data) {}
128
129 constexpr char kNetwork[] = "network";
130 constexpr char kGceNetworkAccessConfigs[] = "accessConfigs";
Default()131 GceNetworkInterface GceNetworkInterface::Default() {
132 Json::Value json{Json::ValueType::objectValue};
133 json[kNetwork] = "global/networks/default";
134 Json::Value accessConfig{Json::ValueType::objectValue};
135 accessConfig["type"] = "ONE_TO_ONE_NAT";
136 accessConfig["name"] = "External NAT";
137 EnsureArrayMember(json, kGceNetworkAccessConfigs).append(accessConfig);
138 return GceNetworkInterface(json);
139 }
140
Network() const141 std::optional<std::string> GceNetworkInterface::Network() const {
142 return OptStringMember(data_, kNetwork);
143 }
Network(const std::string & network)144 GceNetworkInterface& GceNetworkInterface::Network(
145 const std::string& network) & {
146 data_[kNetwork] = network;
147 return *this;
148 }
Network(const std::string & network)149 GceNetworkInterface GceNetworkInterface::Network(
150 const std::string& network) && {
151 data_[kNetwork] = network;
152 return *this;
153 }
154
155 constexpr char kSubnetwork[] = "subnetwork";
Subnetwork() const156 std::optional<std::string> GceNetworkInterface::Subnetwork() const {
157 return OptStringMember(data_, kSubnetwork);
158 }
Subnetwork(const std::string & subnetwork)159 GceNetworkInterface& GceNetworkInterface::Subnetwork(
160 const std::string& subnetwork) & {
161 data_[kSubnetwork] = subnetwork;
162 return *this;
163 }
Subnetwork(const std::string & subnetwork)164 GceNetworkInterface GceNetworkInterface::Subnetwork(
165 const std::string& subnetwork) && {
166 data_[kSubnetwork] = subnetwork;
167 return *this;
168 }
169
170 constexpr char kGceNetworkExternalIp[] = "natIP";
ExternalIp() const171 std::optional<std::string> GceNetworkInterface::ExternalIp() const {
172 auto accessConfigs = OptArrayMember(data_, kGceNetworkAccessConfigs);
173 if (!accessConfigs || accessConfigs->size() < 1) {
174 return {};
175 }
176 if ((*accessConfigs)[0].type() != Json::ValueType::objectValue) {
177 return {};
178 }
179 return OptStringMember((*accessConfigs)[0], kGceNetworkExternalIp);
180 }
181
182 constexpr char kGceNetworkInternalIp[] = "networkIP";
InternalIp() const183 std::optional<std::string> GceNetworkInterface::InternalIp() const {
184 return OptStringMember(data_, kGceNetworkInternalIp);
185 }
186
AsJson() const187 const Json::Value& GceNetworkInterface::AsJson() const { return data_; }
188
GceInstanceInfo(const Json::Value & json)189 GceInstanceInfo::GceInstanceInfo(const Json::Value& json) : data_(json) {}
190
191 constexpr char kGceZone[] = "zone";
Zone() const192 std::optional<std::string> GceInstanceInfo::Zone() const {
193 return OptStringMember(data_, kGceZone);
194 }
Zone(const std::string & zone)195 GceInstanceInfo& GceInstanceInfo::Zone(const std::string& zone) & {
196 data_[kGceZone] = zone;
197 return *this;
198 }
Zone(const std::string & zone)199 GceInstanceInfo GceInstanceInfo::Zone(const std::string& zone) && {
200 data_[kGceZone] = zone;
201 return *this;
202 }
203
204 constexpr char kGceName[] = "name";
Name() const205 std::optional<std::string> GceInstanceInfo::Name() const {
206 return OptStringMember(data_, kGceName);
207 }
Name(const std::string & name)208 GceInstanceInfo& GceInstanceInfo::Name(const std::string& name) & {
209 data_[kGceName] = name;
210 return *this;
211 }
Name(const std::string & name)212 GceInstanceInfo GceInstanceInfo::Name(const std::string& name) && {
213 data_[kGceName] = name;
214 return *this;
215 }
216
217 constexpr char kGceMachineType[] = "machineType";
MachineType() const218 std::optional<std::string> GceInstanceInfo::MachineType() const {
219 return OptStringMember(data_, kGceMachineType);
220 }
MachineType(const std::string & type)221 GceInstanceInfo& GceInstanceInfo::MachineType(const std::string& type) & {
222 data_[kGceMachineType] = type;
223 return *this;
224 }
MachineType(const std::string & type)225 GceInstanceInfo GceInstanceInfo::MachineType(const std::string& type) && {
226 data_[kGceMachineType] = type;
227 return *this;
228 }
229
230 constexpr char kGceDisks[] = "disks";
AddDisk(const GceInstanceDisk & disk)231 GceInstanceInfo& GceInstanceInfo::AddDisk(const GceInstanceDisk& disk) & {
232 EnsureArrayMember(data_, kGceDisks).append(disk.AsJson());
233 return *this;
234 }
AddDisk(const GceInstanceDisk & disk)235 GceInstanceInfo GceInstanceInfo::AddDisk(const GceInstanceDisk& disk) && {
236 EnsureArrayMember(data_, kGceDisks).append(disk.AsJson());
237 return *this;
238 }
239
240 constexpr char kGceNetworkInterfaces[] = "networkInterfaces";
AddNetworkInterface(const GceNetworkInterface & net)241 GceInstanceInfo& GceInstanceInfo::AddNetworkInterface(
242 const GceNetworkInterface& net) & {
243 EnsureArrayMember(data_, kGceNetworkInterfaces).append(net.AsJson());
244 return *this;
245 }
AddNetworkInterface(const GceNetworkInterface & net)246 GceInstanceInfo GceInstanceInfo::AddNetworkInterface(
247 const GceNetworkInterface& net) && {
248 EnsureArrayMember(data_, kGceNetworkInterfaces).append(net.AsJson());
249 return *this;
250 }
NetworkInterfaces() const251 std::vector<GceNetworkInterface> GceInstanceInfo::NetworkInterfaces() const {
252 auto jsonNetworkInterfaces = OptArrayMember(data_, kGceNetworkInterfaces);
253 if (!jsonNetworkInterfaces) {
254 return {};
255 }
256 std::vector<GceNetworkInterface> interfaces;
257 for (const Json::Value& jsonNetworkInterface : *jsonNetworkInterfaces) {
258 interfaces.push_back(GceNetworkInterface(jsonNetworkInterface));
259 }
260 return interfaces;
261 }
262
263 constexpr char kGceMetadata[] = "metadata";
264 constexpr char kGceMetadataItems[] = "items";
265 constexpr char kGceMetadataKey[] = "key";
266 constexpr char kGceMetadataValue[] = "value";
AddMetadata(const std::string & key,const std::string & value)267 GceInstanceInfo& GceInstanceInfo::AddMetadata(const std::string& key,
268 const std::string& value) & {
269 Json::Value item{Json::ValueType::objectValue};
270 item[kGceMetadataKey] = key;
271 item[kGceMetadataValue] = value;
272 auto& metadata = EnsureObjMember(data_, kGceMetadata);
273 EnsureArrayMember(metadata, kGceMetadataItems).append(item);
274 return *this;
275 }
AddMetadata(const std::string & key,const std::string & value)276 GceInstanceInfo GceInstanceInfo::AddMetadata(const std::string& key,
277 const std::string& value) && {
278 Json::Value item{Json::ValueType::objectValue};
279 item[kGceMetadataKey] = key;
280 item[kGceMetadataValue] = value;
281 auto& metadata = EnsureObjMember(data_, kGceMetadata);
282 EnsureArrayMember(metadata, kGceMetadataItems).append(item);
283 return *this;
284 }
285
286 constexpr char kGceServiceAccounts[] = "serviceAccounts";
287 constexpr char kGceScopes[] = "scopes";
AddScope(const std::string & scope)288 GceInstanceInfo& GceInstanceInfo::AddScope(const std::string& scope) & {
289 auto& serviceAccounts = EnsureArrayMember(data_, kGceServiceAccounts);
290 if (serviceAccounts.size() == 0) {
291 serviceAccounts.append(Json::Value(Json::ValueType::objectValue));
292 }
293 serviceAccounts[0]["email"] = "default";
294 auto& scopes = EnsureArrayMember(serviceAccounts[0], kGceScopes);
295 scopes.append(scope);
296 return *this;
297 }
AddScope(const std::string & scope)298 GceInstanceInfo GceInstanceInfo::AddScope(const std::string& scope) && {
299 auto& serviceAccounts = EnsureArrayMember(data_, kGceServiceAccounts);
300 if (serviceAccounts.size() == 0) {
301 serviceAccounts.append(Json::Value(Json::ValueType::objectValue));
302 }
303 serviceAccounts[0]["email"] = "default";
304 auto& scopes = EnsureArrayMember(serviceAccounts[0], kGceScopes);
305 scopes.append(scope);
306 return *this;
307 }
308
AsJson() const309 const Json::Value& GceInstanceInfo::AsJson() const { return data_; }
310
GceApi(HttpClient & http_client,CredentialSource & credentials,const std::string & project)311 GceApi::GceApi(HttpClient& http_client, CredentialSource& credentials,
312 const std::string& project)
313 : http_client_(http_client), credentials_(credentials), project_(project) {}
314
Headers()315 Result<std::vector<std::string>> GceApi::Headers() {
316 return {{
317 "Authorization:Bearer " + CF_EXPECT(credentials_.Credential()),
318 "Content-Type: application/json",
319 }};
320 }
321
322 class GceApi::Operation::Impl {
323 public:
Impl(GceApi & gce_api,std::function<Result<Json::Value> ()> initial_request)324 Impl(GceApi& gce_api, std::function<Result<Json::Value>()> initial_request)
325 : gce_api_(gce_api), initial_request_(std::move(initial_request)) {
326 operation_future_ = std::async([this]() { return Run(); });
327 }
328
Run()329 Result<bool> Run() {
330 auto initial_response =
331 CF_EXPECT(initial_request_(), "Initial request failed: ");
332
333 auto url = OptStringMember(initial_response, "selfLink");
334 if (!url) {
335 return CF_ERR("Operation " << initial_response
336 << " was missing `selfLink` field.");
337 }
338 url = *url + "/wait";
339 running_ = true;
340
341 while (running_) {
342 auto response = CF_EXPECT(gce_api_.http_client_.PostToJson(
343 *url, std::string{""}, CF_EXPECT(gce_api_.Headers())));
344 const auto& json = response.data;
345 Json::Value errors;
346 if (auto j_error = OptObjMember(json, "error"); j_error) {
347 if (auto j_errors = OptArrayMember(*j_error, "errors"); j_errors) {
348 errors = j_errors->size() > 0 ? *j_errors : Json::Value();
349 }
350 }
351 Json::Value warnings;
352 if (auto j_warnings = OptArrayMember(json, "warnings"); j_warnings) {
353 warnings = j_warnings->size() > 0 ? *j_warnings : Json::Value();
354 }
355 LOG(DEBUG) << "Requested operation status at \"" << *url
356 << "\", received " << json;
357 if (!response.HttpSuccess() || errors != Json::Value()) {
358 return CF_ERR("Error accessing \"" << *url << "\". Errors: " << errors
359 << ", Warnings: " << warnings);
360 }
361 if (!json.isMember("status") ||
362 json["status"].type() != Json::ValueType::stringValue) {
363 return CF_ERR(json << " \"status\" field invalid");
364 }
365 if (json["status"] == "DONE") {
366 return true;
367 }
368 }
369 return false;
370 }
371
372 private:
373 GceApi& gce_api_;
374 std::function<Result<Json::Value>()> initial_request_;
375 bool running_;
376 std::future<Result<bool>> operation_future_;
377 friend class GceApi::Operation;
378 };
379
Operation(std::unique_ptr<GceApi::Operation::Impl> impl)380 GceApi::Operation::Operation(std::unique_ptr<GceApi::Operation::Impl> impl)
381 : impl_(std::move(impl)) {}
382
383 GceApi::Operation::~Operation() = default;
384
StopWaiting()385 void GceApi::Operation::StopWaiting() { impl_->running_ = false; }
386
Future()387 std::future<Result<bool>>& GceApi::Operation::Future() {
388 return impl_->operation_future_;
389 }
390
RandomUuid()391 static std::string RandomUuid() {
392 uuid_t uuid;
393 uuid_generate_random(uuid);
394 std::string uuid_str = "xxxxxxxx-xxxx-Mxxx-Nxxx-xxxxxxxxxxxx";
395 uuid_unparse(uuid, uuid_str.data());
396 return uuid_str;
397 }
398
399 // GCE gives back full URLs for zones, but it only wants the last part in
400 // requests
SanitizeZone(const std::string & zone)401 static std::string SanitizeZone(const std::string& zone) {
402 auto last_slash = zone.rfind("/");
403 if (last_slash == std::string::npos) {
404 return zone;
405 }
406 return zone.substr(last_slash + 1);
407 }
408
Get(const GceInstanceInfo & instance)409 std::future<Result<GceInstanceInfo>> GceApi::Get(
410 const GceInstanceInfo& instance) {
411 auto name = instance.Name();
412 if (!name) {
413 auto task = [json = instance.AsJson()]() -> Result<GceInstanceInfo> {
414 return CF_ERR("Missing a name for \"" << json << "\"");
415 };
416 return std::async(std::launch::deferred, task);
417 }
418 auto zone = instance.Zone();
419 if (!zone) {
420 auto task = [json = instance.AsJson()]() -> Result<GceInstanceInfo> {
421 return CF_ERR("Missing a zone for \"" << json << "\"");
422 };
423 return std::async(std::launch::deferred, task);
424 }
425 return Get(*zone, *name);
426 }
427
Get(const std::string & zone,const std::string & name)428 std::future<Result<GceInstanceInfo>> GceApi::Get(const std::string& zone,
429 const std::string& name) {
430 std::stringstream url;
431 url << "https://compute.googleapis.com/compute/v1";
432 url << "/projects/" << http_client_.UrlEscape(project_);
433 url << "/zones/" << http_client_.UrlEscape(SanitizeZone(zone));
434 url << "/instances/" << http_client_.UrlEscape(name);
435 auto task = [this, url = url.str()]() -> Result<GceInstanceInfo> {
436 auto response =
437 CF_EXPECT(http_client_.DownloadToJson(url, CF_EXPECT(Headers())));
438 if (!response.HttpSuccess()) {
439 return CF_ERR("Failed to get instance info, received "
440 << response.data << " with code " << response.http_code);
441 }
442 return GceInstanceInfo(response.data);
443 };
444 return std::async(task);
445 }
446
Insert(const Json::Value & request)447 GceApi::Operation GceApi::Insert(const Json::Value& request) {
448 if (!request.isMember("zone") ||
449 request["zone"].type() != Json::ValueType::stringValue) {
450 auto task = [request]() -> Result<Json::Value> {
451 return CF_ERR("Missing a zone for \"" << request << "\"");
452 };
453 return Operation(
454 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
455 }
456 auto zone = request["zone"].asString();
457 Json::Value requestNoZone = request;
458 requestNoZone.removeMember("zone");
459 std::stringstream url;
460 url << "https://compute.googleapis.com/compute/v1";
461 url << "/projects/" << http_client_.UrlEscape(project_);
462 url << "/zones/" << http_client_.UrlEscape(SanitizeZone(zone));
463 url << "/instances";
464 url << "?requestId=" << RandomUuid(); // Avoid duplication on request retry
465 auto task = [this, requestNoZone, url = url.str()]() -> Result<Json::Value> {
466 auto response = CF_EXPECT(
467 http_client_.PostToJson(url, requestNoZone, CF_EXPECT(Headers())));
468 if (!response.HttpSuccess()) {
469 return CF_ERR("Failed to create instance: "
470 << response.data << ". Sent request " << requestNoZone);
471 }
472 return response.data;
473 };
474 return Operation(
475 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
476 }
477
Insert(const GceInstanceInfo & request)478 GceApi::Operation GceApi::Insert(const GceInstanceInfo& request) {
479 return Insert(request.AsJson());
480 }
481
Reset(const std::string & zone,const std::string & name)482 GceApi::Operation GceApi::Reset(const std::string& zone,
483 const std::string& name) {
484 std::stringstream url;
485 url << "https://compute.googleapis.com/compute/v1";
486 url << "/projects/" << http_client_.UrlEscape(project_);
487 url << "/zones/" << http_client_.UrlEscape(SanitizeZone(zone));
488 url << "/instances/" << http_client_.UrlEscape(name);
489 url << "/reset";
490 url << "?requestId=" << RandomUuid(); // Avoid duplication on request retry
491 auto task = [this, url = url.str()]() -> Result<Json::Value> {
492 auto response = CF_EXPECT(
493 http_client_.PostToJson(url, Json::Value(), CF_EXPECT(Headers())));
494 if (!response.HttpSuccess()) {
495 return CF_ERR("Failed to create instance: " << response.data);
496 }
497 return response.data;
498 };
499 return Operation(
500 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
501 }
502
Reset(const GceInstanceInfo & instance)503 GceApi::Operation GceApi::Reset(const GceInstanceInfo& instance) {
504 auto name = instance.Name();
505 if (!name) {
506 auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
507 return CF_ERR("Missing a name for \"" << json << "\"");
508 };
509 return Operation(
510 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
511 }
512 auto zone = instance.Zone();
513 if (!zone) {
514 auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
515 return CF_ERR("Missing a zone for \"" << json << "\"");
516 };
517 return Operation(
518 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
519 }
520 return Reset(*zone, *name);
521 }
522
Delete(const std::string & zone,const std::string & name)523 GceApi::Operation GceApi::Delete(const std::string& zone,
524 const std::string& name) {
525 std::stringstream url;
526 url << "https://compute.googleapis.com/compute/v1";
527 url << "/projects/" << http_client_.UrlEscape(project_);
528 url << "/zones/" << http_client_.UrlEscape(SanitizeZone(zone));
529 url << "/instances/" << http_client_.UrlEscape(name);
530 url << "?requestId=" << RandomUuid(); // Avoid duplication on request retry
531 auto task = [this, url = url.str()]() -> Result<Json::Value> {
532 auto response =
533 CF_EXPECT(http_client_.DeleteToJson(url, CF_EXPECT(Headers())));
534 if (!response.HttpSuccess()) {
535 return CF_ERR("Failed to delete instance: " << response.data);
536 }
537 return response.data;
538 };
539 return Operation(
540 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
541 }
542
Delete(const GceInstanceInfo & instance)543 GceApi::Operation GceApi::Delete(const GceInstanceInfo& instance) {
544 auto name = instance.Name();
545 if (!name) {
546 auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
547 return CF_ERR("Missing a name for \"" << json << "\"");
548 };
549 return Operation(
550 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
551 }
552 auto zone = instance.Zone();
553 if (!zone) {
554 auto task = [json = instance.AsJson()]() -> Result<Json::Value> {
555 return CF_ERR("Missing a zone for \"" << json << "\"");
556 };
557 return Operation(
558 std::unique_ptr<Operation::Impl>(new Operation::Impl(*this, task)));
559 }
560 return Delete(*zone, *name);
561 }
562
563 } // namespace cuttlefish
564