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 }