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 }