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