Skip to content

Commit

Permalink
Merge pull request cloudfoundry-community#258 from cloudfoundry-commu…
Browse files Browse the repository at this point in the history
…nity/fix-metadata

Fix v3 metadata handling
  • Loading branch information
ArthurHlt authored May 15, 2020
2 parents 54e5338 + 7703895 commit c00221c
Show file tree
Hide file tree
Showing 5 changed files with 151 additions and 94 deletions.
26 changes: 14 additions & 12 deletions cloudfoundry/managers/bits/bitsmanager.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,9 @@ func NewBitsManager(clientV2 *ccv2.Client, clientV3 *ccv3.Client, rawClient *raw
// CopyApp - Copy one app to another by using only api
func (m BitsManager) CopyApp(origAppGuid string, newAppGuid string) error {
path := fmt.Sprintf("/v2/apps/%s/copy_bits", newAppGuid)
data := bytes.NewReader([]byte(fmt.Sprintf(`{"source_app_guid":"%s"}`, origAppGuid)))
req, err := m.rawClient.NewRequest("POST", path, ioutil.NopCloser(data))
data := []byte(fmt.Sprintf(`{"source_app_guid":"%s"}`, origAppGuid))

req, err := m.rawClient.NewRequest("POST", path, data)
if err != nil {
return err
}
Expand Down Expand Up @@ -112,16 +113,17 @@ func (m BitsManager) UploadBuildpack(buildpackGUID string, bpPath string) error
}
mpw.Close()
}()
request, err := m.rawClient.NewRequest("PUT", apiURL, nil)

req, err := m.rawClient.NewRequest("PUT", apiURL, nil)
if err != nil {
return err
}
contentType := fmt.Sprintf("multipart/form-data; boundary=%s", mpw.Boundary())
request.Header.Set("Content-Type", contentType)
request.ContentLength = m.predictPartBuildpack(baseName, fileSize, mpw.Boundary())
request.Body = r
req.Header.Set("Content-Type", contentType)
req.ContentLength = m.predictPartBuildpack(baseName, fileSize, mpw.Boundary())
req.Body = r

_, err = m.rawClient.Do(request)
_, err = m.rawClient.Do(req)
if err != nil {
panic(err)
}
Expand Down Expand Up @@ -170,16 +172,16 @@ func (m BitsManager) UploadApp(appGUID string, path string) error {
}
mpw.Close()
}()
request, err := m.rawClient.NewRequest("PUT", apiURL, nil)
req, err := m.rawClient.NewRequest("PUT", apiURL, nil)
if err != nil {
return err
}
contentType := fmt.Sprintf("multipart/form-data; boundary=%s", mpw.Boundary())
request.Header.Set("Content-Type", contentType)
request.ContentLength = m.predictPartApp(fileSize, mpw.Boundary())
request.Body = r
req.Header.Set("Content-Type", contentType)
req.ContentLength = m.predictPartApp(fileSize, mpw.Boundary())
req.Body = r

_, err = m.rawClient.Do(request)
_, err = m.rawClient.Do(req)
if err != nil {
panic(err)
}
Expand Down
24 changes: 18 additions & 6 deletions cloudfoundry/managers/raw/rawclient.go
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package raw

import (
"bytes"
"code.cloudfoundry.org/cli/api/cloudcontroller"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccv3"
"crypto/tls"
"fmt"
"io"
"net"
"net/http"
Expand Down Expand Up @@ -64,15 +66,25 @@ func NewRawClient(config RawClientConfig, wrappers ...ccv3.ConnectionWrapper) *R
}

