Merge pull request #437 from smashwilson/clb-creation-err

Rackspace CLB ExtractNodes call does not propagate errors correctly
diff --git a/pagination/http.go b/pagination/http.go
index cabcccd..1b3fe94 100644
--- a/pagination/http.go
+++ b/pagination/http.go
@@ -36,13 +36,19 @@
 		parsedBody = rawBody
 	}
 
+	return PageResultFromParsed(resp, parsedBody), err
+}
+
+// PageResultFromParsed constructs a PageResult from an HTTP response that has already had its
+// body parsed as JSON (and closed).
+func PageResultFromParsed(resp *http.Response, body interface{}) PageResult {
 	return PageResult{
 		Result: gophercloud.Result{
-			Body:   parsedBody,
+			Body:   body,
 			Header: resp.Header,
 		},
 		URL: *resp.Request.URL,
-	}, err
+	}
 }
 
 // Request performs an HTTP request and extracts the http.Response from the result.
diff --git a/rackspace/lb/v1/nodes/fixtures.go b/rackspace/lb/v1/nodes/fixtures.go
index 7c85945..8899fc5 100644
--- a/rackspace/lb/v1/nodes/fixtures.go
+++ b/rackspace/lb/v1/nodes/fixtures.go
@@ -107,6 +107,42 @@
 	})
 }
 
+func mockCreateErrResponse(t *testing.T, lbID int) {
+	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
+		th.TestMethod(t, r, "POST")
+		th.TestHeader(t, r, "X-Auth-Token", fake.TokenID)
+
+		th.TestJSONRequest(t, r, `
+{
+  "nodes": [
+    {
+      "address": "10.2.2.3",
+      "port": 80,
+      "condition": "ENABLED",
+      "type": "PRIMARY"
+    },
+    {
+      "address": "10.2.2.4",
+      "port": 81,
+      "condition": "ENABLED",
+      "type": "SECONDARY"
+    }
+  ]
+}
+    `)
+
+		w.Header().Add("Content-Type", "application/json")
+		w.WriteHeader(422) // Unprocessable Entity
+
+		fmt.Fprintf(w, `
+{
+  "code": 422,
+  "message": "Load Balancer '%d' has a status of 'PENDING_UPDATE' and is considered immutable."
+}
+  `, lbID)
+	})
+}
+
 func mockBatchDeleteResponse(t *testing.T, lbID int, ids []int) {
 	th.Mux.HandleFunc(_rootURL(lbID), func(w http.ResponseWriter, r *http.Request) {
 		th.TestMethod(t, r, "DELETE")
diff --git a/rackspace/lb/v1/nodes/requests.go b/rackspace/lb/v1/nodes/requests.go
index 02af86b..dc2d46c 100644
--- a/rackspace/lb/v1/nodes/requests.go
+++ b/rackspace/lb/v1/nodes/requests.go
@@ -119,12 +119,7 @@
 		return res
 	}
 
-	pr, err := pagination.PageResultFrom(resp)
-	if err != nil {
-		res.Err = err
-		return res
-	}
-
+	pr := pagination.PageResultFromParsed(resp, res.Body)
 	return CreateResult{pagination.SinglePageBase(pr)}
 }
 
diff --git a/rackspace/lb/v1/nodes/requests_test.go b/rackspace/lb/v1/nodes/requests_test.go
index 003d347..a964af8 100644
--- a/rackspace/lb/v1/nodes/requests_test.go
+++ b/rackspace/lb/v1/nodes/requests_test.go
@@ -108,6 +108,38 @@
 	th.CheckDeepEquals(t, expected, actual)
 }
 
+func TestCreateErr(t *testing.T) {
+	th.SetupHTTP()
+	defer th.TeardownHTTP()
+
+	mockCreateErrResponse(t, lbID)
+
+	opts := CreateOpts{
+		CreateOpt{
+			Address:   "10.2.2.3",
+			Port:      80,
+			Condition: ENABLED,
+			Type:      PRIMARY,
+		},
+		CreateOpt{
+			Address:   "10.2.2.4",
+			Port:      81,
+			Condition: ENABLED,
+			Type:      SECONDARY,
+		},
+	}
+
+	page := Create(client.ServiceClient(), lbID, opts)
+
+	actual, err := page.ExtractNodes()
+	if err == nil {
+		t.Fatal("Did not receive expected error from ExtractNodes")
+	}
+	if actual != nil {
+		t.Fatalf("Received non-nil result from failed ExtractNodes: %#v", actual)
+	}
+}
+
 func TestBulkDelete(t *testing.T) {
 	th.SetupHTTP()
 	defer th.TeardownHTTP()
diff --git a/rackspace/lb/v1/nodes/results.go b/rackspace/lb/v1/nodes/results.go
index 916485f..57835dc 100644
--- a/rackspace/lb/v1/nodes/results.go
+++ b/rackspace/lb/v1/nodes/results.go
@@ -126,6 +126,9 @@
 
 // ExtractNodes extracts a slice of Node structs from a CreateResult.
 func (res CreateResult) ExtractNodes() ([]Node, error) {
+	if res.Err != nil {
+		return nil, res.Err
+	}
 	return commonExtractNodes(res.Body)
 }