github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/environs/imagedownloads/simplestreams.go (about)

     1  // Copyright 2016 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package imagedownloads
     5  
     6  import (
     7  	"fmt"
     8  	"net/url"
     9  	"sort"
    10  
    11  	"github.com/juju/errors"
    12  
    13  	"github.com/juju/juju/core/arch"
    14  	corebase "github.com/juju/juju/core/base"
    15  	"github.com/juju/juju/environs/imagemetadata"
    16  	"github.com/juju/juju/environs/simplestreams"
    17  )
    18  
    19  func init() {
    20  	simplestreams.RegisterStructTags(Metadata{})
    21  }
    22  
    23  const (
    24  	// DataType is the simplestreams datatype.
    25  	DataType = "image-downloads"
    26  )
    27  
    28  // DefaultSource creates a new signed simplestreams datasource for use with the
    29  // image-downloads datatype.
    30  func DefaultSource(factory simplestreams.DataSourceFactory) func() simplestreams.DataSource {
    31  	ubuntuImagesURL := imagemetadata.UbuntuCloudImagesURL + "/" + imagemetadata.ReleasedImagesPath
    32  	return newDataSourceFunc(factory, ubuntuImagesURL)
    33  }
    34  
    35  // NewDataSource returns a new simplestreams.DataSource from the provided
    36  // baseURL. baseURL MUST include the image stream.
    37  func NewDataSource(factory simplestreams.DataSourceFactory, baseURL string) simplestreams.DataSource {
    38  	return newDataSourceFunc(factory, baseURL)()
    39  }
    40  
    41  // NewDataSource returns a datasourceFunc from the baseURL provided.
    42  func newDataSourceFunc(factory simplestreams.DataSourceFactory, baseURL string) func() simplestreams.DataSource {
    43  	return func() simplestreams.DataSource {
    44  		return factory.NewDataSource(
    45  			simplestreams.Config{
    46  				Description:          "ubuntu cloud images",
    47  				BaseURL:              baseURL,
    48  				PublicSigningKey:     imagemetadata.SimplestreamsImagesPublicKey,
    49  				HostnameVerification: true,
    50  				Priority:             simplestreams.DEFAULT_CLOUD_DATA,
    51  				RequireSigned:        true,
    52  			},
    53  		)
    54  	}
    55  }
    56  
    57  // Metadata models the information about a particular cloud image download
    58  // product.
    59  type Metadata struct {
    60  	Arch    string `json:"arch,omitempty"`
    61  	Release string `json:"release,omitempty"`
    62  	Version string `json:"version,omitempty"`
    63  	FType   string `json:"ftype,omitempty"`
    64  	SHA256  string `json:"sha256,omitempty"`
    65  	Path    string `json:"path,omitempty"`
    66  	Size    int64  `json:"size,omitempty"`
    67  }
    68  
    69  // DownloadURL returns the URL representing the image.
    70  func (m *Metadata) DownloadURL(baseURL string) (*url.URL, error) {
    71  	if baseURL == "" {
    72  		baseURL = imagemetadata.UbuntuCloudImagesURL
    73  	}
    74  	u, err := url.Parse(baseURL + "/" + m.Path)
    75  	if err != nil {
    76  		return nil, errors.Annotate(err, "failed to create url")
    77  	}
    78  	return u, nil
    79  }
    80  
    81  // Fetch gets product results as Metadata from the provided datasources, given
    82  // some constraints and an optional filter function.
    83  func Fetch(
    84  	fetcher imagemetadata.SimplestreamsFetcher,
    85  	src []simplestreams.DataSource,
    86  	cons *imagemetadata.ImageConstraint,
    87  	ff simplestreams.AppendMatchingFunc) ([]*Metadata, *simplestreams.ResolveInfo, error) {
    88  	if ff == nil {
    89  		ff = Filter("")
    90  	}
    91  	params := simplestreams.GetMetadataParams{
    92  		StreamsVersion:   imagemetadata.StreamsVersionV1,
    93  		LookupConstraint: cons,
    94  		ValueParams: simplestreams.ValueParams{
    95  			DataType:      DataType,
    96  			FilterFunc:    ff,
    97  			ValueTemplate: Metadata{},
    98  		},
    99  	}
   100  
   101  	items, resolveInfo, err := fetcher.GetMetadata(src, params)
   102  	if err != nil {
   103  		return nil, resolveInfo, err
   104  	}
   105  	md := make([]*Metadata, len(items))
   106  	for i, im := range items {
   107  		md[i] = im.(*Metadata)
   108  	}
   109  
   110  	Sort(md)
   111  
   112  	return md, resolveInfo, nil
   113  }
   114  
   115  func validateArgs(arch, release, ftype string) error {
   116  	bad := map[string]string{}
   117  
   118  	if !validArches[arch] {
   119  		bad[arch] = fmt.Sprintf("arch=%q", arch)
   120  	}
   121  
   122  	validVersion := false
   123  	workloadVersions, err := corebase.AllWorkloadVersions()
   124  	if err != nil {
   125  		return errors.Trace(err)
   126  	}
   127  	for _, supported := range workloadVersions.Values() {
   128  		if release == supported {
   129  			validVersion = true
   130  			break
   131  		}
   132  	}
   133  	if !validVersion {
   134  		bad[release] = fmt.Sprintf("version=%q", release)
   135  	}
   136  
   137  	if !validFTypes[ftype] {
   138  		bad[ftype] = fmt.Sprintf("ftype=%q", ftype)
   139  	}
   140  
   141  	if len(bad) > 0 {
   142  		errMsg := "invalid parameters supplied"
   143  		for _, k := range []string{arch, release, ftype} {
   144  			if v, ok := bad[k]; ok {
   145  				errMsg += fmt.Sprintf(" %s", v)
   146  			}
   147  		}
   148  		return errors.New(errMsg)
   149  	}
   150  	return nil
   151  }
   152  
   153  // One gets Metadata for one content download item:
   154  // The most recent of:
   155  //   - architecture
   156  //   - OS release
   157  //   - Simplestreams stream
   158  //   - File image type.
   159  //
   160  // src exists to pass in a data source for testing.
   161  func One(fetcher imagemetadata.SimplestreamsFetcher, arch, release, stream, ftype string, src func() simplestreams.DataSource) (*Metadata, error) {
   162  	if err := validateArgs(arch, release, ftype); err != nil {
   163  		return nil, errors.Trace(err)
   164  	}
   165  	if src == nil {
   166  		src = DefaultSource(fetcher)
   167  	}
   168  	ds := []simplestreams.DataSource{src()}
   169  	limit, err := imagemetadata.NewImageConstraint(
   170  		simplestreams.LookupParams{
   171  			Arches:   []string{arch},
   172  			Releases: []string{release},
   173  			Stream:   stream,
   174  		},
   175  	)
   176  	if err != nil {
   177  		return nil, errors.Trace(err)
   178  	}
   179  
   180  	md, _, err := Fetch(fetcher, ds, limit, Filter(ftype))
   181  	if err != nil {
   182  		// It doesn't appear that arch is vetted anywhere else so we can wind
   183  		// up with empty results if someone requests any arch with valid series
   184  		// and ftype..
   185  		return nil, errors.Trace(err)
   186  	}
   187  	if len(md) < 1 {
   188  		return nil, errors.Errorf("no results for %q, %q, %q, %q", arch, release, stream, ftype)
   189  	}
   190  	if len(md) > 1 {
   191  		// Should not be possible.
   192  		return nil, errors.Errorf("got %d results expected 1 for %q, %q, %q, %q", len(md), arch, release, stream, ftype)
   193  	}
   194  	return md[0], nil
   195  }
   196  
   197  // validFTypes is a simple map of file types that we can glean from looking at
   198  // simple streams.
   199  var validFTypes = map[string]bool{
   200  	"disk1.img":   true,
   201  	"lxd.tar.xz":  true,
   202  	"manifest":    true,
   203  	"ova":         true,
   204  	"root.tar.gz": true,
   205  	"root.tar.xz": true,
   206  	"tar.gz":      true,
   207  	"uefi1.img":   true,
   208  }
   209  
   210  // validArches are the arches we support running kvm containers on.
   211  var validArches = map[string]bool{
   212  	arch.AMD64:   true,
   213  	arch.ARM64:   true,
   214  	arch.PPC64EL: true,
   215  }
   216  
   217  // Filter collects only matching products. Release and Arch are filtered by
   218  // imagemetadata.ImageConstraints. So this really only let's us filter on a
   219  // file type.
   220  func Filter(ftype string) simplestreams.AppendMatchingFunc {
   221  	return func(source simplestreams.DataSource, matchingImages []interface{},
   222  		images map[string]interface{}, cons simplestreams.LookupConstraint) ([]interface{}, error) {
   223  
   224  		imagesMap := make(map[imageKey]*Metadata, len(matchingImages))
   225  		for _, val := range matchingImages {
   226  			im := val.(*Metadata)
   227  			imagesMap[imageKey{im.Arch, im.FType, im.Release, im.Version}] = im
   228  		}
   229  		for _, val := range images {
   230  			im := val.(*Metadata)
   231  			if ftype != "" {
   232  				if im.FType != ftype {
   233  					continue
   234  				}
   235  			}
   236  			if _, ok := imagesMap[imageKey{im.Arch, im.FType, im.Release, im.Version}]; !ok {
   237  				matchingImages = append(matchingImages, im)
   238  			}
   239  		}
   240  		return matchingImages, nil
   241  	}
   242  }
   243  
   244  // imageKey is the key used while filtering products.
   245  type imageKey struct {
   246  	arch    string
   247  	ftype   string
   248  	release string
   249  	version string
   250  }
   251  
   252  // Sort sorts a slice of ImageMetadata in ascending order of their id
   253  // in order to ensure the results of Fetch are ordered deterministically.
   254  func Sort(metadata []*Metadata) {
   255  	sort.Sort(byRelease(metadata))
   256  }
   257  
   258  type byRelease []*Metadata
   259  
   260  func (b byRelease) Len() int           { return len(b) }
   261  func (b byRelease) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   262  func (b byRelease) Less(i, j int) bool { return b[i].Release < b[j].Release }