github.com/akamai/AkamaiOPEN-edgegrid-golang/v8@v8.1.0/pkg/dns/zone.go (about) 1 package dns 2 3 import ( 4 "context" 5 "fmt" 6 "io" 7 "io/ioutil" 8 "net/http" 9 10 "bytes" 11 "encoding/json" 12 "reflect" 13 "strconv" 14 "strings" 15 "sync" 16 ) 17 18 var ( 19 zoneWriteLock sync.Mutex 20 ) 21 22 type ( 23 // Zones contains operations available on Zone resources. 24 Zones interface { 25 // ListZones retrieves a list of all zones user can access. 26 // 27 // See: https://techdocs.akamai.com/edge-dns/reference/get-zones 28 ListZones(context.Context, ...ZoneListQueryArgs) (*ZoneListResponse, error) 29 // GetZone retrieves Zone metadata. 30 // 31 // See: https://techdocs.akamai.com/edge-dns/reference/get-zone 32 GetZone(context.Context, string) (*ZoneResponse, error) 33 //GetChangeList retrieves Zone changelist. 34 // 35 // See: https://techdocs.akamai.com/edge-dns/reference/get-changelists-zone 36 GetChangeList(context.Context, string) (*ChangeListResponse, error) 37 // GetMasterZoneFile retrieves master zone file. 38 // 39 // See: https://techdocs.akamai.com/edge-dns/reference/get-zones-zone-zone-file 40 GetMasterZoneFile(context.Context, string) (string, error) 41 // PostMasterZoneFile updates master zone file. 42 // 43 // See: https://techdocs.akamai.com/edge-dns/reference/post-zones-zone-zone-file 44 PostMasterZoneFile(context.Context, string, string) error 45 // CreateZone creates new zone. 46 // 47 // See: https://techdocs.akamai.com/edge-dns/reference/post-zone 48 CreateZone(context.Context, *ZoneCreate, ZoneQueryString, ...bool) error 49 // SaveChangelist creates a new Change List based on the most recent version of a zone. 50 // 51 // See: https://techdocs.akamai.com/edge-dns/reference/post-changelists 52 SaveChangelist(context.Context, *ZoneCreate) error 53 // SubmitChangelist submits changelist for the Zone to create default NS SOA records. 54 // 55 // See: https://techdocs.akamai.com/edge-dns/reference/post-changelists-zone-submit 56 SubmitChangelist(context.Context, *ZoneCreate) error 57 // UpdateZone updates zone. 58 // 59 // See: https://techdocs.akamai.com/edge-dns/reference/put-zone 60 UpdateZone(context.Context, *ZoneCreate, ZoneQueryString) error 61 // GetZoneNames retrieves a list of a zone's record names. 62 // 63 // See: https://techdocs.akamai.com/edge-dns/reference/get-zone-names 64 GetZoneNames(context.Context, string) (*ZoneNamesResponse, error) 65 // GetZoneNameTypes retrieves a zone name's record types. 66 // 67 // See: https://techdocs.akamai.com/edge-dns/reference/get-zone-name-types 68 GetZoneNameTypes(context.Context, string, string) (*ZoneNameTypesResponse, error) 69 // CreateBulkZones submits create bulk zone request. 70 // 71 // See: https://techdocs.akamai.com/edge-dns/reference/post-zones-create-requests 72 CreateBulkZones(context.Context, *BulkZonesCreate, ZoneQueryString) (*BulkZonesResponse, error) 73 // DeleteBulkZones submits delete bulk zone request. 74 // 75 // See: https://techdocs.akamai.com/edge-dns/reference/post-zones-delete-requests 76 DeleteBulkZones(context.Context, *ZoneNameListResponse, ...bool) (*BulkZonesResponse, error) 77 // GetBulkZoneCreateStatus retrieves submit request status. 78 // 79 // See: https://techdocs.akamai.com/edge-dns/reference/get-zones-create-requests-requestid 80 GetBulkZoneCreateStatus(context.Context, string) (*BulkStatusResponse, error) 81 //GetBulkZoneDeleteStatus retrieves submit request status. 82 // 83 // See: https://techdocs.akamai.com/edge-dns/reference/get-zones-delete-requests-requestid 84 GetBulkZoneDeleteStatus(context.Context, string) (*BulkStatusResponse, error) 85 // GetBulkZoneCreateResult retrieves create request result. 86 // 87 // See: https://techdocs.akamai.com/edge-dns/reference/get-zones-create-requests-requestid-result 88 GetBulkZoneCreateResult(ctx context.Context, requestid string) (*BulkCreateResultResponse, error) 89 // GetBulkZoneDeleteResult retrieves delete request result. 90 // 91 // See: https://techdocs.akamai.com/edge-dns/reference/get-zones-delete-requests-requestid-result 92 GetBulkZoneDeleteResult(context.Context, string) (*BulkDeleteResultResponse, error) 93 } 94 95 // ZoneQueryString contains zone query parameters 96 ZoneQueryString struct { 97 Contract string 98 Group string 99 } 100 101 // ZoneCreate contains zone create request 102 ZoneCreate struct { 103 Zone string `json:"zone"` 104 Type string `json:"type"` 105 Masters []string `json:"masters,omitempty"` 106 Comment string `json:"comment,omitempty"` 107 SignAndServe bool `json:"signAndServe"` 108 SignAndServeAlgorithm string `json:"signAndServeAlgorithm,omitempty"` 109 TSIGKey *TSIGKey `json:"tsigKey,omitempty"` 110 Target string `json:"target,omitempty"` 111 EndCustomerID string `json:"endCustomerId,omitempty"` 112 ContractID string `json:"contractId,omitempty"` 113 } 114 115 // ZoneResponse contains zone create response 116 ZoneResponse struct { 117 Zone string `json:"zone,omitempty"` 118 Type string `json:"type,omitempty"` 119 Masters []string `json:"masters,omitempty"` 120 Comment string `json:"comment,omitempty"` 121 SignAndServe bool `json:"signAndServe"` 122 SignAndServeAlgorithm string `json:"signAndServeAlgorithm,omitempty"` 123 TSIGKey *TSIGKey `json:"tsigKey,omitempty"` 124 Target string `json:"target,omitempty"` 125 EndCustomerID string `json:"endCustomerId,omitempty"` 126 ContractID string `json:"contractId,omitempty"` 127 AliasCount int64 `json:"aliasCount,omitempty"` 128 ActivationState string `json:"activationState,omitempty"` 129 LastActivationDate string `json:"lastActivationDate,omitempty"` 130 LastModifiedBy string `json:"lastModifiedBy,omitempty"` 131 LastModifiedDate string `json:"lastModifiedDate,omitempty"` 132 VersionID string `json:"versionId,omitempty"` 133 } 134 135 // ZoneListQueryArgs contains parameters for List Zones query 136 ZoneListQueryArgs struct { 137 ContractIDs string 138 Page int 139 PageSize int 140 Search string 141 ShowAll bool 142 SortBy string 143 Types string 144 } 145 146 // ListMetadata contains metadata for List Zones request 147 ListMetadata struct { 148 ContractIDs []string `json:"contractIds"` 149 Page int `json:"page"` 150 PageSize int `json:"pageSize"` 151 ShowAll bool `json:"showAll"` 152 TotalElements int `json:"totalElements"` 153 } 154 155 // ZoneListResponse contains response for List Zones request 156 ZoneListResponse struct { 157 Metadata *ListMetadata `json:"metadata,omitempty"` 158 Zones []*ZoneResponse `json:"zones,omitempty"` 159 } 160 161 // ChangeListResponse contains metadata about a change list 162 ChangeListResponse struct { 163 Zone string `json:"zone,omitempty"` 164 ChangeTag string `json:"changeTag,omitempty"` 165 ZoneVersionID string `json:"zoneVersionId,omitempty"` 166 LastModifiedDate string `json:"lastModifiedDate,omitempty"` 167 Stale bool `json:"stale,omitempty"` 168 } 169 170 // ZoneNameListResponse contains response with a list of zone's names and aliases 171 ZoneNameListResponse struct { 172 Zones []string `json:"zones"` 173 Aliases []string `json:"aliases,omitempty"` 174 } 175 176 // ZoneNamesResponse contains record set names for zone 177 ZoneNamesResponse struct { 178 Names []string `json:"names"` 179 } 180 181 // ZoneNameTypesResponse contains record set types for zone 182 ZoneNameTypesResponse struct { 183 Types []string `json:"types"` 184 } 185 ) 186 187 var zoneStructMap = map[string]string{ 188 "Zone": "zone", 189 "Type": "type", 190 "Masters": "masters", 191 "Comment": "comment", 192 "SignAndServe": "signAndServe", 193 "SignAndServeAlgorithm": "signAndServeAlgorithm", 194 "TSIGKey": "tsigKey", 195 "Target": "target", 196 "EndCustomerID": "endCustomerId", 197 "ContractId": "contractId"} 198 199 // Util to convert struct to http request body, eg. io.reader 200 func convertStructToReqBody(srcStruct interface{}) (io.Reader, error) { 201 reqBody, err := json.Marshal(srcStruct) 202 if err != nil { 203 return nil, err 204 } 205 return bytes.NewBuffer(reqBody), nil 206 } 207 208 func (d *dns) ListZones(ctx context.Context, queryArgs ...ZoneListQueryArgs) (*ZoneListResponse, error) { 209 logger := d.Log(ctx) 210 logger.Debug("ListZones") 211 212 getURL := fmt.Sprintf("/config-dns/v2/zones") 213 if len(queryArgs) > 1 { 214 return nil, fmt.Errorf("ListZones QueryArgs invalid") 215 } 216 217 req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) 218 if err != nil { 219 return nil, fmt.Errorf("failed to create listzones request: %w", err) 220 } 221 222 q := req.URL.Query() 223 if len(queryArgs) > 0 { 224 if queryArgs[0].Page > 0 { 225 q.Add("page", strconv.Itoa(queryArgs[0].Page)) 226 } 227 if queryArgs[0].PageSize > 0 { 228 q.Add("pageSize", strconv.Itoa(queryArgs[0].PageSize)) 229 } 230 if queryArgs[0].Search != "" { 231 q.Add("search", queryArgs[0].Search) 232 } 233 q.Add("showAll", strconv.FormatBool(queryArgs[0].ShowAll)) 234 if queryArgs[0].SortBy != "" { 235 q.Add("sortBy", queryArgs[0].SortBy) 236 } 237 if queryArgs[0].Types != "" { 238 q.Add("types", queryArgs[0].Types) 239 } 240 if queryArgs[0].ContractIDs != "" { 241 q.Add("contractIds", queryArgs[0].ContractIDs) 242 } 243 req.URL.RawQuery = q.Encode() 244 } 245 246 var result ZoneListResponse 247 resp, err := d.Exec(req, &result) 248 if err != nil { 249 return nil, fmt.Errorf("listzones request failed: %w", err) 250 } 251 252 if resp.StatusCode != http.StatusOK { 253 return nil, d.Error(resp) 254 } 255 256 return &result, nil 257 } 258 259 func (d *dns) GetZone(ctx context.Context, zoneName string) (*ZoneResponse, error) { 260 logger := d.Log(ctx) 261 logger.Debug("GetZone") 262 263 getURL := fmt.Sprintf("/config-dns/v2/zones/%s", zoneName) 264 req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) 265 if err != nil { 266 return nil, fmt.Errorf("failed to create GetZone request: %w", err) 267 } 268 269 var result ZoneResponse 270 resp, err := d.Exec(req, &result) 271 if err != nil { 272 return nil, fmt.Errorf("GetZone request failed: %w", err) 273 } 274 275 if resp.StatusCode != http.StatusOK { 276 return nil, d.Error(resp) 277 } 278 279 return &result, nil 280 } 281 282 func (d *dns) GetChangeList(ctx context.Context, zone string) (*ChangeListResponse, error) { 283 logger := d.Log(ctx) 284 logger.Debug("GetChangeList") 285 286 getURL := fmt.Sprintf("/config-dns/v2/changelists/%s", zone) 287 req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) 288 if err != nil { 289 return nil, fmt.Errorf("failed to create GetChangeList request: %w", err) 290 } 291 292 var result ChangeListResponse 293 resp, err := d.Exec(req, &result) 294 if err != nil { 295 return nil, fmt.Errorf("GetChangeList request failed: %w", err) 296 } 297 298 if resp.StatusCode != http.StatusOK { 299 return nil, d.Error(resp) 300 } 301 302 return &result, nil 303 } 304 305 func (d *dns) GetMasterZoneFile(ctx context.Context, zone string) (string, error) { 306 logger := d.Log(ctx) 307 logger.Debug("GetMasterZoneFile") 308 309 getURL := fmt.Sprintf("/config-dns/v2/zones/%s/zone-file", zone) 310 req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) 311 if err != nil { 312 return "", fmt.Errorf("failed to create GetMasterZoneFile request: %w", err) 313 } 314 req.Header.Add("Accept", "text/dns") 315 316 resp, err := d.Exec(req, nil) 317 if err != nil { 318 return "", fmt.Errorf("GetMasterZoneFile request failed: %w", err) 319 } 320 321 if resp.StatusCode != http.StatusOK { 322 return "", d.Error(resp) 323 } 324 325 masterFile, err := ioutil.ReadAll(resp.Body) 326 if err != nil { 327 return "", fmt.Errorf("GetMasterZoneFile request failed: %w", err) 328 } 329 330 return string(masterFile), nil 331 } 332 333 func (d *dns) PostMasterZoneFile(ctx context.Context, zone string, fileData string) error { 334 logger := d.Log(ctx) 335 logger.Debug("PostMasterZoneFile") 336 337 mtResp := "" 338 pmzfURL := fmt.Sprintf("/config-dns/v2/zones/%s/zone-file", zone) 339 buf := bytes.NewReader([]byte(fileData)) 340 req, err := http.NewRequestWithContext(ctx, http.MethodPost, pmzfURL, buf) 341 if err != nil { 342 return fmt.Errorf("failed to create PostMasterZoneFile request: %w", err) 343 } 344 345 req.Header.Set("Content-Type", "text/dns") 346 347 resp, err := d.Exec(req, &mtResp) 348 if err != nil { 349 return fmt.Errorf("Create PostMasterZoneFile failed: %w", err) 350 } 351 352 if resp.StatusCode != http.StatusNoContent { 353 return d.Error(resp) 354 } 355 356 return nil 357 } 358 359 func (d *dns) CreateZone(ctx context.Context, zone *ZoneCreate, zoneQueryString ZoneQueryString, clearConn ...bool) error { 360 // This lock will restrict the concurrency of API calls 361 // to 1 save request at a time. This is needed for the Soa.Serial value which 362 // is required to be incremented for every subsequent update to a zone, 363 // so we have to save just one request at a time to ensure this is always 364 // incremented properly 365 366 zoneWriteLock.Lock() 367 defer zoneWriteLock.Unlock() 368 369 logger := d.Log(ctx) 370 logger.Debug("Zone Create") 371 372 if err := ValidateZone(zone); err != nil { 373 return err 374 } 375 376 zoneMap := filterZoneCreate(zone) 377 378 var zoneResponse ZoneResponse 379 zoneURL := "/config-dns/v2/zones/?contractId=" + zoneQueryString.Contract 380 if len(zoneQueryString.Group) > 0 { 381 zoneURL += "&gid=" + zoneQueryString.Group 382 } 383 384 reqBody, err := convertStructToReqBody(zoneMap) 385 if err != nil { 386 return fmt.Errorf("failed to generate request body: %w", err) 387 } 388 389 req, err := http.NewRequestWithContext(ctx, http.MethodPost, zoneURL, reqBody) 390 if err != nil { 391 return fmt.Errorf("failed to create Zone Create request: %w", err) 392 } 393 394 resp, err := d.Exec(req, &zoneResponse) 395 if err != nil { 396 return fmt.Errorf("Create Zone request failed: %w", err) 397 } 398 399 if resp.StatusCode != http.StatusCreated { 400 return d.Error(resp) 401 } 402 403 if strings.ToUpper(zone.Type) == "PRIMARY" { 404 // Timing issue with Create immediately followed by SaveChangelist 405 for _, clear := range clearConn { 406 // should only be one entry 407 if clear { 408 logger.Info("Clearing Idle Connections") 409 d.Client().CloseIdleConnections() 410 } 411 } 412 } 413 414 return nil 415 } 416 417 func (d *dns) SaveChangelist(ctx context.Context, zone *ZoneCreate) error { 418 // This lock will restrict the concurrency of API calls 419 // to 1 save request at a time. This is needed for the Soa.Serial value which 420 // is required to be incremented for every subsequent update to a zone 421 // so we have to save just one request at a time to ensure this is always 422 // incremented properly 423 424 zoneWriteLock.Lock() 425 defer zoneWriteLock.Unlock() 426 427 logger := d.Log(ctx) 428 logger.Debug("SaveChangeList") 429 430 reqBody, err := convertStructToReqBody("") 431 if err != nil { 432 return fmt.Errorf("failed to generate request body: %w", err) 433 } 434 435 postURL := fmt.Sprintf("/config-dns/v2/changelists/?zone=%s", zone.Zone) 436 req, err := http.NewRequestWithContext(ctx, http.MethodPost, postURL, reqBody) 437 if err != nil { 438 return fmt.Errorf("failed to create SaveChangeList request: %w", err) 439 } 440 441 resp, err := d.Exec(req, nil) 442 if err != nil { 443 return fmt.Errorf("SaveChangeList request failed: %w", err) 444 } 445 446 if resp.StatusCode != http.StatusCreated { 447 return d.Error(resp) 448 } 449 450 return nil 451 } 452 453 func (d *dns) SubmitChangelist(ctx context.Context, zone *ZoneCreate) error { 454 // This lock will restrict the concurrency of API calls 455 // to 1 save request at a time. This is needed for the Soa.Serial value which 456 // is required to be incremented for every subsequent update to a zone 457 // so we have to save just one request at a time to ensure this is always 458 // incremented properly 459 460 zoneWriteLock.Lock() 461 defer zoneWriteLock.Unlock() 462 463 logger := d.Log(ctx) 464 logger.Debug("SubmitChangeList") 465 466 reqBody, err := convertStructToReqBody("") 467 if err != nil { 468 return fmt.Errorf("failed to generate request body: %w", err) 469 } 470 471 postURL := fmt.Sprintf("/config-dns/v2/changelists/%s/submit", zone.Zone) 472 req, err := http.NewRequestWithContext(ctx, http.MethodPost, postURL, reqBody) 473 if err != nil { 474 return fmt.Errorf("failed to create SubmitChangeList request: %w", err) 475 } 476 477 resp, err := d.Exec(req, nil) 478 if err != nil { 479 return fmt.Errorf("SubmitChangeList request failed: %w", err) 480 } 481 482 if resp.StatusCode != http.StatusNoContent { 483 return d.Error(resp) 484 } 485 486 return nil 487 } 488 489 func (d *dns) UpdateZone(ctx context.Context, zone *ZoneCreate, _ ZoneQueryString) error { 490 // This lock will restrict the concurrency of API calls 491 // to 1 save request at a time. This is needed for the Soa.Serial value which 492 // is required to be incremented for every subsequent update to a zone 493 // so we have to save just one request at a time to ensure this is always 494 // incremented properly 495 496 zoneWriteLock.Lock() 497 defer zoneWriteLock.Unlock() 498 499 logger := d.Log(ctx) 500 logger.Debug("Zone Update") 501 502 if err := ValidateZone(zone); err != nil { 503 return err 504 } 505 506 zoneMap := filterZoneCreate(zone) 507 reqBody, err := convertStructToReqBody(zoneMap) 508 if err != nil { 509 return fmt.Errorf("failed to generate request body: %w", err) 510 } 511 512 putURL := fmt.Sprintf("/config-dns/v2/zones/%s", zone.Zone) 513 req, err := http.NewRequestWithContext(ctx, http.MethodPut, putURL, reqBody) 514 if err != nil { 515 return fmt.Errorf("failed to create Get Update request: %w", err) 516 } 517 518 var result ZoneResponse 519 resp, err := d.Exec(req, &result) 520 if err != nil { 521 return fmt.Errorf("Zone Update request failed: %w", err) 522 } 523 524 if resp.StatusCode != http.StatusOK { 525 return d.Error(resp) 526 } 527 528 return nil 529 } 530 531 func filterZoneCreate(zone *ZoneCreate) map[string]interface{} { 532 zoneType := strings.ToUpper(zone.Type) 533 filteredZone := make(map[string]interface{}) 534 zoneElems := reflect.ValueOf(zone).Elem() 535 for i := 0; i < zoneElems.NumField(); i++ { 536 varName := zoneElems.Type().Field(i).Name 537 varLower := zoneStructMap[varName] 538 varValue := zoneElems.Field(i).Interface() 539 switch varName { 540 case "Target": 541 if zoneType == "ALIAS" { 542 filteredZone[varLower] = varValue 543 } 544 case "TsigKey": 545 if zoneType == "SECONDARY" { 546 filteredZone[varLower] = varValue 547 } 548 case "Masters": 549 if zoneType == "SECONDARY" { 550 filteredZone[varLower] = varValue 551 } 552 case "SignAndServe": 553 if zoneType != "ALIAS" { 554 filteredZone[varLower] = varValue 555 } 556 case "SignAndServeAlgorithm": 557 if zoneType != "ALIAS" { 558 filteredZone[varLower] = varValue 559 } 560 default: 561 filteredZone[varLower] = varValue 562 } 563 } 564 565 return filteredZone 566 } 567 568 // ValidateZone validates ZoneCreate Object 569 func ValidateZone(zone *ZoneCreate) error { 570 if len(zone.Zone) == 0 { 571 return fmt.Errorf("Zone name is required") 572 } 573 zType := strings.ToUpper(zone.Type) 574 if zType != "PRIMARY" && zType != "SECONDARY" && zType != "ALIAS" { 575 return fmt.Errorf("Invalid zone type") 576 } 577 if zType != "SECONDARY" && zone.TSIGKey != nil { 578 return fmt.Errorf("TsigKey is invalid for %s zone type", zType) 579 } 580 if zType == "ALIAS" { 581 if len(zone.Target) == 0 { 582 return fmt.Errorf("Target is required for Alias zone type") 583 } 584 if zone.Masters != nil && len(zone.Masters) > 0 { 585 return fmt.Errorf("Masters is invalid for Alias zone type") 586 } 587 if zone.SignAndServe { 588 return fmt.Errorf("SignAndServe is invalid for Alias zone type") 589 } 590 if len(zone.SignAndServeAlgorithm) > 0 { 591 return fmt.Errorf("SignAndServeAlgorithm is invalid for Alias zone type") 592 } 593 return nil 594 } 595 // Primary or Secondary 596 if len(zone.Target) > 0 { 597 return fmt.Errorf("Target is invalid for %s zone type", zType) 598 } 599 if zone.Masters != nil && len(zone.Masters) > 0 && zType == "PRIMARY" { 600 return fmt.Errorf("Masters is invalid for Primary zone type") 601 } 602 603 return nil 604 } 605 606 func (d *dns) GetZoneNames(ctx context.Context, zone string) (*ZoneNamesResponse, error) { 607 logger := d.Log(ctx) 608 logger.Debug("GetZoneNames") 609 610 getURL := fmt.Sprintf("/config-dns/v2/zones/%s/names", zone) 611 req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) 612 if err != nil { 613 return nil, fmt.Errorf("failed to create GetZoneNames request: %w", err) 614 } 615 616 var result ZoneNamesResponse 617 resp, err := d.Exec(req, &result) 618 if err != nil { 619 return nil, fmt.Errorf("GetZoneNames request failed: %w", err) 620 } 621 622 if resp.StatusCode != http.StatusOK { 623 return nil, d.Error(resp) 624 } 625 626 return &result, nil 627 } 628 629 func (d *dns) GetZoneNameTypes(ctx context.Context, zName, zone string) (*ZoneNameTypesResponse, error) { 630 logger := d.Log(ctx) 631 logger.Debug(" GetZoneNameTypes") 632 633 getURL := fmt.Sprintf("/config-dns/v2/zones/%s/names/%s/types", zone, zName) 634 req, err := http.NewRequestWithContext(ctx, http.MethodGet, getURL, nil) 635 if err != nil { 636 return nil, fmt.Errorf("failed to create GetZoneNameTypes request: %w", err) 637 } 638 639 var result ZoneNameTypesResponse 640 resp, err := d.Exec(req, &result) 641 if err != nil { 642 return nil, fmt.Errorf("GetZoneNameTypes request failed: %w", err) 643 } 644 645 if resp.StatusCode != http.StatusOK { 646 return nil, d.Error(resp) 647 } 648 649 return &result, nil 650 }