github.com/polydawn/docket@v0.5.4-0.20140630233848-90b70fb433da/dex/graph.go (about) 1 package dex 2 3 import ( 4 "fmt" 5 "io/ioutil" 6 "os" 7 "path/filepath" 8 . "polydawn.net/pogo/gosh" 9 . "polydawn.net/hroot/crocker" 10 "polydawn.net/hroot/util" 11 "strings" 12 ) 13 14 type Graph struct { 15 /* 16 Absolute path to the base/working dir for of the graph git repository. 17 */ 18 dir string 19 20 /* 21 Cached command template for exec'ing git with this graph's cwd. 22 */ 23 cmd Command 24 } 25 26 // strap this in only sometimes -- some git commands need this prefix to be explicit about branches instead of tags; others refuse it because they're already forcibly about branches. 27 const git_branch_ref_prefix = "refs/heads/" 28 const hroot_ref_prefix = "hroot/" 29 const hroot_image_ref_prefix = hroot_ref_prefix+"image/" 30 31 /* 32 Loads a Graph if there is a git repo initialized at the given dir; returns nil if a graph repo not found. 33 The dir must be the root of the working tree of the git dir. 34 35 A graph git repo is distingushed by containing branches that start with "hroot/" -- this is how hroot outputs branches that contain its data. 36 */ 37 func LoadGraph(dir string) *Graph { 38 // optimistically, set up the struct we're checking out 39 g := newGraph(dir) 40 41 // ask git what it thinks of all this. 42 if g.isHrootGraphRepo() { 43 return g 44 } else { 45 return nil 46 } 47 } 48 49 /* 50 Attempts to load a Graph at the given dir, or creates a new one if no graph repo is found. 51 If a new graph is fabricated, it will be initialized by: 52 - creating a new git repo, 53 - making a blank root commit, 54 - and tagging it with a branch name that declares it to be a graph repo. 55 56 Note if your cwd is already in a git repo, the new graph will not be commited, nor will it be made a submodule. 57 You're free to make it a submodule yourself, but git quite wants you to have a remote url before it accepts your submodule. 58 */ 59 func NewGraph(dir string) *Graph { 60 g := newGraph(dir) 61 if g.isHrootGraphRepo() { 62 // if we can just be a load, do it 63 return g 64 } else if g.isRepoRoot() { 65 // if this is a repo root, but didn't look like a real graph... 66 util.ExitGently("Attempted to make a hroot graph at ", g.dir, ", but there is already a git repo there and it does not appear to belong to hroot.") 67 } // else carry on, make it! 68 69 // we'll make exactly one new dir if the path doesn't exist yet. more is probably argument error and we abort. 70 // this is actually implemented via MkdirAll here (because Mkdir errors on existing, and I can't be arsed) and letting the SaneDir check earlier blow up if we're way out. 71 err := os.MkdirAll(g.dir, 0755) 72 if err != nil { panic(err); } 73 74 // git init 75 g.cmd("init")("--bare")() 76 77 g.withTempTree(func (cmd Command) { 78 // set up basic repo to identify as graph repo 79 cmd("commit", "--allow-empty", "-mhroot")() 80 cmd("checkout", "-b", hroot_ref_prefix+"init")() 81 82 // discard master branch. a hroot graph has no real use for it. 83 cmd("branch", "-D", "master")() 84 }) 85 86 // should be good to go 87 return g 88 } 89 90 func newGraph(dir string) *Graph { 91 dir = util.SanePath(dir) 92 93 // optimistically, set up the struct. 94 // we still need to either verify or initalize git here. 95 return &Graph{ 96 dir: dir, 97 cmd: Sh("git")(DefaultIO)(Opts{Cwd: dir}), 98 } 99 } 100 101 func (g *Graph) isRepoRoot() (v bool) { 102 defer func() { 103 // if the path doesn't even exist, launching the command will panic, and that's fine. 104 // if the path isn't within a git repo at all, it will exit with 128, gosh will panic, and that's fine. 105 if recover() != nil { 106 v = false 107 } 108 }() 109 revp := g.cmd(NullIO)("rev-parse", "--is-bare-repository").Output() 110 v = (revp == "true\n") 111 return 112 } 113 114 //Is git ready and configured to make commits? 115 func (g *Graph) IsConfigReady() bool { 116 //Get the current git configuration 117 config := g.cmd(NullIO)("config", "--list").Output() 118 119 //Check that a user name and email is defined 120 return strings.Contains(config, "user.name=") && strings.Contains(config, "user.email=") 121 } 122 123 /* 124 Creates a temporary working tree in a new directory. Changes the cwd to that location. 125 The directory will be empty. The directory will be removed when your function returns. 126 */ 127 func (g *Graph) withTempTree(fn func(cmd Command)) { 128 // ensure zone for temp trees is established 129 tmpTreeBase := filepath.Join(g.dir, "worktrees") 130 err := os.MkdirAll(tmpTreeBase, 0755) 131 if err != nil { panic(err); } 132 133 // make temp dir for tree 134 tmpdir, err := ioutil.TempDir(tmpTreeBase, "tree.") 135 if err != nil { panic(err); } 136 defer os.RemoveAll(tmpdir) 137 138 // set cwd 139 retreat, err := os.Getwd() 140 if err != nil { panic(err); } 141 defer os.Chdir(retreat) 142 err = os.Chdir(tmpdir) 143 if err != nil { panic(err); } 144 145 // construct git command template that knows what's up 146 gt := g.cmd( 147 Opts{ 148 Cwd:tmpdir, 149 }, 150 Env{ 151 "GIT_WORK_TREE": tmpdir, 152 "GIT_DIR": g.dir, 153 }, 154 ) 155 156 // go time 157 fn(gt) 158 } 159 160 func (g *Graph) Publish(lineage string, ancestor string, gr GraphStoreRequest) (hash string) { 161 // Handle tags - currently, we discard them when dealing with a graph repo. 162 lineage, _ = SplitImageName(lineage) 163 ancestor, _ = SplitImageName(ancestor) 164 165 g.withTempTree(func(cmd Command) { 166 fmt.Println("Starting publish of ", lineage, " <-- ", ancestor) 167 168 // check if appropriate branches already exist, and make them if necesary 169 if strings.Count(g.cmd("branch", "--list", hroot_image_ref_prefix+lineage).Output(), "\n") >= 1 { 170 fmt.Println("Lineage already existed.") 171 // this is an existing lineage 172 g.cmd("symbolic-ref", "HEAD", git_branch_ref_prefix+hroot_image_ref_prefix+lineage)() 173 } else { 174 // this is a new lineage 175 if ancestor == "" { 176 fmt.Println("New lineage! Making orphan branch for it.") 177 g.cmd("checkout", "--orphan", hroot_image_ref_prefix+lineage)() 178 } else { 179 fmt.Println("New lineage! Forking it from ancestor branch.") 180 g.cmd("branch", hroot_image_ref_prefix+lineage, hroot_image_ref_prefix+ancestor)() 181 g.cmd("symbolic-ref", "HEAD", git_branch_ref_prefix+hroot_image_ref_prefix+lineage)() 182 } 183 } 184 g.cmd("reset") 185 186 // apply the GraphStoreRequest to unpack the fs (read from fs.tarReader, essentially) 187 gr.place(".") 188 189 // exec git add, tree write, merge, commit. 190 g.cmd("add", "--all")() 191 g.forceMerge(ancestor, lineage) 192 193 hash = "" //FIXME 194 }) 195 return 196 } 197 198 func (g *Graph) Load(lineage string, gr GraphLoadRequest) (hash string) { 199 lineage, _ = SplitImageName(lineage) //Handle tags 200 201 //Check if the image is in the graph so we can generate a relatively friendly error message 202 if !g.HasBranch(hroot_image_ref_prefix+lineage) { //HALP 203 util.ExitGently("Image branch name", lineage, "not found in graph.") 204 } 205 206 g.withTempTree(func(cmd Command) { 207 // checkout lineage. 208 // "-f" because otherwise if git thinks we already had this branch checked out, this working tree is just chock full of deletes. 209 g.cmd("checkout", "-f", git_branch_ref_prefix+hroot_image_ref_prefix+lineage)() 210 211 // the gr consumes this filesystem and shoves it at whoever it deals with; we're actually hands free after handing over a dir. 212 gr.receive(".") 213 214 hash = "" //FIXME 215 }) 216 return 217 } 218 219 // having a load-by-hash: 220 // - you can't combine it with lineage, because git doesn't really know what branches are, historically speaking. 221 // - we could traverse up from the lineage branch ref and make sure the hash is reachable from it, but more than one ref is going to be able to reach most hashes (i.e. hashes that are pd-base will be reachable from pd-nginx). 222 // - unless we decide we're committing lineage in some structured form of the commit messages. 223 // - which... yes, yes we are. the first word of any commit is going to be the image lineage name. 224 // - after the space can be anything, but we're going to default to a short description of where it came from. 225 // - additional lines can be anything you want. 226 // - if we need more attributes in the future, we'll start doing them with the git psuedo-standard of trailing "Signed-Off-By: %{name}\n" key-value pairs. 227 // - we won't validate any of this if you're not using load-by-hash. 228 229 230 func (g *Graph) forceMerge(source string, target string) { 231 writeTree := g.cmd("write-tree").Output() 232 writeTree = strings.Trim(writeTree, "\n") 233 commitMsg := "" 234 if source == "" { 235 commitMsg = fmt.Sprintf("%s imported from an external source", target) 236 } else { 237 commitMsg = fmt.Sprintf("%s updated from %s", target, source) 238 } 239 commitTreeCmd := g.cmd("commit-tree", writeTree, Opts{In: commitMsg}) 240 if source != "" { 241 commitTreeCmd = commitTreeCmd( 242 "-p", git_branch_ref_prefix+hroot_image_ref_prefix+source, 243 "-p", git_branch_ref_prefix+hroot_image_ref_prefix+target, 244 ) 245 } 246 mergeTree := strings.Trim(commitTreeCmd.Output(), "\n") 247 g.cmd("merge", "-q", mergeTree)() 248 } 249 250 //Checks if the graph has a branch. 251 func (g *Graph) HasBranch(branch string) bool { 252 //Git magic is involved. Response will be of non-zero length if branch exists. 253 result := g.cmd("ls-remote", ".", git_branch_ref_prefix + branch).Output() 254 return len(result) > 0 255 } 256 257 /* 258 Check if a git repo exists and if it has the branches that declare it a hroot graph. 259 */ 260 func (g *Graph) isHrootGraphRepo() bool { 261 if !g.isRepoRoot() { return false; } 262 if !g.HasBranch("hroot/init") { return false; } 263 // We could say a hroot graph shouldn't have a master branch, but we won't. 264 // We don't create one by default, but you're perfectly welcome to do so and put a readme for your coworkers in it or whatever. 265 return true 266 }