github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/environs/tools/simplestreams.go (about) 1 // Copyright 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package tools 5 6 import ( 7 "bytes" 8 "crypto/sha256" 9 "encoding/json" 10 "fmt" 11 "hash" 12 "io" 13 "path" 14 "sort" 15 "time" 16 17 "github.com/juju/collections/set" 18 "github.com/juju/errors" 19 "github.com/juju/version/v2" 20 21 "github.com/juju/juju/core/os/ostype" 22 "github.com/juju/juju/environs/simplestreams" 23 "github.com/juju/juju/environs/storage" 24 coretools "github.com/juju/juju/tools" 25 ) 26 27 func init() { 28 simplestreams.RegisterStructTags(ToolsMetadata{}) 29 } 30 31 const ( 32 // ContentDownload is the simplestreams tools content type. 33 ContentDownload = "content-download" 34 35 // StreamsVersionV1 is used to construct the path for accessing streams data. 36 StreamsVersionV1 = "v1" 37 38 // IndexFileVersion is used to construct the streams index file. 39 IndexFileVersion = 2 40 41 // streamsAgentURL is the path to the default simplestreams agent metadata. 42 streamsAgentURL = "https://streams.canonical.com/juju/tools" 43 ) 44 45 var currentStreamsVersion = StreamsVersionV1 46 47 // This needs to be a var so we can override it for testing. 48 var DefaultBaseURL = streamsAgentURL 49 50 // toolsReleaseAltMapping is a simple table that can be used when generating 51 // metadata for a tools tarball by finding alternative release names to create 52 // metadata for. 53 var toolsReleaseAltMapping = map[string][]string{ 54 "linux": {"ubuntu", "centos", "genericlinux"}, 55 "darwin": {"osx"}, 56 } 57 58 const ( 59 // Used to specify the released tools metadata. 60 ReleasedStream = "released" 61 62 // Used to specify metadata for testing tools. 63 TestingStream = "testing" 64 65 // Used to specify the proposed tools metadata. 66 ProposedStream = "proposed" 67 68 // Used to specify the devel tools metadata. 69 DevelStream = "devel" 70 ) 71 72 // ToolsConstraint defines criteria used to find a tools metadata record. 73 type ToolsConstraint struct { 74 simplestreams.LookupParams 75 Version version.Number 76 MajorVersion int 77 MinorVersion int 78 } 79 80 // NewVersionedToolsConstraint returns a ToolsConstraint for a tools with a specific version. 81 func NewVersionedToolsConstraint(vers version.Number, params simplestreams.LookupParams) *ToolsConstraint { 82 return &ToolsConstraint{LookupParams: params, Version: vers} 83 } 84 85 // NewGeneralToolsConstraint returns a ToolsConstraint for tools with matching major/minor version numbers. 86 func NewGeneralToolsConstraint(majorVersion, minorVersion int, params simplestreams.LookupParams) *ToolsConstraint { 87 return &ToolsConstraint{LookupParams: params, Version: version.Zero, 88 MajorVersion: majorVersion, MinorVersion: minorVersion} 89 } 90 91 // IndexIds generates a string array representing product ids formed similarly to an ISCSI qualified name (IQN). 92 func (tc *ToolsConstraint) IndexIds() []string { 93 if tc.Stream == "" { 94 return nil 95 } 96 return []string{ToolsContentId(tc.Stream)} 97 } 98 99 // ProductIds generates a string array representing product ids formed similarly to an ISCSI qualified name (IQN). 100 func (tc *ToolsConstraint) ProductIds() ([]string, error) { 101 var allIds []string 102 for _, release := range tc.Releases { 103 if !ostype.IsValidOSTypeName(release) { 104 logger.Debugf("ignoring unknown os type %q", release) 105 continue 106 } 107 ids := make([]string, len(tc.Arches)) 108 for i, arch := range tc.Arches { 109 ids[i] = fmt.Sprintf("com.ubuntu.juju:%s:%s", release, arch) 110 } 111 allIds = append(allIds, ids...) 112 } 113 return allIds, nil 114 } 115 116 // ToolsMetadata holds information about a particular tools tarball. 117 type ToolsMetadata struct { 118 Release string `json:"release"` 119 Version string `json:"version"` 120 Arch string `json:"arch"` 121 Size int64 `json:"size"` 122 Path string `json:"path"` 123 FullPath string `json:"-"` 124 FileType string `json:"ftype"` 125 SHA256 string `json:"sha256"` 126 } 127 128 func (t *ToolsMetadata) String() string { 129 return fmt.Sprintf("%+v", *t) 130 } 131 132 // sortString is used by byVersion to sort a list of ToolsMetadata. 133 func (t *ToolsMetadata) sortString() string { 134 return fmt.Sprintf("%v-%s-%s", t.Version, t.Release, t.Arch) 135 } 136 137 // binary returns the tools metadata's binary version, which may be used for 138 // map lookup. 139 func (t *ToolsMetadata) binary() (version.Binary, error) { 140 num, err := version.Parse(t.Version) 141 if err != nil { 142 return version.Binary{}, errors.Trace(err) 143 } 144 return version.Binary{ 145 Number: num, 146 Release: t.Release, 147 Arch: t.Arch, 148 }, nil 149 } 150 151 func (t *ToolsMetadata) productId() (string, error) { 152 if !ostype.IsValidOSTypeName(t.Release) { 153 return "", errors.NotValidf("os type %q", t.Release) 154 } 155 return fmt.Sprintf("com.ubuntu.juju:%s:%s", t.Release, t.Arch), nil 156 } 157 158 // SimplestreamsFetcher defines a way to fetch metadata from the simplestreams 159 // server. 160 type SimplestreamsFetcher interface { 161 NewDataSource(simplestreams.Config) simplestreams.DataSource 162 GetMetadata([]simplestreams.DataSource, simplestreams.GetMetadataParams) ([]interface{}, *simplestreams.ResolveInfo, error) 163 } 164 165 // Fetch returns a list of tools for the specified cloud matching the constraint. 166 // The base URL locations are as specified - the first location which has a file is the one used. 167 // Signed data is preferred, but if there is no signed data available and onlySigned is false, 168 // then unsigned data is used. 169 func Fetch(ss SimplestreamsFetcher, sources []simplestreams.DataSource, cons *ToolsConstraint, 170 ) ([]*ToolsMetadata, *simplestreams.ResolveInfo, error) { 171 params := simplestreams.GetMetadataParams{ 172 StreamsVersion: currentStreamsVersion, 173 LookupConstraint: cons, 174 ValueParams: simplestreams.ValueParams{ 175 DataType: ContentDownload, 176 FilterFunc: appendMatchingTools, 177 MirrorContentId: ToolsContentId(cons.Stream), 178 ValueTemplate: ToolsMetadata{}, 179 }, 180 } 181 items, resolveInfo, err := ss.GetMetadata(sources, params) 182 if err != nil { 183 return nil, nil, err 184 } 185 metadata := make([]*ToolsMetadata, len(items)) 186 for i, md := range items { 187 metadata[i] = md.(*ToolsMetadata) 188 } 189 // Sorting the metadata is not strictly necessary, but it ensures consistent ordering for 190 // all compilers, and it just makes it easier to look at the data. 191 Sort(metadata) 192 return metadata, resolveInfo, nil 193 } 194 195 // Sort sorts a slice of ToolsMetadata in ascending order of their version 196 // in order to ensure the results of Fetch are ordered deterministically. 197 func Sort(metadata []*ToolsMetadata) { 198 sort.Sort(byVersion(metadata)) 199 } 200 201 type byVersion []*ToolsMetadata 202 203 func (b byVersion) Len() int { return len(b) } 204 func (b byVersion) Swap(i, j int) { b[i], b[j] = b[j], b[i] } 205 func (b byVersion) Less(i, j int) bool { return b[i].sortString() < b[j].sortString() } 206 207 // appendMatchingTools updates matchingTools with tools metadata records from tools which belong to the 208 // specified os type. If a tools record already exists in matchingTools, it is not overwritten. 209 func appendMatchingTools(source simplestreams.DataSource, matchingTools []interface{}, 210 tools map[string]interface{}, cons simplestreams.LookupConstraint) ([]interface{}, error) { 211 212 toolsMap := make(map[version.Binary]*ToolsMetadata, len(matchingTools)) 213 for _, val := range matchingTools { 214 tm := val.(*ToolsMetadata) 215 binary, err := tm.binary() 216 if err != nil { 217 return nil, errors.Trace(err) 218 } 219 toolsMap[binary] = tm 220 } 221 for _, val := range tools { 222 tm := val.(*ToolsMetadata) 223 if !set.NewStrings(cons.Params().Releases...).Contains(tm.Release) { 224 continue 225 } 226 if toolsConstraint, ok := cons.(*ToolsConstraint); ok { 227 tmNumber := version.MustParse(tm.Version) 228 if toolsConstraint.Version == version.Zero { 229 if toolsConstraint.MajorVersion > 0 && toolsConstraint.MajorVersion != tmNumber.Major { 230 continue 231 } 232 if toolsConstraint.MinorVersion >= 0 && toolsConstraint.MinorVersion != tmNumber.Minor { 233 continue 234 } 235 } else { 236 if toolsConstraint.Version != tmNumber { 237 continue 238 } 239 } 240 } 241 binary, err := tm.binary() 242 if err != nil { 243 return nil, errors.Trace(err) 244 } 245 if _, ok := toolsMap[binary]; !ok { 246 tm.FullPath, _ = source.URL(tm.Path) 247 matchingTools = append(matchingTools, tm) 248 } 249 } 250 return matchingTools, nil 251 } 252 253 type MetadataFile struct { 254 Path string 255 Data []byte 256 } 257 258 // MetadataFromTools returns a tools metadata list derived from the 259 // given tools list. The size and sha256 will not be computed if 260 // missing. 261 func MetadataFromTools(toolsList coretools.List, toolsDir string) []*ToolsMetadata { 262 metadata := make([]*ToolsMetadata, 0, len(toolsList)) 263 for _, t := range toolsList { 264 toolNamedRelease := t.Version.Release 265 allToolReleases := append(toolsReleaseAltMapping[toolNamedRelease], toolNamedRelease) 266 267 for _, release := range allToolReleases { 268 path := fmt.Sprintf("%s/juju-%s-%s-%s.tgz", toolsDir, t.Version.Number, toolNamedRelease, t.Version.Arch) 269 metadata = append(metadata, &ToolsMetadata{ 270 Release: release, 271 Version: t.Version.Number.String(), 272 Arch: t.Version.Arch, 273 Path: path, 274 FileType: "tar.gz", 275 Size: t.Size, 276 SHA256: t.SHA256, 277 }) 278 } 279 } 280 return metadata 281 } 282 283 // ResolveMetadata resolves incomplete metadata 284 // by fetching the tools from storage and computing 285 // the size and hash locally. 286 func ResolveMetadata(stor storage.StorageReader, toolsDir string, metadata []*ToolsMetadata) error { 287 for _, md := range metadata { 288 if md.Size != 0 { 289 continue 290 } 291 binary, err := md.binary() 292 if err != nil { 293 return errors.Annotate(err, "cannot resolve metadata") 294 } 295 logger.Infof("Fetching agent binaries from dir %q to generate hash: %v", toolsDir, binary) 296 size, sha256hash, err := fetchToolsHash(stor, md.Path) 297 if err != nil { 298 return err 299 } 300 md.Size = size 301 md.SHA256 = fmt.Sprintf("%x", sha256hash.Sum(nil)) 302 } 303 return nil 304 } 305 306 // MergeMetadata merges the given tools metadata. 307 // If metadata for the same tools version exists in both lists, 308 // an entry with non-empty size/SHA256 takes precedence; if 309 // the two entries have different sizes/hashes, then an error is 310 // returned. 311 func MergeMetadata(tmlist1, tmlist2 []*ToolsMetadata) ([]*ToolsMetadata, error) { 312 merged := make(map[version.Binary]*ToolsMetadata) 313 for _, tm := range tmlist1 { 314 binary, err := tm.binary() 315 if err != nil { 316 return nil, errors.Annotate(err, "cannot merge metadata") 317 } 318 merged[binary] = tm 319 } 320 for _, tm := range tmlist2 { 321 binary, err := tm.binary() 322 if err != nil { 323 return nil, errors.Annotate(err, "cannot merge metadata") 324 } 325 if existing, ok := merged[binary]; ok { 326 if tm.Size != 0 { 327 if existing.Size == 0 { 328 merged[binary] = tm 329 } else if existing.Size != tm.Size || existing.SHA256 != tm.SHA256 { 330 return nil, fmt.Errorf( 331 "metadata mismatch for %s: sizes=(%v,%v) sha256=(%v,%v)", 332 binary.String(), 333 existing.Size, tm.Size, 334 existing.SHA256, tm.SHA256, 335 ) 336 } 337 } 338 } else { 339 merged[binary] = tm 340 } 341 } 342 list := make([]*ToolsMetadata, 0, len(merged)) 343 for _, metadata := range merged { 344 list = append(list, metadata) 345 } 346 Sort(list) 347 return list, nil 348 } 349 350 // ReadMetadata returns the tools metadata from the given storage for the specified stream. 351 func ReadMetadata(ss SimplestreamsFetcher, store storage.StorageReader, stream string) ([]*ToolsMetadata, error) { 352 dataSource := storage.NewStorageSimpleStreamsDataSource("existing metadata", store, storage.BaseToolsPath, simplestreams.EXISTING_CLOUD_DATA, false) 353 toolsConstraint, err := makeToolsConstraint(simplestreams.CloudSpec{}, stream, -1, -1, coretools.Filter{}) 354 if err != nil { 355 return nil, err 356 } 357 metadata, _, err := Fetch(ss, []simplestreams.DataSource{dataSource}, toolsConstraint) 358 if err != nil && !errors.IsNotFound(err) { 359 return nil, err 360 } 361 return metadata, nil 362 } 363 364 // AllMetadataStreams is the set of streams for which there will be simplestreams tools metadata. 365 var AllMetadataStreams = []string{ReleasedStream, ProposedStream, TestingStream, DevelStream} 366 367 // ReadAllMetadata returns the tools metadata from the given storage for all streams. 368 // The result is a map of metadata slices, keyed on stream. 369 func ReadAllMetadata(ss SimplestreamsFetcher, store storage.StorageReader) (map[string][]*ToolsMetadata, error) { 370 streamMetadata := make(map[string][]*ToolsMetadata) 371 for _, stream := range AllMetadataStreams { 372 metadata, err := ReadMetadata(ss, store, stream) 373 if err != nil { 374 return nil, err 375 } 376 if len(metadata) == 0 { 377 continue 378 } 379 streamMetadata[stream] = metadata 380 } 381 return streamMetadata, nil 382 } 383 384 // removeMetadataUpdated unmarshalls simplestreams metadata, clears the 385 // updated attribute, and then marshalls back to a string. 386 func removeMetadataUpdated(metadataBytes []byte) (string, error) { 387 var metadata map[string]interface{} 388 err := json.Unmarshal(metadataBytes, &metadata) 389 if err != nil { 390 return "", err 391 } 392 delete(metadata, "updated") 393 394 metadataJson, err := json.Marshal(metadata) 395 if err != nil { 396 return "", err 397 } 398 return string(metadataJson), nil 399 } 400 401 // metadataUnchanged returns true if the content of metadata for stream in stor is the same 402 // as generatedMetadata, ignoring the "updated" attribute. 403 func metadataUnchanged(stor storage.Storage, stream string, generatedMetadata []byte) (bool, error) { 404 mdPath := ProductMetadataPath(stream) 405 filePath := path.Join(storage.BaseToolsPath, mdPath) 406 existingDataReader, err := stor.Get(filePath) 407 // If the file can't be retrieved, consider it has changed. 408 if err != nil { 409 return false, nil 410 } 411 defer existingDataReader.Close() 412 existingData, err := io.ReadAll(existingDataReader) 413 if err != nil { 414 return false, err 415 } 416 417 // To do the comparison, we unmarshall the metadata, clear the 418 // updated value, and marshall back to a string. 419 existingMetadata, err := removeMetadataUpdated(existingData) 420 if err != nil { 421 return false, err 422 } 423 newMetadata, err := removeMetadataUpdated(generatedMetadata) 424 if err != nil { 425 return false, err 426 } 427 return existingMetadata == newMetadata, nil 428 } 429 430 // WriteMetadata writes the given tools metadata for the specified streams to the given storage. 431 // streamMetadata contains all known metadata so that the correct index files can be written. 432 // Only product files for the specified streams are written. 433 func WriteMetadata(stor storage.Storage, streamMetadata map[string][]*ToolsMetadata, streams []string, writeMirrors ShouldWriteMirrors) error { 434 // TODO(perrito666) 2016-05-02 lp:1558657 435 updated := time.Now() 436 index, legacyIndex, products, err := MarshalToolsMetadataJSON(streamMetadata, updated) 437 if err != nil { 438 return err 439 } 440 metadataInfo := []MetadataFile{ 441 {simplestreams.UnsignedIndex(currentStreamsVersion, IndexFileVersion), index}, 442 } 443 if legacyIndex != nil { 444 metadataInfo = append(metadataInfo, MetadataFile{ 445 simplestreams.UnsignedIndex(currentStreamsVersion, 1), legacyIndex, 446 }) 447 } 448 for _, stream := range streams { 449 if metadata, ok := products[stream]; ok { 450 // If metadata hasn't changed, do not overwrite. 451 unchanged, err := metadataUnchanged(stor, stream, metadata) 452 if err != nil { 453 return err 454 } 455 if unchanged { 456 logger.Infof("Metadata for stream %q unchanged", stream) 457 continue 458 } 459 // Metadata is different, so include it. 460 metadataInfo = append(metadataInfo, MetadataFile{ProductMetadataPath(stream), metadata}) 461 } 462 } 463 if writeMirrors { 464 streamsMirrorsMetadata := make(map[string][]simplestreams.MirrorReference) 465 for stream := range streamMetadata { 466 streamsMirrorsMetadata[ToolsContentId(stream)] = []simplestreams.MirrorReference{{ 467 Updated: updated.Format("20060102"), // YYYYMMDD 468 DataType: ContentDownload, 469 Format: simplestreams.MirrorFormat, 470 Path: simplestreams.MirrorFile, 471 }} 472 } 473 mirrorsMetadata := map[string]map[string][]simplestreams.MirrorReference{ 474 "mirrors": streamsMirrorsMetadata, 475 } 476 mirrorsInfo, err := json.MarshalIndent(&mirrorsMetadata, "", " ") 477 if err != nil { 478 return err 479 } 480 metadataInfo = append( 481 metadataInfo, MetadataFile{simplestreams.UnsignedMirror(currentStreamsVersion), mirrorsInfo}) 482 } 483 return writeMetadataFiles(stor, metadataInfo) 484 } 485 486 var writeMetadataFiles = func(stor storage.Storage, metadataInfo []MetadataFile) error { 487 for _, md := range metadataInfo { 488 filePath := path.Join(storage.BaseToolsPath, md.Path) 489 logger.Infof("Writing %s", filePath) 490 err := stor.Put(filePath, bytes.NewReader(md.Data), int64(len(md.Data))) 491 if err != nil { 492 return err 493 } 494 } 495 return nil 496 } 497 498 type ShouldWriteMirrors bool 499 500 const ( 501 WriteMirrors = ShouldWriteMirrors(true) 502 DoNotWriteMirrors = ShouldWriteMirrors(false) 503 ) 504 505 // MergeAndWriteMetadata reads the existing metadata from storage (if any), 506 // and merges it with metadata generated from the given tools list. The 507 // resulting metadata is written to storage. 508 func MergeAndWriteMetadata(ss SimplestreamsFetcher, store storage.Storage, toolsDir, stream string, tools coretools.List, writeMirrors ShouldWriteMirrors) error { 509 existing, err := ReadAllMetadata(ss, store) 510 if err != nil { 511 return err 512 } 513 metadata := MetadataFromTools(tools, toolsDir) 514 if metadata, err = MergeMetadata(metadata, existing[stream]); err != nil { 515 return err 516 } 517 existing[stream] = metadata 518 return WriteMetadata(store, existing, []string{stream}, writeMirrors) 519 } 520 521 // fetchToolsHash fetches the tools from storage and calculates 522 // its size in bytes and computes a SHA256 hash of its contents. 523 func fetchToolsHash(stor storage.StorageReader, toolsPath string) (size int64, sha256hash hash.Hash, err error) { 524 r, err := storage.Get(stor, fmt.Sprintf("tools/%s", toolsPath)) 525 if err != nil { 526 return 0, nil, err 527 } 528 defer r.Close() 529 sha256hash = sha256.New() 530 size, err = io.Copy(sha256hash, r) 531 return size, sha256hash, err 532 }