- Re-introduce config migration; fix panics occurring from older configs

- Introduce boilerplate for config.json migrations

Signed-off-by: Nathan LeClaire <nathan.leclaire@gmail.com>
This commit is contained in:
Nathan LeClaire 2015-07-17 18:02:02 -07:00
parent 79b9450fef
commit fb2e843e99
11 changed files with 270 additions and 177 deletions

View File

@ -437,7 +437,7 @@ func (c *GenericClient) Authenticate(d *Driver) error {
return err return err
} }
provider.UserAgent.Prepend(fmt.Sprintf("docker-machine/v%s", version.VERSION)) provider.UserAgent.Prepend(fmt.Sprintf("docker-machine/v%s", version.Version))
if d.Insecure { if d.Insecure {
// Configure custom TLS settings. // Configure custom TLS settings.

View File

@ -42,7 +42,7 @@ func (c *Client) Authenticate(d *openstack.Driver) error {
return err return err
} }
provider.UserAgent.Prepend(fmt.Sprintf("docker-machine/v%s", version.VERSION)) provider.UserAgent.Prepend(fmt.Sprintf("docker-machine/v%s", version.Version))
err = rackspace.Authenticate(provider, opts) err = rackspace.Authenticate(provider, opts)
if err != nil { if err != nil {

View File

@ -19,7 +19,11 @@ type Filestore struct {
} }
func NewFilestore(rootPath string, caCert string, privateKey string) *Filestore { func NewFilestore(rootPath string, caCert string, privateKey string) *Filestore {
return &Filestore{path: rootPath, caCertPath: caCert, privateKeyPath: privateKey} return &Filestore{
path: rootPath,
caCertPath: caCert,
privateKeyPath: privateKey,
}
} }
func (s Filestore) loadHost(name string) (*Host, error) { func (s Filestore) loadHost(name string) (*Host, error) {
@ -30,13 +34,15 @@ func (s Filestore) loadHost(name string) (*Host, error) {
} }
} }
host := &Host{Name: name, StorePath: hostPath} host := &Host{
Name: name,
StorePath: hostPath,
}
if err := host.LoadConfig(); err != nil { if err := host.LoadConfig(); err != nil {
return nil, err return nil, err
} }
h := FillNestedHost(host) return host, nil
return h, nil
} }
func (s Filestore) GetPath() string { func (s Filestore) GetPath() string {
@ -85,7 +91,7 @@ func (s Filestore) List() ([]*Host, error) {
for _, file := range dir { for _, file := range dir {
// don't load hidden dirs; used for configs // don't load hidden dirs; used for configs
if file.IsDir() && strings.Index(file.Name(), ".") != 0 { if file.IsDir() && !strings.HasPrefix(file.Name(), ".") {
host, err := s.Get(file.Name()) host, err := s.Get(file.Name())
if err != nil { if err != nil {
log.Errorf("error loading host %q: %s", file.Name(), err) log.Errorf("error loading host %q: %s", file.Name(), err)

View File

@ -19,6 +19,7 @@ import (
"github.com/docker/machine/ssh" "github.com/docker/machine/ssh"
"github.com/docker/machine/state" "github.com/docker/machine/state"
"github.com/docker/machine/utils" "github.com/docker/machine/utils"
"github.com/docker/machine/version"
) )
var ( var (
@ -28,22 +29,12 @@ var (
) )
type Host struct { type Host struct {
Name string `json:"-"` ConfigVersion int
DriverName string
Driver drivers.Driver Driver drivers.Driver
StorePath string DriverName string
HostOptions *HostOptions HostOptions *HostOptions
Name string `json:"-"`
// deprecated options; these are left to assist in config migrations StorePath string
SwarmHost string
SwarmMaster bool
SwarmDiscovery string
CaCertPath string
PrivateKeyPath string
ServerCertPath string
ServerKeyPath string
ClientCertPath string
ClientKeyPath string
} }
type HostOptions struct { type HostOptions struct {
@ -56,14 +47,9 @@ type HostOptions struct {
} }
type HostMetadata struct { type HostMetadata struct {
ConfigVersion int
DriverName string DriverName string
HostOptions HostOptions HostOptions HostOptions
StorePath string
CaCertPath string
PrivateKeyPath string
ServerCertPath string
ServerKeyPath string
ClientCertPath string
} }
type HostListItem struct { type HostListItem struct {
@ -75,6 +61,14 @@ type HostListItem struct {
SwarmOptions swarm.SwarmOptions SwarmOptions swarm.SwarmOptions
} }
type ErrSavingConfig struct {
wrappedErr error
}
func (e ErrSavingConfig) Error() string {
return fmt.Sprintf("Error saving config: %s", e.wrappedErr)
}
func NewHost(name, driverName string, hostOptions *HostOptions) (*Host, error) { func NewHost(name, driverName string, hostOptions *HostOptions) (*Host, error) {
authOptions := hostOptions.AuthOptions authOptions := hostOptions.AuthOptions
storePath := filepath.Join(utils.GetMachineDir(), name) storePath := filepath.Join(utils.GetMachineDir(), name)
@ -84,6 +78,7 @@ func NewHost(name, driverName string, hostOptions *HostOptions) (*Host, error) {
} }
return &Host{ return &Host{
Name: name, Name: name,
ConfigVersion: version.ConfigVersion,
DriverName: driverName, DriverName: driverName,
Driver: driver, Driver: driver,
StorePath: storePath, StorePath: storePath,
@ -100,6 +95,7 @@ func LoadHost(name string, StorePath string) (*Host, error) {
if err := host.LoadConfig(); err != nil { if err := host.LoadConfig(); err != nil {
return nil, err return nil, err
} }
return host, nil return host, nil
} }
@ -287,26 +283,28 @@ func (h *Host) LoadConfig() error {
return err return err
} }
// First pass: find the driver name and load the driver // Remember the machine name and store path so we don't have to pass it
var hostMetadata HostMetadata // through each struct in the migration.
if err := json.Unmarshal(data, &hostMetadata); err != nil { name := h.Name
return err storePath := h.StorePath
}
meta := FillNestedHostMetadata(&hostMetadata) // If we end up performing a migration, we should save afterwards so we don't have to do it again on subsequent invocations.
migrationPerformed := h.ConfigVersion != version.ConfigVersion
authOptions := meta.HostOptions.AuthOptions migratedHost, err := MigrateHost(h, data)
driver, err := drivers.NewDriver(hostMetadata.DriverName, h.Name, h.StorePath, authOptions.CaCertPath, authOptions.PrivateKeyPath)
if err != nil { if err != nil {
return err return fmt.Errorf("Error getting migrated host: %s", err)
} }
h.Driver = driver *h = *migratedHost
// Second pass: unmarshal driver config into correct driver h.Name = name
if err := json.Unmarshal(data, &h); err != nil { h.StorePath = storePath
return err
if migrationPerformed {
if err := h.SaveConfig(); err != nil {
return fmt.Errorf("Error saving config after migration was performed: %s", err)
}
} }
return nil return nil

34
libmachine/host_v0.go Normal file
View File

@ -0,0 +1,34 @@
package libmachine
import "github.com/docker/machine/drivers"
type HostV0 struct {
Name string `json:"-"`
Driver drivers.Driver
DriverName string
ConfigVersion int
HostOptions *HostOptions
StorePath string
CaCertPath string
PrivateKeyPath string
ServerCertPath string
ServerKeyPath string
ClientCertPath string
SwarmHost string
SwarmMaster bool
SwarmDiscovery string
ClientKeyPath string
}
type HostMetadataV0 struct {
HostOptions HostOptions
DriverName string
StorePath string
CaCertPath string
PrivateKeyPath string
ServerCertPath string
ServerKeyPath string
ClientCertPath string
}

View File

@ -1,122 +1,65 @@
package libmachine package libmachine
import ( import (
"path/filepath" "encoding/json"
"fmt"
"github.com/docker/machine/libmachine/auth" "github.com/docker/machine/drivers"
"github.com/docker/machine/libmachine/engine" "github.com/docker/machine/version"
"github.com/docker/machine/libmachine/swarm"
"github.com/docker/machine/utils"
) )
// In the 0.0.1 => 0.0.2 transition, the JSON representation of func getMigratedHostMetadata(data []byte) (*HostMetadata, error) {
// machines changed from a "flat" to a more "nested" structure // HostMetadata is for a "first pass" so we can then load the driver
// for various options and configuration settings. To preserve var (
// compatibility with existing machines, these migration functions hostMetadata *HostMetadataV0
// have been introduced. They preserve backwards compat at the expense )
// of some duplicated information.
// validates host config and modifies if needed if err := json.Unmarshal(data, &hostMetadata); err != nil {
// this is used for configuration updates return &HostMetadata{}, err
func FillNestedHost(host *Host) *Host {
certInfo := getCertInfoFromHost(host)
if host.HostOptions == nil {
host.HostOptions = &HostOptions{}
}
if host.HostOptions.EngineOptions == nil {
host.HostOptions.EngineOptions = &engine.EngineOptions{}
} }
if host.HostOptions.SwarmOptions == nil { migratedHostMetadata := MigrateHostMetadataV0ToHostMetadataV1(hostMetadata)
host.HostOptions.SwarmOptions = &swarm.SwarmOptions{
Address: "",
Discovery: host.SwarmDiscovery,
Host: host.SwarmHost,
Master: host.SwarmMaster,
}
}
host.HostOptions.AuthOptions = &auth.AuthOptions{ return migratedHostMetadata, nil
StorePath: host.StorePath,
CaCertPath: certInfo.CaCertPath,
CaCertRemotePath: "",
ServerCertPath: certInfo.ServerCertPath,
ServerKeyPath: certInfo.ServerKeyPath,
ClientKeyPath: certInfo.ClientKeyPath,
ServerCertRemotePath: "",
ServerKeyRemotePath: "",
PrivateKeyPath: certInfo.CaKeyPath,
ClientCertPath: certInfo.ClientCertPath,
}
return host
} }
// fills nested host metadata and modifies if needed func MigrateHost(h *Host, data []byte) (*Host, error) {
// this is used for configuration updates migratedHostMetadata, err := getMigratedHostMetadata(data)
func FillNestedHostMetadata(m *HostMetadata) *HostMetadata { if err != nil {
if m.HostOptions.EngineOptions == nil { return &Host{}, err
m.HostOptions.EngineOptions = &engine.EngineOptions{}
} }
if m.HostOptions.AuthOptions == nil { authOptions := migratedHostMetadata.HostOptions.AuthOptions
m.HostOptions.AuthOptions = &auth.AuthOptions{
StorePath: m.StorePath, driver, err := drivers.NewDriver(
CaCertPath: m.CaCertPath, migratedHostMetadata.DriverName,
CaCertRemotePath: "", h.Name,
ServerCertPath: m.ServerCertPath, h.StorePath,
ServerKeyPath: m.ServerKeyPath, authOptions.CaCertPath,
ClientKeyPath: "", authOptions.PrivateKeyPath,
ServerCertRemotePath: "", )
ServerKeyRemotePath: "", if err != nil {
PrivateKeyPath: m.PrivateKeyPath, return &Host{}, err
ClientCertPath: m.ClientCertPath, }
for h.ConfigVersion = migratedHostMetadata.ConfigVersion; h.ConfigVersion < version.ConfigVersion; h.ConfigVersion++ {
switch h.ConfigVersion {
case 0:
hostV0 := &HostV0{
Driver: driver,
}
if err := json.Unmarshal(data, &hostV0); err != nil {
return &Host{}, fmt.Errorf("Error unmarshalling host config version 0: %s", err)
}
h = MigrateHostV0ToHostV1(hostV0)
default:
} }
} }
return m h.Driver = driver
} if err := json.Unmarshal(data, &h); err != nil {
return &Host{}, fmt.Errorf("Error unmarshalling most recent host version: %s", err)
func getCertInfoFromHost(h *Host) CertPathInfo { }
// setup cert paths
caCertPath := h.CaCertPath return h, nil
caKeyPath := h.PrivateKeyPath
clientCertPath := h.ClientCertPath
clientKeyPath := h.ClientKeyPath
serverCertPath := h.ServerCertPath
serverKeyPath := h.ServerKeyPath
if caCertPath == "" {
caCertPath = filepath.Join(utils.GetMachineCertDir(), "ca.pem")
}
if caKeyPath == "" {
caKeyPath = filepath.Join(utils.GetMachineCertDir(), "ca-key.pem")
}
if clientCertPath == "" {
clientCertPath = filepath.Join(utils.GetMachineCertDir(), "cert.pem")
}
if clientKeyPath == "" {
clientKeyPath = filepath.Join(utils.GetMachineCertDir(), "key.pem")
}
if serverCertPath == "" {
serverCertPath = filepath.Join(utils.GetMachineCertDir(), "server.pem")
}
if serverKeyPath == "" {
serverKeyPath = filepath.Join(utils.GetMachineCertDir(), "server-key.pem")
}
return CertPathInfo{
CaCertPath: caCertPath,
CaKeyPath: caKeyPath,
ClientCertPath: clientCertPath,
ClientKeyPath: clientKeyPath,
ServerCertPath: serverCertPath,
ServerKeyPath: serverKeyPath,
}
} }

113
libmachine/migrate_v0_v1.go Normal file
View File

@ -0,0 +1,113 @@
package libmachine
import (
"path/filepath"
"github.com/docker/machine/libmachine/auth"
"github.com/docker/machine/libmachine/engine"
"github.com/docker/machine/libmachine/swarm"
"github.com/docker/machine/utils"
)
// In the 0.0.1 => 0.0.2 transition, the JSON representation of
// machines changed from a "flat" to a more "nested" structure
// for various options and configuration settings. To preserve
// compatibility with existing machines, these migration functions
// have been introduced. They preserve backwards compat at the expense
// of some duplicated information.
// validates host config and modifies if needed
// this is used for configuration updates
func MigrateHostV0ToHostV1(hostV0 *HostV0) *Host {
host := &Host{}
certInfoV0 := getCertInfoFromHost(hostV0)
host.HostOptions = &HostOptions{}
host.HostOptions.EngineOptions = &engine.EngineOptions{}
host.HostOptions.SwarmOptions = &swarm.SwarmOptions{
Address: "",
Discovery: hostV0.SwarmDiscovery,
Host: hostV0.SwarmHost,
Master: hostV0.SwarmMaster,
}
host.HostOptions.AuthOptions = &auth.AuthOptions{
StorePath: hostV0.StorePath,
CaCertPath: certInfoV0.CaCertPath,
CaCertRemotePath: "",
ServerCertPath: certInfoV0.ServerCertPath,
ServerKeyPath: certInfoV0.ServerKeyPath,
ClientKeyPath: certInfoV0.ClientKeyPath,
ServerCertRemotePath: "",
ServerKeyRemotePath: "",
PrivateKeyPath: certInfoV0.CaKeyPath,
ClientCertPath: certInfoV0.ClientCertPath,
}
return host
}
// fills nested host metadata and modifies if needed
// this is used for configuration updates
func MigrateHostMetadataV0ToHostMetadataV1(m *HostMetadataV0) *HostMetadata {
hostMetadata := &HostMetadata{}
hostMetadata.DriverName = m.DriverName
hostMetadata.HostOptions.EngineOptions = &engine.EngineOptions{}
hostMetadata.HostOptions.AuthOptions = &auth.AuthOptions{
StorePath: m.StorePath,
CaCertPath: m.CaCertPath,
CaCertRemotePath: "",
ServerCertPath: m.ServerCertPath,
ServerKeyPath: m.ServerKeyPath,
ClientKeyPath: "",
ServerCertRemotePath: "",
ServerKeyRemotePath: "",
PrivateKeyPath: m.PrivateKeyPath,
ClientCertPath: m.ClientCertPath,
}
return hostMetadata
}
func getCertInfoFromHost(h *HostV0) CertPathInfo {
// setup cert paths
caCertPath := h.CaCertPath
caKeyPath := h.PrivateKeyPath
clientCertPath := h.ClientCertPath
clientKeyPath := h.ClientKeyPath
serverCertPath := h.ServerCertPath
serverKeyPath := h.ServerKeyPath
if caCertPath == "" {
caCertPath = filepath.Join(utils.GetMachineCertDir(), "ca.pem")
}
if caKeyPath == "" {
caKeyPath = filepath.Join(utils.GetMachineCertDir(), "ca-key.pem")
}
if clientCertPath == "" {
clientCertPath = filepath.Join(utils.GetMachineCertDir(), "cert.pem")
}
if clientKeyPath == "" {
clientKeyPath = filepath.Join(utils.GetMachineCertDir(), "key.pem")
}
if serverCertPath == "" {
serverCertPath = filepath.Join(utils.GetMachineCertDir(), "server.pem")
}
if serverKeyPath == "" {
serverKeyPath = filepath.Join(utils.GetMachineCertDir(), "server-key.pem")
}
return CertPathInfo{
CaCertPath: caCertPath,
CaKeyPath: caKeyPath,
ClientCertPath: clientCertPath,
ClientKeyPath: clientKeyPath,
ServerCertPath: serverCertPath,
ServerKeyPath: serverKeyPath,
}
}

View File

@ -10,9 +10,9 @@ import (
"github.com/docker/machine/libmachine/swarm" "github.com/docker/machine/libmachine/swarm"
) )
func TestFillNestedHost(t *testing.T) { func TestMigrateHostV0ToV1(t *testing.T) {
os.Setenv("MACHINE_STORAGE_PATH", "/tmp/migration") os.Setenv("MACHINE_STORAGE_PATH", "/tmp/migration")
originalHost := &Host{ originalHost := &HostV0{
HostOptions: nil, HostOptions: nil,
SwarmDiscovery: "token://foobar", SwarmDiscovery: "token://foobar",
SwarmHost: "1.2.3.4:2376", SwarmHost: "1.2.3.4:2376",
@ -42,13 +42,10 @@ func TestFillNestedHost(t *testing.T) {
} }
expectedHost := &Host{ expectedHost := &Host{
SwarmHost: "1.2.3.4:2376",
SwarmDiscovery: "token://foobar",
SwarmMaster: true,
HostOptions: hostOptions, HostOptions: hostOptions,
} }
host := FillNestedHost(originalHost) host := MigrateHostV0ToHostV1(originalHost)
if !reflect.DeepEqual(host, expectedHost) { if !reflect.DeepEqual(host, expectedHost) {
t.Logf("\n%+v\n%+v", host, expectedHost) t.Logf("\n%+v\n%+v", host, expectedHost)
@ -57,8 +54,8 @@ func TestFillNestedHost(t *testing.T) {
} }
} }
func TestFillNestedHostMetadata(t *testing.T) { func TestMigrateHostMetadataV0ToV1(t *testing.T) {
metadata := &HostMetadata{ metadata := &HostMetadataV0{
HostOptions: HostOptions{ HostOptions: HostOptions{
EngineOptions: nil, EngineOptions: nil,
AuthOptions: nil, AuthOptions: nil,
@ -78,12 +75,9 @@ func TestFillNestedHostMetadata(t *testing.T) {
EngineOptions: &engine.EngineOptions{}, EngineOptions: &engine.EngineOptions{},
AuthOptions: expectedAuthOptions, AuthOptions: expectedAuthOptions,
}, },
StorePath: "/tmp/store",
CaCertPath: "/tmp/store/certs/ca.pem",
ServerCertPath: "/tmp/store/certs/server.pem",
} }
m := FillNestedHostMetadata(metadata) m := MigrateHostMetadataV0ToHostMetadataV1(metadata)
if !reflect.DeepEqual(m, expectedMetadata) { if !reflect.DeepEqual(m, expectedMetadata) {
t.Logf("\n%+v\n%+v", m, expectedMetadata) t.Logf("\n%+v\n%+v", m, expectedMetadata)
@ -95,7 +89,7 @@ func TestFillNestedHostMetadata(t *testing.T) {
// due to a schema migration from "flat" to a "nested" structure. // due to a schema migration from "flat" to a "nested" structure.
func TestGetCertInfoFromHost(t *testing.T) { func TestGetCertInfoFromHost(t *testing.T) {
os.Setenv("MACHINE_STORAGE_PATH", "/tmp/migration") os.Setenv("MACHINE_STORAGE_PATH", "/tmp/migration")
host := &Host{ host := &HostV0{
CaCertPath: "", CaCertPath: "",
PrivateKeyPath: "", PrivateKeyPath: "",
ClientCertPath: "", ClientCertPath: "",

View File

@ -67,7 +67,7 @@ func main() {
app.Commands = commands.Commands app.Commands = commands.Commands
app.CommandNotFound = cmdNotFound app.CommandNotFound = cmdNotFound
app.Usage = "Create and manage machines running Docker." app.Usage = "Create and manage machines running Docker."
app.Version = version.VERSION + " (" + version.GITCOMMIT + ")" app.Version = version.Version + " (" + version.GitCommit + ")"
app.Flags = []cli.Flag{ app.Flags = []cli.Flag{
cli.BoolFlag{ cli.BoolFlag{

View File

@ -21,4 +21,4 @@ fi
# Get rid of existing binaries # Get rid of existing binaries
rm -f docker-machine* rm -f docker-machine*
rm -rf Godeps/_workspace/pkg rm -rf Godeps/_workspace/pkg
docker run --rm -v `pwd`:/go/src/github.com/docker/machine docker-machine gox "${OS_PLATFORM_ARG[@]}" "${OS_ARCH_ARG[@]}" -output="docker-machine_{{.OS}}-{{.Arch}}" -ldflags="-w -X github.com/docker/machine/version.GITCOMMIT `git rev-parse --short HEAD`" docker run --rm -v `pwd`:/go/src/github.com/docker/machine docker-machine gox "${OS_PLATFORM_ARG[@]}" "${OS_ARCH_ARG[@]}" -output="docker-machine_{{.OS}}-{{.Arch}}" -ldflags="-w -X github.com/docker/machine/version.GitCommit `git rev-parse --short HEAD`"

View File

@ -1,9 +1,14 @@
package version package version
var ( var (
// VERSION should be updated by hand at each release // ConfigVersion dictates which version of the config.json format is
VERSION = "0.4.0-dev" // used. It needs to be bumped if there is a breaking change, and
// therefore migration, introduced to the config file format.
ConfigVersion = 1
// GITCOMMIT will be overwritten automatically by the build system // Version should be updated by hand at each release
GITCOMMIT = "HEAD" Version = "0.4.0-dev"
// GitCommit will be overwritten automatically by the build system
GitCommit = "HEAD"
) )