Merge lp:~cmars/juju-core/no-default-series into lp:~go-bot/juju-core/trunk
- no-default-series
- Merge into trunk
Status: | Merged |
---|---|
Merged at revision: | 2470 |
Proposed branch: | lp:~cmars/juju-core/no-default-series |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
973 lines (+343/-137) 15 files modified
charm/charm.go (+1/-1) charm/repo.go (+46/-9) charm/repo_test.go (+2/-2) charm/testing/mockstore.go (+15/-2) charm/url.go (+100/-47) charm/url_test.go (+88/-34) cmd/juju/bootstrap_test.go (+2/-2) environs/jujutest/livetests.go (+1/-1) juju/conn_test.go (+7/-5) state/apiserver/charms.go (+6/-4) state/apiserver/client/client_test.go (+14/-9) store/lpad.go (+4/-1) store/server.go (+21/-3) store/server_test.go (+14/-10) testing/charm.go (+22/-7) |
To merge this branch: | bzr merge lp:~cmars/juju-core/no-default-series |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+211389@code.launchpad.net |
Commit message
Description of the change
Resolve implicit series with charm store.
Charm URLs without a series are "unresolved". A charm repository can resolve
the series. Charm store service resolves the series by adding CanonicalURL to
the charm info if it is found & resolved. Local repository uses default
series as configured in the environment. Command-line tools interacting with
a charm repo are responsible for making requests with resolved charm URLs.
State server requires a resolved charm URL for such requests. Charm store
service initially resolves series to a hard-coded value -- to be replaced by
more advanced logic as we transition to new LTS. Updated unit tests & mocks
accordingly.
Casey Marshall (cmars) wrote : | # |
Casey Marshall (cmars) wrote : | # |
Please take a look.
Casey Marshall (cmars) wrote : | # |
Please take a look.
Casey Marshall (cmars) wrote : | # |
Please take a look.
Casey Marshall (cmars) wrote : | # |
Tim,
Please review my proposal for eliminating default series from the juju
client (or pass it along to someone who's up for it). In a nutshell:
- Allow charm.URL to have an implied "empty" series.
- charm.Repository can Resolve them.
- The charm store "resolves" series by using a hard-coded value (for
now).
- A local repository uses the environment config default series.
- It's a client command's responsibility to make sure a charm URL is
resolved before calling the state server with it.
Thanks,
Casey
Dave Cheney (dave-cheney) wrote : | # |
On 2014/03/17 22:55:29, cmars wrote:
> Tim,
> Please review my proposal for eliminating default series from the juju
client
> (or pass it along to someone who's up for it). In a nutshell:
> - Allow charm.URL to have an implied "empty" series.
> - charm.Repository can Resolve them.
> - The charm store "resolves" series by using a hard-coded value (for
now).
> - A local repository uses the environment config default series.
> - It's a client command's responsibility to make sure a charm URL is
resolved
> before calling the state server with it.
> Thanks,
> Casey
I think this needs to be broken out into two changes. The store changes
need to land well before the release that contains the client changes.
Dave Cheney (dave-cheney) wrote : | # |
Some minor comments.
A larger meta comment, a lot of this change is noise due to charm.URL
being treated as a pointer where it is actually a value.
If all the references to *charm.URL were replaced with charm.URL the
amount of defensive coding in this change, and elsewhere would be
reduced.
https:/
File charm/repo.go (right):
https:/
charm/repo.go:154: curl = &*curl
I don't understand what this line does. I think it does nothing. If if
does something then it need a comment to explain the magic.
https:/
charm/repo.go:429: result := &*curl
yuk, how about
result := *curl
if !result ... {
}
return &result, nil
https:/
File store/server.go (right):
https:/
store/server.go:80: curl.Series = "precise"
please make this a constant at the top of the file with a comment
explaining the effect of changing the constant.
Casey Marshall (cmars) wrote : | # |
Please take a look.
https:/
File charm/repo.go (right):
https:/
charm/repo.go:154: curl = &*curl
On 2014/03/18 01:01:10, dfc wrote:
> I don't understand what this line does. I think it does nothing. If if
does
> something then it need a comment to explain the magic.
It doesn't do what I think it did. :\
https:/
charm/repo.go:429: result := &*curl
On 2014/03/18 01:01:10, dfc wrote:
> yuk, how about
> result := *curl
> if !result ... {
> }
> return &result, nil
Done.
https:/
File store/server.go (right):
https:/
store/server.go:80: curl.Series = "precise"
On 2014/03/18 01:01:10, dfc wrote:
> please make this a constant at the top of the file with a comment
explaining the
> effect of changing the constant.
Done.
Casey Marshall (cmars) wrote : | # |
On 2014/03/18 01:01:10, dfc wrote:
> Some minor comments.
> A larger meta comment, a lot of this change is noise due to charm.URL
being
> treated as a pointer where it is actually a value.
> If all the references to *charm.URL were replaced with charm.URL the
amount of
> defensive coding in this change, and elsewhere would be reduced.
I agree that referencing charm.URL as a value type would be ideal. This
morning I explored what it would take. I really don't feel comfortable
landing such a refactoring in this specific proposal -- much more of the
codebase would be affected.
Casey Marshall (cmars) wrote : | # |
dfc wrote:
> I think this needs to be broken out into two changes. The store
changes need to
> land well before the release that contains the client changes.
I've removed all the client-side changes from this proposal. PTAL.
Thanks,
Casey
William Reade (fwereade) wrote : | # |
A few comments, ping me when you're online.
https:/
File charm/repo.go (right):
https:/
charm/repo.go:27: CanonicalURL string `json:"
can we make it canonical-url, please, for general consistency across the
codebase
https:/
File state/apiserver
https:/
state/apiserver
resolved")
I'm a little concerned that the url definition change requires that we
be defensive like this at all possible entry points into the system.
(and, to guard against misses in any one layer, to be defensive at all
subsequent layers)
OTOH, I imagine this is forward-looking in the sense that we *will* end
up with, eg, cs:nova-compute actually referencing a cross-series charm.
If that's the case we'll just hve to grow handling for unresolved urls
through the codebase as we need it, and can then drop the defensiveness.
Is that where we're going with this? If so, +1... but if you can think
of a way to help everybody know they shouldn't be letting unresolved
urls through, I'd love to hear it. As it is it's just a subtle required
feature of the API that anybody could easily miss.
Ha. We need a "how to write API stuff" document *anyway* I guess.
https:/
File store/server.go (right):
https:/
store/server.go:19: const DefaultSeries = "precise"
So, we'll need some care for the switchover here. Are you up to date
with what needs to be done when, and where, to change this over to
trusty and get it deployed? I'm not 100% sure myself, but we need to
figure this out pretty much now...
https:/
File store/server_
https:/
store/server_
not found"},
hmm, the code in the previous file seemed to imply that we do expect to
turn cs:wordpress into cs:precise/
missing somewhere...
https:/
File testing/charm.go (right):
https:/
testing/
should we have a fallback here for stores without DefaultSeries set?
Casey Marshall (cmars) wrote : | # |
Please take a look.
Casey Marshall (cmars) wrote : | # |
https:/
File charm/repo.go (right):
https:/
charm/repo.go:27: CanonicalURL string `json:"
On 2014/03/20 12:13:42, fwereade wrote:
> can we make it canonical-url, please, for general consistency across
the
> codebase
Done.
https:/
File state/apiserver
https:/
state/apiserver
resolved")
On 2014/03/20 12:13:42, fwereade wrote:
> I'm a little concerned that the url definition change requires that we
be
> defensive like this at all possible entry points into the system.
(and, to guard
> against misses in any one layer, to be defensive at all subsequent
layers)
> OTOH, I imagine this is forward-looking in the sense that we *will*
end up with,
> eg, cs:nova-compute actually referencing a cross-series charm. If
that's the
> case we'll just hve to grow handling for unresolved urls through the
codebase as
> we need it, and can then drop the defensiveness. Is that where we're
going with
> this? If so, +1... but if you can think of a way to help everybody
know they
> shouldn't be letting unresolved urls through, I'd love to hear it. As
it is it's
> just a subtle required feature of the API that anybody could easily
miss.
> Ha. We need a "how to write API stuff" document *anyway* I guess.
There should not be a cross-series charm. Charms will declare the series
they support in the charm metadata. To support multiple series, a charm
will have to have per-series branches and content; they'd effectively be
distinct charms from a deployment perspective.
When a client deploys or upgrades a charm, it is responsible for
resolving the URL with the charm repository before making the API call.
As far as I know, these are the only entry points for new charm URLs
from the user into the API server. If this is naive, let's iterate on
improving the defensiveness & I'm open to suggestion.
I'd like the state server to never have to resolve a charm URL series,
and reject all unresolved URLs.
https:/
File store/server.go (right):
https:/
store/server.go:19: const DefaultSeries = "precise"
On 2014/03/20 12:13:42, fwereade wrote:
> So, we'll need some care for the switchover here. Are you up to date
with what
> needs to be done when, and where, to change this over to trusty and
get it
> deployed? I'm not 100% sure myself, but we need to figure this out
pretty much
> now...
The first step is to give the server the capability to resolve a series
for the client, land and deploy this first.
We should probably discuss how much we can get in this first landing. A
hard-coded default is probably the most pessimistic thing that could
possibly work while allowing clients to be forward compatible.
https:/
Casey Marshall (cmars) wrote : | # |
Please take a look.
Casey Marshall (cmars) wrote : | # |
On 2014/03/20 20:01:10, cmars wrote:
> Please take a look.
William,
This latest change has the charm.URL improvements we discussed earlier,
PTAL.
I'll hold off on the charm metadata series and associated "charm store
series solver" for a follow-up branch which I'll work on next.
Thanks,
Casey
John A Meinel (jameinel) wrote : | # |
On Thu, Mar 20, 2014 at 6:51 PM, Casey Marshall
<email address hidden> wrote:
...
>
> When a client deploys or upgrades a charm, it is responsible for
> resolving the URL with the charm repository before making the API call.
> As far as I know, these are the only entry points for new charm URLs
> from the user into the API server. If this is naive, let's iterate on
> improving the defensiveness & I'm open to suggestion.
>
> I'd like the state server to never have to resolve a charm URL series,
> and reject all unresolved URLs.
I'd like to understand this a bit better. Given the API server already
supports "deploy cs:precise/foo" which it will then go to the charm
store and download, it seems perfectly reasonable that the client
could connect to *1* remote machine (the API Server) rather than also
needing to know who the Charm Store is and how it is configured, and
how it can resolve information there.
Why do you feel it is better to push this work into needing Clients to
be smart? (One argument, I guess, is that Juju GUI is already listing
the charm store in order to present anything to clients, so the only
other client today is juju-core.)
But given that the API Server already needs to know how to talk to the
charm store (to install cs:precise/foo) it doesn't seem that bad to
have it also resolve URLs.
John
=:->
Casey Marshall (cmars) wrote : | # |
On 03/23/2014 04:30 AM, John A Meinel wrote:
> On Thu, Mar 20, 2014 at 6:51 PM, Casey Marshall
> <email address hidden> wrote:
> ...
>>
>> When a client deploys or upgrades a charm, it is responsible for
>> resolving the URL with the charm repository before making the API call.
>> As far as I know, these are the only entry points for new charm URLs
>> from the user into the API server. If this is naive, let's iterate on
>> improving the defensiveness & I'm open to suggestion.
>>
>> I'd like the state server to never have to resolve a charm URL series,
>> and reject all unresolved URLs.
>
> I'd like to understand this a bit better. Given the API server already
> supports "deploy cs:precise/foo" which it will then go to the charm
> store and download, it seems perfectly reasonable that the client
> could connect to *1* remote machine (the API Server) rather than also
> needing to know who the Charm Store is and how it is configured, and
> how it can resolve information there.
> Why do you feel it is better to push this work into needing Clients to
> be smart? (One argument, I guess, is that Juju GUI is already listing
> the charm store in order to present anything to clients, so the only
> other client today is juju-core.)
> But given that the API Server already needs to know how to talk to the
> charm store (to install cs:precise/foo) it doesn't seem that bad to
> have it also resolve URLs.
>
My primary goal there is to keep the state server simple, backwards-
(and forwards-) compatible. I decided to keep the current requirements
of a fully-resolved charm URL in deploy, publish, and
upgradecharm-
this is actually enforced by the type-system -- there is a distinction
between charm.URL (a fully-resolved charm location with series) and
charm.Reference (a charm location without a series resolved).
You raise a good point that the (>= 1.18) juju client will not contact
the charm store directly. What would you think of adding an API method,
call it "ResolveCharm", which exposes the charm store series-resolving
functionality back to the state client? The state client would still be
responsible for resolving the charm, but it will not need to contact the
charm store directly to resolve it.
I don't think "ResolveCharm" would necessarily need to land in this
merge proposal. It could go into the follow-up proposal with the
client-side changes.
> John
> =:->
>
-Casey
John A Meinel (jameinel) wrote : | # |
It does mean 2 round trips for something that we are resolving in the
state server anyway, but it isn't a huge problem. Better than having
the client have to go to the Charm store directly. As you say, it
doesn't have to happen in this proposal, and this is probably still a
reasonable way forward.
On Mon, Mar 24, 2014 at 6:24 AM, Casey Marshall
<email address hidden> wrote:
> On 03/23/2014 04:30 AM, John A Meinel wrote:
>> On Thu, Mar 20, 2014 at 6:51 PM, Casey Marshall
>> <email address hidden> wrote:
>> ...
>>>
>>> When a client deploys or upgrades a charm, it is responsible for
>>> resolving the URL with the charm repository before making the API call.
>>> As far as I know, these are the only entry points for new charm URLs
>>> from the user into the API server. If this is naive, let's iterate on
>>> improving the defensiveness & I'm open to suggestion.
>>>
>>> I'd like the state server to never have to resolve a charm URL series,
>>> and reject all unresolved URLs.
>>
>> I'd like to understand this a bit better. Given the API server already
>> supports "deploy cs:precise/foo" which it will then go to the charm
>> store and download, it seems perfectly reasonable that the client
>> could connect to *1* remote machine (the API Server) rather than also
>> needing to know who the Charm Store is and how it is configured, and
>> how it can resolve information there.
>> Why do you feel it is better to push this work into needing Clients to
>> be smart? (One argument, I guess, is that Juju GUI is already listing
>> the charm store in order to present anything to clients, so the only
>> other client today is juju-core.)
>> But given that the API Server already needs to know how to talk to the
>> charm store (to install cs:precise/foo) it doesn't seem that bad to
>> have it also resolve URLs.
>>
>
> My primary goal there is to keep the state server simple, backwards-
> (and forwards-) compatible. I decided to keep the current requirements
> of a fully-resolved charm URL in deploy, publish, and
> upgradecharm-
> this is actually enforced by the type-system -- there is a distinction
> between charm.URL (a fully-resolved charm location with series) and
> charm.Reference (a charm location without a series resolved).
>
> You raise a good point that the (>= 1.18) juju client will not contact
> the charm store directly. What would you think of adding an API method,
> call it "ResolveCharm", which exposes the charm store series-resolving
> functionality back to the state client? The state client would still be
> responsible for resolving the charm, but it will not need to contact the
> charm store directly to resolve it.
>
> I don't think "ResolveCharm" would necessarily need to land in this
> merge proposal. It could go into the follow-up proposal with the
> client-side changes.
>
>> John
>> =:->
>>
>
> -Casey
>
>
> --
> https:/
> You are subscribed to branch lp:juju-core.
William Reade (fwereade) wrote : | # |
LGTM, I think these are all trivials
https:/
File charm/url_test.go (right):
https:/
charm/url_
charm.ErrUnreso
We'd usually check for direct equality with ErrUnresolvedUrl, and
otherwise use gc.ErrorMatches to check the string.
https:/
charm/url_
Explicit test that precise-1 is not allowed, but precise1 is?
https:/
File state/apiserver
https:/
state/apiserver
errors consistent one day.
If these errors are starting to look nicer, please drop the comment (and
the one below). If they're not, would you upgrade this to a tech-debt
bug and fill in the usual TODO template (name, date, bug#, \n, short
description)?
https:/
File store/server.go (right):
https:/
store/server.go:80: return &charm.
DefaultSeries}
I'm not seeing where this is called. Dead code?
Casey Marshall (cmars) wrote : | # |
I've restricted the series name to not include dashes, and resubmitted
the MP in launchpad with the prior branch prereq:
https:/
PTAL, thanks!
I'm a bit unsure whether/how lbox & rietveld will follow the LP
resubmit..
-Casey
https:/
File charm/url_test.go (right):
https:/
charm/url_
charm.ErrUnreso
On 2014/03/24 09:24:09, fwereade wrote:
> We'd usually check for direct equality with ErrUnresolvedUrl, and
otherwise use
> gc.ErrorMatches to check the string.
Done.
https:/
charm/url_
On 2014/03/24 09:24:09, fwereade wrote:
> Explicit test that precise-1 is not allowed, but precise1 is?
Done.
https:/
File state/apiserver
https:/
state/apiserver
errors consistent one day.
On 2014/03/24 09:24:09, fwereade wrote:
> If these errors are starting to look nicer, please drop the comment
(and the one
> below). If they're not, would you upgrade this to a tech-debt bug and
fill in
> the usual TODO template (name, date, bug#, \n, short description)?
Done.
https:/
File store/server.go (right):
https:/
store/server.go:80: return &charm.
DefaultSeries}
On 2014/03/24 09:24:09, fwereade wrote:
> I'm not seeing where this is called. Dead code?
Yes, it is now. I've removed it.
John A Meinel (jameinel) wrote : | # |
You have to "lbox propose" again, and then it will generate a new diff on
Reitveld, it doesn't notice plain 'bzr push' to Launchpad.
On Mon, Mar 24, 2014 at 7:57 PM, Casey Marshall <
<email address hidden>> wrote:
> I've restricted the series name to not include dashes, and resubmitted
> the MP in launchpad with the prior branch prereq:
> https:/
>
> PTAL, thanks!
>
> I'm a bit unsure whether/how lbox & rietveld will follow the LP
> resubmit..
>
> -Casey
>
>
> https:/
> File charm/url_test.go (right):
>
>
> https:/
> charm/url_
> charm.ErrUnreso
> On 2014/03/24 09:24:09, fwereade wrote:
> > We'd usually check for direct equality with ErrUnresolvedUrl, and
> otherwise use
> > gc.ErrorMatches to check the string.
>
> Done.
>
>
> https:/
> charm/url_
> On 2014/03/24 09:24:09, fwereade wrote:
> > Explicit test that precise-1 is not allowed, but precise1 is?
>
> Done.
>
>
> https:/
> File state/apiserver
>
>
> https:/
> state/apiserver
> errors consistent one day.
> On 2014/03/24 09:24:09, fwereade wrote:
> > If these errors are starting to look nicer, please drop the comment
> (and the one
> > below). If they're not, would you upgrade this to a tech-debt bug and
> fill in
> > the usual TODO template (name, date, bug#, \n, short description)?
>
> Done.
>
> https:/
> File store/server.go (right):
>
>
> https:/
> store/server.go:80: return &charm.
> DefaultSeries}
> On 2014/03/24 09:24:09, fwereade wrote:
> > I'm not seeing where this is called. Dead code?
>
> Yes, it is now. I've removed it.
>
> https:/
>
> --
> https:/
> You are subscribed to branch lp:juju-core.
>
Preview Diff
1 | === modified file 'charm/charm.go' |
2 | --- charm/charm.go 2013-11-07 08:51:14 +0000 |
3 | +++ charm/charm.go 2014-03-20 19:57:15 +0000 |
4 | @@ -40,7 +40,7 @@ |
5 | if localRepoPath == "" { |
6 | return nil, errors.New("path to local repository not specified") |
7 | } |
8 | - repo = &LocalRepository{localRepoPath} |
9 | + repo = &LocalRepository{Path: localRepoPath} |
10 | default: |
11 | return nil, fmt.Errorf("unknown schema for charm URL %q", curl) |
12 | } |
13 | |
14 | === modified file 'charm/repo.go' |
15 | --- charm/repo.go 2014-03-07 13:56:22 +0000 |
16 | +++ charm/repo.go 2014-03-20 19:57:15 +0000 |
17 | @@ -24,11 +24,12 @@ |
18 | |
19 | // InfoResponse is sent by the charm store in response to charm-info requests. |
20 | type InfoResponse struct { |
21 | - Revision int `json:"revision"` // Zero is valid. Can't omitempty. |
22 | - Sha256 string `json:"sha256,omitempty"` |
23 | - Digest string `json:"digest,omitempty"` |
24 | - Errors []string `json:"errors,omitempty"` |
25 | - Warnings []string `json:"warnings,omitempty"` |
26 | + CanonicalURL string `json:"canonical-url,omitempty"` |
27 | + Revision int `json:"revision"` // Zero is valid. Can't omitempty. |
28 | + Sha256 string `json:"sha256,omitempty"` |
29 | + Digest string `json:"digest,omitempty"` |
30 | + Errors []string `json:"errors,omitempty"` |
31 | + Warnings []string `json:"warnings,omitempty"` |
32 | } |
33 | |
34 | // EventResponse is sent by the charm store in response to charm-event requests. |
35 | @@ -53,6 +54,7 @@ |
36 | type Repository interface { |
37 | Get(curl *URL) (Charm, error) |
38 | Latest(curls ...*URL) ([]CharmRevision, error) |
39 | + Resolve(ref Reference) (*URL, error) |
40 | } |
41 | |
42 | // Latest returns the latest revision of the charm referenced by curl, regardless |
43 | @@ -137,8 +139,27 @@ |
44 | return http.DefaultClient.Do(req) |
45 | } |
46 | |
47 | +// Resolve canonicalizes charm URLs, resolving references and implied series. |
48 | +func (s *CharmStore) Resolve(ref Reference) (*URL, error) { |
49 | + infos, err := s.Info(ref) |
50 | + if err != nil { |
51 | + return nil, err |
52 | + } |
53 | + if len(infos) == 0 { |
54 | + return nil, fmt.Errorf("missing response when resolving charm URL: %q", ref) |
55 | + } |
56 | + if infos[0].CanonicalURL == "" { |
57 | + return nil, fmt.Errorf("cannot resolve charm URL: %q", ref) |
58 | + } |
59 | + curl, err := ParseURL(infos[0].CanonicalURL) |
60 | + if err != nil { |
61 | + return nil, err |
62 | + } |
63 | + return curl, nil |
64 | +} |
65 | + |
66 | // Info returns details for all the specified charms in the charm store. |
67 | -func (s *CharmStore) Info(curls ...*URL) ([]*InfoResponse, error) { |
68 | +func (s *CharmStore) Info(curls ...Location) ([]*InfoResponse, error) { |
69 | baseURL := s.BaseURL + "/charm-info?" |
70 | queryParams := make([]string, len(curls), len(curls)+1) |
71 | for i, curl := range curls { |
72 | @@ -218,7 +239,7 @@ |
73 | } |
74 | |
75 | // revisions returns the revisions of the charms referenced by curls. |
76 | -func (s *CharmStore) revisions(curls ...*URL) (revisions []CharmRevision, err error) { |
77 | +func (s *CharmStore) revisions(curls ...Location) (revisions []CharmRevision, err error) { |
78 | infos, err := s.Info(curls...) |
79 | if err != nil { |
80 | return nil, err |
81 | @@ -246,7 +267,7 @@ |
82 | // Latest returns the latest revision of the charms referenced by curls, regardless |
83 | // of the revision set on each curl. |
84 | func (s *CharmStore) Latest(curls ...*URL) ([]CharmRevision, error) { |
85 | - baseCurls := make([]*URL, len(curls)) |
86 | + baseCurls := make([]Location, len(curls)) |
87 | for i, curl := range curls { |
88 | baseCurls[i] = curl.WithRevision(-1) |
89 | } |
90 | @@ -387,11 +408,27 @@ |
91 | // /path/to/repository/precise/mongodb.charm |
92 | // /path/to/repository/precise/wordpress/ |
93 | type LocalRepository struct { |
94 | - Path string |
95 | + Path string |
96 | + defaultSeries string |
97 | } |
98 | |
99 | var _ Repository = (*LocalRepository)(nil) |
100 | |
101 | +// WithDefaultSeries returns a Repository with the default series set. |
102 | +func (r *LocalRepository) WithDefaultSeries(defaultSeries string) Repository { |
103 | + localRepo := *r |
104 | + localRepo.defaultSeries = defaultSeries |
105 | + return &localRepo |
106 | +} |
107 | + |
108 | +// Resolve canonicalizes charm URLs, resolving references and implied series. |
109 | +func (r *LocalRepository) Resolve(ref Reference) (*URL, error) { |
110 | + if r.defaultSeries == "" { |
111 | + return nil, fmt.Errorf("cannot resolve, repository has no default series: %q", ref) |
112 | + } |
113 | + return &URL{Reference: ref, Series: r.defaultSeries}, nil |
114 | +} |
115 | + |
116 | // Latest returns the latest revision of the charm referenced by curl, regardless |
117 | // of the revision set on curl itself. |
118 | func (r *LocalRepository) Latest(curls ...*URL) ([]CharmRevision, error) { |
119 | |
120 | === modified file 'charm/repo_test.go' |
121 | --- charm/repo_test.go 2014-03-07 13:56:22 +0000 |
122 | +++ charm/repo_test.go 2014-03-20 19:57:15 +0000 |
123 | @@ -173,7 +173,7 @@ |
124 | // The following tests cover the low-level CharmStore-specific API. |
125 | |
126 | func (s *StoreSuite) TestInfo(c *gc.C) { |
127 | - charmURLs := []*charm.URL{ |
128 | + charmURLs := []charm.Location{ |
129 | charm.MustParseURL("cs:series/good"), |
130 | charm.MustParseURL("cs:series/better"), |
131 | charm.MustParseURL("cs:series/best"), |
132 | @@ -387,7 +387,7 @@ |
133 | func (s *LocalRepoSuite) SetUpTest(c *gc.C) { |
134 | s.LoggingSuite.SetUpTest(c) |
135 | root := c.MkDir() |
136 | - s.repo = &charm.LocalRepository{root} |
137 | + s.repo = &charm.LocalRepository{Path: root} |
138 | s.seriesPath = filepath.Join(root, "quantal") |
139 | c.Assert(os.Mkdir(s.seriesPath, 0777), gc.IsNil) |
140 | } |
141 | |
142 | === modified file 'charm/testing/mockstore.go' |
143 | --- charm/testing/mockstore.go 2014-03-07 14:15:28 +0000 |
144 | +++ charm/testing/mockstore.go 2014-03-20 19:57:15 +0000 |
145 | @@ -6,6 +6,7 @@ |
146 | import ( |
147 | "bytes" |
148 | "encoding/json" |
149 | + "fmt" |
150 | "io" |
151 | "net" |
152 | "net/http" |
153 | @@ -35,13 +36,14 @@ |
154 | Metadata []string |
155 | InfoRequestCount int |
156 | InfoRequestCountNoStats int |
157 | + DefaultSeries string |
158 | |
159 | charms map[string]int |
160 | } |
161 | |
162 | // NewMockStore creates a mock charm store containing the specified charms. |
163 | func NewMockStore(c *gc.C, charms map[string]int) *MockStore { |
164 | - s := &MockStore{charms: charms} |
165 | + s := &MockStore{charms: charms, DefaultSeries: "precise"} |
166 | f, err := os.Open(testing.Charms.BundlePath(c.MkDir(), "dummy")) |
167 | c.Assert(err, gc.IsNil) |
168 | defer f.Close() |
169 | @@ -98,7 +100,17 @@ |
170 | for _, url := range r.Form["charms"] { |
171 | cr := &charm.InfoResponse{} |
172 | response[url] = cr |
173 | - charmURL := charm.MustParseURL(url) |
174 | + charmURL, err := charm.ParseURL(url) |
175 | + if err == charm.ErrUnresolvedUrl { |
176 | + ref, _, err := charm.ParseReference(url) |
177 | + if err != nil { |
178 | + panic(err) |
179 | + } |
180 | + if s.DefaultSeries == "" { |
181 | + panic(fmt.Errorf("mock store lacks a default series cannot resolve charm URL: %q", url)) |
182 | + } |
183 | + charmURL = &charm.URL{Reference: ref, Series: s.DefaultSeries} |
184 | + } |
185 | switch charmURL.Name { |
186 | case "borken": |
187 | cr.Errors = append(cr.Errors, "badness") |
188 | @@ -115,6 +127,7 @@ |
189 | cr.Revision = charmURL.Revision |
190 | } |
191 | cr.Sha256 = s.bundleSha256 |
192 | + cr.CanonicalURL = charmURL.String() |
193 | } else { |
194 | cr.Errors = append(cr.Errors, "entry not found") |
195 | } |
196 | |
197 | === modified file 'charm/url.go' |
198 | --- charm/url.go 2014-01-28 10:45:55 +0000 |
199 | +++ charm/url.go 2014-03-20 19:57:15 +0000 |
200 | @@ -13,23 +13,42 @@ |
201 | "labix.org/v2/mgo/bson" |
202 | ) |
203 | |
204 | -// A charm URL represents charm locations such as: |
205 | +// Location represents a charm location, which must declare a path component |
206 | +// and a string representaion. |
207 | +type Location interface { |
208 | + Path() string |
209 | + String() string |
210 | +} |
211 | + |
212 | +// Reference represents a charm location with an unresolved, untargeted series, |
213 | +// such as: |
214 | +// |
215 | +// cs:~joe/wordpress |
216 | +// cs:wordpress-42 |
217 | +type Reference struct { |
218 | + Schema string // "cs" or "local" |
219 | + User string // "joe" |
220 | + Name string // "wordpress" |
221 | + Revision int // -1 if unset, N otherwise |
222 | +} |
223 | + |
224 | +// URL represents a fully resolved charm location with a specific series, such |
225 | +// as: |
226 | // |
227 | // cs:~joe/oneiric/wordpress |
228 | // cs:oneiric/wordpress-42 |
229 | // local:oneiric/wordpress |
230 | // |
231 | type URL struct { |
232 | - Schema string // "cs" or "local" |
233 | - User string // "joe" |
234 | - Series string // "oneiric" |
235 | - Name string // "wordpress" |
236 | - Revision int // -1 if unset, N otherwise |
237 | + Reference |
238 | + Series string // "oneiric" |
239 | } |
240 | |
241 | +var ErrUnresolvedUrl error = fmt.Errorf("charm url series is not resolved") |
242 | + |
243 | var ( |
244 | validUser = regexp.MustCompile("^[a-z0-9][a-zA-Z0-9+.-]+$") |
245 | - validSeries = regexp.MustCompile("^[a-z]+([a-z-]+[a-z])?$") |
246 | + validSeries = regexp.MustCompile("^[a-z]+([a-z0-9-]+[a-z0-9])?$") |
247 | validName = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$") |
248 | ) |
249 | |
250 | @@ -68,67 +87,83 @@ |
251 | // ParseURL parses the provided charm URL string into its respective |
252 | // structure. |
253 | func ParseURL(url string) (*URL, error) { |
254 | - u := &URL{} |
255 | + r, series, err := ParseReference(url) |
256 | + if err != nil { |
257 | + return nil, err |
258 | + } |
259 | + if series == "" { |
260 | + return nil, ErrUnresolvedUrl |
261 | + } |
262 | + return &URL{Reference: r, Series: series}, nil |
263 | +} |
264 | + |
265 | +// ParseReference parses the provided charm Reference string into its |
266 | +// respective structure and the targeted series, if present. |
267 | +func ParseReference(url string) (Reference, string, error) { |
268 | + r := Reference{Schema: "cs"} |
269 | + series := "" |
270 | i := strings.Index(url, ":") |
271 | - if i > 0 { |
272 | - u.Schema = url[:i] |
273 | + if i >= 0 { |
274 | + r.Schema = url[:i] |
275 | i++ |
276 | + } else { |
277 | + i = 0 |
278 | } |
279 | // cs: or local: |
280 | - if u.Schema != "cs" && u.Schema != "local" { |
281 | - return nil, fmt.Errorf("charm URL has invalid schema: %q", url) |
282 | + if r.Schema != "cs" && r.Schema != "local" { |
283 | + return Reference{}, "", fmt.Errorf("charm URL has invalid schema: %q", url) |
284 | } |
285 | parts := strings.Split(url[i:], "/") |
286 | if len(parts) < 1 || len(parts) > 3 { |
287 | - return nil, fmt.Errorf("charm URL has invalid form: %q", url) |
288 | + return Reference{}, "", fmt.Errorf("charm URL has invalid form: %q", url) |
289 | } |
290 | |
291 | // ~<username> |
292 | if strings.HasPrefix(parts[0], "~") { |
293 | - if u.Schema == "local" { |
294 | - return nil, fmt.Errorf("local charm URL with user name: %q", url) |
295 | + if r.Schema == "local" { |
296 | + return Reference{}, "", fmt.Errorf("local charm URL with user name: %q", url) |
297 | } |
298 | - u.User = parts[0][1:] |
299 | - if !IsValidUser(u.User) { |
300 | - return nil, fmt.Errorf("charm URL has invalid user name: %q", url) |
301 | + r.User = parts[0][1:] |
302 | + if !IsValidUser(r.User) { |
303 | + return Reference{}, "", fmt.Errorf("charm URL has invalid user name: %q", url) |
304 | } |
305 | parts = parts[1:] |
306 | } |
307 | |
308 | // <series> |
309 | - if len(parts) < 2 { |
310 | - return nil, fmt.Errorf("charm URL without series: %q", url) |
311 | - } |
312 | if len(parts) == 2 { |
313 | - u.Series = parts[0] |
314 | - if !IsValidSeries(u.Series) { |
315 | - return nil, fmt.Errorf("charm URL has invalid series: %q", url) |
316 | + series = parts[0] |
317 | + if !IsValidSeries(series) { |
318 | + return Reference{}, "", fmt.Errorf("charm URL has invalid series: %q", url) |
319 | } |
320 | parts = parts[1:] |
321 | } |
322 | + if len(parts) < 1 { |
323 | + return Reference{}, "", fmt.Errorf("charm URL without charm name: %q", url) |
324 | + } |
325 | |
326 | // <name>[-<revision>] |
327 | - u.Name = parts[0] |
328 | - u.Revision = -1 |
329 | - for i := len(u.Name) - 1; i > 0; i-- { |
330 | - c := u.Name[i] |
331 | + r.Name = parts[0] |
332 | + r.Revision = -1 |
333 | + for i := len(r.Name) - 1; i > 0; i-- { |
334 | + c := r.Name[i] |
335 | if c >= '0' && c <= '9' { |
336 | continue |
337 | } |
338 | - if c == '-' && i != len(u.Name)-1 { |
339 | + if c == '-' && i != len(r.Name)-1 { |
340 | var err error |
341 | - u.Revision, err = strconv.Atoi(u.Name[i+1:]) |
342 | + r.Revision, err = strconv.Atoi(r.Name[i+1:]) |
343 | if err != nil { |
344 | panic(err) // We just checked it was right. |
345 | } |
346 | - u.Name = u.Name[:i] |
347 | + r.Name = r.Name[:i] |
348 | } |
349 | break |
350 | } |
351 | - if !IsValidName(u.Name) { |
352 | - return nil, fmt.Errorf("charm URL has invalid charm name: %q", url) |
353 | + if !IsValidName(r.Name) { |
354 | + return Reference{}, "", fmt.Errorf("charm URL has invalid charm name: %q", url) |
355 | } |
356 | - return u, nil |
357 | + return r, series, nil |
358 | } |
359 | |
360 | // InferURL returns a charm URL inferred from src. The provided |
361 | @@ -148,9 +183,12 @@ |
362 | // when src does not include that information; similarly, a missing |
363 | // schema is assumed to be 'cs'. |
364 | func InferURL(src, defaultSeries string) (*URL, error) { |
365 | - if u, err := ParseURL(src); err == nil { |
366 | - // src was a valid charm URL already |
367 | - return u, nil |
368 | + r, series, err := ParseReference(src) |
369 | + if err != nil { |
370 | + return nil, err |
371 | + } |
372 | + if series != "" { |
373 | + return &URL{Reference: r, Series: series}, nil |
374 | } |
375 | if strings.HasPrefix(src, "~") { |
376 | return nil, fmt.Errorf("cannot infer charm URL with user but no schema: %q", src) |
377 | @@ -187,22 +225,37 @@ |
378 | } |
379 | |
380 | func (u *URL) Path() string { |
381 | - if u.User != "" { |
382 | - if u.Revision >= 0 { |
383 | - return fmt.Sprintf("~%s/%s/%s-%d", u.User, u.Series, u.Name, u.Revision) |
384 | - } |
385 | - return fmt.Sprintf("~%s/%s/%s", u.User, u.Series, u.Name) |
386 | - } |
387 | - if u.Revision >= 0 { |
388 | - return fmt.Sprintf("%s/%s-%d", u.Series, u.Name, u.Revision) |
389 | - } |
390 | - return fmt.Sprintf("%s/%s", u.Series, u.Name) |
391 | + return u.path(u.Series) |
392 | +} |
393 | + |
394 | +func (r Reference) path(series string) string { |
395 | + var parts []string |
396 | + if r.User != "" { |
397 | + parts = append(parts, fmt.Sprintf("~%s", r.User)) |
398 | + } |
399 | + if series != "" { |
400 | + parts = append(parts, series) |
401 | + } |
402 | + if r.Revision >= 0 { |
403 | + parts = append(parts, fmt.Sprintf("%s-%d", r.Name, r.Revision)) |
404 | + } else { |
405 | + parts = append(parts, r.Name) |
406 | + } |
407 | + return strings.Join(parts, "/") |
408 | +} |
409 | + |
410 | +func (r Reference) Path() string { |
411 | + return r.path("") |
412 | } |
413 | |
414 | func (u *URL) String() string { |
415 | return fmt.Sprintf("%s:%s", u.Schema, u.Path()) |
416 | } |
417 | |
418 | +func (r Reference) String() string { |
419 | + return fmt.Sprintf("%s:%s", r.Schema, r.Path()) |
420 | +} |
421 | + |
422 | // GetBSON turns u into a bson.Getter so it can be saved directly |
423 | // on a MongoDB database with mgo. |
424 | func (u *URL) GetBSON() (interface{}, error) { |
425 | |
426 | === modified file 'charm/url_test.go' |
427 | --- charm/url_test.go 2014-01-28 10:45:55 +0000 |
428 | +++ charm/url_test.go 2014-03-20 19:57:15 +0000 |
429 | @@ -6,6 +6,7 @@ |
430 | import ( |
431 | "encoding/json" |
432 | "fmt" |
433 | + "strings" |
434 | |
435 | "labix.org/v2/mgo/bson" |
436 | gc "launchpad.net/gocheck" |
437 | @@ -21,38 +22,63 @@ |
438 | s, err string |
439 | url *charm.URL |
440 | }{ |
441 | - {"cs:~user/series/name", "", &charm.URL{"cs", "user", "series", "name", -1}}, |
442 | - {"cs:~user/series/name-0", "", &charm.URL{"cs", "user", "series", "name", 0}}, |
443 | - {"cs:series/name", "", &charm.URL{"cs", "", "series", "name", -1}}, |
444 | - {"cs:series/name-42", "", &charm.URL{"cs", "", "series", "name", 42}}, |
445 | - {"local:series/name-1", "", &charm.URL{"local", "", "series", "name", 1}}, |
446 | - {"local:series/name", "", &charm.URL{"local", "", "series", "name", -1}}, |
447 | - {"local:series/n0-0n-n0", "", &charm.URL{"local", "", "series", "n0-0n-n0", -1}}, |
448 | + {"cs:~user/series/name", "", &charm.URL{charm.Reference{"cs", "user", "name", -1}, "series"}}, |
449 | + {"cs:~user/series/name-0", "", &charm.URL{charm.Reference{"cs", "user", "name", 0}, "series"}}, |
450 | + {"cs:series/name", "", &charm.URL{charm.Reference{"cs", "", "name", -1}, "series"}}, |
451 | + {"cs:series/name-42", "", &charm.URL{charm.Reference{"cs", "", "name", 42}, "series"}}, |
452 | + {"local:series/name-1", "", &charm.URL{charm.Reference{"local", "", "name", 1}, "series"}}, |
453 | + {"local:series/name", "", &charm.URL{charm.Reference{"local", "", "name", -1}, "series"}}, |
454 | + {"local:series/n0-0n-n0", "", &charm.URL{charm.Reference{"local", "", "n0-0n-n0", -1}, "series"}}, |
455 | + {"cs:~user/name", "", &charm.URL{charm.Reference{"cs", "user", "name", -1}, ""}}, |
456 | + {"cs:name", "", &charm.URL{charm.Reference{"cs", "", "name", -1}, ""}}, |
457 | + {"local:name", "", &charm.URL{charm.Reference{"local", "", "name", -1}, ""}}, |
458 | |
459 | {"bs:~user/series/name-1", "charm URL has invalid schema: .*", nil}, |
460 | {"cs:~1/series/name-1", "charm URL has invalid user name: .*", nil}, |
461 | + {"cs:~user", "charm URL without charm name: .*", nil}, |
462 | {"cs:~user/1/name-1", "charm URL has invalid series: .*", nil}, |
463 | {"cs:~user/series/name-1-2", "charm URL has invalid charm name: .*", nil}, |
464 | {"cs:~user/series/name-1-name-2", "charm URL has invalid charm name: .*", nil}, |
465 | {"cs:~user/series/name--name-2", "charm URL has invalid charm name: .*", nil}, |
466 | {"cs:~user/series/huh/name-1", "charm URL has invalid form: .*", nil}, |
467 | - {"cs:~user/name", "charm URL without series: .*", nil}, |
468 | - {"cs:name", "charm URL without series: .*", nil}, |
469 | + {"cs:/name", "charm URL has invalid series: .*", nil}, |
470 | {"local:~user/series/name", "local charm URL with user name: .*", nil}, |
471 | {"local:~user/name", "local charm URL with user name: .*", nil}, |
472 | - {"local:name", "charm URL without series: .*", nil}, |
473 | } |
474 | |
475 | func (s *URLSuite) TestParseURL(c *gc.C) { |
476 | for i, t := range urlTests { |
477 | c.Logf("test %d", i) |
478 | - url, err := charm.ParseURL(t.s) |
479 | + url, uerr := charm.ParseURL(t.s) |
480 | + ref, series, rerr := charm.ParseReference(t.s) |
481 | comment := gc.Commentf("ParseURL(%q)", t.s) |
482 | - if t.err != "" { |
483 | - c.Check(err.Error(), gc.Matches, t.err, comment) |
484 | + if t.url != nil && t.url.Series == "" { |
485 | + if t.err != "" { |
486 | + // Expected error should match |
487 | + c.Assert(rerr, gc.NotNil, comment) |
488 | + c.Check(rerr.Error(), gc.Matches, t.err, comment) |
489 | + } else { |
490 | + // Expected charm reference should match |
491 | + c.Check(ref, gc.DeepEquals, t.url.Reference, comment) |
492 | + c.Check(t.url.Reference.String(), gc.Equals, t.s) |
493 | + } |
494 | + if rerr != nil { |
495 | + // If ParseReference has an error, ParseURL should share it |
496 | + c.Check(uerr.Error(), gc.Equals, rerr.Error(), comment) |
497 | + } else { |
498 | + // Otherwise, ParseURL with an empty series should error unresolved. |
499 | + c.Check(uerr.Error(), gc.Equals, charm.ErrUnresolvedUrl.Error(), comment) |
500 | + } |
501 | } else { |
502 | - c.Check(url, gc.DeepEquals, t.url, comment) |
503 | - c.Check(t.url.String(), gc.Equals, t.s) |
504 | + if t.err != "" { |
505 | + c.Assert(uerr, gc.NotNil, comment) |
506 | + c.Check(uerr.Error(), gc.Matches, t.err, comment) |
507 | + c.Check(uerr.Error(), gc.Equals, rerr.Error(), comment) |
508 | + } else { |
509 | + c.Check(url.Series, gc.Equals, series, comment) |
510 | + c.Check(url, gc.DeepEquals, t.url, comment) |
511 | + c.Check(t.url.String(), gc.Equals, t.s) |
512 | + } |
513 | } |
514 | } |
515 | } |
516 | @@ -82,31 +108,35 @@ |
517 | comment := gc.Commentf("InferURL(%q, %q)", t.vague, "defseries") |
518 | inferred, ierr := charm.InferURL(t.vague, "defseries") |
519 | parsed, perr := charm.ParseURL(t.exact) |
520 | - if parsed != nil { |
521 | + if perr == nil { |
522 | c.Check(inferred, gc.DeepEquals, parsed, comment) |
523 | + c.Check(ierr, gc.IsNil) |
524 | } else { |
525 | expect := perr.Error() |
526 | if t.vague != t.exact { |
527 | - expect = fmt.Sprintf("%s (URL inferred from %q)", expect, t.vague) |
528 | + if colIdx := strings.Index(expect, ":"); colIdx > 0 { |
529 | + expect = expect[:colIdx] |
530 | + } |
531 | } |
532 | - c.Check(ierr.Error(), gc.Equals, expect, comment) |
533 | + c.Check(ierr.Error(), gc.Matches, expect+".*", comment) |
534 | } |
535 | } |
536 | u, err := charm.InferURL("~blah", "defseries") |
537 | c.Assert(u, gc.IsNil) |
538 | - c.Assert(err, gc.ErrorMatches, "cannot infer charm URL with user but no schema: .*") |
539 | + c.Assert(err, gc.ErrorMatches, "charm URL without charm name: .*") |
540 | } |
541 | |
542 | var inferNoDefaultSeriesTests = []struct { |
543 | vague, exact string |
544 | + resolved bool |
545 | }{ |
546 | - {"foo", ""}, |
547 | - {"foo-1", ""}, |
548 | - {"cs:foo", ""}, |
549 | - {"cs:~user/foo", ""}, |
550 | - {"series/foo", "cs:series/foo"}, |
551 | - {"cs:series/foo", "cs:series/foo"}, |
552 | - {"cs:~user/series/foo", "cs:~user/series/foo"}, |
553 | + {"foo", "", false}, |
554 | + {"foo-1", "", false}, |
555 | + {"cs:foo", "", false}, |
556 | + {"cs:~user/foo", "", false}, |
557 | + {"series/foo", "cs:series/foo", true}, |
558 | + {"cs:series/foo", "cs:series/foo", true}, |
559 | + {"cs:~user/series/foo", "cs:~user/series/foo", true}, |
560 | } |
561 | |
562 | func (s *URLSuite) TestInferURLNoDefaultSeries(c *gc.C) { |
563 | @@ -122,6 +152,23 @@ |
564 | } |
565 | } |
566 | |
567 | +func (s *URLSuite) TestParseUnresolved(c *gc.C) { |
568 | + for _, t := range inferNoDefaultSeriesTests { |
569 | + if t.resolved { |
570 | + url, err := charm.ParseURL(t.vague) |
571 | + c.Assert(err, gc.IsNil) |
572 | + c.Assert(url.Series, gc.Not(gc.Equals), "") |
573 | + } else { |
574 | + _, series, err := charm.ParseReference(t.vague) |
575 | + c.Assert(err, gc.IsNil) |
576 | + c.Assert(series, gc.Equals, "") |
577 | + _, err = charm.ParseURL(t.vague) |
578 | + c.Assert(err, gc.NotNil) |
579 | + c.Assert(err.Error(), gc.Equals, charm.ErrUnresolvedUrl.Error()) |
580 | + } |
581 | + } |
582 | +} |
583 | + |
584 | var validTests = []struct { |
585 | valid func(string) bool |
586 | string string |
587 | @@ -160,31 +207,38 @@ |
588 | {charm.IsValidSeries, "pre cise", false}, |
589 | {charm.IsValidSeries, "pre-cise", true}, |
590 | {charm.IsValidSeries, "pre^cise", false}, |
591 | - {charm.IsValidSeries, "prec1se", false}, |
592 | + {charm.IsValidSeries, "prec1se", true}, |
593 | {charm.IsValidSeries, "-precise", false}, |
594 | {charm.IsValidSeries, "precise-", false}, |
595 | - {charm.IsValidSeries, "pre-c1se", false}, |
596 | + {charm.IsValidSeries, "pre-c1se", true}, |
597 | } |
598 | |
599 | func (s *URLSuite) TestValidCheckers(c *gc.C) { |
600 | for i, t := range validTests { |
601 | c.Logf("test %d: %s", i, t.string) |
602 | - c.Assert(t.valid(t.string), gc.Equals, t.expect) |
603 | + c.Assert(t.valid(t.string), gc.Equals, t.expect, gc.Commentf("%s", t.string)) |
604 | } |
605 | } |
606 | |
607 | func (s *URLSuite) TestMustParseURL(c *gc.C) { |
608 | url := charm.MustParseURL("cs:series/name") |
609 | - c.Assert(url, gc.DeepEquals, &charm.URL{"cs", "", "series", "name", -1}) |
610 | - f := func() { charm.MustParseURL("local:name") } |
611 | - c.Assert(f, gc.PanicMatches, "charm URL without series: .*") |
612 | + c.Assert(url, gc.DeepEquals, |
613 | + &charm.URL{Reference: charm.Reference{"cs", "", "name", -1}, Series: "series"}) |
614 | + f := func() { charm.MustParseURL("local:@@/name") } |
615 | + c.Assert(f, gc.PanicMatches, "charm URL has invalid series: .*") |
616 | + f = func() { charm.MustParseURL("cs:~user") } |
617 | + c.Assert(f, gc.PanicMatches, "charm URL without charm name: .*") |
618 | + f = func() { charm.MustParseURL("cs:~user") } |
619 | + c.Assert(f, gc.PanicMatches, "charm URL without charm name: .*") |
620 | + f = func() { charm.MustParseURL("cs:name") } |
621 | + c.Assert(f, gc.PanicMatches, "charm url series is not resolved") |
622 | } |
623 | |
624 | func (s *URLSuite) TestWithRevision(c *gc.C) { |
625 | url := charm.MustParseURL("cs:series/name") |
626 | other := url.WithRevision(1) |
627 | - c.Assert(url, gc.DeepEquals, &charm.URL{"cs", "", "series", "name", -1}) |
628 | - c.Assert(other, gc.DeepEquals, &charm.URL{"cs", "", "series", "name", 1}) |
629 | + c.Assert(url, gc.DeepEquals, &charm.URL{charm.Reference{"cs", "", "name", -1}, "series"}) |
630 | + c.Assert(other, gc.DeepEquals, &charm.URL{charm.Reference{"cs", "", "name", 1}, "series"}) |
631 | |
632 | // Should always copy. The opposite behavior is error prone. |
633 | c.Assert(other.WithRevision(1), gc.Not(gc.Equals), other) |
634 | |
635 | === modified file 'cmd/juju/bootstrap_test.go' |
636 | --- cmd/juju/bootstrap_test.go 2014-03-19 04:06:58 +0000 |
637 | +++ cmd/juju/bootstrap_test.go 2014-03-20 19:57:15 +0000 |
638 | @@ -292,8 +292,8 @@ |
639 | err: `invalid value "bad=wrong" for flag --constraints: unknown constraint "bad"`, |
640 | }, { |
641 | info: "bad --series", |
642 | - args: []string{"--series", "bad1"}, |
643 | - err: `invalid value "bad1" for flag --series: invalid series name "bad1"`, |
644 | + args: []string{"--series", "1bad1"}, |
645 | + err: `invalid value "1bad1" for flag --series: invalid series name "1bad1"`, |
646 | }, { |
647 | info: "lonely --series", |
648 | args: []string{"--series", "fine"}, |
649 | |
650 | === modified file 'environs/jujutest/livetests.go' |
651 | --- environs/jujutest/livetests.go 2014-03-14 02:19:35 +0000 |
652 | +++ environs/jujutest/livetests.go 2014-03-20 19:57:15 +0000 |
653 | @@ -450,7 +450,7 @@ |
654 | c.Logf("deploying service") |
655 | repoDir := c.MkDir() |
656 | url := coretesting.Charms.ClonedURL(repoDir, mtools0.Version.Series, "dummy") |
657 | - sch, err := conn.PutCharm(url, &charm.LocalRepository{repoDir}, false) |
658 | + sch, err := conn.PutCharm(url, &charm.LocalRepository{Path: repoDir}, false) |
659 | c.Assert(err, gc.IsNil) |
660 | svc, err := conn.State.AddService("dummy", "user-admin", sch) |
661 | c.Assert(err, gc.IsNil) |
662 | |
663 | === modified file 'juju/conn_test.go' |
664 | --- juju/conn_test.go 2014-03-17 22:42:51 +0000 |
665 | +++ juju/conn_test.go 2014-03-20 19:57:15 +0000 |
666 | @@ -337,10 +337,12 @@ |
667 | // Invent a URL that points to the bundled charm, and |
668 | // test putting that. |
669 | curl := &charm.URL{ |
670 | - Schema: "local", |
671 | - Series: "quantal", |
672 | - Name: "riak", |
673 | - Revision: -1, |
674 | + Reference: charm.Reference{ |
675 | + Schema: "local", |
676 | + Name: "riak", |
677 | + Revision: -1, |
678 | + }, |
679 | + Series: "quantal", |
680 | } |
681 | _, err = s.conn.PutCharm(curl, s.repo, true) |
682 | c.Assert(err, gc.ErrorMatches, `cannot increment revision of charm "local:quantal/riak-7": not a directory`) |
683 | @@ -356,7 +358,7 @@ |
684 | } |
685 | |
686 | func (s *ConnSuite) TestPutCharmUpload(c *gc.C) { |
687 | - repo := &charm.LocalRepository{c.MkDir()} |
688 | + repo := &charm.LocalRepository{Path: c.MkDir()} |
689 | curl := coretesting.Charms.ClonedURL(repo.Path, "quantal", "riak") |
690 | |
691 | // Put charm for the first time. |
692 | |
693 | === modified file 'state/apiserver/charms.go' |
694 | --- state/apiserver/charms.go 2014-03-13 05:57:38 +0000 |
695 | +++ state/apiserver/charms.go 2014-03-20 19:57:15 +0000 |
696 | @@ -197,10 +197,12 @@ |
697 | } |
698 | // We got it, now let's reserve a charm URL for it in state. |
699 | archiveURL := &charm.URL{ |
700 | - Schema: "local", |
701 | - Series: series, |
702 | - Name: archive.Meta().Name, |
703 | - Revision: archive.Revision(), |
704 | + Reference: charm.Reference{ |
705 | + Schema: "local", |
706 | + Name: archive.Meta().Name, |
707 | + Revision: archive.Revision(), |
708 | + }, |
709 | + Series: series, |
710 | } |
711 | preparedURL, err := h.state.PrepareLocalCharmUpload(archiveURL) |
712 | if err != nil { |
713 | |
714 | === modified file 'state/apiserver/client/client_test.go' |
715 | --- state/apiserver/client/client_test.go 2014-03-19 03:18:41 +0000 |
716 | +++ state/apiserver/client/client_test.go 2014-03-20 19:57:15 +0000 |
717 | @@ -220,7 +220,12 @@ |
718 | { |
719 | about: "invalid URL", |
720 | url: "not-valid", |
721 | - err: `charm URL has invalid schema: "not-valid"`, |
722 | + err: "charm url series is not resolved", |
723 | + }, |
724 | + { |
725 | + about: "invalid schema", |
726 | + url: "not-valid:your-arguments", |
727 | + err: `charm URL has invalid schema: "not-valid:your-arguments"`, |
728 | }, |
729 | { |
730 | about: "unknown charm", |
731 | @@ -661,8 +666,8 @@ |
732 | defer restore() |
733 | for url, expect := range map[string]string{ |
734 | // TODO(fwereade) make these errors consistent one day. |
735 | - "wordpress": `charm URL has invalid schema: "wordpress"`, |
736 | - "cs:wordpress": `charm URL without series: "cs:wordpress"`, |
737 | + "wordpress": "charm url series is not resolved", |
738 | + "cs:wordpress": "charm url series is not resolved", |
739 | "cs:precise/wordpress": "charm url must include revision", |
740 | "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`, |
741 | } { |
742 | @@ -839,8 +844,8 @@ |
743 | s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress")) |
744 | for charmUrl, expect := range map[string]string{ |
745 | // TODO(fwereade,Makyo) make these errors consistent one day. |
746 | - "wordpress": `charm URL has invalid schema: "wordpress"`, |
747 | - "cs:wordpress": `charm URL without series: "cs:wordpress"`, |
748 | + "wordpress": "charm url series is not resolved", |
749 | + "cs:wordpress": "charm url series is not resolved", |
750 | "cs:precise/wordpress": "charm url must include revision", |
751 | "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`, |
752 | } { |
753 | @@ -1073,8 +1078,8 @@ |
754 | s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress")) |
755 | for url, expect := range map[string]string{ |
756 | // TODO(fwereade,Makyo) make these errors consistent one day. |
757 | - "wordpress": `charm URL has invalid schema: "wordpress"`, |
758 | - "cs:wordpress": `charm URL without series: "cs:wordpress"`, |
759 | + "wordpress": "charm url series is not resolved", |
760 | + "cs:wordpress": "charm url series is not resolved", |
761 | "cs:precise/wordpress": "charm url must include revision", |
762 | "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`, |
763 | } { |
764 | @@ -1839,8 +1844,8 @@ |
765 | |
766 | client := s.APIState.Client() |
767 | // First test the sanity checks. |
768 | - err := client.AddCharm(&charm.URL{Name: "nonsense"}) |
769 | - c.Assert(err, gc.ErrorMatches, `charm URL has invalid schema: ":/nonsense-0"`) |
770 | + err := client.AddCharm(&charm.URL{Reference: charm.Reference{Name: "nonsense"}}) |
771 | + c.Assert(err, gc.ErrorMatches, `charm URL has invalid schema: ":nonsense-0"`) |
772 | err = client.AddCharm(charm.MustParseURL("local:precise/dummy")) |
773 | c.Assert(err, gc.ErrorMatches, "only charm store charm URLs are supported, with cs: schema") |
774 | err = client.AddCharm(charm.MustParseURL("cs:precise/wordpress")) |
775 | |
776 | === modified file 'store/lpad.go' |
777 | --- store/lpad.go 2013-07-09 10:32:23 +0000 |
778 | +++ store/lpad.go 2014-03-20 19:57:15 +0000 |
779 | @@ -69,7 +69,10 @@ |
780 | urls := []*charm.URL{curl} |
781 | schema, name := curl.Schema, curl.Name |
782 | for _, series := range tip.OfficialSeries { |
783 | - curl = &charm.URL{Schema: schema, Name: name, Series: series, Revision: -1} |
784 | + curl = &charm.URL{ |
785 | + Reference: charm.Reference{Schema: schema, Name: name, Revision: -1}, |
786 | + Series: series, |
787 | + } |
788 | curl.Series = series |
789 | curl.User = "" |
790 | urls = append(urls, curl) |
791 | |
792 | === modified file 'store/server.go' |
793 | --- store/server.go 2013-11-12 14:39:29 +0000 |
794 | +++ store/server.go 2014-03-20 19:57:15 +0000 |
795 | @@ -16,6 +16,8 @@ |
796 | "launchpad.net/juju-core/log" |
797 | ) |
798 | |
799 | +const DefaultSeries = "precise" |
800 | + |
801 | // Server is an http.Handler that serves the HTTP API of juju |
802 | // so that juju clients can retrieve published charms. |
803 | type Server struct { |
804 | @@ -74,6 +76,21 @@ |
805 | return []string{kind, curl.Series, curl.Name, curl.User} |
806 | } |
807 | |
808 | +func (s *Server) resolveSeries(ref charm.Reference) *charm.URL { |
809 | + return &charm.URL{Reference: ref, Series: DefaultSeries} |
810 | +} |
811 | + |
812 | +func (s *Server) resolveURL(url string) (*charm.URL, error) { |
813 | + ref, series, err := charm.ParseReference(url) |
814 | + if err != nil { |
815 | + return nil, err |
816 | + } |
817 | + if series == "" { |
818 | + return &charm.URL{Reference: ref, Series: DefaultSeries}, nil |
819 | + } |
820 | + return &charm.URL{Reference: ref, Series: series}, nil |
821 | +} |
822 | + |
823 | func (s *Server) serveInfo(w http.ResponseWriter, r *http.Request) { |
824 | if r.URL.Path != "/charm-info" { |
825 | w.WriteHeader(http.StatusNotFound) |
826 | @@ -84,7 +101,7 @@ |
827 | for _, url := range r.Form["charms"] { |
828 | c := &charm.InfoResponse{} |
829 | response[url] = c |
830 | - curl, err := charm.ParseURL(url) |
831 | + curl, err := s.resolveURL(url) |
832 | var info *CharmInfo |
833 | if err == nil { |
834 | info, err = s.store.CharmInfo(curl) |
835 | @@ -92,6 +109,7 @@ |
836 | var skey []string |
837 | if err == nil { |
838 | skey = charmStatsKey(curl, "charm-info") |
839 | + c.CanonicalURL = curl.String() |
840 | c.Sha256 = info.BundleSha256() |
841 | c.Revision = info.Revision() |
842 | c.Digest = info.Digest() |
843 | @@ -132,7 +150,7 @@ |
844 | } |
845 | c := &charm.EventResponse{} |
846 | response[url] = c |
847 | - curl, err := charm.ParseURL(url) |
848 | + curl, err := s.resolveURL(url) |
849 | var event *CharmEvent |
850 | if err == nil { |
851 | event, err = s.store.CharmEvent(curl, digest) |
852 | @@ -169,7 +187,7 @@ |
853 | if !strings.HasPrefix(r.URL.Path, "/charm/") { |
854 | panic("serveCharm: bad url") |
855 | } |
856 | - curl, err := charm.ParseURL("cs:" + r.URL.Path[len("/charm/"):]) |
857 | + curl, err := s.resolveURL("cs:" + r.URL.Path[len("/charm/"):]) |
858 | if err != nil { |
859 | w.WriteHeader(http.StatusNotFound) |
860 | return |
861 | |
862 | === modified file 'store/server_test.go' |
863 | --- store/server_test.go 2013-11-12 14:39:29 +0000 |
864 | +++ store/server_test.go 2014-03-20 19:57:15 +0000 |
865 | @@ -21,7 +21,7 @@ |
866 | ) |
867 | |
868 | func (s *StoreSuite) prepareServer(c *gc.C) (*store.Server, *charm.URL) { |
869 | - curl := charm.MustParseURL("cs:oneiric/wordpress") |
870 | + curl := charm.MustParseURL("cs:precise/wordpress") |
871 | pub, err := s.store.CharmPublisher([]*charm.URL{curl}, "some-digest") |
872 | c.Assert(err, gc.IsNil) |
873 | err = pub.Publish(&FakeCharmDir{}) |
874 | @@ -37,10 +37,12 @@ |
875 | req, err := http.NewRequest("GET", "/charm-info", nil) |
876 | c.Assert(err, gc.IsNil) |
877 | |
878 | - var tests = []struct{ url, sha, digest, err string }{ |
879 | - {curl.String(), fakeRevZeroSha, "some-digest", ""}, |
880 | - {"cs:oneiric/non-existent", "", "", "entry not found"}, |
881 | - {"cs:bad", "", "", `charm URL without series: "cs:bad"`}, |
882 | + var tests = []struct{ url, canonical, sha, digest, err string }{ |
883 | + {curl.String(), curl.String(), fakeRevZeroSha, "some-digest", ""}, |
884 | + {"cs:oneiric/non-existent", "", "", "", "entry not found"}, |
885 | + {"cs:wordpress", curl.String(), fakeRevZeroSha, "some-digest", ""}, |
886 | + {"cs:/bad", "", "", "", `charm URL has invalid series: "cs:/bad"`}, |
887 | + {"gopher:archie-server", "", "", "", `charm URL has invalid schema: "gopher:archie-server"`}, |
888 | } |
889 | |
890 | for _, t := range tests { |
891 | @@ -51,9 +53,10 @@ |
892 | expected := make(map[string]interface{}) |
893 | if t.sha != "" { |
894 | expected[t.url] = map[string]interface{}{ |
895 | - "revision": float64(0), |
896 | - "sha256": t.sha, |
897 | - "digest": t.digest, |
898 | + "canonical-url": t.canonical, |
899 | + "revision": float64(0), |
900 | + "sha256": t.sha, |
901 | + "digest": t.digest, |
902 | } |
903 | } else { |
904 | expected[t.url] = map[string]interface{}{ |
905 | @@ -64,11 +67,12 @@ |
906 | obtained := map[string]interface{}{} |
907 | err = json.NewDecoder(rec.Body).Decode(&obtained) |
908 | c.Assert(err, gc.IsNil) |
909 | - c.Assert(obtained, gc.DeepEquals, expected) |
910 | + c.Assert(obtained, gc.DeepEquals, expected, gc.Commentf("URL: %s", t.url)) |
911 | c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "application/json") |
912 | } |
913 | |
914 | - s.checkCounterSum(c, []string{"charm-info", curl.Series, curl.Name}, false, 1) |
915 | + // 2 charm-info events, one for resolved URL, one for the reference. |
916 | + s.checkCounterSum(c, []string{"charm-info", curl.Series, curl.Name}, false, 2) |
917 | s.checkCounterSum(c, []string{"charm-missing", "oneiric", "non-existent"}, false, 1) |
918 | } |
919 | |
920 | |
921 | === modified file 'testing/charm.go' |
922 | --- testing/charm.go 2014-03-07 13:56:22 +0000 |
923 | +++ testing/charm.go 2014-03-20 19:57:15 +0000 |
924 | @@ -96,10 +96,12 @@ |
925 | } |
926 | clone(dst, r.DirPath(name)) |
927 | return &charm.URL{ |
928 | - Schema: "local", |
929 | - Series: series, |
930 | - Name: name, |
931 | - Revision: -1, |
932 | + Reference: charm.Reference{ |
933 | + Schema: "local", |
934 | + Name: name, |
935 | + Revision: -1, |
936 | + }, |
937 | + Series: series, |
938 | } |
939 | } |
940 | |
941 | @@ -126,9 +128,10 @@ |
942 | // MockCharmStore implements charm.Repository and is used to isolate tests |
943 | // that would otherwise need to hit the real charm store. |
944 | type MockCharmStore struct { |
945 | - charms map[string]map[int]*charm.Bundle |
946 | - AuthAttrs string |
947 | - TestMode bool |
948 | + charms map[string]map[int]*charm.Bundle |
949 | + AuthAttrs string |
950 | + TestMode bool |
951 | + DefaultSeries string |
952 | } |
953 | |
954 | func NewMockCharmStore() *MockCharmStore { |
955 | @@ -145,6 +148,18 @@ |
956 | return s |
957 | } |
958 | |
959 | +func (s *MockCharmStore) WithDefaultSeries(series string) charm.Repository { |
960 | + s.DefaultSeries = series |
961 | + return s |
962 | +} |
963 | + |
964 | +func (s *MockCharmStore) Resolve(ref charm.Reference) (*charm.URL, error) { |
965 | + if s.DefaultSeries == "" { |
966 | + return nil, fmt.Errorf("missing default series, cannot resolve charm url: %q", ref) |
967 | + } |
968 | + return &charm.URL{Reference: ref, Series: s.DefaultSeries}, nil |
969 | +} |
970 | + |
971 | // SetCharm adds and removes charms in s. The affected charm is identified by |
972 | // charmURL, which must be revisioned. If bundle is nil, the charm will be |
973 | // removed; otherwise, it will be stored. It is an error to store a bundle |
Reviewers: mp+211389_ code.launchpad. net,
Message:
Please take a look.
Description:
Resolve implicit series with charm store.
Charm URLs without a series are "unresolved". A charm repository can
resolve
the series. Charm store service resolves the series by adding
CanonicalURL to
the charm info if it is found & resolved. Local repository uses default
series as configured in the environment. Command-line tools interacting
with
a charm repo are responsible for making requests with resolved charm
URLs.
State server requires a resolved charm URL for such requests. Charm
store
service initially resolves series to a hard-coded value -- to be
replaced by
more advanced logic as we transition to new LTS. Updated unit tests &
mocks
accordingly.
https:/ /code.launchpad .net/~cmars/ juju-core/ no-default- series/ +merge/ 211389
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/76860044/
Affected files (+222, -79 lines): mockstore. go publish_ test.go upgradecharm. go upgradecharm_ test.go config/ config. go jujutest/ livetests. go /client/ client. go /client/ client_ test.go test.go
A [revision details]
M charm/charm.go
M charm/charm_test.go
M charm/repo.go
M charm/repo_test.go
M charm/testing/
M charm/url.go
M charm/url_test.go
M cmd/juju/deploy.go
M cmd/juju/publish.go
M cmd/juju/
M cmd/juju/
M cmd/juju/
M environs/
M environs/
M juju/conn_test.go
M state/apiserver
M state/apiserver
M store/server.go
M store/server_
M testing/charm.go