Skip to content

Commit

Permalink
FAB-1925 Verify CouchDB connection upon peer startup
Browse files Browse the repository at this point in the history
Motivation for this change:
Upon peer startup, if CouchDB is configured for state database,
need to verify connection to CouchDB. If can't connect to CouchDB
provide a good error and halt peer startup.

Currently, it fails upon the first chain creation step:
Creating KVLedger ledgerID=myc1.
Should check connection upon KVLedger provider creation.

- Add connection verification to couchdb

Change-Id: I3a3dc91463f230af1ee08b55ae595ece049eb4eb
Signed-off-by: Chris Elder <chris.elder@us.ibm.com>
  • Loading branch information
Chris Elder committed Feb 26, 2017
1 parent 22ede47 commit 148438e
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 29 deletions.
67 changes: 55 additions & 12 deletions core/ledger/util/couchdb/couchdb.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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
}
Expand All @@ -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) {

Expand All @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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 {
Expand Down Expand Up @@ -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
}
Expand Down Expand Up @@ -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)

Expand Down Expand Up @@ -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) {
Expand Down
34 changes: 18 additions & 16 deletions core/ledger/util/couchdb/couchdb_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"))
}
}

Expand Down Expand Up @@ -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"))

}
37 changes: 36 additions & 1 deletion core/ledger/util/couchdb/couchdbutil.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package couchdb
import (
"fmt"
"regexp"
"strconv"
"strings"
)

Expand All @@ -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
Expand Down

0 comments on commit 148438e

Please sign in to comment.