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  }