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