Merge lp:~wallyworld/juju-core/marshal-image-metadata into lp:~go-bot/juju-core/trunk
- marshal-image-metadata
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Ian Booth |
Approved revision: | no longer in the source branch. |
Merged at revision: | 1993 |
Proposed branch: | lp:~wallyworld/juju-core/marshal-image-metadata |
Merge into: | lp:~go-bot/juju-core/trunk |
Prerequisite: | lp:~wallyworld/juju-core/improve-image-metadata-command |
Diff against target: |
677 lines (+406/-137) 12 files modified
cmd/plugins/juju-metadata/imagemetadata.go (+6/-1) cmd/plugins/juju-metadata/imagemetadata_test.go (+1/-1) cmd/plugins/juju-metadata/validateimagemetadata_test.go (+6/-1) environs/imagemetadata/generate.go (+37/-124) environs/imagemetadata/generate_test.go (+40/-0) environs/imagemetadata/marshal.go (+97/-0) environs/imagemetadata/marshal_test.go (+127/-0) environs/imagemetadata/simplestreams.go (+15/-6) environs/imagemetadata/testing/testing.go (+70/-0) environs/imagemetadata/validation_test.go (+4/-1) environs/simplestreams/simplestreams.go (+2/-2) environs/tools/marshal.go (+1/-1) |
To merge this branch: | bzr merge lp:~wallyworld/juju-core/marshal-image-metadata |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+190542@code.launchpad.net |
Commit message
Write image metadata using json serialisation
This branch writes outs simplestreams image metadata using
json serialisation instead of a template based approach. It
is a pre-requisit to allow the next step of merging new image
metadata instead of overwriting. This will be used when setting
up private clouds.
Description of the change
Write image metadata using json serialisation
This branch writes outs simplestreams image metadata using
json serialisation instead of a template based approach. It
is a pre-requisit to allow the next step of merging new image
metadata instead of overwriting. This will be used when setting
up private clouds.
Ian Booth (wallyworld) wrote : | # |
Ian Booth (wallyworld) wrote : | # |
Please take a look.
Tim Penhey (thumper) wrote : | # |
https:/
File environs/
https:/
environs/
generateMetadat
This has me queezy.
Surely WriteMetadata would take the metadataInfo as an input parameter?
https:/
File environs/
https:/
environs/
testbase.
https:/
environs/
gc.Equals, 1)
gc.HasLen
https:/
File environs/
https:/
environs/
`json:"-"`
What does json:"-" do?
https:/
File environs/
https:/
environs/
// YYYYMMDD
Some of these changes are the same as the branch I just reviewed.
Ian Booth (wallyworld) wrote : | # |
https:/
File environs/
https:/
environs/
generateMetadat
On 2013/10/16 01:26:11, thumper wrote:
> This has me queezy.
> Surely WriteMetadata would take the metadataInfo as an input
parameter?
The sematics of the write function is to take a ImageMetadata struct and
write out the simplestreams metadata. In order to write the metadata, it
needs to be generated from the struct. The method could be called
WriteAndGenerate I guess. But the generation and writing to file is an
atomic operation.
A subsequent branch renames the method anyway and refactors the content
to align with what Andrew did for tools metadata.
https:/
File environs/
https:/
environs/
On 2013/10/16 01:26:11, thumper wrote:
> testbase.
Will fix
https:/
environs/
gc.Equals, 1)
On 2013/10/16 01:26:11, thumper wrote:
> gc.HasLen
Will fix
https:/
File environs/
https:/
environs/
`json:"-"`
On 2013/10/16 01:26:11, thumper wrote:
> What does json:"-" do?
Tells json not to marshall that attribute. It's there to provide info
for other mthods in the struct to do their thing, but we don't want it
serialised to json.
https:/
File environs/
https:/
environs/
// YYYYMMDD
On 2013/10/16 01:26:11, thumper wrote:
> Some of these changes are the same as the branch I just reviewed.
I fixed this in a downstream branch - I didn't pump the changes back up
the pipe sorry.
Ian Booth (wallyworld) wrote : | # |
Please take a look.
Tim Penhey (thumper) wrote : | # |
Preview Diff
1 | === modified file 'cmd/plugins/juju-metadata/imagemetadata.go' | |||
2 | --- cmd/plugins/juju-metadata/imagemetadata.go 2013-10-16 02:56:51 +0000 | |||
3 | +++ cmd/plugins/juju-metadata/imagemetadata.go 2013-10-16 04:12:27 +0000 | |||
4 | @@ -14,6 +14,7 @@ | |||
5 | 14 | "launchpad.net/juju-core/environs" | 14 | "launchpad.net/juju-core/environs" |
6 | 15 | "launchpad.net/juju-core/environs/config" | 15 | "launchpad.net/juju-core/environs/config" |
7 | 16 | "launchpad.net/juju-core/environs/configstore" | 16 | "launchpad.net/juju-core/environs/configstore" |
8 | 17 | "launchpad.net/juju-core/environs/filestorage" | ||
9 | 17 | "launchpad.net/juju-core/environs/imagemetadata" | 18 | "launchpad.net/juju-core/environs/imagemetadata" |
10 | 18 | "launchpad.net/juju-core/environs/simplestreams" | 19 | "launchpad.net/juju-core/environs/simplestreams" |
11 | 19 | ) | 20 | ) |
12 | @@ -152,7 +153,11 @@ | |||
13 | 152 | Region: c.Region, | 153 | Region: c.Region, |
14 | 153 | Endpoint: c.Endpoint, | 154 | Endpoint: c.Endpoint, |
15 | 154 | } | 155 | } |
17 | 155 | _, err := imagemetadata.GenerateMetadata(c.Series, &im, &cloudSpec, c.Dir) | 156 | targetStorage, err := filestorage.NewFileStorageWriter(c.Dir, filestorage.UseDefaultTmpDir) |
18 | 157 | if err != nil { | ||
19 | 158 | return err | ||
20 | 159 | } | ||
21 | 160 | err = imagemetadata.WriteMetadata(c.Series, &im, &cloudSpec, targetStorage) | ||
22 | 156 | if err != nil { | 161 | if err != nil { |
23 | 157 | return fmt.Errorf("image metadata files could not be created: %v", err) | 162 | return fmt.Errorf("image metadata files could not be created: %v", err) |
24 | 158 | } | 163 | } |
25 | 159 | 164 | ||
26 | === modified file 'cmd/plugins/juju-metadata/imagemetadata_test.go' | |||
27 | --- cmd/plugins/juju-metadata/imagemetadata_test.go 2013-10-16 02:56:51 +0000 | |||
28 | +++ cmd/plugins/juju-metadata/imagemetadata_test.go 2013-10-16 04:12:27 +0000 | |||
29 | @@ -108,7 +108,7 @@ | |||
30 | 108 | 108 | ||
31 | 109 | const ( | 109 | const ( |
32 | 110 | defaultIndexFileName = "index.json" | 110 | defaultIndexFileName = "index.json" |
34 | 111 | defaultImageFileName = "imagemetadata.json" | 111 | defaultImageFileName = "com.ubuntu.cloud:released:imagemetadata.json" |
35 | 112 | ) | 112 | ) |
36 | 113 | 113 | ||
37 | 114 | func (s *ImageMetadataSuite) TestImageMetadataFilesNoEnv(c *gc.C) { | 114 | func (s *ImageMetadataSuite) TestImageMetadataFilesNoEnv(c *gc.C) { |
38 | 115 | 115 | ||
39 | === modified file 'cmd/plugins/juju-metadata/validateimagemetadata_test.go' | |||
40 | --- cmd/plugins/juju-metadata/validateimagemetadata_test.go 2013-10-16 03:38:32 +0000 | |||
41 | +++ cmd/plugins/juju-metadata/validateimagemetadata_test.go 2013-10-16 04:12:27 +0000 | |||
42 | @@ -11,6 +11,7 @@ | |||
43 | 11 | gc "launchpad.net/gocheck" | 11 | gc "launchpad.net/gocheck" |
44 | 12 | 12 | ||
45 | 13 | "launchpad.net/juju-core/cmd" | 13 | "launchpad.net/juju-core/cmd" |
46 | 14 | "launchpad.net/juju-core/environs/filestorage" | ||
47 | 14 | "launchpad.net/juju-core/environs/imagemetadata" | 15 | "launchpad.net/juju-core/environs/imagemetadata" |
48 | 15 | "launchpad.net/juju-core/environs/simplestreams" | 16 | "launchpad.net/juju-core/environs/simplestreams" |
49 | 16 | coretesting "launchpad.net/juju-core/testing" | 17 | coretesting "launchpad.net/juju-core/testing" |
50 | @@ -73,7 +74,11 @@ | |||
51 | 73 | Region: region, | 74 | Region: region, |
52 | 74 | Endpoint: endpoint, | 75 | Endpoint: endpoint, |
53 | 75 | } | 76 | } |
55 | 76 | _, err := imagemetadata.GenerateMetadata(series, &im, &cloudSpec, s.metadataDir) | 77 | targetStorage, err := filestorage.NewFileStorageWriter(s.metadataDir, filestorage.UseDefaultTmpDir) |
56 | 78 | if err != nil { | ||
57 | 79 | return err | ||
58 | 80 | } | ||
59 | 81 | err = imagemetadata.WriteMetadata(series, &im, &cloudSpec, targetStorage) | ||
60 | 77 | if err != nil { | 82 | if err != nil { |
61 | 78 | return err | 83 | return err |
62 | 79 | } | 84 | } |
63 | 80 | 85 | ||
64 | === modified file 'environs/imagemetadata/generate.go' | |||
65 | --- environs/imagemetadata/generate.go 2013-10-11 01:20:34 +0000 | |||
66 | +++ environs/imagemetadata/generate.go 2013-10-16 04:12:27 +0000 | |||
67 | @@ -5,135 +5,48 @@ | |||
68 | 5 | 5 | ||
69 | 6 | import ( | 6 | import ( |
70 | 7 | "bytes" | 7 | "bytes" |
71 | 8 | "fmt" | ||
72 | 9 | "io/ioutil" | ||
73 | 10 | "os" | ||
74 | 11 | "path/filepath" | ||
75 | 12 | "text/template" | ||
76 | 13 | "time" | 8 | "time" |
77 | 14 | 9 | ||
78 | 15 | "launchpad.net/juju-core/environs/simplestreams" | 10 | "launchpad.net/juju-core/environs/simplestreams" |
146 | 16 | ) | 11 | "launchpad.net/juju-core/environs/storage" |
147 | 17 | 12 | ) | |
148 | 18 | const ( | 13 | |
149 | 19 | defaultIndexFileName = "index.json" | 14 | func WriteMetadata(series string, im *ImageMetadata, cloudSpec *simplestreams.CloudSpec, metadataStore storage.Storage) error { |
150 | 20 | defaultImageFileName = "imagemetadata.json" | 15 | metadataInfo, err := generateMetadata(series, im, cloudSpec) |
151 | 21 | ) | 16 | if err != nil { |
85 | 22 | |||
86 | 23 | // GenerateMetadata generates some basic simplestreams metadata using the specified cloud and image details. | ||
87 | 24 | func GenerateMetadata(series string, im *ImageMetadata, cloudSpec *simplestreams.CloudSpec, dest string) ([]string, error) { | ||
88 | 25 | indexFileName := defaultIndexFileName | ||
89 | 26 | imageFileName := defaultImageFileName | ||
90 | 27 | now := time.Now() | ||
91 | 28 | imparams := imageMetadataParams{ | ||
92 | 29 | Id: im.Id, | ||
93 | 30 | Arch: im.Arch, | ||
94 | 31 | Region: cloudSpec.Region, | ||
95 | 32 | URL: cloudSpec.Endpoint, | ||
96 | 33 | Path: "streams/v1", | ||
97 | 34 | ImageFileName: imageFileName, | ||
98 | 35 | Updated: now.Format(time.RFC1123Z), | ||
99 | 36 | VersionKey: now.Format("20060102"), | ||
100 | 37 | } | ||
101 | 38 | |||
102 | 39 | var err error | ||
103 | 40 | imparams.Version, err = simplestreams.SeriesVersion(series) | ||
104 | 41 | if err != nil { | ||
105 | 42 | return nil, fmt.Errorf("invalid series %q", series) | ||
106 | 43 | } | ||
107 | 44 | |||
108 | 45 | streamsPath := filepath.Join(dest, "streams", "v1") | ||
109 | 46 | if err = os.MkdirAll(streamsPath, 0755); err != nil { | ||
110 | 47 | return nil, err | ||
111 | 48 | } | ||
112 | 49 | indexFileName = filepath.Join(streamsPath, indexFileName) | ||
113 | 50 | imageFileName = filepath.Join(streamsPath, imageFileName) | ||
114 | 51 | err = writeJsonFile(imparams, indexFileName, indexBoilerplate) | ||
115 | 52 | if err != nil { | ||
116 | 53 | return nil, err | ||
117 | 54 | } | ||
118 | 55 | err = writeJsonFile(imparams, imageFileName, productBoilerplate) | ||
119 | 56 | if err != nil { | ||
120 | 57 | return nil, err | ||
121 | 58 | } | ||
122 | 59 | return []string{indexFileName, imageFileName}, nil | ||
123 | 60 | } | ||
124 | 61 | |||
125 | 62 | type imageMetadataParams struct { | ||
126 | 63 | Region string | ||
127 | 64 | URL string | ||
128 | 65 | Updated string | ||
129 | 66 | Arch string | ||
130 | 67 | Path string | ||
131 | 68 | Series string | ||
132 | 69 | Version string | ||
133 | 70 | VersionKey string | ||
134 | 71 | Id string | ||
135 | 72 | ImageFileName string | ||
136 | 73 | } | ||
137 | 74 | |||
138 | 75 | func writeJsonFile(imparams imageMetadataParams, filename, boilerplate string) error { | ||
139 | 76 | t := template.Must(template.New("").Parse(boilerplate)) | ||
140 | 77 | var metadata bytes.Buffer | ||
141 | 78 | if err := t.Execute(&metadata, imparams); err != nil { | ||
142 | 79 | panic(fmt.Errorf("cannot generate %s metdata: %v", filename, err)) | ||
143 | 80 | } | ||
144 | 81 | data := metadata.Bytes() | ||
145 | 82 | if err := ioutil.WriteFile(filename, data, 0666); err != nil { | ||
152 | 83 | return err | 17 | return err |
153 | 84 | } | 18 | } |
154 | 19 | for _, md := range metadataInfo { | ||
155 | 20 | err = metadataStore.Put(md.Path, bytes.NewReader(md.Data), int64(len(md.Data))) | ||
156 | 21 | if err != nil { | ||
157 | 22 | return err | ||
158 | 23 | } | ||
159 | 24 | } | ||
160 | 85 | return nil | 25 | return nil |
161 | 86 | } | 26 | } |
162 | 87 | 27 | ||
215 | 88 | var indexBoilerplate = ` | 28 | type MetadataFile struct { |
216 | 89 | { | 29 | Path string |
217 | 90 | "index": { | 30 | Data []byte |
218 | 91 | "com.ubuntu.cloud:custom": { | 31 | } |
219 | 92 | "updated": "{{.Updated}}", | 32 | |
220 | 93 | "clouds": [ | 33 | // generateMetadata generates some basic simplestreams metadata using the specified cloud and image details. |
221 | 94 | { | 34 | func generateMetadata(series string, im *ImageMetadata, cloudSpec *simplestreams.CloudSpec) ([]MetadataFile, error) { |
222 | 95 | "region": "{{.Region}}", | 35 | metadata := &ImageMetadata{ |
223 | 96 | "endpoint": "{{.URL}}" | 36 | Id: im.Id, |
224 | 97 | } | 37 | Arch: im.Arch, |
225 | 98 | ], | 38 | Release: series, |
226 | 99 | "cloudname": "custom", | 39 | RegionName: cloudSpec.Region, |
227 | 100 | "datatype": "image-ids", | 40 | Endpoint: cloudSpec.Endpoint, |
228 | 101 | "format": "products:1.0", | 41 | } |
229 | 102 | "products": [ | 42 | |
230 | 103 | "com.ubuntu.cloud:server:{{.Version}}:{{.Arch}}" | 43 | index, products, err := MarshalImageMetadataJSON([]*ImageMetadata{metadata}, cloudSpec, time.Now()) |
231 | 104 | ], | 44 | if err != nil { |
232 | 105 | "path": "{{.Path}}/{{.ImageFileName}}" | 45 | return nil, err |
233 | 106 | } | 46 | } |
234 | 107 | }, | 47 | objects := []MetadataFile{ |
235 | 108 | "updated": "{{.Updated}}", | 48 | {simplestreams.UnsignedIndex, index}, |
236 | 109 | "format": "index:1.0" | 49 | {ProductMetadataPath, products}, |
237 | 110 | } | 50 | } |
238 | 111 | ` | 51 | return objects, nil |
239 | 112 | 52 | } | |
188 | 113 | var productBoilerplate = ` | ||
189 | 114 | { | ||
190 | 115 | "content_id": "com.ubuntu.cloud:custom", | ||
191 | 116 | "format": "products:1.0", | ||
192 | 117 | "updated": "{{.Updated}}", | ||
193 | 118 | "datatype": "image-ids", | ||
194 | 119 | "products": { | ||
195 | 120 | "com.ubuntu.cloud:server:{{.Version}}:{{.Arch}}": { | ||
196 | 121 | "release": "{{.Series}}", | ||
197 | 122 | "version": "{{.Version}}", | ||
198 | 123 | "arch": "{{.Arch}}", | ||
199 | 124 | "versions": { | ||
200 | 125 | "{{.VersionKey}}": { | ||
201 | 126 | "items": { | ||
202 | 127 | "{{.Id}}": { | ||
203 | 128 | "region": "{{.Region}}", | ||
204 | 129 | "id": "{{.Id}}" | ||
205 | 130 | } | ||
206 | 131 | }, | ||
207 | 132 | "pubname": "ubuntu-{{.Series}}-{{.Version}}-{{.Arch}}-server-{{.VersionKey}}", | ||
208 | 133 | "label": "custom" | ||
209 | 134 | } | ||
210 | 135 | } | ||
211 | 136 | } | ||
212 | 137 | } | ||
213 | 138 | } | ||
214 | 139 | ` | ||
240 | 140 | 53 | ||
241 | === added file 'environs/imagemetadata/generate_test.go' | |||
242 | --- environs/imagemetadata/generate_test.go 1970-01-01 00:00:00 +0000 | |||
243 | +++ environs/imagemetadata/generate_test.go 2013-10-16 04:12:27 +0000 | |||
244 | @@ -0,0 +1,40 @@ | |||
245 | 1 | // Copyright 2013 Canonical Ltd. | ||
246 | 2 | // Licensed under the AGPLv3, see LICENCE file for details. | ||
247 | 3 | |||
248 | 4 | package imagemetadata_test | ||
249 | 5 | |||
250 | 6 | import ( | ||
251 | 7 | gc "launchpad.net/gocheck" | ||
252 | 8 | |||
253 | 9 | "launchpad.net/juju-core/environs/filestorage" | ||
254 | 10 | "launchpad.net/juju-core/environs/imagemetadata" | ||
255 | 11 | "launchpad.net/juju-core/environs/imagemetadata/testing" | ||
256 | 12 | "launchpad.net/juju-core/environs/simplestreams" | ||
257 | 13 | "launchpad.net/juju-core/testing/testbase" | ||
258 | 14 | ) | ||
259 | 15 | |||
260 | 16 | var _ = gc.Suite(&generateSuite{}) | ||
261 | 17 | |||
262 | 18 | type generateSuite struct { | ||
263 | 19 | testbase.LoggingSuite | ||
264 | 20 | } | ||
265 | 21 | |||
266 | 22 | func (s *generateSuite) TestWriteMetadata(c *gc.C) { | ||
267 | 23 | im := &imagemetadata.ImageMetadata{ | ||
268 | 24 | Id: "1234", | ||
269 | 25 | Arch: "amd64", | ||
270 | 26 | } | ||
271 | 27 | cloudSpec := &simplestreams.CloudSpec{ | ||
272 | 28 | Region: "region", | ||
273 | 29 | Endpoint: "endpoint", | ||
274 | 30 | } | ||
275 | 31 | dir := c.MkDir() | ||
276 | 32 | targetStorage, err := filestorage.NewFileStorageWriter(dir, filestorage.UseDefaultTmpDir) | ||
277 | 33 | c.Assert(err, gc.IsNil) | ||
278 | 34 | err = imagemetadata.WriteMetadata("raring", im, cloudSpec, targetStorage) | ||
279 | 35 | c.Assert(err, gc.IsNil) | ||
280 | 36 | metadata := testing.ParseMetadata(c, dir) | ||
281 | 37 | c.Assert(metadata, gc.HasLen, 1) | ||
282 | 38 | im.RegionName = cloudSpec.Region | ||
283 | 39 | c.Assert(im, gc.DeepEquals, metadata[0]) | ||
284 | 40 | } | ||
285 | 0 | 41 | ||
286 | === added file 'environs/imagemetadata/marshal.go' | |||
287 | --- environs/imagemetadata/marshal.go 1970-01-01 00:00:00 +0000 | |||
288 | +++ environs/imagemetadata/marshal.go 2013-10-16 04:12:27 +0000 | |||
289 | @@ -0,0 +1,97 @@ | |||
290 | 1 | // Copyright 2013 Canonical Ltd. | ||
291 | 2 | // Licensed under the AGPLv3, see LICENCE file for details. | ||
292 | 3 | |||
293 | 4 | package imagemetadata | ||
294 | 5 | |||
295 | 6 | import ( | ||
296 | 7 | "encoding/json" | ||
297 | 8 | "time" | ||
298 | 9 | |||
299 | 10 | "launchpad.net/juju-core/environs/simplestreams" | ||
300 | 11 | "launchpad.net/juju-core/utils/set" | ||
301 | 12 | ) | ||
302 | 13 | |||
303 | 14 | const ( | ||
304 | 15 | ProductMetadataPath = "streams/v1/com.ubuntu.cloud:released:imagemetadata.json" | ||
305 | 16 | ) | ||
306 | 17 | |||
307 | 18 | // MarshalImageMetadataJSON marshals image metadata to index and products JSON. | ||
308 | 19 | // | ||
309 | 20 | // updated is the time at which the JSON file was updated. | ||
310 | 21 | func MarshalImageMetadataJSON(metadata []*ImageMetadata, cloudSpec *simplestreams.CloudSpec, updated time.Time) (index, products []byte, err error) { | ||
311 | 22 | if index, err = MarshalImageMetadataIndexJSON(metadata, cloudSpec, updated); err != nil { | ||
312 | 23 | return nil, nil, err | ||
313 | 24 | } | ||
314 | 25 | if products, err = MarshalImageMetadataProductsJSON(metadata, updated); err != nil { | ||
315 | 26 | return nil, nil, err | ||
316 | 27 | } | ||
317 | 28 | return index, products, err | ||
318 | 29 | } | ||
319 | 30 | |||
320 | 31 | // MarshalImageMetadataIndexJSON marshals image metadata to index JSON. | ||
321 | 32 | // | ||
322 | 33 | // updated is the time at which the JSON file was updated. | ||
323 | 34 | func MarshalImageMetadataIndexJSON(metadata []*ImageMetadata, cloudSpec *simplestreams.CloudSpec, updated time.Time) (out []byte, err error) { | ||
324 | 35 | productIds := make([]string, len(metadata)) | ||
325 | 36 | for i, t := range metadata { | ||
326 | 37 | productIds[i], err = t.productId() | ||
327 | 38 | if err != nil { | ||
328 | 39 | return nil, err | ||
329 | 40 | } | ||
330 | 41 | } | ||
331 | 42 | var indices simplestreams.Indices | ||
332 | 43 | indices.Updated = updated.Format(time.RFC1123Z) | ||
333 | 44 | indices.Format = "index:1.0" | ||
334 | 45 | indices.Indexes = map[string]*simplestreams.IndexMetadata{ | ||
335 | 46 | "com.ubuntu.cloud:custom": &simplestreams.IndexMetadata{ | ||
336 | 47 | CloudName: "custom", | ||
337 | 48 | Updated: indices.Updated, | ||
338 | 49 | Format: "products:1.0", | ||
339 | 50 | DataType: "image-ids", | ||
340 | 51 | ProductsFilePath: ProductMetadataPath, | ||
341 | 52 | ProductIds: set.NewStrings(productIds...).SortedValues(), | ||
342 | 53 | Clouds: []simplestreams.CloudSpec{*cloudSpec}, | ||
343 | 54 | }, | ||
344 | 55 | } | ||
345 | 56 | return json.MarshalIndent(&indices, "", " ") | ||
346 | 57 | } | ||
347 | 58 | |||
348 | 59 | // MarshalImageMetadataProductsJSON marshals image metadata to products JSON. | ||
349 | 60 | // | ||
350 | 61 | // updated is the time at which the JSON file was updated. | ||
351 | 62 | func MarshalImageMetadataProductsJSON(metadata []*ImageMetadata, updated time.Time) (out []byte, err error) { | ||
352 | 63 | var cloud simplestreams.CloudMetadata | ||
353 | 64 | cloud.Updated = updated.Format(time.RFC1123Z) | ||
354 | 65 | cloud.Format = "products:1.0" | ||
355 | 66 | cloud.Products = make(map[string]simplestreams.MetadataCatalog) | ||
356 | 67 | itemsversion := updated.Format("20060201") // YYYYMMDD | ||
357 | 68 | for _, t := range metadata { | ||
358 | 69 | id, err := t.productId() | ||
359 | 70 | if err != nil { | ||
360 | 71 | return nil, err | ||
361 | 72 | } | ||
362 | 73 | version, err := simplestreams.SeriesVersion(t.Release) | ||
363 | 74 | if err != nil { | ||
364 | 75 | return nil, err | ||
365 | 76 | } | ||
366 | 77 | toWrite := &ImageMetadata{ | ||
367 | 78 | Id: t.Id, | ||
368 | 79 | } | ||
369 | 80 | if catalog, ok := cloud.Products[id]; ok { | ||
370 | 81 | catalog.Items[itemsversion].Items[t.Id] = t | ||
371 | 82 | } else { | ||
372 | 83 | catalog = simplestreams.MetadataCatalog{ | ||
373 | 84 | Arch: t.Arch, | ||
374 | 85 | RegionName: t.RegionName, | ||
375 | 86 | Version: version, | ||
376 | 87 | Items: map[string]*simplestreams.ItemCollection{ | ||
377 | 88 | itemsversion: &simplestreams.ItemCollection{ | ||
378 | 89 | Items: map[string]interface{}{t.Id: toWrite}, | ||
379 | 90 | }, | ||
380 | 91 | }, | ||
381 | 92 | } | ||
382 | 93 | cloud.Products[id] = catalog | ||
383 | 94 | } | ||
384 | 95 | } | ||
385 | 96 | return json.MarshalIndent(&cloud, "", " ") | ||
386 | 97 | } | ||
387 | 0 | 98 | ||
388 | === added file 'environs/imagemetadata/marshal_test.go' | |||
389 | --- environs/imagemetadata/marshal_test.go 1970-01-01 00:00:00 +0000 | |||
390 | +++ environs/imagemetadata/marshal_test.go 2013-10-16 04:12:27 +0000 | |||
391 | @@ -0,0 +1,127 @@ | |||
392 | 1 | // Copyright 2013 Canonical Ltd. | ||
393 | 2 | // Licensed under the AGPLv3, see LICENCE file for details. | ||
394 | 3 | |||
395 | 4 | package imagemetadata_test | ||
396 | 5 | |||
397 | 6 | import ( | ||
398 | 7 | "time" | ||
399 | 8 | |||
400 | 9 | gc "launchpad.net/gocheck" | ||
401 | 10 | |||
402 | 11 | "launchpad.net/juju-core/environs/imagemetadata" | ||
403 | 12 | "launchpad.net/juju-core/environs/simplestreams" | ||
404 | 13 | ) | ||
405 | 14 | |||
406 | 15 | var _ = gc.Suite(&marshalSuite{}) | ||
407 | 16 | |||
408 | 17 | type marshalSuite struct{} | ||
409 | 18 | |||
410 | 19 | var expectedIndex = `{ | ||
411 | 20 | "index": { | ||
412 | 21 | "com.ubuntu.cloud:custom": { | ||
413 | 22 | "updated": "Thu, 01 Jan 1970 00:00:00 +0000", | ||
414 | 23 | "format": "products:1.0", | ||
415 | 24 | "datatype": "image-ids", | ||
416 | 25 | "cloudname": "custom", | ||
417 | 26 | "clouds": [ | ||
418 | 27 | { | ||
419 | 28 | "region": "region", | ||
420 | 29 | "endpoint": "endpoint" | ||
421 | 30 | } | ||
422 | 31 | ], | ||
423 | 32 | "path": "streams/v1/com.ubuntu.cloud:released:imagemetadata.json", | ||
424 | 33 | "products": [ | ||
425 | 34 | "com.ubuntu.cloud:server:12.04:amd64", | ||
426 | 35 | "com.ubuntu.cloud:server:12.04:arm", | ||
427 | 36 | "com.ubuntu.cloud:server:13.10:arm" | ||
428 | 37 | ] | ||
429 | 38 | } | ||
430 | 39 | }, | ||
431 | 40 | "updated": "Thu, 01 Jan 1970 00:00:00 +0000", | ||
432 | 41 | "format": "index:1.0" | ||
433 | 42 | }` | ||
434 | 43 | |||
435 | 44 | var expectedProducts = `{ | ||
436 | 45 | "products": { | ||
437 | 46 | "com.ubuntu.cloud:server:12.04:amd64": { | ||
438 | 47 | "version": "12.04", | ||
439 | 48 | "arch": "amd64", | ||
440 | 49 | "versions": { | ||
441 | 50 | "19700101": { | ||
442 | 51 | "items": { | ||
443 | 52 | "abcd": { | ||
444 | 53 | "id": "abcd" | ||
445 | 54 | } | ||
446 | 55 | } | ||
447 | 56 | } | ||
448 | 57 | } | ||
449 | 58 | }, | ||
450 | 59 | "com.ubuntu.cloud:server:12.04:arm": { | ||
451 | 60 | "version": "12.04", | ||
452 | 61 | "arch": "arm", | ||
453 | 62 | "versions": { | ||
454 | 63 | "19700101": { | ||
455 | 64 | "items": { | ||
456 | 65 | "5678": { | ||
457 | 66 | "id": "5678" | ||
458 | 67 | } | ||
459 | 68 | } | ||
460 | 69 | } | ||
461 | 70 | } | ||
462 | 71 | }, | ||
463 | 72 | "com.ubuntu.cloud:server:13.10:arm": { | ||
464 | 73 | "version": "13.10", | ||
465 | 74 | "arch": "arm", | ||
466 | 75 | "versions": { | ||
467 | 76 | "19700101": { | ||
468 | 77 | "items": { | ||
469 | 78 | "1234": { | ||
470 | 79 | "id": "1234" | ||
471 | 80 | } | ||
472 | 81 | } | ||
473 | 82 | } | ||
474 | 83 | } | ||
475 | 84 | } | ||
476 | 85 | }, | ||
477 | 86 | "updated": "Thu, 01 Jan 1970 00:00:00 +0000", | ||
478 | 87 | "format": "products:1.0" | ||
479 | 88 | }` | ||
480 | 89 | |||
481 | 90 | var imageMetadataForTesting = []*imagemetadata.ImageMetadata{ | ||
482 | 91 | &imagemetadata.ImageMetadata{ | ||
483 | 92 | Id: "1234", | ||
484 | 93 | Release: "saucy", | ||
485 | 94 | Arch: "arm", | ||
486 | 95 | }, | ||
487 | 96 | &imagemetadata.ImageMetadata{ | ||
488 | 97 | Id: "5678", | ||
489 | 98 | Release: "precise", | ||
490 | 99 | Arch: "arm", | ||
491 | 100 | }, | ||
492 | 101 | &imagemetadata.ImageMetadata{ | ||
493 | 102 | Id: "abcd", | ||
494 | 103 | Release: "precise", | ||
495 | 104 | Arch: "amd64", | ||
496 | 105 | }, | ||
497 | 106 | } | ||
498 | 107 | |||
499 | 108 | func (s *marshalSuite) TestMarshalIndex(c *gc.C) { | ||
500 | 109 | cloudSpec := &simplestreams.CloudSpec{Region: "region", Endpoint: "endpoint"} | ||
501 | 110 | index, err := imagemetadata.MarshalImageMetadataIndexJSON(imageMetadataForTesting, cloudSpec, time.Unix(0, 0).UTC()) | ||
502 | 111 | c.Assert(err, gc.IsNil) | ||
503 | 112 | c.Assert(string(index), gc.Equals, expectedIndex) | ||
504 | 113 | } | ||
505 | 114 | |||
506 | 115 | func (s *marshalSuite) TestMarshalProducts(c *gc.C) { | ||
507 | 116 | products, err := imagemetadata.MarshalImageMetadataProductsJSON(imageMetadataForTesting, time.Unix(0, 0).UTC()) | ||
508 | 117 | c.Assert(err, gc.IsNil) | ||
509 | 118 | c.Assert(string(products), gc.Equals, expectedProducts) | ||
510 | 119 | } | ||
511 | 120 | |||
512 | 121 | func (s *marshalSuite) TestMarshal(c *gc.C) { | ||
513 | 122 | cloudSpec := &simplestreams.CloudSpec{Region: "region", Endpoint: "endpoint"} | ||
514 | 123 | index, products, err := imagemetadata.MarshalImageMetadataJSON(imageMetadataForTesting, cloudSpec, time.Unix(0, 0).UTC()) | ||
515 | 124 | c.Assert(err, gc.IsNil) | ||
516 | 125 | c.Assert(string(index), gc.Equals, expectedIndex) | ||
517 | 126 | c.Assert(string(products), gc.Equals, expectedProducts) | ||
518 | 127 | } | ||
519 | 0 | 128 | ||
520 | === modified file 'environs/imagemetadata/simplestreams.go' | |||
521 | --- environs/imagemetadata/simplestreams.go 2013-10-10 11:40:54 +0000 | |||
522 | +++ environs/imagemetadata/simplestreams.go 2013-10-16 04:12:27 +0000 | |||
523 | @@ -115,12 +115,21 @@ | |||
524 | 115 | // ImageMetadata holds information about a particular cloud image. | 115 | // ImageMetadata holds information about a particular cloud image. |
525 | 116 | type ImageMetadata struct { | 116 | type ImageMetadata struct { |
526 | 117 | Id string `json:"id"` | 117 | Id string `json:"id"` |
533 | 118 | Storage string `json:"root_store"` | 118 | Storage string `json:"root_store,omitempty"` |
534 | 119 | VType string `json:"virt"` | 119 | VType string `json:"virt,omitempty"` |
535 | 120 | Arch string `json:"arch"` | 120 | Arch string `json:"arch,omitempty"` |
536 | 121 | RegionAlias string `json:"crsn"` | 121 | Release string `json:"-"` |
537 | 122 | RegionName string `json:"region"` | 122 | RegionAlias string `json:"crsn,omitempty"` |
538 | 123 | Endpoint string `json:"endpoint"` | 123 | RegionName string `json:"region,omitempty"` |
539 | 124 | Endpoint string `json:"endpoint,omitempty"` | ||
540 | 125 | } | ||
541 | 126 | |||
542 | 127 | func (t *ImageMetadata) productId() (string, error) { | ||
543 | 128 | seriesVersion, err := simplestreams.SeriesVersion(t.Release) | ||
544 | 129 | if err != nil { | ||
545 | 130 | return "", err | ||
546 | 131 | } | ||
547 | 132 | return fmt.Sprintf("com.ubuntu.cloud:server:%s:%s", seriesVersion, t.Arch), nil | ||
548 | 124 | } | 133 | } |
549 | 125 | 134 | ||
550 | 126 | // Fetch returns a list of images for the specified cloud matching the constraint. | 135 | // Fetch returns a list of images for the specified cloud matching the constraint. |
551 | 127 | 136 | ||
552 | === added directory 'environs/imagemetadata/testing' | |||
553 | === added file 'environs/imagemetadata/testing/testing.go' | |||
554 | --- environs/imagemetadata/testing/testing.go 1970-01-01 00:00:00 +0000 | |||
555 | +++ environs/imagemetadata/testing/testing.go 2013-10-16 04:12:27 +0000 | |||
556 | @@ -0,0 +1,70 @@ | |||
557 | 1 | // Copyright 2013 Canonical Ltd. | ||
558 | 2 | // Licensed under the AGPLv3, see LICENCE file for details. | ||
559 | 3 | |||
560 | 4 | package testing | ||
561 | 5 | |||
562 | 6 | import ( | ||
563 | 7 | "fmt" | ||
564 | 8 | "io/ioutil" | ||
565 | 9 | "path/filepath" | ||
566 | 10 | "sort" | ||
567 | 11 | |||
568 | 12 | gc "launchpad.net/gocheck" | ||
569 | 13 | |||
570 | 14 | "launchpad.net/juju-core/environs/imagemetadata" | ||
571 | 15 | "launchpad.net/juju-core/environs/simplestreams" | ||
572 | 16 | "launchpad.net/juju-core/utils/set" | ||
573 | 17 | ) | ||
574 | 18 | |||
575 | 19 | // ParseMetadata loads ImageMetadata from the specified directory. | ||
576 | 20 | func ParseMetadata(c *gc.C, metadataDir string) []*imagemetadata.ImageMetadata { | ||
577 | 21 | params := simplestreams.ValueParams{ | ||
578 | 22 | DataType: "image-ids", | ||
579 | 23 | ValueTemplate: imagemetadata.ImageMetadata{}, | ||
580 | 24 | } | ||
581 | 25 | |||
582 | 26 | source := simplestreams.NewURLDataSource("file://"+metadataDir, simplestreams.VerifySSLHostnames) | ||
583 | 27 | |||
584 | 28 | const requireSigned = false | ||
585 | 29 | indexPath := simplestreams.UnsignedIndex | ||
586 | 30 | indexRef, err := simplestreams.GetIndexWithFormat( | ||
587 | 31 | source, indexPath, "index:1.0", requireSigned, simplestreams.CloudSpec{}, params) | ||
588 | 32 | c.Assert(err, gc.IsNil) | ||
589 | 33 | c.Assert(indexRef.Indexes, gc.HasLen, 1) | ||
590 | 34 | |||
591 | 35 | imageIndexMetadata := indexRef.Indexes["com.ubuntu.cloud:custom"] | ||
592 | 36 | c.Assert(imageIndexMetadata, gc.NotNil) | ||
593 | 37 | |||
594 | 38 | data, err := ioutil.ReadFile(filepath.Join(metadataDir, imageIndexMetadata.ProductsFilePath)) | ||
595 | 39 | c.Assert(err, gc.IsNil) | ||
596 | 40 | |||
597 | 41 | url, err := source.URL(imageIndexMetadata.ProductsFilePath) | ||
598 | 42 | c.Assert(err, gc.IsNil) | ||
599 | 43 | cloudMetadata, err := simplestreams.ParseCloudMetadata(data, "products:1.0", url, imagemetadata.ImageMetadata{}) | ||
600 | 44 | c.Assert(err, gc.IsNil) | ||
601 | 45 | |||
602 | 46 | imageMetadataMap := make(map[string]*imagemetadata.ImageMetadata) | ||
603 | 47 | var expectedProductIds set.Strings | ||
604 | 48 | var imageVersions set.Strings | ||
605 | 49 | for _, mc := range cloudMetadata.Products { | ||
606 | 50 | for _, items := range mc.Items { | ||
607 | 51 | for key, item := range items.Items { | ||
608 | 52 | imageMetadata := item.(*imagemetadata.ImageMetadata) | ||
609 | 53 | imageMetadataMap[key] = imageMetadata | ||
610 | 54 | imageVersions.Add(key) | ||
611 | 55 | productId := fmt.Sprintf("com.ubuntu.cloud:server:%s:%s", mc.Version, imageMetadata.Arch) | ||
612 | 56 | expectedProductIds.Add(productId) | ||
613 | 57 | } | ||
614 | 58 | } | ||
615 | 59 | } | ||
616 | 60 | |||
617 | 61 | // Make sure index's product IDs are all represented in the products metadata. | ||
618 | 62 | sort.Strings(imageIndexMetadata.ProductIds) | ||
619 | 63 | c.Assert(imageIndexMetadata.ProductIds, gc.DeepEquals, expectedProductIds.SortedValues()) | ||
620 | 64 | |||
621 | 65 | imageMetadata := make([]*imagemetadata.ImageMetadata, len(imageMetadataMap)) | ||
622 | 66 | for i, key := range imageVersions.SortedValues() { | ||
623 | 67 | imageMetadata[i] = imageMetadataMap[key] | ||
624 | 68 | } | ||
625 | 69 | return imageMetadata | ||
626 | 70 | } | ||
627 | 0 | 71 | ||
628 | === modified file 'environs/imagemetadata/validation_test.go' | |||
629 | --- environs/imagemetadata/validation_test.go 2013-10-11 04:09:04 +0000 | |||
630 | +++ environs/imagemetadata/validation_test.go 2013-10-16 04:12:27 +0000 | |||
631 | @@ -6,6 +6,7 @@ | |||
632 | 6 | import ( | 6 | import ( |
633 | 7 | gc "launchpad.net/gocheck" | 7 | gc "launchpad.net/gocheck" |
634 | 8 | 8 | ||
635 | 9 | "launchpad.net/juju-core/environs/filestorage" | ||
636 | 9 | "launchpad.net/juju-core/environs/imagemetadata" | 10 | "launchpad.net/juju-core/environs/imagemetadata" |
637 | 10 | "launchpad.net/juju-core/environs/simplestreams" | 11 | "launchpad.net/juju-core/environs/simplestreams" |
638 | 11 | "launchpad.net/juju-core/testing/testbase" | 12 | "launchpad.net/juju-core/testing/testbase" |
639 | @@ -27,7 +28,9 @@ | |||
640 | 27 | Region: region, | 28 | Region: region, |
641 | 28 | Endpoint: endpoint, | 29 | Endpoint: endpoint, |
642 | 29 | } | 30 | } |
644 | 30 | _, err := imagemetadata.GenerateMetadata(series, &im, &cloudSpec, s.metadataDir) | 31 | targetStorage, err := filestorage.NewFileStorageWriter(s.metadataDir, filestorage.UseDefaultTmpDir) |
645 | 32 | c.Assert(err, gc.IsNil) | ||
646 | 33 | err = imagemetadata.WriteMetadata(series, &im, &cloudSpec, targetStorage) | ||
647 | 31 | if err != nil { | 34 | if err != nil { |
648 | 32 | return err | 35 | return err |
649 | 33 | } | 36 | } |
650 | 34 | 37 | ||
651 | === modified file 'environs/simplestreams/simplestreams.go' | |||
652 | --- environs/simplestreams/simplestreams.go 2013-10-10 11:40:54 +0000 | |||
653 | +++ environs/simplestreams/simplestreams.go 2013-10-16 04:12:27 +0000 | |||
654 | @@ -32,8 +32,8 @@ | |||
655 | 32 | 32 | ||
656 | 33 | // CloudSpec uniquely defines a specific cloud deployment. | 33 | // CloudSpec uniquely defines a specific cloud deployment. |
657 | 34 | type CloudSpec struct { | 34 | type CloudSpec struct { |
660 | 35 | Region string | 35 | Region string `json:"region"` |
661 | 36 | Endpoint string | 36 | Endpoint string `json:"endpoint"` |
662 | 37 | } | 37 | } |
663 | 38 | 38 | ||
664 | 39 | // HasRegion is implemented by instances which can provide a region to which they belong. | 39 | // HasRegion is implemented by instances which can provide a region to which they belong. |
665 | 40 | 40 | ||
666 | === modified file 'environs/tools/marshal.go' | |||
667 | --- environs/tools/marshal.go 2013-09-02 23:29:30 +0000 | |||
668 | +++ environs/tools/marshal.go 2013-10-16 04:12:27 +0000 | |||
669 | @@ -66,7 +66,7 @@ | |||
670 | 66 | cloud.Updated = updated.Format(time.RFC1123Z) | 66 | cloud.Updated = updated.Format(time.RFC1123Z) |
671 | 67 | cloud.Format = "products:1.0" | 67 | cloud.Format = "products:1.0" |
672 | 68 | cloud.Products = make(map[string]simplestreams.MetadataCatalog) | 68 | cloud.Products = make(map[string]simplestreams.MetadataCatalog) |
674 | 69 | itemsversion := updated.Format("20060201") // YYYYMMDD | 69 | itemsversion := updated.Format("20060102") // YYYYMMDD |
675 | 70 | for _, t := range metadata { | 70 | for _, t := range metadata { |
676 | 71 | id, err := t.productId() | 71 | id, err := t.productId() |
677 | 72 | if err != nil { | 72 | if err != nil { |
Reviewers: mp+190542_ code.launchpad. net,
Message:
Please take a look.
Description:
Write image metadata using json serialisation
This branch writes outs simplestreams image metadata using
json serialisation instead of a template based approach. It
is a pre-requisit to allow the next step of merging new image
metadata instead of overwriting. This will be used when setting
up private clouds.
https:/ /code.launchpad .net/~wallyworl d/juju- core/marshal- image-metadata/ +merge/ 190542
Requires: /code.launchpad .net/~wallyworl d/juju- core/improve- image-metadata- command/ +merge/ 190517
https:/
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/14540055/
Affected files (+398, -135 lines): juju-metadata/ imagemetadata. go imagemetadata/ generate. go imagemetadata/ generate_ test.go imagemetadata/ marshal. go imagemetadata/ marshal_ test.go imagemetadata/ simplestreams. go imagemetadata/ testing/ testing. go imagemetadata/ validation_ test.go simplestreams/ simplestreams. go tools/marshal. go
A [revision details]
M cmd/plugins/
M environs/
A environs/
A environs/
A environs/
M environs/
A environs/
M environs/
M environs/
M environs/