Merge lp:~cmars/juju-core/no-default-series into lp:~go-bot/juju-core/trunk

Proposed by Casey Marshall
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
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+211389@code.launchpad.net

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.

https://codereview.appspot.com/76860044/

To post a comment you must log in.
Revision history for this message
Casey Marshall (cmars) wrote :

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):
   A [revision details]
   M charm/charm.go
   M charm/charm_test.go
   M charm/repo.go
   M charm/repo_test.go
   M charm/testing/mockstore.go
   M charm/url.go
   M charm/url_test.go
   M cmd/juju/deploy.go
   M cmd/juju/publish.go
   M cmd/juju/publish_test.go
   M cmd/juju/upgradecharm.go
   M cmd/juju/upgradecharm_test.go
   M environs/config/config.go
   M environs/jujutest/livetests.go
   M juju/conn_test.go
   M state/apiserver/client/client.go
   M state/apiserver/client/client_test.go
   M store/server.go
   M store/server_test.go
   M testing/charm.go

Revision history for this message
Casey Marshall (cmars) wrote :
Revision history for this message
Casey Marshall (cmars) wrote :
Revision history for this message
Casey Marshall (cmars) wrote :
Revision history for this message
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

https://codereview.appspot.com/76860044/

Revision history for this message
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.

https://codereview.appspot.com/76860044/

Revision history for this message
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://codereview.appspot.com/76860044/diff/60001/charm/repo.go
File charm/repo.go (right):

https://codereview.appspot.com/76860044/diff/60001/charm/repo.go#newcode154
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://codereview.appspot.com/76860044/diff/60001/charm/repo.go#newcode429
charm/repo.go:429: result := &*curl
yuk, how about

result := *curl
if !result ... {

}
return &result, nil

https://codereview.appspot.com/76860044/diff/60001/store/server.go
File store/server.go (right):

https://codereview.appspot.com/76860044/diff/60001/store/server.go#newcode80
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.

https://codereview.appspot.com/76860044/

Revision history for this message
Casey Marshall (cmars) wrote :

Please take a look.

https://codereview.appspot.com/76860044/diff/60001/charm/repo.go
File charm/repo.go (right):

https://codereview.appspot.com/76860044/diff/60001/charm/repo.go#newcode154
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://codereview.appspot.com/76860044/diff/60001/charm/repo.go#newcode429
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://codereview.appspot.com/76860044/diff/60001/store/server.go
File store/server.go (right):

https://codereview.appspot.com/76860044/diff/60001/store/server.go#newcode80
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.

https://codereview.appspot.com/76860044/

Revision history for this message
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.

https://codereview.appspot.com/76860044/

Revision history for this message
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

https://codereview.appspot.com/76860044/

Revision history for this message
William Reade (fwereade) wrote :

A few comments, ping me when you're online.

https://codereview.appspot.com/76860044/diff/80001/charm/repo.go
File charm/repo.go (right):

https://codereview.appspot.com/76860044/diff/80001/charm/repo.go#newcode27
charm/repo.go:27: CanonicalURL string `json:"canonicalUrl,omitempty"`
can we make it canonical-url, please, for general consistency across the
codebase

https://codereview.appspot.com/76860044/diff/80001/state/apiserver/client/client.go
File state/apiserver/client/client.go (right):

