Merge lp:~dimitern/juju-core/260-lp-1067979-duplicate-charm into lp:~go-bot/juju-core/trunk

Proposed by Dimiter Naydenov
Status: Merged
Approved by: Dimiter Naydenov
Approved revision: no longer in the source branch.
Merged at revision: 2281
Proposed branch: lp:~dimitern/juju-core/260-lp-1067979-duplicate-charm
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 1365 lines (+560/-190)
21 files modified
charm/repo.go (+2/-9)
charm/testing/mockstore.go (+12/-9)
environs/sync/sync.go (+6/-12)
environs/testing/storage.go (+17/-0)
environs/tools/testing/testing.go (+4/-9)
juju/conn.go (+1/-6)
juju/conn_test.go (+1/-1)
juju/testing/conn.go (+1/-1)
juju/testing/repo.go (+2/-7)
state/apiserver/charms.go (+2/-17)
state/apiserver/charms_test.go (+11/-16)
state/apiserver/client/client.go (+37/-13)
state/apiserver/client/client_test.go (+128/-14)
state/apiserver/export_test.go (+3/-4)
state/state.go (+144/-21)
state/state_test.go (+95/-28)
testing/charm.go (+19/-9)
utils/trivial.go (+26/-0)
utils/trivial_test.go (+46/-2)
worker/uniter/charm/charm.go (+2/-6)
worker/uniter/uniter_test.go (+1/-6)
To merge this branch: bzr merge lp:~dimitern/juju-core/260-lp-1067979-duplicate-charm
Reviewer Review Type Date Requested Status
Dimiter Naydenov (community) Approve
Review via email: mp+203291@code.launchpad.net

Commit message

state;apiserver: Fix a race - lp bug #1067979

This introduces some changes to how charm store
charms are added through the API (in state and
to provider storage). Now PrepareStoreCharmUpload
is called before trying to download the charm,
repackage it and upload it to storage, in order
to reserve a charm URL in state with pending
state. Added a test that demonstrates multiple
concurrent deployments of the same charm does
not cause the race issues, like mentioned in
the bug.

A few drive-by fixes brought up during review:
* Added ReadSHA256 and ReadFileSHA256 helpers in
  utils, and changed most places where hashes
  are calculated to use them.
* Charms are now uploaded to storage with a
  randomly generated archive names with the
  format "<charm-name>-<revision>-<uuid>".
  This allows multiple concurrent uploads
  to happen safely, and at the end AddCharm
  in the API checks to see if the charm info
  is already updated in state and if so, deletes
  the duplicated upload.
* Added GetEnvironStorage helper to environs/testing.
* Fixed potential compatibility issues with older
  versions and the recently added PendingUpload
  and Placeholder fields of the charm document.

Also tested multiple concurrent deployments with
the local provider manually and updated the bug
accordingly.

https://codereview.appspot.com/53210044/

R=fwereade

Description of the change

state;apiserver: Fix a race - lp bug #1067979

This introduces some changes to how charm store
charms are added through the API (in state and
to provider storage). Now PrepareStoreCharmUpload
is called before trying to download the charm,
repackage it and upload it to storage, in order
to reserve a charm URL in state with pending
state. Added a test that demonstrates multiple
concurrent deployments of the same charm does
not cause the race issues, like mentioned in
the bug.

A few drive-by fixes brought up during review:
* Added ReadSHA256 and ReadFileSHA256 helpers in
  utils, and changed most places where hashes
  are calculated to use them.
* Charms are now uploaded to storage with a
  randomly generated archive names with the
  format "<charm-name>-<revision>-<uuid>".
  This allows multiple concurrent uploads
  to happen safely, and at the end AddCharm
  in the API checks to see if the charm info
  is already updated in state and if so, deletes
  the duplicated upload.
* Added GetEnvironStorage helper to environs/testing.
* Fixed potential compatibility issues with older
  versions and the recently added PendingUpload
  and Placeholder fields of the charm document.

Also tested multiple concurrent deployments with
the local provider manually and updated the bug
accordingly.

https://codereview.appspot.com/53210044/

To post a comment you must log in.
Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Reviewers: mp+203291_code.launchpad.net,

Message:
Please take a look.

Description:
state;apiserver: Fix a race - lp bug #1067979

This introduces some changes to how charm store
charms are added through the API (in state and
to provider storage). Now PrepareStoreCharmUpload
is called before trying to download the charm,
repackage it and upload it to storage, in order
to reserve a charm URL in state with pending
state. Added a test that demonstrates multiple
concurrent deployments of the same charm does
not cause the race issues, like mentioned in
the bug.

Also tested manually with the local provider
and updated the bug accordingly.

https://code.launchpad.net/~dimitern/juju-core/260-lp-1067979-duplicate-charm/+merge/203291

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/53210044/

Affected files (+236, -54 lines):
   A [revision details]
   M state/apiserver/charms_test.go
   M state/apiserver/client/client.go
   M state/apiserver/client/client_test.go
   M state/state.go
   M state/state_test.go

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

A few comments, reprising what we discussed at lunch

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

https://codereview.appspot.com/53210044/diff/1/state/apiserver/client/client.go#newcode885
state/apiserver/client/client.go:885: storageURL, err :=
storage.URL(name)
I'd prefer to generate unique names than to potentially have multiple
writers to the same storage location. I think it'd be best to have
multiple independent downloads, and multiple independent uploads, and
then just to have a single worker win the race and set a URL that
definitely worked in state. No need to rename or anything -- as far as
I'm aware, charm URL is totally independent of storage URL anyway, and
if it's not that's a bug that should be fixed anyway ;).

https://codereview.appspot.com/53210044/diff/1/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (right):

https://codereview.appspot.com/53210044/diff/1/state/apiserver/client/client_test.go#newcode1850
state/apiserver/client/client_test.go:1850: }
in light of the previous comment,we *do* want multiple charms uploaded
(but all except one to be deleted). And whichever one isn't deleted
should match the archive url of the charm in state.

https://codereview.appspot.com/53210044/

Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Please take a look.

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

https://codereview.appspot.com/53210044/diff/1/state/apiserver/client/client.go#newcode885
state/apiserver/client/client.go:885: storageURL, err :=
storage.URL(name)
On 2014/01/27 12:46:16, fwereade wrote:
> I'd prefer to generate unique names than to potentially have multiple
writers to
> the same storage location. I think it'd be best to have multiple
independent
> downloads, and multiple independent uploads, and then just to have a
single
> worker win the race and set a URL that definitely worked in state. No
need to
> rename or anything -- as far as I'm aware, charm URL is totally
independent of
> storage URL anyway, and if it's not that's a bug that should be fixed
anyway ;).

I'll add code to generate a storage URL based on a random UUID string.
If you don't mind, I'll also follow John's suggestion to include the
charm name and revision as prefix, to make the string a bit less
cryptic.

https://codereview.appspot.com/53210044/diff/1/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (right):

https://codereview.appspot.com/53210044/diff/1/state/apiserver/client/client_test.go#newcode1850
state/apiserver/client/client_test.go:1850: }
On 2014/01/27 12:46:16, fwereade wrote:
> in light of the previous comment,we *do* want multiple charms uploaded
(but all
> except one to be deleted). And whichever one isn't deleted should
match the
> archive url of the charm in state.

Will change accordingly.

https://codereview.appspot.com/53210044/

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

few more comments, but the heart of it looks solid

https://codereview.appspot.com/53210044/diff/40001/charm/url.go
File charm/url.go (right):

