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