Skip to content

Commit

Permalink
Initial upgrade framework
Browse files Browse the repository at this point in the history
  • Loading branch information
wallyworld committed Feb 10, 2014
1 parent c300cd0 commit 37be8f5
Show file tree
Hide file tree
Showing 5 changed files with 360 additions and 0 deletions.
13 changes: 13 additions & 0 deletions upgrades/doc.go
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
6 changes: 6 additions & 0 deletions upgrades/export_test.go
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
11 changes: 11 additions & 0 deletions upgrades/steps118.go
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.
}
}
167 changes: 167 additions & 0 deletions upgrades/upgrade.go
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
}
163 changes: 163 additions & 0 deletions upgrades/upgrade_test.go
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
}
}

0 comments on commit 37be8f5

Please sign in to comment.