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