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