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  }