github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/resource/api/client/client.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package client 5 6 import ( 7 "io" 8 "net/http" 9 "strings" 10 11 "github.com/juju/errors" 12 charmresource "gopkg.in/juju/charm.v6/resource" 13 "gopkg.in/juju/names.v2" 14 "gopkg.in/macaroon.v2-unstable" 15 16 "github.com/juju/juju/apiserver/common" 17 "github.com/juju/juju/apiserver/params" 18 "github.com/juju/juju/charmstore" 19 "github.com/juju/juju/resource" 20 "github.com/juju/juju/resource/api" 21 ) 22 23 // TODO(ericsnow) Move FacadeCaller to a component-central package. 24 25 // FacadeCaller has the api/base.FacadeCaller methods needed for the component. 26 type FacadeCaller interface { 27 FacadeCall(request string, params, response interface{}) error 28 } 29 30 // Doer 31 type Doer interface { 32 Do(req *http.Request, body io.ReadSeeker, resp interface{}) error 33 } 34 35 // Client is the public client for the resources API facade. 36 type Client struct { 37 FacadeCaller 38 io.Closer 39 doer Doer 40 } 41 42 // NewClient returns a new Client for the given raw API caller. 43 func NewClient(caller FacadeCaller, doer Doer, closer io.Closer) *Client { 44 return &Client{ 45 FacadeCaller: caller, 46 Closer: closer, 47 doer: doer, 48 } 49 } 50 51 // ListResources calls the ListResources API server method with 52 // the given application names. 53 func (c Client) ListResources(applications []string) ([]resource.ApplicationResources, error) { 54 args, err := newListResourcesArgs(applications) 55 if err != nil { 56 return nil, errors.Trace(err) 57 } 58 59 var apiResults params.ResourcesResults 60 if err := c.FacadeCall("ListResources", &args, &apiResults); err != nil { 61 return nil, errors.Trace(err) 62 } 63 64 if len(apiResults.Results) != len(applications) { 65 // We don't bother returning the results we *did* get since 66 // something bad happened on the server. 67 return nil, errors.Errorf("got invalid data from server (expected %d results, got %d)", len(applications), len(apiResults.Results)) 68 } 69 70 var errs []error 71 results := make([]resource.ApplicationResources, len(applications)) 72 for i := range applications { 73 apiResult := apiResults.Results[i] 74 75 result, err := api.APIResult2ApplicationResources(apiResult) 76 if err != nil { 77 errs = append(errs, errors.Trace(err)) 78 } 79 results[i] = result 80 } 81 if err := resolveErrors(errs); err != nil { 82 return nil, errors.Trace(err) 83 } 84 85 return results, nil 86 } 87 88 // newListResourcesArgs returns the arguments for the ListResources endpoint. 89 func newListResourcesArgs(applications []string) (params.ListResourcesArgs, error) { 90 var args params.ListResourcesArgs 91 var errs []error 92 for _, application := range applications { 93 if !names.IsValidApplication(application) { 94 err := errors.Errorf("invalid application %q", application) 95 errs = append(errs, err) 96 continue 97 } 98 args.Entities = append(args.Entities, params.Entity{ 99 Tag: names.NewApplicationTag(application).String(), 100 }) 101 } 102 if err := resolveErrors(errs); err != nil { 103 return args, errors.Trace(err) 104 } 105 return args, nil 106 } 107 108 // Upload sends the provided resource blob up to Juju. 109 func (c Client) Upload(application, name, filename string, reader io.ReadSeeker) error { 110 uReq, err := api.NewUploadRequest(application, name, filename, reader) 111 if err != nil { 112 return errors.Trace(err) 113 } 114 req, err := uReq.HTTPRequest() 115 if err != nil { 116 return errors.Trace(err) 117 } 118 119 var response params.UploadResult // ignored 120 if err := c.doer.Do(req, reader, &response); err != nil { 121 return errors.Trace(err) 122 } 123 124 return nil 125 } 126 127 // AddPendingResourcesArgs holds the arguments to AddPendingResources(). 128 type AddPendingResourcesArgs struct { 129 // ApplicationID identifies the application being deployed. 130 ApplicationID string 131 132 // CharmID identifies the application's charm. 133 CharmID charmstore.CharmID 134 135 // CharmStoreMacaroon is the macaroon to use for the charm when 136 // interacting with the charm store. 137 CharmStoreMacaroon *macaroon.Macaroon 138 139 // Resources holds the charm store info for each of the resources 140 // that should be added/updated on the controller. 141 Resources []charmresource.Resource 142 } 143 144 // AddPendingResources sends the provided resource info up to Juju 145 // without making it available yet. 146 func (c Client) AddPendingResources(args AddPendingResourcesArgs) (pendingIDs []string, err error) { 147 apiArgs, err := newAddPendingResourcesArgs(args.ApplicationID, args.CharmID, args.CharmStoreMacaroon, args.Resources) 148 if err != nil { 149 return nil, errors.Trace(err) 150 } 151 152 var result params.AddPendingResourcesResult 153 if err := c.FacadeCall("AddPendingResources", &apiArgs, &result); err != nil { 154 return nil, errors.Trace(err) 155 } 156 if result.Error != nil { 157 err := common.RestoreError(result.Error) 158 return nil, errors.Trace(err) 159 } 160 161 if len(result.PendingIDs) != len(args.Resources) { 162 return nil, errors.Errorf("bad data from server: expected %d IDs, got %d", len(args.Resources), len(result.PendingIDs)) 163 } 164 for i, id := range result.PendingIDs { 165 if id == "" { 166 return nil, errors.Errorf("bad data from server: got an empty ID for resource %q", args.Resources[i].Name) 167 } 168 // TODO(ericsnow) Do other validation? 169 } 170 171 return result.PendingIDs, nil 172 } 173 174 // newAddPendingResourcesArgs returns the arguments for the 175 // AddPendingResources API endpoint. 176 func newAddPendingResourcesArgs(applicationID string, chID charmstore.CharmID, csMac *macaroon.Macaroon, resources []charmresource.Resource) (params.AddPendingResourcesArgs, error) { 177 var args params.AddPendingResourcesArgs 178 179 if !names.IsValidApplication(applicationID) { 180 return args, errors.Errorf("invalid application %q", applicationID) 181 } 182 tag := names.NewApplicationTag(applicationID).String() 183 184 var apiResources []params.CharmResource 185 for _, res := range resources { 186 if err := res.Validate(); err != nil { 187 return args, errors.Trace(err) 188 } 189 apiRes := api.CharmResource2API(res) 190 apiResources = append(apiResources, apiRes) 191 } 192 args.Tag = tag 193 args.Resources = apiResources 194 if chID.URL != nil { 195 args.URL = chID.URL.String() 196 args.Channel = string(chID.Channel) 197 args.CharmStoreMacaroon = csMac 198 } 199 return args, nil 200 } 201 202 // UploadPendingResource sends the provided resource blob up to Juju 203 // and makes it available. 204 func (c Client) UploadPendingResource(applicationID string, res charmresource.Resource, filename string, reader io.ReadSeeker) (pendingID string, err error) { 205 ids, err := c.AddPendingResources(AddPendingResourcesArgs{ 206 ApplicationID: applicationID, 207 Resources: []charmresource.Resource{res}, 208 }) 209 if err != nil { 210 return "", errors.Trace(err) 211 } 212 pendingID = ids[0] 213 214 if reader != nil { 215 uReq, err := api.NewUploadRequest(applicationID, res.Name, filename, reader) 216 if err != nil { 217 return "", errors.Trace(err) 218 } 219 uReq.PendingID = pendingID 220 req, err := uReq.HTTPRequest() 221 if err != nil { 222 return "", errors.Trace(err) 223 } 224 225 var response params.UploadResult // ignored 226 if err := c.doer.Do(req, reader, &response); err != nil { 227 return "", errors.Trace(err) 228 } 229 } 230 231 return pendingID, nil 232 } 233 234 func resolveErrors(errs []error) error { 235 switch len(errs) { 236 case 0: 237 return nil 238 case 1: 239 return errs[0] 240 default: 241 msgs := make([]string, len(errs)) 242 for i, err := range errs { 243 msgs[i] = err.Error() 244 } 245 return errors.New(strings.Join(msgs, "\n")) 246 } 247 }