-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
c300cd0
commit 37be8f5
Showing
5 changed files
with
360 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
// The upgrades package provides infrastructure to upgrade previous Juju | ||
// deployments to the current Juju version. The upgrade is performed on | ||
// a per node basis, across all of the running Juju machines. | ||
// | ||
// Important exported APIs include: | ||
// PerformUpgrade, which is invoked on each node by the machine agent with: | ||
// fromVersion - the Juju version from which the upgrade is occurring | ||
// target - the type of Juju node being upgraded | ||
// context - provides API access to Juju state servers | ||
// | ||
// More to come... | ||
// | ||
package upgrades |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
// Copyright 2014 Canonical Ltd. | ||
// Licensed under the AGPLv3, see LICENCE file for details. | ||
|
||
package upgrades | ||
|
||
var UpgradeOperations = &upgradeOperations |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
// Copyright 2014 Canonical Ltd. | ||
// Licensed under the AGPLv3, see LICENCE file for details. | ||
|
||
package upgrades | ||
|
||
// stepsFor118 returns upgrade steps to upgrade to a Juju 1.18 deployment. | ||
func stepsFor118(context Context) []UpgradeStep { | ||
return []UpgradeStep{ | ||
// Nothing yet. | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,167 @@ | ||
// Copyright 2014 Canonical Ltd. | ||
// Licensed under the AGPLv3, see LICENCE file for details. | ||
|
||
package upgrades | ||
|
||
import ( | ||
"fmt" | ||
|
||
"launchpad.net/juju-core/state/api" | ||
"launchpad.net/juju-core/version" | ||
) | ||
|
||
// UpgradeStep defines an idempotent operation that is run to perform | ||
// a specific upgrade step. | ||
type UpgradeStep interface { | ||
// A human readable description of what the upgrade step does. | ||
Description() string | ||
// The target machine types for which the upgrade step is applicable. | ||
Targets() []UpgradeTarget | ||
// The upgrade business logic. | ||
Run() error | ||
} | ||
|
||
// UpgradeOperation defines a slice of upgrade steps. | ||
type UpgradeOperation interface { | ||
// The Juju version for which this operation is applicable. | ||
// Upgrade operations designed for versions of Juju earlier | ||
// than we are upgrading from are not run since such steps would | ||
// already have been used to get to the version we are running now. | ||
TargetVersion() version.Number | ||
// Steps to perform during an upgrade. | ||
Steps() []UpgradeStep | ||
} | ||
|
||
// UpgradeTarget defines the type of machine for which a particular upgrade | ||
// step can be run. | ||
type UpgradeTarget string | ||
|
||
const ( | ||
HostMachine = UpgradeTarget("hostMachine") | ||
HostContainer = UpgradeTarget("hostContainer") | ||
StateServer = UpgradeTarget("stateServer") | ||
) | ||
|
||
// upgradeToVersion encapsulates the steps which need to be run to | ||
// upgrade any prior version of Juju to targetVersion. | ||
type upgradeToVersion struct { | ||
targetVersion version.Number | ||
steps []UpgradeStep | ||
} | ||
|
||
// Steps is defined on the UpgradeOperation interface. | ||
func (u upgradeToVersion) Steps() []UpgradeStep { | ||
return u.steps | ||
} | ||
|
||
// TargetVersion is defined on the UpgradeOperation interface. | ||
func (u upgradeToVersion) TargetVersion() version.Number { | ||
return u.targetVersion | ||
} | ||
|
||
// Context is used give the upgrade steps attributes needed | ||
// to do their job. | ||
type Context interface { | ||
// An API connection to state. | ||
APIState() *api.State | ||
} | ||
|
||
// UpgradeContext is a default Context implementation. | ||
type UpgradeContext struct { | ||
st *api.State | ||
} | ||
|
||
// APIState is defined on the Context interface. | ||
func (c *UpgradeContext) APIState() *api.State { | ||
return c.st | ||
} | ||
|
||
// upgradeOperation provides base attributes for any upgrade step. | ||
type upgradeOperation struct { | ||
description string | ||
targets []UpgradeTarget | ||
st *api.State | ||
} | ||
|
||
// Description is defined on the UpgradeStep interface. | ||
func (u *upgradeOperation) Description() string { | ||
return u.description | ||
} | ||
|
||
// Targets is defined on the UpgradeStep interface. | ||
func (u *upgradeOperation) Targets() []UpgradeTarget { | ||
return u.targets | ||
} | ||
|
||
// upgradeOperations returns an ordered slice of sets of operations needed | ||
// to upgrade Juju to particular version. The slice is ordered by target | ||
// version, so that the sets of operations are executed in order from oldest | ||
// version to most recent. | ||
var upgradeOperations = func(context Context) []UpgradeOperation { | ||
steps := []UpgradeOperation{ | ||
upgradeToVersion{ | ||
version.MustParse("1.18.0"), | ||
stepsFor118(context), | ||
}, | ||
} | ||
return steps | ||
} | ||
|
||
// upgradeError records a description of the step being performed and the error. | ||
type upgradeError struct { | ||
description string | ||
err error | ||
} | ||
|
||
func (e *upgradeError) Error() string { | ||
return fmt.Sprintf("%s: %v", e.description, e.err) | ||
} | ||
|
||
// PerformUpgrade runs the business logic needed to upgrade the current "from" version to this | ||
// version of Juju on the "target" type of machine. | ||
func PerformUpgrade(from version.Number, target UpgradeTarget, context Context) *upgradeError { | ||
// If from is not known, it is 1.16. | ||
if from == version.Zero { | ||
from = version.MustParse("1.16.0") | ||
} | ||
for _, upgradeOps := range upgradeOperations(context) { | ||
// Do not run steps for versions of Juju earlier than we are upgrading from. | ||
if upgradeOps.TargetVersion().Less(from) { | ||
continue | ||
} | ||
if err := runUpgradeSteps(target, upgradeOps); err != nil { | ||
return err | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
// validTarget returns true if target is in step.Targets(). | ||
func validTarget(target UpgradeTarget, step UpgradeStep) bool { | ||
for _, opTarget := range step.Targets() { | ||
if target == opTarget { | ||
return true | ||
} | ||
} | ||
return len(step.Targets()) == 0 | ||
} | ||
|
||
// runUpgradeSteps runs all the upgrade steps relevant to target. | ||
// As soon as any error is encountered, the operation is aborted since | ||
// subsequent steps may required successful completion of earlier ones. | ||
// The steps must be idempotent so that the entire upgrade operation can | ||
// be retried. | ||
func runUpgradeSteps(target UpgradeTarget, upgradeOp UpgradeOperation) *upgradeError { | ||
for _, step := range upgradeOp.Steps() { | ||
if !validTarget(target, step) { | ||
continue | ||
} | ||
if err := step.Run(); err != nil { | ||
return &upgradeError{ | ||
description: step.Description(), | ||
err: err, | ||
} | ||
} | ||
} | ||
return nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,163 @@ | ||
// Copyright 2014 Canonical Ltd. | ||
// Licensed under the AGPLv3, see LICENCE file for details. | ||
|
||
package upgrades_test | ||
|
||
import ( | ||
"errors" | ||
"strings" | ||
stdtesting "testing" | ||
|
||
gc "launchpad.net/gocheck" | ||
|
||
jujutesting "launchpad.net/juju-core/juju/testing" | ||
"launchpad.net/juju-core/state/api" | ||
coretesting "launchpad.net/juju-core/testing" | ||
jc "launchpad.net/juju-core/testing/checkers" | ||
"launchpad.net/juju-core/upgrades" | ||
"launchpad.net/juju-core/version" | ||
) | ||
|
||
func TestPackage(t *stdtesting.T) { | ||
coretesting.MgoTestPackage(t) | ||
} | ||
|
||
type upgradeSuite struct { | ||
jujutesting.JujuConnSuite | ||
} | ||
|
||
var _ = gc.Suite(&upgradeSuite{}) | ||
|
||
type mockUpgradeOperation struct { | ||
targetVersion version.Number | ||
steps []upgrades.UpgradeStep | ||
} | ||
|
||
func (m *mockUpgradeOperation) TargetVersion() version.Number { | ||
return m.targetVersion | ||
} | ||
|
||
func (m *mockUpgradeOperation) Steps() []upgrades.UpgradeStep { | ||
return m.steps | ||
} | ||
|
||
type mockUpgradeStep struct { | ||
msg string | ||
targets []upgrades.UpgradeTarget | ||
context *mockContext | ||
} | ||
|
||
func (u *mockUpgradeStep) Description() string { | ||
return u.msg | ||
} | ||
|
||
func (u *mockUpgradeStep) Targets() []upgrades.UpgradeTarget { | ||
return u.targets | ||
} | ||
|
||
func (u *mockUpgradeStep) Run() error { | ||
if strings.HasSuffix(u.msg, "error") { | ||
return errors.New("upgrade error occurred") | ||
} | ||
u.context.messages = append(u.context.messages, u.msg) | ||
return nil | ||
} | ||
|
||
type mockContext struct { | ||
messages []string | ||
} | ||
|
||
func (c *mockContext) APIState() *api.State { | ||
return nil | ||
} | ||
|
||
func upgradeOperations(context upgrades.Context) []upgrades.UpgradeOperation { | ||
mockContext := context.(*mockContext) | ||
steps := []upgrades.UpgradeOperation{ | ||
&mockUpgradeOperation{ | ||
targetVersion: version.MustParse("1.12.0"), | ||
steps: []upgrades.UpgradeStep{ | ||
&mockUpgradeStep{"step 1 - 1.12.0", nil, mockContext}, | ||
&mockUpgradeStep{"step 2 error", []upgrades.UpgradeTarget{upgrades.HostMachine}, mockContext}, | ||
&mockUpgradeStep{"step 3", []upgrades.UpgradeTarget{upgrades.HostMachine}, mockContext}, | ||
}, | ||
}, | ||
&mockUpgradeOperation{ | ||
targetVersion: version.MustParse("1.13.0"), | ||
steps: []upgrades.UpgradeStep{ | ||
&mockUpgradeStep{"step 1 - 1.13.0", nil, mockContext}, | ||
&mockUpgradeStep{"step 2 - 1.13.0", []upgrades.UpgradeTarget{upgrades.HostMachine}, mockContext}, | ||
&mockUpgradeStep{"step 3 - 1.13.0", []upgrades.UpgradeTarget{upgrades.StateServer}, mockContext}, | ||
}, | ||
}, | ||
&mockUpgradeOperation{ | ||
targetVersion: version.MustParse("1.16.0"), | ||
steps: []upgrades.UpgradeStep{ | ||
&mockUpgradeStep{"step 1 - 1.16.0", []upgrades.UpgradeTarget{upgrades.HostMachine}, mockContext}, | ||
&mockUpgradeStep{"step 2 - 1.16.0", []upgrades.UpgradeTarget{upgrades.HostMachine}, mockContext}, | ||
&mockUpgradeStep{"step 3 - 1.16.0", []upgrades.UpgradeTarget{upgrades.StateServer}, mockContext}, | ||
}, | ||
}, | ||
} | ||
return steps | ||
} | ||
|
||
type upgradeTest struct { | ||
about string | ||
fromVersion string | ||
target upgrades.UpgradeTarget | ||
expectedSteps []string | ||
err string | ||
} | ||
|
||
var upgradeTests = []upgradeTest{ | ||
{ | ||
about: "from version excludes older steps", | ||
fromVersion: "1.16.0", | ||
target: upgrades.HostMachine, | ||
expectedSteps: []string{"step 1 - 1.16.0", "step 2 - 1.16.0"}, | ||
}, | ||
{ | ||
about: "incompatible targets excluded", | ||
fromVersion: "1.13.0", | ||
target: upgrades.StateServer, | ||
expectedSteps: []string{"step 1 - 1.13.0", "step 3 - 1.13.0", "step 3 - 1.16.0"}, | ||
}, | ||
{ | ||
about: "error aborts, subsequent steps not run", | ||
fromVersion: "1.12.0", | ||
target: upgrades.HostMachine, | ||
expectedSteps: []string{"step 1 - 1.12.0"}, | ||
err: "step 2 error: upgrade error occurred", | ||
}, | ||
} | ||
|
||
func (s *upgradeSuite) TestPerformUpgrade(c *gc.C) { | ||
s.PatchValue(upgrades.UpgradeOperations, upgradeOperations) | ||
for i, test := range upgradeTests { | ||
c.Logf("%d: %s", i, test.about) | ||
var messages []string | ||
ctx := &mockContext{ | ||
messages: messages, | ||
} | ||
fromVersion := version.MustParse(test.fromVersion) | ||
err := upgrades.PerformUpgrade(fromVersion, test.target, ctx) | ||
if test.err == "" { | ||
c.Assert(err, gc.IsNil) | ||
} else { | ||
c.Assert(err, gc.ErrorMatches, test.err) | ||
} | ||
c.Assert(ctx.messages, gc.DeepEquals, test.expectedSteps) | ||
} | ||
} | ||
|
||
func (s *upgradeSuite) TestUpgradeOperationsOrdered(c *gc.C) { | ||
var previous version.Number | ||
for i, utv := range (*upgrades.UpgradeOperations)(nil) { | ||
vers := utv.TargetVersion() | ||
if i > 0 { | ||
c.Assert(previous.Less(vers), jc.IsTrue) | ||
} | ||
previous = vers | ||
} | ||
} |