github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/resources_mig_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 "bytes" 8 "encoding/json" 9 "fmt" 10 "io" 11 "net/http" 12 "net/url" 13 "strings" 14 "time" 15 16 charmresource "github.com/juju/charm/v12/resource" 17 jc "github.com/juju/testing/checkers" 18 "go.uber.org/mock/gomock" 19 gc "gopkg.in/check.v1" 20 21 "github.com/juju/juju/apiserver" 22 "github.com/juju/juju/apiserver/mocks" 23 apitesting "github.com/juju/juju/apiserver/testing" 24 "github.com/juju/juju/core/resources" 25 "github.com/juju/juju/rpc/params" 26 "github.com/juju/juju/state" 27 "github.com/juju/juju/testing/factory" 28 ) 29 30 type resourcesUploadSuite struct { 31 apiserverBaseSuite 32 appName string 33 unit *state.Unit 34 importingState *state.State 35 importingModel *state.Model 36 } 37 38 var _ = gc.Suite(&resourcesUploadSuite{}) 39 40 func (s *resourcesUploadSuite) SetUpTest(c *gc.C) { 41 s.apiserverBaseSuite.SetUpTest(c) 42 43 // Create an importing model to work with. 44 var err error 45 s.importingState = s.Factory.MakeModel(c, nil) 46 s.AddCleanup(func(*gc.C) { s.importingState.Close() }) 47 s.importingModel, err = s.importingState.Model() 48 c.Assert(err, jc.ErrorIsNil) 49 50 newFactory := factory.NewFactory(s.importingState, s.StatePool) 51 app := newFactory.MakeApplication(c, nil) 52 s.appName = app.Name() 53 54 s.unit = newFactory.MakeUnit(c, &factory.UnitParams{ 55 Application: app, 56 }) 57 58 err = s.importingModel.SetMigrationMode(state.MigrationModeImporting) 59 c.Assert(err, jc.ErrorIsNil) 60 } 61 62 func (s *resourcesUploadSuite) sendHTTPRequest(c *gc.C, p apitesting.HTTPRequestParams) *http.Response { 63 p.ExtraHeaders = map[string]string{ 64 params.MigrationModelHTTPHeader: s.importingModel.UUID(), 65 } 66 return s.apiserverBaseSuite.sendHTTPRequest(c, p) 67 } 68 69 func (s *resourcesUploadSuite) TestServedSecurely(c *gc.C) { 70 url := s.resourcesURL("") 71 url.Scheme = "http" 72 apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ 73 Method: "GET", 74 URL: url.String(), 75 ExpectStatus: http.StatusBadRequest, 76 }) 77 } 78 79 func (s *resourcesUploadSuite) TestGETUnsupported(c *gc.C) { 80 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: s.resourcesURI("")}) 81 s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "GET"`) 82 } 83 84 func (s *resourcesUploadSuite) TestPUTUnsupported(c *gc.C) { 85 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "PUT", URL: s.resourcesURI("")}) 86 s.assertErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`) 87 } 88 89 func (s *resourcesUploadSuite) TestPOSTRequiresAuth(c *gc.C) { 90 resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.resourcesURI("")}) 91 body := apitesting.AssertResponse(c, resp, http.StatusUnauthorized, "text/plain; charset=utf-8") 92 c.Assert(string(body), gc.Equals, "authentication failed: no credentials provided\n") 93 } 94 95 func (s *resourcesUploadSuite) TestPOSTRequiresUserAuth(c *gc.C) { 96 // Add a machine and try to login. 97 machine, password := s.Factory.MakeMachineReturningPassword(c, &factory.MachineParams{ 98 Nonce: "noncy", 99 }) 100 resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ 101 Tag: machine.Tag().String(), 102 Password: password, 103 Method: "POST", 104 URL: s.resourcesURI(""), 105 Nonce: "noncy", 106 ContentType: "foo/bar", 107 }) 108 body := apitesting.AssertResponse(c, resp, http.StatusForbidden, "text/plain; charset=utf-8") 109 c.Assert(string(body), gc.Equals, "authorization failed: machine 0 is not a user\n") 110 111 // Now try a user login. 112 resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.resourcesURI("")}) 113 s.assertErrorResponse(c, resp, http.StatusBadRequest, "missing application/unit") 114 } 115 116 func (s *resourcesUploadSuite) TestRejectsInvalidModel(c *gc.C) { 117 params := apitesting.HTTPRequestParams{ 118 Method: "POST", 119 URL: s.resourcesURI(""), 120 ExtraHeaders: map[string]string{ 121 params.MigrationModelHTTPHeader: "dead-beef-123456", 122 }, 123 } 124 resp := s.apiserverBaseSuite.sendHTTPRequest(c, params) 125 s.assertErrorResponse(c, resp, http.StatusNotFound, `.*unknown model: "dead-beef-123456"`) 126 } 127 128 const content = "stuff" 129 130 func (s *resourcesUploadSuite) makeUploadArgs(c *gc.C) url.Values { 131 return s.makeResourceUploadArgs(c, "file") 132 } 133 134 func (s *resourcesUploadSuite) makeDockerUploadArgs(c *gc.C) url.Values { 135 result := s.makeResourceUploadArgs(c, "oci-image") 136 result.Del("path") 137 return result 138 } 139 140 func (s *resourcesUploadSuite) makeResourceUploadArgs(c *gc.C, resType string) url.Values { 141 fp, err := charmresource.GenerateFingerprint(strings.NewReader(content)) 142 c.Assert(err, jc.ErrorIsNil) 143 q := make(url.Values) 144 q.Add("application", s.appName) 145 q.Add("user", "napoleon") 146 q.Add("name", "bin") 147 q.Add("path", "blob.zip") 148 q.Add("description", "hmm") 149 q.Add("type", resType) 150 q.Add("origin", "store") 151 q.Add("revision", "3") 152 q.Add("size", fmt.Sprint(len(content))) 153 q.Add("fingerprint", fp.Hex()) 154 q.Add("timestamp", fmt.Sprint(time.Now().UnixNano())) 155 return q 156 } 157 158 func (s *resourcesUploadSuite) TestUpload(c *gc.C) { 159 outResp := s.uploadAppResource(c, nil) 160 c.Check(outResp.ID, gc.Not(gc.Equals), "") 161 c.Check(outResp.Timestamp.IsZero(), jc.IsFalse) 162 163 rSt := s.importingState.Resources() 164 res, reader, err := rSt.OpenResource(s.appName, "bin") 165 c.Assert(err, jc.ErrorIsNil) 166 defer reader.Close() 167 readContent, err := io.ReadAll(reader) 168 c.Assert(err, jc.ErrorIsNil) 169 c.Assert(string(readContent), gc.Equals, content) 170 c.Assert(res.ID, gc.Equals, outResp.ID) 171 } 172 173 func (s *resourcesUploadSuite) TestUnitUpload(c *gc.C) { 174 // Upload application resource first. A unit resource can't be 175 // uploaded without the application resource being there first. 176 s.uploadAppResource(c, nil) 177 178 q := s.makeUploadArgs(c) 179 q.Del("application") 180 q.Set("unit", s.unit.Name()) 181 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 182 Method: "POST", 183 URL: s.resourcesURI(q.Encode()), 184 ContentType: "application/octet-stream", 185 Body: strings.NewReader(content), 186 }) 187 outResp := s.assertResponse(c, resp, http.StatusOK) 188 c.Check(outResp.ID, gc.Not(gc.Equals), "") 189 c.Check(outResp.Timestamp.IsZero(), jc.IsFalse) 190 } 191 192 func (s *resourcesUploadSuite) TestPlaceholder(c *gc.C) { 193 query := s.makeUploadArgs(c) 194 query.Del("timestamp") // No timestamp means placeholder 195 outResp := s.uploadAppResource(c, &query) 196 c.Check(outResp.ID, gc.Not(gc.Equals), "") 197 c.Check(outResp.Timestamp.IsZero(), jc.IsTrue) 198 199 rSt := s.importingState.Resources() 200 res, err := rSt.GetResource(s.appName, "bin") 201 c.Assert(err, jc.ErrorIsNil) 202 c.Check(res.IsPlaceholder(), jc.IsTrue) 203 c.Check(res.ApplicationID, gc.Equals, s.appName) 204 c.Check(res.Name, gc.Equals, "bin") 205 c.Check(res.Size, gc.Equals, int64(len(content))) 206 } 207 208 func (s *resourcesUploadSuite) uploadAppResource(c *gc.C, query *url.Values) params.ResourceUploadResult { 209 if query == nil { 210 q := s.makeUploadArgs(c) 211 query = &q 212 } 213 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 214 Method: "POST", 215 URL: s.resourcesURI(query.Encode()), 216 ContentType: "application/octet-stream", 217 Body: strings.NewReader(content), 218 }) 219 return s.assertResponse(c, resp, http.StatusOK) 220 } 221 222 func (s *resourcesUploadSuite) TestArgValidation(c *gc.C) { 223 checkBadRequest := func(q url.Values, expected string) { 224 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 225 Method: "POST", 226 URL: s.resourcesURI(q.Encode()), 227 }) 228 s.assertErrorResponse(c, resp, http.StatusBadRequest, expected) 229 } 230 231 q := s.makeUploadArgs(c) 232 q.Del("application") 233 checkBadRequest(q, "missing application/unit") 234 235 q = s.makeUploadArgs(c) 236 q.Set("unit", "some/0") 237 checkBadRequest(q, "application and unit can't be set at the same time") 238 239 q = s.makeUploadArgs(c) 240 q.Del("name") 241 checkBadRequest(q, "missing name") 242 243 q = s.makeUploadArgs(c) 244 q.Del("path") 245 checkBadRequest(q, "missing path") 246 247 q = s.makeUploadArgs(c) 248 q.Set("type", "fooo") 249 checkBadRequest(q, "invalid type") 250 251 q = s.makeUploadArgs(c) 252 q.Set("origin", "fooo") 253 checkBadRequest(q, "invalid origin") 254 255 q = s.makeUploadArgs(c) 256 q.Set("revision", "fooo") 257 checkBadRequest(q, "invalid revision") 258 259 q = s.makeUploadArgs(c) 260 q.Set("size", "fooo") 261 checkBadRequest(q, "invalid size") 262 263 q = s.makeUploadArgs(c) 264 q.Set("fingerprint", "zzz") 265 checkBadRequest(q, "invalid fingerprint") 266 } 267 268 func (s *resourcesUploadSuite) TestArgValidationCAASModel(c *gc.C) { 269 content := `{"ImageName": "image-name", "Username": "fred", "Password":"secret"}` 270 checkRequest := func(q url.Values) { 271 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 272 Method: "POST", 273 URL: s.resourcesURI(q.Encode()), 274 Body: bytes.NewReader([]byte(content)), 275 }) 276 s.assertResponse(c, resp, http.StatusOK) 277 } 278 279 q := s.makeDockerUploadArgs(c) 280 checkRequest(q) 281 } 282 283 func (s *resourcesUploadSuite) TestFailsWhenModelNotImporting(c *gc.C) { 284 err := s.importingModel.SetMigrationMode(state.MigrationModeNone) 285 c.Assert(err, jc.ErrorIsNil) 286 287 q := s.makeUploadArgs(c) 288 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 289 Method: "POST", 290 URL: s.resourcesURI(q.Encode()), 291 ContentType: "application/octet-stream", 292 Body: strings.NewReader(content), 293 }) 294 s.assertResponse(c, resp, http.StatusBadRequest) 295 } 296 297 func (s *resourcesUploadSuite) resourcesURI(query string) string { 298 if query != "" && query[0] == '?' { 299 query = query[1:] 300 } 301 return s.resourcesURL(query).String() 302 } 303 304 func (s *resourcesUploadSuite) resourcesURL(query string) *url.URL { 305 url := s.URL("/migrate/resources", nil) 306 url.RawQuery = query 307 return url 308 } 309 310 func (s *resourcesUploadSuite) assertErrorResponse(c *gc.C, resp *http.Response, expStatus int, expError string) { 311 outResp := s.assertResponse(c, resp, expStatus) 312 err := outResp.Error 313 c.Assert(err, gc.NotNil) 314 c.Check(err.Message, gc.Matches, expError) 315 } 316 317 func (s *resourcesUploadSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.ResourceUploadResult { 318 body := apitesting.AssertResponse(c, resp, expStatus, params.ContentTypeJSON) 319 var outResp params.ResourceUploadResult 320 err := json.Unmarshal(body, &outResp) 321 c.Assert(err, jc.ErrorIsNil, gc.Commentf("Body: %s", body)) 322 return outResp 323 } 324 325 func (s *resourcesUploadSuite) TestSetResource(c *gc.C) { 326 ctrl := gomock.NewController(c) 327 defer ctrl.Finish() 328 329 stResources := mocks.NewMockResources(ctrl) 330 gomock.InOrder( 331 stResources.EXPECT().SetUnitResource(gomock.Any(), gomock.Any(), gomock.Any()).Return(resources.Resource{}, nil), 332 stResources.EXPECT().SetResource(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any(), state.DoNotIncrementCharmModifiedVersion).Return(resources.Resource{}, nil), 333 ) 334 apiserver.SetResource(true, "", "", charmresource.Resource{}, nil, stResources) 335 apiserver.SetResource(false, "", "", charmresource.Resource{}, nil, stResources) 336 }