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
=== modified file 'charm/charm.go'
--- charm/charm.go 2013-11-07 08:51:14 +0000
+++ charm/charm.go 2014-03-20 19:57:15 +0000
@@ -40,7 +40,7 @@
40 if localRepoPath == "" {40 if localRepoPath == "" {
41 return nil, errors.New("path to local repository not specified")41 return nil, errors.New("path to local repository not specified")
42 }42 }
43 repo = &LocalRepository{localRepoPath}43 repo = &LocalRepository{Path: localRepoPath}
44 default:44 default:
45 return nil, fmt.Errorf("unknown schema for charm URL %q", curl)45 return nil, fmt.Errorf("unknown schema for charm URL %q", curl)
46 }46 }
4747
=== modified file 'charm/repo.go'
--- charm/repo.go 2014-03-07 13:56:22 +0000
+++ charm/repo.go 2014-03-20 19:57:15 +0000
@@ -24,11 +24,12 @@
2424
25// InfoResponse is sent by the charm store in response to charm-info requests.25// InfoResponse is sent by the charm store in response to charm-info requests.
26type InfoResponse struct {26type InfoResponse struct {
27 Revision int `json:"revision"` // Zero is valid. Can't omitempty.27 CanonicalURL string `json:"canonical-url,omitempty"`
28 Sha256 string `json:"sha256,omitempty"`28 Revision int `json:"revision"` // Zero is valid. Can't omitempty.
29 Digest string `json:"digest,omitempty"`29 Sha256 string `json:"sha256,omitempty"`
30 Errors []string `json:"errors,omitempty"`30 Digest string `json:"digest,omitempty"`
31 Warnings []string `json:"warnings,omitempty"`31 Errors []string `json:"errors,omitempty"`
32 Warnings []string `json:"warnings,omitempty"`
32}33}
3334
34// EventResponse is sent by the charm store in response to charm-event requests.35// EventResponse is sent by the charm store in response to charm-event requests.
@@ -53,6 +54,7 @@
53type Repository interface {54type Repository interface {
54 Get(curl *URL) (Charm, error)55 Get(curl *URL) (Charm, error)
55 Latest(curls ...*URL) ([]CharmRevision, error)56 Latest(curls ...*URL) ([]CharmRevision, error)
57 Resolve(ref Reference) (*URL, error)
56}58}
5759
58// Latest returns the latest revision of the charm referenced by curl, regardless60// Latest returns the latest revision of the charm referenced by curl, regardless
@@ -137,8 +139,27 @@
137 return http.DefaultClient.Do(req)139 return http.DefaultClient.Do(req)
138}140}
139141
142// Resolve canonicalizes charm URLs, resolving references and implied series.
143func (s *CharmStore) Resolve(ref Reference) (*URL, error) {
144 infos, err := s.Info(ref)
145 if err != nil {
146 return nil, err
147 }
148 if len(infos) == 0 {
149 return nil, fmt.Errorf("missing response when resolving charm URL: %q", ref)
150 }
151 if infos[0].CanonicalURL == "" {
152 return nil, fmt.Errorf("cannot resolve charm URL: %q", ref)
153 }
154 curl, err := ParseURL(infos[0].CanonicalURL)
155 if err != nil {
156 return nil, err
157 }
158 return curl, nil
159}
160
140// Info returns details for all the specified charms in the charm store.161// Info returns details for all the specified charms in the charm store.
141func (s *CharmStore) Info(curls ...*URL) ([]*InfoResponse, error) {162func (s *CharmStore) Info(curls ...Location) ([]*InfoResponse, error) {
142 baseURL := s.BaseURL + "/charm-info?"163 baseURL := s.BaseURL + "/charm-info?"
143 queryParams := make([]string, len(curls), len(curls)+1)164 queryParams := make([]string, len(curls), len(curls)+1)
144 for i, curl := range curls {165 for i, curl := range curls {
@@ -218,7 +239,7 @@
218}239}
219240
220// revisions returns the revisions of the charms referenced by curls.241// revisions returns the revisions of the charms referenced by curls.
221func (s *CharmStore) revisions(curls ...*URL) (revisions []CharmRevision, err error) {242func (s *CharmStore) revisions(curls ...Location) (revisions []CharmRevision, err error) {
222 infos, err := s.Info(curls...)243 infos, err := s.Info(curls...)
223 if err != nil {244 if err != nil {
224 return nil, err245 return nil, err
@@ -246,7 +267,7 @@
246// Latest returns the latest revision of the charms referenced by curls, regardless267// Latest returns the latest revision of the charms referenced by curls, regardless
247// of the revision set on each curl.268// of the revision set on each curl.
248func (s *CharmStore) Latest(curls ...*URL) ([]CharmRevision, error) {269func (s *CharmStore) Latest(curls ...*URL) ([]CharmRevision, error) {
249 baseCurls := make([]*URL, len(curls))270 baseCurls := make([]Location, len(curls))
250 for i, curl := range curls {271 for i, curl := range curls {
251 baseCurls[i] = curl.WithRevision(-1)272 baseCurls[i] = curl.WithRevision(-1)
252 }273 }
@@ -387,11 +408,27 @@
387// /path/to/repository/precise/mongodb.charm408// /path/to/repository/precise/mongodb.charm
388// /path/to/repository/precise/wordpress/409// /path/to/repository/precise/wordpress/
389type LocalRepository struct {410type LocalRepository struct {
390 Path string411 Path string
412 defaultSeries string
391}413}
392414
393var _ Repository = (*LocalRepository)(nil)415var _ Repository = (*LocalRepository)(nil)
394416
417// WithDefaultSeries returns a Repository with the default series set.
418func (r *LocalRepository) WithDefaultSeries(defaultSeries string) Repository {
419 localRepo := *r
420 localRepo.defaultSeries = defaultSeries
421 return &localRepo
422}
423
424// Resolve canonicalizes charm URLs, resolving references and implied series.
425func (r *LocalRepository) Resolve(ref Reference) (*URL, error) {
426 if r.defaultSeries == "" {
427 return nil, fmt.Errorf("cannot resolve, repository has no default series: %q", ref)
428 }
429 return &URL{Reference: ref, Series: r.defaultSeries}, nil
430}
431
395// Latest returns the latest revision of the charm referenced by curl, regardless432// Latest returns the latest revision of the charm referenced by curl, regardless
396// of the revision set on curl itself.433// of the revision set on curl itself.
397func (r *LocalRepository) Latest(curls ...*URL) ([]CharmRevision, error) {434func (r *LocalRepository) Latest(curls ...*URL) ([]CharmRevision, error) {
398435
=== modified file 'charm/repo_test.go'
--- charm/repo_test.go 2014-03-07 13:56:22 +0000
+++ charm/repo_test.go 2014-03-20 19:57:15 +0000
@@ -173,7 +173,7 @@
173// The following tests cover the low-level CharmStore-specific API.173// The following tests cover the low-level CharmStore-specific API.
174174
175func (s *StoreSuite) TestInfo(c *gc.C) {175func (s *StoreSuite) TestInfo(c *gc.C) {
176 charmURLs := []*charm.URL{176 charmURLs := []charm.Location{
177 charm.MustParseURL("cs:series/good"),177 charm.MustParseURL("cs:series/good"),
178 charm.MustParseURL("cs:series/better"),178 charm.MustParseURL("cs:series/better"),
179 charm.MustParseURL("cs:series/best"),179 charm.MustParseURL("cs:series/best"),
@@ -387,7 +387,7 @@
387func (s *LocalRepoSuite) SetUpTest(c *gc.C) {387func (s *LocalRepoSuite) SetUpTest(c *gc.C) {
388 s.LoggingSuite.SetUpTest(c)388 s.LoggingSuite.SetUpTest(c)
389 root := c.MkDir()389 root := c.MkDir()
390 s.repo = &charm.LocalRepository{root}390 s.repo = &charm.LocalRepository{Path: root}
391 s.seriesPath = filepath.Join(root, "quantal")391 s.seriesPath = filepath.Join(root, "quantal")
392 c.Assert(os.Mkdir(s.seriesPath, 0777), gc.IsNil)392 c.Assert(os.Mkdir(s.seriesPath, 0777), gc.IsNil)
393}393}
394394
=== modified file 'charm/testing/mockstore.go'
--- charm/testing/mockstore.go 2014-03-07 14:15:28 +0000
+++ charm/testing/mockstore.go 2014-03-20 19:57:15 +0000
@@ -6,6 +6,7 @@
6import (6import (
7 "bytes"7 "bytes"
8 "encoding/json"8 "encoding/json"
9 "fmt"
9 "io"10 "io"
10 "net"11 "net"
11 "net/http"12 "net/http"
@@ -35,13 +36,14 @@
35 Metadata []string36 Metadata []string
36 InfoRequestCount int37 InfoRequestCount int
37 InfoRequestCountNoStats int38 InfoRequestCountNoStats int
39 DefaultSeries string
3840
39 charms map[string]int41 charms map[string]int
40}42}
4143
42// NewMockStore creates a mock charm store containing the specified charms.44// NewMockStore creates a mock charm store containing the specified charms.
43func NewMockStore(c *gc.C, charms map[string]int) *MockStore {45func NewMockStore(c *gc.C, charms map[string]int) *MockStore {
44 s := &MockStore{charms: charms}46 s := &MockStore{charms: charms, DefaultSeries: "precise"}
45 f, err := os.Open(testing.Charms.BundlePath(c.MkDir(), "dummy"))47 f, err := os.Open(testing.Charms.BundlePath(c.MkDir(), "dummy"))
46 c.Assert(err, gc.IsNil)48 c.Assert(err, gc.IsNil)
47 defer f.Close()49 defer f.Close()
@@ -98,7 +100,17 @@
98 for _, url := range r.Form["charms"] {100 for _, url := range r.Form["charms"] {
99 cr := &charm.InfoResponse{}101 cr := &charm.InfoResponse{}
100 response[url] = cr102 response[url] = cr
101 charmURL := charm.MustParseURL(url)103 charmURL, err := charm.ParseURL(url)
104 if err == charm.ErrUnresolvedUrl {
105 ref, _, err := charm.ParseReference(url)
106 if err != nil {
107 panic(err)
108 }
109 if s.DefaultSeries == "" {
110 panic(fmt.Errorf("mock store lacks a default series cannot resolve charm URL: %q", url))
111 }
112 charmURL = &charm.URL{Reference: ref, Series: s.DefaultSeries}
113 }
102 switch charmURL.Name {114 switch charmURL.Name {
103 case "borken":115 case "borken":
104 cr.Errors = append(cr.Errors, "badness")116 cr.Errors = append(cr.Errors, "badness")
@@ -115,6 +127,7 @@
115 cr.Revision = charmURL.Revision127 cr.Revision = charmURL.Revision
116 }128 }
117 cr.Sha256 = s.bundleSha256129 cr.Sha256 = s.bundleSha256
130 cr.CanonicalURL = charmURL.String()
118 } else {131 } else {
119 cr.Errors = append(cr.Errors, "entry not found")132 cr.Errors = append(cr.Errors, "entry not found")
120 }133 }
121134
=== modified file 'charm/url.go'
--- charm/url.go 2014-01-28 10:45:55 +0000
+++ charm/url.go 2014-03-20 19:57:15 +0000
@@ -13,23 +13,42 @@
13 "labix.org/v2/mgo/bson"13 "labix.org/v2/mgo/bson"
14)14)
1515
16// A charm URL represents charm locations such as:16// Location represents a charm location, which must declare a path component
17// and a string representaion.
18type Location interface {
19 Path() string
20 String() string
21}
22
23// Reference represents a charm location with an unresolved, untargeted series,
24// such as:
25//
26// cs:~joe/wordpress
27// cs:wordpress-42
28type Reference struct {
29 Schema string // "cs" or "local"
30 User string // "joe"
31 Name string // "wordpress"
32 Revision int // -1 if unset, N otherwise
33}
34
35// URL represents a fully resolved charm location with a specific series, such
36// as:
17//37//
18// cs:~joe/oneiric/wordpress38// cs:~joe/oneiric/wordpress
19// cs:oneiric/wordpress-4239// cs:oneiric/wordpress-42
20// local:oneiric/wordpress40// local:oneiric/wordpress
21//41//
22type URL struct {42type URL struct {
23 Schema string // "cs" or "local"43 Reference
24 User string // "joe"44 Series string // "oneiric"
25 Series string // "oneiric"
26 Name string // "wordpress"
27 Revision int // -1 if unset, N otherwise
28}45}
2946
47var ErrUnresolvedUrl error = fmt.Errorf("charm url series is not resolved")
48
30var (49var (
31 validUser = regexp.MustCompile("^[a-z0-9][a-zA-Z0-9+.-]+$")50 validUser = regexp.MustCompile("^[a-z0-9][a-zA-Z0-9+.-]+$")
32 validSeries = regexp.MustCompile("^[a-z]+([a-z-]+[a-z])?$")51 validSeries = regexp.MustCompile("^[a-z]+([a-z0-9-]+[a-z0-9])?$")
33 validName = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$")52 validName = regexp.MustCompile("^[a-z][a-z0-9]*(-[a-z0-9]*[a-z][a-z0-9]*)*$")
34)53)
3554
@@ -68,67 +87,83 @@
68// ParseURL parses the provided charm URL string into its respective87// ParseURL parses the provided charm URL string into its respective
69// structure.88// structure.
70func ParseURL(url string) (*URL, error) {89func ParseURL(url string) (*URL, error) {
71 u := &URL{}90 r, series, err := ParseReference(url)
91 if err != nil {
92 return nil, err
93 }
94 if series == "" {
95 return nil, ErrUnresolvedUrl
96 }
97 return &URL{Reference: r, Series: series}, nil
98}
99
100// ParseReference parses the provided charm Reference string into its
101// respective structure and the targeted series, if present.
102func ParseReference(url string) (Reference, string, error) {
103 r := Reference{Schema: "cs"}
104 series := ""
72 i := strings.Index(url, ":")105 i := strings.Index(url, ":")
73 if i > 0 {106 if i >= 0 {
74 u.Schema = url[:i]107 r.Schema = url[:i]
75 i++108 i++
109 } else {
110 i = 0
76 }111 }
77 // cs: or local:112 // cs: or local:
78 if u.Schema != "cs" && u.Schema != "local" {113 if r.Schema != "cs" && r.Schema != "local" {
79 return nil, fmt.Errorf("charm URL has invalid schema: %q", url)114 return Reference{}, "", fmt.Errorf("charm URL has invalid schema: %q", url)
80 }115 }
81 parts := strings.Split(url[i:], "/")116 parts := strings.Split(url[i:], "/")
82 if len(parts) < 1 || len(parts) > 3 {117 if len(parts) < 1 || len(parts) > 3 {
83 return nil, fmt.Errorf("charm URL has invalid form: %q", url)118 return Reference{}, "", fmt.Errorf("charm URL has invalid form: %q", url)
84 }119 }
85120
86 // ~<username>121 // ~<username>
87 if strings.HasPrefix(parts[0], "~") {122 if strings.HasPrefix(parts[0], "~") {
88 if u.Schema == "local" {123 if r.Schema == "local" {
89 return nil, fmt.Errorf("local charm URL with user name: %q", url)124 return Reference{}, "", fmt.Errorf("local charm URL with user name: %q", url)
90 }125 }
91 u.User = parts[0][1:]126 r.User = parts[0][1:]
92 if !IsValidUser(u.User) {127 if !IsValidUser(r.User) {
93 return nil, fmt.Errorf("charm URL has invalid user name: %q", url)128 return Reference{}, "", fmt.Errorf("charm URL has invalid user name: %q", url)
94 }129 }
95 parts = parts[1:]130 parts = parts[1:]
96 }131 }
97132
98 // <series>133 // <series>
99 if len(parts) < 2 {
100 return nil, fmt.Errorf("charm URL without series: %q", url)
101 }
102 if len(parts) == 2 {134 if len(parts) == 2 {
103 u.Series = parts[0]135 series = parts[0]
104 if !IsValidSeries(u.Series) {136 if !IsValidSeries(series) {
105 return nil, fmt.Errorf("charm URL has invalid series: %q", url)137 return Reference{}, "", fmt.Errorf("charm URL has invalid series: %q", url)
106 }138 }
107 parts = parts[1:]139 parts = parts[1:]
108 }140 }
141 if len(parts) < 1 {
142 return Reference{}, "", fmt.Errorf("charm URL without charm name: %q", url)
143 }
109144
110 // <name>[-<revision>]145 // <name>[-<revision>]
111 u.Name = parts[0]146 r.Name = parts[0]
112 u.Revision = -1147 r.Revision = -1
113 for i := len(u.Name) - 1; i > 0; i-- {148 for i := len(r.Name) - 1; i > 0; i-- {
114 c := u.Name[i]149 c := r.Name[i]
115 if c >= '0' && c <= '9' {150 if c >= '0' && c <= '9' {
116 continue151 continue
117 }152 }
118 if c == '-' && i != len(u.Name)-1 {153 if c == '-' && i != len(r.Name)-1 {
119 var err error154 var err error
120 u.Revision, err = strconv.Atoi(u.Name[i+1:])155 r.Revision, err = strconv.Atoi(r.Name[i+1:])
121 if err != nil {156 if err != nil {
122 panic(err) // We just checked it was right.157 panic(err) // We just checked it was right.
123 }158 }
124 u.Name = u.Name[:i]159 r.Name = r.Name[:i]
125 }160 }
126 break161 break
127 }162 }
128 if !IsValidName(u.Name) {163 if !IsValidName(r.Name) {
129 return nil, fmt.Errorf("charm URL has invalid charm name: %q", url)164 return Reference{}, "", fmt.Errorf("charm URL has invalid charm name: %q", url)
130 }165 }
131 return u, nil166 return r, series, nil
132}167}
133168
134// InferURL returns a charm URL inferred from src. The provided169// InferURL returns a charm URL inferred from src. The provided
@@ -148,9 +183,12 @@
148// when src does not include that information; similarly, a missing183// when src does not include that information; similarly, a missing
149// schema is assumed to be 'cs'.184// schema is assumed to be 'cs'.
150func InferURL(src, defaultSeries string) (*URL, error) {185func InferURL(src, defaultSeries string) (*URL, error) {
151 if u, err := ParseURL(src); err == nil {186 r, series, err := ParseReference(src)
152 // src was a valid charm URL already187 if err != nil {
153 return u, nil188 return nil, err
189 }
190 if series != "" {
191 return &URL{Reference: r, Series: series}, nil
154 }192 }
155 if strings.HasPrefix(src, "~") {193 if strings.HasPrefix(src, "~") {
156 return nil, fmt.Errorf("cannot infer charm URL with user but no schema: %q", src)194 return nil, fmt.Errorf("cannot infer charm URL with user but no schema: %q", src)
@@ -187,22 +225,37 @@
187}225}
188226
189func (u *URL) Path() string {227func (u *URL) Path() string {
190 if u.User != "" {228 return u.path(u.Series)
191 if u.Revision >= 0 {229}
192 return fmt.Sprintf("~%s/%s/%s-%d", u.User, u.Series, u.Name, u.Revision)230
193 }231func (r Reference) path(series string) string {
194 return fmt.Sprintf("~%s/%s/%s", u.User, u.Series, u.Name)232 var parts []string
195 }233 if r.User != "" {
196 if u.Revision >= 0 {234 parts = append(parts, fmt.Sprintf("~%s", r.User))
197 return fmt.Sprintf("%s/%s-%d", u.Series, u.Name, u.Revision)235 }
198 }236 if series != "" {
199 return fmt.Sprintf("%s/%s", u.Series, u.Name)237 parts = append(parts, series)
238 }
239 if r.Revision >= 0 {
240 parts = append(parts, fmt.Sprintf("%s-%d", r.Name, r.Revision))
241 } else {
242 parts = append(parts, r.Name)
243 }
244 return strings.Join(parts, "/")
245}
246
247func (r Reference) Path() string {
248 return r.path("")
200}249}
201250
202func (u *URL) String() string {251func (u *URL) String() string {
203 return fmt.Sprintf("%s:%s", u.Schema, u.Path())252 return fmt.Sprintf("%s:%s", u.Schema, u.Path())
204}253}
205254
255func (r Reference) String() string {
256 return fmt.Sprintf("%s:%s", r.Schema, r.Path())
257}
258
206// GetBSON turns u into a bson.Getter so it can be saved directly259// GetBSON turns u into a bson.Getter so it can be saved directly
207// on a MongoDB database with mgo.260// on a MongoDB database with mgo.
208func (u *URL) GetBSON() (interface{}, error) {261func (u *URL) GetBSON() (interface{}, error) {
209262
=== modified file 'charm/url_test.go'
--- charm/url_test.go 2014-01-28 10:45:55 +0000
+++ charm/url_test.go 2014-03-20 19:57:15 +0000
@@ -6,6 +6,7 @@
6import (6import (
7 "encoding/json"7 "encoding/json"
8 "fmt"8 "fmt"
9 "strings"
910
10 "labix.org/v2/mgo/bson"11 "labix.org/v2/mgo/bson"
11 gc "launchpad.net/gocheck"12 gc "launchpad.net/gocheck"
@@ -21,38 +22,63 @@
21 s, err string22 s, err string
22 url *charm.URL23 url *charm.URL
23}{24}{
24 {"cs:~user/series/name", "", &charm.URL{"cs", "user", "series", "name", -1}},25 {"cs:~user/series/name", "", &charm.URL{charm.Reference{"cs", "user", "name", -1}, "series"}},
25 {"cs:~user/series/name-0", "", &charm.URL{"cs", "user", "series", "name", 0}},26 {"cs:~user/series/name-0", "", &charm.URL{charm.Reference{"cs", "user", "name", 0}, "series"}},
26 {"cs:series/name", "", &charm.URL{"cs", "", "series", "name", -1}},27 {"cs:series/name", "", &charm.URL{charm.Reference{"cs", "", "name", -1}, "series"}},
27 {"cs:series/name-42", "", &charm.URL{"cs", "", "series", "name", 42}},28 {"cs:series/name-42", "", &charm.URL{charm.Reference{"cs", "", "name", 42}, "series"}},
28 {"local:series/name-1", "", &charm.URL{"local", "", "series", "name", 1}},29 {"local:series/name-1", "", &charm.URL{charm.Reference{"local", "", "name", 1}, "series"}},
29 {"local:series/name", "", &charm.URL{"local", "", "series", "name", -1}},30 {"local:series/name", "", &charm.URL{charm.Reference{"local", "", "name", -1}, "series"}},
30 {"local:series/n0-0n-n0", "", &charm.URL{"local", "", "series", "n0-0n-n0", -1}},31 {"local:series/n0-0n-n0", "", &charm.URL{charm.Reference{"local", "", "n0-0n-n0", -1}, "series"}},
32 {"cs:~user/name", "", &charm.URL{charm.Reference{"cs", "user", "name", -1}, ""}},
33 {"cs:name", "", &charm.URL{charm.Reference{"cs", "", "name", -1}, ""}},
34 {"local:name", "", &charm.URL{charm.Reference{"local", "", "name", -1}, ""}},
3135
32 {"bs:~user/series/name-1", "charm URL has invalid schema: .*", nil},36 {"bs:~user/series/name-1", "charm URL has invalid schema: .*", nil},
33 {"cs:~1/series/name-1", "charm URL has invalid user name: .*", nil},37 {"cs:~1/series/name-1", "charm URL has invalid user name: .*", nil},
38 {"cs:~user", "charm URL without charm name: .*", nil},
34 {"cs:~user/1/name-1", "charm URL has invalid series: .*", nil},39 {"cs:~user/1/name-1", "charm URL has invalid series: .*", nil},
35 {"cs:~user/series/name-1-2", "charm URL has invalid charm name: .*", nil},40 {"cs:~user/series/name-1-2", "charm URL has invalid charm name: .*", nil},
36 {"cs:~user/series/name-1-name-2", "charm URL has invalid charm name: .*", nil},41 {"cs:~user/series/name-1-name-2", "charm URL has invalid charm name: .*", nil},
37 {"cs:~user/series/name--name-2", "charm URL has invalid charm name: .*", nil},42 {"cs:~user/series/name--name-2", "charm URL has invalid charm name: .*", nil},
38 {"cs:~user/series/huh/name-1", "charm URL has invalid form: .*", nil},43 {"cs:~user/series/huh/name-1", "charm URL has invalid form: .*", nil},
39 {"cs:~user/name", "charm URL without series: .*", nil},44 {"cs:/name", "charm URL has invalid series: .*", nil},
40 {"cs:name", "charm URL without series: .*", nil},
41 {"local:~user/series/name", "local charm URL with user name: .*", nil},45 {"local:~user/series/name", "local charm URL with user name: .*", nil},
42 {"local:~user/name", "local charm URL with user name: .*", nil},46 {"local:~user/name", "local charm URL with user name: .*", nil},
43 {"local:name", "charm URL without series: .*", nil},
44}47}
4548
46func (s *URLSuite) TestParseURL(c *gc.C) {49func (s *URLSuite) TestParseURL(c *gc.C) {
47 for i, t := range urlTests {50 for i, t := range urlTests {
48 c.Logf("test %d", i)51 c.Logf("test %d", i)
49 url, err := charm.ParseURL(t.s)52 url, uerr := charm.ParseURL(t.s)
53 ref, series, rerr := charm.ParseReference(t.s)
50 comment := gc.Commentf("ParseURL(%q)", t.s)54 comment := gc.Commentf("ParseURL(%q)", t.s)
51 if t.err != "" {55 if t.url != nil && t.url.Series == "" {
52 c.Check(err.Error(), gc.Matches, t.err, comment)56 if t.err != "" {
57 // Expected error should match
58 c.Assert(rerr, gc.NotNil, comment)
59 c.Check(rerr.Error(), gc.Matches, t.err, comment)
60 } else {
61 // Expected charm reference should match
62 c.Check(ref, gc.DeepEquals, t.url.Reference, comment)
63 c.Check(t.url.Reference.String(), gc.Equals, t.s)
64 }
65 if rerr != nil {
66 // If ParseReference has an error, ParseURL should share it
67 c.Check(uerr.Error(), gc.Equals, rerr.Error(), comment)
68 } else {
69 // Otherwise, ParseURL with an empty series should error unresolved.
70 c.Check(uerr.Error(), gc.Equals, charm.ErrUnresolvedUrl.Error(), comment)
71 }
53 } else {72 } else {
54 c.Check(url, gc.DeepEquals, t.url, comment)73 if t.err != "" {
55 c.Check(t.url.String(), gc.Equals, t.s)74 c.Assert(uerr, gc.NotNil, comment)
75 c.Check(uerr.Error(), gc.Matches, t.err, comment)
76 c.Check(uerr.Error(), gc.Equals, rerr.Error(), comment)
77 } else {
78 c.Check(url.Series, gc.Equals, series, comment)
79 c.Check(url, gc.DeepEquals, t.url, comment)
80 c.Check(t.url.String(), gc.Equals, t.s)
81 }
56 }82 }
57 }83 }
58}84}
@@ -82,31 +108,35 @@
82 comment := gc.Commentf("InferURL(%q, %q)", t.vague, "defseries")108 comment := gc.Commentf("InferURL(%q, %q)", t.vague, "defseries")
83 inferred, ierr := charm.InferURL(t.vague, "defseries")109 inferred, ierr := charm.InferURL(t.vague, "defseries")
84 parsed, perr := charm.ParseURL(t.exact)110 parsed, perr := charm.ParseURL(t.exact)
85 if parsed != nil {111 if perr == nil {
86 c.Check(inferred, gc.DeepEquals, parsed, comment)112 c.Check(inferred, gc.DeepEquals, parsed, comment)
113 c.Check(ierr, gc.IsNil)
87 } else {114 } else {
88 expect := perr.Error()115 expect := perr.Error()
89 if t.vague != t.exact {116 if t.vague != t.exact {
90 expect = fmt.Sprintf("%s (URL inferred from %q)", expect, t.vague)117 if colIdx := strings.Index(expect, ":"); colIdx > 0 {
118 expect = expect[:colIdx]
119 }
91 }120 }
92 c.Check(ierr.Error(), gc.Equals, expect, comment)121 c.Check(ierr.Error(), gc.Matches, expect+".*", comment)
93 }122 }
94 }123 }
95 u, err := charm.InferURL("~blah", "defseries")124 u, err := charm.InferURL("~blah", "defseries")
96 c.Assert(u, gc.IsNil)125 c.Assert(u, gc.IsNil)
97 c.Assert(err, gc.ErrorMatches, "cannot infer charm URL with user but no schema: .*")126 c.Assert(err, gc.ErrorMatches, "charm URL without charm name: .*")
98}127}
99128
100var inferNoDefaultSeriesTests = []struct {129var inferNoDefaultSeriesTests = []struct {
101 vague, exact string130 vague, exact string
131 resolved bool
102}{132}{
103 {"foo", ""},133 {"foo", "", false},
104 {"foo-1", ""},134 {"foo-1", "", false},
105 {"cs:foo", ""},135 {"cs:foo", "", false},
106 {"cs:~user/foo", ""},136 {"cs:~user/foo", "", false},
107 {"series/foo", "cs:series/foo"},137 {"series/foo", "cs:series/foo", true},
108 {"cs:series/foo", "cs:series/foo"},138 {"cs:series/foo", "cs:series/foo", true},
109 {"cs:~user/series/foo", "cs:~user/series/foo"},139 {"cs:~user/series/foo", "cs:~user/series/foo", true},
110}140}
111141
112func (s *URLSuite) TestInferURLNoDefaultSeries(c *gc.C) {142func (s *URLSuite) TestInferURLNoDefaultSeries(c *gc.C) {
@@ -122,6 +152,23 @@
122 }152 }
123}153}
124154
155func (s *URLSuite) TestParseUnresolved(c *gc.C) {
156 for _, t := range inferNoDefaultSeriesTests {
157 if t.resolved {
158 url, err := charm.ParseURL(t.vague)
159 c.Assert(err, gc.IsNil)
160 c.Assert(url.Series, gc.Not(gc.Equals), "")
161 } else {
162 _, series, err := charm.ParseReference(t.vague)
163 c.Assert(err, gc.IsNil)
164 c.Assert(series, gc.Equals, "")
165 _, err = charm.ParseURL(t.vague)
166 c.Assert(err, gc.NotNil)
167 c.Assert(err.Error(), gc.Equals, charm.ErrUnresolvedUrl.Error())
168 }
169 }
170}
171
125var validTests = []struct {172var validTests = []struct {
126 valid func(string) bool173 valid func(string) bool
127 string string174 string string
@@ -160,31 +207,38 @@
160 {charm.IsValidSeries, "pre cise", false},207 {charm.IsValidSeries, "pre cise", false},
161 {charm.IsValidSeries, "pre-cise", true},208 {charm.IsValidSeries, "pre-cise", true},
162 {charm.IsValidSeries, "pre^cise", false},209 {charm.IsValidSeries, "pre^cise", false},
163 {charm.IsValidSeries, "prec1se", false},210 {charm.IsValidSeries, "prec1se", true},
164 {charm.IsValidSeries, "-precise", false},211 {charm.IsValidSeries, "-precise", false},
165 {charm.IsValidSeries, "precise-", false},212 {charm.IsValidSeries, "precise-", false},
166 {charm.IsValidSeries, "pre-c1se", false},213 {charm.IsValidSeries, "pre-c1se", true},
167}214}
168215
169func (s *URLSuite) TestValidCheckers(c *gc.C) {216func (s *URLSuite) TestValidCheckers(c *gc.C) {
170 for i, t := range validTests {217 for i, t := range validTests {
171 c.Logf("test %d: %s", i, t.string)218 c.Logf("test %d: %s", i, t.string)
172 c.Assert(t.valid(t.string), gc.Equals, t.expect)219 c.Assert(t.valid(t.string), gc.Equals, t.expect, gc.Commentf("%s", t.string))
173 }220 }
174}221}
175222
176func (s *URLSuite) TestMustParseURL(c *gc.C) {223func (s *URLSuite) TestMustParseURL(c *gc.C) {
177 url := charm.MustParseURL("cs:series/name")224 url := charm.MustParseURL("cs:series/name")
178 c.Assert(url, gc.DeepEquals, &charm.URL{"cs", "", "series", "name", -1})225 c.Assert(url, gc.DeepEquals,
179 f := func() { charm.MustParseURL("local:name") }226 &charm.URL{Reference: charm.Reference{"cs", "", "name", -1}, Series: "series"})
180 c.Assert(f, gc.PanicMatches, "charm URL without series: .*")227 f := func() { charm.MustParseURL("local:@@/name") }
228 c.Assert(f, gc.PanicMatches, "charm URL has invalid series: .*")
229 f = func() { charm.MustParseURL("cs:~user") }
230 c.Assert(f, gc.PanicMatches, "charm URL without charm name: .*")
231 f = func() { charm.MustParseURL("cs:~user") }
232 c.Assert(f, gc.PanicMatches, "charm URL without charm name: .*")
233 f = func() { charm.MustParseURL("cs:name") }
234 c.Assert(f, gc.PanicMatches, "charm url series is not resolved")
181}235}
182236
183func (s *URLSuite) TestWithRevision(c *gc.C) {237func (s *URLSuite) TestWithRevision(c *gc.C) {
184 url := charm.MustParseURL("cs:series/name")238 url := charm.MustParseURL("cs:series/name")
185 other := url.WithRevision(1)239 other := url.WithRevision(1)
186 c.Assert(url, gc.DeepEquals, &charm.URL{"cs", "", "series", "name", -1})240 c.Assert(url, gc.DeepEquals, &charm.URL{charm.Reference{"cs", "", "name", -1}, "series"})
187 c.Assert(other, gc.DeepEquals, &charm.URL{"cs", "", "series", "name", 1})241 c.Assert(other, gc.DeepEquals, &charm.URL{charm.Reference{"cs", "", "name", 1}, "series"})
188242
189 // Should always copy. The opposite behavior is error prone.243 // Should always copy. The opposite behavior is error prone.
190 c.Assert(other.WithRevision(1), gc.Not(gc.Equals), other)244 c.Assert(other.WithRevision(1), gc.Not(gc.Equals), other)
191245
=== modified file 'cmd/juju/bootstrap_test.go'
--- cmd/juju/bootstrap_test.go 2014-03-19 04:06:58 +0000
+++ cmd/juju/bootstrap_test.go 2014-03-20 19:57:15 +0000
@@ -292,8 +292,8 @@
292 err: `invalid value "bad=wrong" for flag --constraints: unknown constraint "bad"`,292 err: `invalid value "bad=wrong" for flag --constraints: unknown constraint "bad"`,
293}, {293}, {
294 info: "bad --series",294 info: "bad --series",
295 args: []string{"--series", "bad1"},295 args: []string{"--series", "1bad1"},
296 err: `invalid value "bad1" for flag --series: invalid series name "bad1"`,296 err: `invalid value "1bad1" for flag --series: invalid series name "1bad1"`,
297}, {297}, {
298 info: "lonely --series",298 info: "lonely --series",
299 args: []string{"--series", "fine"},299 args: []string{"--series", "fine"},
300300
=== modified file 'environs/jujutest/livetests.go'
--- environs/jujutest/livetests.go 2014-03-14 02:19:35 +0000
+++ environs/jujutest/livetests.go 2014-03-20 19:57:15 +0000
@@ -450,7 +450,7 @@
450 c.Logf("deploying service")450 c.Logf("deploying service")
451 repoDir := c.MkDir()451 repoDir := c.MkDir()
452 url := coretesting.Charms.ClonedURL(repoDir, mtools0.Version.Series, "dummy")452 url := coretesting.Charms.ClonedURL(repoDir, mtools0.Version.Series, "dummy")
453 sch, err := conn.PutCharm(url, &charm.LocalRepository{repoDir}, false)453 sch, err := conn.PutCharm(url, &charm.LocalRepository{Path: repoDir}, false)
454 c.Assert(err, gc.IsNil)454 c.Assert(err, gc.IsNil)
455 svc, err := conn.State.AddService("dummy", "user-admin", sch)455 svc, err := conn.State.AddService("dummy", "user-admin", sch)
456 c.Assert(err, gc.IsNil)456 c.Assert(err, gc.IsNil)
457457
=== modified file 'juju/conn_test.go'
--- juju/conn_test.go 2014-03-17 22:42:51 +0000
+++ juju/conn_test.go 2014-03-20 19:57:15 +0000
@@ -337,10 +337,12 @@
337 // Invent a URL that points to the bundled charm, and337 // Invent a URL that points to the bundled charm, and
338 // test putting that.338 // test putting that.
339 curl := &charm.URL{339 curl := &charm.URL{
340 Schema: "local",340 Reference: charm.Reference{
341 Series: "quantal",341 Schema: "local",
342 Name: "riak",342 Name: "riak",
343 Revision: -1,343 Revision: -1,
344 },
345 Series: "quantal",
344 }346 }
345 _, err = s.conn.PutCharm(curl, s.repo, true)347 _, err = s.conn.PutCharm(curl, s.repo, true)
346 c.Assert(err, gc.ErrorMatches, `cannot increment revision of charm "local:quantal/riak-7": not a directory`)348 c.Assert(err, gc.ErrorMatches, `cannot increment revision of charm "local:quantal/riak-7": not a directory`)
@@ -356,7 +358,7 @@
356}358}
357359
358func (s *ConnSuite) TestPutCharmUpload(c *gc.C) {360func (s *ConnSuite) TestPutCharmUpload(c *gc.C) {
359 repo := &charm.LocalRepository{c.MkDir()}361 repo := &charm.LocalRepository{Path: c.MkDir()}
360 curl := coretesting.Charms.ClonedURL(repo.Path, "quantal", "riak")362 curl := coretesting.Charms.ClonedURL(repo.Path, "quantal", "riak")
361363
362 // Put charm for the first time.364 // Put charm for the first time.
363365
=== modified file 'state/apiserver/charms.go'
--- state/apiserver/charms.go 2014-03-13 05:57:38 +0000
+++ state/apiserver/charms.go 2014-03-20 19:57:15 +0000
@@ -197,10 +197,12 @@
197 }197 }
198 // We got it, now let's reserve a charm URL for it in state.198 // We got it, now let's reserve a charm URL for it in state.
199 archiveURL := &charm.URL{199 archiveURL := &charm.URL{
200 Schema: "local",200 Reference: charm.Reference{
201 Series: series,201 Schema: "local",
202 Name: archive.Meta().Name,202 Name: archive.Meta().Name,
203 Revision: archive.Revision(),203 Revision: archive.Revision(),
204 },
205 Series: series,
204 }206 }
205 preparedURL, err := h.state.PrepareLocalCharmUpload(archiveURL)207 preparedURL, err := h.state.PrepareLocalCharmUpload(archiveURL)
206 if err != nil {208 if err != nil {
207209
=== modified file 'state/apiserver/client/client_test.go'
--- state/apiserver/client/client_test.go 2014-03-19 03:18:41 +0000
+++ state/apiserver/client/client_test.go 2014-03-20 19:57:15 +0000
@@ -220,7 +220,12 @@
220 {220 {
221 about: "invalid URL",221 about: "invalid URL",
222 url: "not-valid",222 url: "not-valid",
223 err: `charm URL has invalid schema: "not-valid"`,223 err: "charm url series is not resolved",
224 },
225 {
226 about: "invalid schema",
227 url: "not-valid:your-arguments",
228 err: `charm URL has invalid schema: "not-valid:your-arguments"`,
224 },229 },
225 {230 {
226 about: "unknown charm",231 about: "unknown charm",
@@ -661,8 +666,8 @@
661 defer restore()666 defer restore()
662 for url, expect := range map[string]string{667 for url, expect := range map[string]string{
663 // TODO(fwereade) make these errors consistent one day.668 // TODO(fwereade) make these errors consistent one day.
664 "wordpress": `charm URL has invalid schema: "wordpress"`,669 "wordpress": "charm url series is not resolved",
665 "cs:wordpress": `charm URL without series: "cs:wordpress"`,670 "cs:wordpress": "charm url series is not resolved",
666 "cs:precise/wordpress": "charm url must include revision",671 "cs:precise/wordpress": "charm url must include revision",
667 "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`,672 "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`,
668 } {673 } {
@@ -839,8 +844,8 @@
839 s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress"))844 s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress"))
840 for charmUrl, expect := range map[string]string{845 for charmUrl, expect := range map[string]string{
841 // TODO(fwereade,Makyo) make these errors consistent one day.846 // TODO(fwereade,Makyo) make these errors consistent one day.
842 "wordpress": `charm URL has invalid schema: "wordpress"`,847 "wordpress": "charm url series is not resolved",
843 "cs:wordpress": `charm URL without series: "cs:wordpress"`,848 "cs:wordpress": "charm url series is not resolved",
844 "cs:precise/wordpress": "charm url must include revision",849 "cs:precise/wordpress": "charm url must include revision",
845 "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`,850 "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`,
846 } {851 } {
@@ -1073,8 +1078,8 @@
1073 s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress"))1078 s.AddTestingService(c, "wordpress", s.AddTestingCharm(c, "wordpress"))
1074 for url, expect := range map[string]string{1079 for url, expect := range map[string]string{
1075 // TODO(fwereade,Makyo) make these errors consistent one day.1080 // TODO(fwereade,Makyo) make these errors consistent one day.
1076 "wordpress": `charm URL has invalid schema: "wordpress"`,1081 "wordpress": "charm url series is not resolved",
1077 "cs:wordpress": `charm URL without series: "cs:wordpress"`,1082 "cs:wordpress": "charm url series is not resolved",
1078 "cs:precise/wordpress": "charm url must include revision",1083 "cs:precise/wordpress": "charm url must include revision",
1079 "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`,1084 "cs:precise/wordpress-999999": `cannot download charm ".*": charm not found in mock store: cs:precise/wordpress-999999`,
1080 } {1085 } {
@@ -1839,8 +1844,8 @@
18391844
1840 client := s.APIState.Client()1845 client := s.APIState.Client()
1841 // First test the sanity checks.1846 // First test the sanity checks.
1842 err := client.AddCharm(&charm.URL{Name: "nonsense"})1847 err := client.AddCharm(&charm.URL{Reference: charm.Reference{Name: "nonsense"}})
1843 c.Assert(err, gc.ErrorMatches, `charm URL has invalid schema: ":/nonsense-0"`)1848 c.Assert(err, gc.ErrorMatches, `charm URL has invalid schema: ":nonsense-0"`)
1844 err = client.AddCharm(charm.MustParseURL("local:precise/dummy"))1849 err = client.AddCharm(charm.MustParseURL("local:precise/dummy"))
1845 c.Assert(err, gc.ErrorMatches, "only charm store charm URLs are supported, with cs: schema")1850 c.Assert(err, gc.ErrorMatches, "only charm store charm URLs are supported, with cs: schema")
1846 err = client.AddCharm(charm.MustParseURL("cs:precise/wordpress"))1851 err = client.AddCharm(charm.MustParseURL("cs:precise/wordpress"))
18471852
=== modified file 'store/lpad.go'
--- store/lpad.go 2013-07-09 10:32:23 +0000
+++ store/lpad.go 2014-03-20 19:57:15 +0000
@@ -69,7 +69,10 @@
69 urls := []*charm.URL{curl}69 urls := []*charm.URL{curl}
70 schema, name := curl.Schema, curl.Name70 schema, name := curl.Schema, curl.Name
71 for _, series := range tip.OfficialSeries {71 for _, series := range tip.OfficialSeries {
72 curl = &charm.URL{Schema: schema, Name: name, Series: series, Revision: -1}72 curl = &charm.URL{
73 Reference: charm.Reference{Schema: schema, Name: name, Revision: -1},
74 Series: series,
75 }
73 curl.Series = series76 curl.Series = series
74 curl.User = ""77 curl.User = ""
75 urls = append(urls, curl)78 urls = append(urls, curl)
7679
=== modified file 'store/server.go'
--- store/server.go 2013-11-12 14:39:29 +0000
+++ store/server.go 2014-03-20 19:57:15 +0000
@@ -16,6 +16,8 @@
16 "launchpad.net/juju-core/log"16 "launchpad.net/juju-core/log"
17)17)
1818
19const DefaultSeries = "precise"
20
19// Server is an http.Handler that serves the HTTP API of juju21// Server is an http.Handler that serves the HTTP API of juju
20// so that juju clients can retrieve published charms.22// so that juju clients can retrieve published charms.
21type Server struct {23type Server struct {
@@ -74,6 +76,21 @@
74 return []string{kind, curl.Series, curl.Name, curl.User}76 return []string{kind, curl.Series, curl.Name, curl.User}
75}77}
7678
79func (s *Server) resolveSeries(ref charm.Reference) *charm.URL {
80 return &charm.URL{Reference: ref, Series: DefaultSeries}
81}
82
83func (s *Server) resolveURL(url string) (*charm.URL, error) {
84 ref, series, err := charm.ParseReference(url)
85 if err != nil {
86 return nil, err
87 }
88 if series == "" {
89 return &charm.URL{Reference: ref, Series: DefaultSeries}, nil
90 }
91 return &charm.URL{Reference: ref, Series: series}, nil
92}
93
77func (s *Server) serveInfo(w http.ResponseWriter, r *http.Request) {94func (s *Server) serveInfo(w http.ResponseWriter, r *http.Request) {
78 if r.URL.Path != "/charm-info" {95 if r.URL.Path != "/charm-info" {
79 w.WriteHeader(http.StatusNotFound)96 w.WriteHeader(http.StatusNotFound)
@@ -84,7 +101,7 @@
84 for _, url := range r.Form["charms"] {101 for _, url := range r.Form["charms"] {
85 c := &charm.InfoResponse{}102 c := &charm.InfoResponse{}
86 response[url] = c103 response[url] = c
87 curl, err := charm.ParseURL(url)104 curl, err := s.resolveURL(url)
88 var info *CharmInfo105 var info *CharmInfo
89 if err == nil {106 if err == nil {
90 info, err = s.store.CharmInfo(curl)107 info, err = s.store.CharmInfo(curl)
@@ -92,6 +109,7 @@
92 var skey []string109 var skey []string
93 if err == nil {110 if err == nil {
94 skey = charmStatsKey(curl, "charm-info")111 skey = charmStatsKey(curl, "charm-info")
112 c.CanonicalURL = curl.String()
95 c.Sha256 = info.BundleSha256()113 c.Sha256 = info.BundleSha256()
96 c.Revision = info.Revision()114 c.Revision = info.Revision()
97 c.Digest = info.Digest()115 c.Digest = info.Digest()
@@ -132,7 +150,7 @@
132 }150 }
133 c := &charm.EventResponse{}151 c := &charm.EventResponse{}
134 response[url] = c152 response[url] = c
135 curl, err := charm.ParseURL(url)153 curl, err := s.resolveURL(url)
136 var event *CharmEvent154 var event *CharmEvent
137 if err == nil {155 if err == nil {
138 event, err = s.store.CharmEvent(curl, digest)156 event, err = s.store.CharmEvent(curl, digest)
@@ -169,7 +187,7 @@
169 if !strings.HasPrefix(r.URL.Path, "/charm/") {187 if !strings.HasPrefix(r.URL.Path, "/charm/") {
170 panic("serveCharm: bad url")188 panic("serveCharm: bad url")
171 }189 }
172 curl, err := charm.ParseURL("cs:" + r.URL.Path[len("/charm/"):])190 curl, err := s.resolveURL("cs:" + r.URL.Path[len("/charm/"):])
173 if err != nil {191 if err != nil {
174 w.WriteHeader(http.StatusNotFound)192 w.WriteHeader(http.StatusNotFound)
175 return193 return
176194
=== modified file 'store/server_test.go'
--- store/server_test.go 2013-11-12 14:39:29 +0000
+++ store/server_test.go 2014-03-20 19:57:15 +0000
@@ -21,7 +21,7 @@
21)21)
2222
23func (s *StoreSuite) prepareServer(c *gc.C) (*store.Server, *charm.URL) {23func (s *StoreSuite) prepareServer(c *gc.C) (*store.Server, *charm.URL) {
24 curl := charm.MustParseURL("cs:oneiric/wordpress")24 curl := charm.MustParseURL("cs:precise/wordpress")
25 pub, err := s.store.CharmPublisher([]*charm.URL{curl}, "some-digest")25 pub, err := s.store.CharmPublisher([]*charm.URL{curl}, "some-digest")
26 c.Assert(err, gc.IsNil)26 c.Assert(err, gc.IsNil)
27 err = pub.Publish(&FakeCharmDir{})27 err = pub.Publish(&FakeCharmDir{})
@@ -37,10 +37,12 @@
37 req, err := http.NewRequest("GET", "/charm-info", nil)37 req, err := http.NewRequest("GET", "/charm-info", nil)
38 c.Assert(err, gc.IsNil)38 c.Assert(err, gc.IsNil)
3939
40 var tests = []struct{ url, sha, digest, err string }{40 var tests = []struct{ url, canonical, sha, digest, err string }{
41 {curl.String(), fakeRevZeroSha, "some-digest", ""},41 {curl.String(), curl.String(), fakeRevZeroSha, "some-digest", ""},
42 {"cs:oneiric/non-existent", "", "", "entry not found"},42 {"cs:oneiric/non-existent", "", "", "", "entry not found"},
43 {"cs:bad", "", "", `charm URL without series: "cs:bad"`},43 {"cs:wordpress", curl.String(), fakeRevZeroSha, "some-digest", ""},
44 {"cs:/bad", "", "", "", `charm URL has invalid series: "cs:/bad"`},
45 {"gopher:archie-server", "", "", "", `charm URL has invalid schema: "gopher:archie-server"`},
44 }46 }
4547
46 for _, t := range tests {48 for _, t := range tests {
@@ -51,9 +53,10 @@
51 expected := make(map[string]interface{})53 expected := make(map[string]interface{})
52 if t.sha != "" {54 if t.sha != "" {
53 expected[t.url] = map[string]interface{}{55 expected[t.url] = map[string]interface{}{
54 "revision": float64(0),56 "canonical-url": t.canonical,
55 "sha256": t.sha,57 "revision": float64(0),
56 "digest": t.digest,58 "sha256": t.sha,
59 "digest": t.digest,
57 }60 }
58 } else {61 } else {
59 expected[t.url] = map[string]interface{}{62 expected[t.url] = map[string]interface{}{
@@ -64,11 +67,12 @@
64 obtained := map[string]interface{}{}67 obtained := map[string]interface{}{}
65 err = json.NewDecoder(rec.Body).Decode(&obtained)68 err = json.NewDecoder(rec.Body).Decode(&obtained)
66 c.Assert(err, gc.IsNil)69 c.Assert(err, gc.IsNil)
67 c.Assert(obtained, gc.DeepEquals, expected)70 c.Assert(obtained, gc.DeepEquals, expected, gc.Commentf("URL: %s", t.url))
68 c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "application/json")71 c.Assert(rec.Header().Get("Content-Type"), gc.Equals, "application/json")
69 }72 }
7073
71 s.checkCounterSum(c, []string{"charm-info", curl.Series, curl.Name}, false, 1)74 // 2 charm-info events, one for resolved URL, one for the reference.
75 s.checkCounterSum(c, []string{"charm-info", curl.Series, curl.Name}, false, 2)
72 s.checkCounterSum(c, []string{"charm-missing", "oneiric", "non-existent"}, false, 1)76 s.checkCounterSum(c, []string{"charm-missing", "oneiric", "non-existent"}, false, 1)
73}77}
7478
7579
=== modified file 'testing/charm.go'
--- testing/charm.go 2014-03-07 13:56:22 +0000
+++ testing/charm.go 2014-03-20 19:57:15 +0000
@@ -96,10 +96,12 @@
96 }96 }
97 clone(dst, r.DirPath(name))97 clone(dst, r.DirPath(name))
98 return &charm.URL{98 return &charm.URL{
99 Schema: "local",99 Reference: charm.Reference{
100 Series: series,100 Schema: "local",
101 Name: name,101 Name: name,
102 Revision: -1,102 Revision: -1,
103 },
104 Series: series,
103 }105 }
104}106}
105107
@@ -126,9 +128,10 @@
126// MockCharmStore implements charm.Repository and is used to isolate tests128// MockCharmStore implements charm.Repository and is used to isolate tests
127// that would otherwise need to hit the real charm store.129// that would otherwise need to hit the real charm store.
128type MockCharmStore struct {130type MockCharmStore struct {
129 charms map[string]map[int]*charm.Bundle131 charms map[string]map[int]*charm.Bundle
130 AuthAttrs string132 AuthAttrs string
131 TestMode bool133 TestMode bool
134 DefaultSeries string
132}135}
133136
134func NewMockCharmStore() *MockCharmStore {137func NewMockCharmStore() *MockCharmStore {
@@ -145,6 +148,18 @@
145 return s148 return s
146}149}
147150
151func (s *MockCharmStore) WithDefaultSeries(series string) charm.Repository {
152 s.DefaultSeries = series
153 return s
154}
155
156func (s *MockCharmStore) Resolve(ref charm.Reference) (*charm.URL, error) {
157 if s.DefaultSeries == "" {
158 return nil, fmt.Errorf("missing default series, cannot resolve charm url: %q", ref)
159 }
160 return &charm.URL{Reference: ref, Series: s.DefaultSeries}, nil
161}
162
148// SetCharm adds and removes charms in s. The affected charm is identified by163// SetCharm adds and removes charms in s. The affected charm is identified by
149// charmURL, which must be revisioned. If bundle is nil, the charm will be164// charmURL, which must be revisioned. If bundle is nil, the charm will be
150// removed; otherwise, it will be stored. It is an error to store a bundle165// 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: