Merge lp:~allenap/gwacl/destroy-hosted-service into lp:gwacl

Proposed by Gavin Panella
Status: Merged
Approved by: Raphaël Badin
Approved revision: 175
Merged at revision: 164
Proposed branch: lp:~allenap/gwacl/destroy-hosted-service
Merge into: lp:gwacl
Diff against target: 394 lines (+252/-33)
4 files modified
example/management/run.go (+4/-10)
management.go (+34/-0)
management_base_test.go (+24/-9)
management_test.go (+190/-14)
To merge this branch: bzr merge lp:~allenap/gwacl/destroy-hosted-service
Reviewer Review Type Date Requested Status
Raphaël Badin (community) Approve
Review via email: mp+173057@code.launchpad.net

Commit message

New method to destroy a hosted service.

To post a comment you must log in.
Revision history for this message
Raphaël Badin (rvb) wrote :

Please wait a bit to review and land this… we've got a problem with the underlying DestroyDeployment() method (I've tested it using example/management/run.go and it does not work [blows up trying to stop the VMs]… I thought the only problem is that we should ignore the error we get when stopping the VMs but it turns out it's a bit more involved).

Revision history for this message
Raphaël Badin (rvb) wrote :

I think we should fix DestroyDeployment by doing this first: https://code.launchpad.net/~rvb/gwacl/fix-destroydeploy2/+merge/173095

Since the deployment used in the tests does not contain VMs, this branch will probably work as is, even with that change.

Revision history for this message
Raphaël Badin (rvb) wrote :

Confirmed, this can be reviewed (and landed) independently from my "fix-destroydeploy2" branch.

Revision history for this message
Raphaël Badin (rvb) wrote :

Looks good but please consider my suggestions below:

[0]

We've had trouble lately with using these DestroyDeployment() so I'd advice to do this: now that my "fix-destroydeploy2" branch has landed, merge trunk and then update the example/management/run.go file to call DestroyHostedService() (instead of DestroyDeployment() and the defered which cleans up the hosted service) from there and make sure this works fine when talking to the Azure server.

[1]

126 +var exampleHostedService = &HostedService{
127 + Deployments: []Deployment{
128 + {Name: "one"}, {Name: "two"},
129 + },
130 +}

Having that as a global variable is, I think, an anti-pattern. This saves a bit of duplicate to have that as a global variable and not repeat it in each and every test. But a side effect is that the tests are not "self-contained" anymore and it makes it them much more difficult to understand and reason with.

For instance, the reasoning behind these checks:
159 + assertGetDeploymentRequest(c, api, &GetDeploymentRequest{
160 + request.ServiceName, "one"}, record[1])
161 + assertDeleteDeploymentRequest(c, api, request.ServiceName, "one",
162 + record[2])
163 + // The fourth and fifth requests are a repaat of the previous two, but for
164 + // deployment "two".
165 + assertGetDeploymentRequest(c, api, &GetDeploymentRequest{
166 + request.ServiceName, "two"}, record[3])
167 + assertDeleteDeploymentRequest(c, api, request.ServiceName, "two",
168 + record[4])

is inherently linked to the structure of the tested object… and one has to navigate in the file to find the exampleHostedService's definition to understand why these assertions are meaningful.

I'd suggest:
- creating a tiny makeHostedService method that would take a slice of Deployment objects and return a *HostedService
- calling that method from every test so that the initialization of the tested object is not far from the checks.

Even better, maybe use variables instead of duplicating the strings/objects on which the checks are performed:
deployName1 := "one"
deployName2 := "two"
exampleHostedService = makeHostedService([]Deployment{{Name: deployName1}, {Name: deployName2}})

then the checks can be along the lines of:
assertDeleteDeploymentRequest(c, api, request.ServiceName, deployName2, record[4])

review: Approve
Revision history for this message
Gavin Panella (allenap) wrote :

Thanks for the review.

> [0]
>
> We've had trouble lately with using these DestroyDeployment() so I'd advice to
> do this: now that my "fix-destroydeploy2"  branch has landed, merge trunk and
> then update the example/management/run.go file to call DestroyHostedService()
> (instead of DestroyDeployment() and the defered which cleans up the hosted
> service) from there and make sure this works fine when talking to the Azure
> server.