https://codereview.appspot.com/53210044/diff/40001/charm/url.go#newcode261
charm/url.go:261: func (u *URL) ArchiveName() (string, error) {
I'm certain this is not part of the charm package -- np with the code,
but it's very definitely an internal detail of the state server.

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/charms.go
File state/apiserver/charms.go (right):

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/charms.go#newcode243
state/apiserver/charms.go:243: func GetSHA256(source io.Reader) (string,
int64, error) {
whereas this feels maybe utilsy -- and we do this *all over the place*,
so please hunt them down and make them all use the same utils func,
assuming that is practical

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

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/client/client.go#newcode908
state/apiserver/client/client.go:908: return fmt.Errorf("cannot remove
duplicated charm from storage: %v", err)
log it, I think, but don't spoil the user's day -- we still managed to
do what she asked, after all

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (right):

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/client/client_test.go#newcode1821
state/apiserver/client/client_test.go:1821: close(ops)
race? we should expect 9 Deletes as well, I think

https://codereview.appspot.com/53210044/diff/40001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/53210044/diff/40001/state/state.go#newcode606
state/state.go:606: err = st.charms.Find(D{{"_id", curl},
{"placeholder", false}}).One(&uploadedCharm)
placeholder might be false in a new installation, or missing in an old
one

https://codereview.appspot.com/53210044/diff/40001/state/state.go#newcode624
state/state.go:624: Insert: uploadedCharm,
if there *was* a placeholder charm, the insert would fail, wouldn't it?

https://codereview.appspot.com/53210044/diff/40001/state/state.go#newcode642
state/state.go:642: StillPlaceholder = D{{"placeholder", true}}
I think these should probably not be exported

https://codereview.appspot.com/53210044/

Revision history for this message
Dimiter Naydenov (dimitern) wrote :
Download full text (3.3 KiB)

Please take a look.

https://codereview.appspot.com/53210044/diff/40001/charm/url.go
File charm/url.go (right):

https://codereview.appspot.com/53210044/diff/40001/charm/url.go#newcode261
charm/url.go:261: func (u *URL) ArchiveName() (string, error) {
On 2014/01/28 08:19:41, fwereade wrote:
> I'm certain this is not part of the charm package -- np with the code,
but it's
> very definitely an internal detail of the state server.

I'll move it to the state package then.

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/charms.go
File state/apiserver/charms.go (right):

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/charms.go#newcode243
state/apiserver/charms.go:243: func GetSHA256(source io.Reader) (string,
int64, error) {
On 2014/01/28 08:19:41, fwereade wrote:
> whereas this feels maybe utilsy -- and we do this *all over the
place*, so
> please hunt them down and make them all use the same utils func,
assuming that
> is practical

Done.

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

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/client/client.go#newcode908
state/apiserver/client/client.go:908: return fmt.Errorf("cannot remove
duplicated charm from storage: %v", err)
On 2014/01/28 08:19:41, fwereade wrote:
> log it, I think, but don't spoil the user's day -- we still managed to
do what
> she asked, after all

Wasn't sure whether to log an error or warning. I'll just log it as an
error then, because it seems serious enough (it shouldn't happen
usually).

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (right):

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/client/client_test.go#newcode1821
state/apiserver/client/client_test.go:1821: close(ops)
On 2014/01/28 08:19:41, fwereade wrote:
> race? we should expect 9 Deletes as well, I think

There is no way to detect storage removals with the dummy provider I
could find. But I am checking below that we do have a single upload at
the end. Not sure what race do you mean?

https://codereview.appspot.com/53210044/diff/40001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/53210044/diff/40001/state/state.go#newcode606
state/state.go:606: err = st.charms.Find(D{{"_id", curl},
{"placeholder", false}}).One(&uploadedCharm)
On 2014/01/28 08:19:41, fwereade wrote:
> placeholder might be false in a new installation, or missing in an old
one

I'll modify the condition, but this applies to other places in the code
as well.

https://codereview.appspot.com/53210044/diff/40001/state/state.go#newcode624
state/state.go:624: Insert: uploadedCharm,
On 2014/01/28 08:19:41, fwereade wrote:
> if there *was* a placeholder charm, the insert would fail, wouldn't
it?

Right, but since I changed how we select the document above it shouldn't
be a problem.

https://codereview.appspot.com/53210044/diff/40001/state/state.go#newcode642
state/state.go:642: StillPlaceholder = D{{"placeholder", true}}
On 2014/01/28 08:19:41, fwereade wrote:
> I think these should pr...

Read more...

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

Very nice, last round I think

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (right):

https://codereview.appspot.com/53210044/diff/40001/state/apiserver/client/client_test.go#newcode1821
state/apiserver/client/client_test.go:1821: close(ops)
On 2014/01/28 10:55:05, dimitern wrote:
> On 2014/01/28 08:19:41, fwereade wrote:
> > race? we should expect 9 Deletes as well, I think

> There is no way to detect storage removals with the dummy provider I
could find.
> But I am checking below that we do have a single upload at the end.
Not sure
> what race do you mean?

I was worried that the deletes *were* pushing ops, and that channel
might sometimes be closed before the ops stopped. But yeah, looks like
no delete ops; and I'm fine with the test. Just seemed a bit weird to
mix the testing styles -- but I think we're good as is. Thanks.

https://codereview.appspot.com/53210044/diff/60001/state/apiserver/charms.go
File state/apiserver/charms.go (right):

https://codereview.appspot.com/53210044/diff/60001/state/apiserver/charms.go#newcode229
state/apiserver/charms.go:229: func GetEnvironStorage(st *state.State)
(storage.Storage, error) {
maybe put this in export_test rather than growing the interface here?

https://codereview.appspot.com/53210044/diff/60001/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (right):

https://codereview.appspot.com/53210044/diff/60001/state/apiserver/client/client_test.go#newcode1828
state/apiserver/client/client_test.go:1828: storage, err :=
apiserver.GetEnvironStorage(s.State)
ah, ok, you can't put it in export_test. Hmm. I think I'd maybe prefer
it if this were accessed via the environ on the test, but I'm not sure
of the tradeoffs -- I'll leave it to your judgment.

https://codereview.appspot.com/53210044/diff/60001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/53210044/diff/60001/state/state.go#newcode480
state/state.go:480: what := D{{"_id", curl}, {"placeholder", false},
{"pendingupload", false}}
Yeah, these bits do need to be fixed. Sorry, but they *will* break
stuff.

https://codereview.appspot.com/53210044/diff/60001/state/state.go#newcode625
state/state.go:625: Insert: uploadedCharm,
Don't we still need to deal with a placeholder here?

https://codereview.appspot.com/53210044/diff/60001/utils/trivial.go
File utils/trivial.go (right):

https://codereview.appspot.com/53210044/diff/60001/utils/trivial.go#newcode127
utils/trivial.go:127: func GetSHA256(source io.Reader) (string, int64,
error) {
ReadSHA256?

https://codereview.appspot.com/53210044/diff/60001/utils/trivial.go#newcode139
utils/trivial.go:139: func GetFileSHA256(filename string) (string,
int64, error) {
ReadFileSHA256?

https://codereview.appspot.com/53210044/diff/60001/utils/trivial.go#newcode146
utils/trivial.go:146: }
Also, please test these. Not quite trivial enough to be skippable IMO.

https://codereview.appspot.com/53210044/

Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Please take a look.

https://codereview.appspot.com/53210044/diff/60001/state/apiserver/charms.go
File state/apiserver/charms.go (right):

https://codereview.appspot.com/53210044/diff/60001/state/apiserver/charms.go#newcode229
state/apiserver/charms.go:229: func GetEnvironStorage(st *state.State)
(storage.Storage, error) {
On 2014/01/28 12:00:32, fwereade wrote:
> maybe put this in export_test rather than growing the interface here?

It was there, but I needed it in two places. I'll put it in
environs/testing.

https://codereview.appspot.com/53210044/diff/60001/state/apiserver/client/client_test.go
File state/apiserver/client/client_test.go (right):

https://codereview.appspot.com/53210044/diff/60001/state/apiserver/client/client_test.go#newcode1828
state/apiserver/client/client_test.go:1828: storage, err :=
apiserver.GetEnvironStorage(s.State)
On 2014/01/28 12:00:32, fwereade wrote:
> ah, ok, you can't put it in export_test. Hmm. I think I'd maybe prefer
it if
> this were accessed via the environ on the test, but I'm not sure of
the
> tradeoffs -- I'll leave it to your judgment.

I'll put it in environs/testing.

https://codereview.appspot.com/53210044/diff/60001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/53210044/diff/60001/state/state.go#newcode480
state/state.go:480: what := D{{"_id", curl}, {"placeholder", false},
{"pendingupload", false}}
On 2014/01/28 12:00:32, fwereade wrote:
> Yeah, these bits do need to be fixed. Sorry, but they *will* break
stuff.

Done.

https://codereview.appspot.com/53210044/diff/60001/state/state.go#newcode625
state/state.go:625: Insert: uploadedCharm,
On 2014/01/28 12:00:32, fwereade wrote:
> Don't we still need to deal with a placeholder here?

If we reach this point we know it's not a placeholder and there's no
document for this charm URL. Not setting Placeholder: false here works,
because it's the default value. So it's OK I think.

https://codereview.appspot.com/53210044/diff/60001/utils/trivial.go
File utils/trivial.go (right):

https://codereview.appspot.com/53210044/diff/60001/utils/trivial.go#newcode127
utils/trivial.go:127: func GetSHA256(source io.Reader) (string, int64,
error) {
On 2014/01/28 12:00:32, fwereade wrote:
> ReadSHA256?

Done.

https://codereview.appspot.com/53210044/diff/60001/utils/trivial.go#newcode139
utils/trivial.go:139: func GetFileSHA256(filename string) (string,
int64, error) {
On 2014/01/28 12:00:32, fwereade wrote:
> ReadFileSHA256?

Done.

https://codereview.appspot.com/53210044/diff/60001/utils/trivial.go#newcode146
utils/trivial.go:146: }
On 2014/01/28 12:00:32, fwereade wrote:
> Also, please test these. Not quite trivial enough to be skippable IMO.

Done.

https://codereview.appspot.com/53210044/

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

I still think there's something up with PrepareStoreCharmUpload, but
it's getting late and I may be being dense. Ping me tomorrow morning?

https://codereview.appspot.com/53210044/diff/60001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/53210044/diff/60001/state/state.go#newcode625
state/state.go:625: Insert: uploadedCharm,
On 2014/01/28 17:15:02, dimitern wrote:
> On 2014/01/28 12:00:32, fwereade wrote:
> > Don't we still need to deal with a placeholder here?

> If we reach this point we know it's not a placeholder and there's no
document
> for this charm URL. Not setting Placeholder: false here works, because
it's the
> default value. So it's OK I think.

We will still have fallen through the block above if err is nil but
placeholder is true. Right? I had to think about this harder than I'd
like, which might indicate that we could make that block clearer, but
nothing really springs to mind very obviously... perhaps a switch?

ops := []txn.Op{}
switch err {
case mgo.ErrNotFound:
     ops = append(ops, <insert op>)
case nil:
     if uploadedCharm.Placeholder {
         ops = append(ops, <update op>)
     } else {
         return newCharm(st, &uploadedCharm)
     }
default:
     return nil, err
}

..? Maybe not very compelling?

This might be a case where really pedantic SetTransactionHooks-style
testing would be a boon -- even if the logic is hard to follow we can
just churn through the possible test cases and have some more certainty.

https://codereview.appspot.com/53210044/

Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Please take a look.

https://codereview.appspot.com/53210044/diff/60001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/53210044/diff/60001/state/state.go#newcode625
state/state.go:625: Insert: uploadedCharm,
On 2014/01/28 23:09:31, fwereade wrote:
> On 2014/01/28 17:15:02, dimitern wrote:
> > On 2014/01/28 12:00:32, fwereade wrote:
> > > Don't we still need to deal with a placeholder here?
> >
> > If we reach this point we know it's not a placeholder and there's no
document
> > for this charm URL. Not setting Placeholder: false here works,
because it's
> the
> > default value. So it's OK I think.

> We will still have fallen through the block above if err is nil but
placeholder
> is true. Right? I had to think about this harder than I'd like, which
might
> indicate that we could make that block clearer, but nothing really
springs to
> mind very obviously... perhaps a switch?

> ops := []txn.Op{}
> switch err {
> case mgo.ErrNotFound:
> ops = append(ops, <insert op>)
> case nil:
> if uploadedCharm.Placeholder {
> ops = append(ops, <update op>)
> } else {
> return newCharm(st, &uploadedCharm)
> }
> default:
> return nil, err
> }

> ..? Maybe not very compelling?

> This might be a case where really pedantic SetTransactionHooks-style
testing
> would be a boon -- even if the logic is hard to follow we can just
churn through
> the possible test cases and have some more certainty.

I added a specific test case to cover "uploading a store charm when a
placeholder exists", and indeed it uncovered issues, which I fixed now.
Thanks!

As for the transaction hooks, I can't use them unfortunately outside the
state packages (they're all defined in export_test). But despite that, I
think the code looks solid now.

https://codereview.appspot.com/53210044/

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

Most of these are genuinely trivial now -- if we're in agreement on them
all, LGTM, but ping me if there's any uncertainty

https://codereview.appspot.com/53210044/diff/100001/state/charm.go
File state/charm.go (right):

https://codereview.appspot.com/53210044/diff/100001/state/charm.go#newcode87
state/charm.go:87: func CharmArchiveName(name string, revision int)
(string, error) {
quibble quibble whine, I think this probably belongs in apiserver/client

https://codereview.appspot.com/53210044/diff/100001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/53210044/diff/100001/state/state.go#newcode633
state/state.go:633: Assert: txn.DocExists,
hmm, should be be asserting that it hasn't had archiveurl (etc) filled
in? I think there's a very very cornery corner case in which we read
while it's still a placeholder, but write when it's already uploaded.

That'd be a good transaction-hooks test ;).

https://codereview.appspot.com/53210044/diff/100001/state/state.go#newcode661
state/state.go:661: if err != nil {
this makes me twitch a little -- AFAICT the only possible value is
txn.ErrAborted, but that assumption feels very vulnerable to code
perturbations in the loop above. Maybe check for err == nil inside the
loop, and return newCharm() there, so that ErrAborted becomes the *only*
way to fall out of the loop, and just always return
ErrExcessiveContention?

https://codereview.appspot.com/53210044/diff/100001/utils/trivial_test.go
File utils/trivial_test.go (right):

https://codereview.appspot.com/53210044/diff/100001/utils/trivial_test.go#newcode87
utils/trivial_test.go:87: {
}{{
     ...
},{
     ...

to compress both vertical and horizontal space?

And, hey, can we please just move these inside the test func? If they're
not used elsewhere they really don't deserve to be global IMO.

https://codereview.appspot.com/53210044/diff/100001/utils/trivial_test.go#newcode105
utils/trivial_test.go:105: func (utilsSuite)
TestReadSHA256AndReadFileSHA256(c *gc.C) {
*utilsSuite, please, across the board, lest someone add methods that
require pointer receivers and silently deactivate all the other tests.

https://codereview.appspot.com/53210044/

Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Please take a look.

https://codereview.appspot.com/53210044/diff/100001/state/charm.go
File state/charm.go (right):

https://codereview.appspot.com/53210044/diff/100001/state/charm.go#newcode87
state/charm.go:87: func CharmArchiveName(name string, revision int)
(string, error) {
On 2014/01/29 16:01:22, fwereade wrote:
> quibble quibble whine, I think this probably belongs in
apiserver/client

Done.

https://codereview.appspot.com/53210044/diff/100001/state/state.go
File state/state.go (right):

https://codereview.appspot.com/53210044/diff/100001/state/state.go#newcode633
state/state.go:633: Assert: txn.DocExists,
On 2014/01/29 16:01:22, fwereade wrote:
> hmm, should be be asserting that it hasn't had archiveurl (etc) filled
in? I
> think there's a very very cornery corner case in which we read while
it's still
> a placeholder, but write when it's already uploaded.

> That'd be a good transaction-hooks test ;).

Done.

https://codereview.appspot.com/53210044/diff/100001/state/state.go#newcode661
state/state.go:661: if err != nil {
On 2014/01/29 16:01:22, fwereade wrote:
> this makes me twitch a little -- AFAICT the only possible value is
> txn.ErrAborted, but that assumption feels very vulnerable to code
perturbations
> in the loop above. Maybe check for err == nil inside the loop, and
return
> newCharm() there, so that ErrAborted becomes the *only* way to fall
out of the
> loop, and just always return ErrExcessiveContention?

Done.

https://codereview.appspot.com/53210044/diff/100001/utils/trivial_test.go
File utils/trivial_test.go (right):

https://codereview.appspot.com/53210044/diff/100001/utils/trivial_test.go#newcode87
utils/trivial_test.go:87: {
On 2014/01/29 16:01:22, fwereade wrote:
> }{{
> ...
> },{
> ...

> to compress both vertical and horizontal space?

> And, hey, can we please just move these inside the test func? If
they're not
> used elsewhere they really don't deserve to be global IMO.

Done.

https://codereview.appspot.com/53210044/diff/100001/utils/trivial_test.go#newcode105
utils/trivial_test.go:105: func (utilsSuite)
TestReadSHA256AndReadFileSHA256(c *gc.C) {
On 2014/01/29 16:01:22, fwereade wrote:
> *utilsSuite, please, across the board, lest someone add methods that
require
> pointer receivers and silently deactivate all the other tests.

Done.

https://codereview.appspot.com/53210044/

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (592.0 KiB)

The attempt to merge lp:~dimitern/juju-core/260-lp-1067979-duplicate-charm into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core/agent 1.291s
ok launchpad.net/juju-core/agent/tools 0.305s
ok launchpad.net/juju-core/bzr 7.397s
ok launchpad.net/juju-core/cert 3.282s
ok launchpad.net/juju-core/charm 0.605s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.033s
ok launchpad.net/juju-core/cloudinit/sshinit 1.105s
ok launchpad.net/juju-core/cmd 0.247s
ok launchpad.net/juju-core/cmd/charm-admin 0.867s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 277.574s
ok launchpad.net/juju-core/cmd/jujud 62.207s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 9.802s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/constraints 0.028s
ok launchpad.net/juju-core/container 0.037s
ok launchpad.net/juju-core/container/factory 0.052s
ok launchpad.net/juju-core/container/kvm 0.249s
ok launchpad.net/juju-core/container/kvm/mock 0.036s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 0.422s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.263s
ok launchpad.net/juju-core/environs 3.143s
ok launchpad.net/juju-core/environs/bootstrap 5.124s
ok launchpad.net/juju-core/environs/cloudinit 0.736s
ok launchpad.net/juju-core/environs/config 3.309s
ok launchpad.net/juju-core/environs/configstore 0.056s
ok launchpad.net/juju-core/environs/filestorage 0.035s
ok launchpad.net/juju-core/environs/httpstorage 0.864s
ok launchpad.net/juju-core/environs/imagemetadata 0.652s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.063s
ok launchpad.net/juju-core/environs/jujutest 0.216s
ok launchpad.net/juju-core/environs/manual 8.488s
ok launchpad.net/juju-core/environs/simplestreams 0.401s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 1.387s
ok launchpad.net/juju-core/environs/storage 1.199s
ok launchpad.net/juju-core/environs/sync 35.792s
ok launchpad.net/juju-core/environs/testing 0.217s
ok launchpad.net/juju-core/environs/tools 6.906s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.016s
ok launchpad.net/juju-core/instance 0.023s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 24.611s
ok launchpad.net/juju-core/juju/osenv 0.021s
? launchpad.net/juju-core/juju/testing [no test files]
ok launchpad.net/juju-core/log 0.016s
ok launchpad.net/juju-core/log/syslog 0.025s
ok launchpad.net/juju-core/names 0.028s
? launchpad....

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (591.7 KiB)

The attempt to merge lp:~dimitern/juju-core/260-lp-1067979-duplicate-charm into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core/agent 0.855s
ok launchpad.net/juju-core/agent/tools 0.243s
ok launchpad.net/juju-core/bzr 7.603s
ok launchpad.net/juju-core/cert 3.491s
ok launchpad.net/juju-core/charm 0.600s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.033s
ok launchpad.net/juju-core/cloudinit/sshinit 1.085s
ok launchpad.net/juju-core/cmd 0.213s
ok launchpad.net/juju-core/cmd/charm-admin 0.864s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 238.171s
ok launchpad.net/juju-core/cmd/jujud 62.620s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 11.807s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/constraints 0.030s
ok launchpad.net/juju-core/container 0.037s
ok launchpad.net/juju-core/container/factory 0.052s
ok launchpad.net/juju-core/container/kvm 0.295s
ok launchpad.net/juju-core/container/kvm/mock 0.052s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 0.445s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.387s
ok launchpad.net/juju-core/environs 3.076s
ok launchpad.net/juju-core/environs/bootstrap 4.712s
ok launchpad.net/juju-core/environs/cloudinit 0.631s
ok launchpad.net/juju-core/environs/config 3.180s
ok launchpad.net/juju-core/environs/configstore 0.040s
ok launchpad.net/juju-core/environs/filestorage 0.034s
ok launchpad.net/juju-core/environs/httpstorage 0.956s
ok launchpad.net/juju-core/environs/imagemetadata 0.601s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.061s
ok launchpad.net/juju-core/environs/jujutest 0.229s
ok launchpad.net/juju-core/environs/manual 9.763s
ok launchpad.net/juju-core/environs/simplestreams 0.367s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 1.223s
ok launchpad.net/juju-core/environs/storage 1.177s
ok launchpad.net/juju-core/environs/sync 34.614s
ok launchpad.net/juju-core/environs/testing 0.214s
ok launchpad.net/juju-core/environs/tools 6.831s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.016s
ok launchpad.net/juju-core/instance 0.024s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 24.499s
ok launchpad.net/juju-core/juju/osenv 0.022s
? launchpad.net/juju-core/juju/testing [no test files]
ok launchpad.net/juju-core/log 0.016s
ok launchpad.net/juju-core/log/syslog 0.025s
ok launchpad.net/juju-core/names 0.027s
? launchpad...

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (675.3 KiB)

The attempt to merge lp:~dimitern/juju-core/260-lp-1067979-duplicate-charm into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core/agent 1.313s
ok launchpad.net/juju-core/agent/tools 0.253s
ok launchpad.net/juju-core/bzr 7.768s
ok launchpad.net/juju-core/cert 3.751s
ok launchpad.net/juju-core/charm 0.598s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.032s
ok launchpad.net/juju-core/cloudinit/sshinit 1.137s
ok launchpad.net/juju-core/cmd 0.243s
ok launchpad.net/juju-core/cmd/charm-admin 0.817s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 258.730s
ok launchpad.net/juju-core/cmd/jujud 62.132s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 9.465s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/constraints 0.028s
ok launchpad.net/juju-core/container 0.038s
ok launchpad.net/juju-core/container/factory 0.055s
ok launchpad.net/juju-core/container/kvm 0.342s
ok launchpad.net/juju-core/container/kvm/mock 0.056s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 0.421s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.320s
ok launchpad.net/juju-core/environs 3.263s
ok launchpad.net/juju-core/environs/bootstrap 4.696s
ok launchpad.net/juju-core/environs/cloudinit 0.676s
ok launchpad.net/juju-core/environs/config 4.070s
ok launchpad.net/juju-core/environs/configstore 0.080s
ok launchpad.net/juju-core/environs/filestorage 0.034s
ok launchpad.net/juju-core/environs/httpstorage 0.908s
ok launchpad.net/juju-core/environs/imagemetadata 0.695s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.063s
ok launchpad.net/juju-core/environs/jujutest 0.257s
ok launchpad.net/juju-core/environs/manual 9.759s
ok launchpad.net/juju-core/environs/simplestreams 0.355s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 1.335s
ok launchpad.net/juju-core/environs/storage 1.165s
ok launchpad.net/juju-core/environs/sync 35.200s
ok launchpad.net/juju-core/environs/testing 0.209s
ok launchpad.net/juju-core/environs/tools 6.839s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.016s
ok launchpad.net/juju-core/instance 0.024s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 21.534s
ok launchpad.net/juju-core/juju/osenv 0.020s
? launchpad.net/juju-core/juju/testing [no test files]
ok launchpad.net/juju-core/log 0.015s
ok launchpad.net/juju-core/log/syslog 0.022s
ok launchpad.net/juju-core/names 0.027s
? launchpad....

Revision history for this message
Go Bot (go-bot) wrote :
Download full text (672.8 KiB)

The attempt to merge lp:~dimitern/juju-core/260-lp-1067979-duplicate-charm into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core/agent 1.241s
ok launchpad.net/juju-core/agent/tools 0.265s
ok launchpad.net/juju-core/bzr 7.509s
ok launchpad.net/juju-core/cert 3.259s
ok launchpad.net/juju-core/charm 0.659s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.033s
ok launchpad.net/juju-core/cloudinit/sshinit 1.063s
ok launchpad.net/juju-core/cmd 0.231s
ok launchpad.net/juju-core/cmd/charm-admin 0.827s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 257.042s
ok launchpad.net/juju-core/cmd/jujud 63.020s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 13.084s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/constraints 0.029s
ok launchpad.net/juju-core/container 0.043s
ok launchpad.net/juju-core/container/factory 0.074s
ok launchpad.net/juju-core/container/kvm 0.257s
ok launchpad.net/juju-core/container/kvm/mock 0.036s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 0.363s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.281s
ok launchpad.net/juju-core/environs 3.259s
ok launchpad.net/juju-core/environs/bootstrap 4.443s
ok launchpad.net/juju-core/environs/cloudinit 0.672s
ok launchpad.net/juju-core/environs/config 2.722s
ok launchpad.net/juju-core/environs/configstore 0.049s
ok launchpad.net/juju-core/environs/filestorage 0.032s
ok launchpad.net/juju-core/environs/httpstorage 1.023s
ok launchpad.net/juju-core/environs/imagemetadata 0.685s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.070s
ok launchpad.net/juju-core/environs/jujutest 0.249s
ok launchpad.net/juju-core/environs/manual 8.911s
ok launchpad.net/juju-core/environs/simplestreams 0.359s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 1.247s
ok launchpad.net/juju-core/environs/storage 1.211s
ok launchpad.net/juju-core/environs/sync 34.656s
ok launchpad.net/juju-core/environs/testing 0.203s
ok launchpad.net/juju-core/environs/tools 6.884s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.016s
ok launchpad.net/juju-core/instance 0.023s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 25.459s
ok launchpad.net/juju-core/juju/osenv 0.020s
? launchpad.net/juju-core/juju/testing [no test files]
ok launchpad.net/juju-core/log 0.015s
ok launchpad.net/juju-core/log/syslog 0.035s
ok launchpad.net/juju-core/names 0.027s
? launchpad...

Revision history for this message
Dimiter Naydenov (dimitern) wrote :
Revision history for this message
Dimiter Naydenov (dimitern) wrote :

Self-approving after fixing uniter tests.

review: Approve
Revision history for this message
Go Bot (go-bot) wrote :
Download full text (100.5 KiB)

The attempt to merge lp:~dimitern/juju-core/260-lp-1067979-duplicate-charm into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core/agent 1.293s
ok launchpad.net/juju-core/agent/tools 0.253s
ok launchpad.net/juju-core/bzr 7.562s
ok launchpad.net/juju-core/cert 3.083s
ok launchpad.net/juju-core/charm 0.669s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.032s
ok launchpad.net/juju-core/cloudinit/sshinit 1.096s
ok launchpad.net/juju-core/cmd 0.279s
ok launchpad.net/juju-core/cmd/charm-admin 0.867s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 253.577s
*** Test killed: ran too long (10m0s).
FAIL launchpad.net/juju-core/cmd/jujud 600.018s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 13.476s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/constraints 0.048s
ok launchpad.net/juju-core/container 0.055s
ok launchpad.net/juju-core/container/factory 0.061s
ok launchpad.net/juju-core/container/kvm 0.307s
ok launchpad.net/juju-core/container/kvm/mock 0.048s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 0.384s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.287s
ok launchpad.net/juju-core/environs 3.200s
ok launchpad.net/juju-core/environs/bootstrap 4.596s
ok launchpad.net/juju-core/environs/cloudinit 0.660s
ok launchpad.net/juju-core/environs/config 2.831s
ok launchpad.net/juju-core/environs/configstore 0.062s
ok launchpad.net/juju-core/environs/filestorage 0.040s
ok launchpad.net/juju-core/environs/httpstorage 1.054s
ok launchpad.net/juju-core/environs/imagemetadata 0.666s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.080s
ok launchpad.net/juju-core/environs/jujutest 0.303s
ok launchpad.net/juju-core/environs/manual 9.640s
ok launchpad.net/juju-core/environs/simplestreams 0.380s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 2.166s
ok launchpad.net/juju-core/environs/storage 1.244s
ok launchpad.net/juju-core/environs/sync 34.538s
ok launchpad.net/juju-core/environs/testing 0.243s
ok launchpad.net/juju-core/environs/tools 7.067s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.025s
ok launchpad.net/juju-core/instance 0.035s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 27.392s
ok launchpad.net/juju-core/juju/osenv 0.040s
? launchpad.net/juju-core/juju/testing [no test files]
ok launchpad.net/juju-core/log 0.029s
ok launchpad.net/juju-core/log/syslog 0.027s
ok launchpad.n...

Revision history for this message
Dimiter Naydenov (dimitern) wrote :
Revision history for this message
Go Bot (go-bot) wrote :
Download full text (8.9 KiB)

The attempt to merge lp:~dimitern/juju-core/260-lp-1067979-duplicate-charm into lp:juju-core failed. Below is the output from the failed tests.

ok launchpad.net/juju-core/agent 1.188s
ok launchpad.net/juju-core/agent/tools 0.257s
ok launchpad.net/juju-core/bzr 7.914s
ok launchpad.net/juju-core/cert 3.140s
ok launchpad.net/juju-core/charm 0.642s
? launchpad.net/juju-core/charm/hooks [no test files]
? launchpad.net/juju-core/charm/testing [no test files]
ok launchpad.net/juju-core/cloudinit 0.033s
ok launchpad.net/juju-core/cloudinit/sshinit 1.071s
ok launchpad.net/juju-core/cmd 0.222s
ok launchpad.net/juju-core/cmd/charm-admin 0.865s
? launchpad.net/juju-core/cmd/charmd [no test files]
? launchpad.net/juju-core/cmd/charmload [no test files]
ok launchpad.net/juju-core/cmd/juju 250.766s
ok launchpad.net/juju-core/cmd/jujud 63.331s
ok launchpad.net/juju-core/cmd/plugins/juju-metadata 12.447s
? launchpad.net/juju-core/cmd/plugins/juju-restore [no test files]
ok launchpad.net/juju-core/constraints 0.029s
ok launchpad.net/juju-core/container 0.038s
ok launchpad.net/juju-core/container/factory 0.065s
ok launchpad.net/juju-core/container/kvm 0.354s
ok launchpad.net/juju-core/container/kvm/mock 0.055s
? launchpad.net/juju-core/container/kvm/testing [no test files]
ok launchpad.net/juju-core/container/lxc 0.314s
? launchpad.net/juju-core/container/lxc/mock [no test files]
? launchpad.net/juju-core/container/lxc/testing [no test files]
? launchpad.net/juju-core/container/testing [no test files]
ok launchpad.net/juju-core/downloader 5.287s
ok launchpad.net/juju-core/environs 3.346s
ok launchpad.net/juju-core/environs/bootstrap 5.054s
ok launchpad.net/juju-core/environs/cloudinit 0.684s
ok launchpad.net/juju-core/environs/config 5.756s
ok launchpad.net/juju-core/environs/configstore 0.068s
ok launchpad.net/juju-core/environs/filestorage 0.034s
ok launchpad.net/juju-core/environs/httpstorage 1.023s
ok launchpad.net/juju-core/environs/imagemetadata 0.799s
? launchpad.net/juju-core/environs/imagemetadata/testing [no test files]
ok launchpad.net/juju-core/environs/instances 0.063s
ok launchpad.net/juju-core/environs/jujutest 0.339s
ok launchpad.net/juju-core/environs/manual 7.733s
ok launchpad.net/juju-core/environs/simplestreams 0.331s
? launchpad.net/juju-core/environs/simplestreams/testing [no test files]
ok launchpad.net/juju-core/environs/sshstorage 1.588s
ok launchpad.net/juju-core/environs/storage 1.217s
ok launchpad.net/juju-core/environs/sync 36.899s
ok launchpad.net/juju-core/environs/testing 0.201s
ok launchpad.net/juju-core/environs/tools 7.064s
? launchpad.net/juju-core/environs/tools/testing [no test files]
ok launchpad.net/juju-core/errors 0.017s
ok launchpad.net/juju-core/instance 0.024s
? launchpad.net/juju-core/instance/testing [no test files]
ok launchpad.net/juju-core/juju 30.207s
ok launchpad.net/juju-core/juju/osenv 0.021s
? launchpad.net/juju-core/juju/testing [no test files]
ok launchpad.net/juju-core/log 0.016s
ok launchpad.net/juju-core/log/syslog 0.029s
ok launchpad.net/juju-core/names 0.030s
? launchpad...

Read more...

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'charm/repo.go'
2--- charm/repo.go 2014-01-22 16:48:19 +0000
3+++ charm/repo.go 2014-01-30 14:23:21 +0000
4@@ -4,8 +4,6 @@
5 package charm
6
7 import (
8- "crypto/sha256"
9- "encoding/hex"
10 "encoding/json"
11 "fmt"
12 "io"
13@@ -299,16 +297,11 @@
14 // verify returns an error unless a file exists at path with a hex-encoded
15 // SHA256 matching digest.
16 func verify(path, digest string) error {
17- f, err := os.Open(path)
18+ hash, _, err := utils.ReadFileSHA256(path)
19 if err != nil {
20 return err
21 }
22- defer f.Close()
23- h := sha256.New()
24- if _, err := io.Copy(h, f); err != nil {
25- return err
26- }
27- if hex.EncodeToString(h.Sum(nil)) != digest {
28+ if hash != digest {
29 return fmt.Errorf("bad SHA256 of %q", path)
30 }
31 return nil
32
33=== modified file 'charm/testing/mockstore.go'
34--- charm/testing/mockstore.go 2014-01-28 04:58:43 +0000
35+++ charm/testing/mockstore.go 2014-01-30 14:23:21 +0000
36@@ -4,12 +4,12 @@
37 package testing
38
39 import (
40- "crypto/sha256"
41- "encoding/hex"
42+ "bytes"
43 "encoding/json"
44- "io/ioutil"
45+ "io"
46 "net"
47 "net/http"
48+ "os"
49 "strconv"
50 "strings"
51
52@@ -18,6 +18,7 @@
53
54 "launchpad.net/juju-core/charm"
55 "launchpad.net/juju-core/testing"
56+ "launchpad.net/juju-core/utils"
57 )
58
59 var logger = loggo.GetLogger("juju.charm.testing.mockstore")
60@@ -38,12 +39,14 @@
61 // NewMockStore creates a mock charm store containing the specified charms.
62 func NewMockStore(c *gc.C, charms map[string]int) *MockStore {
63 s := &MockStore{charms: charms}
64- bytes, err := ioutil.ReadFile(testing.Charms.BundlePath(c.MkDir(), "dummy"))
65- c.Assert(err, gc.IsNil)
66- s.bundleBytes = bytes
67- h := sha256.New()
68- h.Write(bytes)
69- s.bundleSha256 = hex.EncodeToString(h.Sum(nil))
70+ f, err := os.Open(testing.Charms.BundlePath(c.MkDir(), "dummy"))
71+ c.Assert(err, gc.IsNil)
72+ defer f.Close()
73+ buf := &bytes.Buffer{}
74+ s.bundleSha256, _, err = utils.ReadSHA256(io.TeeReader(f, buf))
75+ c.Assert(err, gc.IsNil)
76+ s.bundleBytes = buf.Bytes()
77+ c.Assert(err, gc.IsNil)
78 s.mux = http.NewServeMux()
79 s.mux.HandleFunc("/charm-info", s.serveInfo)
80 s.mux.HandleFunc("/charm-event", s.serveEvent)
81
82=== modified file 'environs/sync/sync.go'
83--- environs/sync/sync.go 2014-01-30 06:21:03 +0000
84+++ environs/sync/sync.go 2014-01-30 14:23:21 +0000
85@@ -5,7 +5,6 @@
86
87 import (
88 "bytes"
89- "crypto/sha256"
90 "fmt"
91 "io"
92 "io/ioutil"
93@@ -166,22 +165,17 @@
94 if err != nil {
95 return err
96 }
97+ buf := &bytes.Buffer{}
98 srcFile := resp.Body
99 defer srcFile.Close()
100- // We have to buffer the content, because Put requires the content
101- // length, but Get only returns us a ReadCloser
102- buf := &bytes.Buffer{}
103- nBytes, err := io.Copy(buf, srcFile)
104+ tool.SHA256, tool.Size, err = utils.ReadSHA256(io.TeeReader(srcFile, buf))
105 if err != nil {
106 return err
107 }
108- logger.Infof("downloaded %v (%dkB), uploading", toolsName, (nBytes+512)/1024)
109- logger.Infof("download %dkB, uploading", (nBytes+512)/1024)
110- sha256hash := sha256.New()
111- sha256hash.Write(buf.Bytes())
112- tool.SHA256 = fmt.Sprintf("%x", sha256hash.Sum(nil))
113- tool.Size = nBytes
114- return dest.Put(toolsName, buf, nBytes)
115+ sizeInKB := (tool.Size + 512) / 1024
116+ logger.Infof("downloaded %v (%dkB), uploading", toolsName, sizeInKB)
117+ logger.Infof("download %dkB, uploading", sizeInKB)
118+ return dest.Put(toolsName, buf, tool.Size)
119 }
120
121 // UploadFunc is the type of Upload, which may be
122
123=== modified file 'environs/testing/storage.go'
124--- environs/testing/storage.go 2014-01-14 06:37:55 +0000
125+++ environs/testing/storage.go 2014-01-30 14:23:21 +0000
126@@ -4,13 +4,16 @@
127 package testing
128
129 import (
130+ "fmt"
131 "io"
132
133 gc "launchpad.net/gocheck"
134
135+ "launchpad.net/juju-core/environs"
136 "launchpad.net/juju-core/environs/filestorage"
137 "launchpad.net/juju-core/environs/httpstorage"
138 "launchpad.net/juju-core/environs/storage"
139+ "launchpad.net/juju-core/state"
140 )
141
142 // CreateLocalTestStorage returns the listener, which needs to be closed, and
143@@ -26,3 +29,17 @@
144 closer = listener
145 return
146 }
147+
148+// GetEnvironStorage creates an Environ from the config in state and
149+// returns its storage interface.
150+func GetEnvironStorage(st *state.State) (storage.Storage, error) {
151+ envConfig, err := st.EnvironConfig()
152+ if err != nil {
153+ return nil, fmt.Errorf("cannot get environment config: %v", err)
154+ }
155+ env, err := environs.New(envConfig)
156+ if err != nil {
157+ return nil, fmt.Errorf("cannot access environment: %v", err)
158+ }
159+ return env.Storage(), nil
160+}
161
162=== modified file 'environs/tools/testing/testing.go'
163--- environs/tools/testing/testing.go 2014-01-30 06:21:03 +0000
164+++ environs/tools/testing/testing.go 2014-01-30 14:23:21 +0000
165@@ -4,9 +4,7 @@
166 package testing
167
168 import (
169- "crypto/sha256"
170 "fmt"
171- "io"
172 "io/ioutil"
173 "os"
174 "path"
175@@ -22,6 +20,7 @@
176 "launchpad.net/juju-core/environs/tools"
177 jc "launchpad.net/juju-core/testing/checkers"
178 coretools "launchpad.net/juju-core/tools"
179+ "launchpad.net/juju-core/utils"
180 "launchpad.net/juju-core/utils/set"
181 "launchpad.net/juju-core/version"
182 )
183@@ -70,13 +69,9 @@
184 if strings.HasPrefix(path, "file://") {
185 path = path[len("file://"):]
186 }
187- f, err := os.Open(path)
188- c.Assert(err, gc.IsNil)
189- defer f.Close()
190- hash := sha256.New()
191- size, err := io.Copy(hash, f)
192- c.Assert(err, gc.IsNil)
193- return size, fmt.Sprintf("%x", hash.Sum(nil))
194+ hash, size, err := utils.ReadFileSHA256(path)
195+ c.Assert(err, gc.IsNil)
196+ return size, hash
197 }
198
199 // ParseMetadataFromDir loads ToolsMetadata from the specified directory.
200
201=== modified file 'juju/conn.go'
202--- juju/conn.go 2014-01-22 22:48:54 +0000
203+++ juju/conn.go 2014-01-30 14:23:21 +0000
204@@ -4,11 +4,8 @@
205 package juju
206
207 import (
208- "crypto/sha256"
209- "encoding/hex"
210 stderrors "errors"
211 "fmt"
212- "io"
213 "io/ioutil"
214 "net/url"
215 "os"
216@@ -223,12 +220,10 @@
217 default:
218 return nil, fmt.Errorf("unknown charm type %T", ch)
219 }
220- h := sha256.New()
221- size, err := io.Copy(h, f)
222+ digest, size, err := utils.ReadSHA256(f)
223 if err != nil {
224 return nil, err
225 }
226- digest := hex.EncodeToString(h.Sum(nil))
227 if _, err := f.Seek(0, 0); err != nil {
228 return nil, err
229 }
230
231=== modified file 'juju/conn_test.go'
232--- juju/conn_test.go 2014-01-24 12:39:48 +0000
233+++ juju/conn_test.go 2014-01-30 14:23:21 +0000
234@@ -453,7 +453,7 @@
235
236 func (s *DeployLocalSuite) SetUpSuite(c *gc.C) {
237 s.JujuConnSuite.SetUpSuite(c)
238- s.repo = &charm.LocalRepository{Path: coretesting.Charms.Path}
239+ s.repo = &charm.LocalRepository{Path: coretesting.Charms.Path()}
240 s.oldCacheDir, charm.CacheDir = charm.CacheDir, c.MkDir()
241 }
242
243
244=== modified file 'juju/testing/conn.go'
245--- juju/testing/conn.go 2014-01-24 12:39:48 +0000
246+++ juju/testing/conn.go 2014-01-30 14:23:21 +0000
247@@ -280,7 +280,7 @@
248 ch := testing.Charms.Dir(name)
249 ident := fmt.Sprintf("%s-%d", ch.Meta().Name, ch.Revision())
250 curl := charm.MustParseURL("local:quantal/" + ident)
251- repo, err := charm.InferRepository(curl, testing.Charms.Path)
252+ repo, err := charm.InferRepository(curl, testing.Charms.Path())
253 c.Assert(err, gc.IsNil)
254 sch, err := s.Conn.PutCharm(curl, repo, false)
255 c.Assert(err, gc.IsNil)
256
257=== modified file 'juju/testing/repo.go'
258--- juju/testing/repo.go 2013-12-19 12:39:49 +0000
259+++ juju/testing/repo.go 2014-01-30 14:23:21 +0000
260@@ -1,9 +1,6 @@
261 package testing
262
263 import (
264- "crypto/sha256"
265- "encoding/hex"
266- "io/ioutil"
267 "net/http"
268 "os"
269 "path/filepath"
270@@ -13,6 +10,7 @@
271
272 "launchpad.net/juju-core/charm"
273 "launchpad.net/juju-core/state"
274+ "launchpad.net/juju-core/utils"
275 )
276
277 // RepoSuite acts as a JujuConnSuite but also sets up
278@@ -77,11 +75,8 @@
279 resp, err := http.Get(url.String())
280 c.Assert(err, gc.IsNil)
281 defer resp.Body.Close()
282- body, err := ioutil.ReadAll(resp.Body)
283+ digest, _, err := utils.ReadSHA256(resp.Body)
284 c.Assert(err, gc.IsNil)
285- h := sha256.New()
286- h.Write(body)
287- digest := hex.EncodeToString(h.Sum(nil))
288 c.Assert(ch.BundleSha256(), gc.Equals, digest)
289 }
290
291
292=== modified file 'state/apiserver/charms.go'
293--- state/apiserver/charms.go 2013-12-17 09:49:11 +0000
294+++ state/apiserver/charms.go 2014-01-30 14:23:21 +0000
295@@ -18,8 +18,7 @@
296 "strings"
297
298 "launchpad.net/juju-core/charm"
299- "launchpad.net/juju-core/environs"
300- "launchpad.net/juju-core/environs/storage"
301+ envtesting "launchpad.net/juju-core/environs/testing"
302 "launchpad.net/juju-core/names"
303 "launchpad.net/juju-core/state"
304 "launchpad.net/juju-core/state/api/params"
305@@ -199,7 +198,7 @@
306 }
307
308 // Now upload to provider storage.
309- storage, err := getEnvironStorage(h.state)
310+ storage, err := envtesting.GetEnvironStorage(h.state)
311 if err != nil {
312 return fmt.Errorf("cannot access provider storage: %v", err)
313 }
314@@ -223,17 +222,3 @@
315 }
316 return nil
317 }
318-
319-// getEnvironStorage creates an Environ from the config in state and
320-// returns its storage interface.
321-func getEnvironStorage(st *state.State) (storage.Storage, error) {
322- envConfig, err := st.EnvironConfig()
323- if err != nil {
324- return nil, fmt.Errorf("cannot get environment config: %v", err)
325- }
326- env, err := environs.New(envConfig)
327- if err != nil {
328- return nil, fmt.Errorf("cannot access environment: %v", err)
329- }
330- return env.Storage(), nil
331-}
332
333=== modified file 'state/apiserver/charms_test.go'
334--- state/apiserver/charms_test.go 2013-12-17 09:49:11 +0000
335+++ state/apiserver/charms_test.go 2014-01-30 14:23:21 +0000
336@@ -4,8 +4,6 @@
337 package apiserver_test
338
339 import (
340- "crypto/sha256"
341- "encoding/hex"
342 "encoding/json"
343 "fmt"
344 "io"
345@@ -17,10 +15,10 @@
346 gc "launchpad.net/gocheck"
347
348 "launchpad.net/juju-core/charm"
349+ envtesting "launchpad.net/juju-core/environs/testing"
350 jujutesting "launchpad.net/juju-core/juju/testing"
351 "launchpad.net/juju-core/state"
352 "launchpad.net/juju-core/state/api/params"
353- "launchpad.net/juju-core/state/apiserver"
354 coretesting "launchpad.net/juju-core/testing"
355 jc "launchpad.net/juju-core/testing/checkers"
356 "launchpad.net/juju-core/utils"
357@@ -131,7 +129,7 @@
358 c.Assert(sch.Revision(), gc.Equals, 2)
359 c.Assert(sch.IsUploaded(), jc.IsTrue)
360 // No more checks for these two here, because they
361- // are verified in TestUploadRequiresSingleUploadedFile.
362+ // are verified in TestUploadRespectsLocalRevision.
363 c.Assert(sch.BundleURL(), gc.Not(gc.Equals), "")
364 c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "")
365 }
366@@ -164,16 +162,23 @@
367 c.Assert(err, gc.IsNil)
368
369 // Finally, verify the SHA256 and uploaded URL.
370- expectedSHA256, _, err := getSHA256(tempFile)
371+ expectedSHA256, _, err := utils.ReadSHA256(tempFile)
372 c.Assert(err, gc.IsNil)
373 name := charm.Quote(expectedURL.String())
374- storage, err := apiserver.GetEnvironStorage(s.State)
375+ storage, err := envtesting.GetEnvironStorage(s.State)
376 c.Assert(err, gc.IsNil)
377 expectedUploadURL, err := storage.URL(name)
378 c.Assert(err, gc.IsNil)
379
380 c.Assert(sch.BundleURL().String(), gc.Equals, expectedUploadURL)
381 c.Assert(sch.BundleSha256(), gc.Equals, expectedSHA256)
382+
383+ reader, err := storage.Get(name)
384+ c.Assert(err, gc.IsNil)
385+ defer reader.Close()
386+ downloadedSHA256, _, err := utils.ReadSHA256(reader)
387+ c.Assert(err, gc.IsNil)
388+ c.Assert(downloadedSHA256, gc.Equals, expectedSHA256)
389 }
390
391 func (s *charmsSuite) charmsURI(c *gc.C, query string) string {
392@@ -230,13 +235,3 @@
393 }
394 c.Check(resp.StatusCode, gc.Equals, expCode)
395 }
396-
397-func getSHA256(source io.ReadSeeker) (string, int64, error) {
398- hash := sha256.New()
399- size, err := io.Copy(hash, source)
400- if err != nil {
401- return "", 0, err
402- }
403- digest := hex.EncodeToString(hash.Sum(nil))
404- return digest, size, nil
405-}
406
407=== modified file 'state/apiserver/client/client.go'
408--- state/apiserver/client/client.go 2014-01-30 02:20:09 +0000
409+++ state/apiserver/client/client.go 2014-01-30 14:23:21 +0000
410@@ -4,10 +4,7 @@
411 package client
412
413 import (
414- "crypto/sha256"
415- "encoding/hex"
416 "fmt"
417- "io"
418 "net/url"
419 "os"
420 "strings"
421@@ -29,6 +26,7 @@
422 "launchpad.net/juju-core/state/api/params"
423 "launchpad.net/juju-core/state/apiserver/common"
424 "launchpad.net/juju-core/state/statecmd"
425+ "launchpad.net/juju-core/utils"
426 )
427
428 var logger = loggo.GetLogger("juju.state.apiserver.client")
429@@ -836,9 +834,13 @@
430 return fmt.Errorf("charm URL must include revision")
431 }
432
433- // First check the charm is not already in state.
434- if _, err := c.api.state.Charm(charmURL); err == nil {
435+ // First, check if a pending or a real charm exists in state.
436+ stateCharm, err := c.api.state.PrepareStoreCharmUpload(charmURL)
437+ if err == nil && stateCharm.IsUploaded() {
438+ // Charm already in state (it was uploaded already).
439 return nil
440+ } else if err != nil {
441+ return err
442 }
443
444 // Get the charm and its information from the store.
445@@ -862,12 +864,10 @@
446 return fmt.Errorf("cannot read downloaded charm: %v", err)
447 }
448 defer archive.Close()
449- hash := sha256.New()
450- size, err := io.Copy(hash, archive)
451+ bundleSHA256, size, err := utils.ReadSHA256(archive)
452 if err != nil {
453 return fmt.Errorf("cannot calculate SHA256 hash of charm: %v", err)
454 }
455- bundleSHA256 := hex.EncodeToString(hash.Sum(nil))
456 if _, err := archive.Seek(0, 0); err != nil {
457 return fmt.Errorf("cannot rewind charm archive: %v", err)
458 }
459@@ -878,12 +878,14 @@
460 return fmt.Errorf("cannot access environment: %v", err)
461 }
462 storage := env.Storage()
463- name := charm.Quote(charmURL.String())
464- err = storage.Put(name, archive, size)
465+ archiveName, err := CharmArchiveName(charmURL.Name, charmURL.Revision)
466 if err != nil {
467+ return fmt.Errorf("cannot generate charm archive name: %v", err)
468+ }
469+ if err := storage.Put(archiveName, archive, size); err != nil {
470 return fmt.Errorf("cannot upload charm to provider storage: %v", err)
471 }
472- storageURL, err := storage.URL(name)
473+ storageURL, err := storage.URL(archiveName)
474 if err != nil {
475 return fmt.Errorf("cannot get storage URL for charm: %v", err)
476 }
477@@ -892,7 +894,29 @@
478 return fmt.Errorf("cannot parse storage URL: %v", err)
479 }
480
481- // Finally, add the charm to state.
482- _, err = c.api.state.AddCharm(downloadedCharm, charmURL, bundleURL, bundleSHA256)
483+ // Finally, update the charm data in state and mark it as no longer pending.
484+ _, err = c.api.state.UpdateUploadedCharm(downloadedCharm, charmURL, bundleURL, bundleSHA256)
485+ if err == state.ErrCharmRevisionAlreadyModified ||
486+ state.IsCharmAlreadyUploadedError(err) {
487+ // This is not an error, it just signifies somebody else
488+ // managed to upload and update the charm in state before
489+ // us. This means we have to delete what we just uploaded
490+ // to storage.
491+ if err := storage.Remove(archiveName); err != nil {
492+ logger.Errorf("cannot remove duplicated charm from storage: %v", err)
493+ }
494+ return nil
495+ }
496 return err
497 }
498+
499+// CharmArchiveName returns a string that is suitable as a file name
500+// in a storage URL. It is constructed from the charm name, revision
501+// and a random UUID string.
502+func CharmArchiveName(name string, revision int) (string, error) {
503+ uuid, err := utils.NewUUID()
504+ if err != nil {
505+ return "", err
506+ }
507+ return charm.Quote(fmt.Sprintf("%s-%d-%s", name, revision, uuid)), nil
508+}
509
510=== modified file 'state/apiserver/client/client_test.go'
511--- state/apiserver/client/client_test.go 2014-01-30 03:30:30 +0000
512+++ state/apiserver/client/client_test.go 2014-01-30 14:23:21 +0000
513@@ -8,6 +8,8 @@
514 "net/url"
515 "strconv"
516 "strings"
517+ "sync"
518+ "time"
519
520 gc "launchpad.net/gocheck"
521
522@@ -17,8 +19,11 @@
523 "launchpad.net/juju-core/constraints"
524 "launchpad.net/juju-core/environs/cloudinit"
525 "launchpad.net/juju-core/environs/config"
526+ "launchpad.net/juju-core/environs/storage"
527+ envtesting "launchpad.net/juju-core/environs/testing"
528 "launchpad.net/juju-core/errors"
529 "launchpad.net/juju-core/instance"
530+ "launchpad.net/juju-core/provider/dummy"
531 "launchpad.net/juju-core/state"
532 "launchpad.net/juju-core/state/api"
533 "launchpad.net/juju-core/state/api/params"
534@@ -26,6 +31,7 @@
535 "launchpad.net/juju-core/state/statecmd"
536 coretesting "launchpad.net/juju-core/testing"
537 jc "launchpad.net/juju-core/testing/checkers"
538+ "launchpad.net/juju-core/utils"
539 "launchpad.net/juju-core/version"
540 )
541
542@@ -1787,6 +1793,7 @@
543 bundleURL, err := url.Parse("http://bundles.testing.invalid/" + ident)
544 c.Assert(err, gc.IsNil)
545 sch, err := s.State.AddCharm(charmDir, curl, bundleURL, ident+"-sha256")
546+ c.Assert(err, gc.IsNil)
547
548 name := charm.Quote(sch.URL().String())
549 storage := s.Conn.Environ.Storage()
550@@ -1804,20 +1811,127 @@
551 err = client.AddCharm(curl)
552 c.Assert(err, gc.IsNil)
553
554- // Verify it's in state.
555+ // Verify it's in state and it got uploaded.
556 sch, err = s.State.Charm(curl)
557 c.Assert(err, gc.IsNil)
558-
559- name = charm.Quote(curl.String())
560- storageURL, err := storage.URL(name)
561- c.Assert(err, gc.IsNil)
562-
563- c.Assert(sch.BundleURL().String(), gc.Equals, storageURL)
564- // We don't care about the exact value of the hash here, just that
565- // it's set.
566- c.Assert(sch.BundleSha256(), gc.Not(gc.Equals), "")
567-
568- // Verify it's added to storage.
569- _, err = storage.Get(name)
570- c.Assert(err, gc.IsNil)
571+ s.assertUploaded(c, storage, sch.BundleURL(), sch.BundleSha256())
572+}
573+
574+func (s *clientSuite) TestAddCharmConcurrently(c *gc.C) {
575+ store, restore := makeMockCharmStore()
576+ defer restore()
577+
578+ client := s.APIState.Client()
579+ curl, _ := addCharm(c, store, "wordpress")
580+
581+ // Expect storage Put() to be called once for each goroutine
582+ // below.
583+ ops := make(chan dummy.Operation, 500)
584+ dummy.Listen(ops)
585+ go s.assertPutCalled(c, ops, 10)
586+
587+ // Try adding the same charm concurrently from multiple goroutines
588+ // to test no "duplicate key errors" are reported (see lp bug
589+ // #1067979) and also at the end only one charm document is
590+ // created.
591+
592+ var wg sync.WaitGroup
593+ for i := 0; i < 10; i++ {
594+ wg.Add(1)
595+ go func(index int) {
596+ defer wg.Done()
597+
598+ c.Assert(client.AddCharm(curl), gc.IsNil, gc.Commentf("goroutine %d", index))
599+ sch, err := s.State.Charm(curl)
600+ c.Assert(err, gc.IsNil, gc.Commentf("goroutine %d", index))
601+ c.Assert(sch.URL(), jc.DeepEquals, curl, gc.Commentf("goroutine %d", index))
602+ expectedName := fmt.Sprintf("%s-%d-[0-9a-f-]+", curl.Name, curl.Revision)
603+ c.Assert(getArchiveName(sch.BundleURL()), gc.Matches, expectedName)
604+ }(i)
605+ }
606+ wg.Wait()
607+ close(ops)
608+
609+ // Verify there is only a single uploaded charm remains and it
610+ // contains the correct data.
611+ sch, err := s.State.Charm(curl)
612+ c.Assert(err, gc.IsNil)
613+ storage, err := envtesting.GetEnvironStorage(s.State)
614+ c.Assert(err, gc.IsNil)
615+ uploads, err := storage.List(fmt.Sprintf("%s-%d-", curl.Name, curl.Revision))
616+ c.Assert(err, gc.IsNil)
617+ c.Assert(uploads, gc.HasLen, 1)
618+ c.Assert(getArchiveName(sch.BundleURL()), gc.Equals, uploads[0])
619+ s.assertUploaded(c, storage, sch.BundleURL(), sch.BundleSha256())
620+}
621+
622+func (s *clientSuite) TestAddCharmOverwritesPlaceholders(c *gc.C) {
623+ store, restore := makeMockCharmStore()
624+ defer restore()
625+
626+ client := s.APIState.Client()
627+ curl, _ := addCharm(c, store, "wordpress")
628+
629+ // Add a placeholder with the same charm URL.
630+ err := s.State.AddStoreCharmPlaceholder(curl)
631+ c.Assert(err, gc.IsNil)
632+ _, err = s.State.Charm(curl)
633+ c.Assert(err, jc.Satisfies, errors.IsNotFoundError)
634+
635+ // Now try to add the charm, which will convert the placeholder to
636+ // a pending charm.
637+ err = client.AddCharm(curl)
638+ c.Assert(err, gc.IsNil)
639+
640+ // Make sure the document's flags were reset as expected.
641+ sch, err := s.State.Charm(curl)
642+ c.Assert(err, gc.IsNil)
643+ c.Assert(sch.URL(), jc.DeepEquals, curl)
644+ c.Assert(sch.IsPlaceholder(), jc.IsFalse)
645+ c.Assert(sch.IsUploaded(), jc.IsTrue)
646+}
647+
648+func (s *clientSuite) TestCharmArchiveName(c *gc.C) {
649+ for rev, name := range []string{"Foo", "bar", "wordpress", "mysql"} {
650+ archiveFormat := fmt.Sprintf("%s-%d-[0-9a-f-]+", name, rev)
651+ archiveName, err := client.CharmArchiveName(name, rev)
652+ c.Check(err, gc.IsNil)
653+ c.Check(archiveName, gc.Matches, archiveFormat)
654+ }
655+}
656+
657+func (s *clientSuite) assertPutCalled(c *gc.C, ops chan dummy.Operation, numCalls int) {
658+ calls := 0
659+ select {
660+ case op, ok := <-ops:
661+ if !ok {
662+ return
663+ }
664+ if op, ok := op.(dummy.OpPutFile); ok {
665+ calls++
666+ if calls > numCalls {
667+ c.Fatalf("storage Put() called %d times, expected %d times", calls, numCalls)
668+ return
669+ }
670+ nameFormat := "[0-9a-z-]+-[0-9]+-[0-9a-f-]+"
671+ c.Assert(op.FileName, gc.Matches, nameFormat)
672+ }
673+ case <-time.After(coretesting.LongWait):
674+ c.Fatalf("timed out while waiting for a storage Put() calls")
675+ return
676+ }
677+}
678+
679+func (s *clientSuite) assertUploaded(c *gc.C, storage storage.Storage, bundleURL *url.URL, expectedSHA256 string) {
680+ archiveName := getArchiveName(bundleURL)
681+ reader, err := storage.Get(archiveName)
682+ c.Assert(err, gc.IsNil)
683+ defer reader.Close()
684+ downloadedSHA256, _, err := utils.ReadSHA256(reader)
685+ c.Assert(err, gc.IsNil)
686+ c.Assert(downloadedSHA256, gc.Equals, expectedSHA256)
687+}
688+
689+func getArchiveName(bundleURL *url.URL) string {
690+ return strings.TrimPrefix(bundleURL.RequestURI(), "/dummyenv/private/")
691 }
692
693=== modified file 'state/apiserver/export_test.go'
694--- state/apiserver/export_test.go 2014-01-17 19:33:42 +0000
695+++ state/apiserver/export_test.go 2014-01-30 14:23:21 +0000
696@@ -6,8 +6,7 @@
697 import "reflect"
698
699 var (
700- RootType = reflect.TypeOf(&srvRoot{})
701- NewPingTimeout = newPingTimeout
702- GetEnvironStorage = getEnvironStorage
703- MaxPingInterval = &maxPingInterval
704+ RootType = reflect.TypeOf(&srvRoot{})
705+ NewPingTimeout = newPingTimeout
706+ MaxPingInterval = &maxPingInterval
707 )
708
709=== modified file 'state/state.go'
710--- state/state.go 2014-01-28 04:58:43 +0000
711+++ state/state.go 2014-01-30 14:23:21 +0000
712@@ -445,12 +445,13 @@
713 return coll, id, nil
714 }
715
716-// AddCharm adds the ch charm with curl to the state. bundleUrl must be
717-// set to a URL where the bundle for ch may be downloaded from.
718-// On success the newly added charm state is returned.
719+// AddCharm adds the ch charm with curl to the state. bundleURL must
720+// be set to a URL where the bundle for ch may be downloaded from. On
721+// success the newly added charm state is returned.
722 func (st *State) AddCharm(ch charm.Charm, curl *charm.URL, bundleURL *url.URL, bundleSha256 string) (stch *Charm, err error) {
723- // The charm may already exist in state as a placeholder, so we check for that situation and update the
724- // existing charm record if necessary, otherwise add a new record.
725+ // The charm may already exist in state as a placeholder, so we
726+ // check for that situation and update the existing charm record
727+ // if necessary, otherwise add a new record.
728 var existing charmDoc
729 err = st.charms.Find(D{{"_id", curl.String()}, {"placeholder", true}}).One(&existing)
730 if err == mgo.ErrNotFound {
731@@ -469,13 +470,19 @@
732 } else if err != nil {
733 return nil, err
734 }
735- return st.updateCharmDoc(ch, curl, bundleURL, bundleSha256, StillPlaceholder)
736+ return st.updateCharmDoc(ch, curl, bundleURL, bundleSha256, stillPlaceholder)
737 }
738
739-// Charm returns the charm with the given URL.
740+// Charm returns the charm with the given URL. Charms pending upload
741+// to storage and placeholders are never returned.
742 func (st *State) Charm(curl *charm.URL) (*Charm, error) {
743 cdoc := &charmDoc{}
744- err := st.charms.Find(D{{"_id", curl}, {"pendingupload", false}, {"placeholder", false}}).One(cdoc)
745+ what := D{
746+ {"_id", curl},
747+ {"placeholder", D{{"$ne", true}}},
748+ {"pendingupload", D{{"$ne", true}}},
749+ }
750+ err := st.charms.Find(what).One(&cdoc)
751 if err == mgo.ErrNotFound {
752 return nil, errors.NotFoundf("charm %q", curl)
753 }
754@@ -488,7 +495,8 @@
755 return newCharm(st, cdoc)
756 }
757
758-// LatestPlaceholderCharm returns the latest charm described by the given URL but which is not yet deployed.
759+// LatestPlaceholderCharm returns the latest charm described by the
760+// given URL but which is not yet deployed.
761 func (st *State) LatestPlaceholderCharm(curl *charm.URL) (*Charm, error) {
762 noRevURL := curl.WithRevision(-1)
763 curlRegex := "^" + regexp.QuoteMeta(noRevURL.String())
764@@ -510,10 +518,10 @@
765 return newCharm(st, &latest)
766 }
767
768-// PrepareLocalCharmUpload must be called before a charm is uploaded
769-// to the provider storage in order to create a charm document in
770-// state. It returns a new Charm with no metadata and a unique
771-// revision number.
772+// PrepareLocalCharmUpload must be called before a local charm is
773+// uploaded to the provider storage in order to create a charm
774+// document in state. It returns the chosen unique charm URL reserved
775+// in state for the charm.
776 //
777 // The url's schema must be "local" and it must include a revision.
778 func (st *State) PrepareLocalCharmUpload(curl *charm.URL) (chosenUrl *charm.URL, err error) {
779@@ -575,8 +583,95 @@
780 return chosenUrl, nil
781 }
782
783-var StillPending = D{{"pendingupload", true}}
784-var StillPlaceholder = D{{"placeholder", true}}
785+// PrepareStoreCharmUpload must be called before a charm store charm
786+// is uploaded to the provider storage in order to create a charm
787+// document in state. If a charm with the same URL is already in
788+// state, it will be returned as a *state.Charm (is can be still
789+// pending or already uploaded). Otherwise, a new charm document is
790+// added in state with just the given charm URL and
791+// PendingUpload=true, which is then returned as a *state.Charm.
792+//
793+// The url's schema must be "cs" and it must include a revision.
794+func (st *State) PrepareStoreCharmUpload(curl *charm.URL) (*Charm, error) {
795+ // Perform a few sanity checks first.
796+ if curl.Schema != "cs" {
797+ return nil, fmt.Errorf("expected charm URL with cs schema, got %q", curl)
798+ }
799+ if curl.Revision < 0 {
800+ return nil, fmt.Errorf("expected charm URL with revision, got %q", curl)
801+ }
802+
803+ var (
804+ uploadedCharm charmDoc
805+ err error
806+ )
807+ for attempt := 0; attempt < 3; attempt++ {
808+ // Find an uploaded or pending charm with the given exact curl.
809+ err = st.charms.FindId(curl).One(&uploadedCharm)
810+ if err != nil && err != mgo.ErrNotFound {
811+ return nil, err
812+ } else if err == nil && !uploadedCharm.Placeholder {
813+ // The charm exists and it's either uploaded or still
814+ // pending, but it's not a placeholder. In any case, we
815+ // just return what we got.
816+ return newCharm(st, &uploadedCharm)
817+ } else if err == mgo.ErrNotFound {
818+ // Prepare the pending charm document for insertion.
819+ uploadedCharm = charmDoc{
820+ URL: curl,
821+ PendingUpload: true,
822+ Placeholder: false,
823+ }
824+ }
825+
826+ var ops []txn.Op
827+ if uploadedCharm.Placeholder {
828+ // Convert the placeholder to a pending charm, while
829+ // asserting the fields updated after an upload have not
830+ // changed yet.
831+ ops = []txn.Op{{
832+ C: st.charms.Name,
833+ Id: curl,
834+ Assert: D{
835+ {"bundlesha256", ""},
836+ {"pendingupload", false},
837+ {"placeholder", true},
838+ },
839+ Update: D{{"$set", D{
840+ {"pendingupload", true},
841+ {"placeholder", false},
842+ }}},
843+ }}
844+ // Update the fields of the document we're returning.
845+ uploadedCharm.PendingUpload = true
846+ uploadedCharm.Placeholder = false
847+ } else {
848+ // No charm document with this curl yet, insert it.
849+ ops = []txn.Op{{
850+ C: st.charms.Name,
851+ Id: curl,
852+ Assert: txn.DocMissing,
853+ Insert: uploadedCharm,
854+ }}
855+ }
856+
857+ // Run the transaction, and retry on abort.
858+ err = st.runTransaction(ops)
859+ if err == txn.ErrAborted {
860+ continue
861+ } else if err != nil {
862+ return nil, err
863+ } else if err == nil {
864+ return newCharm(st, &uploadedCharm)
865+ }
866+ }
867+ return nil, ErrExcessiveContention
868+}
869+
870+var (
871+ stillPending = D{{"pendingupload", true}}
872+ stillPlaceholder = D{{"placeholder", true}}
873+)
874
875 // AddStoreCharmPlaceholder creates a charm document in state for the given charm URL which
876 // must reference a charm from the store. The charm document is marked as a placeholder which
877@@ -653,13 +748,39 @@
878 ops = append(ops, txn.Op{
879 C: st.charms.Name,
880 Id: doc.URL.String(),
881- Assert: StillPlaceholder,
882+ Assert: stillPlaceholder,
883 Remove: true,
884 })
885 }
886 return ops, nil
887 }
888
889+// ErrCharmAlreadyUploaded is returned by UpdateUploadedCharm() when
890+// the given charm is already uploaded and marked as not pending in
891+// state.
892+type ErrCharmAlreadyUploaded struct {
893+ curl *charm.URL
894+}
895+
896+func (e *ErrCharmAlreadyUploaded) Error() string {
897+ return fmt.Sprintf("charm %q already uploaded", e.curl)
898+}
899+
900+// IsCharmAlreadyUploadedError returns if the given error is
901+// ErrCharmAlreadyUploaded.
902+func IsCharmAlreadyUploadedError(err interface{}) bool {
903+ if err == nil {
904+ return false
905+ }
906+ _, ok := err.(*ErrCharmAlreadyUploaded)
907+ return ok
908+}
909+
910+// ErrCharmRevisionAlreadyModified is returned when a pending or
911+// placeholder charm is no longer pending or a placeholder, signaling
912+// the charm is available in state with its full information.
913+var ErrCharmRevisionAlreadyModified = fmt.Errorf("charm revision already modified")
914+
915 // UpdateUploadedCharm marks the given charm URL as uploaded and
916 // updates the rest of its data, returning it as *state.Charm.
917 func (st *State) UpdateUploadedCharm(ch charm.Charm, curl *charm.URL, bundleURL *url.URL, bundleSha256 string) (*Charm, error) {
918@@ -672,14 +793,16 @@
919 return nil, err
920 }
921 if !doc.PendingUpload {
922- return nil, fmt.Errorf("charm %q already uploaded", curl)
923+ return nil, &ErrCharmAlreadyUploaded{curl}
924 }
925
926- return st.updateCharmDoc(ch, curl, bundleURL, bundleSha256, StillPending)
927+ return st.updateCharmDoc(ch, curl, bundleURL, bundleSha256, stillPending)
928 }
929
930-// updateCharmDoc updates the charm with specified URL with the given data, and resets the placeholder
931-// and pendingupdate flags.
932+// updateCharmDoc updates the charm with specified URL with the given
933+// data, and resets the placeholder and pendingupdate flags. If the
934+// charm is no longer a placeholder or pending (depending on preReq),
935+// it returns ErrCharmRevisionAlreadyModified.
936 func (st *State) updateCharmDoc(
937 ch charm.Charm, curl *charm.URL, bundleURL *url.URL, bundleSha256 string, preReq interface{}) (*Charm, error) {
938
939@@ -698,7 +821,7 @@
940 Update: updateFields,
941 }}
942 if err := st.runTransaction(ops); err != nil {
943- return nil, onAbort(err, fmt.Errorf("charm revision already modified"))
944+ return nil, onAbort(err, ErrCharmRevisionAlreadyModified)
945 }
946 return st.Charm(curl)
947 }
948
949=== modified file 'state/state_test.go'
950--- state/state_test.go 2014-01-25 11:45:42 +0000
951+++ state/state_test.go 2014-01-30 14:23:21 +0000
952@@ -129,15 +129,26 @@
953 c.Assert(err2, jc.Satisfies, errors.IsNotFoundError)
954 }
955
956+func (s *StateSuite) dummyCharm(c *gc.C, curlOverride string) (ch charm.Charm, curl *charm.URL, bundleURL *url.URL, bundleSHA256 string) {
957+ var err error
958+ ch = testing.Charms.Dir("dummy")
959+ if curlOverride != "" {
960+ curl = charm.MustParseURL(curlOverride)
961+ } else {
962+ curl = charm.MustParseURL(
963+ fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
964+ )
965+ }
966+ bundleURL, err = url.Parse("http://bundles.testing.invalid/dummy-1")
967+ c.Assert(err, gc.IsNil)
968+ bundleSHA256 = "dummy-1-sha256"
969+ return ch, curl, bundleURL, bundleSHA256
970+}
971+
972 func (s *StateSuite) TestAddCharm(c *gc.C) {
973 // Check that adding charms from scratch works correctly.
974- ch := testing.Charms.Dir("dummy")
975- curl := charm.MustParseURL(
976- fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
977- )
978- bundleURL, err := url.Parse("http://bundles.testing.invalid/dummy-1")
979- c.Assert(err, gc.IsNil)
980- dummy, err := s.State.AddCharm(ch, curl, bundleURL, "dummy-1-sha256")
981+ ch, curl, bundleURL, bundleSHA256 := s.dummyCharm(c, "")
982+ dummy, err := s.State.AddCharm(ch, curl, bundleURL, bundleSHA256)
983 c.Assert(err, gc.IsNil)
984 c.Assert(dummy.URL().String(), gc.Equals, curl.String())
985
986@@ -161,7 +172,8 @@
987 // Add a deployed charm.
988 bundleURL, err := url.Parse("http://bundles.testing.invalid/dummy-1")
989 c.Assert(err, gc.IsNil)
990- dummy, err := s.State.AddCharm(ch, curl, bundleURL, "dummy-1-sha256")
991+ bundleSHA256 := "dummy-1-sha256"
992+ dummy, err := s.State.AddCharm(ch, curl, bundleURL, bundleSHA256)
993 c.Assert(err, gc.IsNil)
994 c.Assert(dummy.URL().String(), gc.Equals, curl.String())
995
996@@ -231,18 +243,78 @@
997 c.Assert(curl.Revision, gc.Equals, 1234)
998 }
999
1000+func (s *StateSuite) TestPrepareStoreCharmUpload(c *gc.C) {
1001+ // First test the sanity checks.
1002+ sch, err := s.State.PrepareStoreCharmUpload(charm.MustParseURL("cs:quantal/dummy"))
1003+ c.Assert(err, gc.ErrorMatches, "expected charm URL with revision, got .*")
1004+ c.Assert(sch, gc.IsNil)
1005+ sch, err = s.State.PrepareStoreCharmUpload(charm.MustParseURL("local:quantal/dummy"))
1006+ c.Assert(err, gc.ErrorMatches, "expected charm URL with cs schema, got .*")
1007+ c.Assert(sch, gc.IsNil)
1008+
1009+ // No charm in state, so the call should respect given revision.
1010+ testCurl := charm.MustParseURL("cs:quantal/missing-123")
1011+ sch, err = s.State.PrepareStoreCharmUpload(testCurl)
1012+ c.Assert(err, gc.IsNil)
1013+ c.Assert(sch.URL(), gc.DeepEquals, testCurl)
1014+ c.Assert(sch.IsUploaded(), jc.IsFalse)
1015+
1016+ s.assertPendingCharmExists(c, sch.URL())
1017+
1018+ // Try adding it again with the same revision and ensure we get the same document.
1019+ schCopy, err := s.State.PrepareStoreCharmUpload(testCurl)
1020+ c.Assert(err, gc.IsNil)
1021+ c.Assert(sch, jc.DeepEquals, schCopy)
1022+
1023+ // Now add a charm and try again - we should get the same result
1024+ // as with AddCharm.
1025+ ch, curl, bundleURL, bundleSHA256 := s.dummyCharm(c, "cs:precise/dummy-2")
1026+ sch, err = s.State.AddCharm(ch, curl, bundleURL, bundleSHA256)
1027+ c.Assert(err, gc.IsNil)
1028+ schCopy, err = s.State.PrepareStoreCharmUpload(curl)
1029+ c.Assert(err, gc.IsNil)
1030+ c.Assert(sch, jc.DeepEquals, schCopy)
1031+
1032+ // Finally, try poking around the state with a placeholder and
1033+ // bundlesha256 to make sure we do the right thing.
1034+ curl = curl.WithRevision(999)
1035+ first := state.TransactionHook{
1036+ Before: func() {
1037+ err := s.State.AddStoreCharmPlaceholder(curl)
1038+ c.Assert(err, gc.IsNil)
1039+ },
1040+ After: func() {
1041+ err := s.charms.RemoveId(curl)
1042+ c.Assert(err, gc.IsNil)
1043+ },
1044+ }
1045+ second := state.TransactionHook{
1046+ Before: func() {
1047+ err := s.State.AddStoreCharmPlaceholder(curl)
1048+ c.Assert(err, gc.IsNil)
1049+ },
1050+ After: func() {
1051+ err := s.charms.UpdateId(curl, D{{"$set", D{
1052+ {"bundlesha256", "fake"}},
1053+ }})
1054+ c.Assert(err, gc.IsNil)
1055+ },
1056+ }
1057+ defer state.SetTransactionHooks(
1058+ c, s.State, first, second, first,
1059+ ).Check()
1060+
1061+ _, err = s.State.PrepareStoreCharmUpload(curl)
1062+ c.Assert(err, gc.Equals, state.ErrExcessiveContention)
1063+}
1064+
1065 func (s *StateSuite) TestUpdateUploadedCharm(c *gc.C) {
1066- ch := testing.Charms.Dir("dummy")
1067- curl := charm.MustParseURL(
1068- fmt.Sprintf("local:quantal/%s-%d", ch.Meta().Name, ch.Revision()),
1069- )
1070- bundleURL, err := url.Parse("http://bundles.testing.invalid/dummy-1")
1071- c.Assert(err, gc.IsNil)
1072- _, err = s.State.AddCharm(ch, curl, bundleURL, "dummy-1-sha256")
1073+ ch, curl, bundleURL, bundleSHA256 := s.dummyCharm(c, "")
1074+ _, err := s.State.AddCharm(ch, curl, bundleURL, bundleSHA256)
1075 c.Assert(err, gc.IsNil)
1076
1077- // First test with already uploaded and a missing charms.
1078- sch, err := s.State.UpdateUploadedCharm(ch, curl, bundleURL, "dummy-1-sha256")
1079+ // Test with already uploaded and a missing charms.
1080+ sch, err := s.State.UpdateUploadedCharm(ch, curl, bundleURL, bundleSHA256)
1081 c.Assert(err, gc.ErrorMatches, fmt.Sprintf("charm %q already uploaded", curl))
1082 c.Assert(sch, gc.IsNil)
1083 missingCurl := charm.MustParseURL("local:quantal/missing-1")
1084@@ -250,7 +322,7 @@
1085 c.Assert(err, jc.Satisfies, errors.IsNotFoundError)
1086 c.Assert(sch, gc.IsNil)
1087
1088- // Now try with an uploaded charm.
1089+ // Test with with an uploaded local charm.
1090 _, err = s.State.PrepareLocalCharmUpload(missingCurl)
1091 c.Assert(err, gc.IsNil)
1092 sch, err = s.State.UpdateUploadedCharm(ch, missingCurl, bundleURL, "missing")
1093@@ -258,6 +330,7 @@
1094 c.Assert(sch.URL(), gc.DeepEquals, missingCurl)
1095 c.Assert(sch.Revision(), gc.Equals, missingCurl.Revision)
1096 c.Assert(sch.IsUploaded(), jc.IsTrue)
1097+ c.Assert(sch.IsPlaceholder(), jc.IsFalse)
1098 c.Assert(sch.Meta(), gc.DeepEquals, ch.Meta())
1099 c.Assert(sch.Config(), gc.DeepEquals, ch.Config())
1100 c.Assert(sch.BundleURL(), gc.DeepEquals, bundleURL)
1101@@ -285,11 +358,8 @@
1102
1103 func (s *StateSuite) TestLatestPlaceholderCharm(c *gc.C) {
1104 // Add a deployed charm
1105- ch := testing.Charms.Dir("dummy")
1106- curl := charm.MustParseURL("cs:quantal/dummy-1")
1107- bundleURL, err := url.Parse("http://bundles.testing.invalid/dummy-1")
1108- c.Assert(err, gc.IsNil)
1109- _, err = s.State.AddCharm(ch, curl, bundleURL, "dummy-1-sha256")
1110+ ch, curl, bundleURL, bundleSHA256 := s.dummyCharm(c, "cs:quantal/dummy-1")
1111+ _, err := s.State.AddCharm(ch, curl, bundleURL, bundleSHA256)
1112 c.Assert(err, gc.IsNil)
1113
1114 // Deployed charm not found.
1115@@ -341,11 +411,8 @@
1116
1117 func (s *StateSuite) assertAddStoreCharmPlaceholder(c *gc.C) (*charm.URL, *charm.URL, *state.Charm) {
1118 // Add a deployed charm
1119- ch := testing.Charms.Dir("dummy")
1120- curl := charm.MustParseURL("cs:quantal/dummy-1")
1121- bundleURL, err := url.Parse("http://bundles.testing.invalid/dummy-1")
1122- c.Assert(err, gc.IsNil)
1123- dummy, err := s.State.AddCharm(ch, curl, bundleURL, "dummy-1-sha256")
1124+ ch, curl, bundleURL, bundleSHA256 := s.dummyCharm(c, "cs:quantal/dummy-1")
1125+ dummy, err := s.State.AddCharm(ch, curl, bundleURL, bundleSHA256)
1126 c.Assert(err, gc.IsNil)
1127
1128 // Add a charm placeholder
1129
1130=== modified file 'testing/charm.go'
1131--- testing/charm.go 2014-01-09 04:04:54 +0000
1132+++ testing/charm.go 2014-01-30 14:23:21 +0000
1133@@ -9,16 +9,11 @@
1134 "os"
1135 "os/exec"
1136 "path/filepath"
1137+ "sync"
1138
1139 "launchpad.net/juju-core/charm"
1140 )
1141
1142-func init() {
1143- p, err := build.Import("launchpad.net/juju-core/testing", "", build.FindOnly)
1144- check(err)
1145- Charms = &Repo{Path: filepath.Join(p.Dir, "repo")}
1146-}
1147-
1148 func check(err error) {
1149 if err != nil {
1150 panic(err)
1151@@ -27,12 +22,27 @@
1152
1153 // Repo represents a charm repository used for testing.
1154 type Repo struct {
1155- Path string
1156+ once sync.Once
1157+ path string
1158+}
1159+
1160+func (r *Repo) Path() string {
1161+ r.once.Do(r.init)
1162+ return r.path
1163+}
1164+
1165+// init is called once when r.Path() is called for the first time, and
1166+// it initializes r.path to the location of the local testing
1167+// repository.
1168+func (r *Repo) init() {
1169+ p, err := build.Import("launchpad.net/juju-core/testing", "", build.FindOnly)
1170+ check(err)
1171+ r.path = filepath.Join(p.Dir, "repo")
1172 }
1173
1174 // Charms represents the specific charm repository stored in this package and
1175 // used by the Juju unit tests. The series name is "quantal".
1176-var Charms *Repo
1177+var Charms = &Repo{}
1178
1179 func clone(dst, src string) string {
1180 check(exec.Command("cp", "-r", src, dst).Run())
1181@@ -42,7 +52,7 @@
1182 // DirPath returns the path to a charm directory with the given name in the
1183 // default series
1184 func (r *Repo) DirPath(name string) string {
1185- return filepath.Join(r.Path, "quantal", name)
1186+ return filepath.Join(r.Path(), "quantal", name)
1187 }
1188
1189 // Dir returns the actual charm.Dir named name.
1190
1191=== modified file 'utils/trivial.go'
1192--- utils/trivial.go 2014-01-14 06:49:14 +0000
1193+++ utils/trivial.go 2014-01-30 14:23:21 +0000
1194@@ -6,8 +6,11 @@
1195 import (
1196 "bytes"
1197 "compress/gzip"
1198+ "crypto/sha256"
1199+ "encoding/hex"
1200 "errors"
1201 "fmt"
1202+ "io"
1203 "io/ioutil"
1204 "os"
1205 "strings"
1206@@ -118,3 +121,26 @@
1207 }
1208 return ioutil.ReadAll(r)
1209 }
1210+
1211+// ReadSHA256 returns the SHA256 hash of the contents read from source
1212+// (hex encoded) and the size of the source in bytes.
1213+func ReadSHA256(source io.Reader) (string, int64, error) {
1214+ hash := sha256.New()
1215+ size, err := io.Copy(hash, source)
1216+ if err != nil {
1217+ return "", 0, err
1218+ }
1219+ digest := hex.EncodeToString(hash.Sum(nil))
1220+ return digest, size, nil
1221+}
1222+
1223+// ReadFileSHA256 is like ReadSHA256 but reads the contents of the
1224+// given file.
1225+func ReadFileSHA256(filename string) (string, int64, error) {
1226+ f, err := os.Open(filename)
1227+ if err != nil {
1228+ return "", 0, err
1229+ }
1230+ defer f.Close()
1231+ return ReadSHA256(f)
1232+}
1233
1234=== modified file 'utils/trivial_test.go'
1235--- utils/trivial_test.go 2014-01-17 02:35:28 +0000
1236+++ utils/trivial_test.go 2014-01-30 14:23:21 +0000
1237@@ -4,6 +4,10 @@
1238 package utils_test
1239
1240 import (
1241+ "bytes"
1242+ "fmt"
1243+ "io/ioutil"
1244+ "path/filepath"
1245 "strings"
1246 "testing"
1247
1248@@ -36,7 +40,7 @@
1249 }
1250 )
1251
1252-func (utilsSuite) TestCompression(c *gc.C) {
1253+func (*utilsSuite) TestCompression(c *gc.C) {
1254 cdata := utils.Gzip(data)
1255 c.Assert(len(cdata) < len(data), gc.Equals, true)
1256 data1, err := utils.Gunzip(cdata)
1257@@ -48,7 +52,7 @@
1258 c.Assert(data1, gc.DeepEquals, data)
1259 }
1260
1261-func (utilsSuite) TestCommandString(c *gc.C) {
1262+func (*utilsSuite) TestCommandString(c *gc.C) {
1263 type test struct {
1264 args []string
1265 expected string
1266@@ -71,3 +75,43 @@
1267 c.Assert(result, gc.Equals, test.expected)
1268 }
1269 }
1270+
1271+func (*utilsSuite) TestReadSHA256AndReadFileSHA256(c *gc.C) {
1272+ sha256Tests := []struct {
1273+ content string
1274+ sha256 string
1275+ }{{
1276+ content: "",
1277+ sha256: "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
1278+ }, {
1279+ content: "some content",
1280+ sha256: "290f493c44f5d63d06b374d0a5abd292fae38b92cab2fae5efefe1b0e9347f56",
1281+ }, {
1282+ content: "foo",
1283+ sha256: "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
1284+ }, {
1285+ content: "Foo",
1286+ sha256: "1cbec737f863e4922cee63cc2ebbfaafcd1cff8b790d8cfd2e6a5d550b648afa",
1287+ }, {
1288+ content: "multi\nline\ntext\nhere",
1289+ sha256: "c384f11c0294280792a44d9d6abb81f9fd991904cb7eb851a88311b04114231e",
1290+ }}
1291+
1292+ tempDir := c.MkDir()
1293+ for i, test := range sha256Tests {
1294+ c.Logf("test %d: %q -> %q", i, test.content, test.sha256)
1295+ buf := bytes.NewBufferString(test.content)
1296+ hash, size, err := utils.ReadSHA256(buf)
1297+ c.Check(err, gc.IsNil)
1298+ c.Check(hash, gc.Equals, test.sha256)
1299+ c.Check(int(size), gc.Equals, len(test.content))
1300+
1301+ tempFileName := filepath.Join(tempDir, fmt.Sprintf("sha256-%d", i))
1302+ err = ioutil.WriteFile(tempFileName, []byte(test.content), 0644)
1303+ c.Check(err, gc.IsNil)
1304+ fileHash, fileSize, err := utils.ReadFileSHA256(tempFileName)
1305+ c.Check(err, gc.IsNil)
1306+ c.Check(fileHash, gc.Equals, hash)
1307+ c.Check(fileSize, gc.Equals, size)
1308+ }
1309+}
1310
1311=== modified file 'worker/uniter/charm/charm.go'
1312--- worker/uniter/charm/charm.go 2013-10-03 13:13:25 +0000
1313+++ worker/uniter/charm/charm.go 2014-01-30 14:23:21 +0000
1314@@ -4,10 +4,7 @@
1315 package charm
1316
1317 import (
1318- "crypto/sha256"
1319- "encoding/hex"
1320 "fmt"
1321- "io"
1322 "os"
1323 "path"
1324
1325@@ -75,11 +72,10 @@
1326 }
1327 log.Infof("worker/uniter/charm: download complete")
1328 defer st.File.Close()
1329- hash := sha256.New()
1330- if _, err = io.Copy(hash, st.File); err != nil {
1331+ actualSha256, _, err := utils.ReadSHA256(st.File)
1332+ if err != nil {
1333 return err
1334 }
1335- actualSha256 := hex.EncodeToString(hash.Sum(nil))
1336 archiveSha256, err := sch.ArchiveSha256()
1337 if err != nil {
1338 return err
1339
1340=== modified file 'worker/uniter/uniter_test.go'
1341--- worker/uniter/uniter_test.go 2014-01-28 03:53:50 +0000
1342+++ worker/uniter/uniter_test.go 2014-01-30 14:23:21 +0000
1343@@ -5,10 +5,7 @@
1344
1345 import (
1346 "bytes"
1347- "crypto/sha256"
1348- "encoding/hex"
1349 "fmt"
1350- "io"
1351 "io/ioutil"
1352 "net/rpc"
1353 "net/url"
1354@@ -1199,10 +1196,8 @@
1355 err := s.dir.BundleTo(&buf)
1356 c.Assert(err, gc.IsNil)
1357 body := buf.Bytes()
1358- hasher := sha256.New()
1359- _, err = io.Copy(hasher, &buf)
1360+ hash, _, err := utils.ReadSHA256(&buf)
1361 c.Assert(err, gc.IsNil)
1362- hash := hex.EncodeToString(hasher.Sum(nil))
1363 key := fmt.Sprintf("/charms/%s/%d", s.dir.Meta().Name, s.dir.Revision())
1364 hurl, err := url.Parse(coretesting.Server.URL + key)
1365 c.Assert(err, gc.IsNil)

Subscribers

People subscribed via source and target branches

to status/vote changes: