diff --git a/core/ledger/util/couchdb/couchdb.go b/core/ledger/util/couchdb/couchdb.go index a275458fbe2..406dc794a7b 100644 --- a/core/ledger/util/couchdb/couchdb.go +++ b/core/ledger/util/couchdb/couchdb.go @@ -69,6 +69,15 @@ type DBInfo struct { InstanceStartTime string `json:"instance_start_time"` } +//ConnectionInfo is a structure for capturing the database info and version +type ConnectionInfo struct { + Couchdb string `json:"couchdb"` + Version string `json:"version"` + Vendor struct { + Name string `json:"name"` + } `json:"vendor"` +} + //RangeQueryResponse is used for processing REST range query responses from CouchDB type RangeQueryResponse struct { TotalRows int `json:"total_rows"` @@ -211,7 +220,7 @@ func (dbclient *CouchDatabase) CreateDatabaseIfNotExist() (*DBOperationResponse, connectURL.Path = dbclient.dbName //process the URL with a PUT, creates the database - resp, _, err := dbclient.handleRequest(http.MethodPut, connectURL.String(), nil, "", "") + resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPut, connectURL.String(), nil, "", "") if err != nil { return nil, err } @@ -249,7 +258,7 @@ func (dbclient *CouchDatabase) GetDatabaseInfo() (*DBInfo, *DBReturn, error) { } connectURL.Path = dbclient.dbName - resp, couchDBReturn, err := dbclient.handleRequest(http.MethodGet, connectURL.String(), nil, "", "") + resp, couchDBReturn, err := dbclient.couchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, "", "") if err != nil { return nil, couchDBReturn, err } @@ -270,6 +279,40 @@ func (dbclient *CouchDatabase) GetDatabaseInfo() (*DBInfo, *DBReturn, error) { } +//VerifyConnection method provides function to verify the connection information +func (couchInstance *CouchInstance) VerifyConnection() (*ConnectionInfo, *DBReturn, error) { + + connectURL, err := url.Parse(couchInstance.conf.URL) + if err != nil { + logger.Errorf("URL parse error: %s", err.Error()) + return nil, nil, err + } + connectURL.Path = "/" + + resp, couchDBReturn, err := couchInstance.handleRequest(http.MethodGet, connectURL.String(), nil, "", "") + if err != nil { + return nil, couchDBReturn, err + } + defer resp.Body.Close() + + dbResponse := &ConnectionInfo{} + errJSON := json.NewDecoder(resp.Body).Decode(&dbResponse) + if errJSON != nil { + return nil, nil, errJSON + } + + // trace the database info response + if logger.IsEnabledFor(logging.DEBUG) { + dbResponseJSON, err := json.Marshal(dbResponse) + if err == nil { + logger.Debugf("VerifyConnection() dbResponseJSON: %s", dbResponseJSON) + } + } + + return dbResponse, couchDBReturn, nil + +} + //DropDatabase provides method to drop an existing database func (dbclient *CouchDatabase) DropDatabase() (*DBOperationResponse, error) { @@ -282,7 +325,7 @@ func (dbclient *CouchDatabase) DropDatabase() (*DBOperationResponse, error) { } connectURL.Path = dbclient.dbName - resp, _, err := dbclient.handleRequest(http.MethodDelete, connectURL.String(), nil, "", "") + resp, _, err := dbclient.couchInstance.handleRequest(http.MethodDelete, connectURL.String(), nil, "", "") if err != nil { return nil, err } @@ -319,7 +362,7 @@ func (dbclient *CouchDatabase) EnsureFullCommit() (*DBOperationResponse, error) } connectURL.Path = dbclient.dbName + "/_ensure_full_commit" - resp, _, err := dbclient.handleRequest(http.MethodPost, connectURL.String(), nil, "", "") + resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPost, connectURL.String(), nil, "", "") if err != nil { logger.Errorf("Failed to invoke _ensure_full_commit Error: %s\n", err.Error()) return nil, err @@ -413,7 +456,7 @@ func (dbclient *CouchDatabase) SaveDoc(id string, rev string, couchDoc *CouchDoc } //handle the request for saving the JSON or attachments - resp, _, err := dbclient.handleRequest(http.MethodPut, saveURL.String(), data, rev, defaultBoundary) + resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPut, saveURL.String(), data, rev, defaultBoundary) if err != nil { return "", err } @@ -536,7 +579,7 @@ func (dbclient *CouchDatabase) ReadDoc(id string) (*CouchDoc, string, error) { readURL.RawQuery = query.Encode() - resp, couchDBReturn, err := dbclient.handleRequest(http.MethodGet, readURL.String(), nil, "", "") + resp, couchDBReturn, err := dbclient.couchInstance.handleRequest(http.MethodGet, readURL.String(), nil, "", "") if err != nil { fmt.Printf("couchDBReturn=%v", couchDBReturn) if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { @@ -686,7 +729,7 @@ func (dbclient *CouchDatabase) ReadDocRange(startKey, endKey string, limit, skip rangeURL.RawQuery = queryParms.Encode() - resp, _, err := dbclient.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "") + resp, _, err := dbclient.couchInstance.handleRequest(http.MethodGet, rangeURL.String(), nil, "", "") if err != nil { return nil, err } @@ -781,7 +824,7 @@ func (dbclient *CouchDatabase) DeleteDoc(id, rev string) error { logger.Debugf(" rev=%s", rev) - resp, couchDBReturn, err := dbclient.handleRequest(http.MethodDelete, deleteURL.String(), nil, rev, "") + resp, couchDBReturn, err := dbclient.couchInstance.handleRequest(http.MethodDelete, deleteURL.String(), nil, rev, "") if err != nil { fmt.Printf("couchDBReturn=%v", couchDBReturn) if couchDBReturn != nil && couchDBReturn.StatusCode == 404 { @@ -826,7 +869,7 @@ func (dbclient *CouchDatabase) QueryDocuments(query string, limit, skip int) (*[ data.ReadFrom(bytes.NewReader([]byte(query))) - resp, _, err := dbclient.handleRequest(http.MethodPost, queryURL.String(), data, "", "") + resp, _, err := dbclient.couchInstance.handleRequest(http.MethodPost, queryURL.String(), data, "", "") if err != nil { return nil, err } @@ -887,7 +930,7 @@ func (dbclient *CouchDatabase) QueryDocuments(query string, limit, skip int) (*[ } //handleRequest method is a generic http request handler -func (dbclient *CouchDatabase) handleRequest(method, connectURL string, data io.Reader, rev string, multipartBoundary string) (*http.Response, *DBReturn, error) { +func (couchInstance *CouchInstance) handleRequest(method, connectURL string, data io.Reader, rev string, multipartBoundary string) (*http.Response, *DBReturn, error) { logger.Debugf("Entering handleRequest() method=%s url=%v", method, connectURL) @@ -925,8 +968,8 @@ func (dbclient *CouchDatabase) handleRequest(method, connectURL string, data io. } //If username and password are set the use basic auth - if dbclient.couchInstance.conf.Username != "" && dbclient.couchInstance.conf.Password != "" { - req.SetBasicAuth(dbclient.couchInstance.conf.Username, dbclient.couchInstance.conf.Password) + if couchInstance.conf.Username != "" && couchInstance.conf.Password != "" { + req.SetBasicAuth(couchInstance.conf.Username, couchInstance.conf.Password) } if logger.IsEnabledFor(logging.DEBUG) { diff --git a/core/ledger/util/couchdb/couchdb_test.go b/core/ledger/util/couchdb/couchdb_test.go index 5e7adb7fc4f..4cc5d73758c 100644 --- a/core/ledger/util/couchdb/couchdb_test.go +++ b/core/ledger/util/couchdb/couchdb_test.go @@ -153,22 +153,8 @@ func TestDBBadConnection(t *testing.T) { if ledgerconfig.IsCouchDBEnabled() == true { //create a new instance and database object - couchInstance, err := CreateCouchInstance(badConnectURL, username, password) - testutil.AssertNoError(t, err, fmt.Sprintf("Error when trying to create couch instance")) - db := CouchDatabase{couchInstance: *couchInstance, dbName: database} - - //create a new database - _, errdb := db.CreateDatabaseIfNotExist() - testutil.AssertError(t, errdb, fmt.Sprintf("Error should have been thrown while creating a database with an invalid connecion")) - - //Save the test document - _, saveerr := db.SaveDoc("3", "", &CouchDoc{JSONValue: assetJSON, Attachments: nil}) - testutil.AssertError(t, saveerr, fmt.Sprintf("Error should have been thrown while saving a document with an invalid connecion")) - - //Retrieve the updated test document - _, _, geterr := db.ReadDoc("3") - testutil.AssertError(t, geterr, fmt.Sprintf("Error should have been thrown while retrieving a document with an invalid connecion")) - + _, err := CreateCouchInstance(badConnectURL, username, password) + testutil.AssertError(t, err, fmt.Sprintf("Error should have been thrown for a bad connection")) } } @@ -461,3 +447,19 @@ func TestDBDeleteNonExistingDocument(t *testing.T) { } } } + +func TestCouchDBVersion(t *testing.T) { + + err := checkCouchDBVersion("2.0.0") + testutil.AssertNoError(t, err, fmt.Sprintf("Error should not have been thrown for valid version")) + + err = checkCouchDBVersion("4.5.0") + testutil.AssertNoError(t, err, fmt.Sprintf("Error should not have been thrown for valid version")) + + err = checkCouchDBVersion("1.6.5.4") + testutil.AssertError(t, err, fmt.Sprintf("Error should have been thrown for invalid version")) + + err = checkCouchDBVersion("0.0.0.0") + testutil.AssertError(t, err, fmt.Sprintf("Error should have been thrown for invalid version")) + +} diff --git a/core/ledger/util/couchdb/couchdbutil.go b/core/ledger/util/couchdb/couchdbutil.go index 925268720ad..57d830e22e7 100644 --- a/core/ledger/util/couchdb/couchdbutil.go +++ b/core/ledger/util/couchdb/couchdbutil.go @@ -19,6 +19,7 @@ package couchdb import ( "fmt" "regexp" + "strconv" "strings" ) @@ -35,7 +36,41 @@ func CreateCouchInstance(couchDBConnectURL string, id string, pw string) (*Couch return nil, err } - return &CouchInstance{conf: *couchConf}, nil + //Create the CouchDB instance + couchInstance := &CouchInstance{conf: *couchConf} + + connectInfo, retVal, verifyErr := couchInstance.VerifyConnection() + if verifyErr != nil { + return nil, fmt.Errorf("Unable to connect to CouchDB, check the hostname and port: %s", verifyErr.Error()) + } + + //return an error if the http return value is not 200 + if retVal.StatusCode != 200 { + return nil, fmt.Errorf("CouchDB connection error, expecting return code of 200, received %v", retVal.StatusCode) + } + + //check the CouchDB version number, return an error if the version is not at least 2.0.0 + errVersion := checkCouchDBVersion(connectInfo.Version) + if errVersion != nil { + return nil, errVersion + } + + return couchInstance, nil +} + +//checkCouchDBVersion verifies CouchDB is at least 2.0.0 +func checkCouchDBVersion(version string) error { + + //split the version into parts + majorVersion := strings.Split(version, ".") + + //check to see that the major version number is at least 2 + majorVersionInt, _ := strconv.Atoi(majorVersion[0]) + if majorVersionInt < 2 { + return fmt.Errorf("CouchDB must be at least version 2.0.0. Detected version %s", version) + } + + return nil } //CreateCouchDatabase creates a CouchDB database object, as well as the underlying database if it does not exist