Done.

>
> [1]
>
> 126     +var exampleHostedService = &HostedService{
> 127     +    Deployments: []Deployment{
> 128     +        {Name: "one"}, {Name: "two"},
> 129     +    },
> 130     +}
>
> Having that as a global variable is, I think, an anti-pattern.  This saves a
> bit of duplicate to have that as a global variable and not repeat it in each
> and every test.  But a side effect is that the tests are not "self-contained"
> anymore and it makes it them much more difficult to understand and reason
> with.
...

I've changed some of these example bits to be functions instead, and
I'm not sure it makes it easier to reason about. I've noticed with
some of the test refactoring going on - extracting things into
functions and whatnot - that:

- The loss of locality of what I'm looking at is more harmful than
  having a few extra lines of code in my test.

- The indirection of having all test data constructed at runtime from
  parameters is more harmful than having static test data.

- Using strings like "disk1" is easier to read and grok than either
  deployments[0].RoleList[1].OSVirtualHardDisk[0].DiskName, or storing
  the names of everything in lots of local variables.

- Using functions for creating things in Go is painful because of the
  very limited calling conventions available. We are going to end up
  with 100s of factory functions for creating objects when it would
  simpler and more expressive to create them near the point of use.

Even so, I'll land the few changes that I did make.

Revision history for this message
Julian Edwards (julian-edwards) wrote :

