github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/worker/uniter/charm/git_deployer.go (about)

     1  // Copyright 2012-2014 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  
    14  const (
    15  	gitUpdatePrefix  = "update-"
    16  	gitInstallPrefix = "install-"
    17  	gitCurrentPath   = "current"
    18  )
    19  
    20  // gitDeployer maintains a git repository tracking a series of charm versions,
    21  // and can install and upgrade charm deployments to the current version.
    22  type gitDeployer struct {
    23  	target   *GitDir
    24  	dataPath string
    25  	bundles  BundleReader
    26  	current  *GitDir
    27  }
    28  
    29  // NewGitDeployer creates a new Deployer which stores its state in dataPath,
    30  // and installs or upgrades the charm at charmPath.
    31  func NewGitDeployer(charmPath, dataPath string, bundles BundleReader) Deployer {
    32  	return &gitDeployer{
    33  		target:   NewGitDir(charmPath),
    34  		dataPath: dataPath,
    35  		bundles:  bundles,
    36  		current:  NewGitDir(filepath.Join(dataPath, gitCurrentPath)),
    37  	}
    38  }
    39  
    40  func (d *gitDeployer) Stage(info BundleInfo, abort <-chan struct{}) error {
    41  	// Make sure we've got an actual bundle available.
    42  	bundle, err := d.bundles.Read(info, abort)
    43  	if err != nil {
    44  		return err
    45  	}
    46  
    47  	// Read present state of current.
    48  	if err := os.MkdirAll(d.dataPath, 0755); err != nil {
    49  		return err
    50  	}
    51  	defer collectGitOrphans(d.dataPath)
    52  	srcExists, err := d.current.Exists()
    53  	if err != nil {
    54  		return err
    55  	}
    56  	url := info.URL()
    57  	if srcExists {
    58  		prevURL, err := d.current.ReadCharmURL()
    59  		if err != nil {
    60  			return err
    61  		}
    62  		if *url == *prevURL {
    63  			return nil
    64  		}
    65  	}
    66  
    67  	// Prepare a fresh repository for the update, using current's history
    68  	// if it exists.
    69  	updatePath, err := d.newDir(gitUpdatePrefix)
    70  	if err != nil {
    71  		return err
    72  	}
    73  	var repo *GitDir
    74  	if srcExists {
    75  		repo, err = d.current.Clone(updatePath)
    76  	} else {
    77  		repo = NewGitDir(updatePath)
    78  		err = repo.Init()
    79  	}
    80  	if err != nil {
    81  		return err
    82  	}
    83  
    84  	// Write the desired new state and commit.
    85  	if err = bundle.ExpandTo(updatePath); err != nil {
    86  		return err
    87  	}
    88  	if err = repo.WriteCharmURL(url); err != nil {
    89  		return err
    90  	}
    91  	if err = repo.Snapshotf("Imported charm %q.", url); err != nil {
    92  		return err
    93  	}
    94  
    95  	// Atomically rename fresh repository to current.
    96  	tmplink := filepath.Join(updatePath, "tmplink")
    97  	if err = os.Symlink(updatePath, tmplink); err != nil {
    98  		return err
    99  	}
   100  	return os.Rename(tmplink, d.current.Path())
   101  }
   102  
   103  func (d *gitDeployer) Deploy() (err error) {
   104  	defer func() {
   105  		if err == ErrConflict {
   106  			logger.Warningf("charm deployment completed with conflicts")
   107  		} else if err != nil {
   108  			err = fmt.Errorf("charm deployment failed: %s", err)
   109  			logger.Errorf("%v", err)
   110  		} else {
   111  			logger.Infof("charm deployment succeeded")
   112  		}
   113  	}()
   114  	if exists, err := d.current.Exists(); err != nil {
   115  		return err
   116  	} else if !exists {
   117  		return fmt.Errorf("no charm set")
   118  	}
   119  	if exists, err := d.target.Exists(); err != nil {
   120  		return err
   121  	} else if !exists {
   122  		return d.install()
   123  	}
   124  	return d.upgrade()
   125  }
   126  
   127  func (d *gitDeployer) NotifyRevert() error {
   128  	return d.target.Revert()
   129  }
   130  
   131  func (d *gitDeployer) NotifyResolved() error {
   132  	return d.target.Snapshotf("Upgrade conflict resolved.")
   133  }
   134  
   135  // install creates a new deployment of current, and atomically moves it to
   136  // target.
   137  func (d *gitDeployer) install() error {
   138  	defer collectGitOrphans(d.dataPath)
   139  	logger.Infof("preparing new charm deployment")
   140  	url, err := d.current.ReadCharmURL()
   141  	if err != nil {
   142  		return err
   143  	}
   144  	installPath, err := d.newDir(gitInstallPrefix)
   145  	if err != nil {
   146  		return err
   147  	}
   148  	repo := NewGitDir(installPath)
   149  	if err = repo.Init(); err != nil {
   150  		return err
   151  	}
   152  	if err = repo.Pull(d.current); err != nil {
   153  		return err
   154  	}
   155  	if err = repo.Snapshotf("Deployed charm %q.", url); err != nil {
   156  		return err
   157  	}
   158  	logger.Infof("deploying charm")
   159  	return os.Rename(installPath, d.target.Path())
   160  }
   161  
   162  // upgrade pulls from current into target. If target has local changes, but
   163  // no conflicts, it will be snapshotted before any changes are made.
   164  func (d *gitDeployer) upgrade() error {
   165  	logger.Infof("preparing charm upgrade")
   166  	url, err := d.current.ReadCharmURL()
   167  	if err != nil {
   168  		return err
   169  	}
   170  	if err := d.target.Init(); err != nil {
   171  		return err
   172  	}
   173  	if dirty, err := d.target.Dirty(); err != nil {
   174  		return err
   175  	} else if dirty {
   176  		if conflicted, err := d.target.Conflicted(); err != nil {
   177  			return err
   178  		} else if !conflicted {
   179  			logger.Infof("snapshotting dirty charm before upgrade")
   180  			if err = d.target.Snapshotf("Pre-upgrade snapshot."); err != nil {
   181  				return err
   182  			}
   183  		}
   184  	}
   185  	logger.Infof("deploying charm")
   186  	if err := d.target.Pull(d.current); err != nil {
   187  		return err
   188  	}
   189  	return d.target.Snapshotf("Upgraded charm to %q.", url)
   190  }
   191  
   192  // collectGitOrphans deletes all repos in dataPath except the one pointed to by
   193  // a git deployer's "current" symlink.
   194  // Errors are generally ignored; some are logged. If current does not exist, *all*
   195  // repos are orphans, and all will be deleted; this should only be the case when
   196  // converting a gitDeployer to a manifestDeployer.
   197  func collectGitOrphans(dataPath string) {
   198  	current, err := os.Readlink(filepath.Join(dataPath, gitCurrentPath))
   199  	if os.IsNotExist(err) {
   200  		logger.Warningf("no current staging repo")
   201  	} else if err != nil {
   202  		logger.Warningf("cannot read current staging repo: %v", err)
   203  		return
   204  	} else if !filepath.IsAbs(current) {
   205  		current = filepath.Join(dataPath, current)
   206  	}
   207  	orphans, err := filepath.Glob(filepath.Join(dataPath, fmt.Sprintf("%s*", gitUpdatePrefix)))
   208  	if err != nil {
   209  		return
   210  	}
   211  	installOrphans, err := filepath.Glob(filepath.Join(dataPath, fmt.Sprintf("%s*", gitInstallPrefix)))
   212  	if err != nil {
   213  		return
   214  	}
   215  	orphans = append(orphans, installOrphans...)
   216  	for _, repoPath := range orphans {
   217  		if repoPath != dataPath && repoPath != current {
   218  			if err = os.RemoveAll(repoPath); err != nil {
   219  				logger.Warningf("failed to remove orphan repo at %s: %s", repoPath, err)
   220  			}
   221  		}
   222  	}
   223  }
   224  
   225  // newDir creates a new timestamped directory with the given prefix. It
   226  // assumes that the deployer will not need to create more than 10
   227  // directories in any given second.
   228  func (d *gitDeployer) newDir(prefix string) (string, error) {
   229  	return ioutil.TempDir(d.dataPath, prefix+time.Now().Format("20060102-150405"))
   230  }