Merge lp:~dimitern/juju-core/260-lp-1067979-duplicate-charm into lp:~go-bot/juju-core/trunk
- 260-lp-1067979-duplicate-charm
- Merge into trunk
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 |
Related bugs: |
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 PrepareStoreCha
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-
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:/
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 PrepareStoreCha
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-
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.
Dimiter Naydenov (dimitern) wrote : | # |
William Reade (fwereade) wrote : | # |
A few comments, reprising what we discussed at lunch
https:/
File state/apiserver
https:/
state/apiserver
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:/
File state/apiserver
https:/
state/apiserver
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.
Dimiter Naydenov (dimitern) wrote : | # |
Please take a look.
https:/
File state/apiserver
https:/
state/apiserver
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:/
File state/apiserver
https:/
state/apiserver
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.
Dimiter Naydenov (dimitern) wrote : | # |
Please take a look.
William Reade (fwereade) wrote : | # |
few more comments, but the heart of it looks solid
https:/
File charm/url.go (right):
https:/
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:/
File state/apiserver
https:/
state/apiserver
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:/
File state/apiserver
https:/
state/apiserver
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:/
File state/apiserver
https:/
state/apiserver
race? we should expect 9 Deletes as well, I think
https:/
File state/state.go (right):
https:/
state/state.go:606: err = st.charms.
{"placeholder", false}}
placeholder might be false in a new installation, or missing in an old
one
https:/
state/state.go:624: Insert: uploadedCharm,
if there *was* a placeholder charm, the insert would fail, wouldn't it?
https:/
state/state.go:642: StillPlaceholder = D{{"placeholder", true}}
I think these should probably not be exported
Dimiter Naydenov (dimitern) wrote : | # |
Please take a look.
https:/
File charm/url.go (right):
https:/
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:/
File state/apiserver
https:/
state/apiserver
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:/
File state/apiserver
https:/
state/apiserver
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:/
File state/apiserver
https:/
state/apiserver
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:/
File state/state.go (right):
https:/
state/state.go:606: err = st.charms.
{"placeholder", false}}
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:/
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:/
state/state.go:642: StillPlaceholder = D{{"placeholder", true}}
On 2014/01/28 08:19:41, fwereade wrote:
> I think these should pr...
William Reade (fwereade) wrote : | # |
Very nice, last round I think
https:/
File state/apiserver
https:/
state/apiserver
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:/
File state/apiserver
https:/
state/apiserver
(storage.Storage, error) {
maybe put this in export_test rather than growing the interface here?
https:/
File state/apiserver
https:/
state/apiserver
apiserver.
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:/
File state/state.go (right):
https:/
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:/
state/state.go:625: Insert: uploadedCharm,
Don't we still need to deal with a placeholder here?
https:/
File utils/trivial.go (right):
https:/
utils/trivial.
error) {
ReadSHA256?
https:/
utils/trivial.
int64, error) {
ReadFileSHA256?
https:/
utils/trivial.
Also, please test these. Not quite trivial enough to be skippable IMO.
Dimiter Naydenov (dimitern) wrote : | # |
Please take a look.
https:/
File state/apiserver
https:/
state/apiserver
(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:/
File state/apiserver
https:/
state/apiserver
apiserver.
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:/
File state/state.go (right):
https:/
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:/
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:/
File utils/trivial.go (right):
https:/
utils/trivial.
error) {
On 2014/01/28 12:00:32, fwereade wrote:
> ReadSHA256?
Done.
https:/
utils/trivial.
int64, error) {
On 2014/01/28 12:00:32, fwereade wrote:
> ReadFileSHA256?
Done.
https:/
utils/trivial.
On 2014/01/28 12:00:32, fwereade wrote:
> Also, please test these. Not quite trivial enough to be skippable IMO.
Done.
William Reade (fwereade) wrote : | # |
I still think there's something up with PrepareStoreCha
it's getting late and I may be being dense. Ping me tomorrow morning?
https:/
File state/state.go (right):
https:/
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.
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 SetTransactionH
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.
Dimiter Naydenov (dimitern) wrote : | # |
Please take a look.
https:/
File state/state.go (right):
https:/
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.
> 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 SetTransactionH
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.
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:/
File state/charm.go (right):
https:/
state/charm.go:87: func CharmArchiveNam
(string, error) {
quibble quibble whine, I think this probably belongs in apiserver/client
https:/
File state/state.go (right):
https:/
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:/
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
ErrExcessiveCon
https:/
File utils/trivial_
https:/
utils/trivial_
}{{
...
},{
...
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:/
utils/trivial_
TestReadSHA256A
*utilsSuite, please, across the board, lest someone add methods that
require pointer receivers and silently deactivate all the other tests.
Dimiter Naydenov (dimitern) wrote : | # |
Please take a look.
https:/
File state/charm.go (right):
https:/
state/charm.go:87: func CharmArchiveNam
(string, error) {
On 2014/01/29 16:01:22, fwereade wrote:
> quibble quibble whine, I think this probably belongs in
apiserver/client
Done.
https:/
File state/state.go (right):
https:/
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:/
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 ErrExcessiveCon
Done.
https:/
File utils/trivial_
https:/
utils/trivial_
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:/
utils/trivial_
TestReadSHA256A
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.
Go Bot (go-bot) wrote : | # |
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.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad....
Go Bot (go-bot) wrote : | # |
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.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad...
Go Bot (go-bot) wrote : | # |
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.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad....
Go Bot (go-bot) wrote : | # |
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.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad...
Dimiter Naydenov (dimitern) wrote : | # |
Please take a look.
Dimiter Naydenov (dimitern) wrote : | # |
Self-approving after fixing uniter tests.
Go Bot (go-bot) wrote : | # |
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.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
*** Test killed: ran too long (10m0s).
FAIL launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.n...
Dimiter Naydenov (dimitern) wrote : | # |
Please take a look.
Go Bot (go-bot) wrote : | # |
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.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
? launchpad.
? launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
? launchpad.
ok launchpad.
ok launchpad.
ok launchpad.
? launchpad...
Preview Diff
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) |
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 rmUpload
charms are added through the API (in state and
to provider storage). Now PrepareStoreCha
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): /charms_ test.go /client/ client. go /client/ client_ test.go
A [revision details]
M state/apiserver
M state/apiserver
M state/apiserver
M state/state.go
M state/state_test.go