There are additional revisions which have not been approved in review. Please seek review and approval of these new revisions.

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'example/management/run.go'
2--- example/management/run.go 2013-07-04 17:25:26 +0000
3+++ example/management/run.go 2013-07-05 10:51:26 +0000
4@@ -108,9 +108,10 @@
5 fmt.Println("Done creating a hosted service\n")
6
7 defer func() {
8- fmt.Println("Deleting hosted service...")
9- api.DeleteHostedService(hostServiceName)
10- fmt.Println("Done deleting hosted service\n")
11+ fmt.Println("Destroying hosted service...")
12+ api.DestroyHostedService(&gwacl.DestroyHostedServiceRequest{
13+ ServiceName: hostServiceName})
14+ fmt.Println("Done destroying hosted service\n")
15 }()
16
17 fmt.Println("Listing hosted services...")
18@@ -171,13 +172,6 @@
19 checkError(err)
20 fmt.Println("Done adding VM deployment\n")
21
22- defer func() {
23- fmt.Printf("Destroying deployment %s...\n", machineName)
24- requestDestroyDeployment := &gwacl.DestroyDeploymentRequest{ServiceName: hostServiceName, DeploymentName: machineName}
25- api.DestroyDeployment(requestDestroyDeployment)
26- fmt.Println("Done destroying deployment\n")
27- }()
28-
29 fmt.Println("Starting VM...")
30 err = api.StartRole(&gwacl.StartRoleRequest{hostServiceName, deployment.Name, role.RoleName})
31 checkError(err)
32
33=== modified file 'management.go'
34--- management.go 2013-07-04 20:23:56 +0000
35+++ management.go 2013-07-05 10:51:26 +0000
36@@ -143,3 +143,37 @@
37 // Done.
38 return nil
39 }
40+
41+type DestroyHostedServiceRequest struct {
42+ ServiceName string
43+}
44+
45+// DestroyHostedService destroys all of the hosted service's contained
46+// deployments then deletes the hosted service itself.
47+func (api *ManagementAPI) DestroyHostedService(request *DestroyHostedServiceRequest) error {
48+ // 1. Get properties.
49+ properties, err := api.GetHostedServiceProperties(request.ServiceName, true)
50+ if err != nil {
51+ if IsNotFoundError(err) {
52+ return nil
53+ }
54+ return err
55+ }
56+ // 2. Delete deployments.
57+ for _, deployment := range properties.Deployments {
58+ err := api.DestroyDeployment(&DestroyDeploymentRequest{
59+ ServiceName: request.ServiceName,
60+ DeploymentName: deployment.Name,
61+ })
62+ if err != nil {
63+ return err
64+ }
65+ }
66+ // 3. Delete service.
67+ err = api.DeleteHostedService(request.ServiceName)
68+ if err != nil && !IsNotFoundError(err) {
69+ return err
70+ }
71+ // Done.
72+ return nil
73+}
74
75=== modified file 'management_base_test.go'
76--- management_base_test.go 2013-07-04 07:11:22 +0000
77+++ management_base_test.go 2013-07-05 10:51:26 +0000
78@@ -399,6 +399,18 @@
79 checkOneRequest(c, &recordedRequests, expectedURL, requestPayload, "PUT")
80 }
81
82+func assertGetHostedServicePropertiesRequest(c *C, api *ManagementAPI, serviceName string, embedDetail bool, httpRequest *X509Request) {
83+ var query string
84+ if embedDetail {
85+ query = "embed-detail=true"
86+ } else {
87+ query = "embed-detail=false"
88+ }
89+ expectedURL := fmt.Sprintf("%s%s/services/hostedservices/%s?%s", AZURE_URL,
90+ api.session.subscriptionId, serviceName, query)
91+ checkRequest(c, httpRequest, expectedURL, nil, "GET")
92+}
93+
94 func (suite *managementBaseAPISuite) TestGetHostedServiceProperties_withoutDetails(c *C) {
95 api := makeAPI(c)
96 body := `
97@@ -435,9 +447,8 @@
98
99 properties, err := api.GetHostedServiceProperties(serviceName, false)
100 c.Assert(err, IsNil)
101-
102- expectedURL := AZURE_URL + api.session.subscriptionId + "/services/hostedservices/" + serviceName + "?embed-detail=false"
103- checkOneRequest(c, &recordedRequests, expectedURL, []byte{}, "GET")
104+ c.Check(recordedRequests, HasLen, 1)
105+ assertGetHostedServicePropertiesRequest(c, api, serviceName, false, recordedRequests[0])
106
107 c.Check(properties.URL, Equals, "hosted-service-url")
108 c.Check(properties.ServiceName, Equals, "hosted-service-name")
109@@ -487,9 +498,8 @@
110
111 properties, err := api.GetHostedServiceProperties(serviceName, true)
112 c.Assert(err, IsNil)
113-
114- expectedURL := AZURE_URL + api.session.subscriptionId + "/services/hostedservices/" + serviceName + "?embed-detail=true"
115- checkOneRequest(c, &recordedRequests, expectedURL, []byte{}, "GET")
116+ c.Check(recordedRequests, HasLen, 1)
117+ assertGetHostedServicePropertiesRequest(c, api, serviceName, true, recordedRequests[0])
118
119 c.Check(properties.URL, Equals, "hosted-service-url")
120 c.Check(properties.ServiceName, Equals, "hosted-service-name")
121@@ -510,15 +520,20 @@
122 checkOneRequest(c, recordedRequests, expectedURL, expectedPayload, "POST")
123 }
124
125+func assertDeleteHostedServiceRequest(c *C, api *ManagementAPI, serviceName string, httpRequest *X509Request) {
126+ expectedURL := fmt.Sprintf("%s%s/services/hostedservices/%s", AZURE_URL,
127+ api.session.subscriptionId, serviceName)
128+ checkRequest(c, httpRequest, expectedURL, nil, "DELETE")
129+}
130+
131 func (suite *managementBaseAPISuite) TestDeleteHostedService(c *C) {
132 api := makeAPI(c)
133 recordedRequests := setUpDispatcher("operationID")
134 hostedServiceName := "testName"
135 err := api.DeleteHostedService(hostedServiceName)
136-
137 c.Assert(err, IsNil)
138- expectedURL := AZURE_URL + api.session.subscriptionId + "/services/hostedservices/" + hostedServiceName
139- checkOneRequest(c, recordedRequests, expectedURL, []byte{}, "DELETE")
140+ c.Check(*recordedRequests, HasLen, 1)
141+ assertDeleteHostedServiceRequest(c, api, hostedServiceName, (*recordedRequests)[0])
142 }
143
144 func (suite *managementBaseAPISuite) TestAddDeployment(c *C) {
145
146=== modified file 'management_test.go'
147--- management_test.go 2013-07-04 20:23:56 +0000
148+++ management_test.go 2013-07-05 10:51:26 +0000
149@@ -220,20 +220,6 @@
150 c.Assert(recordedRequests, HasLen, 1)
151 }
152
153-type suiteDestroyDeployment struct{}
154-
155-var _ = Suite(&suiteDestroyDeployment{})
156-
157-var exampleDeployment = &Deployment{
158- RoleInstanceList: makeNamedRoleInstances("one", "two"),
159- RoleList: []Role{
160- {OSVirtualHardDisk: []OSVirtualHardDisk{
161- {DiskName: "disk1"}, {DiskName: "disk2"}}},
162- {OSVirtualHardDisk: []OSVirtualHardDisk{
163- {DiskName: "disk1"}, {DiskName: "disk3"}}},
164- },
165-}
166-
167 var exampleOkayResponse = DispatcherResponse{
168 response: &x509Response{StatusCode: http.StatusOK},
169 }
170@@ -246,9 +232,26 @@
171 response: &x509Response{StatusCode: http.StatusNotFound},
172 }
173
174+type suiteDestroyDeployment struct{}
175+
176+var _ = Suite(&suiteDestroyDeployment{})
177+
178+func (suite *suiteDestroyDeployment) makeExampleDeployment() *Deployment {
179+ return &Deployment{
180+ RoleInstanceList: makeNamedRoleInstances("one", "two"),
181+ RoleList: []Role{
182+ {OSVirtualHardDisk: []OSVirtualHardDisk{
183+ {DiskName: "disk1"}, {DiskName: "disk2"}}},
184+ {OSVirtualHardDisk: []OSVirtualHardDisk{
185+ {DiskName: "disk1"}, {DiskName: "disk3"}}},
186+ },
187+ }
188+}
189+
190 func (suite *suiteDestroyDeployment) TestHappyPath(c *C) {
191 var responses []DispatcherResponse
192 // Prepare.
193+ exampleDeployment := suite.makeExampleDeployment()
194 responses = append(responses, makeOKXMLResponse(c, exampleDeployment)...)
195 // For deleting the deployment.
196 responses = append(responses, exampleOkayResponse)
197@@ -294,6 +297,7 @@
198 func (suite *suiteDestroyDeployment) TestOkayWhenAssetsNotFound(c *C) {
199 var responses []DispatcherResponse
200 // Prepare.
201+ exampleDeployment := suite.makeExampleDeployment()
202 responses = append(responses, makeOKXMLResponse(c, exampleDeployment)...)
203 // For deleting the deployment.
204 responses = append(responses, exampleNotFoundResponse)
205@@ -340,6 +344,7 @@
206 func (suite *suiteDestroyDeployment) TestFailsDeletingDisk(c *C) {
207 var responses []DispatcherResponse
208 // Prepare.
209+ exampleDeployment := suite.makeExampleDeployment()
210 responses = append(responses, makeOKXMLResponse(c, exampleDeployment)...)
211 // For deleting disks.
212 responses = append(responses, exampleFailResponse)
213@@ -360,6 +365,7 @@
214 func (suite *suiteDestroyDeployment) TestFailsDeletingDeployment(c *C) {
215 var responses []DispatcherResponse
216 // Prepare.
217+ exampleDeployment := suite.makeExampleDeployment()
218 responses = append(responses, makeOKXMLResponse(c, exampleDeployment)...)
219 // For deleting disks.
220 responses = append(responses, exampleOkayResponse, exampleOkayResponse, exampleOkayResponse)
221@@ -378,3 +384,173 @@
222 c.Check(err, ErrorMatches, "DELETE request failed [(]500: Internal Server Error[)]")
223 c.Check(record, HasLen, 5)
224 }
225+
226+type suiteDestroyHostedService struct{}
227+
228+var _ = Suite(&suiteDestroyHostedService{})
229+
230+func (suite *suiteDestroyHostedService) makeExampleHostedService(deploymentNames ...string) *HostedService {
231+ var exampleHostedService = &HostedService{}
232+ for _, deploymentName := range deploymentNames {
233+ exampleHostedService.Deployments = append(
234+ exampleHostedService.Deployments,
235+ Deployment{Name: deploymentName})
236+ }
237+ return exampleHostedService
238+}
239+
240+func (suite *suiteDestroyHostedService) TestHappyPath(c *C) {
241+ var responses []DispatcherResponse
242+ // DestroyHostedService first gets the hosted service proerties.
243+ exampleHostedService := suite.makeExampleHostedService("one", "two")
244+ responses = append(responses, makeOKXMLResponse(c, exampleHostedService)...)
245+ // It calls DestroyDeployment, which first gets the deployment's
246+ // properties, deletes any assets contained therein (none in this case)
247+ // then deletes the deployment.
248+ for _, deployment := range exampleHostedService.Deployments {
249+ responses = append(responses, makeOKXMLResponse(c, &deployment)...)
250+ responses = append(responses, exampleOkayResponse)
251+ }
252+ // For deleting the service itself.
253+ responses = append(responses, exampleOkayResponse)
254+ record := []*X509Request{}
255+ rigRecordingPreparedResponseDispatcher(&record, responses)
256+ // Exercise DestroyHostedService.
257+ api := makeAPI(c)
258+ request := &DestroyHostedServiceRequest{
259+ ServiceName: "service-name",
260+ }
261+ err := api.DestroyHostedService(request)
262+ c.Assert(err, IsNil)
263+ c.Check(record, HasLen, 6)
264+ // The first request is for the hosted service.
265+ assertGetHostedServicePropertiesRequest(c, api, request.ServiceName, true, record[0])
266+ // The second and third requests fetch then delete deployment "one"; see
267+ // DestroyDeployment for an explanation of this behaviour.
268+ assertGetDeploymentRequest(c, api, &GetDeploymentRequest{
269+ request.ServiceName, "one"}, record[1])
270+ assertDeleteDeploymentRequest(c, api, request.ServiceName, "one",
271+ record[2])
272+ // The fourth and fifth requests are a repaat of the previous two, but for
273+ // deployment "two".
274+ assertGetDeploymentRequest(c, api, &GetDeploymentRequest{
275+ request.ServiceName, "two"}, record[3])
276+ assertDeleteDeploymentRequest(c, api, request.ServiceName, "two",
277+ record[4])
278+ // The last request deletes the hosted service.
279+ assertDeleteHostedServiceRequest(c, api, request.ServiceName, record[5])
280+}
281+
282+func (suite *suiteDestroyHostedService) TestOkayWhenHostedServiceNotFound(c *C) {
283+ var responses []DispatcherResponse
284+ // Prepare.
285+ responses = []DispatcherResponse{exampleNotFoundResponse}
286+ record := []*X509Request{}
287+ rigRecordingPreparedResponseDispatcher(&record, responses)
288+ // Exercise DestroyHostedService.
289+ api := makeAPI(c)
290+ request := &DestroyHostedServiceRequest{ServiceName: "service-name"}
291+ err := api.DestroyHostedService(request)
292+ c.Assert(err, IsNil)
293+ c.Check(record, HasLen, 1)
294+}
295+
296+func (suite *suiteDestroyHostedService) TestOkayWhenDeploymentsNotFound(c *C) {
297+ var responses []DispatcherResponse
298+ // Prepare.
299+ exampleHostedService := suite.makeExampleHostedService("one", "two")
300+ responses = append(responses, makeOKXMLResponse(c, exampleHostedService)...)
301+ // Someone else has destroyed the deployments in the meantime.
302+ responses = append(responses, exampleNotFoundResponse, exampleNotFoundResponse)
303+ // Success deleting the hosted service.
304+ responses = append(responses, exampleOkayResponse)
305+ record := []*X509Request{}
306+ rigRecordingPreparedResponseDispatcher(&record, responses)
307+ // Exercise DestroyHostedService.
308+ api := makeAPI(c)
309+ request := &DestroyHostedServiceRequest{ServiceName: "service-name"}
310+ err := api.DestroyHostedService(request)
311+ c.Assert(err, IsNil)
312+ c.Check(record, HasLen, 4)
313+ assertGetDeploymentRequest(c, api, &GetDeploymentRequest{
314+ request.ServiceName, "one"}, record[1])
315+ assertGetDeploymentRequest(c, api, &GetDeploymentRequest{
316+ request.ServiceName, "two"}, record[2])
317+ assertDeleteHostedServiceRequest(c, api, request.ServiceName, record[3])
318+}
319+
320+func (suite *suiteDestroyHostedService) TestOkayWhenHostedServiceNotFoundWhenDeleting(c *C) {
321+ var responses []DispatcherResponse
322+ // Prepare.
323+ exampleHostedService := suite.makeExampleHostedService("one", "two")
324+ responses = append(responses, makeOKXMLResponse(c, exampleHostedService)...)
325+ // Someone else has destroyed the deployments in the meantime.
326+ responses = append(responses, exampleNotFoundResponse, exampleNotFoundResponse)
327+ // Someone else has destroyed the hosted service in the meantime.
328+ responses = append(responses, exampleNotFoundResponse)
329+ record := []*X509Request{}
330+ rigRecordingPreparedResponseDispatcher(&record, responses)
331+ // Exercise DestroyHostedService.
332+ api := makeAPI(c)
333+ request := &DestroyHostedServiceRequest{ServiceName: "service-name"}
334+ err := api.DestroyHostedService(request)
335+ c.Assert(err, IsNil)
336+ c.Check(record, HasLen, 4)
337+ assertGetDeploymentRequest(c, api, &GetDeploymentRequest{
338+ request.ServiceName, "one"}, record[1])
339+ assertGetDeploymentRequest(c, api, &GetDeploymentRequest{
340+ request.ServiceName, "two"}, record[2])
341+ assertDeleteHostedServiceRequest(c, api, request.ServiceName, record[3])
342+}
343+
344+func (suite *suiteDestroyHostedService) TestFailsGettingHostedService(c *C) {
345+ var responses []DispatcherResponse
346+ // Prepare.
347+ responses = []DispatcherResponse{exampleFailResponse}
348+ record := []*X509Request{}
349+ rigRecordingPreparedResponseDispatcher(&record, responses)
350+ // Exercise DestroyHostedService.
351+ api := makeAPI(c)
352+ request := &DestroyHostedServiceRequest{ServiceName: "service-name"}
353+ err := api.DestroyHostedService(request)
354+ c.Assert(err, NotNil)
355+ c.Check(err, ErrorMatches, "GET request failed [(]500: Internal Server Error[)]")
356+ c.Check(record, HasLen, 1)
357+}
358+
359+func (suite *suiteDestroyHostedService) TestFailsDestroyingDeployment(c *C) {
360+ var responses []DispatcherResponse
361+ // Prepare.
362+ exampleHostedService := suite.makeExampleHostedService("one", "two")
363+ responses = append(responses, makeOKXMLResponse(c, exampleHostedService)...)
364+ responses = append(responses, exampleFailResponse)
365+ record := []*X509Request{}
366+ rigRecordingPreparedResponseDispatcher(&record, responses)
367+ // Exercise DestroyHostedService.
368+ api := makeAPI(c)
369+ request := &DestroyHostedServiceRequest{ServiceName: "service-name"}
370+ err := api.DestroyHostedService(request)
371+ c.Assert(err, NotNil)
372+ c.Check(err, ErrorMatches, "GET request failed [(]500: Internal Server Error[)]")
373+ c.Check(record, HasLen, 2)
374+}
375+
376+func (suite *suiteDestroyHostedService) TestFailsDeletingHostedService(c *C) {
377+ var responses []DispatcherResponse
378+ // Prepare.
379+ exampleHostedService := suite.makeExampleHostedService("one", "two")
380+ responses = append(responses, makeOKXMLResponse(c, exampleHostedService)...)
381+ // Deployments not found, but that's okay.
382+ responses = append(responses, exampleNotFoundResponse, exampleNotFoundResponse)
383+ // When deleting hosted service.
384+ responses = append(responses, exampleFailResponse)
385+ record := []*X509Request{}
386+ rigRecordingPreparedResponseDispatcher(&record, responses)
387+ // Exercise DestroyHostedService.
388+ api := makeAPI(c)
389+ request := &DestroyHostedServiceRequest{ServiceName: "service-name"}
390+ err := api.DestroyHostedService(request)
391+ c.Assert(err, NotNil)
392+ c.Check(err, ErrorMatches, "DELETE request failed [(]500: Internal Server Error[)]")
393+ c.Check(record, HasLen, 4)
394+}

Subscribers

People subscribed via source and target branches

to all changes: