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