https://codereview.appspot.com/76860044/diff/80001/state/apiserver/client/client.go#newcode857
state/apiserver/client/client.go:857: return fmt.Errorf("charm URL not
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://codereview.appspot.com/76860044/diff/80001/store/server.go
File store/server.go (right):

https://codereview.appspot.com/76860044/diff/80001/store/server.go#newcode19
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://codereview.appspot.com/76860044/diff/80001/store/server_test.go
File store/server_test.go (right):

https://codereview.appspot.com/76860044/diff/80001/store/server_test.go#newcode43
store/server_test.go:43: {"cs:wordpress", curl.String(), "", "", "entry
not found"},
hmm, the code in the previous file seemed to imply that we do expect to
turn cs:wordpress into cs:precise/wordpress. Feels like some tests are
missing somewhere...

https://codereview.appspot.com/76860044/diff/80001/testing/charm.go
File testing/charm.go (right):

https://codereview.appspot.com/76860044/diff/80001/testing/charm.go#newcode158
testing/charm.go:158: }
should we have a fallback here for stores without DefaultSeries set?

https://codereview.appspot.com/76860044/

Revision history for this message
Casey Marshall (cmars) wrote :
Revision history for this message
Casey Marshall (cmars) wrote :
Download full text (4.1 KiB)

https://codereview.appspot.com/76860044/diff/80001/charm/repo.go
File charm/repo.go (right):

https://codereview.appspot.com/76860044/diff/80001/charm/repo.go#newcode27
charm/repo.go:27: CanonicalURL string `json:"canonicalUrl,omitempty"`
On 2014/03/20 12:13:42, fwereade wrote:
> can we make it canonical-url, please, for general consistency across
the
> codebase

Done.

https://codereview.appspot.com/76860044/diff/80001/state/apiserver/client/client.go
File state/apiserver/client/client.go (right):

https://codereview.appspot.com/76860044/diff/80001/state/apiserver/client/client.go#newcode857
state/apiserver/client/client.go:857: return fmt.Errorf("charm URL not
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://codereview.appspot.com/76860044/diff/80001/store/server.go
File store/server.go (right):

https://codereview.appspot.com/76860044/diff/80001/store/server.go#newcode19
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://codereview.appspot.com/76860044/diff/80001/store/server_...

Read more...

Revision history for this message
Casey Marshall (cmars) wrote :
Revision history for this message
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

https://codereview.appspot.com/76860044/

Revision history for this message
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
=:->

Revision history for this message
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-related API calls. In my latest changes to this proposal,
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

Revision history for this message
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-related API calls. In my latest changes to this proposal,
> 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://code.launchpad.net/~cmars/juju-core/no-default-series/+merge/211389
> You are subscribed to branch lp:juju-core.

Revision history for this message
William Reade (fwereade) wrote :

LGTM, I think these are all trivials

https://codereview.appspot.com/76860044/diff/120001/charm/url_test.go
File charm/url_test.go (right):

https://codereview.appspot.com/76860044/diff/120001/charm/url_test.go#newcode167
charm/url_test.go:167: c.Assert(err.Error(), gc.Equals,
charm.ErrUnresolvedUrl.Error())
We'd usually check for direct equality with ErrUnresolvedUrl, and
otherwise use gc.ErrorMatches to check the string.

https://codereview.appspot.com/76860044/diff/120001/charm/url_test.go#newcode213
charm/url_test.go:213: {charm.IsValidSeries, "pre-c1se", true},
Explicit test that precise-1 is not allowed, but precise1 is?

https://codereview.appspot.com/76860044/diff/120001/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (left):

https://codereview.appspot.com/76860044/diff/120001/state/apiserver/client/client_test.go#oldcode663
state/apiserver/client/client_test.go:663: // TODO(fwereade) make these
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://codereview.appspot.com/76860044/diff/120001/store/server.go
File store/server.go (right):

https://codereview.appspot.com/76860044/diff/120001/store/server.go#newcode80
store/server.go:80: return &charm.URL{Reference: ref, Series:
DefaultSeries}
I'm not seeing where this is called. Dead code?

https://codereview.appspot.com/76860044/

Revision history for this message
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://code.launchpad.net/~cmars/juju-core/cs-series-solver/+merge/212407

PTAL, thanks!

I'm a bit unsure whether/how lbox & rietveld will follow the LP
resubmit..

-Casey

https://codereview.appspot.com/76860044/diff/120001/charm/url_test.go
File charm/url_test.go (right):

https://codereview.appspot.com/76860044/diff/120001/charm/url_test.go#newcode167
charm/url_test.go:167: c.Assert(err.Error(), gc.Equals,
charm.ErrUnresolvedUrl.Error())
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://codereview.appspot.com/76860044/diff/120001/charm/url_test.go#newcode213
charm/url_test.go:213: {charm.IsValidSeries, "pre-c1se", true},
On 2014/03/24 09:24:09, fwereade wrote:
> Explicit test that precise-1 is not allowed, but precise1 is?

Done.

https://codereview.appspot.com/76860044/diff/120001/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (left):

https://codereview.appspot.com/76860044/diff/120001/state/apiserver/client/client_test.go#oldcode663
state/apiserver/client/client_test.go:663: // TODO(fwereade) make these
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://codereview.appspot.com/76860044/diff/120001/store/server.go
File store/server.go (right):

https://codereview.appspot.com/76860044/diff/120001/store/server.go#newcode80
store/server.go:80: return &charm.URL{Reference: ref, Series:
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://codereview.appspot.com/76860044/

Revision history for this message
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://code.launchpad.net/~cmars/juju-core/cs-series-solver/+merge/212407
>
> PTAL, thanks!
>
> I'm a bit unsure whether/how lbox & rietveld will follow the LP
> resubmit..
>
> -Casey
>
>
> https://codereview.appspot.com/76860044/diff/120001/charm/url_test.go
> File charm/url_test.go (right):
>
>
> https://codereview.appspot.com/76860044/diff/120001/charm/url_test.go#newcode167
> charm/url_test.go:167: c.Assert(err.Error(), gc.Equals,
> charm.ErrUnresolvedUrl.Error())
> 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://codereview.appspot.com/76860044/diff/120001/charm/url_test.go#newcode213
> charm/url_test.go:213: {charm.IsValidSeries, "pre-c1se", true},
> On 2014/03/24 09:24:09, fwereade wrote:
> > Explicit test that precise-1 is not allowed, but precise1 is?
>
> Done.
>
>
> https://codereview.appspot.com/76860044/diff/120001/state/apiserver/client/client_test.go
> File state/apiserver/client/client_test.go (left):
>
>
> https://codereview.appspot.com/76860044/diff/120001/state/apiserver/client/client_test.go#oldcode663
> state/apiserver/client/client_test.go:663: // TODO(fwereade) make these
> 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://codereview.appspot.com/76860044/diff/120001/store/server.go
> File store/server.go (right):
>
>
> https://codereview.appspot.com/76860044/diff/120001/store/server.go#newcode80
> store/server.go:80: return &charm.URL{Reference: ref, Series:
> 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://codereview.appspot.com/76860044/
>
> --
> https://code.launchpad.net/~cmars/juju-core/no-default-series/+merge/211389
> You are subscribed to branch lp:juju-core.
>

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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

Subscribers

People subscribed via source and target branches

to status/vote changes: