github.com/loggregator/cli@v6.33.1-0.20180224010324-82334f081791+incompatible/api/cloudcontroller/ccv2/job.go (about) 1 package ccv2 2 3 import ( 4 "bytes" 5 "encoding/json" 6 "io" 7 "mime/multipart" 8 "net/url" 9 "time" 10 11 "code.cloudfoundry.org/cli/api/cloudcontroller" 12 "code.cloudfoundry.org/cli/api/cloudcontroller/ccerror" 13 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2/constant" 14 "code.cloudfoundry.org/cli/api/cloudcontroller/ccv2/internal" 15 ) 16 17 //go:generate counterfeiter . Reader 18 19 // Reader is an io.Reader. 20 type Reader interface { 21 io.Reader 22 } 23 24 // Job represents a Cloud Controller Job. 25 type Job struct { 26 27 // Error is the error a job returns if it failed. It is otherwise empty. 28 Error string 29 30 // ErrorDetails is a detailed description of a job failure returned by the 31 // Cloud Controller. 32 ErrorDetails struct { 33 Description string 34 } 35 36 // GUID is the unique job identifier. 37 GUID string 38 39 // Status is the current state of the job. 40 Status constant.JobStatus 41 } 42 43 // UnmarshalJSON helps unmarshal a Cloud Controller Job response. 44 func (job *Job) UnmarshalJSON(data []byte) error { 45 var ccJob struct { 46 Entity struct { 47 Error string `json:"error"` 48 ErrorDetails struct { 49 Description string `json:"description"` 50 } `json:"error_details"` 51 GUID string `json:"guid"` 52 Status string `json:"status"` 53 } `json:"entity"` 54 Metadata internal.Metadata `json:"metadata"` 55 } 56 if err := json.Unmarshal(data, &ccJob); err != nil { 57 return err 58 } 59 60 job.Error = ccJob.Entity.Error 61 job.ErrorDetails.Description = ccJob.Entity.ErrorDetails.Description 62 job.GUID = ccJob.Entity.GUID 63 job.Status = constant.JobStatus(ccJob.Entity.Status) 64 return nil 65 } 66 67 // Finished returns true when the job has completed successfully. 68 func (job Job) Finished() bool { 69 return job.Status == constant.JobStatusFinished 70 } 71 72 // Failed returns true when the job has completed with an error/failure. 73 func (job Job) Failed() bool { 74 return job.Status == constant.JobStatusFailed 75 } 76 77 // DeleteOrganizationJob deletes the Organization associated with the provided 78 // GUID. It will return the Cloud Controller job that is assigned to the 79 // Organization deletion. 80 func (client *Client) DeleteOrganizationJob(guid string) (Job, Warnings, error) { 81 request, err := client.newHTTPRequest(requestOptions{ 82 RequestName: internal.DeleteOrganizationRequest, 83 URIParams: Params{"organization_guid": guid}, 84 Query: url.Values{ 85 "recursive": {"true"}, 86 "async": {"true"}, 87 }, 88 }) 89 if err != nil { 90 return Job{}, nil, err 91 } 92 93 var job Job 94 response := cloudcontroller.Response{ 95 Result: &job, 96 } 97 98 err = client.connection.Make(request, &response) 99 return job, response.Warnings, err 100 } 101 102 // DeleteSpaceJob deletes the Space associated with the provided GUID. It will 103 // return the Cloud Controller job that is assigned to the Space deletion. 104 func (client *Client) DeleteSpaceJob(guid string) (Job, Warnings, error) { 105 request, err := client.newHTTPRequest(requestOptions{ 106 RequestName: internal.DeleteSpaceRequest, 107 URIParams: Params{"space_guid": guid}, 108 Query: url.Values{ 109 "recursive": {"true"}, 110 "async": {"true"}, 111 }, 112 }) 113 if err != nil { 114 return Job{}, nil, err 115 } 116 117 var job Job 118 response := cloudcontroller.Response{ 119 Result: &job, 120 } 121 122 err = client.connection.Make(request, &response) 123 return job, response.Warnings, err 124 } 125 126 // GetJob returns a job for the provided GUID. 127 func (client *Client) GetJob(jobGUID string) (Job, Warnings, error) { 128 request, err := client.newHTTPRequest(requestOptions{ 129 RequestName: internal.GetJobRequest, 130 URIParams: Params{"job_guid": jobGUID}, 131 }) 132 if err != nil { 133 return Job{}, nil, err 134 } 135 136 var job Job 137 response := cloudcontroller.Response{ 138 Result: &job, 139 } 140 141 err = client.connection.Make(request, &response) 142 return job, response.Warnings, err 143 } 144 145 // PollJob will keep polling the given job until the job has terminated, an 146 // error is encountered, or config.OverallPollingTimeout is reached. In the 147 // last case, a JobTimeoutError is returned. 148 func (client *Client) PollJob(job Job) (Warnings, error) { 149 originalJobGUID := job.GUID 150 151 var ( 152 err error 153 warnings Warnings 154 allWarnings Warnings 155 ) 156 157 startTime := time.Now() 158 for time.Now().Sub(startTime) < client.jobPollingTimeout { 159 job, warnings, err = client.GetJob(job.GUID) 160 allWarnings = append(allWarnings, Warnings(warnings)...) 161 if err != nil { 162 return allWarnings, err 163 } 164 165 if job.Failed() { 166 return allWarnings, ccerror.JobFailedError{ 167 JobGUID: originalJobGUID, 168 Message: job.ErrorDetails.Description, 169 } 170 } 171 172 if job.Finished() { 173 return allWarnings, nil 174 } 175 176 time.Sleep(client.jobPollingInterval) 177 } 178 179 return allWarnings, ccerror.JobTimeoutError{ 180 JobGUID: originalJobGUID, 181 Timeout: client.jobPollingTimeout, 182 } 183 } 184 185 // UploadApplicationPackage uploads the newResources and a list of existing 186 // resources to the cloud controller. A job that combines the requested/newly 187 // uploaded bits is returned. The function will act differently given the 188 // following Readers: 189 // io.ReadSeeker: Will function properly on retry. 190 // io.Reader: Will return a ccerror.PipeSeekError on retry. 191 // nil: Will not add the "application" section to the request. 192 // newResourcesLength is ignored in this case. 193 func (client *Client) UploadApplicationPackage(appGUID string, existingResources []Resource, newResources Reader, newResourcesLength int64) (Job, Warnings, error) { 194 if existingResources == nil { 195 return Job{}, nil, ccerror.NilObjectError{Object: "existingResources"} 196 } 197 198 if newResources == nil { 199 return client.uploadExistingResourcesOnly(appGUID, existingResources) 200 } 201 202 return client.uploadNewAndExistingResources(appGUID, existingResources, newResources, newResourcesLength) 203 } 204 205 // UploadDroplet defines and uploads a previously staged droplet that an 206 // application will run, using a multipart PUT request. The uploaded file 207 // should be a gzipped tar file. 208 func (client *Client) UploadDroplet(appGUID string, droplet io.Reader, dropletLength int64) (Job, Warnings, error) { 209 contentLength, err := client.calculateDropletRequestSize(dropletLength) 210 if err != nil { 211 return Job{}, nil, err 212 } 213 214 contentType, body, writeErrors := client.createMultipartBodyAndHeaderForDroplet(droplet) 215 216 request, err := client.newHTTPRequest(requestOptions{ 217 RequestName: internal.PutDropletRequest, 218 URIParams: Params{"app_guid": appGUID}, 219 Body: body, 220 }) 221 if err != nil { 222 return Job{}, nil, err 223 } 224 225 request.Header.Set("Content-Type", contentType) 226 request.ContentLength = contentLength 227 228 return client.uploadAsynchronously(request, writeErrors) 229 } 230 231 func (*Client) createMultipartBodyAndHeaderForAppBits(existingResources []Resource, newResources io.Reader, newResourcesLength int64) (string, io.ReadSeeker, <-chan error) { 232 writerOutput, writerInput := cloudcontroller.NewPipeBomb() 233 form := multipart.NewWriter(writerInput) 234 235 writeErrors := make(chan error) 236 237 go func() { 238 defer close(writeErrors) 239 defer writerInput.Close() 240 241 jsonResources, err := json.Marshal(existingResources) 242 if err != nil { 243 writeErrors <- err 244 return 245 } 246 247 err = form.WriteField("resources", string(jsonResources)) 248 if err != nil { 249 writeErrors <- err 250 return 251 } 252 253 writer, err := form.CreateFormFile("application", "application.zip") 254 if err != nil { 255 writeErrors <- err 256 return 257 } 258 259 if newResourcesLength != 0 { 260 _, err = io.Copy(writer, newResources) 261 if err != nil { 262 writeErrors <- err 263 return 264 } 265 } 266 267 err = form.Close() 268 if err != nil { 269 writeErrors <- err 270 } 271 }() 272 273 return form.FormDataContentType(), writerOutput, writeErrors 274 } 275 276 func (*Client) createMultipartBodyAndHeaderForDroplet(droplet io.Reader) (string, io.ReadSeeker, <-chan error) { 277 writerOutput, writerInput := cloudcontroller.NewPipeBomb() 278 form := multipart.NewWriter(writerInput) 279 280 writeErrors := make(chan error) 281 282 go func() { 283 defer close(writeErrors) 284 defer writerInput.Close() 285 286 writer, err := form.CreateFormFile("droplet", "droplet.tgz") 287 if err != nil { 288 writeErrors <- err 289 return 290 } 291 292 _, err = io.Copy(writer, droplet) 293 if err != nil { 294 writeErrors <- err 295 return 296 } 297 298 err = form.Close() 299 if err != nil { 300 writeErrors <- err 301 } 302 }() 303 304 return form.FormDataContentType(), writerOutput, writeErrors 305 } 306 307 func (*Client) calculateAppBitsRequestSize(existingResources []Resource, newResourcesLength int64) (int64, error) { 308 body := &bytes.Buffer{} 309 form := multipart.NewWriter(body) 310 311 jsonResources, err := json.Marshal(existingResources) 312 if err != nil { 313 return 0, err 314 } 315 err = form.WriteField("resources", string(jsonResources)) 316 if err != nil { 317 return 0, err 318 } 319 _, err = form.CreateFormFile("application", "application.zip") 320 if err != nil { 321 return 0, err 322 } 323 err = form.Close() 324 if err != nil { 325 return 0, err 326 } 327 328 return int64(body.Len()) + newResourcesLength, nil 329 } 330 331 func (*Client) calculateDropletRequestSize(dropletSize int64) (int64, error) { 332 body := &bytes.Buffer{} 333 form := multipart.NewWriter(body) 334 335 _, err := form.CreateFormFile("droplet", "droplet.tgz") 336 if err != nil { 337 return 0, err 338 } 339 340 err = form.Close() 341 if err != nil { 342 return 0, err 343 } 344 345 return int64(body.Len()) + dropletSize, nil 346 } 347 348 func (client *Client) uploadExistingResourcesOnly(appGUID string, existingResources []Resource) (Job, Warnings, error) { 349 jsonResources, err := json.Marshal(existingResources) 350 if err != nil { 351 return Job{}, nil, err 352 } 353 354 body := bytes.NewBuffer(nil) 355 form := multipart.NewWriter(body) 356 err = form.WriteField("resources", string(jsonResources)) 357 if err != nil { 358 return Job{}, nil, err 359 } 360 361 err = form.Close() 362 if err != nil { 363 return Job{}, nil, err 364 } 365 366 request, err := client.newHTTPRequest(requestOptions{ 367 RequestName: internal.PutAppBitsRequest, 368 URIParams: Params{"app_guid": appGUID}, 369 Query: url.Values{ 370 "async": {"true"}, 371 }, 372 Body: bytes.NewReader(body.Bytes()), 373 }) 374 if err != nil { 375 return Job{}, nil, err 376 } 377 378 request.Header.Set("Content-Type", form.FormDataContentType()) 379 380 var job Job 381 response := cloudcontroller.Response{ 382 Result: &job, 383 } 384 385 err = client.connection.Make(request, &response) 386 return job, response.Warnings, err 387 } 388 389 func (client *Client) uploadAsynchronously(request *cloudcontroller.Request, writeErrors <-chan error) (Job, Warnings, error) { 390 var job Job 391 response := cloudcontroller.Response{ 392 Result: &job, 393 } 394 395 httpErrors := make(chan error) 396 397 go func() { 398 defer close(httpErrors) 399 400 err := client.connection.Make(request, &response) 401 if err != nil { 402 httpErrors <- err 403 } 404 }() 405 406 // The following section makes the following assumptions: 407 // 1) If an error occurs during file reading, an EOF is sent to the request 408 // object. Thus ending the request transfer. 409 // 2) If an error occurs during request transfer, an EOF is sent to the pipe. 410 // Thus ending the writing routine. 411 var firstError error 412 var writeClosed, httpClosed bool 413 414 for { 415 select { 416 case writeErr, ok := <-writeErrors: 417 if !ok { 418 writeClosed = true 419 break // for select 420 } 421 if firstError == nil { 422 firstError = writeErr 423 } 424 case httpErr, ok := <-httpErrors: 425 if !ok { 426 httpClosed = true 427 break // for select 428 } 429 if firstError == nil { 430 firstError = httpErr 431 } 432 } 433 434 if writeClosed && httpClosed { 435 break // for for 436 } 437 } 438 439 return job, response.Warnings, firstError 440 } 441 442 func (client *Client) uploadNewAndExistingResources(appGUID string, existingResources []Resource, newResources Reader, newResourcesLength int64) (Job, Warnings, error) { 443 contentLength, err := client.calculateAppBitsRequestSize(existingResources, newResourcesLength) 444 if err != nil { 445 return Job{}, nil, err 446 } 447 448 contentType, body, writeErrors := client.createMultipartBodyAndHeaderForAppBits(existingResources, newResources, newResourcesLength) 449 450 request, err := client.newHTTPRequest(requestOptions{ 451 RequestName: internal.PutAppBitsRequest, 452 URIParams: Params{"app_guid": appGUID}, 453 Query: url.Values{ 454 "async": {"true"}, 455 }, 456 Body: body, 457 }) 458 if err != nil { 459 return Job{}, nil, err 460 } 461 462 request.Header.Set("Content-Type", contentType) 463 request.ContentLength = contentLength 464 465 return client.uploadAsynchronously(request, writeErrors) 466 }