Merge lp:~axwalk/juju-core/juju-metadata-generate-tools into lp:~go-bot/juju-core/trunk
- juju-metadata-generate-tools
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Andrew Wilkins |
Approved revision: | no longer in the source branch. |
Merged at revision: | 1741 |
Proposed branch: | lp:~axwalk/juju-core/juju-metadata-generate-tools |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
1260 lines (+943/-115) 17 files modified
cmd/plugins/juju-metadata/metadata.go (+1/-0) cmd/plugins/juju-metadata/metadataplugin_test.go (+1/-0) cmd/plugins/juju-metadata/toolsmetadata.go (+182/-0) cmd/plugins/juju-metadata/toolsmetadata_test.go (+259/-0) environs/filestorage/filestorage.go (+83/-0) environs/localstorage/storage.go (+6/-0) environs/localstorage/storage_test.go (+9/-0) environs/simplestreams/json.go (+55/-0) environs/simplestreams/json_test.go (+34/-0) environs/simplestreams/simplestreams.go (+20/-34) environs/simplestreams/simplestreams_test.go (+1/-0) environs/sync/sync.go (+2/-73) environs/tools/boilerplate.go (+1/-1) environs/tools/marshal.go (+86/-0) environs/tools/marshal_test.go (+150/-0) environs/tools/simplestreams.go (+12/-7) environs/tools/simplestreams_test.go (+41/-0) |
To merge this branch: | bzr merge lp:~axwalk/juju-core/juju-metadata-generate-tools |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+183070@code.launchpad.net |
Commit message
juju-metadata: add generate-tools subcommand
This is a new subcommand for the juju-metadata plugin,
which can be used to generate simplestreams metadata
(index and products) for an existing set of tools.
The command will list the tools in the public bucket
or a local directory, and generate the index and
tools metadata in the same location.
Description of the change
juju-metadata: add generate-tools subcommand
This is a new subcommand for the juju-metadata plugin,
which can be used to generate simplestreams metadata
(index and products) for an existing set of tools.
The command will list the tools in the public bucket
or a local directory, and generate the index and
tools metadata in the same location.
Andrew Wilkins (axwalk) wrote : | # |
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Ian Booth (wallyworld) wrote : | # |
Coming along nicely. But not quite there. See various comments.
https:/
File cmd/plugins/
https:/
cmd/plugins/
"tools/"
I'd make this "releases"
https:/
cmd/plugins/
environs.
There's little point using an env to read the tools - the tools are
readily accessible directly from "https:/
(see synctools). Ideally, this tool would look also at the specified
directory (via -d) and read the tools tarballs from there (under
releases) and generate the metadata. Or even better, take a URL like
file:// as an arg (but dir will be fine for the first iteration of
this).
The only reason an env would be used is so that tools metadata could be
written to the env's private bucket. However, what will ultimately
happen is synctools will evolve to not just copy the tools tarballs like
it does now, but also write the metadata using the new
MarshallToolsMe
oriented things like easily adding new tool tarballs into the mix so
they can used by juju.
https:/
cmd/plugins/
or private storage at this point.
urlPath should have "releases" prepended.
If the base url is http://
will be at
releases/foo.tar.gz etc
and simplestreams metadata will be at
streams/
https:/
File environs/
https:/
environs/
Do we still need this given the implementation has changed?
https:/
File environs/
https:/
environs/
What's this used for?
https:/
File environs/
https:/
environs/
UnmarshalJSON(b []byte) error {
Please move this and associated code/tests to json.go
https:/
File environs/
https:/
environs/
Andrew Wilkins (axwalk) wrote : | # |
https:/
File cmd/plugins/
https:/
cmd/plugins/
"tools/"
On 2013/08/30 06:40:42, wallyworld wrote:
> I'd make this "releases"
Done.
https:/
cmd/plugins/
environs.
On 2013/08/30 06:40:42, wallyworld wrote:
> There's little point using an env to read the tools - the tools are
readily
> accessible directly from "https:/
synctools).
Gotcha. Sorry, I missed the bit in sync-tools that did the magic. I'll
move the fileStorageReader out of there so I can reuse it.
https:/
cmd/plugins/
or private storage at this point.
On 2013/08/30 06:40:42, wallyworld wrote:
> urlPath should have "releases" prepended.
> If the base url is http://
tarballs will be
> at
> releases/foo.tar.gz etc
> and simplestreams metadata will be at
> streams/
Done.
https:/
File environs/
https:/
environs/
On 2013/08/30 06:40:42, wallyworld wrote:
> Do we still need this given the implementation has changed?
No, I'll back it out.
https:/
File environs/
https:/
environs/
On 2013/08/30 06:40:42, wallyworld wrote:
> What's this used for?
Drive-by fix. List shouldn't error if there are no matching files.
https:/
File environs/
https:/
environs/
UnmarshalJSON(b []byte) error {
On 2013/08/30 06:40:42, wallyworld wrote:
> Please move this and associated code/tests to json.go
Done.
https:/
File environs/
https:/
environs/
On 2013/08/30 06:40:42, wallyworld wrote:
> Could we please make this a big number to test the issue is solved
This test isn't verifying that problem. It's verifying that we get
default JSON unmarshalling behaviour ...
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Ian Booth (wallyworld) wrote : | # |
LGTM with the issues highlighted addressed
https:/
File environs/
https:/
environs/
I'd prefer returning an empty array here ie []string{}
Returning nil, nil seems wrong
https:/
File environs/
https:/
environs/
Sorry if I was unclear, the structs which model the simplestreams data
stay where they were; it's only the json marshall/unmarshall code which
goes in here
https:/
File environs/
https:/
environs/
Please make this a big number so that we can ensure the fix actually
works. I know it's tested elsewhere too, but this code is where the
implementation is
https:/
environs/
why float when we want an int?
https:/
File environs/
https:/
environs/
[]*ToolsMetadata) (index, products []byte, err error) {
Where are the tests for the methods in this file?
Andrew Wilkins (axwalk) wrote : | # |
On 2013/09/02 01:44:13, wallyworld wrote:
> LGTM with the issues highlighted addressed
https:/
> File environs/
https:/
> environs/
> I'd prefer returning an empty array here ie []string{}
> Returning nil, nil seems wrong
Done.
https:/
> File environs/
https:/
> environs/
> Sorry if I was unclear, the structs which model the simplestreams data
stay
> where they were; it's only the json marshall/unmarshall code which
goes in here
Fixed. I've left "type itemCollection" (the ItemCollection clone), and
ItemCollection unmarshalling methods in json.go.
https:/
> File environs/
https:/
> environs/
> Please make this a big number so that we can ensure the fix actually
works. I
> know it's tested elsewhere too, but this code is where the
implementation is
As discussed on IRC, this is not testing the fix for numbers
unmarshalling to int64. This is testing what happens if someone attempts
to un/marshal an ItemCollection directly, not through
ParseCloudMetadata.
https:/
> environs/
> why float when we want an int?
As above. Default encoding/json behaviour for parsing a number into an
interface{} is to unmarshal as float64.
https:/
> File environs/
https:/
> environs/
> []*ToolsMetadata) (index, products []byte, err error) {
> Where are the tests for the methods in this file?
On their way :)
There's tests in cmd/juju-metadata, just haven't migrated yet. Will do
before landing.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Go Bot (go-bot) wrote : | # |
Attempt to merge into lp:juju-core failed due to conflicts:
text conflict in environs/
Go Bot (go-bot) wrote : | # |
The attempt to merge lp:~axwalk/juju-core/juju-metadata-generate-tools into lp:juju-core failed. Below is the output from the failed tests.
# launchpad.
cmd/plugins/
cmd/plugins/
Go Bot (go-bot) wrote : | # |
The attempt to merge lp:~axwalk/juju-core/juju-metadata-generate-tools into lp:juju-core failed. Below is the output from the failed tests.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
-------
FAIL: machine_
[LOG] 82.52202 INFO juju environs/testing: uploading FAKE tools 1.15.0-
[LOG] 82.53892 INFO juju.environs.
[LOG] 82.53895 INFO juju.environs.tools reading tools with major.minor version 1.15
[LOG] 82.53897 INFO juju.environs.tools filtering tools by version: 1.15.0
[LOG] 82.53898 INFO juju.environs.tools filtering tools by series: precise
[LOG] 82.53899 DEBUG juju.environs.tools reading v1.15 tools
[LOG] 82.53900 DEBUG juju.environs.tools reading v1.15 tools
[LOG] 82.53903 DEBUG juju.environs.tools found 1.15.0-
[LOG] 82.53907 INFO juju environs/dummy: would pick tools from 1.15.0-
[LOG] 82.57437 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 82.59163 INFO juju.state connection established
[LOG] 82.61661 INFO juju.state initializing environment
[LOG] 82.63706 INFO juju state/api: listening on "127.0.0.1:58455"
[LOG] 82.65638 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 82.66323 INFO juju.state connection established
[LOG] 82.67894 INFO juju juju: authorization error while connecting to state server; retrying
[LOG] 82.67899 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 82.68542 INFO juju.state connection established
[LOG] 82.71861 INFO juju state/api: dialing "wss://
[LOG] 82.72164 INFO juju state/api: connection established
[LOG] 82.72176 DEBUG juju rpc/jsoncodec: <- {"RequestId"
[LOG] 82.72211 DEBUG juju rpc/jsoncodec: -> {"RequestId"
[LOG] 82.72240 INFO juju.container.lxc lxcObjectFactory replaced with mock lxc factory
[LOG] 82.72344 DEBUG juju rpc/jsoncodec: <- {"RequestId"
[LOG] 82.72351 DEBUG juju rpc/jsoncodec: -> {"RequestId"
[LOG] 82.79369 INFO juju machine agent machine-0 start
[LOG] 82.80775 INFO juju Starting StateWorker for machine-0
[LOG] 82.80780 INFO juju worker: start "state"
[LOG] 82.80783 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 82.80811 INFO juju worker: start "api"
[LOG] 82.80832 INFO juju state/api: dialing "wss://
[...
Go Bot (go-bot) wrote : | # |
The attempt to merge lp:~axwalk/juju-core/juju-metadata-generate-tools into lp:juju-core failed. Below is the output from the failed tests.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
-------
FAIL: toolsmetadata_
clearing private storage
removing files: []
clearing public storage
removing files: []
toolsmetadata_
c.Assert(
Release: "precise",
Version: currentVersion.
Arch: "amd64",
Size: 85,
Path: fmt.Sprintf(
FileType: "tar.gz",
SHA256: "6bd6e4ff34f88a
})
... obtained *tools.
... expected *tools.
OOPS: 31 passed, 1 FAILED
--- FAIL: Test (1.29 seconds)
FAIL
FAIL launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok ...
John A Meinel (jameinel) wrote : | # |
https:/
File cmd/plugins/
https:/
cmd/plugins/
"9268ba87201b15
How did you come up with these SHA sums? It certainly seems very
site-specific because the actual SHA will end up depending on the
details of the compression (by default gzip compression inserts date
stamps into the stream, so you won't ever be able to predict the
compression of a newly generated file).
If we can validate them through some other path, I would love to see it,
but if we have to, you can do:
SHA256: metadata[0].SHA256
Can we grab the actual .tgz before it is uploaded and check its sha that
way?
Andrew Wilkins (axwalk) wrote : | # |
https:/
File cmd/plugins/
https:/
cmd/plugins/
"9268ba87201b15
On 2013/09/02 05:48:30, jameinel wrote:
> How did you come up with these SHA sums? It certainly seems very
site-specific
> because the actual SHA will end up depending on the details of the
compression
> (by default gzip compression inserts date stamps into the stream, so
you won't
> ever be able to predict the compression of a newly generated file).
I've changed this a bit already, so the contents of the files are not
tgz, but a well defined content string. So the SHA1 sum is just a sum of
that content.
> If we can validate them through some other path, I would love to see
it, but if
> we have to, you can do:
> SHA256: metadata[0].SHA256
What would be the point? I'm checking the value of metadata[N] :)
> Can we grab the actual .tgz before it is uploaded and check its sha
that way?
Yes, that's probably more sensible. I'll do that.
Go Bot (go-bot) wrote : | # |
The attempt to merge lp:~axwalk/juju-core/juju-metadata-generate-tools into lp:juju-core failed. Below is the output from the failed tests.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
-------
FAIL: machine_
[LOG] 3.36609 INFO juju environs/testing: uploading FAKE tools 1.15.0-
[LOG] 3.38755 INFO juju.environs.
[LOG] 3.38758 INFO juju.environs.tools reading tools with major.minor version 1.15
[LOG] 3.38760 INFO juju.environs.tools filtering tools by version: 1.15.0
[LOG] 3.38761 INFO juju.environs.tools filtering tools by series: precise
[LOG] 3.38762 DEBUG juju.environs.tools reading v1.15 tools
[LOG] 3.38764 DEBUG juju.environs.tools reading v1.15 tools
[LOG] 3.38771 DEBUG juju.environs.tools found 1.15.0-
[LOG] 3.38775 INFO juju environs/dummy: would pick tools from 1.15.0-
[LOG] 3.41266 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 3.43032 INFO juju.state connection established
[LOG] 3.46754 INFO juju.state initializing environment
[LOG] 3.50535 INFO juju state/api: listening on "127.0.0.1:47605"
[LOG] 3.52603 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 3.54228 INFO juju.state connection established
[LOG] 3.54291 INFO juju juju: authorization error while connecting to state server; retrying
[LOG] 3.54298 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 3.54845 INFO juju.state connection established
[LOG] 3.57492 INFO juju state/api: dialing "wss://
[LOG] 3.57794 INFO juju state/api: connection established
[LOG] 3.57806 DEBUG juju rpc/jsoncodec: <- {"RequestId"
[LOG] 3.57838 DEBUG juju rpc/jsoncodec: -> {"RequestId"
[LOG] 3.57864 INFO juju.container.lxc lxcObjectFactory replaced with mock lxc factory
[LOG] 3.57970 DEBUG juju rpc/jsoncodec: <- {"RequestId"
[LOG] 3.57976 DEBUG juju rpc/jsoncodec: -> {"RequestId"
[LOG] 3.65935 INFO juju machine agent machine-0 start
[LOG] 3.67458 INFO juju Starting StateWorker for machine-0
[LOG] 3.67464 INFO juju worker: start "state"
[LOG] 3.67466 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 3.67490 INFO juju worker: start "api"
[LOG] 3.67511 INFO juju state/api: dialing "wss://
[LOG] 3.67574 ERROR juju state/a...
Go Bot (go-bot) wrote : | # |
The attempt to merge lp:~axwalk/juju-core/juju-metadata-generate-tools into lp:juju-core failed. Below is the output from the failed tests.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
-------
FAIL: machine_
[LOG] 2.87215 INFO juju environs/testing: uploading FAKE tools 1.15.0-
[LOG] 2.88895 INFO juju.environs.
[LOG] 2.88898 INFO juju.environs.tools reading tools with major.minor version 1.15
[LOG] 2.88901 INFO juju.environs.tools filtering tools by version: 1.15.0
[LOG] 2.88901 INFO juju.environs.tools filtering tools by series: precise
[LOG] 2.88902 DEBUG juju.environs.tools reading v1.15 tools
[LOG] 2.88904 DEBUG juju.environs.tools reading v1.15 tools
[LOG] 2.88907 DEBUG juju.environs.tools found 1.15.0-
[LOG] 2.88911 INFO juju environs/dummy: would pick tools from 1.15.0-
[LOG] 2.91988 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 2.93285 INFO juju.state connection established
[LOG] 2.96211 INFO juju.state initializing environment
[LOG] 2.99495 INFO juju state/api: listening on "127.0.0.1:57889"
[LOG] 3.01132 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 3.03637 INFO juju.state connection established
[LOG] 3.03833 INFO juju juju: authorization error while connecting to state server; retrying
[LOG] 3.03845 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 3.04553 INFO juju.state connection established
[LOG] 3.07147 INFO juju state/api: dialing "wss://
[LOG] 3.07452 INFO juju state/api: connection established
[LOG] 3.07463 DEBUG juju rpc/jsoncodec: <- {"RequestId"
[LOG] 3.07497 DEBUG juju rpc/jsoncodec: -> {"RequestId"
[LOG] 3.07523 INFO juju.container.lxc lxcObjectFactory replaced with mock lxc factory
[LOG] 3.07630 DEBUG juju rpc/jsoncodec: <- {"RequestId"
[LOG] 3.07637 DEBUG juju rpc/jsoncodec: -> {"RequestId"
[LOG] 3.14233 INFO juju machine agent machine-0 start
[LOG] 3.15640 INFO juju Starting StateWorker for machine-0
[LOG] 3.15647 INFO juju worker: start "state"
[LOG] 3.15650 INFO juju.state opening state; mongo addresses: ["localhost:
[LOG] 3.15680 INFO juju worker: start "api"
[LOG] 3.15707 INFO juju state/api: dialing "wss://
[LOG] 3.15769 ERROR juju state/a...
Preview Diff
1 | === modified file 'cmd/plugins/juju-metadata/metadata.go' |
2 | --- cmd/plugins/juju-metadata/metadata.go 2013-08-19 23:32:17 +0000 |
3 | +++ cmd/plugins/juju-metadata/metadata.go 2013-09-02 06:06:26 +0000 |
4 | @@ -34,6 +34,7 @@ |
5 | |
6 | metadatacmd.Register(&ValidateImageMetadataCommand{}) |
7 | metadatacmd.Register(&ImageMetadataCommand{}) |
8 | + metadatacmd.Register(&ToolsMetadataCommand{}) |
9 | |
10 | metadatacmd.Register(&ValidateToolsMetadataCommand{}) |
11 | |
12 | |
13 | === modified file 'cmd/plugins/juju-metadata/metadataplugin_test.go' |
14 | --- cmd/plugins/juju-metadata/metadataplugin_test.go 2013-08-13 04:55:12 +0000 |
15 | +++ cmd/plugins/juju-metadata/metadataplugin_test.go 2013-09-02 06:06:26 +0000 |
16 | @@ -30,6 +30,7 @@ |
17 | |
18 | var metadataCommandNames = []string{ |
19 | "generate-image", |
20 | + "generate-tools", |
21 | "help", |
22 | "validate-images", |
23 | "validate-tools", |
24 | |
25 | === added file 'cmd/plugins/juju-metadata/toolsmetadata.go' |
26 | --- cmd/plugins/juju-metadata/toolsmetadata.go 1970-01-01 00:00:00 +0000 |
27 | +++ cmd/plugins/juju-metadata/toolsmetadata.go 2013-09-02 06:06:26 +0000 |
28 | @@ -0,0 +1,182 @@ |
29 | +// Copyright 2013 Canonical Ltd. |
30 | +// Licensed under the AGPLv3, see LICENCE file for details. |
31 | + |
32 | +package main |
33 | + |
34 | +import ( |
35 | + "crypto/sha256" |
36 | + "fmt" |
37 | + "hash" |
38 | + "io" |
39 | + "io/ioutil" |
40 | + "net/http" |
41 | + "os" |
42 | + "path/filepath" |
43 | + "time" |
44 | + |
45 | + "launchpad.net/gnuflag" |
46 | + |
47 | + "launchpad.net/juju-core/cmd" |
48 | + "launchpad.net/juju-core/environs" |
49 | + "launchpad.net/juju-core/environs/config" |
50 | + "launchpad.net/juju-core/environs/localstorage" |
51 | + "launchpad.net/juju-core/environs/simplestreams" |
52 | + "launchpad.net/juju-core/environs/sync" |
53 | + "launchpad.net/juju-core/environs/tools" |
54 | + "launchpad.net/juju-core/provider/ec2" |
55 | + "launchpad.net/juju-core/utils" |
56 | + "launchpad.net/juju-core/version" |
57 | +) |
58 | + |
59 | +var DefaultToolsLocation = sync.DefaultToolsLocation |
60 | + |
61 | +// ToolsMetadataCommand is used to generate simplestreams metadata for |
62 | +// juju tools. |
63 | +type ToolsMetadataCommand struct { |
64 | + cmd.EnvCommandBase |
65 | + fetch bool |
66 | + metadataDir string |
67 | + |
68 | + // noS3 is used in testing to disable the use of S3 public storage |
69 | + // as a backup. |
70 | + noS3 bool |
71 | +} |
72 | + |
73 | +func (c *ToolsMetadataCommand) Info() *cmd.Info { |
74 | + return &cmd.Info{ |
75 | + Name: "generate-tools", |
76 | + Purpose: "generate simplestreams tools metadata", |
77 | + } |
78 | +} |
79 | + |
80 | +func (c *ToolsMetadataCommand) SetFlags(f *gnuflag.FlagSet) { |
81 | + c.EnvCommandBase.SetFlags(f) |
82 | + f.BoolVar(&c.fetch, "fetch", true, "fetch tools and compute content size and hash") |
83 | + f.StringVar(&c.metadataDir, "d", "", "local directory in which to store metadata") |
84 | +} |
85 | + |
86 | +func (c *ToolsMetadataCommand) Run(context *cmd.Context) error { |
87 | + if c.metadataDir == "" { |
88 | + c.metadataDir = config.JujuHome() |
89 | + } |
90 | + c.metadataDir = utils.NormalizePath(c.metadataDir) |
91 | + |
92 | + // Create a StorageReader that will get a tools list from the local disk. |
93 | + // Since ReadList expects tools to be immediately under "tools/", and we |
94 | + // want them to be in tools/releases, we have to wrap the storage. |
95 | + listener, err := localstorage.Serve("127.0.0.1:0", c.metadataDir) |
96 | + if err != nil { |
97 | + return err |
98 | + } |
99 | + defer listener.Close() |
100 | + var sourceStorage environs.StorageReader = localstorage.Client(listener.Addr().String()) |
101 | + sourceStorage = prefixedToolsStorage{sourceStorage} |
102 | + |
103 | + fmt.Fprintln(context.Stdout, "Finding tools...") |
104 | + const minorVersion = -1 |
105 | + toolsList, err := tools.ReadList(sourceStorage, version.Current.Major, minorVersion) |
106 | + if err == tools.ErrNoTools && !c.noS3 { |
107 | + sourceStorage = ec2.NewHTTPStorageReader(sync.DefaultToolsLocation) |
108 | + toolsList, err = tools.ReadList(sourceStorage, version.Current.Major, minorVersion) |
109 | + } |
110 | + if err != nil { |
111 | + return err |
112 | + } |
113 | + |
114 | + metadata := make([]*tools.ToolsMetadata, len(toolsList)) |
115 | + for i, t := range toolsList { |
116 | + var size int64 |
117 | + var sha256hex string |
118 | + if c.fetch { |
119 | + fmt.Fprintln(context.Stdout, "Fetching tools to generate hash:", t.URL) |
120 | + var sha256hash hash.Hash |
121 | + size, sha256hash, err = fetchToolsHash(t.URL) |
122 | + if err != nil { |
123 | + return err |
124 | + } |
125 | + sha256hex = fmt.Sprintf("%x", sha256hash.Sum(nil)) |
126 | + } |
127 | + |
128 | + path := fmt.Sprintf("releases/juju-%s-%s-%s.tgz", t.Version.Number, t.Version.Series, t.Version.Arch) |
129 | + metadata[i] = &tools.ToolsMetadata{ |
130 | + Release: t.Version.Series, |
131 | + Version: t.Version.Number.String(), |
132 | + Arch: t.Version.Arch, |
133 | + Path: path, |
134 | + FileType: "tar.gz", |
135 | + Size: size, |
136 | + SHA256: sha256hex, |
137 | + } |
138 | + } |
139 | + |
140 | + index, products, err := tools.MarshalToolsMetadataJSON(metadata, time.Now()) |
141 | + if err != nil { |
142 | + return err |
143 | + } |
144 | + objects := []struct { |
145 | + path string |
146 | + data []byte |
147 | + }{ |
148 | + {simplestreams.DefaultIndexPath + simplestreams.UnsignedSuffix, index}, |
149 | + {tools.ProductMetadataPath, products}, |
150 | + } |
151 | + for _, object := range objects { |
152 | + path := filepath.Join(c.metadataDir, "tools", object.path) |
153 | + fmt.Fprintf(context.Stdout, "Writing %s\n", path) |
154 | + if err = writeFile(path, object.data); err != nil { |
155 | + return err |
156 | + } |
157 | + } |
158 | + return nil |
159 | +} |
160 | + |
161 | +func writeFile(path string, data []byte) error { |
162 | + dir := filepath.Dir(path) |
163 | + if err := os.MkdirAll(dir, 0755); err != nil && !os.IsExist(err) { |
164 | + return err |
165 | + } |
166 | + return ioutil.WriteFile(path, data, 0644) |
167 | +} |
168 | + |
169 | +// fetchToolsHash fetches the file at the specified URL, |
170 | +// and calculates its size in bytes and computes a SHA256 |
171 | +// hash of its contents. |
172 | +func fetchToolsHash(url string) (size int64, sha256hash hash.Hash, err error) { |
173 | + resp, err := http.Get(url) |
174 | + if err != nil { |
175 | + return 0, nil, err |
176 | + } |
177 | + sha256hash = sha256.New() |
178 | + size, err = io.Copy(sha256hash, resp.Body) |
179 | + resp.Body.Close() |
180 | + return size, sha256hash, err |
181 | +} |
182 | + |
183 | +const fromprefix = "tools/" |
184 | +const toprefix = "tools/releases/" |
185 | + |
186 | +type prefixedToolsStorage struct { |
187 | + environs.StorageReader |
188 | +} |
189 | + |
190 | +func (s prefixedToolsStorage) translate(name string) string { |
191 | + return toprefix + name[len(fromprefix):] |
192 | +} |
193 | + |
194 | +func (s prefixedToolsStorage) Get(name string) (io.ReadCloser, error) { |
195 | + return s.StorageReader.Get(s.translate(name)) |
196 | +} |
197 | + |
198 | +func (s prefixedToolsStorage) List(prefix string) ([]string, error) { |
199 | + names, err := s.StorageReader.List(s.translate(prefix)) |
200 | + if err == nil { |
201 | + for i, name := range names { |
202 | + names[i] = fromprefix + name[len(toprefix):] |
203 | + } |
204 | + } |
205 | + return names, err |
206 | +} |
207 | + |
208 | +func (s prefixedToolsStorage) URL(name string) (string, error) { |
209 | + return s.StorageReader.URL(s.translate(name)) |
210 | +} |
211 | |
212 | === added file 'cmd/plugins/juju-metadata/toolsmetadata_test.go' |
213 | --- cmd/plugins/juju-metadata/toolsmetadata_test.go 1970-01-01 00:00:00 +0000 |
214 | +++ cmd/plugins/juju-metadata/toolsmetadata_test.go 2013-09-02 06:06:26 +0000 |
215 | @@ -0,0 +1,259 @@ |
216 | +// Copyright 2013 Canonical Ltd. |
217 | +// Licensed under the AGPLv3, see LICENCE file for details. |
218 | + |
219 | +package main |
220 | + |
221 | +import ( |
222 | + "bytes" |
223 | + "crypto/sha256" |
224 | + "fmt" |
225 | + "io" |
226 | + "io/ioutil" |
227 | + "net/http" |
228 | + "os" |
229 | + "path" |
230 | + "path/filepath" |
231 | + "regexp" |
232 | + "sort" |
233 | + "strings" |
234 | + |
235 | + gc "launchpad.net/gocheck" |
236 | + |
237 | + "launchpad.net/juju-core/cmd" |
238 | + "launchpad.net/juju-core/environs" |
239 | + "launchpad.net/juju-core/environs/config" |
240 | + "launchpad.net/juju-core/environs/simplestreams" |
241 | + envtesting "launchpad.net/juju-core/environs/testing" |
242 | + "launchpad.net/juju-core/environs/tools" |
243 | + _ "launchpad.net/juju-core/provider/dummy" |
244 | + coretesting "launchpad.net/juju-core/testing" |
245 | + "launchpad.net/juju-core/utils/set" |
246 | + "launchpad.net/juju-core/version" |
247 | +) |
248 | + |
249 | +type ToolsMetadataSuite struct { |
250 | + home *coretesting.FakeHome |
251 | + env environs.Environ |
252 | +} |
253 | + |
254 | +var _ = gc.Suite(&ToolsMetadataSuite{}) |
255 | + |
256 | +func (s *ToolsMetadataSuite) SetUpTest(c *gc.C) { |
257 | + s.home = coretesting.MakeSampleHome(c) |
258 | + env, err := environs.PrepareFromName("erewhemos") |
259 | + c.Assert(err, gc.IsNil) |
260 | + s.env = env |
261 | + envtesting.RemoveAllTools(c, s.env) |
262 | +} |
263 | + |
264 | +func (s *ToolsMetadataSuite) TearDownTest(c *gc.C) { |
265 | + s.home.Restore() |
266 | +} |
267 | + |
268 | +func (s *ToolsMetadataSuite) parseMetadata(c *gc.C, metadataDir string) []*tools.ToolsMetadata { |
269 | + params := simplestreams.ValueParams{ |
270 | + DataType: tools.ContentDownload, |
271 | + ValueTemplate: tools.ToolsMetadata{}, |
272 | + } |
273 | + |
274 | + baseURL := "file://" + metadataDir + "/tools" |
275 | + transport := &http.Transport{} |
276 | + transport.RegisterProtocol("file", http.NewFileTransport(http.Dir("/"))) |
277 | + old := simplestreams.SetHttpClient(&http.Client{Transport: transport}) |
278 | + defer simplestreams.SetHttpClient(old) |
279 | + |
280 | + const requireSigned = false |
281 | + indexPath := simplestreams.DefaultIndexPath + simplestreams.UnsignedSuffix |
282 | + indexRef, err := simplestreams.GetIndexWithFormat(baseURL, indexPath, "index:1.0", requireSigned, params) |
283 | + c.Assert(err, gc.IsNil) |
284 | + c.Assert(indexRef.Indexes, gc.HasLen, 1) |
285 | + |
286 | + toolsIndexMetadata := indexRef.Indexes["com.ubuntu.juju:released:tools"] |
287 | + c.Assert(toolsIndexMetadata, gc.NotNil) |
288 | + |
289 | + data, err := ioutil.ReadFile(filepath.Join(metadataDir, "tools", toolsIndexMetadata.ProductsFilePath)) |
290 | + c.Assert(err, gc.IsNil) |
291 | + |
292 | + url := path.Join(baseURL, toolsIndexMetadata.ProductsFilePath) |
293 | + c.Assert(err, gc.IsNil) |
294 | + cloudMetadata, err := simplestreams.ParseCloudMetadata(data, "products:1.0", url, tools.ToolsMetadata{}) |
295 | + c.Assert(err, gc.IsNil) |
296 | + |
297 | + toolsMetadataMap := make(map[string]*tools.ToolsMetadata) |
298 | + var expectedProductIds set.Strings |
299 | + var toolsVersions set.Strings |
300 | + for _, mc := range cloudMetadata.Products { |
301 | + for _, items := range mc.Items { |
302 | + for key, item := range items.Items { |
303 | + toolsMetadata := item.(*tools.ToolsMetadata) |
304 | + toolsMetadataMap[key] = toolsMetadata |
305 | + toolsVersions.Add(key) |
306 | + productId := fmt.Sprintf("com.ubuntu.juju:%s:%s", toolsMetadata.Version, toolsMetadata.Arch) |
307 | + expectedProductIds.Add(productId) |
308 | + } |
309 | + } |
310 | + } |
311 | + |
312 | + // Make sure index's product IDs are all represented in the products metadata. |
313 | + sort.Strings(toolsIndexMetadata.ProductIds) |
314 | + c.Assert(toolsIndexMetadata.ProductIds, gc.DeepEquals, expectedProductIds.SortedValues()) |
315 | + |
316 | + toolsMetadata := make([]*tools.ToolsMetadata, len(toolsMetadataMap)) |
317 | + for i, key := range toolsVersions.SortedValues() { |
318 | + toolsMetadata[i] = toolsMetadataMap[key] |
319 | + } |
320 | + return toolsMetadata |
321 | +} |
322 | + |
323 | +func (s *ToolsMetadataSuite) makeTools(c *gc.C, metadataDir string, versionStrings []string) { |
324 | + toolsDir := filepath.Join(metadataDir, "tools", "releases") |
325 | + c.Assert(os.MkdirAll(toolsDir, 0755), gc.IsNil) |
326 | + for _, versionString := range versionStrings { |
327 | + binary := version.MustParseBinary(versionString) |
328 | + path := filepath.Join(toolsDir, fmt.Sprintf("juju-%s.tgz", binary)) |
329 | + err := ioutil.WriteFile(path, []byte(binary.String()), 0644) |
330 | + c.Assert(err, gc.IsNil) |
331 | + } |
332 | +} |
333 | + |
334 | +var currentVersionStrings = []string{ |
335 | + // only these ones will make it into the JSON files. |
336 | + version.CurrentNumber().String() + "-quantal-amd64", |
337 | + version.CurrentNumber().String() + "-quantal-arm", |
338 | + version.CurrentNumber().String() + "-quantal-i386", |
339 | +} |
340 | + |
341 | +var versionStrings = append([]string{ |
342 | + "1.12.0-precise-amd64", |
343 | + "1.12.0-precise-i386", |
344 | + "1.12.0-raring-amd64", |
345 | + "1.12.0-raring-i386", |
346 | + "1.13.0-precise-amd64", |
347 | +}, currentVersionStrings...) |
348 | + |
349 | +var expectedOutputCommon = makeExpectedOutputCommon() |
350 | + |
351 | +func makeExpectedOutputCommon() string { |
352 | + expected := `Finding tools\.\.\. |
353 | +Fetching tools to generate hash: http://.*/tools/releases/juju-1\.12\.0-precise-amd64\.tgz |
354 | +Fetching tools to generate hash: http://.*/tools/releases/juju-1\.12\.0-precise-i386\.tgz |
355 | +Fetching tools to generate hash: http://.*/tools/releases/juju-1\.12\.0-raring-amd64\.tgz |
356 | +Fetching tools to generate hash: http://.*/tools/releases/juju-1\.12\.0-raring-i386\.tgz |
357 | +Fetching tools to generate hash: http://.*/tools/releases/juju-1\.13\.0-precise-amd64\.tgz |
358 | +` |
359 | + f := "Fetching tools to generate hash: http://.*/tools/releases/juju-%s\\.tgz\n" |
360 | + for _, v := range currentVersionStrings { |
361 | + expected += fmt.Sprintf(f, regexp.QuoteMeta(v)) |
362 | + } |
363 | + return strings.TrimSpace(expected) |
364 | +} |
365 | + |
366 | +var expectedOutputDirectory = expectedOutputCommon + ` |
367 | +Writing %s/tools/streams/v1/index\.json |
368 | +Writing %s/tools/streams/v1/com\.ubuntu\.juju:released:tools\.json |
369 | +` |
370 | + |
371 | +func (s *ToolsMetadataSuite) TestGenerateDefaultDirectory(c *gc.C) { |
372 | + metadataDir := config.JujuHome() // default metadata dir |
373 | + s.makeTools(c, metadataDir, versionStrings) |
374 | + ctx := coretesting.Context(c) |
375 | + code := cmd.Main(&ToolsMetadataCommand{noS3: true}, ctx, nil) |
376 | + c.Assert(code, gc.Equals, 0) |
377 | + output := ctx.Stdout.(*bytes.Buffer).String() |
378 | + expected := fmt.Sprintf(expectedOutputDirectory, metadataDir, metadataDir) |
379 | + c.Assert(output, gc.Matches, expected) |
380 | + metadata := s.parseMetadata(c, metadataDir) |
381 | + c.Assert(metadata, gc.HasLen, len(versionStrings)) |
382 | + obtainedVersionStrings := make([]string, len(versionStrings)) |
383 | + for i, metadata := range metadata { |
384 | + s := fmt.Sprintf("%s-%s-%s", metadata.Version, metadata.Release, metadata.Arch) |
385 | + obtainedVersionStrings[i] = s |
386 | + } |
387 | + c.Assert(obtainedVersionStrings, gc.DeepEquals, versionStrings) |
388 | +} |
389 | + |
390 | +func (s *ToolsMetadataSuite) TestGenerateDirectory(c *gc.C) { |
391 | + metadataDir := c.MkDir() |
392 | + s.makeTools(c, metadataDir, versionStrings) |
393 | + ctx := coretesting.Context(c) |
394 | + code := cmd.Main(&ToolsMetadataCommand{noS3: true}, ctx, []string{"-d", metadataDir}) |
395 | + c.Assert(code, gc.Equals, 0) |
396 | + output := ctx.Stdout.(*bytes.Buffer).String() |
397 | + expected := fmt.Sprintf(expectedOutputDirectory, metadataDir, metadataDir) |
398 | + c.Assert(output, gc.Matches, expected) |
399 | + metadata := s.parseMetadata(c, metadataDir) |
400 | + c.Assert(metadata, gc.HasLen, len(versionStrings)) |
401 | + obtainedVersionStrings := make([]string, len(versionStrings)) |
402 | + for i, metadata := range metadata { |
403 | + s := fmt.Sprintf("%s-%s-%s", metadata.Version, metadata.Release, metadata.Arch) |
404 | + obtainedVersionStrings[i] = s |
405 | + } |
406 | + c.Assert(obtainedVersionStrings, gc.DeepEquals, versionStrings) |
407 | +} |
408 | + |
409 | +func (s *ToolsMetadataSuite) TestNoTools(c *gc.C) { |
410 | + ctx := coretesting.Context(c) |
411 | + code := cmd.Main(&ToolsMetadataCommand{noS3: true}, ctx, nil) |
412 | + c.Assert(code, gc.Equals, 1) |
413 | + stdout := ctx.Stdout.(*bytes.Buffer).String() |
414 | + c.Assert(stdout, gc.Matches, "Finding tools\\.\\.\\.\n") |
415 | + stderr := ctx.Stderr.(*bytes.Buffer).String() |
416 | + c.Assert(stderr, gc.Matches, "error: no tools available\n") |
417 | +} |
418 | + |
419 | +func (s *ToolsMetadataSuite) TestPatchLevels(c *gc.C) { |
420 | + currentVersion := version.CurrentNumber() |
421 | + currentVersion.Build = 0 |
422 | + versionStrings := []string{ |
423 | + currentVersion.String() + "-precise-amd64", |
424 | + currentVersion.String() + ".1-precise-amd64", |
425 | + } |
426 | + metadataDir := config.JujuHome() // default metadata dir |
427 | + s.makeTools(c, metadataDir, versionStrings) |
428 | + ctx := coretesting.Context(c) |
429 | + code := cmd.Main(&ToolsMetadataCommand{noS3: true}, ctx, nil) |
430 | + c.Assert(code, gc.Equals, 0) |
431 | + output := ctx.Stdout.(*bytes.Buffer).String() |
432 | + expectedOutput := fmt.Sprintf(` |
433 | +Finding tools\.\.\. |
434 | +Fetching tools to generate hash: http://.*/tools/releases/juju-%s\.tgz |
435 | +Fetching tools to generate hash: http://.*/tools/releases/juju-%s\.tgz |
436 | +Writing %s/tools/streams/v1/index\.json |
437 | +Writing %s/tools/streams/v1/com\.ubuntu\.juju:released:tools\.json |
438 | +`[1:], regexp.QuoteMeta(versionStrings[0]), regexp.QuoteMeta(versionStrings[1]), metadataDir, metadataDir) |
439 | + c.Assert(output, gc.Matches, expectedOutput) |
440 | + metadata := s.parseMetadata(c, metadataDir) |
441 | + c.Assert(metadata, gc.HasLen, 2) |
442 | + |
443 | + filename := fmt.Sprintf("juju-%s-precise-amd64.tgz", currentVersion) |
444 | + c.Assert(metadata[0], gc.DeepEquals, &tools.ToolsMetadata{ |
445 | + Release: "precise", |
446 | + Version: currentVersion.String(), |
447 | + Arch: "amd64", |
448 | + Size: 20, |
449 | + Path: "releases/" + filename, |
450 | + FileType: "tar.gz", |
451 | + SHA256: sha256sum(c, filepath.Join(metadataDir, "tools", "releases", filename)), |
452 | + }) |
453 | + |
454 | + filename = fmt.Sprintf("juju-%s.1-precise-amd64.tgz", currentVersion) |
455 | + c.Assert(metadata[1], gc.DeepEquals, &tools.ToolsMetadata{ |
456 | + Release: "precise", |
457 | + Version: currentVersion.String() + ".1", |
458 | + Arch: "amd64", |
459 | + Size: 22, |
460 | + Path: "releases/" + filename, |
461 | + FileType: "tar.gz", |
462 | + SHA256: sha256sum(c, filepath.Join(metadataDir, "tools", "releases", filename)), |
463 | + }) |
464 | +} |
465 | + |
466 | +func sha256sum(c *gc.C, path string) string { |
467 | + f, err := os.Open(path) |
468 | + c.Assert(err, gc.IsNil) |
469 | + defer f.Close() |
470 | + hash := sha256.New() |
471 | + _, err = io.Copy(hash, f) |
472 | + c.Assert(err, gc.IsNil) |
473 | + return fmt.Sprintf("%x", hash.Sum(nil)) |
474 | +} |
475 | |
476 | === added directory 'environs/filestorage' |
477 | === added file 'environs/filestorage/filestorage.go' |
478 | --- environs/filestorage/filestorage.go 1970-01-01 00:00:00 +0000 |
479 | +++ environs/filestorage/filestorage.go 2013-09-02 06:06:26 +0000 |
480 | @@ -0,0 +1,83 @@ |
481 | +// Copyright 2012, 2013 Canonical Ltd. |
482 | +// Licensed under the AGPLv3, see LICENCE file for details. |
483 | + |
484 | +package filestorage |
485 | + |
486 | +import ( |
487 | + "fmt" |
488 | + "io" |
489 | + "os" |
490 | + "path" |
491 | + "path/filepath" |
492 | + "sort" |
493 | + |
494 | + "launchpad.net/juju-core/environs" |
495 | + "launchpad.net/juju-core/utils" |
496 | +) |
497 | + |
498 | +// fileStorageReader implements StorageReader backed |
499 | +// by the local filesystem. |
500 | +type fileStorageReader struct { |
501 | + path string |
502 | +} |
503 | + |
504 | +// newFileStorageReader returns a new storage reader for |
505 | +// a directory inside the local file system. |
506 | +func NewFileStorageReader(path string) (environs.StorageReader, error) { |
507 | + p := filepath.Clean(path) |
508 | + fi, err := os.Stat(p) |
509 | + if err != nil { |
510 | + return nil, err |
511 | + } |
512 | + if !fi.Mode().IsDir() { |
513 | + return nil, fmt.Errorf("specified source path is not a directory: %s", path) |
514 | + } |
515 | + return &fileStorageReader{p}, nil |
516 | +} |
517 | + |
518 | +// Get implements environs.StorageReader.Get. |
519 | +func (f *fileStorageReader) Get(name string) (io.ReadCloser, error) { |
520 | + filename, err := f.URL(name) |
521 | + if err != nil { |
522 | + return nil, err |
523 | + } |
524 | + file, err := os.Open(filename) |
525 | + if err != nil { |
526 | + return nil, err |
527 | + } |
528 | + return file, nil |
529 | +} |
530 | + |
531 | +// List implements environs.StorageReader.List. |
532 | +func (f *fileStorageReader) List(prefix string) ([]string, error) { |
533 | + // Add one for the missing path separator. |
534 | + pathlen := len(f.path) + 1 |
535 | + pattern := filepath.Join(f.path, prefix+"*") |
536 | + matches, err := filepath.Glob(pattern) |
537 | + if err != nil { |
538 | + return nil, err |
539 | + } |
540 | + list := []string{} |
541 | + for _, match := range matches { |
542 | + fi, err := os.Stat(match) |
543 | + if err != nil { |
544 | + return nil, err |
545 | + } |
546 | + if !fi.Mode().IsDir() { |
547 | + filename := match[pathlen:] |
548 | + list = append(list, filename) |
549 | + } |
550 | + } |
551 | + sort.Strings(list) |
552 | + return list, nil |
553 | +} |
554 | + |
555 | +// URL implements environs.StorageReader.URL. |
556 | +func (f *fileStorageReader) URL(name string) (string, error) { |
557 | + return path.Join(f.path, name), nil |
558 | +} |
559 | + |
560 | +// ConsistencyStrategy implements environs.StorageReader.ConsistencyStrategy. |
561 | +func (f *fileStorageReader) ConsistencyStrategy() utils.AttemptStrategy { |
562 | + return utils.AttemptStrategy{} |
563 | +} |
564 | |
565 | === modified file 'environs/localstorage/storage.go' |
566 | --- environs/localstorage/storage.go 2013-08-30 16:31:45 +0000 |
567 | +++ environs/localstorage/storage.go 2013-09-02 06:06:26 +0000 |
568 | @@ -63,6 +63,12 @@ |
569 | return nil, err |
570 | } |
571 | if resp.StatusCode != http.StatusOK { |
572 | + // If the path is not found, it's not an error |
573 | + // because it's only created when the first |
574 | + // file is put. |
575 | + if resp.StatusCode == http.StatusNotFound { |
576 | + return []string{}, nil |
577 | + } |
578 | return nil, fmt.Errorf("%s", resp.Status) |
579 | } |
580 | defer resp.Body.Close() |
581 | |
582 | === modified file 'environs/localstorage/storage_test.go' |
583 | --- environs/localstorage/storage_test.go 2013-08-19 11:19:07 +0000 |
584 | +++ environs/localstorage/storage_test.go 2013-09-02 06:06:26 +0000 |
585 | @@ -22,6 +22,15 @@ |
586 | |
587 | var _ = gc.Suite(&storageSuite{}) |
588 | |
589 | +func (s *storageSuite) TestList(c *gc.C) { |
590 | + listener, _, _ := startServer(c) |
591 | + defer listener.Close() |
592 | + storage := localstorage.Client(listener.Addr().String()) |
593 | + names, err := storage.List("a/b/c") |
594 | + c.Assert(err, gc.IsNil) |
595 | + c.Assert(names, gc.HasLen, 0) |
596 | +} |
597 | + |
598 | // TestPersistence tests the adding, reading, listing and removing |
599 | // of files from the local storage. |
600 | func (s *storageSuite) TestPersistence(c *gc.C) { |
601 | |
602 | === added file 'environs/simplestreams/json.go' |
603 | --- environs/simplestreams/json.go 1970-01-01 00:00:00 +0000 |
604 | +++ environs/simplestreams/json.go 2013-09-02 06:06:26 +0000 |
605 | @@ -0,0 +1,55 @@ |
606 | +// Copyright 2013 Canonical Ltd. |
607 | +// Licensed under the AGPLv3, see LICENCE file for details. |
608 | + |
609 | +package simplestreams |
610 | + |
611 | +import ( |
612 | + "encoding/json" |
613 | + "reflect" |
614 | +) |
615 | + |
616 | +// itemCollection is a clone of ItemCollection, but |
617 | +// does not implement json.Unmarshaler. |
618 | +type itemCollection struct { |
619 | + Items map[string]*json.RawMessage `json:"items"` |
620 | + Arch string `json:"arch,omitempty"` |
621 | + Version string `json:"version,omitempty"` |
622 | + RegionName string `json:"region,omitempty"` |
623 | + Endpoint string `json:"endpoint,omitempty"` |
624 | +} |
625 | + |
626 | +// ItemsCollection.UnmarshalJSON unmarshals the ItemCollection, |
627 | +// storing the raw bytes for each item. These can later be |
628 | +// unmarshalled again into product-specific types. |
629 | +func (c *ItemCollection) UnmarshalJSON(b []byte) error { |
630 | + var raw itemCollection |
631 | + if err := json.Unmarshal(b, &raw); err != nil { |
632 | + return err |
633 | + } |
634 | + c.rawItems = raw.Items |
635 | + c.Items = make(map[string]interface{}, len(raw.Items)) |
636 | + c.Arch = raw.Arch |
637 | + c.Version = raw.Version |
638 | + c.RegionName = raw.RegionName |
639 | + c.Endpoint = raw.Endpoint |
640 | + for key, rawv := range raw.Items { |
641 | + var v interface{} |
642 | + if err := json.Unmarshal([]byte(*rawv), &v); err != nil { |
643 | + return err |
644 | + } |
645 | + c.Items[key] = v |
646 | + } |
647 | + return nil |
648 | +} |
649 | + |
650 | +func (c *ItemCollection) construct(itemType reflect.Type) error { |
651 | + for key, rawv := range c.rawItems { |
652 | + itemValuePtr := reflect.New(itemType) |
653 | + err := json.Unmarshal([]byte(*rawv), itemValuePtr.Interface()) |
654 | + if err != nil { |
655 | + return err |
656 | + } |
657 | + c.Items[key] = itemValuePtr.Interface() |
658 | + } |
659 | + return nil |
660 | +} |
661 | |
662 | === added file 'environs/simplestreams/json_test.go' |
663 | --- environs/simplestreams/json_test.go 1970-01-01 00:00:00 +0000 |
664 | +++ environs/simplestreams/json_test.go 2013-09-02 06:06:26 +0000 |
665 | @@ -0,0 +1,34 @@ |
666 | +package simplestreams_test |
667 | + |
668 | +import ( |
669 | + "encoding/json" |
670 | + |
671 | + gc "launchpad.net/gocheck" |
672 | + |
673 | + "launchpad.net/juju-core/environs/simplestreams" |
674 | +) |
675 | + |
676 | +type jsonSuite struct{} |
677 | + |
678 | +func (s *jsonSuite) TestItemCollectionMarshalling(c *gc.C) { |
679 | + // Ensure that unmarshalling a simplestreams.ItemCollection |
680 | + // directly (not through ParseCloudMetadata) doesn't |
681 | + // cause any surprises. |
682 | + var m simplestreams.ItemCollection |
683 | + m.Items = make(map[string]interface{}) |
684 | + err := json.Unmarshal([]byte(`{ |
685 | + "items": { |
686 | + "a": "b", |
687 | + "c": 123 |
688 | + } |
689 | + }`), &m) |
690 | + c.Assert(err, gc.IsNil) |
691 | + c.Assert(m.Items, gc.DeepEquals, map[string]interface{}{ |
692 | + "a": "b", |
693 | + "c": float64(123), |
694 | + }) |
695 | + // Ensure marshalling works as expected, too. |
696 | + b, err := json.Marshal(&m) |
697 | + c.Assert(err, gc.IsNil) |
698 | + c.Assert(string(b), gc.Equals, `{"items":{"a":"b","c":123}}`) |
699 | +} |
700 | |
701 | === modified file 'environs/simplestreams/simplestreams.go' |
702 | --- environs/simplestreams/simplestreams.go 2013-08-26 12:23:09 +0000 |
703 | +++ environs/simplestreams/simplestreams.go 2013-09-02 06:06:26 +0000 |
704 | @@ -134,32 +134,33 @@ |
705 | type attributeValues map[string]string |
706 | type aliasesByAttribute map[string]attributeValues |
707 | |
708 | -// Exported for testing |
709 | type CloudMetadata struct { |
710 | Products map[string]MetadataCatalog `json:"products"` |
711 | - Aliases map[string]aliasesByAttribute `json:"_aliases"` |
712 | + Aliases map[string]aliasesByAttribute `json:"_aliases,omitempty"` |
713 | Updated string `json:"updated"` |
714 | Format string `json:"format"` |
715 | } |
716 | |
717 | -type itemsByVersion map[string]*ItemCollection |
718 | - |
719 | type MetadataCatalog struct { |
720 | - Series string `json:"release"` |
721 | - Version string `json:"version"` |
722 | - Arch string `json:"arch"` |
723 | - RegionName string `json:"region"` |
724 | - Endpoint string `json:"endpoint"` |
725 | - Items itemsByVersion `json:"versions"` |
726 | + Series string `json:"release,omitempty"` |
727 | + Version string `json:"version,omitempty"` |
728 | + Arch string `json:"arch,omitempty"` |
729 | + RegionName string `json:"region,omitempty"` |
730 | + Endpoint string `json:"endpoint,omitempty"` |
731 | + |
732 | + // Items is a mapping from version to an ItemCollection, |
733 | + // where the version is the date the items were produced, |
734 | + // in the format YYYYMMDD. |
735 | + Items map[string]*ItemCollection `json:"versions"` |
736 | } |
737 | |
738 | -// Exported for testing |
739 | type ItemCollection struct { |
740 | + rawItems map[string]*json.RawMessage |
741 | Items map[string]interface{} `json:"items"` |
742 | - Arch string `json:"arch"` |
743 | - Version string `json:"version"` |
744 | - RegionName string `json:"region"` |
745 | - Endpoint string `json:"endpoint"` |
746 | + Arch string `json:"arch,omitempty"` |
747 | + Version string `json:"version,omitempty"` |
748 | + RegionName string `json:"region,omitempty"` |
749 | + Endpoint string `json:"endpoint,omitempty"` |
750 | } |
751 | |
752 | // These structs define the model used for metadata indices. |
753 | @@ -181,8 +182,8 @@ |
754 | Updated string `json:"updated"` |
755 | Format string `json:"format"` |
756 | DataType string `json:"datatype"` |
757 | - CloudName string `json:"cloudname"` |
758 | - Clouds []CloudSpec `json:"clouds"` |
759 | + CloudName string `json:"cloudname,omitempty"` |
760 | + Clouds []CloudSpec `json:"clouds,omitempty"` |
761 | ProductsFilePath string `json:"path"` |
762 | ProductIds []string `json:"products"` |
763 | } |
764 | @@ -706,29 +707,14 @@ |
765 | func (metadata *CloudMetadata) construct(valueType reflect.Type) error { |
766 | for _, metadataCatalog := range metadata.Products { |
767 | for _, ItemCollection := range metadataCatalog.Items { |
768 | - for i, item := range ItemCollection.Items { |
769 | - val, err := structFromMap(valueType, item.(map[string]interface{})) |
770 | - if err != nil { |
771 | - return err |
772 | - } |
773 | - ItemCollection.Items[i] = val |
774 | + if err := ItemCollection.construct(valueType); err != nil { |
775 | + return err |
776 | } |
777 | } |
778 | } |
779 | return nil |
780 | } |
781 | |
782 | -// structFromMap marshalls a mapf of values into a metadata struct. |
783 | -func structFromMap(valueType reflect.Type, attr map[string]interface{}) (interface{}, error) { |
784 | - data, err := json.Marshal(attr) |
785 | - if err != nil { |
786 | - return nil, err |
787 | - } |
788 | - val := reflect.New(valueType).Interface() |
789 | - err = json.Unmarshal([]byte(data), &val) |
790 | - return val, err |
791 | -} |
792 | - |
793 | type structTags map[reflect.Type]map[string]int |
794 | |
795 | var tagsForType structTags = make(structTags) |
796 | |
797 | === modified file 'environs/simplestreams/simplestreams_test.go' |
798 | --- environs/simplestreams/simplestreams_test.go 2013-08-16 05:39:37 +0000 |
799 | +++ environs/simplestreams/simplestreams_test.go 2013-09-02 06:06:26 +0000 |
800 | @@ -17,6 +17,7 @@ |
801 | func Test(t *testing.T) { |
802 | registerSimpleStreamsTests() |
803 | gc.Suite(&signingSuite{}) |
804 | + gc.Suite(&jsonSuite{}) |
805 | gc.TestingT(t) |
806 | } |
807 | |
808 | |
809 | === modified file 'environs/sync/sync.go' |
810 | --- environs/sync/sync.go 2013-08-28 00:25:31 +0000 |
811 | +++ environs/sync/sync.go 2013-09-02 06:06:26 +0000 |
812 | @@ -7,19 +7,15 @@ |
813 | "bytes" |
814 | "fmt" |
815 | "io" |
816 | - "os" |
817 | - "path" |
818 | - "path/filepath" |
819 | - "sort" |
820 | "time" |
821 | |
822 | "launchpad.net/loggo" |
823 | |
824 | "launchpad.net/juju-core/environs" |
825 | + "launchpad.net/juju-core/environs/filestorage" |
826 | envtools "launchpad.net/juju-core/environs/tools" |
827 | "launchpad.net/juju-core/provider/ec2" |
828 | coretools "launchpad.net/juju-core/tools" |
829 | - "launchpad.net/juju-core/utils" |
830 | "launchpad.net/juju-core/version" |
831 | ) |
832 | |
833 | @@ -123,7 +119,7 @@ |
834 | if ctx.Source == "" { |
835 | return ec2.NewHTTPStorageReader(DefaultToolsLocation), nil |
836 | } |
837 | - return newFileStorageReader(ctx.Source) |
838 | + return filestorage.NewFileStorageReader(ctx.Source) |
839 | } |
840 | |
841 | // copyTools copies a set of tools from the source to the target. |
842 | @@ -165,73 +161,6 @@ |
843 | return nil |
844 | } |
845 | |
846 | -// fileStorageReader implements StorageReader backed |
847 | -// by the local filesystem. |
848 | -type fileStorageReader struct { |
849 | - path string |
850 | -} |
851 | - |
852 | -// newFileStorageReader returns a new storage reader for |
853 | -// a directory inside the local file system. |
854 | -func newFileStorageReader(path string) (environs.StorageReader, error) { |
855 | - p := filepath.Clean(path) |
856 | - fi, err := os.Stat(p) |
857 | - if err != nil { |
858 | - return nil, err |
859 | - } |
860 | - if !fi.Mode().IsDir() { |
861 | - return nil, fmt.Errorf("specified source path is not a directory: %s", path) |
862 | - } |
863 | - return &fileStorageReader{p}, nil |
864 | -} |
865 | - |
866 | -// Get implements environs.StorageReader.Get. |
867 | -func (f *fileStorageReader) Get(name string) (io.ReadCloser, error) { |
868 | - filename, err := f.URL(name) |
869 | - if err != nil { |
870 | - return nil, err |
871 | - } |
872 | - file, err := os.Open(filename) |
873 | - if err != nil { |
874 | - return nil, err |
875 | - } |
876 | - return file, nil |
877 | -} |
878 | - |
879 | -// List implements environs.StorageReader.List. |
880 | -func (f *fileStorageReader) List(prefix string) ([]string, error) { |
881 | - // Add one for the missing path separator. |
882 | - pathlen := len(f.path) + 1 |
883 | - pattern := filepath.Join(f.path, prefix+"*") |
884 | - matches, err := filepath.Glob(pattern) |
885 | - if err != nil { |
886 | - return nil, err |
887 | - } |
888 | - list := []string{} |
889 | - for _, match := range matches { |
890 | - fi, err := os.Stat(match) |
891 | - if err != nil { |
892 | - return nil, err |
893 | - } |
894 | - if !fi.Mode().IsDir() { |
895 | - filename := match[pathlen:] |
896 | - list = append(list, filename) |
897 | - } |
898 | - } |
899 | - sort.Strings(list) |
900 | - return list, nil |
901 | -} |
902 | - |
903 | -// URL implements environs.StorageReader.URL. |
904 | -func (f *fileStorageReader) URL(name string) (string, error) { |
905 | - return path.Join(f.path, name), nil |
906 | -} |
907 | - |
908 | -// ConsistencyStrategy implements environs.StorageReader.ConsistencyStrategy. |
909 | -func (f *fileStorageReader) ConsistencyStrategy() utils.AttemptStrategy { |
910 | - return utils.AttemptStrategy{} |
911 | -} |
912 | - |
913 | // NewSyncLogWriter creates a loggo writer for registration |
914 | // by the callers of Sync. This way the logged output can also |
915 | // be displayed otherwise, e.g. on the screen. |
916 | |
917 | === modified file 'environs/tools/boilerplate.go' |
918 | --- environs/tools/boilerplate.go 2013-08-21 02:01:09 +0000 |
919 | +++ environs/tools/boilerplate.go 2013-09-02 06:06:26 +0000 |
920 | @@ -74,7 +74,7 @@ |
921 | |
922 | type toolsMetadataParams struct { |
923 | ToolsBinaryPath string |
924 | - ToolsBinarySize float64 |
925 | + ToolsBinarySize int64 |
926 | ToolsBinarySHA256 string |
927 | Region string |
928 | URL string |
929 | |
930 | === added file 'environs/tools/marshal.go' |
931 | --- environs/tools/marshal.go 1970-01-01 00:00:00 +0000 |
932 | +++ environs/tools/marshal.go 2013-09-02 06:06:26 +0000 |
933 | @@ -0,0 +1,86 @@ |
934 | +// Copyright 2013 Canonical Ltd. |
935 | +// Licensed under the AGPLv3, see LICENCE file for details. |
936 | + |
937 | +// The tools package supports locating, parsing, and filtering Ubuntu tools metadata in simplestreams format. |
938 | +// See http://launchpad.net/simplestreams and in particular the doc/README file in that project for more information |
939 | +// about the file formats. |
940 | +package tools |
941 | + |
942 | +import ( |
943 | + "encoding/json" |
944 | + "fmt" |
945 | + "time" |
946 | + |
947 | + "launchpad.net/juju-core/environs/simplestreams" |
948 | + "launchpad.net/juju-core/utils/set" |
949 | +) |
950 | + |
951 | +const ( |
952 | + ProductMetadataPath = "streams/v1/com.ubuntu.juju:released:tools.json" |
953 | +) |
954 | + |
955 | +// MarshalToolsMetadataJSON marshals tools metadata to index and products JSON. |
956 | +// |
957 | +// updated is the time at which the JSON file was updated. |
958 | +func MarshalToolsMetadataJSON(metadata []*ToolsMetadata, updated time.Time) (index, products []byte, err error) { |
959 | + if index, err = MarshalToolsMetadataIndexJSON(metadata, updated); err != nil { |
960 | + return nil, nil, err |
961 | + } |
962 | + if products, err = MarshalToolsMetadataProductsJSON(metadata, updated); err != nil { |
963 | + return nil, nil, err |
964 | + } |
965 | + return index, products, err |
966 | +} |
967 | + |
968 | +// MarshalToolsMetadataIndexJSON marshals tools metadata to index JSON. |
969 | +// |
970 | +// updated is the time at which the JSON file was updated. |
971 | +func MarshalToolsMetadataIndexJSON(metadata []*ToolsMetadata, updated time.Time) (out []byte, err error) { |
972 | + productIds := make([]string, len(metadata)) |
973 | + for i, t := range metadata { |
974 | + productIds[i] = t.productId() |
975 | + } |
976 | + var indices simplestreams.Indices |
977 | + indices.Updated = updated.Format(time.RFC1123Z) |
978 | + indices.Format = "index:1.0" |
979 | + indices.Indexes = map[string]*simplestreams.IndexMetadata{ |
980 | + "com.ubuntu.juju:released:tools": &simplestreams.IndexMetadata{ |
981 | + Updated: indices.Updated, |
982 | + Format: "products:1.0", |
983 | + DataType: "content-download", |
984 | + ProductsFilePath: ProductMetadataPath, |
985 | + ProductIds: set.NewStrings(productIds...).SortedValues(), |
986 | + }, |
987 | + } |
988 | + return json.MarshalIndent(&indices, "", " ") |
989 | +} |
990 | + |
991 | +// MarshalToolsMetadataProductsJSON marshals tools metadata to products JSON. |
992 | +// |
993 | +// updated is the time at which the JSON file was updated. |
994 | +func MarshalToolsMetadataProductsJSON(metadata []*ToolsMetadata, updated time.Time) (out []byte, err error) { |
995 | + var cloud simplestreams.CloudMetadata |
996 | + cloud.Updated = updated.Format(time.RFC1123Z) |
997 | + cloud.Format = "products:1.0" |
998 | + cloud.Products = make(map[string]simplestreams.MetadataCatalog) |
999 | + itemsversion := updated.Format("20060201") // YYYYMMDD |
1000 | + for _, t := range metadata { |
1001 | + id := t.productId() |
1002 | + itemid := fmt.Sprintf("%s-%s-%s", t.Version, t.Release, t.Arch) |
1003 | + if catalog, ok := cloud.Products[id]; ok { |
1004 | + catalog.Items[itemsversion].Items[itemid] = t |
1005 | + } else { |
1006 | + catalog = simplestreams.MetadataCatalog{ |
1007 | + Arch: t.Arch, |
1008 | + Version: t.Version, |
1009 | + Items: map[string]*simplestreams.ItemCollection{ |
1010 | + itemsversion: &simplestreams.ItemCollection{ |
1011 | + Items: map[string]interface{}{itemid: t}, |
1012 | + }, |
1013 | + }, |
1014 | + } |
1015 | + cloud.Products[id] = catalog |
1016 | + } |
1017 | + } |
1018 | + return json.MarshalIndent(&cloud, "", " ") |
1019 | +} |
1020 | |
1021 | === added file 'environs/tools/marshal_test.go' |
1022 | --- environs/tools/marshal_test.go 1970-01-01 00:00:00 +0000 |
1023 | +++ environs/tools/marshal_test.go 2013-09-02 06:06:26 +0000 |
1024 | @@ -0,0 +1,150 @@ |
1025 | +// Copyright 2013 Canonical Ltd. |
1026 | +// Licensed under the AGPLv3, see LICENCE file for details. |
1027 | + |
1028 | +package tools_test |
1029 | + |
1030 | +import ( |
1031 | + "time" |
1032 | + |
1033 | + gc "launchpad.net/gocheck" |
1034 | + |
1035 | + "launchpad.net/juju-core/environs/tools" |
1036 | + jc "launchpad.net/juju-core/testing/checkers" |
1037 | +) |
1038 | + |
1039 | +var _ = gc.Suite(&marshalSuite{}) |
1040 | + |
1041 | +type marshalSuite struct{} |
1042 | + |
1043 | +func (s *marshalSuite) TestLargeNumber(c *gc.C) { |
1044 | + metadata := []*tools.ToolsMetadata{ |
1045 | + &tools.ToolsMetadata{ |
1046 | + Release: "saucy", |
1047 | + Version: "1.2.3.4", |
1048 | + Arch: "arm", |
1049 | + Size: 9223372036854775807, |
1050 | + Path: "/somewhere/over/the/rainbow.tar.gz", |
1051 | + FileType: "tar.gz", |
1052 | + }, |
1053 | + } |
1054 | + _, products, err := tools.MarshalToolsMetadataJSON(metadata, time.Now()) |
1055 | + c.Assert(err, gc.IsNil) |
1056 | + c.Assert(string(products), jc.Contains, `"size": 9223372036854775807`) |
1057 | +} |
1058 | + |
1059 | +var expectedIndex = `{ |
1060 | + "index": { |
1061 | + "com.ubuntu.juju:released:tools": { |
1062 | + "updated": "Thu, 01 Jan 1970 00:00:00 +0000", |
1063 | + "format": "products:1.0", |
1064 | + "datatype": "content-download", |
1065 | + "path": "streams/v1/com.ubuntu.juju:released:tools.json", |
1066 | + "products": [ |
1067 | + "com.ubuntu.juju:1.2.3.4:arm", |
1068 | + "com.ubuntu.juju:4.3.2.1:amd64" |
1069 | + ] |
1070 | + } |
1071 | + }, |
1072 | + "updated": "Thu, 01 Jan 1970 00:00:00 +0000", |
1073 | + "format": "index:1.0" |
1074 | +}` |
1075 | + |
1076 | +var expectedProducts = `{ |
1077 | + "products": { |
1078 | + "com.ubuntu.juju:1.2.3.4:arm": { |
1079 | + "version": "1.2.3.4", |
1080 | + "arch": "arm", |
1081 | + "versions": { |
1082 | + "19700101": { |
1083 | + "items": { |
1084 | + "1.2.3.4-precise-arm": { |
1085 | + "release": "precise", |
1086 | + "version": "1.2.3.4", |
1087 | + "arch": "arm", |
1088 | + "size": 42, |
1089 | + "path": "toenlightenment.tar.gz", |
1090 | + "ftype": "tar.gz", |
1091 | + "sha256": "" |
1092 | + }, |
1093 | + "1.2.3.4-saucy-arm": { |
1094 | + "release": "saucy", |
1095 | + "version": "1.2.3.4", |
1096 | + "arch": "arm", |
1097 | + "size": 9223372036854775807, |
1098 | + "path": "/somewhere/over/the/rainbow.tar.gz", |
1099 | + "ftype": "tar.gz", |
1100 | + "sha256": "" |
1101 | + } |
1102 | + } |
1103 | + } |
1104 | + } |
1105 | + }, |
1106 | + "com.ubuntu.juju:4.3.2.1:amd64": { |
1107 | + "version": "4.3.2.1", |
1108 | + "arch": "amd64", |
1109 | + "versions": { |
1110 | + "19700101": { |
1111 | + "items": { |
1112 | + "4.3.2.1-precise-amd64": { |
1113 | + "release": "precise", |
1114 | + "version": "4.3.2.1", |
1115 | + "arch": "amd64", |
1116 | + "size": 0, |
1117 | + "path": "whatever.tar.gz", |
1118 | + "ftype": "tar.gz", |
1119 | + "sha256": "afb14e65c794464e378def12cbad6a96f9186d69" |
1120 | + } |
1121 | + } |
1122 | + } |
1123 | + } |
1124 | + } |
1125 | + }, |
1126 | + "updated": "Thu, 01 Jan 1970 00:00:00 +0000", |
1127 | + "format": "products:1.0" |
1128 | +}` |
1129 | + |
1130 | +var toolMetadataForTesting = []*tools.ToolsMetadata{ |
1131 | + &tools.ToolsMetadata{ |
1132 | + Release: "saucy", |
1133 | + Version: "1.2.3.4", |
1134 | + Arch: "arm", |
1135 | + Size: 9223372036854775807, |
1136 | + Path: "/somewhere/over/the/rainbow.tar.gz", |
1137 | + FileType: "tar.gz", |
1138 | + }, |
1139 | + &tools.ToolsMetadata{ |
1140 | + Release: "precise", |
1141 | + Version: "1.2.3.4", |
1142 | + Arch: "arm", |
1143 | + Size: 42, |
1144 | + Path: "toenlightenment.tar.gz", |
1145 | + FileType: "tar.gz", |
1146 | + }, |
1147 | + &tools.ToolsMetadata{ |
1148 | + Release: "precise", |
1149 | + Version: "4.3.2.1", |
1150 | + Arch: "amd64", |
1151 | + Path: "whatever.tar.gz", |
1152 | + FileType: "tar.gz", |
1153 | + SHA256: "afb14e65c794464e378def12cbad6a96f9186d69", |
1154 | + }, |
1155 | +} |
1156 | + |
1157 | +func (s *marshalSuite) TestMarshalIndex(c *gc.C) { |
1158 | + index, err := tools.MarshalToolsMetadataIndexJSON(toolMetadataForTesting, time.Unix(0, 0).UTC()) |
1159 | + c.Assert(err, gc.IsNil) |
1160 | + c.Assert(string(index), gc.Equals, expectedIndex) |
1161 | +} |
1162 | + |
1163 | +func (s *marshalSuite) TestMarshalProducts(c *gc.C) { |
1164 | + products, err := tools.MarshalToolsMetadataProductsJSON(toolMetadataForTesting, time.Unix(0, 0).UTC()) |
1165 | + c.Assert(err, gc.IsNil) |
1166 | + c.Assert(string(products), gc.Equals, expectedProducts) |
1167 | +} |
1168 | + |
1169 | +func (s *marshalSuite) TestMarshal(c *gc.C) { |
1170 | + index, products, err := tools.MarshalToolsMetadataJSON(toolMetadataForTesting, time.Unix(0, 0).UTC()) |
1171 | + c.Assert(err, gc.IsNil) |
1172 | + c.Assert(string(index), gc.Equals, expectedIndex) |
1173 | + c.Assert(string(products), gc.Equals, expectedProducts) |
1174 | +} |
1175 | |
1176 | === modified file 'environs/tools/simplestreams.go' |
1177 | --- environs/tools/simplestreams.go 2013-08-21 02:01:09 +0000 |
1178 | +++ environs/tools/simplestreams.go 2013-09-02 06:06:26 +0000 |
1179 | @@ -8,6 +8,7 @@ |
1180 | |
1181 | import ( |
1182 | "fmt" |
1183 | + |
1184 | "launchpad.net/juju-core/environs/simplestreams" |
1185 | ) |
1186 | |
1187 | @@ -44,13 +45,17 @@ |
1188 | |
1189 | // ToolsMetadata holds information about a particular tools tarball. |
1190 | type ToolsMetadata struct { |
1191 | - Release string `json:"release"` |
1192 | - Version string `json:"version"` |
1193 | - Arch string `json:"arch"` |
1194 | - Size float64 `json:"size"` |
1195 | - Path string `json:"path"` |
1196 | - FileType string `json:"ftype"` |
1197 | - SHA256 string `json:"sha256"` |
1198 | + Release string `json:"release"` |
1199 | + Version string `json:"version"` |
1200 | + Arch string `json:"arch"` |
1201 | + Size int64 `json:"size"` |
1202 | + Path string `json:"path"` |
1203 | + FileType string `json:"ftype"` |
1204 | + SHA256 string `json:"sha256"` |
1205 | +} |
1206 | + |
1207 | +func (t *ToolsMetadata) productId() string { |
1208 | + return fmt.Sprintf("com.ubuntu.juju:%s:%s", t.Version, t.Arch) |
1209 | } |
1210 | |
1211 | // Fetch returns a list of tools for the specified cloud matching the constraint. |
1212 | |
1213 | === modified file 'environs/tools/simplestreams_test.go' |
1214 | --- environs/tools/simplestreams_test.go 2013-08-21 01:58:26 +0000 |
1215 | +++ environs/tools/simplestreams_test.go 2013-09-02 06:06:26 +0000 |
1216 | @@ -233,3 +233,44 @@ |
1217 | c.Assert(err, gc.IsNil) |
1218 | c.Assert(ids, gc.DeepEquals, []string{"com.ubuntu.juju:1.10.1:amd64"}) |
1219 | } |
1220 | + |
1221 | +func (s *productSpecSuite) TestLargeNumber(c *gc.C) { |
1222 | + json := `{ |
1223 | + "updated": "Fri, 30 Aug 2013 16:12:58 +0800", |
1224 | + "format": "products:1.0", |
1225 | + "products": { |
1226 | + "com.ubuntu.juju:1.10.0:amd64": { |
1227 | + "version": "1.10.0", |
1228 | + "arch": "amd64", |
1229 | + "versions": { |
1230 | + "20133008": { |
1231 | + "items": { |
1232 | + "1.10.0-precise-amd64": { |
1233 | + "release": "precise", |
1234 | + "version": "1.10.0", |
1235 | + "arch": "amd64", |
1236 | + "size": 9223372036854775807, |
1237 | + "path": "releases/juju-1.10.0-precise-amd64.tgz", |
1238 | + "ftype": "tar.gz", |
1239 | + "sha256": "" |
1240 | + } |
1241 | + } |
1242 | + } |
1243 | + } |
1244 | + } |
1245 | + } |
1246 | + }` |
1247 | + cloudMetadata, err := simplestreams.ParseCloudMetadata([]byte(json), "products:1.0", "", tools.ToolsMetadata{}) |
1248 | + c.Assert(err, gc.IsNil) |
1249 | + c.Assert(cloudMetadata.Products, gc.HasLen, 1) |
1250 | + product := cloudMetadata.Products["com.ubuntu.juju:1.10.0:amd64"] |
1251 | + c.Assert(product, gc.NotNil) |
1252 | + c.Assert(product.Items, gc.HasLen, 1) |
1253 | + version := product.Items["20133008"] |
1254 | + c.Assert(version, gc.NotNil) |
1255 | + c.Assert(version.Items, gc.HasLen, 1) |
1256 | + item := version.Items["1.10.0-precise-amd64"] |
1257 | + c.Assert(item, gc.NotNil) |
1258 | + c.Assert(item, gc.FitsTypeOf, &tools.ToolsMetadata{}) |
1259 | + c.Assert(item.(*tools.ToolsMetadata).Size, gc.Equals, int64(9223372036854775807)) |
1260 | +} |
Reviewers: mp+183070_ code.launchpad. net,
Message:
Please take a look.
Description:
juju-metadata: add generate-tools subcommand
This is a new subcommand for the juju-metadata plugin,
which can be used to generate simplestreams metadata
(index and products) for an existing set of tools.
The command will list the tools in the public bucket
or a local directory, and generate the index and
tools metadata in the same location.
https:/ /code.launchpad .net/~axwalk/ juju-core/ juju-metadata- generate- tools/+ merge/183070
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/13417043/
Affected files: juju-metadata/ metadata. go juju-metadata/ metadataplugin_ test.go juju-metadata/ toolsmetadata. go juju-metadata/ toolsmetadata_ test.go emptystorage. go emptystorage_ test.go localstorage/ storage. go localstorage/ storage_ test.go simplestreams/ simplestreams. go simplestreams/ simplestreams_ test.go tools/boilerpla te.go tools/simplestr eams.go
A [revision details]
M cmd/plugins/
M cmd/plugins/
A cmd/plugins/
A cmd/plugins/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/
M environs/