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