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  }