github.com/cloudbase/juju-core@v0.0.0-20140504232958-a7271ac7912f/worker/uniter/charm/deployer.go (about) 1 // Copyright 2012, 2013 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 "runtime" 13 14 "launchpad.net/juju-core/log" 15 "launchpad.net/juju-core/utils" 16 ) 17 18 const ( 19 updatePrefix = "update-" 20 installPrefix = "install-" 21 ) 22 23 // Deployer is responsible for installing and upgrading charms. 24 type Deployer interface { 25 26 // Stage must be called to prime the Deployer to install or upgrade the 27 // bundle identified by the supplied info. The abort chan can be used to 28 // notify an implementation that it need not complete the operation, and 29 // can immediately error out if it convenient to do so. It must always 30 // be safe to restage the same bundle, or to stage a new bundle. 31 Stage(info BundleInfo, abort <-chan struct{}) error 32 33 // Deploy will install or upgrade the most recently staged bundle. 34 // Behaviour is undefined if Stage has not been called. 35 Deploy() error 36 37 // NotifyRevert must be called when a conflicted deploy is abandoned, in 38 // preparation for a new upgrade. 39 NotifyRevert() error 40 41 // NotifyResolved must be called when the cause of a deploy conflict has 42 // been resolved, and a new deploy attempt will be made. 43 NotifyResolved() error 44 } 45 46 // gitDeployer maintains a git repository tracking a series of charm versions, 47 // and can install and upgrade charm deployments to the current version. 48 type gitDeployer struct { 49 target *GitDir 50 dataPath string 51 bundles BundleReader 52 current *GitDir 53 } 54 55 // NewGitDeployer creates a new Deployer which stores its state in dataPath, 56 // and installs or upgrades the charm at charmPath. 57 func NewGitDeployer(charmPath, dataPath string, bundles BundleReader) Deployer { 58 return &gitDeployer{ 59 target: NewGitDir(charmPath), 60 dataPath: dataPath, 61 bundles: bundles, 62 current: NewGitDir(filepath.Join(dataPath, "current")), 63 } 64 } 65 66 func (d *gitDeployer) Stage(info BundleInfo, abort <-chan struct{}) error { 67 // Make sure we've got an actual bundle available. 68 bundle, err := d.bundles.Read(info, abort) 69 if err != nil { 70 return err 71 } 72 73 // Read present state of current. 74 if err := os.MkdirAll(d.dataPath, 0755); err != nil { 75 return err 76 } 77 defer d.collectOrphans() 78 srcExists, err := d.current.Exists() 79 if err != nil { 80 return err 81 } 82 url := info.URL() 83 if srcExists { 84 prevURL, err := ReadCharmURL(d.current) 85 if err != nil { 86 return err 87 } 88 if *url == *prevURL { 89 return nil 90 } 91 } 92 93 // Prepare a fresh repository for the update, using current's history 94 // if it exists. 95 updatePath, err := d.newDir(updatePrefix) 96 if err != nil { 97 return err 98 } 99 var repo *GitDir 100 if srcExists { 101 repo, err = d.current.Clone(updatePath) 102 } else { 103 repo = NewGitDir(updatePath) 104 err = repo.Init() 105 } 106 if err != nil { 107 return err 108 } 109 110 // Write the desired new state and commit. 111 if err = bundle.ExpandTo(updatePath); err != nil { 112 return err 113 } 114 if err = WriteCharmURL(repo, url); err != nil { 115 return err 116 } 117 if err = repo.Snapshotf("Imported charm %q from %q.", url, bundle.Path); err != nil { 118 return err 119 } 120 121 // Atomically rename fresh repository to current. 122 tmplink := filepath.Join(updatePath, "tmplink") 123 if err = utils.Symlink(updatePath, tmplink); err != nil { 124 return err 125 } 126 // You cannot rename a symlink if destination exists 127 if runtime.GOOS == "windows" { 128 if _, err := os.Stat(d.current.Path()); err == nil { 129 _ = os.RemoveAll(d.current.Path()) 130 } 131 } 132 return os.Rename(tmplink, d.current.Path()) 133 } 134 135 func (d *gitDeployer) Deploy() (err error) { 136 defer func() { 137 if err == ErrConflict { 138 log.Warningf("worker/uniter/charm: charm deployment completed with conflicts") 139 } else if err != nil { 140 err = fmt.Errorf("charm deployment failed: %s", err) 141 log.Errorf("worker/uniter/charm: %v", err) 142 } else { 143 log.Infof("worker/uniter/charm: charm deployment succeeded") 144 } 145 }() 146 if exists, err := d.current.Exists(); err != nil { 147 return err 148 } else if !exists { 149 return fmt.Errorf("no charm set") 150 } 151 if exists, err := d.target.Exists(); err != nil { 152 return err 153 } else if !exists { 154 return d.install() 155 } 156 return d.upgrade() 157 } 158 159 func (d *gitDeployer) NotifyRevert() error { 160 return d.target.Revert() 161 } 162 163 func (d *gitDeployer) NotifyResolved() error { 164 return d.target.Snapshotf("Upgrade conflict resolved.") 165 } 166 167 // install creates a new deployment of current, and atomically moves it to 168 // target. 169 func (d *gitDeployer) install() error { 170 defer d.collectOrphans() 171 log.Infof("worker/uniter/charm: preparing new charm deployment") 172 url, err := ReadCharmURL(d.current) 173 if err != nil { 174 return err 175 } 176 installPath, err := d.newDir(installPrefix) 177 if err != nil { 178 return err 179 } 180 repo := NewGitDir(installPath) 181 if err = repo.Init(); err != nil { 182 return err 183 } 184 if err = repo.Pull(d.current); err != nil { 185 return err 186 } 187 if err = repo.Snapshotf("Deployed charm %q.", url); err != nil { 188 return err 189 } 190 log.Infof("worker/uniter/charm: deploying charm") 191 return os.Rename(installPath, d.target.Path()) 192 } 193 194 // upgrade pulls from current into target. If target has local changes, but 195 // no conflicts, it will be snapshotted before any changes are made. 196 func (d *gitDeployer) upgrade() error { 197 log.Infof("worker/uniter/charm: preparing charm upgrade") 198 url, err := ReadCharmURL(d.current) 199 if err != nil { 200 return err 201 } 202 if err := d.target.Init(); err != nil { 203 return err 204 } 205 if dirty, err := d.target.Dirty(); err != nil { 206 return err 207 } else if dirty { 208 if conflicted, err := d.target.Conflicted(); err != nil { 209 return err 210 } else if !conflicted { 211 log.Infof("worker/uniter/charm: snapshotting dirty charm before upgrade") 212 if err = d.target.Snapshotf("Pre-upgrade snapshot."); err != nil { 213 return err 214 } 215 } 216 } 217 log.Infof("worker/uniter/charm: deploying charm") 218 if err := d.target.Pull(d.current); err != nil { 219 return err 220 } 221 return d.target.Snapshotf("Upgraded charm to %q.", url) 222 } 223 224 // collectOrphans deletes all repos in dataPath except the one pointed to by current. 225 // Errors are generally ignored; some are logged. 226 func (d *gitDeployer) collectOrphans() { 227 current, err := os.Readlink(d.current.Path()) 228 if err != nil { 229 return 230 } 231 if !filepath.IsAbs(current) { 232 current = filepath.Join(d.dataPath, current) 233 } 234 orphans, err := filepath.Glob(filepath.Join(d.dataPath, fmt.Sprintf("%s*", updatePrefix))) 235 if err != nil { 236 return 237 } 238 installOrphans, err := filepath.Glob(filepath.Join(d.dataPath, fmt.Sprintf("%s*", installPrefix))) 239 if err != nil { 240 return 241 } 242 orphans = append(orphans, installOrphans...) 243 for _, repoPath := range orphans { 244 if repoPath != d.dataPath && repoPath != current { 245 if err = os.RemoveAll(repoPath); err != nil { 246 log.Warningf("worker/uniter/charm: failed to remove orphan repo at %s: %s", repoPath, err) 247 } 248 } 249 } 250 } 251 252 // newDir creates a new timestamped directory with the given prefix. It 253 // assumes that the deployer will not need to create more than 10 254 // directories in any given second. 255 func (d *gitDeployer) newDir(prefix string) (string, error) { 256 return ioutil.TempDir(d.dataPath, prefix+time.Now().Format("20060102-150405")) 257 }