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