github.com/swisscom/cloudfoundry-cli@v7.1.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 "code.cloudfoundry.org/cli/resources" 16 ) 17 18 //go:generate counterfeiter io.Reader 19 20 // Package represents a Cloud Controller V3 Package. 21 type Package struct { 22 // CreatedAt is the time with zone when the object was created. 23 CreatedAt string 24 25 // DockerImage is the registry address of the docker image. 26 DockerImage string 27 28 // DockerPassword is the password for the docker image's registry. 29 DockerPassword string 30 31 // DockerUsername is the username for the docker image's registry. 32 DockerUsername string 33 34 // GUID is the unique identifier of the package. 35 GUID string 36 37 // Links are links to related resources. 38 Links APILinks 39 40 // Relationships are a list of relationships to other resources. 41 Relationships resources.Relationships 42 43 // State is the state of the package. 44 State constant.PackageState 45 46 // Type is the package type. 47 Type constant.PackageType 48 } 49 50 // MarshalJSON converts a Package into a Cloud Controller Package. 51 func (p Package) MarshalJSON() ([]byte, error) { 52 type ccPackageData struct { 53 Image string `json:"image,omitempty"` 54 Username string `json:"username,omitempty"` 55 Password string `json:"password,omitempty"` 56 } 57 var ccPackage struct { 58 GUID string `json:"guid,omitempty"` 59 CreatedAt string `json:"created_at,omitempty"` 60 Links APILinks `json:"links,omitempty"` 61 Relationships resources.Relationships `json:"relationships,omitempty"` 62 State constant.PackageState `json:"state,omitempty"` 63 Type constant.PackageType `json:"type,omitempty"` 64 Data *ccPackageData `json:"data,omitempty"` 65 } 66 67 ccPackage.GUID = p.GUID 68 ccPackage.CreatedAt = p.CreatedAt 69 ccPackage.Links = p.Links 70 ccPackage.Relationships = p.Relationships 71 ccPackage.State = p.State 72 ccPackage.Type = p.Type 73 if p.DockerImage != "" { 74 ccPackage.Data = &ccPackageData{ 75 Image: p.DockerImage, 76 Username: p.DockerUsername, 77 Password: p.DockerPassword, 78 } 79 } 80 81 return json.Marshal(ccPackage) 82 } 83 84 // UnmarshalJSON helps unmarshal a Cloud Controller Package response. 85 func (p *Package) UnmarshalJSON(data []byte) error { 86 var ccPackage struct { 87 GUID string `json:"guid,omitempty"` 88 CreatedAt string `json:"created_at,omitempty"` 89 Links APILinks `json:"links,omitempty"` 90 Relationships resources.Relationships `json:"relationships,omitempty"` 91 State constant.PackageState `json:"state,omitempty"` 92 Type constant.PackageType `json:"type,omitempty"` 93 Data struct { 94 Image string `json:"image"` 95 Username string `json:"username"` 96 Password string `json:"password"` 97 } `json:"data"` 98 } 99 err := cloudcontroller.DecodeJSON(data, &ccPackage) 100 if err != nil { 101 return err 102 } 103 104 p.GUID = ccPackage.GUID 105 p.CreatedAt = ccPackage.CreatedAt 106 p.Links = ccPackage.Links 107 p.Relationships = ccPackage.Relationships 108 p.State = ccPackage.State 109 p.Type = ccPackage.Type 110 p.DockerImage = ccPackage.Data.Image 111 p.DockerUsername = ccPackage.Data.Username 112 p.DockerPassword = ccPackage.Data.Password 113 114 return nil 115 } 116 117 // CreatePackage creates a package with the given settings, Type and the 118 // ApplicationRelationship must be set. 119 func (client *Client) CreatePackage(pkg Package) (Package, Warnings, error) { 120 var responseBody Package 121 122 _, warnings, err := client.MakeRequest(RequestParams{ 123 RequestName: internal.PostPackageRequest, 124 RequestBody: pkg, 125 ResponseBody: &responseBody, 126 }) 127 128 return responseBody, warnings, err 129 } 130 131 // GetPackage returns the package with the given GUID. 132 func (client *Client) GetPackage(packageGUID string) (Package, Warnings, error) { 133 var responseBody Package 134 135 _, warnings, err := client.MakeRequest(RequestParams{ 136 RequestName: internal.GetPackageRequest, 137 URIParams: internal.Params{"package_guid": packageGUID}, 138 ResponseBody: &responseBody, 139 }) 140 141 return responseBody, warnings, err 142 } 143 144 // GetPackages returns the list of packages. 145 func (client *Client) GetPackages(query ...Query) ([]Package, Warnings, error) { 146 var resources []Package 147 148 _, warnings, err := client.MakeListRequest(RequestParams{ 149 RequestName: internal.GetPackagesRequest, 150 Query: query, 151 ResponseBody: Package{}, 152 AppendToList: func(item interface{}) error { 153 resources = append(resources, item.(Package)) 154 return nil 155 }, 156 }) 157 158 return resources, warnings, err 159 } 160 161 // UploadBitsPackage uploads the newResources and a list of existing resources 162 // to the cloud controller. An updated package is returned. The function will 163 // act differently given the following Readers: 164 // - io.ReadSeeker: Will function properly on retry. 165 // - io.Reader: Will return a ccerror.PipeSeekError on retry. 166 // - nil: Will not add the "application" section to the request. The newResourcesLength is ignored in this case. 167 // 168 // Note: In order to determine if package creation is successful, poll the 169 // Package's state field for more information. 170 func (client *Client) UploadBitsPackage(pkg Package, matchedResources []Resource, newResources io.Reader, newResourcesLength int64) (Package, Warnings, error) { 171 if matchedResources == nil { 172 return Package{}, nil, ccerror.NilObjectError{Object: "matchedResources"} 173 } 174 175 if newResources == nil { 176 return client.uploadExistingResourcesOnly(pkg.GUID, matchedResources) 177 } 178 179 return client.uploadNewAndExistingResources(pkg.GUID, matchedResources, newResources, newResourcesLength) 180 } 181 182 // UploadPackage uploads a file to a given package's Upload resource. Note: 183 // fileToUpload is read entirely into memory prior to sending data to CC. 184 func (client *Client) UploadPackage(pkg Package, fileToUpload string) (Package, Warnings, error) { 185 body, contentType, err := client.createUploadBuffer(fileToUpload, "bits") 186 if err != nil { 187 return Package{}, nil, err 188 } 189 190 responsePackage := Package{} 191 _, warnings, err := client.MakeRequestSendRaw( 192 internal.PostPackageBitsRequest, 193 internal.Params{"package_guid": pkg.GUID}, 194 body.Bytes(), 195 contentType, 196 &responsePackage, 197 ) 198 199 return responsePackage, warnings, err 200 } 201 202 // CopyPackage copies a package from a source package to a destination package 203 // Note: source app guid is in URL; dest app guid is in body 204 func (client *Client) CopyPackage(sourcePkgGUID string, targetAppGUID string) (Package, Warnings, error) { 205 var targetPackage Package 206 207 _, warnings, err := client.MakeRequest(RequestParams{ 208 RequestName: internal.PostPackageRequest, 209 Query: []Query{{Key: SourceGUID, Values: []string{sourcePkgGUID}}}, 210 RequestBody: map[string]resources.Relationships{ 211 "relationships": { 212 constant.RelationshipTypeApplication: resources.Relationship{GUID: targetAppGUID}, 213 }, 214 }, 215 ResponseBody: &targetPackage, 216 }) 217 218 return targetPackage, warnings, err 219 } 220 221 func (client *Client) calculateAppBitsRequestSize(matchedResources []Resource, newResourcesLength int64) (int64, error) { 222 body := &bytes.Buffer{} 223 form := multipart.NewWriter(body) 224 225 jsonResources, err := json.Marshal(matchedResources) 226 if err != nil { 227 return 0, err 228 } 229 err = form.WriteField("resources", string(jsonResources)) 230 if err != nil { 231 return 0, err 232 } 233 _, err = form.CreateFormFile("bits", "package.zip") 234 if err != nil { 235 return 0, err 236 } 237 err = form.Close() 238 if err != nil { 239 return 0, err 240 } 241 242 return int64(body.Len()) + newResourcesLength, nil 243 } 244 245 func (client *Client) createMultipartBodyAndHeaderForAppBits(matchedResources []Resource, newResources io.Reader, newResourcesLength int64) (string, io.ReadSeeker, <-chan error) { 246 writerOutput, writerInput := cloudcontroller.NewPipeBomb() 247 form := multipart.NewWriter(writerInput) 248 249 writeErrors := make(chan error) 250 251 go func() { 252 defer close(writeErrors) 253 defer writerInput.Close() 254 255 jsonResources, err := json.Marshal(matchedResources) 256 if err != nil { 257 writeErrors <- err 258 return 259 } 260 261 err = form.WriteField("resources", string(jsonResources)) 262 if err != nil { 263 writeErrors <- err 264 return 265 } 266 267 writer, err := form.CreateFormFile("bits", "package.zip") 268 if err != nil { 269 writeErrors <- err 270 return 271 } 272 273 if newResourcesLength != 0 { 274 _, err = io.Copy(writer, newResources) 275 if err != nil { 276 writeErrors <- err 277 return 278 } 279 } 280 281 err = form.Close() 282 if err != nil { 283 writeErrors <- err 284 } 285 }() 286 287 return form.FormDataContentType(), writerOutput, writeErrors 288 } 289 290 func (*Client) createUploadBuffer(path string, paramName string) (bytes.Buffer, string, error) { 291 file, err := os.Open(path) 292 if err != nil { 293 return bytes.Buffer{}, "", err 294 } 295 defer file.Close() 296 297 body := bytes.Buffer{} 298 writer := multipart.NewWriter(&body) 299 part, err := writer.CreateFormFile(paramName, filepath.Base(path)) 300 if err != nil { 301 return bytes.Buffer{}, "", err 302 } 303 _, err = io.Copy(part, file) 304 if err != nil { 305 return bytes.Buffer{}, "", err 306 } 307 308 err = writer.Close() 309 310 return body, writer.FormDataContentType(), err 311 } 312 313 func (client *Client) uploadExistingResourcesOnly(packageGUID string, matchedResources []Resource) (Package, Warnings, error) { 314 jsonResources, err := json.Marshal(matchedResources) 315 if err != nil { 316 return Package{}, nil, err 317 } 318 319 body := bytes.NewBuffer(nil) 320 form := multipart.NewWriter(body) 321 err = form.WriteField("resources", string(jsonResources)) 322 if err != nil { 323 return Package{}, nil, err 324 } 325 326 err = form.Close() 327 if err != nil { 328 return Package{}, nil, err 329 } 330 331 responsePackage := Package{} 332 333 _, warnings, err := client.MakeRequestSendRaw( 334 internal.PostPackageBitsRequest, 335 internal.Params{"package_guid": packageGUID}, 336 body.Bytes(), 337 form.FormDataContentType(), 338 &responsePackage, 339 ) 340 341 return responsePackage, warnings, err 342 } 343 344 func (client *Client) uploadNewAndExistingResources(packageGUID string, matchedResources []Resource, newResources io.Reader, newResourcesLength int64) (Package, Warnings, error) { 345 contentLength, err := client.calculateAppBitsRequestSize(matchedResources, newResourcesLength) 346 if err != nil { 347 return Package{}, nil, err 348 } 349 350 contentType, body, writeErrors := client.createMultipartBodyAndHeaderForAppBits(matchedResources, newResources, newResourcesLength) 351 352 responseBody := Package{} 353 _, warnings, err := client.MakeRequestUploadAsync( 354 internal.PostPackageBitsRequest, 355 internal.Params{"package_guid": packageGUID}, 356 contentType, 357 body, 358 contentLength, 359 &responseBody, 360 writeErrors, 361 ) 362 return responseBody, warnings, err 363 }