// Do - Do the request with given http client and wrappers
func (c RawClient) Do(req *http.Request) (*http.Response, error) {
func (c RawClient) Do(req *cloudcontroller.Request) (*http.Response, error) {
resp := &cloudcontroller.Response{}
err := c.connection.Make(&cloudcontroller.Request{
Request: req,
}, resp)
err := c.connection.Make(req, resp)
return resp.HTTPResponse, err
}

// NewRequest - Create a new request with setting api endpoint to the path
func (c RawClient) NewRequest(method, path string, body io.ReadCloser) (*http.Request, error) {
return http.NewRequest(method, c.apiEndpoint+path, body)
func (c RawClient) NewRequest(method string, path string, data []byte) (*cloudcontroller.Request, error) {
var reader io.ReadSeeker
if data != nil {
reader = bytes.NewReader(data)
}

url := fmt.Sprintf("%s%s", c.apiEndpoint, path)
baseReq, err := http.NewRequest(method, url, reader)
if err != nil {
return nil, err
}

cfReq := cloudcontroller.NewRequest(baseReq, reader)
return cfReq, nil
}
166 changes: 95 additions & 71 deletions cloudfoundry/metadata.go
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package cloudfoundry

import (
"bytes"
"code.cloudfoundry.org/cli/api/cloudcontroller/ccerror"
"encoding/json"
"fmt"
Expand All @@ -18,20 +17,21 @@ type MetadataRequest struct {
}

type Metadata struct {
Labels map[string]string `json:"labels,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
Labels map[string]*string `json:"labels,omitempty"`
Annotations map[string]*string `json:"annotations,omitempty"`
}

const (
labelsKey = "labels"
annotationsKey = "annotations"

orgMetadata metadataType = "organizations"
spaceMetadata metadataType = "spaces"
buildpackMetadata metadataType = "buildpacks"
appMetadata metadataType = "apps"
stackMetadata metadataType = "stacks"
segmentMetadata metadataType = "isolation_segments"
orgMetadata metadataType = "organizations"
spaceMetadata metadataType = "spaces"
buildpackMetadata metadataType = "buildpacks"
appMetadata metadataType = "apps"
stackMetadata metadataType = "stacks"
segmentMetadata metadataType = "isolation_segments"
serviceBrokerMetadata metadataType = "service_brokers"
)

func labelsSchema() *schema.Schema {
Expand All @@ -51,83 +51,75 @@ func annotationsSchema() *schema.Schema {
}

func metadataCreate(t metadataType, d *schema.ResourceData, meta interface{}) error {
if !isMetadataApiCompat(meta) {
if !isMetadataAPICompat(t, meta) {
return nil
}
return metadataUpdate(t, d, meta)
}

func isMetadataApiCompat(meta interface{}) bool {
func isMetadataAPICompat(t metadataType, meta interface{}) bool {
apiVersion := meta.(*managers.Session).ClientV3.CloudControllerAPIVersion()
v, err := semver.Parse(apiVersion)
if err != nil {
// in case version is incorrect
// we set true anyway, it will only do the calls to api but not fail if endpoint is not found in crud
return true
}

expectedRange := semver.MustParseRange(">=3.63.0")
if t == serviceBrokerMetadata {
expectedRange = semver.MustParseRange(">=3.71.0")
}
return expectedRange(v)
}

func metadataUpdate(t metadataType, d *schema.ResourceData, meta interface{}) error {
if !isMetadataApiCompat(meta) {
return nil
}
metadata := resourceMetadataToMetadata(d)
if len(metadata.Labels) == 0 && len(metadata.Annotations) == 0 &&
!d.HasChange(labelsKey) && !d.HasChange(annotationsKey) {
return nil
}

oldMetadata, err := metadataRetrieve(t, d, meta)
if err != nil {
return err
func resourceToMetadata(d *schema.ResourceData) Metadata {
return Metadata{
Labels: resourceToPayload(d, labelsKey),
Annotations: resourceToPayload(d, annotationsKey),
}
}

metadata = mergeMetadata(oldMetadata, metadata)
// resourceToPayload - create metadata update payload from resource state
//
// note: we *should* construct payload in a way where only new/changed value
// are present, but re-giving existing values clarifies the code
//
// 1. construct payload as requested by "new" value
// 2. find delete keys and create { "key" : nil } in payload
// ie: keys existing in "old" but not in "new"
func resourceToPayload(d *schema.ResourceData, key string) map[string]*string {
res := map[string]*string{}
old, new := d.GetChange(key)
oldV := mapInterfaceToMapString(old.(map[string]interface{}))
newV := mapInterfaceToMapString(new.(map[string]interface{}))

b, err := json.Marshal(metadata)
if err != nil {
return err
}
client := meta.(*managers.Session).RawClient
req, err := client.NewRequest("PUT", pathMetadata(t, d), ioutil.NopCloser(bytes.NewBuffer(b)))
if err != nil {
return err
// 1.
for key, val := range newV {
res[key] = &val
}
resp, err := client.Do(req)
if err != nil {
return err
}
defer func() {
err := resp.Body.Close()
if err != nil {
panic(err)
}
}()
if resp.StatusCode != 200 && resp.StatusCode != 404 {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return ccerror.RawHTTPStatusError{
StatusCode: resp.StatusCode,
RawResponse: b,

// 2.
for key := range oldV {
if _, ok := newV[key]; !ok {
res[key] = nil
}
}
return nil

return res
}

func metadataRead(t metadataType, d *schema.ResourceData, meta interface{}, forceRead bool) error {
if !isMetadataApiCompat(meta) {
if !isMetadataAPICompat(t, meta) {
return nil
}
_, hasLabels := d.GetOk(labelsKey)
_, hasAnnotations := d.GetOk(annotationsKey)
if !hasAnnotations && !hasLabels && !forceRead && !IsImportState(d) {
return nil
}
metadata := resourceMetadataToMetadata(d)

metadata := resourceToMetadata(d)
oldMetadata, err := metadataRetrieve(t, d, meta)
if err != nil {
return err
Expand Down Expand Up @@ -166,29 +158,52 @@ func metadataRead(t metadataType, d *schema.ResourceData, meta interface{}, forc
return nil
}

func resourceMetadataToMetadata(d *schema.ResourceData) Metadata {
labels := mapInterfaceToMapString(d.Get(labelsKey).(map[string]interface{}))
annotations := mapInterfaceToMapString(d.Get(annotationsKey).(map[string]interface{}))
return Metadata{
Labels: labels,
Annotations: annotations,
func metadataUpdate(t metadataType, d *schema.ResourceData, meta interface{}) error {
if !isMetadataAPICompat(t, meta) {
return nil
}
}

func mergeMetadata(o Metadata, n Metadata) Metadata {
labels := o.Labels
for k, v := range n.Labels {
labels[k] = v
metadata := resourceToMetadata(d)
if len(metadata.Labels) == 0 && len(metadata.Annotations) == 0 {
return nil
}

annotations := o.Annotations
for k, v := range n.Annotations {
annotations[k] = v
b, err := json.Marshal(MetadataRequest{Metadata: metadata})
if err != nil {
return err
}
return Metadata{
Labels: labels,
Annotations: annotations,

client := meta.(*managers.Session).RawClient
endpoint := pathMetadata(t, d)
req, err := client.NewRequest("PATCH", endpoint, b)
if err != nil {
return err
}
req.Header.Add("Content-Type", "application/json")

resp, err := client.Do(req)
if err != nil {
return err
}

defer func() {
err := resp.Body.Close()
if err != nil {
panic(err)
}
}()

if resp.StatusCode != 200 && resp.StatusCode != 404 && resp.StatusCode != 202 {
b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return err
}
return ccerror.RawHTTPStatusError{
StatusCode: resp.StatusCode,
RawResponse: b,
}
}
return nil
}

func metadataRetrieve(t metadataType, d *schema.ResourceData, meta interface{}) (Metadata, error) {
Expand All @@ -197,20 +212,24 @@ func metadataRetrieve(t metadataType, d *schema.ResourceData, meta interface{})
if err != nil {
return Metadata{}, err
}

resp, err := client.Do(req)
if err != nil {
return Metadata{}, err
}

defer func() {
err := resp.Body.Close()
if err != nil {
panic(err)
}
}()

b, err := ioutil.ReadAll(resp.Body)
if err != nil {
return Metadata{}, err
}

if resp.StatusCode != 200 {
if resp.StatusCode == 404 {
return Metadata{}, nil
Expand All @@ -220,6 +239,7 @@ func metadataRetrieve(t metadataType, d *schema.ResourceData, meta interface{})
RawResponse: b,
}
}

var metadataReq MetadataRequest
err = json.Unmarshal(b, &metadataReq)
if err != nil {
Expand All @@ -231,3 +251,7 @@ func metadataRetrieve(t metadataType, d *schema.ResourceData, meta interface{})
func pathMetadata(t metadataType, d *schema.ResourceData) string {
return fmt.Sprintf("/v3/%s/%s", t, d.Id())
}

// Local Variables:
// ispell-local-dictionary: "american"
// End:
Loading

0 comments on commit c00221c

Please sign in to comment.