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 }