github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/worker/uniter/charm/manifest_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  	"os"
     9  	"path/filepath"
    10  
    11  	"github.com/juju/utils"
    12  	"github.com/juju/utils/set"
    13  
    14  	"github.com/juju/juju/charm"
    15  )
    16  
    17  const (
    18  	// deployingURLPath holds the path in the charm dir where the manifest
    19  	// deployer writes what charm is currently being deployed.
    20  	deployingURLPath = ".juju-deploying"
    21  
    22  	// manifestsDataPath holds the path in the data dir where the manifest
    23  	// deployer stores the manifests for its charms.
    24  	manifestsDataPath = "manifests"
    25  )
    26  
    27  // NewManifestDeployer returns a Deployer that installs bundles from the
    28  // supplied BundleReader into charmPath, and which reads and writes its
    29  // persistent data into dataPath.
    30  //
    31  // It works by always writing the full contents of a deployed charm; and, if
    32  // another charm was previously deployed, deleting only those files unique to
    33  // that base charm. It thus leaves user files in place, with the exception of
    34  // those in directories referenced only in the original charm, which will be
    35  // deleted.
    36  func NewManifestDeployer(charmPath, dataPath string, bundles BundleReader) Deployer {
    37  	return &manifestDeployer{
    38  		charmPath: charmPath,
    39  		dataPath:  dataPath,
    40  		bundles:   bundles,
    41  	}
    42  }
    43  
    44  type manifestDeployer struct {
    45  	charmPath string
    46  	dataPath  string
    47  	bundles   BundleReader
    48  	staged    struct {
    49  		url      *charm.URL
    50  		bundle   Bundle
    51  		manifest set.Strings
    52  	}
    53  }
    54  
    55  func (d *manifestDeployer) Stage(info BundleInfo, abort <-chan struct{}) error {
    56  	bundle, err := d.bundles.Read(info, abort)
    57  	if err != nil {
    58  		return err
    59  	}
    60  	manifest, err := bundle.Manifest()
    61  	if err != nil {
    62  		return err
    63  	}
    64  	url := info.URL()
    65  	if err := d.storeManifest(url, manifest); err != nil {
    66  		return err
    67  	}
    68  	d.staged.url = url
    69  	d.staged.bundle = bundle
    70  	d.staged.manifest = manifest
    71  	return nil
    72  }
    73  
    74  func (d *manifestDeployer) Deploy() (err error) {
    75  	if d.staged.url == nil {
    76  		return fmt.Errorf("charm deployment failed: no charm set")
    77  	}
    78  
    79  	// Detect and resolve state of charm directory.
    80  	baseURL, baseManifest, err := d.loadManifest(charmURLPath)
    81  	if err != nil && !os.IsNotExist(err) {
    82  		return err
    83  	}
    84  	upgrading := baseURL != nil
    85  	defer manifestDeployError(&err, upgrading)
    86  	if err := d.ensureBaseFiles(baseManifest); err != nil {
    87  		return err
    88  	}
    89  
    90  	// Write or overwrite the deploying URL to point to the staged one.
    91  	if err := d.startDeploy(); err != nil {
    92  		return err
    93  	}
    94  
    95  	// Delete files in the base version not present in the staged charm.
    96  	if upgrading {
    97  		if err := d.removeDiff(baseManifest, d.staged.manifest); err != nil {
    98  			return err
    99  		}
   100  	}
   101  
   102  	// Overwrite whatever's in place with the staged charm.
   103  	logger.Debugf("deploying charm %q", d.staged.url)
   104  	if err := d.staged.bundle.ExpandTo(d.charmPath); err != nil {
   105  		return err
   106  	}
   107  
   108  	// Move the deploying file over the charm URL file, and we're done.
   109  	return d.finishDeploy()
   110  }
   111  
   112  func (d *manifestDeployer) NotifyResolved() error {
   113  	// Maybe it is resolved, maybe not. We'll find out soon enough, but we
   114  	// don't need to take any action now; if it's not, we'll just ErrConflict
   115  	// out of Deploy again.
   116  	return nil
   117  }
   118  
   119  func (d *manifestDeployer) NotifyRevert() error {
   120  	// The Deploy implementation always effectively reverts when required
   121  	// anyway, so we need take no action right now.
   122  	return nil
   123  }
   124  
   125  // startDeploy persists the fact that we've started deploying the staged bundle.
   126  func (d *manifestDeployer) startDeploy() error {
   127  	logger.Debugf("preparing to deploy charm %q", d.staged.url)
   128  	if err := os.MkdirAll(d.charmPath, 0755); err != nil {
   129  		return err
   130  	}
   131  	return WriteCharmURL(d.CharmPath(deployingURLPath), d.staged.url)
   132  }
   133  
   134  // removeDiff removes every path in oldManifest that is not present in newManifest.
   135  func (d *manifestDeployer) removeDiff(oldManifest, newManifest set.Strings) error {
   136  	diff := oldManifest.Difference(newManifest)
   137  	for _, path := range diff.SortedValues() {
   138  		fullPath := filepath.Join(d.charmPath, filepath.FromSlash(path))
   139  		if err := os.RemoveAll(fullPath); err != nil {
   140  			return err
   141  		}
   142  	}
   143  	return nil
   144  }
   145  
   146  // finishDeploy persists the fact that we've finished deploying the staged bundle.
   147  func (d *manifestDeployer) finishDeploy() error {
   148  	logger.Debugf("finishing deploy of charm %q", d.staged.url)
   149  	oldPath := d.CharmPath(deployingURLPath)
   150  	newPath := d.CharmPath(charmURLPath)
   151  	return utils.ReplaceFile(oldPath, newPath)
   152  }
   153  
   154  // ensureBaseFiles checks for an interrupted deploy operation and, if it finds
   155  // one, removes all entries in the manifest unique to the interrupted operation.
   156  // This leaves files from the base charm in an indeterminate state, but ready to
   157  // be either removed (if they are not referenced by the new charm) or overwritten
   158  // (if they are referenced by the new charm).
   159  //
   160  // Note that deployingURLPath is *not* written, because the charm state remains
   161  // indeterminate; that file will be removed when and only when a deploy completes
   162  // successfully.
   163  func (d *manifestDeployer) ensureBaseFiles(baseManifest set.Strings) error {
   164  	deployingURL, deployingManifest, err := d.loadManifest(deployingURLPath)
   165  	if err == nil {
   166  		logger.Infof("detected interrupted deploy of charm %q", deployingURL)
   167  		if *deployingURL != *d.staged.url {
   168  			logger.Infof("removing files from charm %q", deployingURL)
   169  			if err := d.removeDiff(deployingManifest, baseManifest); err != nil {
   170  				return err
   171  			}
   172  		}
   173  	}
   174  	if os.IsNotExist(err) {
   175  		err = nil
   176  	}
   177  	return err
   178  }
   179  
   180  // storeManifest stores, into dataPath, the supplied manifest for the supplied charm.
   181  func (d *manifestDeployer) storeManifest(url *charm.URL, manifest set.Strings) error {
   182  	if err := os.MkdirAll(d.DataPath(manifestsDataPath), 0755); err != nil {
   183  		return err
   184  	}
   185  	name := charm.Quote(url.String())
   186  	path := filepath.Join(d.DataPath(manifestsDataPath), name)
   187  	return utils.WriteYaml(path, manifest.SortedValues())
   188  }
   189  
   190  // loadManifest loads, from dataPath, the manifest for the charm identified by the
   191  // identity file at the supplied path within the charm directory.
   192  func (d *manifestDeployer) loadManifest(urlFilePath string) (*charm.URL, set.Strings, error) {
   193  	url, err := ReadCharmURL(d.CharmPath(urlFilePath))
   194  	if err != nil {
   195  		return nil, set.NewStrings(), err
   196  	}
   197  	name := charm.Quote(url.String())
   198  	path := filepath.Join(d.DataPath(manifestsDataPath), name)
   199  	manifest := []string{}
   200  	err = utils.ReadYaml(path, &manifest)
   201  	if os.IsNotExist(err) {
   202  		logger.Warningf("manifest not found at %q: files from charm %q may be left unremoved", path, url)
   203  		err = nil
   204  	}
   205  	return url, set.NewStrings(manifest...), err
   206  }
   207  
   208  // CharmPath returns the supplied path joined to the ManifestDeployer's charm directory.
   209  func (d *manifestDeployer) CharmPath(path string) string {
   210  	return filepath.Join(d.charmPath, path)
   211  }
   212  
   213  // DataPath returns the supplied path joined to the ManifestDeployer's data directory.
   214  func (d *manifestDeployer) DataPath(path string) string {
   215  	return filepath.Join(d.dataPath, path)
   216  }
   217  
   218  // manifestDeployError annotates or replaces the supplied error according
   219  // to whether or not an upgrade operation is in play. It was extracted from
   220  // Deploy to aid that method's readability.
   221  func manifestDeployError(err *error, upgrading bool) {
   222  	if *err != nil {
   223  		if upgrading {
   224  			// We now treat any failure to overwrite the charm -- or otherwise
   225  			// manipulate the charm directory -- as a conflict, because it's
   226  			// actually plausible for a user (or at least a charm author, who
   227  			// is the real audience for this case) to get in there and fix it.
   228  			logger.Errorf("cannot upgrade charm: %v", *err)
   229  			*err = ErrConflict
   230  		} else {
   231  			// ...but if we can't install at all, we just fail out as the old
   232  			// gitDeployer did, because I'm not willing to mess around with
   233  			// the uniter to enable ErrConflict handling on install. We've
   234  			// never heard of it actually happening, so this is probably not
   235  			// a big deal.
   236  			*err = fmt.Errorf("cannot install charm: %v", *err)
   237  		}
   238  	}
   239  }