github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/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  	"time"
    11  
    12  	"github.com/juju/charm/v12"
    13  	"github.com/juju/clock"
    14  	"github.com/juju/collections/set"
    15  	"github.com/juju/errors"
    16  	"github.com/juju/retry"
    17  	"github.com/juju/utils/v3"
    18  )
    19  
    20  const (
    21  	// deployingURLPath holds the path in the charm dir where the manifest
    22  	// deployer writes what charm is currently being deployed.
    23  	deployingURLPath = ".juju-deploying"
    24  
    25  	// manifestsDataPath holds the path in the data dir where the manifest
    26  	// deployer stores the manifests for its charms.
    27  	manifestsDataPath = "manifests"
    28  )
    29  
    30  // NewManifestDeployer returns a Deployer that installs bundles from the
    31  // supplied BundleReader into charmPath, and which reads and writes its
    32  // persistent data into dataPath.
    33  //
    34  // It works by always writing the full contents of a deployed charm; and, if
    35  // another charm was previously deployed, deleting only those files unique to
    36  // that base charm. It thus leaves user files in place, with the exception of
    37  // those in directories referenced only in the original charm, which will be
    38  // deleted.
    39  func NewManifestDeployer(charmPath, dataPath string, bundles BundleReader, logger Logger) Deployer {
    40  	return &manifestDeployer{
    41  		charmPath: charmPath,
    42  		dataPath:  dataPath,
    43  		bundles:   bundles,
    44  		logger:    logger,
    45  	}
    46  }
    47  
    48  type manifestDeployer struct {
    49  	charmPath string
    50  	dataPath  string
    51  	bundles   BundleReader
    52  	logger    Logger
    53  	staged    struct {
    54  		url      string
    55  		bundle   Bundle
    56  		manifest set.Strings
    57  	}
    58  }
    59  
    60  func (d *manifestDeployer) Stage(info BundleInfo, abort <-chan struct{}) error {
    61  	bdr := RetryingBundleReader{
    62  		BundleReader: d.bundles,
    63  		Clock:        clock.WallClock,
    64  		Logger:       d.logger,
    65  	}
    66  	bundle, err := bdr.Read(info, abort)
    67  	if err != nil {
    68  		return err
    69  	}
    70  	manifest, err := bundle.ArchiveMembers()
    71  	if err != nil {
    72  		return err
    73  	}
    74  	url := info.URL()
    75  	if err := d.storeManifest(url, manifest); err != nil {
    76  		return err
    77  	}
    78  	d.staged.url = url
    79  	d.staged.bundle = bundle
    80  	d.staged.manifest = manifest
    81  	return nil
    82  }
    83  
    84  func (d *manifestDeployer) Deploy() (err error) {
    85  	if d.staged.url == "" {
    86  		return fmt.Errorf("charm deployment failed: no charm set")
    87  	}
    88  
    89  	// Detect and resolve state of charm directory.
    90  	baseURL, baseManifest, err := d.loadManifest(CharmURLPath)
    91  	if err != nil && !os.IsNotExist(err) {
    92  		return err
    93  	}
    94  	upgrading := baseURL != ""
    95  	defer func(err *error) {
    96  		if *err != nil {
    97  			if upgrading {
    98  				// We now treat any failure to overwrite the charm -- or otherwise
    99  				// manipulate the charm directory -- as a conflict, because it's
   100  				// actually plausible for a user (or at least a charm author, who
   101  				// is the real audience for this case) to get in there and fix it.
   102  				d.logger.Errorf("cannot upgrade charm: %v", *err)
   103  				*err = ErrConflict
   104  			} else {
   105  				// ...but if we can't install at all, we just fail out as the old
   106  				// gitDeployer did, because I'm not willing to mess around with
   107  				// the uniter to enable ErrConflict handling on install. We've
   108  				// never heard of it actually happening, so this is probably not
   109  				// a big deal.
   110  				*err = errors.Annotate(*err, "cannot install charm")
   111  			}
   112  		}
   113  	}(&err)
   114  
   115  	if err := d.ensureBaseFiles(baseManifest); err != nil {
   116  		return err
   117  	}
   118  
   119  	// Write or overwrite the deploying URL to point to the staged one.
   120  	if err := d.startDeploy(); err != nil {
   121  		return err
   122  	}
   123  
   124  	// Delete files in the base version not present in the staged charm.
   125  	if upgrading {
   126  		if err := d.removeDiff(baseManifest, d.staged.manifest); err != nil {
   127  			return err
   128  		}
   129  	}
   130  
   131  	// Overwrite whatever's in place with the staged charm.
   132  	d.logger.Debugf("deploying charm %q", d.staged.url)
   133  	if err := d.staged.bundle.ExpandTo(d.charmPath); err != nil {
   134  		return err
   135  	}
   136  
   137  	// Move the deploying file over the charm URL file, and we're done.
   138  	return d.finishDeploy()
   139  }
   140  
   141  // startDeploy persists the fact that we've started deploying the staged bundle.
   142  func (d *manifestDeployer) startDeploy() error {
   143  	d.logger.Debugf("preparing to deploy charm %q", d.staged.url)
   144  	if err := os.MkdirAll(d.charmPath, 0755); err != nil {
   145  		return err
   146  	}
   147  	return WriteCharmURL(d.CharmPath(deployingURLPath), d.staged.url)
   148  }
   149  
   150  // removeDiff removes every path in oldManifest that is not present in newManifest.
   151  func (d *manifestDeployer) removeDiff(oldManifest, newManifest set.Strings) error {
   152  	diff := oldManifest.Difference(newManifest)
   153  	for _, path := range diff.SortedValues() {
   154  		fullPath := filepath.Join(d.charmPath, filepath.FromSlash(path))
   155  		if err := os.RemoveAll(fullPath); err != nil {
   156  			return err
   157  		}
   158  	}
   159  	return nil
   160  }
   161  
   162  // finishDeploy persists the fact that we've finished deploying the staged bundle.
   163  func (d *manifestDeployer) finishDeploy() error {
   164  	d.logger.Debugf("finishing deploy of charm %q", d.staged.url)
   165  	oldPath := d.CharmPath(deployingURLPath)
   166  	newPath := d.CharmPath(CharmURLPath)
   167  	return utils.ReplaceFile(oldPath, newPath)
   168  }
   169  
   170  // ensureBaseFiles checks for an interrupted deploy operation and, if it finds
   171  // one, removes all entries in the manifest unique to the interrupted operation.
   172  // This leaves files from the base charm in an indeterminate state, but ready to
   173  // be either removed (if they are not referenced by the new charm) or overwritten
   174  // (if they are referenced by the new charm).
   175  //
   176  // Note that deployingURLPath is *not* written, because the charm state remains
   177  // indeterminate; that file will be removed when and only when a deploy completes
   178  // successfully.
   179  func (d *manifestDeployer) ensureBaseFiles(baseManifest set.Strings) error {
   180  	deployingURL, deployingManifest, err := d.loadManifest(deployingURLPath)
   181  	if err == nil {
   182  		d.logger.Infof("detected interrupted deploy of charm %q", deployingURL)
   183  		if deployingURL != d.staged.url {
   184  			d.logger.Infof("removing files from charm %q", deployingURL)
   185  			if err := d.removeDiff(deployingManifest, baseManifest); err != nil {
   186  				return err
   187  			}
   188  		}
   189  	}
   190  	if os.IsNotExist(err) {
   191  		err = nil
   192  	}
   193  	return err
   194  }
   195  
   196  // storeManifest stores, into dataPath, the supplied manifest for the supplied charm.
   197  func (d *manifestDeployer) storeManifest(url string, manifest set.Strings) error {
   198  	if err := os.MkdirAll(d.DataPath(manifestsDataPath), 0755); err != nil {
   199  		return err
   200  	}
   201  	name := charm.Quote(url)
   202  	path := filepath.Join(d.DataPath(manifestsDataPath), name)
   203  	return utils.WriteYaml(path, manifest.SortedValues())
   204  }
   205  
   206  // loadManifest loads, from dataPath, the manifest for the charm identified by the
   207  // identity file at the supplied path within the charm directory.
   208  func (d *manifestDeployer) loadManifest(urlFilePath string) (string, set.Strings, error) {
   209  	url, err := ReadCharmURL(d.CharmPath(urlFilePath))
   210  	if err != nil {
   211  		return "", nil, err
   212  	}
   213  	name := charm.Quote(url)
   214  	path := filepath.Join(d.DataPath(manifestsDataPath), name)
   215  	manifest := []string{}
   216  	err = utils.ReadYaml(path, &manifest)
   217  	if os.IsNotExist(err) {
   218  		d.logger.Warningf("manifest not found at %q: files from charm %q may be left unremoved", path, url)
   219  		err = nil
   220  	}
   221  	return url, set.NewStrings(manifest...), err
   222  }
   223  
   224  // CharmPath returns the supplied path joined to the ManifestDeployer's charm directory.
   225  func (d *manifestDeployer) CharmPath(path string) string {
   226  	return filepath.Join(d.charmPath, path)
   227  }
   228  
   229  // DataPath returns the supplied path joined to the ManifestDeployer's data directory.
   230  func (d *manifestDeployer) DataPath(path string) string {
   231  	return filepath.Join(d.dataPath, path)
   232  }
   233  
   234  type RetryingBundleReader struct {
   235  	BundleReader
   236  
   237  	Clock  clock.Clock
   238  	Logger Logger
   239  }
   240  
   241  func (rbr RetryingBundleReader) Read(bi BundleInfo, abort <-chan struct{}) (Bundle, error) {
   242  	var (
   243  		bundle   Bundle
   244  		minDelay = 200 * time.Millisecond
   245  		maxDelay = 8 * time.Second
   246  	)
   247  
   248  	fetchErr := retry.Call(retry.CallArgs{
   249  		Attempts:    10,
   250  		Delay:       minDelay,
   251  		BackoffFunc: retry.ExpBackoff(minDelay, maxDelay, 2.0, true),
   252  		Clock:       rbr.Clock,
   253  		Func: func() error {
   254  			b, err := rbr.BundleReader.Read(bi, abort)
   255  			if err != nil {
   256  				return err
   257  			}
   258  			bundle = b
   259  			return nil
   260  		},
   261  		IsFatalError: func(err error) bool {
   262  			return err != nil && !errors.IsNotYetAvailable(err)
   263  		},
   264  	})
   265  
   266  	if fetchErr != nil {
   267  		// If the charm is still not available something went wrong.
   268  		// Report a NotFound error instead
   269  		if errors.Is(fetchErr, errors.NotYetAvailable) {
   270  			rbr.Logger.Errorf("exceeded max retry attempts while waiting for blob data for %q to become available", bi.URL())
   271  			fetchErr = fmt.Errorf("blob data for %q %w", bi.URL(), errors.NotFound)
   272  		}
   273  		return nil, errors.Trace(fetchErr)
   274  	}
   275  	return bundle, nil
   276  }