github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/environs/tools/simplestreams_test.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package tools_test 5 6 import ( 7 "bytes" 8 "encoding/json" 9 "flag" 10 "fmt" 11 "io" 12 "net/http" 13 "net/http/httptest" 14 "os" 15 "path/filepath" 16 "reflect" 17 "strings" 18 "testing" 19 20 "github.com/aws/aws-sdk-go-v2/service/ec2" 21 "github.com/juju/errors" 22 jc "github.com/juju/testing/checkers" 23 "github.com/juju/version/v2" 24 gc "gopkg.in/check.v1" 25 26 corebase "github.com/juju/juju/core/base" 27 coreos "github.com/juju/juju/core/os" 28 "github.com/juju/juju/environs/filestorage" 29 "github.com/juju/juju/environs/simplestreams" 30 sstesting "github.com/juju/juju/environs/simplestreams/testing" 31 "github.com/juju/juju/environs/storage" 32 "github.com/juju/juju/environs/tools" 33 toolstesting "github.com/juju/juju/environs/tools/testing" 34 "github.com/juju/juju/juju/keys" 35 coretesting "github.com/juju/juju/testing" 36 coretools "github.com/juju/juju/tools" 37 ) 38 39 var live = flag.Bool("live", false, "Include live simplestreams tests") 40 var vendor = flag.String("vendor", "", "The vendor representing the source of the simplestream data") 41 42 type liveTestData struct { 43 baseURL string 44 requireSigned bool 45 validCloudSpec simplestreams.CloudSpec 46 } 47 48 func getLiveURLs() (map[string]liveTestData, error) { 49 resolver := ec2.NewDefaultEndpointResolver() 50 ep, err := resolver.ResolveEndpoint("us-east-1", ec2.EndpointResolverOptions{}) 51 if err != nil { 52 return nil, errors.Trace(err) 53 } 54 55 return map[string]liveTestData{ 56 "ec2": { 57 baseURL: tools.DefaultBaseURL, 58 requireSigned: true, 59 validCloudSpec: simplestreams.CloudSpec{ 60 Region: "us-east-1", 61 Endpoint: ep.URL, 62 }, 63 }, 64 "canonistack": { 65 baseURL: "https://swift.canonistack.canonical.com/v1/AUTH_526ad877f3e3464589dc1145dfeaac60/juju-tools", 66 requireSigned: false, 67 validCloudSpec: simplestreams.CloudSpec{ 68 Region: "lcy01", 69 Endpoint: "https://keystone.canonistack.canonical.com:443/v1.0/", 70 }, 71 }, 72 }, nil 73 } 74 75 func setupSimpleStreamsTests(t *testing.T) { 76 if *live { 77 if *vendor == "" { 78 t.Fatal("missing vendor") 79 } 80 var ok bool 81 var testData liveTestData 82 liveURLs, err := getLiveURLs() 83 if err != nil { 84 t.Fatalf(err.Error()) 85 } 86 if testData, ok = liveURLs[*vendor]; !ok { 87 keys := reflect.ValueOf(liveURLs).MapKeys() 88 t.Fatalf("Unknown vendor %s. Must be one of %s", *vendor, keys) 89 } 90 registerLiveSimpleStreamsTests(testData.baseURL, 91 tools.NewVersionedToolsConstraint(version.MustParse("1.13.0"), simplestreams.LookupParams{ 92 CloudSpec: testData.validCloudSpec, 93 Releases: []string{coreos.HostOSTypeName()}, 94 Arches: []string{"amd64"}, 95 Stream: "released", 96 }), testData.requireSigned) 97 } 98 registerSimpleStreamsTests() 99 } 100 101 func registerSimpleStreamsTests() { 102 gc.Suite(&simplestreamsSuite{ 103 LocalLiveSimplestreamsSuite: sstesting.LocalLiveSimplestreamsSuite{ 104 Source: sstesting.VerifyDefaultCloudDataSource("test", "test:"), 105 RequireSigned: false, 106 DataType: tools.ContentDownload, 107 StreamsVersion: tools.CurrentStreamsVersion, 108 ValidConstraint: tools.NewVersionedToolsConstraint(version.MustParse("1.13.0"), simplestreams.LookupParams{ 109 CloudSpec: simplestreams.CloudSpec{ 110 Region: "us-east-1", 111 Endpoint: "https://ec2.us-east-1.amazonaws.com", 112 }, 113 Releases: []string{"ubuntu"}, 114 Arches: []string{"amd64", "arm"}, 115 Stream: "released", 116 }), 117 }, 118 }) 119 gc.Suite(&signedSuite{}) 120 } 121 122 func registerLiveSimpleStreamsTests(baseURL string, validToolsConstraint simplestreams.LookupConstraint, requireSigned bool) { 123 factory := sstesting.TestDataSourceFactory() 124 gc.Suite(&sstesting.LocalLiveSimplestreamsSuite{ 125 Source: factory.NewDataSource(simplestreams.Config{ 126 Description: "test", 127 BaseURL: baseURL, 128 HostnameVerification: true, 129 Priority: simplestreams.DEFAULT_CLOUD_DATA, 130 RequireSigned: requireSigned, 131 }), 132 RequireSigned: requireSigned, 133 DataType: tools.ContentDownload, 134 StreamsVersion: tools.CurrentStreamsVersion, 135 ValidConstraint: validToolsConstraint, 136 }) 137 } 138 139 type simplestreamsSuite struct { 140 sstesting.LocalLiveSimplestreamsSuite 141 sstesting.TestDataSuite 142 } 143 144 func (s *simplestreamsSuite) SetUpSuite(c *gc.C) { 145 s.LocalLiveSimplestreamsSuite.SetUpSuite(c) 146 s.TestDataSuite.SetUpSuite(c) 147 s.PatchValue(&corebase.UbuntuDistroInfo, "/path/notexists") 148 } 149 150 func (s *simplestreamsSuite) TearDownSuite(c *gc.C) { 151 s.TestDataSuite.TearDownSuite(c) 152 s.LocalLiveSimplestreamsSuite.TearDownSuite(c) 153 } 154 155 var fetchTests = []struct { 156 region string 157 osType string 158 version string 159 stream string 160 major int 161 minor int 162 arches []string 163 tools []*tools.ToolsMetadata 164 }{{ 165 osType: "ubuntu", 166 arches: []string{"amd64", "arm"}, 167 version: "1.13.0", 168 tools: []*tools.ToolsMetadata{ 169 { 170 Release: "ubuntu", 171 Version: "1.13.0", 172 Arch: "amd64", 173 Size: 2973595, 174 Path: "tools/released/20130806/juju-1.13.0-ubuntu-amd64.tgz", 175 FileType: "tar.gz", 176 SHA256: "447aeb6a934a5eaec4f703eda4ef2dde", 177 }, 178 }, 179 }, { 180 osType: "ubuntu", 181 arches: []string{"amd64", "arm"}, 182 version: "1.11.4", 183 tools: []*tools.ToolsMetadata{ 184 { 185 Release: "ubuntu", 186 Version: "1.11.4", 187 Arch: "arm", 188 Size: 1951096, 189 Path: "tools/released/20130806/juju-1.11.4-ubuntu-arm.tgz", 190 FileType: "tar.gz", 191 SHA256: "f65a92b3b41311bdf398663ee1c5cd0c", 192 }, 193 }, 194 }, { 195 osType: "ubuntu", 196 arches: []string{"amd64", "arm"}, 197 major: 2, 198 tools: []*tools.ToolsMetadata{ 199 { 200 Release: "ubuntu", 201 Version: "2.0.1", 202 Arch: "arm", 203 Size: 1951096, 204 Path: "tools/released/20130806/juju-2.0.1-ubuntu-arm.tgz", 205 FileType: "tar.gz", 206 SHA256: "f65a92b3b41311bdf398663ee1c5cd0c", 207 }, 208 }, 209 }, { 210 osType: "ubuntu", 211 arches: []string{"amd64", "arm"}, 212 major: 1, 213 minor: 11, 214 tools: []*tools.ToolsMetadata{ 215 { 216 Release: "ubuntu", 217 Version: "1.11.4", 218 Arch: "arm", 219 Size: 1951096, 220 Path: "tools/released/20130806/juju-1.11.4-ubuntu-arm.tgz", 221 FileType: "tar.gz", 222 SHA256: "f65a92b3b41311bdf398663ee1c5cd0c", 223 }, 224 { 225 Release: "ubuntu", 226 Version: "1.11.5", 227 Arch: "arm", 228 Size: 2031281, 229 Path: "tools/released/20130803/juju-1.11.5-ubuntu-arm.tgz", 230 FileType: "tar.gz", 231 SHA256: "df07ac5e1fb4232d4e9aa2effa57918a", 232 }, 233 }, 234 }, { 235 osType: "ubuntu", 236 arches: []string{"amd64"}, 237 version: "1.16.0", 238 stream: "testing", 239 tools: []*tools.ToolsMetadata{ 240 { 241 Release: "ubuntu", 242 Version: "1.16.0", 243 Arch: "amd64", 244 Size: 2973512, 245 Path: "tools/testing/20130806/juju-1.16.0-ubuntu-amd64.tgz", 246 FileType: "tar.gz", 247 SHA256: "447aeb6a934a5eaec4f703eda4ef2dac", 248 }, 249 }, 250 }} 251 252 func (s *simplestreamsSuite) TestFetch(c *gc.C) { 253 for i, t := range fetchTests { 254 c.Logf("test %d", i) 255 if t.stream == "" { 256 t.stream = "released" 257 } 258 var toolsConstraint *tools.ToolsConstraint 259 if t.version == "" { 260 toolsConstraint = tools.NewGeneralToolsConstraint(t.major, t.minor, simplestreams.LookupParams{ 261 CloudSpec: simplestreams.CloudSpec{"us-east-1", "https://ec2.us-east-1.amazonaws.com"}, 262 Releases: []string{t.osType}, 263 Arches: t.arches, 264 Stream: t.stream, 265 }) 266 } else { 267 toolsConstraint = tools.NewVersionedToolsConstraint(version.MustParse(t.version), 268 simplestreams.LookupParams{ 269 CloudSpec: simplestreams.CloudSpec{"us-east-1", "https://ec2.us-east-1.amazonaws.com"}, 270 Releases: []string{t.osType}, 271 Arches: t.arches, 272 Stream: t.stream, 273 }) 274 } 275 // Add invalid datasource and check later that resolveInfo is correct. 276 invalidSource := sstesting.InvalidDataSource(s.RequireSigned) 277 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 278 toolsMetadata, resolveInfo, err := tools.Fetch(ss, []simplestreams.DataSource{invalidSource, s.Source}, toolsConstraint) 279 if !c.Check(err, jc.ErrorIsNil) { 280 continue 281 } 282 for _, tm := range t.tools { 283 tm.FullPath, err = s.Source.URL(tm.Path) 284 c.Assert(err, jc.ErrorIsNil) 285 } 286 c.Check(toolsMetadata, gc.DeepEquals, t.tools) 287 c.Check(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{ 288 Source: "test", 289 Signed: s.RequireSigned, 290 IndexURL: "test:/streams/v1/index.json", 291 MirrorURL: "", 292 }) 293 } 294 } 295 296 func (s *simplestreamsSuite) TestFetchNoMatchingStream(c *gc.C) { 297 toolsConstraint := tools.NewGeneralToolsConstraint(2, -1, simplestreams.LookupParams{ 298 CloudSpec: simplestreams.CloudSpec{"us-east-1", "https://ec2.us-east-1.amazonaws.com"}, 299 Releases: []string{"ubuntu"}, 300 Arches: []string{}, 301 Stream: "proposed", 302 }) 303 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 304 _, _, err := tools.Fetch(ss, 305 []simplestreams.DataSource{s.Source}, toolsConstraint) 306 c.Assert(err, gc.ErrorMatches, `"content-download" data not found`) 307 } 308 309 func (s *simplestreamsSuite) TestFetchWithMirror(c *gc.C) { 310 toolsConstraint := tools.NewGeneralToolsConstraint(1, 13, simplestreams.LookupParams{ 311 CloudSpec: simplestreams.CloudSpec{"us-west-2", "https://ec2.us-west-2.amazonaws.com"}, 312 Releases: []string{"ubuntu"}, 313 Arches: []string{"amd64"}, 314 Stream: "released", 315 }) 316 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 317 toolsMetadata, resolveInfo, err := tools.Fetch(ss, 318 []simplestreams.DataSource{s.Source}, toolsConstraint) 319 c.Assert(err, jc.ErrorIsNil) 320 c.Assert(len(toolsMetadata), gc.Equals, 1) 321 322 expectedMetadata := &tools.ToolsMetadata{ 323 Release: "ubuntu", 324 Version: "1.13.0", 325 Arch: "amd64", 326 Size: 2973595, 327 Path: "mirrored-path/juju-1.13.0-ubuntu-amd64.tgz", 328 FullPath: "test:/mirrored-path/juju-1.13.0-ubuntu-amd64.tgz", 329 FileType: "tar.gz", 330 SHA256: "447aeb6a934a5eaec4f703eda4ef2dde", 331 } 332 c.Assert(err, jc.ErrorIsNil) 333 c.Assert(toolsMetadata[0], gc.DeepEquals, expectedMetadata) 334 c.Assert(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{ 335 Source: "test", 336 Signed: s.RequireSigned, 337 IndexURL: "test:/streams/v1/index.json", 338 MirrorURL: "test:/", 339 }) 340 } 341 342 func assertMetadataMatches(c *gc.C, stream string, toolList coretools.List, metadata []*tools.ToolsMetadata) { 343 var expectedMetadata = make([]*tools.ToolsMetadata, len(toolList)) 344 for i, tool := range toolList { 345 expectedMetadata[i] = &tools.ToolsMetadata{ 346 Release: tool.Version.Release, 347 Version: tool.Version.Number.String(), 348 Arch: tool.Version.Arch, 349 Size: tool.Size, 350 Path: fmt.Sprintf("%s/juju-%s.tgz", stream, tool.Version.String()), 351 FileType: "tar.gz", 352 SHA256: tool.SHA256, 353 } 354 } 355 c.Assert(metadata, gc.DeepEquals, expectedMetadata) 356 } 357 358 func (s *simplestreamsSuite) TestWriteMetadataNoFetch(c *gc.C) { 359 toolsList := coretools.List{ 360 { 361 Version: version.MustParseBinary("1.2.3-ubuntu-amd64"), 362 Size: 123, 363 SHA256: "abcd", 364 }, { 365 Version: version.MustParseBinary("2.0.1-windows-amd64"), 366 Size: 456, 367 SHA256: "xyz", 368 }, 369 } 370 expected := toolsList 371 372 // Add tools with an unknown osType. 373 // We need to support this case for times when a new Ubuntu os type 374 // is released and jujud does not know about it yet. 375 vers, err := version.ParseBinary("3.2.1-xuanhuaceratops-amd64") 376 c.Assert(err, jc.ErrorIsNil) 377 toolsList = append(toolsList, &coretools.Tools{ 378 Version: vers, 379 Size: 456, 380 SHA256: "wqe", 381 }) 382 383 dir := c.MkDir() 384 writer, err := filestorage.NewFileStorageWriter(dir) 385 c.Assert(err, jc.ErrorIsNil) 386 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 387 err = tools.MergeAndWriteMetadata(ss, writer, "proposed", "proposed", toolsList, tools.DoNotWriteMirrors) 388 c.Assert(err, jc.ErrorIsNil) 389 metadata := toolstesting.ParseMetadataFromDir(c, dir, "proposed", false) 390 assertMetadataMatches(c, "proposed", expected, metadata) 391 } 392 393 func (s *simplestreamsSuite) assertWriteMetadata(c *gc.C, withMirrors bool) { 394 var versionStrings = []string{ 395 "1.2.3-ubuntu-amd64", 396 "2.0.1-ubuntu-amd64", 397 } 398 dir := c.MkDir() 399 toolstesting.MakeTools(c, dir, "proposed", versionStrings) 400 401 toolsList := coretools.List{ 402 { 403 // If sha256/size is already known, do not recalculate 404 Version: version.MustParseBinary("1.2.3-ubuntu-amd64"), 405 Size: 123, 406 SHA256: "abcd", 407 }, { 408 Version: version.MustParseBinary("2.0.1-ubuntu-amd64"), 409 // The URL is not used for generating metadata. 410 URL: "bogus://", 411 }, 412 } 413 writer, err := filestorage.NewFileStorageWriter(dir) 414 c.Assert(err, jc.ErrorIsNil) 415 writeMirrors := tools.DoNotWriteMirrors 416 if withMirrors { 417 writeMirrors = tools.WriteMirrors 418 } 419 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 420 err = tools.MergeAndWriteMetadata(ss, writer, "proposed", "proposed", toolsList, writeMirrors) 421 c.Assert(err, jc.ErrorIsNil) 422 423 metadata := toolstesting.ParseMetadataFromDir(c, dir, "proposed", withMirrors) 424 assertMetadataMatches(c, "proposed", toolsList, metadata) 425 426 // No release stream generated so there will not be a legacy index file created. 427 _, err = writer.Get("tools/streams/v1/index.json") 428 c.Assert(err, gc.NotNil) 429 } 430 431 func (s *simplestreamsSuite) TestWriteMetadata(c *gc.C) { 432 s.assertWriteMetadata(c, false) 433 } 434 435 func (s *simplestreamsSuite) TestWriteMetadataWithMirrors(c *gc.C) { 436 s.assertWriteMetadata(c, true) 437 } 438 439 func (s *simplestreamsSuite) TestWriteMetadataMergeWithExisting(c *gc.C) { 440 dir := c.MkDir() 441 existingToolsList := coretools.List{ 442 { 443 Version: version.MustParseBinary("1.2.3-ubuntu-amd64"), 444 Size: 123, 445 SHA256: "abc", 446 }, { 447 Version: version.MustParseBinary("2.0.1-ubuntu-amd64"), 448 Size: 456, 449 SHA256: "xyz", 450 }, 451 } 452 writer, err := filestorage.NewFileStorageWriter(dir) 453 c.Assert(err, jc.ErrorIsNil) 454 455 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 456 err = tools.MergeAndWriteMetadata(ss, writer, "testing", "testing", existingToolsList, tools.WriteMirrors) 457 c.Assert(err, jc.ErrorIsNil) 458 459 newToolsList := coretools.List{ 460 existingToolsList[0], 461 { 462 Version: version.MustParseBinary("2.1.0-ubuntu-amd64"), 463 Size: 789, 464 SHA256: "def", 465 }, 466 } 467 err = tools.MergeAndWriteMetadata(ss, writer, "testing", "testing", newToolsList, tools.WriteMirrors) 468 c.Assert(err, jc.ErrorIsNil) 469 requiredToolsList := append(existingToolsList, newToolsList[1]) 470 metadata := toolstesting.ParseMetadataFromDir(c, dir, "testing", true) 471 assertMetadataMatches(c, "testing", requiredToolsList, metadata) 472 473 err = tools.MergeAndWriteMetadata(ss, writer, "devel", "devel", newToolsList, tools.WriteMirrors) 474 c.Assert(err, jc.ErrorIsNil) 475 476 metadata = toolstesting.ParseMetadataFromDir(c, dir, "testing", true) 477 assertMetadataMatches(c, "testing", requiredToolsList, metadata) 478 479 metadata = toolstesting.ParseMetadataFromDir(c, dir, "devel", true) 480 assertMetadataMatches(c, "devel", newToolsList, metadata) 481 } 482 483 type productSpecSuite struct{} 484 485 var _ = gc.Suite(&productSpecSuite{}) 486 487 func (s *productSpecSuite) TestIndexIdNoStream(c *gc.C) { 488 toolsConstraint := tools.NewVersionedToolsConstraint(version.MustParse("1.13.0"), simplestreams.LookupParams{ 489 Releases: []string{"ubuntu"}, 490 Arches: []string{"amd64"}, 491 }) 492 ids := toolsConstraint.IndexIds() 493 c.Assert(ids, gc.HasLen, 0) 494 } 495 496 func (s *productSpecSuite) TestIndexId(c *gc.C) { 497 toolsConstraint := tools.NewVersionedToolsConstraint(version.MustParse("1.13.0"), simplestreams.LookupParams{ 498 Releases: []string{"ubuntu"}, 499 Arches: []string{"amd64"}, 500 Stream: "proposed", 501 }) 502 ids := toolsConstraint.IndexIds() 503 c.Assert(ids, gc.DeepEquals, []string{"com.ubuntu.juju:proposed:agents"}) 504 } 505 506 func (s *productSpecSuite) TestProductId(c *gc.C) { 507 toolsConstraint := tools.NewVersionedToolsConstraint(version.MustParse("1.13.0"), simplestreams.LookupParams{ 508 Releases: []string{"ubuntu"}, 509 Arches: []string{"amd64"}, 510 }) 511 ids, err := toolsConstraint.ProductIds() 512 c.Assert(err, jc.ErrorIsNil) 513 c.Assert(ids, gc.DeepEquals, []string{"com.ubuntu.juju:ubuntu:amd64"}) 514 } 515 516 func (s *productSpecSuite) TestIdMultiArch(c *gc.C) { 517 toolsConstraint := tools.NewVersionedToolsConstraint(version.MustParse("1.11.3"), simplestreams.LookupParams{ 518 Releases: []string{"ubuntu"}, 519 Arches: []string{"amd64", "arm"}, 520 }) 521 ids, err := toolsConstraint.ProductIds() 522 c.Assert(err, jc.ErrorIsNil) 523 c.Assert(ids, gc.DeepEquals, []string{ 524 "com.ubuntu.juju:ubuntu:amd64", 525 "com.ubuntu.juju:ubuntu:arm"}) 526 } 527 528 func (s *productSpecSuite) TestIdMultiOSType(c *gc.C) { 529 toolsConstraint := tools.NewVersionedToolsConstraint(version.MustParse("1.11.3"), simplestreams.LookupParams{ 530 Releases: []string{"ubuntu", "windows"}, 531 Arches: []string{"amd64"}, 532 Stream: "released", 533 }) 534 ids, err := toolsConstraint.ProductIds() 535 c.Assert(err, jc.ErrorIsNil) 536 c.Assert(ids, gc.DeepEquals, []string{ 537 "com.ubuntu.juju:ubuntu:amd64", 538 "com.ubuntu.juju:windows:amd64"}) 539 } 540 541 func (s *productSpecSuite) TestIdIgnoresInvalidOSType(c *gc.C) { 542 toolsConstraint := tools.NewVersionedToolsConstraint(version.MustParse("1.11.3"), simplestreams.LookupParams{ 543 Releases: []string{"ubuntu", "foobar"}, 544 Arches: []string{"amd64"}, 545 Stream: "released", 546 }) 547 ids, err := toolsConstraint.ProductIds() 548 c.Assert(err, jc.ErrorIsNil) 549 c.Assert(ids, gc.DeepEquals, []string{"com.ubuntu.juju:ubuntu:amd64"}) 550 } 551 552 func (s *productSpecSuite) TestIdWithMajorVersionOnly(c *gc.C) { 553 toolsConstraint := tools.NewGeneralToolsConstraint(1, -1, simplestreams.LookupParams{ 554 Releases: []string{"ubuntu"}, 555 Arches: []string{"amd64"}, 556 Stream: "released", 557 }) 558 ids, err := toolsConstraint.ProductIds() 559 c.Assert(err, jc.ErrorIsNil) 560 c.Assert(ids, gc.DeepEquals, []string{`com.ubuntu.juju:ubuntu:amd64`}) 561 } 562 563 func (s *productSpecSuite) TestIdWithMajorMinorVersion(c *gc.C) { 564 toolsConstraint := tools.NewGeneralToolsConstraint(1, 2, simplestreams.LookupParams{ 565 Releases: []string{"ubuntu"}, 566 Arches: []string{"amd64"}, 567 Stream: "released", 568 }) 569 ids, err := toolsConstraint.ProductIds() 570 c.Assert(err, jc.ErrorIsNil) 571 c.Assert(ids, gc.DeepEquals, []string{`com.ubuntu.juju:ubuntu:amd64`}) 572 } 573 574 func (s *productSpecSuite) TestLargeNumber(c *gc.C) { 575 json := `{ 576 "updated": "Fri, 30 Aug 2013 16:12:58 +0800", 577 "format": "products:1.0", 578 "products": { 579 "com.ubuntu.juju:1.10.0:amd64": { 580 "version": "1.10.0", 581 "arch": "amd64", 582 "versions": { 583 "20133008": { 584 "items": { 585 "1.10.0-ubuntu-amd64": { 586 "release": "ubuntu", 587 "version": "1.10.0", 588 "arch": "amd64", 589 "size": 9223372036854775807, 590 "path": "releases/juju-1.10.0-ubuntu-amd64.tgz", 591 "ftype": "tar.gz", 592 "sha256": "" 593 } 594 } 595 } 596 } 597 } 598 } 599 }` 600 cloudMetadata, err := simplestreams.ParseCloudMetadata([]byte(json), "products:1.0", "", tools.ToolsMetadata{}) 601 c.Assert(err, jc.ErrorIsNil) 602 c.Assert(cloudMetadata.Products, gc.HasLen, 1) 603 product := cloudMetadata.Products["com.ubuntu.juju:1.10.0:amd64"] 604 c.Assert(product, gc.NotNil) 605 c.Assert(product.Items, gc.HasLen, 1) 606 version := product.Items["20133008"] 607 c.Assert(version, gc.NotNil) 608 c.Assert(version.Items, gc.HasLen, 1) 609 item := version.Items["1.10.0-ubuntu-amd64"] 610 c.Assert(item, gc.NotNil) 611 c.Assert(item, gc.FitsTypeOf, &tools.ToolsMetadata{}) 612 c.Assert(item.(*tools.ToolsMetadata).Size, gc.Equals, int64(9223372036854775807)) 613 } 614 615 type metadataHelperSuite struct { 616 coretesting.BaseSuite 617 } 618 619 var _ = gc.Suite(&metadataHelperSuite{}) 620 621 func (*metadataHelperSuite) TestMetadataFromTools(c *gc.C) { 622 metadata := tools.MetadataFromTools(nil, "proposed") 623 c.Assert(metadata, gc.HasLen, 0) 624 625 toolsList := coretools.List{{ 626 Version: version.MustParseBinary("1.2.3-ubuntu-amd64"), 627 Size: 123, 628 SHA256: "abc", 629 }, { 630 Version: version.MustParseBinary("2.0.1-ubuntu-amd64"), 631 URL: "file:///tmp/proposed/juju-2.0.1-ubuntu-amd64.tgz", 632 Size: 456, 633 SHA256: "xyz", 634 }} 635 metadata = tools.MetadataFromTools(toolsList, "proposed") 636 c.Assert(metadata, gc.HasLen, len(toolsList)) 637 for i, t := range toolsList { 638 md := metadata[i] 639 c.Assert(md.Release, gc.Equals, "ubuntu") 640 c.Assert(md.Version, gc.Equals, t.Version.Number.String()) 641 c.Assert(md.Arch, gc.Equals, t.Version.Arch) 642 // FullPath is only filled out when reading tools using simplestreams. 643 // It's not needed elsewhere and requires a URL() call. 644 c.Assert(md.FullPath, gc.Equals, "") 645 c.Assert(md.Path, gc.Equals, tools.StorageName(t.Version, "proposed")[len("tools/"):]) 646 c.Assert(md.FileType, gc.Equals, "tar.gz") 647 c.Assert(md.Size, gc.Equals, t.Size) 648 c.Assert(md.SHA256, gc.Equals, t.SHA256) 649 } 650 } 651 652 type countingStorage struct { 653 storage.StorageReader 654 counter int 655 } 656 657 func (c *countingStorage) Get(name string) (io.ReadCloser, error) { 658 c.counter++ 659 return c.StorageReader.Get(name) 660 } 661 662 func (*metadataHelperSuite) TestResolveMetadata(c *gc.C) { 663 var versionStrings = []string{"1.2.3-ubuntu-amd64"} 664 dir := c.MkDir() 665 toolstesting.MakeTools(c, dir, "released", versionStrings) 666 toolsList := coretools.List{{ 667 Version: version.MustParseBinary(versionStrings[0]), 668 Size: 123, 669 SHA256: "abc", 670 }} 671 672 stor, err := filestorage.NewFileStorageReader(dir) 673 c.Assert(err, jc.ErrorIsNil) 674 err = tools.ResolveMetadata(stor, "released", nil) 675 c.Assert(err, jc.ErrorIsNil) 676 677 // We already have size/sha256, so ensure that storage isn't consulted. 678 countingStorage := &countingStorage{StorageReader: stor} 679 metadata := tools.MetadataFromTools(toolsList, "released") 680 err = tools.ResolveMetadata(countingStorage, "released", metadata) 681 c.Assert(err, jc.ErrorIsNil) 682 c.Assert(countingStorage.counter, gc.Equals, 0) 683 684 // Now clear size/sha256, and check that it is called, and 685 // the size/sha256 sum are updated. 686 metadata[0].Size = 0 687 metadata[0].SHA256 = "" 688 err = tools.ResolveMetadata(countingStorage, "released", metadata) 689 c.Assert(err, jc.ErrorIsNil) 690 c.Assert(countingStorage.counter, gc.Equals, 1) 691 c.Assert(metadata[0].Size, gc.Not(gc.Equals), 0) 692 c.Assert(metadata[0].SHA256, gc.Not(gc.Equals), "") 693 } 694 695 func (*metadataHelperSuite) TestMergeMetadata(c *gc.C) { 696 md1 := &tools.ToolsMetadata{ 697 Release: "ubuntu", 698 Version: "1.2.3", 699 Arch: "amd64", 700 Path: "path1", 701 } 702 md2 := &tools.ToolsMetadata{ 703 Release: "ubuntu", 704 Version: "1.2.3", 705 Arch: "amd64", 706 Path: "path2", 707 } 708 md3 := &tools.ToolsMetadata{ 709 Release: "windows", 710 Version: "1.2.3", 711 Arch: "amd64", 712 Path: "path3", 713 } 714 715 withSize := func(md *tools.ToolsMetadata, size int64) *tools.ToolsMetadata { 716 clone := *md 717 clone.Size = size 718 return &clone 719 } 720 withSHA256 := func(md *tools.ToolsMetadata, sha256 string) *tools.ToolsMetadata { 721 clone := *md 722 clone.SHA256 = sha256 723 return &clone 724 } 725 726 type mdlist []*tools.ToolsMetadata 727 type test struct { 728 name string 729 lhs, rhs, merged []*tools.ToolsMetadata 730 err string 731 } 732 tests := []test{{ 733 name: "non-empty lhs, empty rhs", 734 lhs: mdlist{md1}, 735 rhs: nil, 736 merged: mdlist{md1}, 737 }, { 738 name: "empty lhs, non-empty rhs", 739 lhs: nil, 740 rhs: mdlist{md2}, 741 merged: mdlist{md2}, 742 }, { 743 name: "identical lhs, rhs", 744 lhs: mdlist{md1}, 745 rhs: mdlist{md1}, 746 merged: mdlist{md1}, 747 }, { 748 name: "same tools in lhs and rhs, neither have size: prefer lhs", 749 lhs: mdlist{md1}, 750 rhs: mdlist{md2}, 751 merged: mdlist{md1}, 752 }, { 753 name: "same tools in lhs and rhs, only lhs has a size: prefer lhs", 754 lhs: mdlist{withSize(md1, 123)}, 755 rhs: mdlist{md2}, 756 merged: mdlist{withSize(md1, 123)}, 757 }, { 758 name: "same tools in lhs and rhs, only rhs has a size: prefer rhs", 759 lhs: mdlist{md1}, 760 rhs: mdlist{withSize(md2, 123)}, 761 merged: mdlist{withSize(md2, 123)}, 762 }, { 763 name: "same tools in lhs and rhs, both have the same size: prefer lhs", 764 lhs: mdlist{withSize(md1, 123)}, 765 rhs: mdlist{withSize(md2, 123)}, 766 merged: mdlist{withSize(md1, 123)}, 767 }, { 768 name: "same tools in lhs and rhs, both have different sizes: error", 769 lhs: mdlist{withSize(md1, 123)}, 770 rhs: mdlist{withSize(md2, 456)}, 771 err: "metadata mismatch for 1\\.2\\.3-ubuntu-amd64: sizes=\\(123,456\\) sha256=\\(,\\)", 772 }, { 773 name: "same tools in lhs and rhs, both have same size but different sha256: error", 774 lhs: mdlist{withSHA256(withSize(md1, 123), "a")}, 775 rhs: mdlist{withSHA256(withSize(md2, 123), "b")}, 776 err: "metadata mismatch for 1\\.2\\.3-ubuntu-amd64: sizes=\\(123,123\\) sha256=\\(a,b\\)", 777 }, { 778 name: "lhs is a proper superset of rhs: union of lhs and rhs", 779 lhs: mdlist{md1, md3}, 780 rhs: mdlist{md1}, 781 merged: mdlist{md1, md3}, 782 }, { 783 name: "rhs is a proper superset of lhs: union of lhs and rhs", 784 lhs: mdlist{md1}, 785 rhs: mdlist{md1, md3}, 786 merged: mdlist{md1, md3}, 787 }} 788 for i, test := range tests { 789 c.Logf("test %d: %s", i, test.name) 790 merged, err := tools.MergeMetadata(test.lhs, test.rhs) 791 if test.err == "" { 792 c.Assert(err, jc.ErrorIsNil) 793 c.Assert(merged, gc.DeepEquals, test.merged) 794 } else { 795 c.Assert(err, gc.ErrorMatches, test.err) 796 c.Assert(merged, gc.IsNil) 797 } 798 } 799 } 800 801 func (*metadataHelperSuite) TestReadWriteMetadataSingleStream(c *gc.C) { 802 metadata := map[string][]*tools.ToolsMetadata{ 803 "released": {{ 804 Release: "ubuntu", 805 Version: "1.2.3", 806 Arch: "amd64", 807 Path: "path1", 808 }}, 809 } 810 811 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 812 store, err := filestorage.NewFileStorageWriter(c.MkDir()) 813 c.Assert(err, jc.ErrorIsNil) 814 815 out, err := tools.ReadAllMetadata(ss, store) 816 c.Assert(err, jc.ErrorIsNil) // non-existence is not an error 817 c.Assert(out, gc.HasLen, 0) 818 819 err = tools.WriteMetadata(store, metadata, []string{"released"}, tools.DoNotWriteMirrors) 820 c.Assert(err, jc.ErrorIsNil) 821 822 // Read back what was just written. 823 out, err = tools.ReadAllMetadata(ss, store) 824 c.Assert(err, jc.ErrorIsNil) 825 for _, outMetadata := range out { 826 for _, md := range outMetadata { 827 // FullPath is set by ReadAllMetadata. 828 c.Assert(md.FullPath, gc.Not(gc.Equals), "") 829 md.FullPath = "" 830 } 831 } 832 c.Assert(out, jc.DeepEquals, metadata) 833 } 834 835 func (*metadataHelperSuite) writeMetadataMultipleStream(c *gc.C) (*simplestreams.Simplestreams, storage.StorageReader, map[string][]*tools.ToolsMetadata) { 836 metadata := map[string][]*tools.ToolsMetadata{ 837 "released": {{ 838 Release: "ubuntu", 839 Version: "1.2.3", 840 Arch: "amd64", 841 Path: "path1", 842 }}, 843 "proposed": {{ 844 Release: "ubuntu", 845 Version: "1.2.3", 846 Arch: "amd64", 847 Path: "path2", 848 }}, 849 } 850 851 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 852 store, err := filestorage.NewFileStorageWriter(c.MkDir()) 853 c.Assert(err, jc.ErrorIsNil) 854 855 out, err := tools.ReadAllMetadata(ss, store) 856 c.Assert(out, gc.HasLen, 0) 857 c.Assert(err, jc.ErrorIsNil) // non-existence is not an error 858 859 err = tools.WriteMetadata(store, metadata, []string{"released", "proposed"}, tools.DoNotWriteMirrors) 860 c.Assert(err, jc.ErrorIsNil) 861 return ss, store, metadata 862 } 863 864 func (s *metadataHelperSuite) TestReadWriteMetadataMultipleStream(c *gc.C) { 865 ss, store, metadata := s.writeMetadataMultipleStream(c) 866 // Read back what was just written. 867 out, err := tools.ReadAllMetadata(ss, store) 868 c.Assert(err, jc.ErrorIsNil) 869 for _, outMetadata := range out { 870 for _, md := range outMetadata { 871 // FullPath is set by ReadAllMetadata. 872 c.Assert(md.FullPath, gc.Not(gc.Equals), "") 873 md.FullPath = "" 874 } 875 } 876 c.Assert(out, jc.DeepEquals, metadata) 877 } 878 879 func (s *metadataHelperSuite) TestWriteMetadataLegacyIndex(c *gc.C) { 880 _, stor, _ := s.writeMetadataMultipleStream(c) 881 // Read back the legacy index 882 rdr, err := stor.Get("tools/streams/v1/index.json") 883 defer rdr.Close() 884 c.Assert(err, jc.ErrorIsNil) 885 data, err := io.ReadAll(rdr) 886 c.Assert(err, jc.ErrorIsNil) 887 var indices simplestreams.Indices 888 err = json.Unmarshal(data, &indices) 889 c.Assert(err, jc.ErrorIsNil) 890 c.Assert(indices.Indexes, gc.HasLen, 1) 891 indices.Updated = "" 892 c.Assert(indices.Indexes["com.ubuntu.juju:released:agents"], gc.NotNil) 893 indices.Indexes["com.ubuntu.juju:released:agents"].Updated = "" 894 expected := simplestreams.Indices{ 895 Format: "index:1.0", 896 Indexes: map[string]*simplestreams.IndexMetadata{ 897 "com.ubuntu.juju:released:agents": { 898 Format: "products:1.0", 899 DataType: "content-download", 900 ProductsFilePath: "streams/v1/com.ubuntu.juju-released-agents.json", 901 ProductIds: []string{"com.ubuntu.juju:ubuntu:amd64"}, 902 }, 903 }, 904 } 905 c.Assert(indices, jc.DeepEquals, expected) 906 } 907 908 func (s *metadataHelperSuite) TestReadWriteMetadataUnchanged(c *gc.C) { 909 metadata := map[string][]*tools.ToolsMetadata{ 910 "released": {{ 911 Release: "ubuntu", 912 Version: "1.2.3", 913 Arch: "amd64", 914 Path: "path1", 915 }, { 916 Release: "ubuntu", 917 Version: "1.2.4", 918 Arch: "amd64", 919 Path: "path2", 920 }}, 921 } 922 923 stor, err := filestorage.NewFileStorageWriter(c.MkDir()) 924 c.Assert(err, jc.ErrorIsNil) 925 err = tools.WriteMetadata(stor, metadata, []string{"released"}, tools.DoNotWriteMirrors) 926 c.Assert(err, jc.ErrorIsNil) 927 928 s.PatchValue(tools.WriteMetadataFiles, func(stor storage.Storage, metadataInfo []tools.MetadataFile) error { 929 // The product data is the same, we only write the indices. 930 c.Assert(metadataInfo, gc.HasLen, 2) 931 c.Assert(metadataInfo[0].Path, gc.Equals, "streams/v1/index2.json") 932 c.Assert(metadataInfo[1].Path, gc.Equals, "streams/v1/index.json") 933 return nil 934 }) 935 err = tools.WriteMetadata(stor, metadata, []string{"released"}, tools.DoNotWriteMirrors) 936 c.Assert(err, jc.ErrorIsNil) 937 } 938 939 func (*metadataHelperSuite) TestReadMetadataPrefersNewIndex(c *gc.C) { 940 metadataDir := c.MkDir() 941 942 // Generate metadata and rename index to index.json 943 metadata := map[string][]*tools.ToolsMetadata{ 944 "proposed": {{ 945 Release: "ubuntu", 946 Version: "1.2.3", 947 Arch: "amd64", 948 Path: "path1", 949 }}, 950 "released": {{ 951 Release: "ubuntu", 952 Version: "1.2.3", 953 Arch: "amd64", 954 Path: "path1", 955 }}, 956 } 957 store, err := filestorage.NewFileStorageWriter(metadataDir) 958 c.Assert(err, jc.ErrorIsNil) 959 err = tools.WriteMetadata(store, metadata, []string{"proposed", "released"}, tools.DoNotWriteMirrors) 960 c.Assert(err, jc.ErrorIsNil) 961 err = os.Rename( 962 filepath.Join(metadataDir, "tools", "streams", "v1", "index2.json"), 963 filepath.Join(metadataDir, "tools", "streams", "v1", "index.json"), 964 ) 965 c.Assert(err, jc.ErrorIsNil) 966 967 // Generate different metadata with index2.json 968 metadata = map[string][]*tools.ToolsMetadata{ 969 "released": {{ 970 Release: "ubuntu", 971 Version: "1.2.3", 972 Arch: "amd64", 973 Path: "path1", 974 }}, 975 } 976 err = tools.WriteMetadata(store, metadata, []string{"released"}, tools.DoNotWriteMirrors) 977 c.Assert(err, jc.ErrorIsNil) 978 979 // Read back all metadata, expecting to find metadata in index2.json. 980 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 981 out, err := tools.ReadAllMetadata(ss, store) 982 c.Assert(err, jc.ErrorIsNil) 983 for _, outMetadata := range out { 984 for _, md := range outMetadata { 985 // FullPath is set by ReadAllMetadata. 986 c.Assert(md.FullPath, gc.Not(gc.Equals), "") 987 md.FullPath = "" 988 } 989 } 990 c.Assert(out, jc.DeepEquals, metadata) 991 } 992 993 type signedSuite struct { 994 coretesting.BaseSuite 995 } 996 997 func (s *signedSuite) SetUpSuite(c *gc.C) { 998 s.BaseSuite.SetUpSuite(c) 999 s.PatchValue(&keys.JujuPublicKey, sstesting.SignedMetadataPublicKey) 1000 } 1001 1002 func (s *signedSuite) TearDownSuite(c *gc.C) { 1003 s.BaseSuite.TearDownSuite(c) 1004 } 1005 1006 func (s *signedSuite) TestSignedToolsMetadata(c *gc.C) { 1007 ts := httptest.NewServer(&sstreamsHandler{}) 1008 defer ts.Close() 1009 signedSource := simplestreams.NewDataSource( 1010 simplestreams.Config{ 1011 Description: "test", 1012 BaseURL: fmt.Sprintf("%s/signed", ts.URL), 1013 PublicSigningKey: sstesting.SignedMetadataPublicKey, 1014 HostnameVerification: true, 1015 Priority: simplestreams.DEFAULT_CLOUD_DATA, 1016 RequireSigned: true, 1017 }, 1018 ) 1019 toolsConstraint := tools.NewVersionedToolsConstraint(version.MustParse("1.13.0"), simplestreams.LookupParams{ 1020 CloudSpec: simplestreams.CloudSpec{"us-east-1", "https://ec2.us-east-1.amazonaws.com"}, 1021 Releases: []string{"ubuntu"}, 1022 Arches: []string{"amd64"}, 1023 Stream: "released", 1024 }) 1025 ss := simplestreams.NewSimpleStreams(sstesting.TestDataSourceFactory()) 1026 toolsMetadata, resolveInfo, err := tools.Fetch(ss, 1027 []simplestreams.DataSource{signedSource}, toolsConstraint) 1028 c.Assert(err, jc.ErrorIsNil) 1029 c.Assert(len(toolsMetadata), gc.Equals, 1) 1030 c.Assert(toolsMetadata[0].Path, gc.Equals, "tools/releases/20130806/juju-1.13.1-ubuntu-amd64.tgz") 1031 c.Assert(resolveInfo, gc.DeepEquals, &simplestreams.ResolveInfo{ 1032 Source: "test", 1033 Signed: true, 1034 IndexURL: fmt.Sprintf("%s/signed/streams/v1/index.sjson", ts.URL), 1035 MirrorURL: "", 1036 }) 1037 } 1038 1039 type sstreamsHandler struct{} 1040 1041 func (h *sstreamsHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { 1042 switch r.URL.Path { 1043 case "/unsigned/streams/v1/index.json": 1044 w.Header().Set("Content-Type", "application/json") 1045 w.WriteHeader(http.StatusOK) 1046 _, _ = io.WriteString(w, unsignedIndex) 1047 case "/unsigned/streams/v1/image_metadata.json": 1048 w.Header().Set("Content-Type", "application/json") 1049 w.WriteHeader(http.StatusOK) 1050 _, _ = io.WriteString(w, unsignedProduct) 1051 case "/signed/streams/v1/tools_metadata.sjson": 1052 w.Header().Set("Content-Type", "application/json") 1053 w.WriteHeader(http.StatusOK) 1054 rawUnsignedProduct := strings.Replace( 1055 unsignedProduct, "juju-1.13.0", "juju-1.13.1", -1) 1056 _, _ = io.WriteString(w, encode(rawUnsignedProduct)) 1057 return 1058 case "/signed/streams/v1/index.sjson": 1059 w.Header().Set("Content-Type", "application/json") 1060 w.WriteHeader(http.StatusOK) 1061 rawUnsignedIndex := strings.Replace( 1062 unsignedIndex, "streams/v1/tools_metadata.json", "streams/v1/tools_metadata.sjson", -1) 1063 _, _ = io.WriteString(w, encode(rawUnsignedIndex)) 1064 return 1065 default: 1066 http.Error(w, r.URL.Path, 404) 1067 return 1068 } 1069 } 1070 1071 func encode(data string) string { 1072 reader := bytes.NewReader([]byte(data)) 1073 signedData, _ := simplestreams.Encode( 1074 reader, sstesting.SignedMetadataPrivateKey, sstesting.PrivateKeyPassphrase) 1075 return string(signedData) 1076 } 1077 1078 var unsignedIndex = ` 1079 { 1080 "index": { 1081 "com.ubuntu.juju:released:agents": { 1082 "updated": "Mon, 05 Aug 2013 11:07:04 +0000", 1083 "datatype": "content-download", 1084 "format": "products:1.0", 1085 "products": [ 1086 "com.ubuntu.juju:ubuntu:amd64" 1087 ], 1088 "path": "streams/v1/tools_metadata.json" 1089 } 1090 }, 1091 "updated": "Wed, 01 May 2013 13:31:26 +0000", 1092 "format": "index:1.0" 1093 } 1094 ` 1095 var unsignedProduct = ` 1096 { 1097 "updated": "Wed, 01 May 2013 13:31:26 +0000", 1098 "content_id": "com.ubuntu.juju:released:aws", 1099 "datatype": "content-download", 1100 "products": { 1101 "com.ubuntu.juju:ubuntu:amd64": { 1102 "arch": "amd64", 1103 "release": "ubuntu", 1104 "versions": { 1105 "20130806": { 1106 "items": { 1107 "1130ubuntuamd64": { 1108 "version": "1.13.0", 1109 "size": 2973595, 1110 "path": "tools/releases/20130806/juju-1.13.0-ubuntu-amd64.tgz", 1111 "ftype": "tar.gz", 1112 "sha256": "447aeb6a934a5eaec4f703eda4ef2dde" 1113 } 1114 } 1115 } 1116 } 1117 } 1118 }, 1119 "format": "products:1.0" 1120 } 1121 `