github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/worker/uniter/charm/deployer.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package charm
     5  
     6  import (
     7  	"fmt"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"time"
    12  	"runtime"
    13  
    14  	"launchpad.net/juju-core/log"
    15      "launchpad.net/juju-core/utils"
    16  )
    17  
    18  const (
    19  	updatePrefix  = "update-"
    20  	installPrefix = "install-"
    21  )
    22  
    23  // Deployer is responsible for installing and upgrading charms.
    24  type Deployer interface {
    25  
    26  	// Stage must be called to prime the Deployer to install or upgrade the
    27  	// bundle identified by the supplied info. The abort chan can be used to
    28  	// notify an implementation that it need not complete the operation, and
    29  	// can immediately error out if it convenient to do so. It must always
    30  	// be safe to restage the same bundle, or to stage a new bundle.
    31  	Stage(info BundleInfo, abort <-chan struct{}) error
    32  
    33  	// Deploy will install or upgrade the most recently staged bundle.
    34  	// Behaviour is undefined if Stage has not been called.
    35  	Deploy() error
    36  
    37  	// NotifyRevert must be called when a conflicted deploy is abandoned, in
    38  	// preparation for a new upgrade.
    39  	NotifyRevert() error
    40  
    41  	// NotifyResolved must be called when the cause of a deploy conflict has
    42  	// been resolved, and a new deploy attempt will be made.
    43  	NotifyResolved() error
    44  }
    45  
    46  // gitDeployer maintains a git repository tracking a series of charm versions,
    47  // and can install and upgrade charm deployments to the current version.
    48  type gitDeployer struct {
    49  	target   *GitDir
    50  	dataPath string
    51  	bundles  BundleReader
    52  	current  *GitDir
    53  }
    54  
    55  // NewGitDeployer creates a new Deployer which stores its state in dataPath,
    56  // and installs or upgrades the charm at charmPath.
    57  func NewGitDeployer(charmPath, dataPath string, bundles BundleReader) Deployer {
    58  	return &gitDeployer{
    59  		target:   NewGitDir(charmPath),
    60  		dataPath: dataPath,
    61  		bundles:  bundles,
    62  		current:  NewGitDir(filepath.Join(dataPath, "current")),
    63  	}
    64  }
    65  
    66  func (d *gitDeployer) Stage(info BundleInfo, abort <-chan struct{}) error {
    67  	// Make sure we've got an actual bundle available.
    68  	bundle, err := d.bundles.Read(info, abort)
    69  	if err != nil {
    70  		return err
    71  	}
    72  
    73  	// Read present state of current.
    74  	if err := os.MkdirAll(d.dataPath, 0755); err != nil {
    75  		return err
    76  	}
    77  	defer d.collectOrphans()
    78  	srcExists, err := d.current.Exists()
    79  	if err != nil {
    80  		return err
    81  	}
    82  	url := info.URL()
    83  	if srcExists {
    84  		prevURL, err := ReadCharmURL(d.current)
    85  		if err != nil {
    86  			return err
    87  		}
    88  		if *url == *prevURL {
    89  			return nil
    90  		}
    91  	}
    92  
    93  	// Prepare a fresh repository for the update, using current's history
    94  	// if it exists.
    95  	updatePath, err := d.newDir(updatePrefix)
    96  	if err != nil {
    97  		return err
    98  	}
    99  	var repo *GitDir
   100  	if srcExists {
   101  		repo, err = d.current.Clone(updatePath)
   102  	} else {
   103  		repo = NewGitDir(updatePath)
   104  		err = repo.Init()
   105  	}
   106  	if err != nil {
   107  		return err
   108  	}
   109  
   110  	// Write the desired new state and commit.
   111  	if err = bundle.ExpandTo(updatePath); err != nil {
   112  		return err
   113  	}
   114  	if err = WriteCharmURL(repo, url); err != nil {
   115  		return err
   116  	}
   117  	if err = repo.Snapshotf("Imported charm %q from %q.", url, bundle.Path); err != nil {
   118  		return err
   119  	}
   120  
   121  	// Atomically rename fresh repository to current.
   122  	tmplink := filepath.Join(updatePath, "tmplink")
   123  	if err = utils.Symlink(updatePath, tmplink); err != nil {
   124  		return err
   125  	}
   126  	// You cannot rename a symlink if destination exists
   127  	if runtime.GOOS == "windows" {
   128  		if _, err := os.Stat(d.current.Path()); err == nil {
   129  	        _ = os.RemoveAll(d.current.Path())
   130  	    }
   131  	}
   132  	return os.Rename(tmplink, d.current.Path())
   133  }
   134  
   135  func (d *gitDeployer) Deploy() (err error) {
   136  	defer func() {
   137  		if err == ErrConflict {
   138  			log.Warningf("worker/uniter/charm: charm deployment completed with conflicts")
   139  		} else if err != nil {
   140  			err = fmt.Errorf("charm deployment failed: %s", err)
   141  			log.Errorf("worker/uniter/charm: %v", err)
   142  		} else {
   143  			log.Infof("worker/uniter/charm: charm deployment succeeded")
   144  		}
   145  	}()
   146  	if exists, err := d.current.Exists(); err != nil {
   147  		return err
   148  	} else if !exists {
   149  		return fmt.Errorf("no charm set")
   150  	}
   151  	if exists, err := d.target.Exists(); err != nil {
   152  		return err
   153  	} else if !exists {
   154  		return d.install()
   155  	}
   156  	return d.upgrade()
   157  }
   158  
   159  func (d *gitDeployer) NotifyRevert() error {
   160  	return d.target.Revert()
   161  }
   162  
   163  func (d *gitDeployer) NotifyResolved() error {
   164  	return d.target.Snapshotf("Upgrade conflict resolved.")
   165  }
   166  
   167  // install creates a new deployment of current, and atomically moves it to
   168  // target.
   169  func (d *gitDeployer) install() error {
   170  	defer d.collectOrphans()
   171  	log.Infof("worker/uniter/charm: preparing new charm deployment")
   172  	url, err := ReadCharmURL(d.current)
   173  	if err != nil {
   174  		return err
   175  	}
   176  	installPath, err := d.newDir(installPrefix)
   177  	if err != nil {
   178  		return err
   179  	}
   180  	repo := NewGitDir(installPath)
   181  	if err = repo.Init(); err != nil {
   182  		return err
   183  	}
   184  	if err = repo.Pull(d.current); err != nil {
   185  		return err
   186  	}
   187  	if err = repo.Snapshotf("Deployed charm %q.", url); err != nil {
   188  		return err
   189  	}
   190  	log.Infof("worker/uniter/charm: deploying charm")
   191  	return os.Rename(installPath, d.target.Path())
   192  }
   193  
   194  // upgrade pulls from current into target. If target has local changes, but
   195  // no conflicts, it will be snapshotted before any changes are made.
   196  func (d *gitDeployer) upgrade() error {
   197  	log.Infof("worker/uniter/charm: preparing charm upgrade")
   198  	url, err := ReadCharmURL(d.current)
   199  	if err != nil {
   200  		return err
   201  	}
   202  	if err := d.target.Init(); err != nil {
   203  		return err
   204  	}
   205  	if dirty, err := d.target.Dirty(); err != nil {
   206  		return err
   207  	} else if dirty {
   208  		if conflicted, err := d.target.Conflicted(); err != nil {
   209  			return err
   210  		} else if !conflicted {
   211  			log.Infof("worker/uniter/charm: snapshotting dirty charm before upgrade")
   212  			if err = d.target.Snapshotf("Pre-upgrade snapshot."); err != nil {
   213  				return err
   214  			}
   215  		}
   216  	}
   217  	log.Infof("worker/uniter/charm: deploying charm")
   218  	if err := d.target.Pull(d.current); err != nil {
   219  		return err
   220  	}
   221  	return d.target.Snapshotf("Upgraded charm to %q.", url)
   222  }
   223  
   224  // collectOrphans deletes all repos in dataPath except the one pointed to by current.
   225  // Errors are generally ignored; some are logged.
   226  func (d *gitDeployer) collectOrphans() {
   227  	current, err := os.Readlink(d.current.Path())
   228  	if err != nil {
   229  		return
   230  	}
   231  	if !filepath.IsAbs(current) {
   232  		current = filepath.Join(d.dataPath, current)
   233  	}
   234  	orphans, err := filepath.Glob(filepath.Join(d.dataPath, fmt.Sprintf("%s*", updatePrefix)))
   235  	if err != nil {
   236  		return
   237  	}
   238  	installOrphans, err := filepath.Glob(filepath.Join(d.dataPath, fmt.Sprintf("%s*", installPrefix)))
   239  	if err != nil {
   240  		return
   241  	}
   242  	orphans = append(orphans, installOrphans...)
   243  	for _, repoPath := range orphans {
   244  		if repoPath != d.dataPath && repoPath != current {
   245  			if err = os.RemoveAll(repoPath); err != nil {
   246  				log.Warningf("worker/uniter/charm: failed to remove orphan repo at %s: %s", repoPath, err)
   247  			}
   248  		}
   249  	}
   250  }
   251  
   252  // newDir creates a new timestamped directory with the given prefix. It
   253  // assumes that the deployer will not need to create more than 10
   254  // directories in any given second.
   255  func (d *gitDeployer) newDir(prefix string) (string, error) {
   256  	return ioutil.TempDir(d.dataPath, prefix+time.Now().Format("20060102-150405"))
   257  }