launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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  
    13  	"launchpad.net/errgo/errors"
    14  	"launchpad.net/juju-core/charm"
    15  	"launchpad.net/juju-core/log"
    16  )
    17  
    18  const (
    19  	updatePrefix  = "update-"
    20  	installPrefix = "install-"
    21  )
    22  
    23  // Deployer maintains a git repository tracking a series of charm versions,
    24  // and can install and upgrade charm deployments to the current version.
    25  type Deployer struct {
    26  	path    string
    27  	current *GitDir
    28  }
    29  
    30  // NewDeployer creates a new Deployer which stores its state in the supplied
    31  // directory.
    32  func NewDeployer(path string) *Deployer {
    33  	return &Deployer{
    34  		path:    path,
    35  		current: NewGitDir(filepath.Join(path, "current")),
    36  	}
    37  }
    38  
    39  // Stage causes subsequent calls to Deploy to deploy the supplied charm.
    40  func (d *Deployer) Stage(bun *charm.Bundle, url *charm.URL) error {
    41  	// Read present state of current.
    42  	if err := os.MkdirAll(d.path, 0755); err != nil {
    43  		return mask(err)
    44  	}
    45  	defer d.collectOrphans()
    46  	srcExists, err := d.current.Exists()
    47  	if err != nil {
    48  		return mask(err)
    49  	}
    50  	if srcExists {
    51  		prevURL, err := ReadCharmURL(d.current)
    52  		if err != nil {
    53  			return mask(err)
    54  		}
    55  		if *url == *prevURL {
    56  			return nil
    57  		}
    58  	}
    59  
    60  	// Prepare a fresh repository for the update, using current's history
    61  	// if it exists.
    62  	updatePath, err := d.newDir(updatePrefix)
    63  	if err != nil {
    64  		return mask(err)
    65  	}
    66  	var repo *GitDir
    67  	if srcExists {
    68  		repo, err = d.current.Clone(updatePath)
    69  	} else {
    70  		repo = NewGitDir(updatePath)
    71  		err = repo.Init()
    72  	}
    73  	if err != nil {
    74  		return mask(err)
    75  	}
    76  
    77  	// Write the desired new state and commit.
    78  	if err = bun.ExpandTo(updatePath); err != nil {
    79  		return mask(err)
    80  	}
    81  	if err = WriteCharmURL(repo, url); err != nil {
    82  		return mask(err)
    83  	}
    84  	if err = repo.Snapshotf("Imported charm %q from %q.", url, bun.Path); err != nil {
    85  		return mask(err)
    86  	}
    87  
    88  	// Atomically rename fresh repository to current.
    89  	tmplink := filepath.Join(updatePath, "tmplink")
    90  	if err = os.Symlink(updatePath, tmplink); err != nil {
    91  		return mask(err)
    92  	}
    93  	return os.Rename(tmplink, d.current.Path())
    94  }
    95  
    96  // Deploy deploys the current charm to the target directory.
    97  func (d *Deployer) Deploy(target *GitDir) (err error) {
    98  	defer func() {
    99  		if errors.Cause(err) == ErrConflict {
   100  			log.Warningf("worker/uniter/charm: charm deployment completed with conflicts")
   101  		} else if err != nil {
   102  			err = errors.Newf("charm deployment failed: %s", err)
   103  			log.Errorf("worker/uniter/charm: %v", err)
   104  		} else {
   105  			log.Infof("worker/uniter/charm: charm deployment succeeded")
   106  		}
   107  	}()
   108  	if exists, err := d.current.Exists(); err != nil {
   109  		return mask(err)
   110  	} else if !exists {
   111  		return errors.Newf("no charm set")
   112  	}
   113  	if exists, err := target.Exists(); err != nil {
   114  		return mask(err)
   115  	} else if !exists {
   116  		return d.install(target)
   117  	}
   118  	return d.upgrade(target)
   119  }
   120  
   121  // install creates a new deployment of current, and atomically moves it to
   122  // target.
   123  func (d *Deployer) install(target *GitDir) error {
   124  	defer d.collectOrphans()
   125  	log.Infof("worker/uniter/charm: preparing new charm deployment")
   126  	url, err := ReadCharmURL(d.current)
   127  	if err != nil {
   128  		return mask(err)
   129  	}
   130  	installPath, err := d.newDir(installPrefix)
   131  	if err != nil {
   132  		return mask(err)
   133  	}
   134  	repo := NewGitDir(installPath)
   135  	if err = repo.Init(); err != nil {
   136  		return mask(err)
   137  	}
   138  	if err = repo.Pull(d.current); err != nil {
   139  		return mask(err)
   140  	}
   141  	if err = repo.Snapshotf("Deployed charm %q.", url); err != nil {
   142  		return mask(err)
   143  	}
   144  	log.Infof("worker/uniter/charm: deploying charm")
   145  	return os.Rename(installPath, target.Path())
   146  }
   147  
   148  // upgrade pulls from current into target. If target has local changes, but
   149  // no conflicts, it will be snapshotted before any changes are made.
   150  func (d *Deployer) upgrade(target *GitDir) error {
   151  	log.Infof("worker/uniter/charm: preparing charm upgrade")
   152  	url, err := ReadCharmURL(d.current)
   153  	if err != nil {
   154  		return mask(err)
   155  	}
   156  	if err := target.Init(); err != nil {
   157  		return mask(err)
   158  	}
   159  	if dirty, err := target.Dirty(); err != nil {
   160  		return mask(err)
   161  	} else if dirty {
   162  		if conflicted, err := target.Conflicted(); err != nil {
   163  			return mask(err)
   164  		} else if !conflicted {
   165  			log.Infof("worker/uniter/charm: snapshotting dirty charm before upgrade")
   166  			if err = target.Snapshotf("Pre-upgrade snapshot."); err != nil {
   167  				return mask(err)
   168  			}
   169  		}
   170  	}
   171  	log.Infof("worker/uniter/charm: deploying charm")
   172  	if err := target.Pull(d.current); err != nil {
   173  		return mask(err, errors.Is(ErrConflict))
   174  	}
   175  	return target.Snapshotf("Upgraded charm to %q.", url)
   176  }
   177  
   178  // collectOrphans deletes all repos in path except the one pointed to by current.
   179  // Errors are generally ignored; some are logged.
   180  func (d *Deployer) collectOrphans() {
   181  	current, err := os.Readlink(d.current.Path())
   182  	if err != nil {
   183  		return
   184  	}
   185  	if !filepath.IsAbs(current) {
   186  		current = filepath.Join(d.path, current)
   187  	}
   188  	orphans, err := filepath.Glob(filepath.Join(d.path, fmt.Sprintf("%s*", updatePrefix)))
   189  	if err != nil {
   190  		return
   191  	}
   192  	installOrphans, err := filepath.Glob(filepath.Join(d.path, fmt.Sprintf("%s*", installPrefix)))
   193  	if err != nil {
   194  		return
   195  	}
   196  	orphans = append(orphans, installOrphans...)
   197  	for _, repoPath := range orphans {
   198  		if repoPath != d.path && repoPath != current {
   199  			if err = os.RemoveAll(repoPath); err != nil {
   200  				log.Warningf("worker/uniter/charm: failed to remove orphan repo at %s: %s", repoPath, err)
   201  			}
   202  		}
   203  	}
   204  }
   205  
   206  // newDir creates a new timestamped directory with the given prefix. It
   207  // assumes that the deployer will not need to create more than 10
   208  // directories in any given second.
   209  func (d *Deployer) newDir(prefix string) (string, error) {
   210  	return ioutil.TempDir(d.path, prefix+time.Now().Format("20060102-150405"))
   211  }