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