github.com/mook-as/cf-cli@v7.0.0-beta.28.0.20200120190804-b91c115fae48+incompatible/api/cloudcontroller/ccv3/package.go (about)

     1  package ccv3
     2  
     3  import (
     4  	"bytes"
     5  	"encoding/json"
     6  	"io"
     7  	"mime/multipart"
     8  	"os"
     9  	"path/filepath"
    10  
    11  	"code.cloudfoundry.org/cli/api/cloudcontroller"
    12  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
    13  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/constant"
    14  	"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3/internal"
    15  )
    16  
    17  //go:generate counterfeiter io.Reader
    18  
    19  // Package represents a Cloud Controller V3 Package.
    20  type Package struct {
    21  	// CreatedAt is the time with zone when the object was created.
    22  	CreatedAt string
    23  
    24  	// DockerImage is the registry address of the docker image.
    25  	DockerImage string
    26  
    27  	// DockerPassword is the password for the docker image's registry.
    28  	DockerPassword string
    29  
    30  	// DockerUsername is the username for the docker image's registry.
    31  	DockerUsername string
    32  
    33  	// GUID is the unique identifier of the package.
    34  	GUID string
    35  
    36  	// Links are links to related resources.
    37  	Links APILinks
    38  
    39  	// Relationships are a list of relationships to other resources.
    40  	Relationships Relationships
    41  
    42  	// State is the state of the package.
    43  	State constant.PackageState
    44  
    45  	// Type is the package type.
    46  	Type constant.PackageType
    47  }
    48  
    49  // MarshalJSON converts a Package into a Cloud Controller Package.
    50  func (p Package) MarshalJSON() ([]byte, error) {
    51  	type ccPackageData struct {
    52  		Image    string `json:"image,omitempty"`
    53  		Username string `json:"username,omitempty"`
    54  		Password string `json:"password,omitempty"`
    55  	}
    56  	var ccPackage struct {
    57  		GUID          string                `json:"guid,omitempty"`
    58  		CreatedAt     string                `json:"created_at,omitempty"`
    59  		Links         APILinks              `json:"links,omitempty"`
    60  		Relationships Relationships         `json:"relationships,omitempty"`
    61  		State         constant.PackageState `json:"state,omitempty"`
    62  		Type          constant.PackageType  `json:"type,omitempty"`
    63  		Data          *ccPackageData        `json:"data,omitempty"`
    64  	}
    65  
    66  	ccPackage.GUID = p.GUID
    67  	ccPackage.CreatedAt = p.CreatedAt
    68  	ccPackage.Links = p.Links
    69  	ccPackage.Relationships = p.Relationships
    70  	ccPackage.State = p.State
    71  	ccPackage.Type = p.Type
    72  	if p.DockerImage != "" {
    73  		ccPackage.Data = &ccPackageData{
    74  			Image:    p.DockerImage,
    75  			Username: p.DockerUsername,
    76  			Password: p.DockerPassword,
    77  		}
    78  	}
    79  
    80  	return json.Marshal(ccPackage)
    81  }
    82  
    83  // UnmarshalJSON helps unmarshal a Cloud Controller Package response.
    84  func (p *Package) UnmarshalJSON(data []byte) error {
    85  	var ccPackage struct {
    86  		GUID          string                `json:"guid,omitempty"`
    87  		CreatedAt     string                `json:"created_at,omitempty"`
    88  		Links         APILinks              `json:"links,omitempty"`
    89  		Relationships Relationships         `json:"relationships,omitempty"`
    90  		State         constant.PackageState `json:"state,omitempty"`
    91  		Type          constant.PackageType  `json:"type,omitempty"`
    92  		Data          struct {
    93  			Image    string `json:"image"`
    94  			Username string `json:"username"`
    95  			Password string `json:"password"`
    96  		} `json:"data"`
    97  	}
    98  	err := cloudcontroller.DecodeJSON(data, &ccPackage)
    99  	if err != nil {
   100  		return err
   101  	}
   102  
   103  	p.GUID = ccPackage.GUID
   104  	p.CreatedAt = ccPackage.CreatedAt
   105  	p.Links = ccPackage.Links
   106  	p.Relationships = ccPackage.Relationships
   107  	p.State = ccPackage.State
   108  	p.Type = ccPackage.Type
   109  	p.DockerImage = ccPackage.Data.Image
   110  	p.DockerUsername = ccPackage.Data.Username
   111  	p.DockerPassword = ccPackage.Data.Password
   112  
   113  	return nil
   114  }
   115  
   116  // CreatePackage creates a package with the given settings, Type and the
   117  // ApplicationRelationship must be set.
   118  func (client *Client) CreatePackage(pkg Package) (Package, Warnings, error) {
   119  	bodyBytes, err := json.Marshal(pkg)
   120  	if err != nil {
   121  		return Package{}, nil, err
   122  	}
   123  
   124  	request, err := client.newHTTPRequest(requestOptions{
   125  		RequestName: internal.PostPackageRequest,
   126  		Body:        bytes.NewReader(bodyBytes),
   127  	})
   128  	if err != nil {
   129  		return Package{}, nil, err
   130  	}
   131  
   132  	var responsePackage Package
   133  	response := cloudcontroller.Response{
   134  		DecodeJSONResponseInto: &responsePackage,
   135  	}
   136  	err = client.connection.Make(request, &response)
   137  
   138  	return responsePackage, response.Warnings, err
   139  }
   140  
   141  // GetPackage returns the package with the given GUID.
   142  func (client *Client) GetPackage(packageGUID string) (Package, Warnings, error) {
   143  	request, err := client.newHTTPRequest(requestOptions{
   144  		RequestName: internal.GetPackageRequest,
   145  		URIParams:   internal.Params{"package_guid": packageGUID},
   146  	})
   147  	if err != nil {
   148  		return Package{}, nil, err
   149  	}
   150  
   151  	var responsePackage Package
   152  	response := cloudcontroller.Response{
   153  		DecodeJSONResponseInto: &responsePackage,
   154  	}
   155  	err = client.connection.Make(request, &response)
   156  
   157  	return responsePackage, response.Warnings, err
   158  }
   159  
   160  // GetPackages returns the list of packages.
   161  func (client *Client) GetPackages(query ...Query) ([]Package, Warnings, error) {
   162  	request, err := client.newHTTPRequest(requestOptions{
   163  		RequestName: internal.GetPackagesRequest,
   164  		Query:       query,
   165  	})
   166  	if err != nil {
   167  		return nil, nil, err
   168  	}
   169  
   170  	var fullPackagesList []Package
   171  	warnings, err := client.paginate(request, Package{}, func(item interface{}) error {
   172  		if pkg, ok := item.(Package); ok {
   173  			fullPackagesList = append(fullPackagesList, pkg)
   174  		} else {
   175  			return ccerror.UnknownObjectInListError{
   176  				Expected:   Package{},
   177  				Unexpected: item,
   178  			}
   179  		}
   180  		return nil
   181  	})
   182  
   183  	return fullPackagesList, warnings, err
   184  }
   185  
   186  // UploadBitsPackage uploads the newResources and a list of existing resources
   187  // to the cloud controller. An updated package is returned. The function will
   188  // act differently given the 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. The newResourcesLength is ignored in this case.
   192  //
   193  // Note: In order to determine if package creation is successful, poll the
   194  // Package's state field for more information.
   195  func (client *Client) UploadBitsPackage(pkg Package, matchedResources []Resource, newResources io.Reader, newResourcesLength int64) (Package, Warnings, error) {
   196  	if matchedResources == nil {
   197  		return Package{}, nil, ccerror.NilObjectError{Object: "matchedResources"}
   198  	}
   199  
   200  	if newResources == nil {
   201  		return client.uploadExistingResourcesOnly(pkg.GUID, matchedResources)
   202  	}
   203  
   204  	return client.uploadNewAndExistingResources(pkg.GUID, matchedResources, newResources, newResourcesLength)
   205  }
   206  
   207  // UploadPackage uploads a file to a given package's Upload resource. Note:
   208  // fileToUpload is read entirely into memory prior to sending data to CC.
   209  func (client *Client) UploadPackage(pkg Package, fileToUpload string) (Package, Warnings, error) {
   210  	body, contentType, err := client.createUploadStream(fileToUpload, "bits")
   211  	if err != nil {
   212  		return Package{}, nil, err
   213  	}
   214  
   215  	request, err := client.newHTTPRequest(requestOptions{
   216  		RequestName: internal.PostPackageBitsRequest,
   217  		URIParams:   internal.Params{"package_guid": pkg.GUID},
   218  		Body:        body,
   219  	})
   220  	if err != nil {
   221  		return Package{}, nil, err
   222  	}
   223  
   224  	request.Header.Set("Content-Type", contentType)
   225  
   226  	var responsePackage Package
   227  	response := cloudcontroller.Response{
   228  		DecodeJSONResponseInto: &responsePackage,
   229  	}
   230  	err = client.connection.Make(request, &response)
   231  
   232  	return responsePackage, response.Warnings, err
   233  }
   234  
   235  func (client *Client) calculateAppBitsRequestSize(matchedResources []Resource, newResourcesLength int64) (int64, error) {
   236  	body := &bytes.Buffer{}
   237  	form := multipart.NewWriter(body)
   238  
   239  	jsonResources, err := json.Marshal(matchedResources)
   240  	if err != nil {
   241  		return 0, err
   242  	}
   243  	err = form.WriteField("resources", string(jsonResources))
   244  	if err != nil {
   245  		return 0, err
   246  	}
   247  	_, err = form.CreateFormFile("bits", "package.zip")
   248  	if err != nil {
   249  		return 0, err
   250  	}
   251  	err = form.Close()
   252  	if err != nil {
   253  		return 0, err
   254  	}
   255  
   256  	return int64(body.Len()) + newResourcesLength, nil
   257  }
   258  
   259  func (client *Client) createMultipartBodyAndHeaderForAppBits(matchedResources []Resource, newResources io.Reader, newResourcesLength int64) (string, io.ReadSeeker, <-chan error) {
   260  	writerOutput, writerInput := cloudcontroller.NewPipeBomb()
   261  	form := multipart.NewWriter(writerInput)
   262  
   263  	writeErrors := make(chan error)
   264  
   265  	go func() {
   266  		defer close(writeErrors)
   267  		defer writerInput.Close()
   268  
   269  		jsonResources, err := json.Marshal(matchedResources)
   270  		if err != nil {
   271  			writeErrors <- err
   272  			return
   273  		}
   274  
   275  		err = form.WriteField("resources", string(jsonResources))
   276  		if err != nil {
   277  			writeErrors <- err
   278  			return
   279  		}
   280  
   281  		writer, err := form.CreateFormFile("bits", "package.zip")
   282  		if err != nil {
   283  			writeErrors <- err
   284  			return
   285  		}
   286  
   287  		if newResourcesLength != 0 {
   288  			_, err = io.Copy(writer, newResources)
   289  			if err != nil {
   290  				writeErrors <- err
   291  				return
   292  			}
   293  		}
   294  
   295  		err = form.Close()
   296  		if err != nil {
   297  			writeErrors <- err
   298  		}
   299  	}()
   300  
   301  	return form.FormDataContentType(), writerOutput, writeErrors
   302  }
   303  
   304  func (*Client) createUploadStream(path string, paramName string) (io.ReadSeeker, string, error) {
   305  	file, err := os.Open(path)
   306  	if err != nil {
   307  		return nil, "", err
   308  	}
   309  	defer file.Close()
   310  
   311  	body := &bytes.Buffer{}
   312  	writer := multipart.NewWriter(body)
   313  	part, err := writer.CreateFormFile(paramName, filepath.Base(path))
   314  	if err != nil {
   315  		return nil, "", err
   316  	}
   317  	_, err = io.Copy(part, file)
   318  	if err != nil {
   319  		return nil, "", err
   320  	}
   321  
   322  	err = writer.Close()
   323  
   324  	return bytes.NewReader(body.Bytes()), writer.FormDataContentType(), err
   325  }
   326  
   327  func (client *Client) uploadAsynchronously(request *cloudcontroller.Request, writeErrors <-chan error) (Package, Warnings, error) {
   328  	var pkg Package
   329  	response := cloudcontroller.Response{
   330  		DecodeJSONResponseInto: &pkg,
   331  	}
   332  
   333  	httpErrors := make(chan error)
   334  
   335  	go func() {
   336  		defer close(httpErrors)
   337  
   338  		err := client.connection.Make(request, &response)
   339  		if err != nil {
   340  			httpErrors <- err
   341  		}
   342  	}()
   343  
   344  	// The following section makes the following assumptions:
   345  	// 1) If an error occurs during file reading, an EOF is sent to the request
   346  	// object. Thus ending the request transfer.
   347  	// 2) If an error occurs during request transfer, an EOF is sent to the pipe.
   348  	// Thus ending the writing routine.
   349  	var firstError error
   350  	var writeClosed, httpClosed bool
   351  
   352  	for {
   353  		select {
   354  		case writeErr, ok := <-writeErrors:
   355  			if !ok {
   356  				writeClosed = true
   357  				break // for select
   358  			}
   359  			if firstError == nil {
   360  				firstError = writeErr
   361  			}
   362  		case httpErr, ok := <-httpErrors:
   363  			if !ok {
   364  				httpClosed = true
   365  				break // for select
   366  			}
   367  			if firstError == nil {
   368  				firstError = httpErr
   369  			}
   370  		}
   371  
   372  		if writeClosed && httpClosed {
   373  			break // for for
   374  		}
   375  	}
   376  
   377  	return pkg, response.Warnings, firstError
   378  }
   379  
   380  func (client *Client) uploadExistingResourcesOnly(packageGUID string, matchedResources []Resource) (Package, Warnings, error) {
   381  	jsonResources, err := json.Marshal(matchedResources)
   382  	if err != nil {
   383  		return Package{}, nil, err
   384  	}
   385  
   386  	body := bytes.NewBuffer(nil)
   387  	form := multipart.NewWriter(body)
   388  	err = form.WriteField("resources", string(jsonResources))
   389  	if err != nil {
   390  		return Package{}, nil, err
   391  	}
   392  
   393  	err = form.Close()
   394  	if err != nil {
   395  		return Package{}, nil, err
   396  	}
   397  
   398  	request, err := client.newHTTPRequest(requestOptions{
   399  		RequestName: internal.PostPackageBitsRequest,
   400  		URIParams:   internal.Params{"package_guid": packageGUID},
   401  		Body:        bytes.NewReader(body.Bytes()),
   402  	})
   403  	if err != nil {
   404  		return Package{}, nil, err
   405  	}
   406  
   407  	request.Header.Set("Content-Type", form.FormDataContentType())
   408  
   409  	var pkg Package
   410  	response := cloudcontroller.Response{
   411  		DecodeJSONResponseInto: &pkg,
   412  	}
   413  
   414  	err = client.connection.Make(request, &response)
   415  	return pkg, response.Warnings, err
   416  }
   417  
   418  func (client *Client) uploadNewAndExistingResources(packageGUID string, matchedResources []Resource, newResources io.Reader, newResourcesLength int64) (Package, Warnings, error) {
   419  	contentLength, err := client.calculateAppBitsRequestSize(matchedResources, newResourcesLength)
   420  	if err != nil {
   421  		return Package{}, nil, err
   422  	}
   423  
   424  	contentType, body, writeErrors := client.createMultipartBodyAndHeaderForAppBits(matchedResources, newResources, newResourcesLength)
   425  
   426  	request, err := client.newHTTPRequest(requestOptions{
   427  		RequestName: internal.PostPackageBitsRequest,
   428  		URIParams:   internal.Params{"package_guid": packageGUID},
   429  		Body:        body,
   430  	})
   431  	if err != nil {
   432  		return Package{}, nil, err
   433  	}
   434  
   435  	request.Header.Set("Content-Type", contentType)
   436  	request.ContentLength = contentLength
   437  
   438  	return client.uploadAsynchronously(request, writeErrors)
   439  }