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 }