github.com/arunkumar7540/cli@v6.45.0+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  	link, ok := pkg.Links["upload"]
   197  	if !ok {
   198  		return Package{}, nil, ccerror.UploadLinkNotFoundError{PackageGUID: pkg.GUID}
   199  	}
   200  
   201  	if matchedResources == nil {
   202  		return Package{}, nil, ccerror.NilObjectError{Object: "matchedResources"}
   203  	}
   204  
   205  	if newResources == nil {
   206  		return client.uploadExistingResourcesOnly(link, matchedResources)
   207  	}
   208  
   209  	return client.uploadNewAndExistingResources(link, matchedResources, newResources, newResourcesLength)
   210  }
   211  
   212  // UploadPackage uploads a file to a given package's Upload resource. Note:
   213  // fileToUpload is read entirely into memory prior to sending data to CC.
   214  func (client *Client) UploadPackage(pkg Package, fileToUpload string) (Package, Warnings, error) {
   215  	link, ok := pkg.Links["upload"]
   216  	if !ok {
   217  		return Package{}, nil, ccerror.UploadLinkNotFoundError{PackageGUID: pkg.GUID}
   218  	}
   219  
   220  	body, contentType, err := client.createUploadStream(fileToUpload, "bits")
   221  	if err != nil {
   222  		return Package{}, nil, err
   223  	}
   224  
   225  	request, err := client.newHTTPRequest(requestOptions{
   226  		URL:    link.HREF,
   227  		Method: link.Method,
   228  		Body:   body,
   229  	})
   230  	if err != nil {
   231  		return Package{}, nil, err
   232  	}
   233  
   234  	request.Header.Set("Content-Type", contentType)
   235  
   236  	var responsePackage Package
   237  	response := cloudcontroller.Response{
   238  		DecodeJSONResponseInto: &responsePackage,
   239  	}
   240  	err = client.connection.Make(request, &response)
   241  
   242  	return responsePackage, response.Warnings, err
   243  }
   244  
   245  func (client *Client) calculateAppBitsRequestSize(matchedResources []Resource, newResourcesLength int64) (int64, error) {
   246  	body := &bytes.Buffer{}
   247  	form := multipart.NewWriter(body)
   248  
   249  	jsonResources, err := json.Marshal(matchedResources)
   250  	if err != nil {
   251  		return 0, err
   252  	}
   253  	err = form.WriteField("resources", string(jsonResources))
   254  	if err != nil {
   255  		return 0, err
   256  	}
   257  	_, err = form.CreateFormFile("bits", "package.zip")
   258  	if err != nil {
   259  		return 0, err
   260  	}
   261  	err = form.Close()
   262  	if err != nil {
   263  		return 0, err
   264  	}
   265  
   266  	return int64(body.Len()) + newResourcesLength, nil
   267  }
   268  
   269  func (client *Client) createMultipartBodyAndHeaderForAppBits(matchedResources []Resource, newResources io.Reader, newResourcesLength int64) (string, io.ReadSeeker, <-chan error) {
   270  	writerOutput, writerInput := cloudcontroller.NewPipeBomb()
   271  	form := multipart.NewWriter(writerInput)
   272  
   273  	writeErrors := make(chan error)
   274  
   275  	go func() {
   276  		defer close(writeErrors)
   277  		defer writerInput.Close()
   278  
   279  		jsonResources, err := json.Marshal(matchedResources)
   280  		if err != nil {
   281  			writeErrors <- err
   282  			return
   283  		}
   284  
   285  		err = form.WriteField("resources", string(jsonResources))
   286  		if err != nil {
   287  			writeErrors <- err
   288  			return
   289  		}
   290  
   291  		writer, err := form.CreateFormFile("bits", "package.zip")
   292  		if err != nil {
   293  			writeErrors <- err
   294  			return
   295  		}
   296  
   297  		if newResourcesLength != 0 {
   298  			_, err = io.Copy(writer, newResources)
   299  			if err != nil {
   300  				writeErrors <- err
   301  				return
   302  			}
   303  		}
   304  
   305  		err = form.Close()
   306  		if err != nil {
   307  			writeErrors <- err
   308  		}
   309  	}()
   310  
   311  	return form.FormDataContentType(), writerOutput, writeErrors
   312  }
   313  
   314  func (*Client) createUploadStream(path string, paramName string) (io.ReadSeeker, string, error) {
   315  	file, err := os.Open(path)
   316  	if err != nil {
   317  		return nil, "", err
   318  	}
   319  	defer file.Close()
   320  
   321  	body := &bytes.Buffer{}
   322  	writer := multipart.NewWriter(body)
   323  	part, err := writer.CreateFormFile(paramName, filepath.Base(path))
   324  	if err != nil {
   325  		return nil, "", err
   326  	}
   327  	_, err = io.Copy(part, file)
   328  	if err != nil {
   329  		return nil, "", err
   330  	}
   331  
   332  	err = writer.Close()
   333  
   334  	return bytes.NewReader(body.Bytes()), writer.FormDataContentType(), err
   335  }
   336  
   337  func (client *Client) uploadAsynchronously(request *cloudcontroller.Request, writeErrors <-chan error) (Package, Warnings, error) {
   338  	var pkg Package
   339  	response := cloudcontroller.Response{
   340  		DecodeJSONResponseInto: &pkg,
   341  	}
   342  
   343  	httpErrors := make(chan error)
   344  
   345  	go func() {
   346  		defer close(httpErrors)
   347  
   348  		err := client.connection.Make(request, &response)
   349  		if err != nil {
   350  			httpErrors <- err
   351  		}
   352  	}()
   353  
   354  	// The following section makes the following assumptions:
   355  	// 1) If an error occurs during file reading, an EOF is sent to the request
   356  	// object. Thus ending the request transfer.
   357  	// 2) If an error occurs during request transfer, an EOF is sent to the pipe.
   358  	// Thus ending the writing routine.
   359  	var firstError error
   360  	var writeClosed, httpClosed bool
   361  
   362  	for {
   363  		select {
   364  		case writeErr, ok := <-writeErrors:
   365  			if !ok {
   366  				writeClosed = true
   367  				break // for select
   368  			}
   369  			if firstError == nil {
   370  				firstError = writeErr
   371  			}
   372  		case httpErr, ok := <-httpErrors:
   373  			if !ok {
   374  				httpClosed = true
   375  				break // for select
   376  			}
   377  			if firstError == nil {
   378  				firstError = httpErr
   379  			}
   380  		}
   381  
   382  		if writeClosed && httpClosed {
   383  			break // for for
   384  		}
   385  	}
   386  
   387  	return pkg, response.Warnings, firstError
   388  }
   389  
   390  func (client *Client) uploadExistingResourcesOnly(uploadLink APILink, matchedResources []Resource) (Package, Warnings, error) {
   391  	jsonResources, err := json.Marshal(matchedResources)
   392  	if err != nil {
   393  		return Package{}, nil, err
   394  	}
   395  
   396  	body := bytes.NewBuffer(nil)
   397  	form := multipart.NewWriter(body)
   398  	err = form.WriteField("resources", string(jsonResources))
   399  	if err != nil {
   400  		return Package{}, nil, err
   401  	}
   402  
   403  	err = form.Close()
   404  	if err != nil {
   405  		return Package{}, nil, err
   406  	}
   407  
   408  	request, err := client.newHTTPRequest(requestOptions{
   409  		URL:    uploadLink.HREF,
   410  		Method: uploadLink.Method,
   411  		Body:   bytes.NewReader(body.Bytes()),
   412  	})
   413  	if err != nil {
   414  		return Package{}, nil, err
   415  	}
   416  
   417  	request.Header.Set("Content-Type", form.FormDataContentType())
   418  
   419  	var pkg Package
   420  	response := cloudcontroller.Response{
   421  		DecodeJSONResponseInto: &pkg,
   422  	}
   423  
   424  	err = client.connection.Make(request, &response)
   425  	return pkg, response.Warnings, err
   426  }
   427  
   428  func (client *Client) uploadNewAndExistingResources(uploadLink APILink, matchedResources []Resource, newResources io.Reader, newResourcesLength int64) (Package, Warnings, error) {
   429  	contentLength, err := client.calculateAppBitsRequestSize(matchedResources, newResourcesLength)
   430  	if err != nil {
   431  		return Package{}, nil, err
   432  	}
   433  
   434  	contentType, body, writeErrors := client.createMultipartBodyAndHeaderForAppBits(matchedResources, newResources, newResourcesLength)
   435  
   436  	// This request uses URL/Method instead of an internal RequestName to support
   437  	// the possibility of external bit services.
   438  	request, err := client.newHTTPRequest(requestOptions{
   439  		URL:    uploadLink.HREF,
   440  		Method: uploadLink.Method,
   441  		Body:   body,
   442  	})
   443  	if err != nil {
   444  		return Package{}, nil, err
   445  	}
   446  
   447  	request.Header.Set("Content-Type", contentType)
   448  	request.ContentLength = contentLength
   449  
   450  	return client.uploadAsynchronously(request, writeErrors)
   451  }