github.com/cloudfoundry-attic/cli-with-i18n@v6.32.1-0.20171002233121-7401370d3b85+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/internal" 14 ) 15 16 //go:generate counterfeiter . Reader 17 18 // Reader is an io.Reader. 19 type Reader interface { 20 io.Reader 21 } 22 23 // JobStatus is the current state of a job. 24 type JobStatus string 25 26 const ( 27 // JobStatusFailed is when the job is no longer running due to a failure. 28 JobStatusFailed JobStatus = "failed" 29 30 // JobStatusFinished is when the job is no longer and it was successful. 31 JobStatusFinished JobStatus = "finished" 32 33 // JobStatusQueued is when the job is waiting to be run. 34 JobStatusQueued JobStatus = "queued" 35 36 // JobStatusRunning is when the job is running. 37 JobStatusRunning JobStatus = "running" 38 ) 39 40 // Job represents a Cloud Controller Job. 41 type Job struct { 42 Error string 43 ErrorDetails struct { 44 Description string 45 } 46 GUID string 47 Status JobStatus 48 } 49 50 // UnmarshalJSON helps unmarshal a Cloud Controller Job response. 51 func (job *Job) UnmarshalJSON(data []byte) error { 52 var ccJob struct { 53 Entity struct { 54 Error string `json:"error"` 55 ErrorDetails struct { 56 Description string `json:"description"` 57 } `json:"error_details"` 58 GUID string `json:"guid"` 59 Status string `json:"status"` 60 } `json:"entity"` 61 Metadata internal.Metadata `json:"metadata"` 62 } 63 if err := json.Unmarshal(data, &ccJob); err != nil { 64 return err 65 } 66 67 job.Error = ccJob.Entity.Error 68 job.ErrorDetails.Description = ccJob.Entity.ErrorDetails.Description 69 job.GUID = ccJob.Entity.GUID 70 job.Status = JobStatus(ccJob.Entity.Status) 71 return nil 72 } 73 74 // Finished returns true when the job has completed successfully. 75 func (job Job) Finished() bool { 76 return job.Status == JobStatusFinished 77 } 78 79 // Failed returns true when the job has completed with an error/failure. 80 func (job Job) Failed() bool { 81 return job.Status == JobStatusFailed 82 } 83 84 // GetJob returns a job for the provided GUID. 85 func (client *Client) GetJob(jobGUID string) (Job, Warnings, error) { 86 request, err := client.newHTTPRequest(requestOptions{ 87 RequestName: internal.GetJobRequest, 88 URIParams: Params{"job_guid": jobGUID}, 89 }) 90 if err != nil { 91 return Job{}, nil, err 92 } 93 94 var job Job 95 response := cloudcontroller.Response{ 96 Result: &job, 97 } 98 99 err = client.connection.Make(request, &response) 100 return job, response.Warnings, err 101 } 102 103 // PollJob will keep polling the given job until the job has terminated, an 104 // error is encountered, or config.OverallPollingTimeout is reached. In the 105 // last case, a JobTimeoutError is returned. 106 func (client *Client) PollJob(job Job) (Warnings, error) { 107 originalJobGUID := job.GUID 108 109 var ( 110 err error 111 warnings Warnings 112 allWarnings Warnings 113 ) 114 115 startTime := time.Now() 116 for time.Now().Sub(startTime) < client.jobPollingTimeout { 117 job, warnings, err = client.GetJob(job.GUID) 118 allWarnings = append(allWarnings, Warnings(warnings)...) 119 if err != nil { 120 return allWarnings, err 121 } 122 123 if job.Failed() { 124 return allWarnings, ccerror.JobFailedError{ 125 JobGUID: originalJobGUID, 126 Message: job.ErrorDetails.Description, 127 } 128 } 129 130 if job.Finished() { 131 return allWarnings, nil 132 } 133 134 time.Sleep(client.jobPollingInterval) 135 } 136 137 return allWarnings, ccerror.JobTimeoutError{ 138 JobGUID: originalJobGUID, 139 Timeout: client.jobPollingTimeout, 140 } 141 } 142 143 // UploadApplicationPackage uploads the newResources and a list of existing 144 // resources to the cloud controller. A job that combines the requested/newly 145 // uploaded bits is returned. If passed an io.Reader, this request will return 146 // a PipeSeekError on retry. 147 func (client *Client) UploadApplicationPackage(appGUID string, existingResources []Resource, newResources Reader, newResourcesLength int64) (Job, Warnings, error) { 148 if existingResources == nil { 149 return Job{}, nil, ccerror.NilObjectError{Object: "existingResources"} 150 } 151 if newResources == nil { 152 return Job{}, nil, ccerror.NilObjectError{Object: "newResources"} 153 } 154 155 contentLength, err := client.overallRequestSize(existingResources, newResourcesLength) 156 if err != nil { 157 return Job{}, nil, err 158 } 159 160 contentType, body, writeErrors := client.createMultipartBodyAndHeaderForAppBits(existingResources, newResources, newResourcesLength) 161 162 request, err := client.newHTTPRequest(requestOptions{ 163 RequestName: internal.PutAppBitsRequest, 164 URIParams: Params{"app_guid": appGUID}, 165 Query: url.Values{ 166 "async": {"true"}, 167 }, 168 Body: body, 169 }) 170 if err != nil { 171 return Job{}, nil, err 172 } 173 174 request.Header.Set("Content-Type", contentType) 175 request.ContentLength = contentLength 176 177 var job Job 178 response := cloudcontroller.Response{ 179 Result: &job, 180 } 181 182 httpErrors := client.uploadBits(request, &response) 183 184 // The following section makes the following assumptions: 185 // 1) If an error occurs during file reading, an EOF is sent to the request 186 // object. Thus ending the request transfer. 187 // 2) If an error occurs during request transfer, an EOF is sent to the pipe. 188 // Thus ending the writing routine. 189 var firstError error 190 var writeClosed, httpClosed bool 191 192 for { 193 select { 194 case writeErr, ok := <-writeErrors: 195 if !ok { 196 writeClosed = true 197 break 198 } 199 if firstError == nil { 200 firstError = writeErr 201 } 202 case httpErr, ok := <-httpErrors: 203 if !ok { 204 httpClosed = true 205 break 206 } 207 if firstError == nil { 208 firstError = httpErr 209 } 210 } 211 212 if writeClosed && httpClosed { 213 break 214 } 215 } 216 217 return job, response.Warnings, firstError 218 } 219 220 func (*Client) createMultipartBodyAndHeaderForAppBits(existingResources []Resource, newResources io.Reader, newResourcesLength int64) (string, io.ReadSeeker, <-chan error) { 221 writerOutput, writerInput := cloudcontroller.NewPipeBomb() 222 form := multipart.NewWriter(writerInput) 223 224 writeErrors := make(chan error) 225 226 go func() { 227 defer close(writeErrors) 228 defer writerInput.Close() 229 230 jsonResources, err := json.Marshal(existingResources) 231 if err != nil { 232 writeErrors <- err 233 return 234 } 235 236 err = form.WriteField("resources", string(jsonResources)) 237 if err != nil { 238 writeErrors <- err 239 return 240 } 241 242 writer, err := form.CreateFormFile("application", "application.zip") 243 if err != nil { 244 writeErrors <- err 245 return 246 } 247 248 if newResourcesLength != 0 { 249 _, err = io.Copy(writer, newResources) 250 if err != nil { 251 writeErrors <- err 252 return 253 } 254 } 255 256 err = form.Close() 257 if err != nil { 258 writeErrors <- err 259 } 260 }() 261 262 return form.FormDataContentType(), writerOutput, writeErrors 263 } 264 265 func (*Client) overallRequestSize(existingResources []Resource, newResourcesLength int64) (int64, error) { 266 body := &bytes.Buffer{} 267 form := multipart.NewWriter(body) 268 269 jsonResources, err := json.Marshal(existingResources) 270 if err != nil { 271 return 0, err 272 } 273 err = form.WriteField("resources", string(jsonResources)) 274 if err != nil { 275 return 0, err 276 } 277 _, err = form.CreateFormFile("application", "application.zip") 278 if err != nil { 279 return 0, err 280 } 281 err = form.Close() 282 if err != nil { 283 return 0, err 284 } 285 286 return int64(body.Len()) + newResourcesLength, nil 287 } 288 289 func (client *Client) uploadBits(request *cloudcontroller.Request, response *cloudcontroller.Response) <-chan error { 290 httpErrors := make(chan error) 291 292 go func() { 293 defer close(httpErrors) 294 295 err := client.connection.Make(request, response) 296 if err != nil { 297 httpErrors <- err 298 } 299 }() 300 301 return httpErrors 302 }