launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/environs/simplestreams/simplestreams.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 // The simplestreams package supports locating, parsing, and filtering metadata in simplestreams format. 5 // See http://launchpad.net/simplestreams and in particular the doc/README file in that project for more information 6 // about the file formats. 7 // 8 // Users of this package provide an empty struct and a matching function to be able to query and return a list 9 // of typed values for a given criteria. 10 package simplestreams 11 12 import ( 13 "bufio" 14 "encoding/json" 15 "fmt" 16 "io" 17 "io/ioutil" 18 "net/http" 19 "os" 20 "path" 21 "reflect" 22 "sort" 23 "strings" 24 "sync" 25 26 "github.com/loggo/loggo" 27 28 "launchpad.net/juju-core/errors" 29 ) 30 31 var logger = loggo.GetLogger("juju.environs.simplestreams") 32 33 // CloudSpec uniquely defines a specific cloud deployment. 34 type CloudSpec struct { 35 Region string `json:"region"` 36 Endpoint string `json:"endpoint"` 37 } 38 39 // equals returns true if spec == other, allowing for endpoints 40 // with or without a trailing "/". 41 func (spec *CloudSpec) equals(other *CloudSpec) bool { 42 if spec.Region != other.Region { 43 return false 44 } 45 specEndpoint := spec.Endpoint 46 if !strings.HasSuffix(specEndpoint, "/") { 47 specEndpoint += "/" 48 } 49 otherEndpoint := other.Endpoint 50 if !strings.HasSuffix(otherEndpoint, "/") { 51 otherEndpoint += "/" 52 } 53 return specEndpoint == otherEndpoint 54 } 55 56 // EmptyCloudSpec is used when we want all records regardless of cloud to be loaded. 57 var EmptyCloudSpec = CloudSpec{} 58 59 // HasRegion is implemented by instances which can provide a region to which they belong. 60 // A region is defined by region name and endpoint. 61 type HasRegion interface { 62 // Region returns the necessary attributes to uniquely identify this cloud instance. 63 // Currently these attributes are "region" and "endpoint" values. 64 Region() (CloudSpec, error) 65 } 66 67 type LookupConstraint interface { 68 // Generates a string array representing product ids formed similarly to an ISCSI qualified name (IQN). 69 Ids() ([]string, error) 70 // Returns the constraint parameters. 71 Params() LookupParams 72 } 73 74 // LookupParams defines criteria used to find a metadata record. 75 // Derived structs implement the Ids() method. 76 type LookupParams struct { 77 CloudSpec 78 Series []string 79 Arches []string 80 // Stream can be "" or "released" for the default "released" stream, 81 // or "daily" for daily images, or any other stream that the available 82 // simplestreams metadata supports. 83 Stream string 84 } 85 86 func (p LookupParams) Params() LookupParams { 87 return p 88 } 89 90 // seriesVersions provides a mapping between Ubuntu series names and version numbers. 91 // The values here are current as of the time of writing. On Ubuntu systems, we update 92 // these values from /usr/share/distro-info/ubuntu.csv to ensure we have the latest values. 93 // On non-Ubuntu systems, these values provide a nice fallback option. 94 // Exported so tests can change the values to ensure the distro-info lookup works. 95 var seriesVersions = map[string]string{ 96 "precise": "12.04", 97 "quantal": "12.10", 98 "raring": "13.04", 99 "saucy": "13.10", 100 "trusty": "14.04", 101 } 102 103 var ( 104 seriesVersionsMutex sync.Mutex 105 updatedseriesVersions bool 106 ) 107 108 // SeriesVersion returns the version number for the specified Ubuntu series. 109 func SeriesVersion(series string) (string, error) { 110 if series == "" { 111 panic("cannot pass empty series to SeriesVersion()") 112 } 113 seriesVersionsMutex.Lock() 114 defer seriesVersionsMutex.Unlock() 115 if vers, ok := seriesVersions[series]; ok { 116 return vers, nil 117 } 118 updateSeriesVersions() 119 if vers, ok := seriesVersions[series]; ok { 120 return vers, nil 121 } 122 return "", fmt.Errorf("invalid series %q", series) 123 } 124 125 // Supported series returns the Ubuntu series for which we expect to find metadata. 126 func SupportedSeries() []string { 127 seriesVersionsMutex.Lock() 128 defer seriesVersionsMutex.Unlock() 129 updateSeriesVersions() 130 series := []string{} 131 for s, _ := range seriesVersions { 132 series = append(series, s) 133 } 134 return series 135 } 136 137 func updateSeriesVersions() { 138 if !updatedseriesVersions { 139 err := updateDistroInfo() 140 if err != nil { 141 logger.Warningf("failed to update distro info: %v", err) 142 } 143 updatedseriesVersions = true 144 } 145 } 146 147 // updateDistroInfo updates seriesVersions from /usr/share/distro-info/ubuntu.csv if possible.. 148 func updateDistroInfo() error { 149 // We need to find the series version eg 12.04 from the series eg precise. Use the information found in 150 // /usr/share/distro-info/ubuntu.csv provided by distro-info-data package. 151 f, err := os.Open("/usr/share/distro-info/ubuntu.csv") 152 if err != nil { 153 // On non-Ubuntu systems this file won't exist but that's expected. 154 return nil 155 } 156 defer f.Close() 157 bufRdr := bufio.NewReader(f) 158 // Only find info for precise or later. 159 preciseOrLaterFound := false 160 for { 161 line, err := bufRdr.ReadString('\n') 162 if err == io.EOF { 163 break 164 } 165 if err != nil { 166 return fmt.Errorf("reading distro info file file: %v", err) 167 } 168 // lines are of the form: "12.04 LTS,Precise Pangolin,precise,2011-10-13,2012-04-26,2017-04-26" 169 parts := strings.Split(line, ",") 170 // Ignore any malformed lines. 171 if len(parts) < 3 { 172 continue 173 } 174 series := parts[2] 175 if series == "precise" { 176 preciseOrLaterFound = true 177 } 178 if series != "precise" && !preciseOrLaterFound { 179 continue 180 } 181 // the numeric version may contain a LTS moniker so strip that out. 182 seriesInfo := strings.Split(parts[0], " ") 183 seriesVersions[series] = seriesInfo[0] 184 } 185 return nil 186 } 187 188 // The following structs define the data model used in the JSON metadata files. 189 // Not every model attribute is defined here, only the ones we care about. 190 // See the doc/README file in lp:simplestreams for more information. 191 192 // Metadata attribute values may point to a map of attribute values (aka aliases) and these attributes 193 // are used to override/augment the existing attributes. 194 type attributeValues map[string]string 195 type aliasesByAttribute map[string]attributeValues 196 197 type CloudMetadata struct { 198 Products map[string]MetadataCatalog `json:"products"` 199 Aliases map[string]aliasesByAttribute `json:"_aliases,omitempty"` 200 Updated string `json:"updated"` 201 Format string `json:"format"` 202 ContentId string `json:"content_id"` 203 } 204 205 type MetadataCatalog struct { 206 Series string `json:"release,omitempty"` 207 Version string `json:"version,omitempty"` 208 Arch string `json:"arch,omitempty"` 209 RegionName string `json:"region,omitempty"` 210 Endpoint string `json:"endpoint,omitempty"` 211 212 // Items is a mapping from version to an ItemCollection, 213 // where the version is the date the items were produced, 214 // in the format YYYYMMDD. 215 Items map[string]*ItemCollection `json:"versions"` 216 } 217 218 type ItemCollection struct { 219 rawItems map[string]*json.RawMessage 220 Items map[string]interface{} `json:"items"` 221 Arch string `json:"arch,omitempty"` 222 Series string `json:"release,omitempty"` 223 Version string `json:"version,omitempty"` 224 RegionName string `json:"region,omitempty"` 225 Endpoint string `json:"endpoint,omitempty"` 226 } 227 228 // These structs define the model used for metadata indices. 229 230 type Indices struct { 231 Indexes map[string]*IndexMetadata `json:"index"` 232 Updated string `json:"updated"` 233 Format string `json:"format"` 234 } 235 236 // Exported for testing. 237 type IndexReference struct { 238 Indices 239 MirroredProductsPath string 240 Source DataSource 241 valueParams ValueParams 242 } 243 244 type IndexMetadata struct { 245 Updated string `json:"updated"` 246 Format string `json:"format"` 247 DataType string `json:"datatype"` 248 CloudName string `json:"cloudname,omitempty"` 249 Clouds []CloudSpec `json:"clouds,omitempty"` 250 ProductsFilePath string `json:"path"` 251 ProductIds []string `json:"products"` 252 } 253 254 // These structs define the model used to describe download mirrors. 255 256 type MirrorRefs struct { 257 Mirrors map[string][]MirrorReference `json:"mirrors"` 258 } 259 260 type MirrorReference struct { 261 Updated string `json:"updated"` 262 Format string `json:"format"` 263 DataType string `json:"datatype"` 264 Path string `json:"path"` 265 Clouds []CloudSpec `json:"clouds"` 266 } 267 268 type MirrorMetadata struct { 269 Updated string `json:"updated"` 270 Format string `json:"format"` 271 Mirrors map[string][]MirrorInfo `json:"mirrors"` 272 } 273 274 type MirrorInfo struct { 275 Clouds []CloudSpec `json:"clouds"` 276 MirrorURL string `json:"mirror"` 277 Path string `json:"path"` 278 } 279 280 type MirrorInfoSlice []MirrorInfo 281 type MirrorRefSlice []MirrorReference 282 283 // filter returns those entries from an MirrorInfo array for which the given 284 // match function returns true. It preserves order. 285 func (entries MirrorInfoSlice) filter(match func(*MirrorInfo) bool) MirrorInfoSlice { 286 result := MirrorInfoSlice{} 287 for _, mirrorInfo := range entries { 288 if match(&mirrorInfo) { 289 result = append(result, mirrorInfo) 290 } 291 } 292 return result 293 } 294 295 // filter returns those entries from an MirrorInfo array for which the given 296 // match function returns true. It preserves order. 297 func (entries MirrorRefSlice) filter(match func(*MirrorReference) bool) MirrorRefSlice { 298 result := MirrorRefSlice{} 299 for _, mirrorRef := range entries { 300 if match(&mirrorRef) { 301 result = append(result, mirrorRef) 302 } 303 } 304 return result 305 } 306 307 // extractCatalogsForProducts gives you just those catalogs from a 308 // cloudImageMetadata that are for the given product IDs. They are kept in 309 // the order of the parameter. 310 func (metadata *CloudMetadata) extractCatalogsForProducts(productIds []string) []MetadataCatalog { 311 result := []MetadataCatalog{} 312 for _, id := range productIds { 313 if catalog, ok := metadata.Products[id]; ok { 314 result = append(result, catalog) 315 } 316 } 317 return result 318 } 319 320 // extractIndexes returns just the array of indexes, in arbitrary order. 321 func (ind *Indices) extractIndexes() IndexMetadataSlice { 322 result := make(IndexMetadataSlice, 0, len(ind.Indexes)) 323 for _, metadata := range ind.Indexes { 324 result = append(result, metadata) 325 } 326 return result 327 } 328 329 func (metadata *IndexMetadata) String() string { 330 return fmt.Sprintf("%v", *metadata) 331 } 332 333 // hasCloud tells you whether an IndexMetadata has the given cloud in its 334 // Clouds list. If IndexMetadata has no clouds defined, then hasCloud 335 // returns true regardless so that the corresponding product records 336 // are searched. 337 func (metadata *IndexMetadata) hasCloud(cloud CloudSpec) bool { 338 for _, metadataCloud := range metadata.Clouds { 339 if metadataCloud.equals(&cloud) { 340 return true 341 } 342 } 343 return len(metadata.Clouds) == 0 344 } 345 346 // hasProduct tells you whether an IndexMetadata provides any of the given 347 // product IDs. 348 func (metadata *IndexMetadata) hasProduct(prodIds []string) bool { 349 for _, pid := range metadata.ProductIds { 350 if containsString(prodIds, pid) { 351 return true 352 } 353 } 354 return false 355 } 356 357 type IndexMetadataSlice []*IndexMetadata 358 359 // filter returns those entries from an IndexMetadata array for which the given 360 // match function returns true. It preserves order. 361 func (entries IndexMetadataSlice) filter(match func(*IndexMetadata) bool) IndexMetadataSlice { 362 result := IndexMetadataSlice{} 363 for _, metadata := range entries { 364 if match(metadata) { 365 result = append(result, metadata) 366 } 367 } 368 return result 369 } 370 371 func init() { 372 RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) 373 } 374 375 // RegisterProtocol registers a new protocol with the simplestreams http client. 376 // Exported for testing. 377 func RegisterProtocol(scheme string, rt http.RoundTripper) { 378 http.DefaultTransport.(*http.Transport).RegisterProtocol(scheme, rt) 379 } 380 381 // noMatchingProductsError is used to indicate that metadata files have been located, 382 // but there is no metadata satisfying a product criteria. 383 // It is used to distinguish from the situation where the metadata files could not be found. 384 type noMatchingProductsError struct { 385 msg string 386 } 387 388 func (e *noMatchingProductsError) Error() string { 389 return e.msg 390 } 391 392 func newNoMatchingProductsError(message string, args ...interface{}) error { 393 return &noMatchingProductsError{fmt.Sprintf(message, args...)} 394 } 395 396 const ( 397 StreamsDir = "streams/v1" 398 UnsignedIndex = "streams/v1/index.json" 399 DefaultIndexPath = "streams/v1/index" 400 UnsignedMirror = "streams/v1/mirrors.json" 401 mirrorsPath = "streams/v1/mirrors" 402 signedSuffix = ".sjson" 403 UnsignedSuffix = ".json" 404 ) 405 406 type appendMatchingFunc func(DataSource, []interface{}, map[string]interface{}, LookupConstraint) []interface{} 407 408 // ValueParams contains the information required to pull out from the metadata structs of a particular type. 409 type ValueParams struct { 410 // The simplestreams data type key. 411 DataType string 412 // The key to use when looking for content mirrors. 413 MirrorContentId string 414 // A function used to filter and return records of a given type. 415 FilterFunc appendMatchingFunc 416 // An struct representing the type of records to return. 417 ValueTemplate interface{} 418 // For signed metadata, the public key used to validate the signature. 419 PublicKey string 420 } 421 422 // GetMetadata returns metadata records matching the specified constraint,looking in each source for signed metadata. 423 // If onlySigned is false and no signed metadata is found in a source, the source is used to look for unsigned metadata. 424 // Each source is tried in turn until at least one signed (or unsigned) match is found. 425 func GetMetadata(sources []DataSource, baseIndexPath string, cons LookupConstraint, onlySigned bool, params ValueParams) (items []interface{}, err error) { 426 for _, source := range sources { 427 items, err = getMaybeSignedMetadata(source, baseIndexPath, cons, true, params) 428 // If no items are found using signed metadata, check unsigned. 429 if err != nil && len(items) == 0 && !onlySigned { 430 items, err = getMaybeSignedMetadata(source, baseIndexPath, cons, false, params) 431 } 432 if err == nil { 433 break 434 } 435 } 436 if _, ok := err.(*noMatchingProductsError); ok { 437 // no matching products is an internal error only 438 err = nil 439 } 440 return items, err 441 } 442 443 // getMaybeSignedMetadata returns metadata records matching the specified constraint. 444 func getMaybeSignedMetadata(source DataSource, baseIndexPath string, cons LookupConstraint, signed bool, params ValueParams) ([]interface{}, error) { 445 indexPath := baseIndexPath + UnsignedSuffix 446 if signed { 447 indexPath = baseIndexPath + signedSuffix 448 } 449 var items []interface{} 450 indexURL, err := source.URL(indexPath) 451 if err != nil { 452 // Some providers return an error if asked for the URL of a non-existent file. 453 // So the best we can do is use the relative path for the URL when logging messages. 454 indexURL = indexPath 455 } 456 indexRef, err := GetIndexWithFormat(source, indexPath, "index:1.0", signed, cons.Params().CloudSpec, params) 457 if err != nil { 458 if errors.IsNotFoundError(err) || errors.IsUnauthorizedError(err) { 459 logger.Debugf("cannot load index %q: %v", indexURL, err) 460 } 461 return nil, err 462 } 463 logger.Debugf("read metadata index at %q", indexURL) 464 items, err = indexRef.getLatestMetadataWithFormat(cons, "products:1.0", signed) 465 if err != nil { 466 if errors.IsNotFoundError(err) { 467 logger.Debugf("skipping index because of error getting latest metadata %q: %v", indexURL, err) 468 return nil, err 469 } 470 if _, ok := err.(*noMatchingProductsError); ok { 471 logger.Debugf("%v", err) 472 } 473 } 474 return items, err 475 } 476 477 // fetchData gets all the data from the given source located at the specified path. 478 // It returns the data found and the full URL used. 479 func fetchData(source DataSource, path string, requireSigned bool, publicKey string) (data []byte, dataURL string, err error) { 480 rc, dataURL, err := source.Fetch(path) 481 if err != nil { 482 logger.Debugf("fetchData failed for %q: %v", dataURL, err) 483 return nil, dataURL, errors.NotFoundf("invalid URL %q", dataURL) 484 } 485 defer rc.Close() 486 if requireSigned { 487 data, err = DecodeCheckSignature(rc, publicKey) 488 } else { 489 data, err = ioutil.ReadAll(rc) 490 } 491 if err != nil { 492 return nil, dataURL, fmt.Errorf("cannot read URL data, %v", err) 493 } 494 return data, dataURL, nil 495 } 496 497 // GetIndexWithFormat returns a simplestreams index of the specified format. 498 // Exported for testing. 499 func GetIndexWithFormat(source DataSource, indexPath, indexFormat string, requireSigned bool, 500 cloudSpec CloudSpec, params ValueParams) (*IndexReference, error) { 501 502 data, url, err := fetchData(source, indexPath, requireSigned, params.PublicKey) 503 if err != nil { 504 if errors.IsNotFoundError(err) || errors.IsUnauthorizedError(err) { 505 return nil, err 506 } 507 return nil, fmt.Errorf("cannot read index data, %v", err) 508 } 509 var indices Indices 510 err = json.Unmarshal(data, &indices) 511 if err != nil { 512 return nil, fmt.Errorf("cannot unmarshal JSON index metadata at URL %q: %v", url, err) 513 } 514 if indices.Format != indexFormat { 515 return nil, fmt.Errorf( 516 "unexpected index file format %q, expected %q at URL %q", indices.Format, indexFormat, url) 517 } 518 519 mirrors, url, err := getMirrorRefs(source, mirrorsPath, requireSigned, params) 520 if err != nil && !errors.IsNotFoundError(err) && !errors.IsUnauthorizedError(err) { 521 return nil, fmt.Errorf("cannot load mirror metadata at URL %q: %v", url, err) 522 } 523 524 indexRef := &IndexReference{ 525 Source: source, 526 Indices: indices, 527 valueParams: params, 528 } 529 530 // Apply any mirror information to the source. 531 if params.MirrorContentId != "" { 532 mirrorInfo, err := getMirror( 533 source, mirrors, params.DataType, params.MirrorContentId, cloudSpec, requireSigned, params.PublicKey) 534 if err == nil { 535 logger.Debugf("using mirrored products path: %s", path.Join(mirrorInfo.MirrorURL, mirrorInfo.Path)) 536 indexRef.Source = NewURLDataSource(mirrorInfo.MirrorURL, VerifySSLHostnames) 537 indexRef.MirroredProductsPath = mirrorInfo.Path 538 } else { 539 logger.Debugf("no mirror information available for %s: %v", cloudSpec, err) 540 } 541 } 542 543 return indexRef, nil 544 } 545 546 // getMirrorRefs parses and returns a simplestreams mirror reference. 547 func getMirrorRefs(source DataSource, baseMirrorsPath string, requireSigned bool, 548 params ValueParams) (MirrorRefs, string, error) { 549 550 mirrorsPath := baseMirrorsPath + UnsignedSuffix 551 if requireSigned { 552 mirrorsPath = baseMirrorsPath + signedSuffix 553 } 554 var mirrors MirrorRefs 555 data, url, err := fetchData(source, mirrorsPath, requireSigned, params.PublicKey) 556 if err != nil { 557 if errors.IsNotFoundError(err) || errors.IsUnauthorizedError(err) { 558 logger.Debugf("no mirror index file found") 559 return mirrors, url, err 560 } 561 return mirrors, url, fmt.Errorf("cannot read mirrors data, %v", err) 562 } 563 err = json.Unmarshal(data, &mirrors) 564 if err != nil { 565 return mirrors, url, fmt.Errorf("cannot unmarshal JSON mirror metadata at URL %q: %v", url, err) 566 } 567 return mirrors, url, err 568 } 569 570 // getMirror returns a mirror info struct matching the specified content and cloud. 571 func getMirror(source DataSource, mirrors MirrorRefs, datatype, contentId string, cloudSpec CloudSpec, 572 requireSigned bool, publicKey string) (*MirrorInfo, error) { 573 574 mirrorRef, err := mirrors.getMirrorReference(datatype, contentId, cloudSpec) 575 if err != nil { 576 return nil, err 577 } 578 mirrorInfo, err := mirrorRef.getMirrorInfo(source, contentId, cloudSpec, "mirrors:1.0", requireSigned, publicKey) 579 if err != nil { 580 return nil, err 581 } 582 if mirrorInfo == nil { 583 return nil, errors.NotFoundf("mirror metadata for %q and cloud %v", contentId, cloudSpec) 584 } 585 return mirrorInfo, nil 586 } 587 588 // GetProductsPath returns the path to the metadata file containing products for the specified constraint. 589 // Exported for testing. 590 func (indexRef *IndexReference) GetProductsPath(cons LookupConstraint) (string, error) { 591 if indexRef.MirroredProductsPath != "" { 592 return indexRef.MirroredProductsPath, nil 593 } 594 prodIds, err := cons.Ids() 595 if err != nil { 596 return "", err 597 } 598 candidates := indexRef.extractIndexes() 599 // Restrict to image-ids entries. 600 dataTypeMatches := func(metadata *IndexMetadata) bool { 601 return metadata.DataType == indexRef.valueParams.DataType 602 } 603 candidates = candidates.filter(dataTypeMatches) 604 if len(candidates) == 0 { 605 return "", errors.NotFoundf("index file missing %q data", indexRef.valueParams.DataType) 606 } 607 // Restrict by cloud spec, if required. 608 if cons.Params().CloudSpec != EmptyCloudSpec { 609 hasRightCloud := func(metadata *IndexMetadata) bool { 610 return metadata.hasCloud(cons.Params().CloudSpec) 611 } 612 candidates = candidates.filter(hasRightCloud) 613 if len(candidates) == 0 { 614 return "", errors.NotFoundf("index file has no data for cloud %v", cons.Params().CloudSpec) 615 } 616 } 617 // Restrict by product IDs. 618 hasProduct := func(metadata *IndexMetadata) bool { 619 return metadata.hasProduct(prodIds) 620 } 621 candidates = candidates.filter(hasProduct) 622 if len(candidates) == 0 { 623 return "", newNoMatchingProductsError("index file has no data for product name(s) %q", prodIds) 624 } 625 626 logger.Debugf("candidate matches for products %q are %v", prodIds, candidates) 627 628 // Pick arbitrary match. 629 return candidates[0].ProductsFilePath, nil 630 } 631 632 // extractMirrorRefs returns just the array of MirrorRef structs for the contentId, in arbitrary order. 633 func (mirrorRefs *MirrorRefs) extractMirrorRefs(contentId string) MirrorRefSlice { 634 for id, refs := range mirrorRefs.Mirrors { 635 if id == contentId { 636 return refs 637 } 638 } 639 return nil 640 } 641 642 // hasCloud tells you whether a MirrorReference has the given cloud in its 643 // Clouds list. 644 func (mirrorRef *MirrorReference) hasCloud(cloud CloudSpec) bool { 645 for _, refCloud := range mirrorRef.Clouds { 646 if refCloud.equals(&cloud) { 647 return true 648 } 649 } 650 return false 651 } 652 653 // GetMirrorReference returns the reference to the metadata file containing mirrors for the specified content and cloud. 654 func (mirrorRefs *MirrorRefs) getMirrorReference(datatype, contentId string, cloud CloudSpec) (*MirrorReference, error) { 655 candidates := mirrorRefs.extractMirrorRefs(contentId) 656 if len(candidates) == 0 { 657 return nil, errors.NotFoundf("mirror data for %q", contentId) 658 } 659 // Restrict by cloud spec and datatype. 660 hasRightCloud := func(mirrorRef *MirrorReference) bool { 661 return mirrorRef.hasCloud(cloud) && mirrorRef.DataType == datatype 662 } 663 matchingCandidates := candidates.filter(hasRightCloud) 664 if len(matchingCandidates) == 0 { 665 // No cloud specific mirrors found so look for a non cloud specific mirror. 666 for _, candidate := range candidates { 667 if len(candidate.Clouds) == 0 { 668 logger.Debugf("using default candidate for content id %q are %v", contentId, candidate) 669 return &candidate, nil 670 } 671 } 672 return nil, errors.NotFoundf("index file with cloud %v", cloud) 673 } 674 675 logger.Debugf("candidate matches for content id %q are %v", contentId, candidates) 676 677 // Pick arbitrary match. 678 return &matchingCandidates[0], nil 679 } 680 681 // getMirrorInfo returns mirror information from the mirror file at the given path for the specified content and cloud. 682 func (mirrorRef *MirrorReference) getMirrorInfo(source DataSource, contentId string, cloud CloudSpec, format string, 683 requireSigned bool, publicKey string) (*MirrorInfo, error) { 684 685 metadata, err := GetMirrorMetadataWithFormat(source, mirrorRef.Path, format, requireSigned, publicKey) 686 if err != nil { 687 return nil, err 688 } 689 mirrorInfo, err := metadata.getMirrorInfo(contentId, cloud) 690 if err != nil { 691 return nil, err 692 } 693 return mirrorInfo, nil 694 } 695 696 // GetMirrorMetadataWithFormat returns simplestreams mirror data of the specified format. 697 // Exported for testing. 698 func GetMirrorMetadataWithFormat(source DataSource, mirrorPath, format string, 699 requireSigned bool, publicKey string) (*MirrorMetadata, error) { 700 701 data, url, err := fetchData(source, mirrorPath, requireSigned, publicKey) 702 if err != nil { 703 if errors.IsNotFoundError(err) || errors.IsUnauthorizedError(err) { 704 return nil, err 705 } 706 return nil, fmt.Errorf("cannot read mirror data, %v", err) 707 } 708 var mirrors MirrorMetadata 709 err = json.Unmarshal(data, &mirrors) 710 if err != nil { 711 return nil, fmt.Errorf("cannot unmarshal JSON mirror metadata at URL %q: %v", url, err) 712 } 713 if mirrors.Format != format { 714 return nil, fmt.Errorf("unexpected mirror file format %q, expected %q at URL %q", mirrors.Format, format, url) 715 } 716 return &mirrors, nil 717 } 718 719 // hasCloud tells you whether an MirrorInfo has the given cloud in its 720 // Clouds list. 721 func (mirrorInfo *MirrorInfo) hasCloud(cloud CloudSpec) bool { 722 for _, metadataCloud := range mirrorInfo.Clouds { 723 if metadataCloud.equals(&cloud) { 724 return true 725 } 726 } 727 return false 728 } 729 730 // getMirrorInfo returns the mirror metadata for the specified content and cloud. 731 func (mirrorMetadata *MirrorMetadata) getMirrorInfo(contentId string, cloud CloudSpec) (*MirrorInfo, error) { 732 var candidates MirrorInfoSlice 733 for id, m := range mirrorMetadata.Mirrors { 734 if id == contentId { 735 candidates = m 736 break 737 } 738 } 739 if len(candidates) == 0 { 740 return nil, errors.NotFoundf("mirror info for %q", contentId) 741 } 742 743 // Restrict by cloud spec. 744 hasRightCloud := func(mirrorInfo *MirrorInfo) bool { 745 return mirrorInfo.hasCloud(cloud) 746 } 747 candidates = candidates.filter(hasRightCloud) 748 if len(candidates) == 0 { 749 return nil, errors.NotFoundf("mirror info with cloud %v", cloud) 750 } 751 752 // Pick arbitrary match. 753 return &candidates[0], nil 754 } 755 756 // utility function to see if element exists in values slice. 757 func containsString(values []string, element string) bool { 758 for _, value := range values { 759 if value == element { 760 return true 761 } 762 } 763 return false 764 } 765 766 // To keep the metadata concise, attributes on the metadata struct which have the same value for each 767 // item may be moved up to a higher level in the tree. denormaliseMetadata descends the tree 768 // and fills in any missing attributes with values from a higher level. 769 func (metadata *CloudMetadata) denormaliseMetadata() { 770 for _, metadataCatalog := range metadata.Products { 771 for _, ItemCollection := range metadataCatalog.Items { 772 for _, item := range ItemCollection.Items { 773 coll := *ItemCollection 774 inherit(&coll, metadataCatalog) 775 inherit(item, &coll) 776 } 777 } 778 } 779 } 780 781 // inherit sets any blank fields in dst to their equivalent values in fields in src that have matching json tags. 782 // The dst parameter must be a pointer to a struct. 783 func inherit(dst, src interface{}) { 784 for tag := range tags(dst) { 785 setFieldByTag(dst, tag, fieldByTag(src, tag), false) 786 } 787 } 788 789 // processAliases looks through the struct fields to see if 790 // any aliases apply, and sets attributes appropriately if so. 791 func (metadata *CloudMetadata) processAliases(item interface{}) { 792 for tag := range tags(item) { 793 aliases, ok := metadata.Aliases[tag] 794 if !ok { 795 continue 796 } 797 // We have found a set of aliases for one of the fields in the metadata struct. 798 // Now check to see if the field matches one of the defined aliases. 799 fields, ok := aliases[fieldByTag(item, tag)] 800 if !ok { 801 continue 802 } 803 // The alias matches - set all the aliased fields in the struct. 804 for attr, val := range fields { 805 setFieldByTag(item, attr, val, true) 806 } 807 } 808 } 809 810 // Apply any attribute aliases to the metadata records. 811 func (metadata *CloudMetadata) applyAliases() { 812 for _, metadataCatalog := range metadata.Products { 813 for _, ItemCollection := range metadataCatalog.Items { 814 for _, item := range ItemCollection.Items { 815 metadata.processAliases(item) 816 } 817 } 818 } 819 } 820 821 // construct iterates over the metadata records and replaces the generic maps of values 822 // with structs of the required type. 823 func (metadata *CloudMetadata) construct(valueType reflect.Type) error { 824 for _, metadataCatalog := range metadata.Products { 825 for _, ItemCollection := range metadataCatalog.Items { 826 if err := ItemCollection.construct(valueType); err != nil { 827 return err 828 } 829 } 830 } 831 return nil 832 } 833 834 type structTags map[reflect.Type]map[string]int 835 836 var tagsForType structTags = make(structTags) 837 838 // RegisterStructTags ensures the json tags for the given structs are able to be used 839 // when parsing the simplestreams metadata. 840 func RegisterStructTags(vals ...interface{}) { 841 tags := mkTags(vals...) 842 for k, v := range tags { 843 tagsForType[k] = v 844 } 845 } 846 847 func init() { 848 RegisterStructTags(MetadataCatalog{}, ItemCollection{}) 849 } 850 851 func mkTags(vals ...interface{}) map[reflect.Type]map[string]int { 852 typeMap := make(map[reflect.Type]map[string]int) 853 for _, v := range vals { 854 t := reflect.TypeOf(v) 855 typeMap[t] = jsonTags(t) 856 } 857 return typeMap 858 } 859 860 // jsonTags returns a map from json tag to the field index for the string fields in the given type. 861 func jsonTags(t reflect.Type) map[string]int { 862 if t.Kind() != reflect.Struct { 863 panic(fmt.Errorf("cannot get json tags on type %s", t)) 864 } 865 tags := make(map[string]int) 866 for i := 0; i < t.NumField(); i++ { 867 f := t.Field(i) 868 if f.Type != reflect.TypeOf("") { 869 continue 870 } 871 if tag := f.Tag.Get("json"); tag != "" { 872 if i := strings.Index(tag, ","); i >= 0 { 873 tag = tag[0:i] 874 } 875 if tag == "-" { 876 continue 877 } 878 if tag != "" { 879 f.Name = tag 880 } 881 } 882 tags[f.Name] = i 883 } 884 return tags 885 } 886 887 // tags returns the field offsets for the JSON tags defined by the given value, which must be 888 // a struct or a pointer to a struct. 889 func tags(x interface{}) map[string]int { 890 t := reflect.TypeOf(x) 891 if t.Kind() == reflect.Ptr { 892 t = t.Elem() 893 } 894 if t.Kind() != reflect.Struct { 895 panic(fmt.Errorf("expected struct, not %s", t)) 896 } 897 898 if tagm := tagsForType[t]; tagm != nil { 899 return tagm 900 } 901 panic(fmt.Errorf("%s not found in type table", t)) 902 } 903 904 // fieldByTag returns the value for the field in x with the given JSON tag, or "" if there is no such field. 905 func fieldByTag(x interface{}, tag string) string { 906 tagm := tags(x) 907 v := reflect.ValueOf(x) 908 if v.Kind() == reflect.Ptr { 909 v = v.Elem() 910 } 911 if i, ok := tagm[tag]; ok { 912 return v.Field(i).Interface().(string) 913 } 914 return "" 915 } 916 917 // setFieldByTag sets the value for the field in x with the given JSON tag to val. 918 // The override parameter specifies whether the value will be set even if the original value is non-empty. 919 func setFieldByTag(x interface{}, tag, val string, override bool) { 920 i, ok := tags(x)[tag] 921 if !ok { 922 return 923 } 924 v := reflect.ValueOf(x).Elem() 925 f := v.Field(i) 926 if override || f.Interface().(string) == "" { 927 f.Set(reflect.ValueOf(val)) 928 } 929 } 930 931 // GetCloudMetadataWithFormat loads the entire cloud metadata encoded using the specified format. 932 // Exported for testing. 933 func (indexRef *IndexReference) GetCloudMetadataWithFormat(cons LookupConstraint, format string, requireSigned bool) (*CloudMetadata, error) { 934 productFilesPath, err := indexRef.GetProductsPath(cons) 935 if err != nil { 936 return nil, err 937 } 938 logger.Debugf("finding products at path %q", productFilesPath) 939 data, url, err := fetchData(indexRef.Source, productFilesPath, requireSigned, indexRef.valueParams.PublicKey) 940 if err != nil { 941 return nil, fmt.Errorf("cannot read product data, %v", err) 942 } 943 return ParseCloudMetadata(data, format, url, indexRef.valueParams.ValueTemplate) 944 } 945 946 // ParseCloudMetadata parses the given bytes into simplestreams metadata. 947 func ParseCloudMetadata(data []byte, format, url string, valueTemplate interface{}) (*CloudMetadata, error) { 948 var metadata CloudMetadata 949 err := json.Unmarshal(data, &metadata) 950 if err != nil { 951 return nil, fmt.Errorf("cannot unmarshal JSON metadata at URL %q: %v", url, err) 952 } 953 if metadata.Format != format { 954 return nil, fmt.Errorf("unexpected index file format %q, expected %q at URL %q", metadata.Format, format, url) 955 } 956 if valueTemplate != nil { 957 err = metadata.construct(reflect.TypeOf(valueTemplate)) 958 } 959 if err != nil { 960 return nil, fmt.Errorf("cannot unmarshal JSON metadata at URL %q: %v", url, err) 961 } 962 metadata.applyAliases() 963 metadata.denormaliseMetadata() 964 return &metadata, nil 965 } 966 967 // getLatestMetadataWithFormat loads the metadata for the given cloud and orders the resulting structs 968 // starting with the most recent, and returns items which match the product criteria, choosing from the 969 // latest versions first. 970 func (indexRef *IndexReference) getLatestMetadataWithFormat(cons LookupConstraint, format string, requireSigned bool) ([]interface{}, error) { 971 metadata, err := indexRef.GetCloudMetadataWithFormat(cons, format, requireSigned) 972 if err != nil { 973 return nil, err 974 } 975 logger.Debugf("metadata: %v", metadata) 976 matches, err := GetLatestMetadata(metadata, cons, indexRef.Source, indexRef.valueParams.FilterFunc) 977 if err != nil { 978 return nil, err 979 } 980 if len(matches) == 0 { 981 return nil, newNoMatchingProductsError("index has no matching records") 982 } 983 return matches, nil 984 } 985 986 // GetLatestMetadata extracts and returns the metadata records matching the given criteria. 987 func GetLatestMetadata(metadata *CloudMetadata, cons LookupConstraint, source DataSource, filterFunc appendMatchingFunc) ([]interface{}, error) { 988 prodIds, err := cons.Ids() 989 if err != nil { 990 return nil, err 991 } 992 993 catalogs := metadata.extractCatalogsForProducts(prodIds) 994 if len(catalogs) == 0 { 995 availableProducts := make([]string, 0, len(metadata.Products)) 996 for product := range metadata.Products { 997 availableProducts = append(availableProducts, product) 998 } 999 return nil, newNoMatchingProductsError( 1000 "index has no records for product ids %v; it does have product ids %v", prodIds, availableProducts) 1001 } 1002 1003 var matchingItems []interface{} 1004 for _, catalog := range catalogs { 1005 var bv byVersionDesc = make(byVersionDesc, len(catalog.Items)) 1006 i := 0 1007 for vers, itemColl := range catalog.Items { 1008 bv[i] = collectionVersion{vers, itemColl} 1009 i++ 1010 } 1011 sort.Sort(bv) 1012 for _, itemCollVersion := range bv { 1013 matchingItems = filterFunc(source, matchingItems, itemCollVersion.ItemCollection.Items, cons) 1014 } 1015 } 1016 return matchingItems, nil 1017 } 1018 1019 type collectionVersion struct { 1020 version string 1021 ItemCollection *ItemCollection 1022 } 1023 1024 // byVersionDesc is used to sort a slice of collections in descending order of their 1025 // version in YYYYMMDD. 1026 type byVersionDesc []collectionVersion 1027 1028 func (bv byVersionDesc) Len() int { return len(bv) } 1029 func (bv byVersionDesc) Swap(i, j int) { 1030 bv[i], bv[j] = bv[j], bv[i] 1031 } 1032 func (bv byVersionDesc) Less(i, j int) bool { 1033 return bv[i].version > bv[j].version 1034 }