github.com/osdi23p228/fabric@v0.0.0-20221218062954-77808885f5db/core/ledger/kvledger/txmgmt/statedb/statecouchdb/couchdb.go (about) 1 /* 2 Copyright IBM Corp. All Rights Reserved. 3 4 SPDX-License-Identifier: Apache-2.0 5 */ 6 7 package statecouchdb 8 9 import ( 10 "bytes" 11 "compress/gzip" 12 "context" 13 "encoding/base64" 14 "encoding/json" 15 "fmt" 16 "io" 17 "io/ioutil" 18 "log" 19 "mime" 20 "mime/multipart" 21 "net/http" 22 "net/http/httputil" 23 "net/textproto" 24 "net/url" 25 "regexp" 26 "strconv" 27 "strings" 28 "time" 29 "unicode/utf8" 30 31 "github.com/osdi23p228/fabric/common/flogging" 32 "github.com/osdi23p228/fabric/core/ledger" 33 "github.com/pkg/errors" 34 "go.uber.org/zap/zapcore" 35 ) 36 37 var couchdbLogger = flogging.MustGetLogger("couchdb") 38 39 //time between retry attempts in milliseconds 40 const retryWaitTime = 125 41 42 // dbOperationResponse is body for successful database calls. 43 type dbOperationResponse struct { 44 Ok bool 45 } 46 47 // dbInfo is body for database information. 48 type dbInfo struct { 49 DbName string `json:"db_name"` 50 Sizes struct { 51 File int `json:"file"` 52 External int `json:"external"` 53 Active int `json:"active"` 54 } `json:"sizes"` 55 Other struct { 56 DataSize int `json:"data_size"` 57 } `json:"other"` 58 DocDelCount int `json:"doc_del_count"` 59 DocCount int `json:"doc_count"` 60 DiskSize int `json:"disk_size"` 61 DiskFormatVersion int `json:"disk_format_version"` 62 DataSize int `json:"data_size"` 63 CompactRunning bool `json:"compact_running"` 64 InstanceStartTime string `json:"instance_start_time"` 65 } 66 67 //connectionInfo is a structure for capturing the database info and version 68 type connectionInfo struct { 69 Couchdb string `json:"couchdb"` 70 Version string `json:"version"` 71 Vendor struct { 72 Name string `json:"name"` 73 } `json:"vendor"` 74 } 75 76 //rangeQueryResponse is used for processing REST range query responses from CouchDB 77 type rangeQueryResponse struct { 78 TotalRows int32 `json:"total_rows"` 79 Offset int32 `json:"offset"` 80 Rows []struct { 81 ID string `json:"id"` 82 Key string `json:"key"` 83 Value struct { 84 Rev string `json:"rev"` 85 } `json:"value"` 86 Doc json.RawMessage `json:"doc"` 87 } `json:"rows"` 88 } 89 90 //queryResponse is used for processing REST query responses from CouchDB 91 type queryResponse struct { 92 Warning string `json:"warning"` 93 Docs []json.RawMessage `json:"docs"` 94 Bookmark string `json:"bookmark"` 95 } 96 97 // docMetadata is used for capturing CouchDB document header info, 98 // used to capture id, version, rev and attachments returned in the query from CouchDB 99 type docMetadata struct { 100 ID string `json:"_id"` 101 Rev string `json:"_rev"` 102 Version string `json:"~version"` 103 AttachmentsInfo map[string]*attachmentInfo `json:"_attachments"` 104 } 105 106 //queryResult is used for returning query results from CouchDB 107 type queryResult struct { 108 id string 109 value []byte 110 attachments []*attachmentInfo 111 } 112 113 //couchInstance represents a CouchDB instance 114 type couchInstance struct { 115 conf *ledger.CouchDBConfig 116 client *http.Client // a client to connect to this instance 117 stats *stats 118 } 119 120 //couchDatabase represents a database within a CouchDB instance 121 type couchDatabase struct { 122 couchInstance *couchInstance //connection configuration 123 dbName string 124 indexWarmCounter int 125 } 126 127 //dbReturn contains an error reported by CouchDB 128 type dbReturn struct { 129 StatusCode int `json:"status_code"` 130 Error string `json:"error"` 131 Reason string `json:"reason"` 132 } 133 134 //createIndexResponse contains an the index creation response from CouchDB 135 type createIndexResponse struct { 136 Result string `json:"result"` 137 ID string `json:"id"` 138 Name string `json:"name"` 139 } 140 141 //attachmentInfo contains the definition for an attached file for couchdb 142 type attachmentInfo struct { 143 Name string 144 ContentType string `json:"content_type"` 145 Length uint64 146 AttachmentBytes []byte `json:"data"` 147 } 148 149 //fileDetails defines the structure needed to send an attachment to couchdb 150 type fileDetails struct { 151 Follows bool `json:"follows"` 152 ContentType string `json:"content_type"` 153 Length int `json:"length"` 154 } 155 156 //batchRetrieveDocMetadataResponse is used for processing REST batch responses from CouchDB 157 type batchRetrieveDocMetadataResponse struct { 158 Rows []struct { 159 ID string `json:"id"` 160 DocMetadata struct { 161 ID string `json:"_id"` 162 Rev string `json:"_rev"` 163 Version string `json:"~version"` 164 } `json:"doc"` 165 } `json:"rows"` 166 } 167 168 //batchUpdateResponse defines a structure for batch update response 169 type batchUpdateResponse struct { 170 ID string `json:"id"` 171 Error string `json:"error"` 172 Reason string `json:"reason"` 173 Ok bool `json:"ok"` 174 Rev string `json:"rev"` 175 } 176 177 //base64Attachment contains the definition for an attached file for couchdb 178 type base64Attachment struct { 179 ContentType string `json:"content_type"` 180 AttachmentData string `json:"data"` 181 } 182 183 //indexResult contains the definition for a couchdb index 184 type indexResult struct { 185 DesignDocument string `json:"designdoc"` 186 Name string `json:"name"` 187 Definition string `json:"definition"` 188 } 189 190 //databaseSecurity contains the definition for CouchDB database security 191 type databaseSecurity struct { 192 Admins struct { 193 Names []string `json:"names"` 194 Roles []string `json:"roles"` 195 } `json:"admins"` 196 Members struct { 197 Names []string `json:"names"` 198 Roles []string `json:"roles"` 199 } `json:"members"` 200 } 201 202 //couchDoc defines the structure for a JSON document value 203 type couchDoc struct { 204 jsonValue []byte 205 attachments []*attachmentInfo 206 } 207 208 func (d *couchDoc) key() (string, error) { 209 m := make(jsonValue) 210 if err := json.Unmarshal(d.jsonValue, &m); err != nil { 211 return "", err 212 } 213 return m[idField].(string), nil 214 215 } 216 217 // closeResponseBody discards the body and then closes it to enable returning it to 218 // connection pool 219 func closeResponseBody(resp *http.Response) { 220 if resp != nil { 221 io.Copy(ioutil.Discard, resp.Body) // discard whatever is remaining of body 222 resp.Body.Close() 223 } 224 } 225 226 //createDatabaseIfNotExist method provides function to create database 227 func (dbclient *couchDatabase) createDatabaseIfNotExist() error { 228 couchdbLogger.Debugf("[%s] Entering CreateDatabaseIfNotExist()", dbclient.dbName) 229 230 dbInfo, couchDBReturn, err := dbclient.getDatabaseInfo() 231 if err != nil { 232 if couchDBReturn == nil || couchDBReturn.StatusCode != 404 { 233 return err 234 } 235 } 236 237 if dbInfo == nil || couchDBReturn.StatusCode == 404 { 238 couchdbLogger.Debugf("[%s] Database does not exist.", dbclient.dbName) 239 240 connectURL, err := url.Parse(dbclient.couchInstance.url()) 241 if err != nil { 242 couchdbLogger.Errorf("URL parse error: %s", err) 243 return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 244 } 245 246 //get the number of retries 247 maxRetries := dbclient.couchInstance.conf.MaxRetries 248 249 //process the URL with a PUT, creates the database 250 resp, _, err := dbclient.handleRequest(http.MethodPut, "CreateDatabaseIfNotExist", connectURL, nil, "", "", maxRetries, true, nil) 251 if err != nil { 252 // Check to see if the database exists 253 // Even though handleRequest() returned an error, the 254 // database may have been created and a false error 255 // returned due to a timeout or race condition. 256 // Do a final check to see if the database really got created. 257 dbInfo, couchDBReturn, dbInfoErr := dbclient.getDatabaseInfo() 258 if dbInfoErr != nil || dbInfo == nil || couchDBReturn.StatusCode == 404 { 259 return err 260 } 261 } 262 defer closeResponseBody(resp) 263 couchdbLogger.Infof("Created state database %s", dbclient.dbName) 264 } else { 265 couchdbLogger.Debugf("[%s] Database already exists", dbclient.dbName) 266 } 267 268 if dbclient.dbName != "_users" { 269 errSecurity := dbclient.applyDatabasePermissions() 270 if errSecurity != nil { 271 return errSecurity 272 } 273 } 274 275 couchdbLogger.Debugf("[%s] Exiting CreateDatabaseIfNotExist()", dbclient.dbName) 276 return nil 277 } 278 279 func (dbclient *couchDatabase) applyDatabasePermissions() error { 280 281 //If the username and password are not set, then skip applying permissions 282 if dbclient.couchInstance.conf.Username == "" && dbclient.couchInstance.conf.Password == "" { 283 return nil 284 } 285 286 securityPermissions := &databaseSecurity{} 287 288 securityPermissions.Admins.Names = append(securityPermissions.Admins.Names, dbclient.couchInstance.conf.Username) 289 securityPermissions.Members.Names = append(securityPermissions.Members.Names, dbclient.couchInstance.conf.Username) 290 291 err := dbclient.applyDatabaseSecurity(securityPermissions) 292 if err != nil { 293 return err 294 } 295 296 return nil 297 } 298 299 //getDatabaseInfo method provides function to retrieve database information 300 func (dbclient *couchDatabase) getDatabaseInfo() (*dbInfo, *dbReturn, error) { 301 302 connectURL, err := url.Parse(dbclient.couchInstance.url()) 303 if err != nil { 304 couchdbLogger.Errorf("URL parse error: %s", err) 305 return nil, nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 306 } 307 308 //get the number of retries 309 maxRetries := dbclient.couchInstance.conf.MaxRetries 310 311 resp, couchDBReturn, err := dbclient.handleRequest(http.MethodGet, "GetDatabaseInfo", connectURL, nil, "", "", maxRetries, true, nil) 312 if err != nil { 313 return nil, couchDBReturn, err 314 } 315 defer closeResponseBody(resp) 316 317 dbResponse := &dbInfo{} 318 decodeErr := json.NewDecoder(resp.Body).Decode(&dbResponse) 319 if decodeErr != nil { 320 return nil, nil, errors.Wrap(decodeErr, "error decoding response body") 321 } 322 323 // trace the database info response 324 couchdbLogger.Debugw("GetDatabaseInfo()", "dbResponseJSON", dbResponse) 325 326 return dbResponse, couchDBReturn, nil 327 328 } 329 330 //verifyCouchConfig method provides function to verify the connection information 331 func (couchInstance *couchInstance) verifyCouchConfig() (*connectionInfo, *dbReturn, error) { 332 333 couchdbLogger.Debugf("Entering VerifyCouchConfig()") 334 defer couchdbLogger.Debugf("Exiting VerifyCouchConfig()") 335 336 connectURL, err := url.Parse(couchInstance.url()) 337 if err != nil { 338 couchdbLogger.Errorf("URL parse error: %s", err) 339 return nil, nil, errors.Wrapf(err, "error parsing couch instance URL: %s", couchInstance.url()) 340 } 341 connectURL.Path = "/" 342 343 //get the number of retries for startup 344 maxRetriesOnStartup := couchInstance.conf.MaxRetriesOnStartup 345 346 resp, couchDBReturn, err := couchInstance.handleRequest(context.Background(), http.MethodGet, "", "VerifyCouchConfig", connectURL, nil, 347 "", "", maxRetriesOnStartup, true, nil) 348 349 if err != nil { 350 return nil, couchDBReturn, errors.WithMessage(err, "unable to connect to CouchDB, check the hostname and port") 351 } 352 defer closeResponseBody(resp) 353 354 dbResponse := &connectionInfo{} 355 decodeErr := json.NewDecoder(resp.Body).Decode(&dbResponse) 356 if decodeErr != nil { 357 return nil, nil, errors.Wrap(decodeErr, "error decoding response body") 358 } 359 360 // trace the database info response 361 couchdbLogger.Debugw("VerifyConnection()", "dbResponseJSON", dbResponse) 362 363 //check to see if the system databases exist 364 //Verifying the existence of the system database accomplishes two steps 365 //1. Ensures the system databases are created 366 //2. Verifies the username password provided in the CouchDB config are valid for system admin 367 err = createSystemDatabasesIfNotExist(couchInstance) 368 if err != nil { 369 couchdbLogger.Errorf("Unable to connect to CouchDB, error: %s. Check the admin username and password.", err) 370 return nil, nil, errors.WithMessage(err, "unable to connect to CouchDB. Check the admin username and password") 371 } 372 373 return dbResponse, couchDBReturn, nil 374 } 375 376 // isEmpty returns false if couchInstance contains any databases 377 // (except couchdb system databases and any database name supplied in the parameter 'databasesToIgnore') 378 func (couchInstance *couchInstance) isEmpty(databasesToIgnore []string) (bool, error) { 379 toIgnore := map[string]bool{} 380 for _, s := range databasesToIgnore { 381 toIgnore[s] = true 382 } 383 applicationDBNames, err := couchInstance.retrieveApplicationDBNames() 384 if err != nil { 385 return false, err 386 } 387 for _, dbName := range applicationDBNames { 388 if !toIgnore[dbName] { 389 return false, nil 390 } 391 } 392 return true, nil 393 } 394 395 // retrieveApplicationDBNames returns all the application database names in the couch instance 396 func (couchInstance *couchInstance) retrieveApplicationDBNames() ([]string, error) { 397 connectURL, err := url.Parse(couchInstance.url()) 398 if err != nil { 399 couchdbLogger.Errorf("URL parse error: %s", err) 400 return nil, errors.Wrapf(err, "error parsing couch instance URL: %s", couchInstance.url()) 401 } 402 connectURL.Path = "/_all_dbs" 403 maxRetries := couchInstance.conf.MaxRetries 404 resp, _, err := couchInstance.handleRequest( 405 context.Background(), 406 http.MethodGet, 407 "", 408 "IsEmpty", 409 connectURL, 410 nil, 411 "", 412 "", 413 maxRetries, 414 true, 415 nil, 416 ) 417 418 if err != nil { 419 return nil, errors.WithMessage(err, "unable to connect to CouchDB, check the hostname and port") 420 } 421 422 var dbNames []string 423 defer closeResponseBody(resp) 424 if err := json.NewDecoder(resp.Body).Decode(&dbNames); err != nil { 425 return nil, errors.Wrap(err, "error decoding response body") 426 } 427 couchdbLogger.Debugf("dbNames = %s", dbNames) 428 applicationsDBNames := []string{} 429 for _, d := range dbNames { 430 if !isCouchSystemDBName(d) { 431 applicationsDBNames = append(applicationsDBNames, d) 432 } 433 } 434 return applicationsDBNames, nil 435 } 436 437 func isCouchSystemDBName(name string) bool { 438 return strings.HasPrefix(name, "_") 439 } 440 441 // healthCheck checks if the peer is able to communicate with CouchDB 442 func (couchInstance *couchInstance) healthCheck(ctx context.Context) error { 443 connectURL, err := url.Parse(couchInstance.url()) 444 if err != nil { 445 couchdbLogger.Errorf("URL parse error: %s", err) 446 return errors.Wrapf(err, "error parsing CouchDB URL: %s", couchInstance.url()) 447 } 448 _, _, err = couchInstance.handleRequest(ctx, http.MethodHead, "", "HealthCheck", connectURL, nil, "", "", 0, true, nil) 449 if err != nil { 450 return fmt.Errorf("failed to connect to couch db [%s]", err) 451 } 452 return nil 453 } 454 455 // internalQueryLimit returns the maximum number of records to return internally 456 // when querying CouchDB. 457 func (couchInstance *couchInstance) internalQueryLimit() int32 { 458 return int32(couchInstance.conf.InternalQueryLimit) 459 } 460 461 // maxBatchUpdateSize returns the maximum number of records to include in a 462 // bulk update operation. 463 func (couchInstance *couchInstance) maxBatchUpdateSize() int { 464 return couchInstance.conf.MaxBatchUpdateSize 465 } 466 467 // url returns the URL for the CouchDB instance. 468 func (couchInstance *couchInstance) url() string { 469 URL := &url.URL{ 470 Host: couchInstance.conf.Address, 471 Scheme: "http", 472 } 473 return URL.String() 474 } 475 476 //dropDatabase provides method to drop an existing database 477 func (dbclient *couchDatabase) dropDatabase() (*dbOperationResponse, error) { 478 dbName := dbclient.dbName 479 480 couchdbLogger.Debugf("[%s] Entering DropDatabase()", dbName) 481 482 connectURL, err := url.Parse(dbclient.couchInstance.url()) 483 if err != nil { 484 couchdbLogger.Errorf("URL parse error: %s", err) 485 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 486 } 487 488 //get the number of retries 489 maxRetries := dbclient.couchInstance.conf.MaxRetries 490 491 resp, _, err := dbclient.handleRequest(http.MethodDelete, "DropDatabase", connectURL, nil, "", "", maxRetries, true, nil) 492 if err != nil { 493 return nil, err 494 } 495 defer closeResponseBody(resp) 496 497 dbResponse := &dbOperationResponse{} 498 decodeErr := json.NewDecoder(resp.Body).Decode(&dbResponse) 499 if decodeErr != nil { 500 return nil, errors.Wrap(decodeErr, "error decoding response body") 501 } 502 503 if dbResponse.Ok { 504 couchdbLogger.Debugf("[%s] Dropped database", dbclient.dbName) 505 } 506 507 couchdbLogger.Debugf("[%s] Exiting DropDatabase()", dbclient.dbName) 508 509 if dbResponse.Ok { 510 return dbResponse, nil 511 } 512 513 return dbResponse, errors.New("error dropping database") 514 } 515 516 //saveDoc method provides a function to save a document, id and byte array 517 func (dbclient *couchDatabase) saveDoc(id string, rev string, couchDoc *couchDoc) (string, error) { 518 dbName := dbclient.dbName 519 520 couchdbLogger.Debugf("[%s] Entering SaveDoc() id=[%s]", dbName, id) 521 522 if !utf8.ValidString(id) { 523 return "", errors.Errorf("doc id [%x] not a valid utf8 string", id) 524 } 525 526 saveURL, err := url.Parse(dbclient.couchInstance.url()) 527 if err != nil { 528 couchdbLogger.Errorf("URL parse error: %s", err) 529 return "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 530 } 531 532 //Set up a buffer for the data to be pushed to couchdb 533 var data []byte 534 535 //Set up a default boundary for use by multipart if sending attachments 536 defaultBoundary := "" 537 538 //Create a flag for shared connections. This is set to false for zero length attachments 539 keepConnectionOpen := true 540 541 //check to see if attachments is nil, if so, then this is a JSON only 542 if couchDoc.attachments == nil { 543 544 //Test to see if this is a valid JSON 545 if !isJSON(string(couchDoc.jsonValue)) { 546 return "", errors.New("JSON format is not valid") 547 } 548 549 // if there are no attachments, then use the bytes passed in as the JSON 550 data = couchDoc.jsonValue 551 552 } else { // there are attachments 553 554 //attachments are included, create the multipart definition 555 multipartData, multipartBoundary, err3 := createAttachmentPart(couchDoc) 556 if err3 != nil { 557 return "", err3 558 } 559 560 //If there is a zero length attachment, do not keep the connection open 561 for _, attach := range couchDoc.attachments { 562 if attach.Length < 1 { 563 keepConnectionOpen = false 564 } 565 } 566 567 //Set the data buffer to the data from the create multi-part data 568 data = multipartData.Bytes() 569 570 //Set the default boundary to the value generated in the multipart creation 571 defaultBoundary = multipartBoundary 572 573 } 574 575 //get the number of retries 576 maxRetries := dbclient.couchInstance.conf.MaxRetries 577 578 //handle the request for saving document with a retry if there is a revision conflict 579 resp, _, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodPut, dbName, "SaveDoc", saveURL, data, rev, defaultBoundary, maxRetries, keepConnectionOpen, nil) 580 581 if err != nil { 582 return "", err 583 } 584 defer closeResponseBody(resp) 585 586 //get the revision and return 587 revision, err := getRevisionHeader(resp) 588 if err != nil { 589 return "", err 590 } 591 592 couchdbLogger.Debugf("[%s] Exiting SaveDoc()", dbclient.dbName) 593 594 return revision, nil 595 596 } 597 598 //getDocumentRevision will return the revision if the document exists, otherwise it will return "" 599 func (dbclient *couchDatabase) getDocumentRevision(id string) string { 600 601 var rev = "" 602 603 //See if the document already exists, we need the rev for saves and deletes 604 _, revdoc, err := dbclient.readDoc(id) 605 if err == nil { 606 //set the revision to the rev returned from the document read 607 rev = revdoc 608 } 609 return rev 610 } 611 612 func createAttachmentPart(couchDoc *couchDoc) (bytes.Buffer, string, error) { 613 614 //Create a buffer for writing the result 615 writeBuffer := new(bytes.Buffer) 616 617 // read the attachment and save as an attachment 618 writer := multipart.NewWriter(writeBuffer) 619 620 //retrieve the boundary for the multipart 621 defaultBoundary := writer.Boundary() 622 623 fileAttachments := map[string]fileDetails{} 624 625 for _, attachment := range couchDoc.attachments { 626 fileAttachments[attachment.Name] = fileDetails{true, attachment.ContentType, len(attachment.AttachmentBytes)} 627 } 628 629 attachmentJSONMap := map[string]interface{}{ 630 "_attachments": fileAttachments, 631 } 632 633 //Add any data uploaded with the files 634 if couchDoc.jsonValue != nil { 635 636 //create a generic map 637 genericMap := make(map[string]interface{}) 638 639 //unmarshal the data into the generic map 640 decoder := json.NewDecoder(bytes.NewBuffer(couchDoc.jsonValue)) 641 decoder.UseNumber() 642 decodeErr := decoder.Decode(&genericMap) 643 if decodeErr != nil { 644 return *writeBuffer, "", errors.Wrap(decodeErr, "error decoding json data") 645 } 646 647 //add all key/values to the attachmentJSONMap 648 for jsonKey, jsonValue := range genericMap { 649 attachmentJSONMap[jsonKey] = jsonValue 650 } 651 652 } 653 654 filesForUpload, err := json.Marshal(attachmentJSONMap) 655 if err != nil { 656 return *writeBuffer, "", errors.Wrap(err, "error marshalling json data") 657 } 658 659 couchdbLogger.Debugf(string(filesForUpload)) 660 661 //create the header for the JSON 662 header := make(textproto.MIMEHeader) 663 header.Set("Content-Type", "application/json") 664 665 part, err := writer.CreatePart(header) 666 if err != nil { 667 return *writeBuffer, defaultBoundary, errors.Wrap(err, "error creating multipart") 668 } 669 670 part.Write(filesForUpload) 671 672 for _, attachment := range couchDoc.attachments { 673 674 header := make(textproto.MIMEHeader) 675 part, err2 := writer.CreatePart(header) 676 if err2 != nil { 677 return *writeBuffer, defaultBoundary, errors.Wrap(err2, "error creating multipart") 678 } 679 part.Write(attachment.AttachmentBytes) 680 681 } 682 683 err = writer.Close() 684 if err != nil { 685 return *writeBuffer, defaultBoundary, errors.Wrap(err, "error closing multipart writer") 686 } 687 688 return *writeBuffer, defaultBoundary, nil 689 690 } 691 692 func getRevisionHeader(resp *http.Response) (string, error) { 693 694 if resp == nil { 695 return "", errors.New("no response received from CouchDB") 696 } 697 698 revision := resp.Header.Get("Etag") 699 700 if revision == "" { 701 return "", errors.New("no revision tag detected") 702 } 703 704 reg := regexp.MustCompile(`"([^"]*)"`) 705 revisionNoQuotes := reg.ReplaceAllString(revision, "${1}") 706 return revisionNoQuotes, nil 707 708 } 709 710 //readDoc method provides function to retrieve a document and its revision 711 //from the database by id 712 func (dbclient *couchDatabase) readDoc(id string) (*couchDoc, string, error) { 713 var couchDoc couchDoc 714 attachments := []*attachmentInfo{} 715 dbName := dbclient.dbName 716 717 couchdbLogger.Debugf("[%s] Entering ReadDoc() id=[%s]", dbName, id) 718 if !utf8.ValidString(id) { 719 return nil, "", errors.Errorf("doc id [%x] not a valid utf8 string", id) 720 } 721 722 readURL, err := url.Parse(dbclient.couchInstance.url()) 723 if err != nil { 724 couchdbLogger.Errorf("URL parse error: %s", err) 725 return nil, "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 726 } 727 728 query := readURL.Query() 729 query.Add("attachments", "true") 730 731 //get the number of retries 732 maxRetries := dbclient.couchInstance.conf.MaxRetries 733 734 resp, couchDBReturn, err := dbclient.handleRequest(http.MethodGet, "ReadDoc", readURL, nil, "", "", maxRetries, true, &query, id) 735 if err != nil { 736 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 737 couchdbLogger.Debugf("[%s] Document not found (404), returning nil value instead of 404 error", dbclient.dbName) 738 // non-existent document should return nil value instead of a 404 error 739 // for details see https://github.com/hyperledger-archives/fabric/issues/936 740 return nil, "", nil 741 } 742 couchdbLogger.Debugf("[%s] couchDBReturn=%v\n", dbclient.dbName, couchDBReturn) 743 return nil, "", err 744 } 745 defer closeResponseBody(resp) 746 747 //Get the media type from the Content-Type header 748 mediaType, params, err := mime.ParseMediaType(resp.Header.Get("Content-Type")) 749 if err != nil { 750 log.Fatal(err) 751 } 752 753 //Get the revision from header 754 revision, err := getRevisionHeader(resp) 755 if err != nil { 756 return nil, "", err 757 } 758 759 //check to see if the is multipart, handle as attachment if multipart is detected 760 if strings.HasPrefix(mediaType, "multipart/") { 761 //Set up the multipart reader based on the boundary 762 multipartReader := multipart.NewReader(resp.Body, params["boundary"]) 763 for { 764 p, err := multipartReader.NextPart() 765 if err == io.EOF { 766 break // processed all parts 767 } 768 if err != nil { 769 return nil, "", errors.Wrap(err, "error reading next multipart") 770 } 771 772 defer p.Close() 773 774 couchdbLogger.Debugf("[%s] part header=%s", dbclient.dbName, p.Header) 775 switch p.Header.Get("Content-Type") { 776 case "application/json": 777 partdata, err := ioutil.ReadAll(p) 778 if err != nil { 779 return nil, "", errors.Wrap(err, "error reading multipart data") 780 } 781 couchDoc.jsonValue = partdata 782 default: 783 784 //Create an attachment structure and load it 785 attachment := &attachmentInfo{} 786 attachment.ContentType = p.Header.Get("Content-Type") 787 contentDispositionParts := strings.Split(p.Header.Get("Content-Disposition"), ";") 788 if strings.TrimSpace(contentDispositionParts[0]) == "attachment" { 789 switch p.Header.Get("Content-Encoding") { 790 case "gzip": //See if the part is gzip encoded 791 792 var respBody []byte 793 794 gr, err := gzip.NewReader(p) 795 if err != nil { 796 return nil, "", errors.Wrap(err, "error creating gzip reader") 797 } 798 respBody, err = ioutil.ReadAll(gr) 799 if err != nil { 800 return nil, "", errors.Wrap(err, "error reading gzip data") 801 } 802 803 couchdbLogger.Debugf("[%s] Retrieved attachment data", dbclient.dbName) 804 attachment.AttachmentBytes = respBody 805 attachment.Length = uint64(len(attachment.AttachmentBytes)) 806 attachment.Name = p.FileName() 807 attachments = append(attachments, attachment) 808 809 default: 810 811 //retrieve the data, this is not gzip 812 partdata, err := ioutil.ReadAll(p) 813 if err != nil { 814 return nil, "", errors.Wrap(err, "error reading multipart data") 815 } 816 couchdbLogger.Debugf("[%s] Retrieved attachment data", dbclient.dbName) 817 attachment.AttachmentBytes = partdata 818 attachment.Length = uint64(len(attachment.AttachmentBytes)) 819 attachment.Name = p.FileName() 820 attachments = append(attachments, attachment) 821 822 } // end content-encoding switch 823 } // end if attachment 824 } // end content-type switch 825 } // for all multiparts 826 827 couchDoc.attachments = attachments 828 829 return &couchDoc, revision, nil 830 } 831 832 //handle as JSON document 833 couchDoc.jsonValue, err = ioutil.ReadAll(resp.Body) 834 if err != nil { 835 return nil, "", errors.Wrap(err, "error reading response body") 836 } 837 838 couchdbLogger.Debugf("[%s] Exiting ReadDoc()", dbclient.dbName) 839 return &couchDoc, revision, nil 840 } 841 842 //readDocRange method provides function to a range of documents based on the start and end keys 843 //startKey and endKey can also be empty strings. If startKey and endKey are empty, all documents are returned 844 //This function provides a limit option to specify the max number of entries and is supplied by config. 845 //Skip is reserved for possible future future use. 846 func (dbclient *couchDatabase) readDocRange(startKey, endKey string, limit int32) ([]*queryResult, string, error) { 847 dbName := dbclient.dbName 848 couchdbLogger.Debugf("[%s] Entering ReadDocRange() startKey=%s, endKey=%s", dbName, startKey, endKey) 849 850 var results []*queryResult 851 852 rangeURL, err := url.Parse(dbclient.couchInstance.url()) 853 if err != nil { 854 couchdbLogger.Errorf("URL parse error: %s", err) 855 return nil, "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 856 } 857 858 queryParms := rangeURL.Query() 859 //Increment the limit by 1 to see if there are more qualifying records 860 queryParms.Set("limit", strconv.FormatInt(int64(limit+1), 10)) 861 queryParms.Add("include_docs", "true") 862 queryParms.Add("inclusive_end", "false") // endkey should be exclusive to be consistent with goleveldb 863 queryParms.Add("attachments", "true") // get the attachments as well 864 865 //Append the startKey if provided 866 if startKey != "" { 867 if startKey, err = encodeForJSON(startKey); err != nil { 868 return nil, "", err 869 } 870 queryParms.Add("startkey", "\""+startKey+"\"") 871 } 872 873 //Append the endKey if provided 874 if endKey != "" { 875 var err error 876 if endKey, err = encodeForJSON(endKey); err != nil { 877 return nil, "", err 878 } 879 queryParms.Add("endkey", "\""+endKey+"\"") 880 } 881 882 //get the number of retries 883 maxRetries := dbclient.couchInstance.conf.MaxRetries 884 885 resp, _, err := dbclient.handleRequest(http.MethodGet, "RangeDocRange", rangeURL, nil, "", "", maxRetries, true, &queryParms, "_all_docs") 886 if err != nil { 887 return nil, "", err 888 } 889 defer closeResponseBody(resp) 890 891 if couchdbLogger.IsEnabledFor(zapcore.DebugLevel) { 892 dump, err2 := httputil.DumpResponse(resp, false) 893 if err2 != nil { 894 log.Fatal(err2) 895 } 896 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 897 couchdbLogger.Debugf("[%s] HTTP Response: %s", dbclient.dbName, bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 898 } 899 900 //handle as JSON document 901 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 902 if err != nil { 903 return nil, "", errors.Wrap(err, "error reading response body") 904 } 905 906 var jsonResponse = &rangeQueryResponse{} 907 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 908 if err2 != nil { 909 return nil, "", errors.Wrap(err2, "error unmarshalling json data") 910 } 911 912 //if an additional record is found, then reduce the count by 1 913 //and populate the nextStartKey 914 if jsonResponse.TotalRows > limit { 915 jsonResponse.TotalRows = limit 916 } 917 918 couchdbLogger.Debugf("[%s] Total Rows: %d", dbclient.dbName, jsonResponse.TotalRows) 919 920 //Use the next endKey as the starting default for the nextStartKey 921 nextStartKey := endKey 922 923 for index, row := range jsonResponse.Rows { 924 925 var docMetadata = &docMetadata{} 926 err3 := json.Unmarshal(row.Doc, &docMetadata) 927 if err3 != nil { 928 return nil, "", errors.Wrap(err3, "error unmarshalling json data") 929 } 930 931 //if there is an extra row for the nextStartKey, then do not add the row to the result set 932 //and populate the nextStartKey variable 933 if int32(index) >= jsonResponse.TotalRows { 934 nextStartKey = docMetadata.ID 935 continue 936 } 937 938 if docMetadata.AttachmentsInfo != nil { 939 940 couchdbLogger.Debugf("[%s] Adding JSON document and attachments for id: %s", dbclient.dbName, docMetadata.ID) 941 942 attachments := []*attachmentInfo{} 943 for attachmentName, attachment := range docMetadata.AttachmentsInfo { 944 attachment.Name = attachmentName 945 946 attachments = append(attachments, attachment) 947 } 948 949 var addDocument = &queryResult{docMetadata.ID, row.Doc, attachments} 950 results = append(results, addDocument) 951 952 } else { 953 954 couchdbLogger.Debugf("[%s] Adding json docment for id: %s", dbclient.dbName, docMetadata.ID) 955 956 var addDocument = &queryResult{docMetadata.ID, row.Doc, nil} 957 results = append(results, addDocument) 958 959 } 960 961 } 962 963 couchdbLogger.Debugf("[%s] Exiting ReadDocRange()", dbclient.dbName) 964 965 return results, nextStartKey, nil 966 967 } 968 969 //deleteDoc method provides function to delete a document from the database by id 970 func (dbclient *couchDatabase) deleteDoc(id, rev string) error { 971 dbName := dbclient.dbName 972 973 couchdbLogger.Debugf("[%s] Entering DeleteDoc() id=%s", dbName, id) 974 975 deleteURL, err := url.Parse(dbclient.couchInstance.url()) 976 if err != nil { 977 couchdbLogger.Errorf("URL parse error: %s", err) 978 return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 979 } 980 981 //get the number of retries 982 maxRetries := dbclient.couchInstance.conf.MaxRetries 983 984 //handle the request for saving document with a retry if there is a revision conflict 985 resp, couchDBReturn, err := dbclient.handleRequestWithRevisionRetry(id, http.MethodDelete, dbName, "DeleteDoc", 986 deleteURL, nil, "", "", maxRetries, true, nil) 987 988 if err != nil { 989 if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { 990 couchdbLogger.Debugf("[%s] Document not found (404), returning nil value instead of 404 error", dbclient.dbName) 991 // non-existent document should return nil value instead of a 404 error 992 // for details see https://github.com/hyperledger-archives/fabric/issues/936 993 return nil 994 } 995 return err 996 } 997 defer closeResponseBody(resp) 998 999 couchdbLogger.Debugf("[%s] Exiting DeleteDoc()", dbclient.dbName) 1000 1001 return nil 1002 1003 } 1004 1005 //queryDocuments method provides function for processing a query 1006 func (dbclient *couchDatabase) queryDocuments(query string) ([]*queryResult, string, error) { 1007 dbName := dbclient.dbName 1008 1009 couchdbLogger.Debugf("[%s] Entering QueryDocuments() query=%s", dbName, query) 1010 1011 var results []*queryResult 1012 1013 queryURL, err := url.Parse(dbclient.couchInstance.url()) 1014 if err != nil { 1015 couchdbLogger.Errorf("URL parse error: %s", err) 1016 return nil, "", errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 1017 } 1018 1019 //get the number of retries 1020 maxRetries := dbclient.couchInstance.conf.MaxRetries 1021 1022 resp, _, err := dbclient.handleRequest(http.MethodPost, "QueryDocuments", queryURL, []byte(query), "", "", maxRetries, true, nil, "_find") 1023 if err != nil { 1024 return nil, "", err 1025 } 1026 defer closeResponseBody(resp) 1027 1028 if couchdbLogger.IsEnabledFor(zapcore.DebugLevel) { 1029 dump, err2 := httputil.DumpResponse(resp, false) 1030 if err2 != nil { 1031 log.Fatal(err2) 1032 } 1033 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1034 couchdbLogger.Debugf("[%s] HTTP Response: %s", dbclient.dbName, bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1035 } 1036 1037 //handle as JSON document 1038 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1039 if err != nil { 1040 return nil, "", errors.Wrap(err, "error reading response body") 1041 } 1042 1043 var jsonResponse = &queryResponse{} 1044 1045 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1046 if err2 != nil { 1047 return nil, "", errors.Wrap(err2, "error unmarshalling json data") 1048 } 1049 1050 if jsonResponse.Warning != "" { 1051 couchdbLogger.Warnf("The query [%s] caused the following warning: [%s]", query, jsonResponse.Warning) 1052 } 1053 1054 for _, row := range jsonResponse.Docs { 1055 1056 var docMetadata = &docMetadata{} 1057 err3 := json.Unmarshal(row, &docMetadata) 1058 if err3 != nil { 1059 return nil, "", errors.Wrap(err3, "error unmarshalling json data") 1060 } 1061 1062 // JSON Query results never have attachments 1063 // The If block below will never be executed 1064 if docMetadata.AttachmentsInfo != nil { 1065 1066 couchdbLogger.Debugf("[%s] Adding JSON docment and attachments for id: %s", dbclient.dbName, docMetadata.ID) 1067 1068 couchDoc, _, err := dbclient.readDoc(docMetadata.ID) 1069 if err != nil { 1070 return nil, "", err 1071 } 1072 var addDocument = &queryResult{id: docMetadata.ID, value: couchDoc.jsonValue, attachments: couchDoc.attachments} 1073 results = append(results, addDocument) 1074 1075 } else { 1076 couchdbLogger.Debugf("[%s] Adding json docment for id: %s", dbclient.dbName, docMetadata.ID) 1077 var addDocument = &queryResult{id: docMetadata.ID, value: row, attachments: nil} 1078 1079 results = append(results, addDocument) 1080 1081 } 1082 } 1083 1084 couchdbLogger.Debugf("[%s] Exiting QueryDocuments()", dbclient.dbName) 1085 1086 return results, jsonResponse.Bookmark, nil 1087 1088 } 1089 1090 // listIndex method lists the defined indexes for a database 1091 func (dbclient *couchDatabase) listIndex() ([]*indexResult, error) { 1092 1093 //IndexDefinition contains the definition for a couchdb index 1094 type indexDefinition struct { 1095 DesignDocument string `json:"ddoc"` 1096 Name string `json:"name"` 1097 Type string `json:"type"` 1098 Definition json.RawMessage `json:"def"` 1099 } 1100 1101 //ListIndexResponse contains the definition for listing couchdb indexes 1102 type listIndexResponse struct { 1103 TotalRows int `json:"total_rows"` 1104 Indexes []indexDefinition `json:"indexes"` 1105 } 1106 1107 dbName := dbclient.dbName 1108 couchdbLogger.Debugf("[%s] Entering ListIndex()", dbName) 1109 1110 indexURL, err := url.Parse(dbclient.couchInstance.url()) 1111 if err != nil { 1112 couchdbLogger.Errorf("URL parse error: %s", err) 1113 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 1114 } 1115 1116 //get the number of retries 1117 maxRetries := dbclient.couchInstance.conf.MaxRetries 1118 1119 resp, _, err := dbclient.handleRequest(http.MethodGet, "ListIndex", indexURL, nil, "", "", maxRetries, true, nil, "_index") 1120 if err != nil { 1121 return nil, err 1122 } 1123 defer closeResponseBody(resp) 1124 1125 //handle as JSON document 1126 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1127 if err != nil { 1128 return nil, errors.Wrap(err, "error reading response body") 1129 } 1130 1131 var jsonResponse = &listIndexResponse{} 1132 1133 err2 := json.Unmarshal(jsonResponseRaw, jsonResponse) 1134 if err2 != nil { 1135 return nil, errors.Wrap(err2, "error unmarshalling json data") 1136 } 1137 1138 var results []*indexResult 1139 1140 for _, row := range jsonResponse.Indexes { 1141 1142 //if the DesignDocument does not begin with "_design/", then this is a system 1143 //level index and is not meaningful and cannot be edited or deleted 1144 designDoc := row.DesignDocument 1145 s := strings.SplitAfterN(designDoc, "_design/", 2) 1146 if len(s) > 1 { 1147 designDoc = s[1] 1148 1149 //Add the index definition to the results 1150 var addIndexResult = &indexResult{DesignDocument: designDoc, Name: row.Name, Definition: fmt.Sprintf("%s", row.Definition)} 1151 results = append(results, addIndexResult) 1152 } 1153 1154 } 1155 1156 couchdbLogger.Debugf("[%s] Exiting ListIndex()", dbclient.dbName) 1157 1158 return results, nil 1159 1160 } 1161 1162 // createIndex method provides a function creating an index 1163 func (dbclient *couchDatabase) createIndex(indexdefinition string) (*createIndexResponse, error) { 1164 dbName := dbclient.dbName 1165 1166 couchdbLogger.Debugf("[%s] Entering CreateIndex() indexdefinition=%s", dbName, indexdefinition) 1167 1168 //Test to see if this is a valid JSON 1169 if !isJSON(indexdefinition) { 1170 return nil, errors.New("JSON format is not valid") 1171 } 1172 1173 indexURL, err := url.Parse(dbclient.couchInstance.url()) 1174 if err != nil { 1175 couchdbLogger.Errorf("URL parse error: %s", err) 1176 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 1177 } 1178 1179 //get the number of retries 1180 maxRetries := dbclient.couchInstance.conf.MaxRetries 1181 1182 resp, _, err := dbclient.handleRequest(http.MethodPost, "CreateIndex", indexURL, []byte(indexdefinition), "", "", maxRetries, true, nil, "_index") 1183 if err != nil { 1184 return nil, err 1185 } 1186 defer closeResponseBody(resp) 1187 1188 if resp == nil { 1189 return nil, errors.New("invalid response received from CouchDB") 1190 } 1191 1192 //Read the response body 1193 respBody, err := ioutil.ReadAll(resp.Body) 1194 if err != nil { 1195 return nil, errors.Wrap(err, "error reading response body") 1196 } 1197 1198 couchDBReturn := &createIndexResponse{} 1199 1200 jsonBytes := []byte(respBody) 1201 1202 //unmarshal the response 1203 err = json.Unmarshal(jsonBytes, &couchDBReturn) 1204 if err != nil { 1205 return nil, errors.Wrap(err, "error unmarshalling json data") 1206 } 1207 1208 if couchDBReturn.Result == "created" { 1209 1210 couchdbLogger.Infof("Created CouchDB index [%s] in state database [%s] using design document [%s]", couchDBReturn.Name, dbclient.dbName, couchDBReturn.ID) 1211 1212 return couchDBReturn, nil 1213 1214 } 1215 1216 couchdbLogger.Infof("Updated CouchDB index [%s] in state database [%s] using design document [%s]", couchDBReturn.Name, dbclient.dbName, couchDBReturn.ID) 1217 1218 return couchDBReturn, nil 1219 } 1220 1221 // deleteIndex method provides a function deleting an index 1222 func (dbclient *couchDatabase) deleteIndex(designdoc, indexname string) error { 1223 dbName := dbclient.dbName 1224 1225 couchdbLogger.Debugf("[%s] Entering DeleteIndex() designdoc=%s indexname=%s", dbName, designdoc, indexname) 1226 1227 indexURL, err := url.Parse(dbclient.couchInstance.url()) 1228 if err != nil { 1229 couchdbLogger.Errorf("URL parse error: %s", err) 1230 return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 1231 } 1232 1233 //get the number of retries 1234 maxRetries := dbclient.couchInstance.conf.MaxRetries 1235 1236 resp, _, err := dbclient.handleRequest(http.MethodDelete, "DeleteIndex", indexURL, nil, "", "", maxRetries, true, nil, "_index", designdoc, "json", indexname) 1237 if err != nil { 1238 return err 1239 } 1240 defer closeResponseBody(resp) 1241 1242 return nil 1243 1244 } 1245 1246 //warmIndex method provides a function for warming a single index 1247 func (dbclient *couchDatabase) warmIndex(designdoc, indexname string) error { 1248 dbName := dbclient.dbName 1249 1250 couchdbLogger.Debugf("[%s] Entering WarmIndex() designdoc=%s indexname=%s", dbName, designdoc, indexname) 1251 1252 indexURL, err := url.Parse(dbclient.couchInstance.url()) 1253 if err != nil { 1254 couchdbLogger.Errorf("URL parse error: %s", err) 1255 return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 1256 } 1257 1258 queryParms := indexURL.Query() 1259 //Query parameter that allows the execution of the URL to return immediately 1260 //The update_after will cause the index update to run after the URL returns 1261 queryParms.Add("stale", "update_after") 1262 1263 //get the number of retries 1264 maxRetries := dbclient.couchInstance.conf.MaxRetries 1265 1266 resp, _, err := dbclient.handleRequest(http.MethodGet, "WarmIndex", indexURL, nil, "", "", maxRetries, true, &queryParms, "_design", designdoc, "_view", indexname) 1267 if err != nil { 1268 return err 1269 } 1270 defer closeResponseBody(resp) 1271 1272 return nil 1273 1274 } 1275 1276 //runWarmIndexAllIndexes is a wrapper for WarmIndexAllIndexes to catch and report any errors 1277 func (dbclient *couchDatabase) runWarmIndexAllIndexes() { 1278 1279 err := dbclient.warmIndexAllIndexes() 1280 if err != nil { 1281 couchdbLogger.Errorf("Error detected during WarmIndexAllIndexes(): %+v", err) 1282 } 1283 1284 } 1285 1286 //warmIndexAllIndexes method provides a function for warming all indexes for a database 1287 func (dbclient *couchDatabase) warmIndexAllIndexes() error { 1288 1289 couchdbLogger.Debugf("[%s] Entering WarmIndexAllIndexes()", dbclient.dbName) 1290 1291 //Retrieve all indexes 1292 listResult, err := dbclient.listIndex() 1293 if err != nil { 1294 return err 1295 } 1296 1297 //For each index definition, execute an index refresh 1298 for _, elem := range listResult { 1299 1300 err := dbclient.warmIndex(elem.DesignDocument, elem.Name) 1301 if err != nil { 1302 return err 1303 } 1304 1305 } 1306 1307 couchdbLogger.Debugf("[%s] Exiting WarmIndexAllIndexes()", dbclient.dbName) 1308 1309 return nil 1310 1311 } 1312 1313 //getDatabaseSecurity method provides function to retrieve the security config for a database 1314 func (dbclient *couchDatabase) getDatabaseSecurity() (*databaseSecurity, error) { 1315 dbName := dbclient.dbName 1316 1317 couchdbLogger.Debugf("[%s] Entering GetDatabaseSecurity()", dbName) 1318 1319 securityURL, err := url.Parse(dbclient.couchInstance.url()) 1320 if err != nil { 1321 couchdbLogger.Errorf("URL parse error: %s", err) 1322 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 1323 } 1324 1325 //get the number of retries 1326 maxRetries := dbclient.couchInstance.conf.MaxRetries 1327 1328 resp, _, err := dbclient.handleRequest(http.MethodGet, "GetDatabaseSecurity", securityURL, nil, "", "", maxRetries, true, nil, "_security") 1329 1330 if err != nil { 1331 return nil, err 1332 } 1333 defer closeResponseBody(resp) 1334 1335 //handle as JSON document 1336 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1337 if err != nil { 1338 return nil, errors.Wrap(err, "error reading response body") 1339 } 1340 1341 var jsonResponse = &databaseSecurity{} 1342 1343 err2 := json.Unmarshal(jsonResponseRaw, jsonResponse) 1344 if err2 != nil { 1345 return nil, errors.Wrap(err2, "error unmarshalling json data") 1346 } 1347 1348 couchdbLogger.Debugf("[%s] Exiting GetDatabaseSecurity()", dbclient.dbName) 1349 1350 return jsonResponse, nil 1351 1352 } 1353 1354 //applyDatabaseSecurity method provides function to update the security config for a database 1355 func (dbclient *couchDatabase) applyDatabaseSecurity(databaseSecurity *databaseSecurity) error { 1356 dbName := dbclient.dbName 1357 1358 couchdbLogger.Debugf("[%s] Entering ApplyDatabaseSecurity()", dbName) 1359 1360 securityURL, err := url.Parse(dbclient.couchInstance.url()) 1361 if err != nil { 1362 couchdbLogger.Errorf("URL parse error: %s", err) 1363 return errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 1364 } 1365 1366 //Ensure all of the arrays are initialized to empty arrays instead of nil 1367 if databaseSecurity.Admins.Names == nil { 1368 databaseSecurity.Admins.Names = make([]string, 0) 1369 } 1370 if databaseSecurity.Admins.Roles == nil { 1371 databaseSecurity.Admins.Roles = make([]string, 0) 1372 } 1373 if databaseSecurity.Members.Names == nil { 1374 databaseSecurity.Members.Names = make([]string, 0) 1375 } 1376 if databaseSecurity.Members.Roles == nil { 1377 databaseSecurity.Members.Roles = make([]string, 0) 1378 } 1379 1380 //get the number of retries 1381 maxRetries := dbclient.couchInstance.conf.MaxRetries 1382 1383 databaseSecurityJSON, err := json.Marshal(databaseSecurity) 1384 if err != nil { 1385 return errors.Wrap(err, "error unmarshalling json data") 1386 } 1387 1388 couchdbLogger.Debugf("[%s] Applying security to database: %s", dbclient.dbName, string(databaseSecurityJSON)) 1389 1390 resp, _, err := dbclient.handleRequest(http.MethodPut, "ApplyDatabaseSecurity", securityURL, databaseSecurityJSON, "", "", maxRetries, true, nil, "_security") 1391 1392 if err != nil { 1393 return err 1394 } 1395 defer closeResponseBody(resp) 1396 1397 couchdbLogger.Debugf("[%s] Exiting ApplyDatabaseSecurity()", dbclient.dbName) 1398 1399 return nil 1400 1401 } 1402 1403 //batchRetrieveDocumentMetadata - batch method to retrieve document metadata for a set of keys, 1404 //including ID, couchdb revision number, and ledger version 1405 func (dbclient *couchDatabase) batchRetrieveDocumentMetadata(keys []string) ([]*docMetadata, error) { 1406 1407 couchdbLogger.Debugf("[%s] Entering BatchRetrieveDocumentMetadata() keys=%s", dbclient.dbName, keys) 1408 1409 batchRetrieveURL, err := url.Parse(dbclient.couchInstance.url()) 1410 if err != nil { 1411 couchdbLogger.Errorf("URL parse error: %s", err) 1412 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 1413 } 1414 1415 queryParms := batchRetrieveURL.Query() 1416 1417 // While BatchRetrieveDocumentMetadata() does not return the entire document, 1418 // for reads/writes, we do need to get document so that we can get the ledger version of the key. 1419 // TODO For blind writes we do not need to get the version, therefore when we bulk get 1420 // the revision numbers for the write keys that were not represented in read set 1421 // (the second time BatchRetrieveDocumentMetadata is called during block processing), 1422 // we could set include_docs to false to optimize the response. 1423 queryParms.Add("include_docs", "true") 1424 1425 keymap := make(map[string]interface{}) 1426 1427 keymap["keys"] = keys 1428 1429 jsonKeys, err := json.Marshal(keymap) 1430 if err != nil { 1431 return nil, errors.Wrap(err, "error marshalling json data") 1432 } 1433 1434 //get the number of retries 1435 maxRetries := dbclient.couchInstance.conf.MaxRetries 1436 1437 resp, _, err := dbclient.handleRequest(http.MethodPost, "BatchRetrieveDocumentMetadata", batchRetrieveURL, jsonKeys, "", "", maxRetries, true, &queryParms, "_all_docs") 1438 if err != nil { 1439 return nil, err 1440 } 1441 defer closeResponseBody(resp) 1442 1443 if couchdbLogger.IsEnabledFor(zapcore.DebugLevel) { 1444 dump, _ := httputil.DumpResponse(resp, false) 1445 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1446 couchdbLogger.Debugf("[%s] HTTP Response: %s", dbclient.dbName, bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1447 } 1448 1449 //handle as JSON document 1450 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1451 if err != nil { 1452 return nil, errors.Wrap(err, "error reading response body") 1453 } 1454 1455 var jsonResponse = &batchRetrieveDocMetadataResponse{} 1456 1457 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1458 if err2 != nil { 1459 return nil, errors.Wrap(err2, "error unmarshalling json data") 1460 } 1461 1462 docMetadataArray := []*docMetadata{} 1463 1464 for _, row := range jsonResponse.Rows { 1465 docMetadata := &docMetadata{ID: row.ID, Rev: row.DocMetadata.Rev, Version: row.DocMetadata.Version} 1466 docMetadataArray = append(docMetadataArray, docMetadata) 1467 } 1468 1469 couchdbLogger.Debugf("[%s] Exiting BatchRetrieveDocumentMetadata()", dbclient.dbName) 1470 1471 return docMetadataArray, nil 1472 1473 } 1474 1475 //batchUpdateDocuments - batch method to batch update documents 1476 func (dbclient *couchDatabase) batchUpdateDocuments(documents []*couchDoc) ([]*batchUpdateResponse, error) { 1477 dbName := dbclient.dbName 1478 1479 if couchdbLogger.IsEnabledFor(zapcore.DebugLevel) { 1480 documentIdsString, err := printDocumentIds(documents) 1481 if err == nil { 1482 couchdbLogger.Debugf("[%s] Entering BatchUpdateDocuments() document ids=[%s]", dbName, documentIdsString) 1483 } else { 1484 couchdbLogger.Debugf("[%s] Entering BatchUpdateDocuments() Could not print document ids due to error: %+v", dbName, err) 1485 } 1486 } 1487 1488 batchUpdateURL, err := url.Parse(dbclient.couchInstance.url()) 1489 if err != nil { 1490 couchdbLogger.Errorf("URL parse error: %s", err) 1491 return nil, errors.Wrapf(err, "error parsing CouchDB URL: %s", dbclient.couchInstance.url()) 1492 } 1493 1494 documentMap := make(map[string]interface{}) 1495 1496 var jsonDocumentMap []interface{} 1497 1498 for _, jsonDocument := range documents { 1499 1500 //create a document map 1501 var document = make(map[string]interface{}) 1502 1503 //unmarshal the JSON component of the couchDoc into the document 1504 err = json.Unmarshal(jsonDocument.jsonValue, &document) 1505 if err != nil { 1506 return nil, errors.Wrap(err, "error unmarshalling json data") 1507 } 1508 1509 //iterate through any attachments 1510 if len(jsonDocument.attachments) > 0 { 1511 1512 //create a file attachment map 1513 fileAttachment := make(map[string]interface{}) 1514 1515 //for each attachment, create a base64Attachment, name the attachment, 1516 //add the content type and base64 encode the attachment 1517 for _, attachment := range jsonDocument.attachments { 1518 fileAttachment[attachment.Name] = base64Attachment{attachment.ContentType, 1519 base64.StdEncoding.EncodeToString(attachment.AttachmentBytes)} 1520 } 1521 1522 //add attachments to the document 1523 document["_attachments"] = fileAttachment 1524 1525 } 1526 1527 //Append the document to the map of documents 1528 jsonDocumentMap = append(jsonDocumentMap, document) 1529 1530 } 1531 1532 //Add the documents to the "docs" item 1533 documentMap["docs"] = jsonDocumentMap 1534 1535 bulkDocsJSON, err := json.Marshal(documentMap) 1536 if err != nil { 1537 return nil, errors.Wrap(err, "error marshalling json data") 1538 } 1539 1540 //get the number of retries 1541 maxRetries := dbclient.couchInstance.conf.MaxRetries 1542 1543 resp, _, err := dbclient.handleRequest(http.MethodPost, "BatchUpdateDocuments", batchUpdateURL, bulkDocsJSON, "", "", maxRetries, true, nil, "_bulk_docs") 1544 if err != nil { 1545 return nil, err 1546 } 1547 defer closeResponseBody(resp) 1548 1549 if couchdbLogger.IsEnabledFor(zapcore.DebugLevel) { 1550 dump, _ := httputil.DumpResponse(resp, false) 1551 // compact debug log by replacing carriage return / line feed with dashes to separate http headers 1552 couchdbLogger.Debugf("[%s] HTTP Response: %s", dbclient.dbName, bytes.Replace(dump, []byte{0x0d, 0x0a}, []byte{0x20, 0x7c, 0x20}, -1)) 1553 } 1554 1555 //handle as JSON document 1556 jsonResponseRaw, err := ioutil.ReadAll(resp.Body) 1557 if err != nil { 1558 return nil, errors.Wrap(err, "error reading response body") 1559 } 1560 1561 var jsonResponse = []*batchUpdateResponse{} 1562 err2 := json.Unmarshal(jsonResponseRaw, &jsonResponse) 1563 if err2 != nil { 1564 return nil, errors.Wrap(err2, "error unmarshalling json data") 1565 } 1566 1567 couchdbLogger.Debugf("[%s] Exiting BatchUpdateDocuments() _bulk_docs response=[%s]", dbclient.dbName, string(jsonResponseRaw)) 1568 1569 return jsonResponse, nil 1570 1571 } 1572 1573 //handleRequestWithRevisionRetry method is a generic http request handler with 1574 //a retry for document revision conflict errors, 1575 //which may be detected during saves or deletes that timed out from client http perspective, 1576 //but which eventually succeeded in couchdb 1577 func (dbclient *couchDatabase) handleRequestWithRevisionRetry(id, method, dbName, functionName string, connectURL *url.URL, data []byte, rev string, 1578 multipartBoundary string, maxRetries int, keepConnectionOpen bool, queryParms *url.Values) (*http.Response, *dbReturn, error) { 1579 1580 //Initialize a flag for the revision conflict 1581 revisionConflictDetected := false 1582 var resp *http.Response 1583 var couchDBReturn *dbReturn 1584 var errResp error 1585 1586 //attempt the http request for the max number of retries 1587 //In this case, the retry is to catch problems where a client timeout may miss a 1588 //successful CouchDB update and cause a document revision conflict on a retry in handleRequest 1589 for attempts := 0; attempts <= maxRetries; attempts++ { 1590 1591 //if the revision was not passed in, or if a revision conflict is detected on prior attempt, 1592 //query CouchDB for the document revision 1593 if rev == "" || revisionConflictDetected { 1594 rev = dbclient.getDocumentRevision(id) 1595 } 1596 1597 //handle the request for saving/deleting the couchdb data 1598 resp, couchDBReturn, errResp = dbclient.couchInstance.handleRequest(context.Background(), method, dbName, functionName, connectURL, 1599 data, rev, multipartBoundary, maxRetries, keepConnectionOpen, queryParms, id) 1600 1601 //If there was a 409 conflict error during the save/delete, log it and retry it. 1602 //Otherwise, break out of the retry loop 1603 if couchDBReturn != nil && couchDBReturn.StatusCode == 409 { 1604 couchdbLogger.Warningf("CouchDB document revision conflict detected, retrying. Attempt:%v", attempts+1) 1605 revisionConflictDetected = true 1606 } else { 1607 break 1608 } 1609 } 1610 1611 // return the handleRequest results 1612 return resp, couchDBReturn, errResp 1613 } 1614 1615 func (dbclient *couchDatabase) handleRequest(method, functionName string, connectURL *url.URL, data []byte, rev, multipartBoundary string, 1616 maxRetries int, keepConnectionOpen bool, queryParms *url.Values, pathElements ...string) (*http.Response, *dbReturn, error) { 1617 1618 return dbclient.couchInstance.handleRequest(context.Background(), 1619 method, dbclient.dbName, functionName, connectURL, data, rev, multipartBoundary, 1620 maxRetries, keepConnectionOpen, queryParms, pathElements..., 1621 ) 1622 } 1623 1624 //handleRequest method is a generic http request handler. 1625 // If it returns an error, it ensures that the response body is closed, else it is the 1626 // callee's responsibility to close response correctly. 1627 // Any http error or CouchDB error (4XX or 500) will result in a golang error getting returned 1628 func (couchInstance *couchInstance) handleRequest(ctx context.Context, method, dbName, functionName string, connectURL *url.URL, data []byte, rev string, 1629 multipartBoundary string, maxRetries int, keepConnectionOpen bool, queryParms *url.Values, pathElements ...string) (*http.Response, *dbReturn, error) { 1630 1631 couchdbLogger.Debugf("Entering handleRequest() method=%s url=%v dbName=%s", method, connectURL, dbName) 1632 1633 //create the return objects for couchDB 1634 var resp *http.Response 1635 var errResp error 1636 couchDBReturn := &dbReturn{} 1637 defer couchInstance.recordMetric(time.Now(), dbName, functionName, couchDBReturn) 1638 1639 //set initial wait duration for retries 1640 waitDuration := retryWaitTime * time.Millisecond 1641 1642 if maxRetries < 0 { 1643 return nil, nil, errors.New("number of retries must be zero or greater") 1644 } 1645 1646 requestURL := constructCouchDBUrl(connectURL, dbName, pathElements...) 1647 1648 if queryParms != nil { 1649 requestURL.RawQuery = queryParms.Encode() 1650 } 1651 1652 couchdbLogger.Debugf("Request URL: %s", requestURL) 1653 1654 //attempt the http request for the max number of retries 1655 // if maxRetries is 0, the database creation will be attempted once and will 1656 // return an error if unsuccessful 1657 // if maxRetries is 3 (default), a maximum of 4 attempts (one attempt with 3 retries) 1658 // will be made with warning entries for unsuccessful attempts 1659 for attempts := 0; attempts <= maxRetries; attempts++ { 1660 1661 //Set up a buffer for the payload data 1662 payloadData := new(bytes.Buffer) 1663 1664 payloadData.ReadFrom(bytes.NewReader(data)) 1665 1666 //Create request based on URL for couchdb operation 1667 req, err := http.NewRequestWithContext(ctx, method, requestURL.String(), payloadData) 1668 if err != nil { 1669 return nil, nil, errors.Wrap(err, "error creating http request") 1670 } 1671 1672 //set the request to close on completion if shared connections are not allowSharedConnection 1673 //Current CouchDB has a problem with zero length attachments, do not allow the connection to be reused. 1674 //Apache JIRA item for CouchDB https://issues.apache.org/jira/browse/COUCHDB-3394 1675 if !keepConnectionOpen { 1676 req.Close = true 1677 } 1678 1679 //add content header for PUT 1680 if method == http.MethodPut || method == http.MethodPost || method == http.MethodDelete { 1681 1682 //If the multipartBoundary is not set, then this is a JSON and content-type should be set 1683 //to application/json. Else, this is contains an attachment and needs to be multipart 1684 if multipartBoundary == "" { 1685 req.Header.Set("Content-Type", "application/json") 1686 } else { 1687 req.Header.Set("Content-Type", "multipart/related;boundary=\""+multipartBoundary+"\"") 1688 } 1689 1690 //check to see if the revision is set, if so, pass as a header 1691 if rev != "" { 1692 req.Header.Set("If-Match", rev) 1693 } 1694 } 1695 1696 //add content header for PUT 1697 if method == http.MethodPut || method == http.MethodPost { 1698 req.Header.Set("Accept", "application/json") 1699 } 1700 1701 //add content header for GET 1702 if method == http.MethodGet { 1703 req.Header.Set("Accept", "multipart/related") 1704 } 1705 1706 //If username and password are set the use basic auth 1707 if couchInstance.conf.Username != "" && couchInstance.conf.Password != "" { 1708 //req.Header.Set("Authorization", "Basic YWRtaW46YWRtaW5w") 1709 req.SetBasicAuth(couchInstance.conf.Username, couchInstance.conf.Password) 1710 } 1711 1712 //Execute http request 1713 resp, errResp = couchInstance.client.Do(req) 1714 1715 //check to see if the return from CouchDB is valid 1716 if invalidCouchDBReturn(resp, errResp) { 1717 continue 1718 } 1719 1720 //if there is no golang http error and no CouchDB 500 error, then drop out of the retry 1721 if errResp == nil && resp != nil && resp.StatusCode < 500 { 1722 // if this is an error, then populate the couchDBReturn 1723 if resp.StatusCode >= 400 { 1724 //Read the response body and close it for next attempt 1725 jsonError, err := ioutil.ReadAll(resp.Body) 1726 if err != nil { 1727 return nil, nil, errors.Wrap(err, "error reading response body") 1728 } 1729 defer closeResponseBody(resp) 1730 1731 errorBytes := []byte(jsonError) 1732 //Unmarshal the response 1733 err = json.Unmarshal(errorBytes, &couchDBReturn) 1734 if err != nil { 1735 return nil, nil, errors.Wrap(err, "error unmarshalling json data") 1736 } 1737 } 1738 1739 break 1740 } 1741 1742 // If the maxRetries is greater than 0, then log the retry info 1743 if maxRetries > 0 { 1744 1745 retryMessage := fmt.Sprintf("Retrying couchdb request in %s", waitDuration) 1746 if attempts == maxRetries { 1747 retryMessage = "Retries exhausted" 1748 } 1749 1750 //if this is an unexpected golang http error, log the error and retry 1751 if errResp != nil { 1752 1753 //Log the error with the retry count and continue 1754 couchdbLogger.Warningf("Attempt %d of %d returned error: %s. %s", attempts+1, maxRetries+1, errResp.Error(), retryMessage) 1755 1756 //otherwise this is an unexpected 500 error from CouchDB. Log the error and retry. 1757 } else { 1758 //Read the response body and close it for next attempt 1759 jsonError, err := ioutil.ReadAll(resp.Body) 1760 defer closeResponseBody(resp) 1761 if err != nil { 1762 return nil, nil, errors.Wrap(err, "error reading response body") 1763 } 1764 1765 errorBytes := []byte(jsonError) 1766 //Unmarshal the response 1767 err = json.Unmarshal(errorBytes, &couchDBReturn) 1768 if err != nil { 1769 return nil, nil, errors.Wrap(err, "error unmarshalling json data") 1770 } 1771 1772 //Log the 500 error with the retry count and continue 1773 couchdbLogger.Warningf("Attempt %d of %d returned Couch DB Error:%s, Status Code:%v Reason:%s. %s", 1774 attempts+1, maxRetries+1, couchDBReturn.Error, resp.Status, couchDBReturn.Reason, retryMessage) 1775 1776 } 1777 //if there are more retries remaining, sleep for specified sleep time, then retry 1778 if attempts < maxRetries { 1779 time.Sleep(waitDuration) 1780 } 1781 1782 //backoff, doubling the retry time for next attempt 1783 waitDuration *= 2 1784 1785 } 1786 1787 } // end retry loop 1788 1789 //if a golang http error is still present after retries are exhausted, return the error 1790 if errResp != nil { 1791 return nil, couchDBReturn, errors.Wrap(errResp, "http error calling couchdb") 1792 } 1793 1794 //This situation should not occur according to the golang spec. 1795 //if this error returned (errResp) from an http call, then the resp should be not nil, 1796 //this is a structure and StatusCode is an int 1797 //This is meant to provide a more graceful error if this should occur 1798 if invalidCouchDBReturn(resp, errResp) { 1799 return nil, nil, errors.New("unable to connect to CouchDB, check the hostname and port") 1800 } 1801 1802 //set the return code for the couchDB request 1803 couchDBReturn.StatusCode = resp.StatusCode 1804 1805 // check to see if the status code from couchdb is 400 or higher 1806 // response codes 4XX and 500 will be treated as errors - 1807 // golang error will be created from the couchDBReturn contents and both will be returned 1808 if resp.StatusCode >= 400 { 1809 1810 // if the status code is 400 or greater, log and return an error 1811 couchdbLogger.Debugf("Error handling CouchDB request. Error:%s, Status Code:%v, Reason:%s", 1812 couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason) 1813 1814 return nil, couchDBReturn, errors.Errorf("error handling CouchDB request. Error:%s, Status Code:%v, Reason:%s", 1815 couchDBReturn.Error, resp.StatusCode, couchDBReturn.Reason) 1816 1817 } 1818 1819 couchdbLogger.Debugf("Exiting handleRequest()") 1820 1821 //If no errors, then return the http response and the couchdb return object 1822 return resp, couchDBReturn, nil 1823 } 1824 1825 func (couchInstance *couchInstance) recordMetric(startTime time.Time, dbName, api string, couchDBReturn *dbReturn) { 1826 couchInstance.stats.observeProcessingTime(startTime, dbName, api, strconv.Itoa(couchDBReturn.StatusCode)) 1827 } 1828 1829 //invalidCouchDBResponse checks to make sure either a valid response or error is returned 1830 func invalidCouchDBReturn(resp *http.Response, errResp error) bool { 1831 if resp == nil && errResp == nil { 1832 return true 1833 } 1834 return false 1835 } 1836 1837 //isJSON tests a string to determine if a valid JSON 1838 func isJSON(s string) bool { 1839 var js map[string]interface{} 1840 return json.Unmarshal([]byte(s), &js) == nil 1841 } 1842 1843 // encodePathElement uses Golang for url path encoding, additionally: 1844 // '/' is replaced by %2F, otherwise path encoding will treat as path separator and ignore it 1845 // '+' is replaced by %2B, otherwise path encoding will ignore it, while CouchDB will unencode the plus as a space 1846 // Note that all other URL special characters have been tested successfully without need for special handling 1847 func encodePathElement(str string) string { 1848 1849 u := &url.URL{} 1850 u.Path = str 1851 encodedStr := u.EscapedPath() // url encode using golang url path encoding rules 1852 encodedStr = strings.Replace(encodedStr, "/", "%2F", -1) 1853 encodedStr = strings.Replace(encodedStr, "+", "%2B", -1) 1854 1855 return encodedStr 1856 } 1857 1858 func encodeForJSON(str string) (string, error) { 1859 buf := &bytes.Buffer{} 1860 encoder := json.NewEncoder(buf) 1861 if err := encoder.Encode(str); err != nil { 1862 return "", errors.Wrap(err, "error encoding json data") 1863 } 1864 // Encode adds double quotes to string and terminates with \n - stripping them as bytes as they are all ascii(0-127) 1865 buffer := buf.Bytes() 1866 return string(buffer[1 : len(buffer)-2]), nil 1867 } 1868 1869 // printDocumentIds is a convenience method to print readable log entries for arrays of pointers 1870 // to couch document IDs 1871 func printDocumentIds(documentPointers []*couchDoc) (string, error) { 1872 1873 documentIds := []string{} 1874 1875 for _, documentPointer := range documentPointers { 1876 docMetadata := &docMetadata{} 1877 err := json.Unmarshal(documentPointer.jsonValue, &docMetadata) 1878 if err != nil { 1879 return "", errors.Wrap(err, "error unmarshalling json data") 1880 } 1881 documentIds = append(documentIds, docMetadata.ID) 1882 } 1883 return strings.Join(documentIds, ","), nil 1884 }