github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/apiserver/tools_test.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package apiserver_test 5 6 import ( 7 "bytes" 8 "crypto/sha256" 9 "encoding/json" 10 "fmt" 11 "io" 12 "net/http" 13 "net/url" 14 "os" 15 "path/filepath" 16 "strings" 17 "sync" 18 "time" 19 20 "github.com/juju/errors" 21 jc "github.com/juju/testing/checkers" 22 "github.com/juju/utils/v3" 23 "github.com/juju/version/v2" 24 gc "gopkg.in/check.v1" 25 26 apitesting "github.com/juju/juju/apiserver/testing" 27 "github.com/juju/juju/environs" 28 "github.com/juju/juju/environs/simplestreams" 29 "github.com/juju/juju/environs/storage" 30 envtesting "github.com/juju/juju/environs/testing" 31 envtools "github.com/juju/juju/environs/tools" 32 toolstesting "github.com/juju/juju/environs/tools/testing" 33 "github.com/juju/juju/rpc/params" 34 "github.com/juju/juju/state" 35 "github.com/juju/juju/state/binarystorage" 36 "github.com/juju/juju/testing" 37 "github.com/juju/juju/testing/factory" 38 coretools "github.com/juju/juju/tools" 39 ) 40 41 type baseToolsSuite struct { 42 apiserverBaseSuite 43 } 44 45 func (s *baseToolsSuite) toolsURL(query string) *url.URL { 46 return s.modelToolsURL(s.Model.UUID(), query) 47 } 48 49 func (s *baseToolsSuite) modelToolsURL(model, query string) *url.URL { 50 u := s.URL(fmt.Sprintf("/model/%s/tools", model), nil) 51 u.RawQuery = query 52 return u 53 } 54 55 func (s *baseToolsSuite) toolsURI(query string) string { 56 if query != "" && query[0] == '?' { 57 query = query[1:] 58 } 59 return s.toolsURL(query).String() 60 } 61 62 func (s *baseToolsSuite) uploadRequest(c *gc.C, url, contentType string, content io.Reader) *http.Response { 63 return s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 64 Method: "POST", 65 URL: url, 66 ContentType: contentType, 67 Body: content, 68 }) 69 } 70 71 func (s *baseToolsSuite) downloadRequest(c *gc.C, version version.Binary, uuid string) *http.Response { 72 url := s.toolsURL("") 73 if uuid == "" { 74 url.Path = fmt.Sprintf("/tools/%s", version) 75 } else { 76 url.Path = fmt.Sprintf("/model/%s/tools/%s", uuid, version) 77 } 78 return apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: url.String()}) 79 } 80 81 func (s *baseToolsSuite) assertUploadResponse(c *gc.C, resp *http.Response, agentTools *coretools.Tools) { 82 toolsResponse := s.assertResponse(c, resp, http.StatusOK) 83 c.Check(toolsResponse.Error, gc.IsNil) 84 c.Check(toolsResponse.ToolsList, jc.DeepEquals, coretools.List{agentTools}) 85 } 86 87 func (s *baseToolsSuite) assertJSONErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) { 88 toolsResponse := s.assertResponse(c, resp, expCode) 89 c.Check(toolsResponse.ToolsList, gc.IsNil) 90 c.Check(toolsResponse.Error, gc.NotNil) 91 c.Check(toolsResponse.Error.Message, gc.Matches, expError) 92 } 93 94 func (s *baseToolsSuite) assertPlainErrorResponse(c *gc.C, resp *http.Response, expCode int, expError string) { 95 body := apitesting.AssertResponse(c, resp, expCode, "text/plain; charset=utf-8") 96 c.Assert(string(body), gc.Matches, expError+"\n") 97 } 98 99 func (s *baseToolsSuite) assertResponse(c *gc.C, resp *http.Response, expStatus int) params.ToolsResult { 100 body := apitesting.AssertResponse(c, resp, expStatus, params.ContentTypeJSON) 101 var toolsResponse params.ToolsResult 102 err := json.Unmarshal(body, &toolsResponse) 103 c.Assert(err, jc.ErrorIsNil, gc.Commentf("Body: %s", body)) 104 return toolsResponse 105 } 106 107 func (s *baseToolsSuite) storeFakeTools(c *gc.C, st *state.State, content string, metadata binarystorage.Metadata) *coretools.Tools { 108 storage, err := st.ToolsStorage() 109 c.Assert(err, jc.ErrorIsNil) 110 defer storage.Close() 111 err = storage.Add(strings.NewReader(content), metadata) 112 c.Assert(err, jc.ErrorIsNil) 113 return &coretools.Tools{ 114 Version: version.MustParseBinary(metadata.Version), 115 Size: metadata.Size, 116 SHA256: metadata.SHA256, 117 } 118 } 119 120 func (s *baseToolsSuite) getToolsFromStorage(c *gc.C, st *state.State, vers string) (binarystorage.Metadata, []byte) { 121 storage, err := st.ToolsStorage() 122 c.Assert(err, jc.ErrorIsNil) 123 defer storage.Close() 124 metadata, r, err := storage.Open(vers) 125 c.Assert(err, jc.ErrorIsNil) 126 data, err := io.ReadAll(r) 127 r.Close() 128 c.Assert(err, jc.ErrorIsNil) 129 return metadata, data 130 } 131 132 func (s *baseToolsSuite) getToolsMetadataFromStorage(c *gc.C, st *state.State) []binarystorage.Metadata { 133 storage, err := st.ToolsStorage() 134 c.Assert(err, jc.ErrorIsNil) 135 defer storage.Close() 136 metadata, err := storage.AllMetadata() 137 c.Assert(err, jc.ErrorIsNil) 138 return metadata 139 } 140 141 func (s *baseToolsSuite) testDownload(c *gc.C, tools *coretools.Tools, uuid string) []byte { 142 resp := s.downloadRequest(c, tools.Version, uuid) 143 defer resp.Body.Close() 144 data, err := io.ReadAll(resp.Body) 145 c.Assert(err, jc.ErrorIsNil) 146 c.Assert(data, gc.HasLen, int(tools.Size)) 147 148 hash := sha256.New() 149 hash.Write(data) 150 c.Assert(fmt.Sprintf("%x", hash.Sum(nil)), gc.Equals, tools.SHA256) 151 return data 152 } 153 154 type toolsSuite struct { 155 baseToolsSuite 156 } 157 158 var _ = gc.Suite(&toolsSuite{}) 159 160 func (s *toolsSuite) TestToolsUploadedSecurely(c *gc.C) { 161 url := s.toolsURL("") 162 url.Scheme = "http" 163 apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ 164 Method: "PUT", 165 URL: url.String(), 166 ExpectStatus: http.StatusBadRequest, 167 }) 168 } 169 170 func (s *toolsSuite) TestRequiresAuth(c *gc.C) { 171 resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "GET", URL: s.toolsURI("")}) 172 s.assertPlainErrorResponse(c, resp, http.StatusUnauthorized, "authentication failed: no credentials provided") 173 } 174 175 func (s *toolsSuite) TestRequiresPOST(c *gc.C) { 176 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "PUT", URL: s.toolsURI("")}) 177 s.assertJSONErrorResponse(c, resp, http.StatusMethodNotAllowed, `unsupported method: "PUT"`) 178 } 179 180 func (s *toolsSuite) TestAuthRequiresUser(c *gc.C) { 181 // Add a machine and try to login. 182 machine, err := s.State.AddMachine(state.UbuntuBase("12.10"), state.JobHostUnits) 183 c.Assert(err, jc.ErrorIsNil) 184 err = machine.SetProvisioned("foo", "", "fake_nonce", nil) 185 c.Assert(err, jc.ErrorIsNil) 186 password, err := utils.RandomPassword() 187 c.Assert(err, jc.ErrorIsNil) 188 err = machine.SetPassword(password) 189 c.Assert(err, jc.ErrorIsNil) 190 191 resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ 192 Tag: machine.Tag().String(), 193 Password: password, 194 Method: "POST", 195 URL: s.toolsURI(""), 196 Nonce: "fake_nonce", 197 }) 198 s.assertPlainErrorResponse( 199 c, resp, http.StatusForbidden, 200 "authorization failed: tag kind machine not valid", 201 ) 202 203 // Now try a user login. 204 resp = s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.toolsURI("")}) 205 s.assertJSONErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument") 206 } 207 208 func (s *toolsSuite) TestUploadRequiresVersion(c *gc.C) { 209 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{Method: "POST", URL: s.toolsURI("")}) 210 s.assertJSONErrorResponse(c, resp, http.StatusBadRequest, "expected binaryVersion argument") 211 } 212 213 func (s *toolsSuite) TestUploadFailsWithNoTools(c *gc.C) { 214 var empty bytes.Buffer 215 resp := s.uploadRequest(c, s.toolsURI("?binaryVersion=1.18.0-ubuntu-amd64"), "application/x-tar-gz", &empty) 216 s.assertJSONErrorResponse(c, resp, http.StatusBadRequest, "no agent binaries uploaded") 217 } 218 219 func (s *toolsSuite) TestUploadFailsWithInvalidContentType(c *gc.C) { 220 var empty bytes.Buffer 221 // Now try with the default Content-Type. 222 resp := s.uploadRequest(c, s.toolsURI("?binaryVersion=1.18.0-ubuntu-amd64"), "application/octet-stream", &empty) 223 s.assertJSONErrorResponse( 224 c, resp, http.StatusBadRequest, "expected Content-Type: application/x-tar-gz, got: application/octet-stream") 225 } 226 227 func (s *toolsSuite) setupToolsForUpload(c *gc.C) (coretools.List, version.Binary, []byte) { 228 localStorage := c.MkDir() 229 vers := version.MustParseBinary("1.9.0-ubuntu-amd64") 230 versionStrings := []string{vers.String()} 231 expectedTools := toolstesting.MakeToolsWithCheckSum(c, localStorage, "released", versionStrings) 232 toolsFile := envtools.StorageName(vers, "released") 233 toolsContent, err := os.ReadFile(filepath.Join(localStorage, toolsFile)) 234 c.Assert(err, jc.ErrorIsNil) 235 return expectedTools, vers, toolsContent 236 } 237 238 func (s *toolsSuite) TestUpload(c *gc.C) { 239 // Make some fake tools. 240 expectedTools, v, toolsContent := s.setupToolsForUpload(c) 241 vers := v.String() 242 243 // Now try uploading them. 244 resp := s.uploadRequest( 245 c, s.toolsURI("?binaryVersion="+vers), 246 "application/x-tar-gz", 247 bytes.NewReader(toolsContent), 248 ) 249 250 // Check the response. 251 expectedTools[0].URL = s.toolsURL("").String() + "/" + vers 252 s.assertUploadResponse(c, resp, expectedTools[0]) 253 254 // Check the contents. 255 metadata, uploadedData := s.getToolsFromStorage(c, s.State, vers) 256 c.Assert(uploadedData, gc.DeepEquals, toolsContent) 257 allMetadata := s.getToolsMetadataFromStorage(c, s.State) 258 c.Assert(allMetadata, jc.DeepEquals, []binarystorage.Metadata{metadata}) 259 } 260 261 func (s *toolsSuite) TestMigrateTools(c *gc.C) { 262 // Make some fake tools. 263 expectedTools, v, toolsContent := s.setupToolsForUpload(c) 264 vers := v.String() 265 266 newSt := s.Factory.MakeModel(c, nil) 267 defer newSt.Close() 268 importedModel, err := newSt.Model() 269 c.Assert(err, jc.ErrorIsNil) 270 err = importedModel.SetMigrationMode(state.MigrationModeImporting) 271 c.Assert(err, jc.ErrorIsNil) 272 273 // Now try uploading them. 274 uri := s.URL("/migrate/tools", url.Values{"binaryVersion": {vers}}) 275 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 276 Method: "POST", 277 URL: uri.String(), 278 ContentType: "application/x-tar-gz", 279 Body: bytes.NewReader(toolsContent), 280 ExtraHeaders: map[string]string{ 281 params.MigrationModelHTTPHeader: importedModel.UUID(), 282 }, 283 }) 284 285 // Check the response. 286 expectedTools[0].URL = s.modelToolsURL(s.State.ControllerModelUUID(), "").String() + "/" + vers 287 s.assertUploadResponse(c, resp, expectedTools[0]) 288 289 // Check the contents. 290 metadata, uploadedData := s.getToolsFromStorage(c, newSt, vers) 291 c.Assert(uploadedData, gc.DeepEquals, toolsContent) 292 allMetadata := s.getToolsMetadataFromStorage(c, newSt) 293 c.Assert(allMetadata, jc.DeepEquals, []binarystorage.Metadata{metadata}) 294 } 295 296 func (s *toolsSuite) TestMigrateToolsNotMigrating(c *gc.C) { 297 // Make some fake tools. 298 _, v, toolsContent := s.setupToolsForUpload(c) 299 vers := v.String() 300 301 newSt := s.Factory.MakeModel(c, nil) 302 defer newSt.Close() 303 304 uri := s.URL("/migrate/tools", url.Values{"binaryVersion": {vers}}) 305 resp := s.sendHTTPRequest(c, apitesting.HTTPRequestParams{ 306 Method: "POST", 307 URL: uri.String(), 308 ContentType: "application/x-tar-gz", 309 Body: bytes.NewReader(toolsContent), 310 ExtraHeaders: map[string]string{ 311 params.MigrationModelHTTPHeader: newSt.ModelUUID(), 312 }, 313 }) 314 315 // Now try uploading them. 316 s.assertJSONErrorResponse( 317 c, resp, http.StatusBadRequest, 318 `model migration mode is "" instead of "importing"`, 319 ) 320 } 321 322 func (s *toolsSuite) TestMigrateToolsUnauth(c *gc.C) { 323 // Try uploading as a non controller admin. 324 url := s.URL("/migrate/tools", nil).String() 325 user := s.Factory.MakeUser(c, &factory.UserParams{Password: "hunter2"}) 326 resp := apitesting.SendHTTPRequest(c, apitesting.HTTPRequestParams{ 327 Method: "POST", 328 URL: url, 329 Tag: user.Tag().String(), 330 Password: "hunter2", 331 }) 332 s.assertPlainErrorResponse( 333 c, resp, http.StatusForbidden, 334 "authorization failed: user .* is not a controller admin", 335 ) 336 } 337 338 func (s *toolsSuite) TestBlockUpload(c *gc.C) { 339 // Make some fake tools. 340 _, v, toolsContent := s.setupToolsForUpload(c) 341 vers := v.String() 342 343 // Block all changes. 344 err := s.State.SwitchBlockOn(state.ChangeBlock, "TestUpload") 345 c.Assert(err, jc.ErrorIsNil) 346 347 // Now try uploading them. 348 resp := s.uploadRequest( 349 c, s.toolsURI("?binaryVersion="+vers), 350 "application/x-tar-gz", 351 bytes.NewReader(toolsContent), 352 ) 353 toolsResponse := s.assertResponse(c, resp, http.StatusBadRequest) 354 c.Assert(toolsResponse.Error, jc.Satisfies, params.IsCodeOperationBlocked) 355 c.Assert(errors.Cause(toolsResponse.Error), gc.DeepEquals, ¶ms.Error{ 356 Message: "TestUpload", 357 Code: "operation is blocked", 358 }) 359 360 // Check the contents. 361 storage, err := s.State.ToolsStorage() 362 c.Assert(err, jc.ErrorIsNil) 363 defer storage.Close() 364 _, _, err = storage.Open(vers) 365 c.Assert(errors.IsNotFound(err), jc.IsTrue) 366 } 367 368 func (s *toolsSuite) TestUploadAllowsTopLevelPath(c *gc.C) { 369 // Backwards compatibility check, that we can upload tools to 370 // https://host:port/tools 371 expectedTools, vers, toolsContent := s.setupToolsForUpload(c) 372 url := s.toolsURL("binaryVersion=" + vers.String()) 373 url.Path = "/tools" 374 resp := s.uploadRequest(c, url.String(), "application/x-tar-gz", bytes.NewReader(toolsContent)) 375 expectedTools[0].URL = s.modelToolsURL(s.State.ControllerModelUUID(), "").String() + "/" + vers.String() 376 s.assertUploadResponse(c, resp, expectedTools[0]) 377 } 378 379 func (s *toolsSuite) TestUploadAllowsModelUUIDPath(c *gc.C) { 380 // Check that we can upload tools to https://host:port/ModelUUID/tools 381 expectedTools, vers, toolsContent := s.setupToolsForUpload(c) 382 url := s.toolsURL("binaryVersion=" + vers.String()) 383 resp := s.uploadRequest(c, url.String(), "application/x-tar-gz", bytes.NewReader(toolsContent)) 384 // Check the response. 385 expectedTools[0].URL = s.toolsURL("").String() + "/" + vers.String() 386 s.assertUploadResponse(c, resp, expectedTools[0]) 387 } 388 389 func (s *toolsSuite) TestUploadAllowsOtherModelUUIDPath(c *gc.C) { 390 newSt := s.Factory.MakeModel(c, nil) 391 defer newSt.Close() 392 393 // Check that we can upload tools to https://host:port/ModelUUID/tools 394 expectedTools, vers, toolsContent := s.setupToolsForUpload(c) 395 url := s.modelToolsURL(newSt.ModelUUID(), "binaryVersion="+vers.String()) 396 resp := s.uploadRequest(c, url.String(), "application/x-tar-gz", bytes.NewReader(toolsContent)) 397 398 // Check the response. 399 expectedTools[0].URL = s.modelToolsURL(newSt.ModelUUID(), "").String() + "/" + vers.String() 400 s.assertUploadResponse(c, resp, expectedTools[0]) 401 } 402 403 func (s *toolsSuite) TestDownloadModelUUIDPath(c *gc.C) { 404 tools := s.storeFakeTools(c, s.State, "abc", binarystorage.Metadata{ 405 Version: testing.CurrentVersion().String(), 406 Size: 3, 407 SHA256: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", 408 }) 409 s.testDownload(c, tools, s.State.ModelUUID()) 410 } 411 412 func (s *toolsSuite) TestDownloadOtherModelUUIDPath(c *gc.C) { 413 newSt := s.Factory.MakeModel(c, nil) 414 defer newSt.Close() 415 416 tools := s.storeFakeTools(c, newSt, "abc", binarystorage.Metadata{ 417 Version: testing.CurrentVersion().String(), 418 Size: 3, 419 SHA256: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", 420 }) 421 s.testDownload(c, tools, newSt.ModelUUID()) 422 } 423 424 func (s *toolsSuite) TestDownloadTopLevelPath(c *gc.C) { 425 tools := s.storeFakeTools(c, s.State, "abc", binarystorage.Metadata{ 426 Version: testing.CurrentVersion().String(), 427 Size: 3, 428 SHA256: "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", 429 }) 430 s.testDownload(c, tools, "") 431 } 432 433 func (s *toolsSuite) TestDownloadMissingConcurrent(c *gc.C) { 434 closer, testStorage, _ := envtesting.CreateLocalTestStorage(c) 435 defer closer.Close() 436 437 var mut sync.Mutex 438 resolutions := 0 439 envtools.RegisterToolsDataSourceFunc("local storage", func(environs.Environ) (simplestreams.DataSource, error) { 440 // Add some delay to make sure all goroutines are waiting. 441 time.Sleep(10 * time.Millisecond) 442 mut.Lock() 443 defer mut.Unlock() 444 resolutions++ 445 return storage.NewStorageSimpleStreamsDataSource("test datasource", testStorage, "tools", simplestreams.CUSTOM_CLOUD_DATA, false), nil 446 }) 447 defer envtools.UnregisterToolsDataSourceFunc("local storage") 448 449 toolsBinaries := []version.Binary{ 450 version.MustParseBinary("2.9.98-ubuntu-amd64"), 451 version.MustParseBinary("2.9.99-ubuntu-amd64"), 452 } 453 stream := "released" 454 tools, err := envtesting.UploadFakeToolsVersions(testStorage, stream, stream, toolsBinaries...) 455 c.Assert(err, jc.ErrorIsNil) 456 457 var wg sync.WaitGroup 458 const n = 8 459 wg.Add(n) 460 for i := 0; i < n; i++ { 461 tool := tools[i%len(toolsBinaries)] 462 go func() { 463 defer wg.Done() 464 s.testDownload(c, tool, s.State.ModelUUID()) 465 }() 466 } 467 wg.Wait() 468 469 c.Assert(resolutions, gc.Equals, len(toolsBinaries)) 470 } 471 472 type caasToolsSuite struct { 473 baseToolsSuite 474 } 475 476 var _ = gc.Suite(&caasToolsSuite{}) 477 478 func (s *caasToolsSuite) SetUpTest(c *gc.C) { 479 s.ControllerModelType = state.ModelTypeCAAS 480 s.baseToolsSuite.SetUpTest(c) 481 } 482 483 func (s *caasToolsSuite) TestToolDownloadNotSharedCAASController(c *gc.C) { 484 closer, testStorage, _ := envtesting.CreateLocalTestStorage(c) 485 defer closer.Close() 486 487 const n = 8 488 states := []*state.State{} 489 for i := 0; i < n; i++ { 490 testState := s.Factory.MakeModel(c, nil) 491 defer testState.Close() 492 states = append(states, testState) 493 } 494 495 var mut sync.Mutex 496 resolutions := 0 497 envtools.RegisterToolsDataSourceFunc("local storage", func(environs.Environ) (simplestreams.DataSource, error) { 498 // Add some delay to make sure all goroutines are waiting. 499 time.Sleep(10 * time.Millisecond) 500 mut.Lock() 501 defer mut.Unlock() 502 resolutions++ 503 return storage.NewStorageSimpleStreamsDataSource("test datasource", testStorage, "tools", simplestreams.CUSTOM_CLOUD_DATA, false), nil 504 }) 505 defer envtools.UnregisterToolsDataSourceFunc("local storage") 506 507 tool := version.MustParseBinary("2.9.99-ubuntu-amd64") 508 stream := "released" 509 tools, err := envtesting.UploadFakeToolsVersions(testStorage, stream, stream, tool) 510 c.Assert(err, jc.ErrorIsNil) 511 c.Assert(tools, gc.HasLen, 1) 512 513 var wg sync.WaitGroup 514 wg.Add(n) 515 for i := 0; i < n; i++ { 516 i := i 517 go func() { 518 defer wg.Done() 519 s.testDownload(c, tools[0], states[i].ModelUUID()) 520 }() 521 } 522 wg.Wait() 523 524 c.Assert(resolutions, gc.Equals, n) 525 }