github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/resources_test.go (about) 1 // Copyright 2017 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver_test 5 6 import ( 7 "encoding/json" 8 "fmt" 9 "io" 10 "net/http" 11 "net/http/httptest" 12 "strconv" 13 "strings" 14 "time" 15 16 charmresource "github.com/juju/charm/v12/resource" 17 "github.com/juju/collections/set" 18 "github.com/juju/errors" 19 "github.com/juju/names/v5" 20 "github.com/juju/testing" 21 jc "github.com/juju/testing/checkers" 22 gc "gopkg.in/check.v1" 23 24 api "github.com/juju/juju/api/client/resources" 25 "github.com/juju/juju/apiserver" 26 apiservererrors "github.com/juju/juju/apiserver/errors" 27 apiservertesting "github.com/juju/juju/apiserver/testing" 28 "github.com/juju/juju/core/resources" 29 resourcetesting "github.com/juju/juju/core/resources/testing" 30 "github.com/juju/juju/rpc/params" 31 "github.com/juju/juju/state" 32 ) 33 34 type ResourcesHandlerSuite struct { 35 testing.IsolationSuite 36 37 stateAuthErr error 38 backend *fakeBackend 39 username string 40 req *http.Request 41 recorder *httptest.ResponseRecorder 42 handler *apiserver.ResourcesHandler 43 } 44 45 var _ = gc.Suite(&ResourcesHandlerSuite{}) 46 47 func (s *ResourcesHandlerSuite) SetUpTest(c *gc.C) { 48 s.IsolationSuite.SetUpTest(c) 49 50 s.stateAuthErr = nil 51 s.backend = new(fakeBackend) 52 s.username = "youknowwho" 53 54 method := "..." 55 urlStr := "..." 56 body := strings.NewReader("...") 57 req, err := http.NewRequest(method, urlStr, body) 58 c.Assert(err, jc.ErrorIsNil) 59 s.req = req 60 s.recorder = httptest.NewRecorder() 61 s.handler = &apiserver.ResourcesHandler{ 62 StateAuthFunc: s.authState, 63 ChangeAllowedFunc: func(*http.Request) error { return nil }, 64 } 65 } 66 67 func (s *ResourcesHandlerSuite) authState(req *http.Request, tagKinds ...string) ( 68 apiserver.ResourcesBackend, state.PoolHelper, names.Tag, error, 69 ) { 70 if s.stateAuthErr != nil { 71 return nil, nil, nil, errors.Trace(s.stateAuthErr) 72 } 73 74 ph := apiservertesting.StubPoolHelper{StubRelease: func() bool { return false }} 75 tag := names.NewUserTag(s.username) 76 return s.backend, ph, tag, nil 77 } 78 79 func (s *ResourcesHandlerSuite) TestExpectedAuthTags(c *gc.C) { 80 expectedTags := set.NewStrings(names.UserTagKind, names.MachineTagKind, names.ControllerAgentTagKind, names.ApplicationTagKind) 81 82 s.handler.StateAuthFunc = func(req *http.Request, tagKinds ...string) (apiserver.ResourcesBackend, state.PoolHelper, names.Tag, error) { 83 gotTags := set.NewStrings(tagKinds...) 84 if gotTags.Difference(expectedTags).Size() != 0 || expectedTags.Difference(gotTags).Size() != 0 { 85 c.Fatalf("unexpected tag kinds %v", tagKinds) 86 return nil, nil, nil, errors.NotValidf("tag kinds %v", tagKinds) 87 } 88 ph := apiservertesting.StubPoolHelper{StubRelease: func() bool { return false }} 89 tag := names.NewUserTag(s.username) 90 return s.backend, ph, tag, nil 91 } 92 s.req.Method = "GET" 93 s.handler.ServeHTTP(s.recorder, s.req) 94 s.checkResp(c, http.StatusOK, "application/octet-stream", resourceBody) 95 } 96 97 func (s *ResourcesHandlerSuite) TestStateAuthFailure(c *gc.C) { 98 failure, expected := apiFailure("<failure>", "") 99 s.stateAuthErr = failure 100 101 s.handler.ServeHTTP(s.recorder, s.req) 102 103 s.checkResp(c, http.StatusInternalServerError, "application/json", expected) 104 } 105 106 func (s *ResourcesHandlerSuite) TestUnsupportedMethod(c *gc.C) { 107 s.req.Method = "POST" 108 109 s.handler.ServeHTTP(s.recorder, s.req) 110 111 _, expected := apiFailure(`unsupported method: "POST"`, params.CodeMethodNotAllowed) 112 s.checkResp(c, http.StatusMethodNotAllowed, "application/json", expected) 113 } 114 115 func (s *ResourcesHandlerSuite) TestGetSuccess(c *gc.C) { 116 s.req.Method = "GET" 117 s.handler.ServeHTTP(s.recorder, s.req) 118 s.checkResp(c, http.StatusOK, "application/octet-stream", resourceBody) 119 } 120 121 func (s *ResourcesHandlerSuite) TestPutSuccess(c *gc.C) { 122 uploadContent := "<some data>" 123 res, _ := newResource(c, "spam", "a-user", content) 124 stored, _ := newResource(c, "spam", "", "") 125 s.backend.ReturnGetResource = stored 126 s.backend.ReturnSetResource = res 127 128 req, _ := newUploadRequest(c, "spam", "a-application", uploadContent) 129 s.handler.ServeHTTP(s.recorder, req) 130 131 expected := mustMarshalJSON(¶ms.UploadResult{ 132 Resource: api.Resource2API(res), 133 }) 134 s.checkResp(c, http.StatusOK, "application/json", string(expected)) 135 } 136 137 func (s *ResourcesHandlerSuite) TestPutChangeBlocked(c *gc.C) { 138 uploadContent := "<some data>" 139 res, _ := newResource(c, "spam", "a-user", content) 140 stored, _ := newResource(c, "spam", "", "") 141 s.backend.ReturnGetResource = stored 142 s.backend.ReturnSetResource = res 143 144 expectedError := apiservererrors.OperationBlockedError("test block") 145 s.handler.ChangeAllowedFunc = func(*http.Request) error { 146 return expectedError 147 } 148 149 req, _ := newUploadRequest(c, "spam", "a-application", uploadContent) 150 s.handler.ServeHTTP(s.recorder, req) 151 152 expected := mustMarshalJSON(¶ms.ErrorResult{apiservererrors.ServerError(expectedError)}) 153 s.checkResp(c, http.StatusBadRequest, "application/json", string(expected)) 154 } 155 156 func (s *ResourcesHandlerSuite) TestPutSuccessDockerResource(c *gc.C) { 157 uploadContent := "<some data>" 158 res := newDockerResource(c, "spam", "a-user", content) 159 stored := newDockerResource(c, "spam", "", "") 160 s.backend.ReturnGetResource = stored 161 s.backend.ReturnSetResource = res 162 163 req, _ := newUploadRequest(c, "spam", "a-application", uploadContent) 164 s.handler.ServeHTTP(s.recorder, req) 165 166 expected := mustMarshalJSON(¶ms.UploadResult{ 167 Resource: api.Resource2API(res), 168 }) 169 s.checkResp(c, http.StatusOK, "application/json", string(expected)) 170 } 171 172 func (s *ResourcesHandlerSuite) TestPutExtensionMismatch(c *gc.C) { 173 content := "<some data>" 174 175 // newResource returns a resource with a Path = name + ".tgz" 176 res, _ := newResource(c, "spam", "a-user", content) 177 stored, _ := newResource(c, "spam", "", "") 178 s.backend.ReturnGetResource = stored 179 s.backend.ReturnSetResource = res 180 181 req, _ := newUploadRequest(c, "spam", "a-application", content) 182 req.Header.Set("Content-Disposition", "form-data; filename=different.ext") 183 s.handler.ServeHTTP(s.recorder, req) 184 185 _, expected := apiFailure(`incorrect extension on resource upload "different.ext", expected ".tgz"`, 186 "") 187 s.checkResp(c, http.StatusInternalServerError, "application/json", expected) 188 } 189 190 func (s *ResourcesHandlerSuite) TestPutWithPending(c *gc.C) { 191 uploadContent := "<some data>" 192 res, _ := newResource(c, "spam", "a-user", uploadContent) 193 res.PendingID = "some-unique-id" 194 stored, _ := newResource(c, "spam", "", "") 195 stored.PendingID = "some-unique-id" 196 s.backend.ReturnGetPendingResource = stored 197 s.backend.ReturnUpdatePendingResource = res 198 199 req, _ := newUploadRequest(c, "spam", "a-application", content) 200 req.URL.RawQuery += "&pendingid=some-unique-id" 201 s.handler.ServeHTTP(s.recorder, req) 202 203 expected := mustMarshalJSON(¶ms.UploadResult{ 204 Resource: api.Resource2API(res), 205 }) 206 s.checkResp(c, http.StatusOK, "application/json", string(expected)) 207 } 208 209 func (s *ResourcesHandlerSuite) TestPutSetResourceFailure(c *gc.C) { 210 content := "<some data>" 211 stored, _ := newResource(c, "spam", "", "") 212 s.backend.ReturnGetResource = stored 213 failure, expected := apiFailure("boom", "") 214 s.backend.SetResourceErr = failure 215 216 req, _ := newUploadRequest(c, "spam", "a-application", content) 217 s.handler.ServeHTTP(s.recorder, req) 218 s.checkResp(c, http.StatusInternalServerError, "application/json", expected) 219 } 220 221 func (s *ResourcesHandlerSuite) checkResp(c *gc.C, status int, ctype, body string) { 222 checkHTTPResp(c, s.recorder, status, ctype, body) 223 } 224 225 func checkHTTPResp(c *gc.C, recorder *httptest.ResponseRecorder, status int, ctype, body string) { 226 c.Assert(recorder.Code, gc.Equals, status) 227 hdr := recorder.Header() 228 c.Check(hdr.Get("Content-Type"), gc.Equals, ctype) 229 c.Check(hdr.Get("Content-Length"), gc.Equals, strconv.Itoa(len(body))) 230 231 actualBody, err := io.ReadAll(recorder.Body) 232 c.Assert(err, jc.ErrorIsNil) 233 c.Check(string(actualBody), gc.Equals, body) 234 } 235 236 type fakeBackend struct { 237 ReturnGetResource resources.Resource 238 ReturnGetPendingResource resources.Resource 239 ReturnSetResource resources.Resource 240 SetResourceErr error 241 ReturnUpdatePendingResource resources.Resource 242 } 243 244 const resourceBody = "body" 245 246 func (s *fakeBackend) OpenResource(application, name string) (resources.Resource, io.ReadCloser, error) { 247 res := resources.Resource{} 248 res.Size = int64(len(resourceBody)) 249 reader := io.NopCloser(strings.NewReader(resourceBody)) 250 return res, reader, nil 251 } 252 253 func (s *fakeBackend) GetResource(service, name string) (resources.Resource, error) { 254 return s.ReturnGetResource, nil 255 } 256 257 func (s *fakeBackend) GetPendingResource(service, name, pendingID string) (resources.Resource, error) { 258 return s.ReturnGetPendingResource, nil 259 } 260 261 func (s *fakeBackend) SetResource( 262 applicationID, userID string, 263 res charmresource.Resource, r io.Reader, 264 incrementCharmModifiedVersion state.IncrementCharmModifiedVersionType, 265 ) (resources.Resource, error) { 266 if s.SetResourceErr != nil { 267 return resources.Resource{}, s.SetResourceErr 268 } 269 return s.ReturnSetResource, nil 270 } 271 272 func (s *fakeBackend) UpdatePendingResource(applicationID, pendingID, userID string, res charmresource.Resource, r io.Reader) (resources.Resource, error) { 273 return s.ReturnUpdatePendingResource, nil 274 } 275 276 func newDockerResource(c *gc.C, name, username, data string) resources.Resource { 277 opened := resourcetesting.NewDockerResource(c, nil, name, "a-application", data) 278 res := opened.Resource 279 res.Username = username 280 if username == "" { 281 res.Timestamp = time.Time{} 282 } 283 return res 284 } 285 286 func newResource(c *gc.C, name, username, data string) (resources.Resource, params.Resource) { 287 opened := resourcetesting.NewResource(c, nil, name, "a-application", data) 288 res := opened.Resource 289 res.Username = username 290 if username == "" { 291 res.Timestamp = time.Time{} 292 } 293 294 apiRes := params.Resource{ 295 CharmResource: params.CharmResource{ 296 Name: name, 297 Description: name + " description", 298 Type: "file", 299 Path: res.Path, 300 Origin: "upload", 301 Revision: 0, 302 Fingerprint: res.Fingerprint.Bytes(), 303 Size: res.Size, 304 }, 305 ID: res.ID, 306 ApplicationID: res.ApplicationID, 307 Username: username, 308 Timestamp: res.Timestamp, 309 } 310 311 return res, apiRes 312 } 313 314 func newUploadRequest(c *gc.C, name, service, content string) (*http.Request, io.Reader) { 315 fp, err := charmresource.GenerateFingerprint(strings.NewReader(content)) 316 c.Assert(err, jc.ErrorIsNil) 317 318 method := "PUT" 319 urlStr := "https://api:17017/applications/%s/resources/%s" 320 urlStr += "?:application=%s&:resource=%s" // ...added by the mux. 321 urlStr = fmt.Sprintf(urlStr, service, name, service, name) 322 body := strings.NewReader(content) 323 req, err := http.NewRequest(method, urlStr, body) 324 c.Assert(err, jc.ErrorIsNil) 325 326 req.Header.Set("Content-Type", "application/octet-stream") 327 req.Header.Set("Content-Length", fmt.Sprint(len(content))) 328 req.Header.Set("Content-SHA384", fp.String()) 329 req.Header.Set("Content-Disposition", "form-data; filename="+name+".tgz") 330 331 return req, body 332 } 333 334 func apiFailure(msg, code string) (error, string) { 335 failure := errors.New(msg) 336 data := mustMarshalJSON(params.ErrorResult{ 337 Error: ¶ms.Error{ 338 Message: msg, 339 Code: code, 340 }, 341 }) 342 return failure, string(data) 343 } 344 345 func mustMarshalJSON(v interface{}) []byte { 346 data, err := json.Marshal(v) 347 if err != nil { 348 panic(err) 349 } 350 return data 351 }