Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(174)

Side by Side Diff: deploytool/cmd/checkout.go

Issue 2182213002: deploytool: Add README.md, migrate docs to it. (Closed) Base URL: https://github.com/luci/luci-go@master
Patch Set: Rename to "luci_deploy" Created 4 years, 4 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « deploytool/cmd/build.go ('k') | deploytool/cmd/config.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 The LUCI Authors. All rights reserved.
2 // Use of this source code is governed under the Apache License, Version 2.0
3 // that can be found in the LICENSE file.
4
5 package main
6
7 import (
8 "crypto/sha256"
9 "encoding/hex"
10 "fmt"
11 "net/url"
12 "os"
13 "path/filepath"
14 "sort"
15 "strconv"
16 "strings"
17
18 "github.com/luci/luci-go/common/cli"
19 "github.com/luci/luci-go/common/errors"
20 log "github.com/luci/luci-go/common/logging"
21 "github.com/luci/luci-go/deploytool/api/deploy"
22 "github.com/luci/luci-go/deploytool/managedfs"
23
24 "github.com/maruel/subcommands"
25 )
26
27 // deployToolCfg is the name of the source-root deployment configuration file.
28 const (
29 // deployToolCfg is the name of the source-root deploytool configuration file.
30 deployToolCfg = ".luci-deploytool.cfg"
31
32 // checkoutsSubdir is the name of the checkouts directory underneath the
33 // working directory.
34 checkoutsSubdir = "checkouts"
35 // frozenCheckoutName is the name in the checkout directory of the froze n
36 // checkout file.
37 frozenCheckoutName = "checkout.frozen.cfg"
38
39 // gitMajorVersionSize it the number of characters from the Git revision hash
40 // to use for its major version.
41 gitMajorVersionSize = 7
42 )
43
44 var cmdCheckout = subcommands.Command{
45 UsageLine: "checkout",
46 ShortDesc: "Performs a checkout of some or all Sources.",
47 LongDesc: "Performs a checkout of some or all Sources into the working directory.",
48 CommandRun: func() subcommands.CommandRun {
49 var cmd cmdCheckoutRun
50 cmd.Flags.BoolVar(&cmd.local, "local", false,
51 "Apply user-configured URL overrides.")
52 return &cmd
53 },
54 }
55
56 type cmdCheckoutRun struct {
57 subcommands.CommandRunBase
58
59 local bool
60 }
61
62 func (cmd *cmdCheckoutRun) Run(app subcommands.Application, args []string) int {
63 a, c := app.(*application), cli.GetContext(app, cmd)
64
65 // Perform the checkout.
66 err := a.runWork(c, func(w *work) error {
67 return checkout(w, &a.layout, cmd.local)
68 })
69 if err != nil {
70 logError(c, err, "Failed to checkout.")
71 return 1
72 }
73 return 0
74 }
75
76 func checkout(w *work, l *deployLayout, applyOverrides bool) error {
77 frozen, err := l.initFrozenCheckout(w)
78 if err != nil {
79 return errors.Annotate(err).Reason("failed to initialize checkou t").Err()
80 }
81
82 // reg is our internal checkout registry. This represents the actual
83 // repository checkouts that we perform. Duplicate sources to the same
84 // repository will be deduplicated here.
85 fs, err := l.workingFilesystem()
86 if err != nil {
87 return errors.Annotate(err).Err()
88 }
89 checkoutDir, err := fs.Base().EnsureDirectory(checkoutsSubdir)
90 if err != nil {
91 return errors.Annotate(err).Reason("failed to create checkout di rectory").Err()
92 }
93
94 repoDir, err := checkoutDir.EnsureDirectory("repository")
95 if err != nil {
96 return errors.Annotate(err).Reason("failed to create repository directory %(dir)q").
97 D("dir", repoDir).Err()
98 }
99
100 reg := checkoutRegistry{
101 repoDir: repoDir,
102 }
103
104 // Do a central checkout of registry repositories. We will project this using
105 // sorted keys so that checkout failures happen consistently.
106 var (
107 scs []*sourceCheckout
108 sgSources = make(map[string][]*sourceCheckout, len(frozen.Source Group))
109 )
110
111 sgKeys := make([]string, 0, len(frozen.SourceGroup))
112 for k := range frozen.SourceGroup {
113 sgKeys = append(sgKeys, k)
114 }
115 sort.Strings(sgKeys)
116
117 for _, sgKey := range sgKeys {
118 sg := frozen.SourceGroup[sgKey]
119
120 srcKeys := make([]string, 0, len(sg.Source))
121 groupSrcs := make([]*sourceCheckout, len(sg.Source))
122 for k := range sg.Source {
123 srcKeys = append(srcKeys, k)
124 }
125 sort.Strings(srcKeys)
126
127 for i, srcKey := range srcKeys {
128 sc := sourceCheckout{
129 FrozenLayout_Source: sg.Source[srcKey],
130 group: sgKey,
131 name: srcKey,
132 }
133
134 if err := sc.addRegistryRepos(&reg); err != nil {
135 return errors.Annotate(err).Reason("failed to ad d [%(sourceCheckout)s] to registry").
136 D("sourceCheckout", sc).Err()
137 }
138
139 // If we're overriding sources, and this source is overr idden, then apply
140 // this and add the overriding source to the registry as well.
141 //
142 // We will still keep the original source in the registr y so it doesn't
143 // get deleted during cleanup.
144 if applyOverrides {
145 if override, ok := l.userSourceOverrides[sc.over rideURL]; ok {
146 log.Infof(w, "Applying user repository o verride: [%+v] => [%+v]", sc.Source, override)
147
148 sc.FrozenLayout_Source.Source = override
149 if err := sc.addRegistryRepos(&reg); err != nil {
150 return errors.Annotate(err).Reas on("failed to add (overridden) [%(sourceCheckout)s] to registry").
151 D("sourceCheckout", sc). Err()
152 }
153 }
154 }
155
156 groupSrcs[i] = &sc
157 scs = append(scs, &sc)
158 }
159
160 sgSources[sgKey] = groupSrcs
161 }
162 if err := reg.checkout(w); err != nil {
163 return errors.Annotate(err).Reason("failed to checkout sources") .Err()
164 }
165
166 // Execute each source checkout in parallel.
167 sourcesDir, err := checkoutDir.EnsureDirectory("sources")
168 if err != nil {
169 return errors.Annotate(err).Reason("failed to create sources dir ectory").Err()
170 }
171
172 err = w.RunMulti(func(workC chan<- func() error) {
173 for _, sc := range scs {
174 sc := sc
175 workC <- func() error {
176 root, err := sourcesDir.EnsureDirectory(sc.group , sc.name)
177 if err != nil {
178 return errors.Annotate(err).Reason("fail ed to create checkout directory").Err()
179 }
180
181 if err := sc.checkout(w, root); err != nil {
182 return errors.Annotate(err).Reason("fail ed to checkout %(sourceCheckout)s").
183 D("sourceCheckout", sc).Err()
184 }
185 return nil
186 }
187 }
188 })
189 if err != nil {
190 return err
191 }
192
193 // Build our source groups' checkout revision hashes.
194 for _, sgKey := range sgKeys {
195 sg := frozen.SourceGroup[sgKey]
196 if sg.RevisionHash != "" {
197 // Already calculated.
198 continue
199 }
200
201 hash := sha256.New()
202 for _, sc := range sgSources[sgKey] {
203 if sc.Revision == "" {
204 return errors.Reason("source %(sourceCheckout)q has an empty revision").
205 D("sourceCheckout", sc.String()).Err()
206 }
207 fmt.Fprintf(hash, "%s@%s\x00", sc.name, sc.Revision)
208
209 // If any of our sources was determined to be tainted, t aint the source
210 // group too.
211 if sc.cs.tainted || sc.Source.Tainted {
212 sg.Tainted = true
213 }
214 }
215
216 sg.RevisionHash = hex.EncodeToString(hash.Sum(nil))
217 log.Fields{
218 "sourceGroup": sgKey,
219 "revision": sg.RevisionHash,
220 "tainted": sg.Tainted,
221 }.Debugf(w, "Checked out source group.")
222 }
223
224 // Create the frozen checkout file.
225 frozenFile := checkoutDir.File(frozenCheckoutName)
226 if err := frozenFile.GenerateTextProto(w, frozen); err != nil {
227 return errors.Annotate(err).Reason("failed to create frozen chec kout protobuf").Err()
228 }
229
230 if err := checkoutDir.CleanUp(); err != nil {
231 return errors.Annotate(err).Reason("failed to do full cleanup of cleanup sources filesystem").Err()
232 }
233
234 return nil
235 }
236
237 func checkoutFrozen(l *deployLayout) (*deploy.FrozenLayout, error) {
238 path := filepath.Join(l.WorkingPath, checkoutsSubdir, frozenCheckoutName )
239
240 var frozen deploy.FrozenLayout
241 if err := unmarshalTextProtobuf(path, &frozen); err != nil {
242 return nil, errors.Annotate(err).Err()
243 }
244 return &frozen, nil
245 }
246
247 // sourceCheckout manages the operation of checking out the specified layout
248 // source.
249 type sourceCheckout struct {
250 *deploy.FrozenLayout_Source
251
252 // group is the name of the source group.
253 group string
254 // name is the name of this source.
255 name string
256
257 // overrideURL is the calculated user config override URL that this sour ce
258 // matches.
259 overrideURL string
260
261 // cs is the checkout singleton populated from the checkout registry.
262 cs *checkoutSingleton
263 }
264
265 func (sc *sourceCheckout) String() string {
266 return fmt.Sprintf("%s.%s", sc.group, sc.name)
267 }
268
269 func (sc *sourceCheckout) addRegistryRepos(reg *checkoutRegistry) error {
270 switch t := sc.Source.GetSource().(type) {
271 case *deploy.Source_Git:
272 g := t.Git
273
274 u, err := url.Parse(g.Url)
275 if err != nil {
276 return errors.Annotate(err).Reason("failed to parse Git URL [%(url)s]").D("url", g.Url).Err()
277 }
278
279 // Add a Git checkout operation for this source to the registry.
280 sc.cs = reg.add(&gitCheckoutOperation{
281 url: u,
282 ref: g.Ref,
283 }, sc.Source.RunScripts)
284 sc.overrideURL = g.Url
285 }
286
287 return nil
288 }
289
290 func (sc *sourceCheckout) checkout(w *work, root *managedfs.Dir) error {
291 checkoutPath := root.File("c")
292
293 switch t := sc.Source.GetSource().(type) {
294 case *deploy.Source_Git:
295 if sc.cs.path == "" {
296 panic("registry repo path is not set")
297 }
298
299 // Add a symlink between our raw checkout and our current checko ut.
300 if err := checkoutPath.SymlinkFrom(sc.cs.path, true); err != nil {
301 return errors.Annotate(err).Err()
302 }
303 sc.Relpath = checkoutPath.RelPath()
304
305 default:
306 return errors.Reason("don't know how to checkout %(type)T").D("t ype", t).Err()
307 }
308
309 sc.Revision = sc.cs.revision
310 sc.MajorVersion = sc.cs.majorVersion
311 sc.MinorVersion = sc.cs.minorVersion
312 sc.InitResult = &deploy.SourceInitResult{
313 GoPath: append(sc.Source.GoPath, sc.cs.sir.GoPath...),
314 }
315 return nil
316 }
317
318 // checkoutSingleton represents a single unique checkout.
319 //
320 // It is constructed by checkoutRegistry and populated during checkout.
321 type checkoutSingleton struct {
322 // op is the operation to execute to populate this singleton.
323 op checkoutOperation
324 // runInit, if true, says that at least one of the sources is permitting
325 // this repository to run its initialization scripts.
326 runInit bool
327
328 // path is the path to the base directory of the checkout. It is populat ed by
329 // op's checkout method.
330 path string
331 // revision is the checkout's actual revision.
332 revision string
333 // majorVersion is the source's major version value.
334 majorVersion string
335 // minorVersion is the source's minor version value.
336 minorVersion string
337 // tainted is true if this checkout was detected to be tainted.
338 tainted bool
339
340 // sir is the SourceInitResult constructed during the checkout.
341 sir *deploy.SourceInitResult
342 }
343
344 // checkoutRegistry maps unique checkout identifiers to Promise-backed checkout
345 // resolvers.
346 //
347 // This is a more foundational layer than "repository", and is responsible for
348 // actually resolving a given checkout exactly once. Multiple checkout entries
349 // may map to a single registry item.
350 type checkoutRegistry struct {
351 // repoDir is the base checkout path. All checkouts will be placed
352 // in hash-named files underneath of this path.
353 repoDir *managedfs.Dir
354
355 // singletons is a set of checkout singletons registered for each unique
356 // repository key.
357 singletons map[string]*checkoutSingleton
358 }
359
360 func (reg *checkoutRegistry) add(op checkoutOperation, runInit bool) *checkoutSi ngleton {
361 key := op.key()
362
363 cs, ok := reg.singletons[key]
364 if !ok {
365 cs = &checkoutSingleton{
366 op: op,
367 }
368
369 if reg.singletons == nil {
370 reg.singletons = make(map[string]*checkoutSingleton)
371 }
372 reg.singletons[key] = cs
373 }
374 if runInit {
375 cs.runInit = true
376 }
377
378 return cs
379 }
380
381 func (reg *checkoutRegistry) checkout(w *work) error {
382 opKeys := make([]string, 0, len(reg.singletons))
383 for key := range reg.singletons {
384 opKeys = append(opKeys, key)
385 }
386 sort.Strings(opKeys)
387
388 err := w.RunMulti(func(workC chan<- func() error) {
389 for _, key := range opKeys {
390 key, cs := key, reg.singletons[key]
391 workC <- func() error {
392 // Generate the path of this checkout. We do thi s by hashing the checkout's
393 // key.
394 pathHash := sha256.Sum256([]byte(key))
395
396 // Perform the actual checkout operation.
397 checkoutDir, err := reg.repoDir.EnsureDirectory( hex.EncodeToString(pathHash[:]))
398 if err != nil {
399 return errors.Annotate(err).Reason("fail ed to create checkout directory for %(key)q").D("key", key).Err()
400 }
401
402 log.Fields{
403 "key": key,
404 "checkoutPath": checkoutDir,
405 }.Debugf(w, "Creating checkout directory.")
406 if err := cs.op.checkout(w, cs, checkoutDir); er r != nil {
407 return err
408 }
409
410 // Make sure "checkout" did what it was supposed to.
411 if cs.path == "" {
412 return errors.New("checkout did not popu late path")
413 }
414
415 // If there is a deployment configuration, load/ parse/execute it.
416 sl, err := loadSourceLayout(cs.path)
417 if err != nil {
418 return errors.Annotate(err).Reason("fail ed to load source layout").Err()
419 }
420
421 var sir deploy.SourceInitResult
422 if sl != nil {
423 sir.GoPath = sl.GoPath
424
425 if len(sl.Init) > 0 {
426 if cs.runInit {
427 for i, in := range sl.In it {
428 inResult, err := sourceInit(w, cs.path, in)
429 if err != nil {
430 return e rrors.Annotate(err).Reason("failed to run source init #%(index)d").
431 D("index", i).Err()
432 }
433
434 // Merge this So urceInitResult into the common repository
435 // result.
436 sir.GoPath = app end(sir.GoPath, inResult.GoPath...)
437 }
438 } else {
439 log.Fields{
440 "key": key,
441 "path": cs.path,
442 }.Warningf(w, "Source de fines initialization scripts, but is not configured to run them.")
443 }
444 }
445 }
446 cs.sir = &sir
447 return nil
448 }
449 }
450 })
451 if err != nil {
452 return err
453 }
454
455 return nil
456 }
457
458 type checkoutOperation interface {
459 // key returns a unique key that describes this checkout. It should be
460 // sufficiently general such that any identical checkout will share this
461 // key.
462 key() string
463
464 // checkout performs the actual checkout operation.
465 //
466 // Upon success, checkout should populate the following checkoutSingleto n
467 // fields:
468 // - path
469 // - revision
470 checkout(*work, *checkoutSingleton, *managedfs.Dir) error
471 }
472
473 type gitCheckoutOperation struct {
474 // url is the URL of the Git repository.
475 url *url.URL
476 // ref is the Git ref to check out.
477 ref string
478 }
479
480 func (g *gitCheckoutOperation) key() string {
481 return fmt.Sprintf("git+%s@%s", g.url.String(), g.ref)
482 }
483
484 func (g *gitCheckoutOperation) checkout(w *work, cs *checkoutSingleton, dir *man agedfs.Dir) error {
485 git, err := w.git()
486 if err != nil {
487 return err
488 }
489
490 // If our URL is a file URL, the checkout should be an absolute symlink to the
491 // file.
492 var (
493 path string
494 )
495 if g.url.Scheme == "file" {
496 fileLink := dir.File("file")
497 if err := fileLink.SymlinkFrom(fileURLToPath(g.url.Path), false) ; err != nil {
498 return err
499 }
500
501 cs.tainted = true
502 path = fileLink.String()
503 } else {
504 // This is a Git-managed directory, so we don't need to pay atte ntion to its
505 // file contents.
506 dir.Ignore()
507
508 // Get current state of target directory.
509 path = dir.String()
510 ref := g.ref
511 if ref == "" {
512 ref = "master"
513 }
514 gitDir := filepath.Join(path, ".git")
515 needsFetch, resetRef := true, "refs/deploytool/checkout"
516 switch st, err := os.Stat(gitDir); {
517 case err == nil:
518 if !st.IsDir() {
519 return errors.Reason("checkout Git path [%(path) s] exists, and is not a directory").D("path", gitDir).Err()
520 }
521
522 case isNotExist(err):
523 // If the target directory doesn't exist, run "git clone ".
524 log.Fields{
525 "source": g.url,
526 "destination": path,
527 }.Infof(w, "No current checkout; cloning...")
528 if err := git.clone(w, g.url.String(), path); err != nil {
529 return err
530 }
531 if err = git.exec(path, "update-ref", resetRef, ref).che ck(w); err != nil {
532 return errors.Annotate(err).Reason("failed to ch eckout %(ref)q from %(url)q").D("ref", ref).D("url", g.url).Err()
533 }
534 needsFetch = false
535
536 default:
537 return errors.Annotate(err).Reason("failed to stat check out Git directory [%(dir)s]").D("dir", gitDir).Err()
538 }
539
540 // Check out the desired commit/ref by resetting the repository.
541 //
542 // Check if the referenced ref is a commit that is already prese nt in the
543 // repository.
544 x := git.exec(path, "rev-parse", ref)
545 switch rc, err := x.run(w); {
546 case err != nil:
547 return errors.Annotate(err).Reason("failed to check for commit %(ref)q").D("ref", ref).Err()
548
549 case rc == 0:
550 // If the ref resolved to itself, then it's a commit and it's already in the
551 // repository, so no need to fetch.
552 if strings.TrimSpace(x.stdout.String()) == ref {
553 resetRef = ref
554 needsFetch = false
555 }
556 fallthrough
557
558 default:
559 // If our checkout isn't ready, fetch the ref remotely.
560 if needsFetch {
561 if err := git.exec(path, "fetch", "origin", fmt. Sprintf("%s:%s", ref, resetRef)).check(w); err != nil {
562 return errors.Annotate(err).Reason("fail ed to fetch %(ref)q from remote").D("ref", ref).Err()
563 }
564 }
565
566 // Reset to "resetRef".
567 if err := git.exec(path, "reset", "--hard", resetRef).ch eck(w); err != nil {
568 return errors.Annotate(err).Reason("failed to ch eckout %(ref)q (%(localRef)q) from %(url)q").
569 D("ref", ref).D("localRef", resetRef).D( "url", g.url).Err()
570 }
571 }
572 }
573
574 // Get the current Git repository parameters.
575 var (
576 revision, mergeBase string
577 revCount int
578 )
579 err = w.RunMulti(func(workC chan<- func() error) {
580 // Get HEAD revision.
581 workC <- func() (err error) {
582 revision, err = git.getHEAD(w, path)
583 return
584 }
585
586 // Get merge base revision.
587 workC <- func() (err error) {
588 mergeBase, err = git.getMergeBase(w, path, "origin/maste r")
589 return
590 }
591
592 // Get commit depth.
593 workC <- func() (err error) {
594 revCount, err = git.getRevListCount(w, path)
595 return
596 }
597 })
598 if err != nil {
599 return errors.Annotate(err).Reason("failed to get Git repository properties").Err()
600 }
601
602 // We're tainted if our merge base doesn't equal our current revision.
603 if mergeBase != revision {
604 cs.tainted = true
605 }
606
607 log.Fields{
608 "url": g.url,
609 "ref": g.ref,
610 "path": path,
611 "mergeBase": mergeBase,
612 "revision": revision,
613 "revCount": revCount,
614 "tainted": cs.tainted,
615 }.Debugf(w, "Checked out Git repository.")
616
617 cs.path = path
618 cs.revision = revision
619 cs.majorVersion = string([]rune(mergeBase)[:gitMajorVersionSize])
620 cs.minorVersion = strconv.Itoa(revCount)
621 return nil
622 }
623
624 func loadSourceLayout(path string) (*deploy.SourceLayout, error) {
625 layoutPath := filepath.Join(path, deployToolCfg)
626 var sl deploy.SourceLayout
627 switch err := unmarshalTextProtobuf(layoutPath, &sl); {
628 case err == nil:
629 return &sl, nil
630
631 case isNotExist(err):
632 // There is no source layout definition in this source repositor y.
633 return nil, nil
634
635 default:
636 // An error occurred loading the source layout.
637 return nil, err
638 }
639 }
640
641 func sourceInit(w *work, path string, in *deploy.SourceLayout_Init) (*deploy.Sou rceInitResult, error) {
642 switch t := in.GetOperation().(type) {
643 case *deploy.SourceLayout_Init_PythonScript_:
644 ps := t.PythonScript
645
646 python, err := w.python()
647 if err != nil {
648 return nil, err
649 }
650
651 // Create a temporary directory for the SourceInitResult.
652 var r deploy.SourceInitResult
653 err = withTempDir(func(tdir string) error {
654 resultPath := filepath.Join(tdir, "source_init_result.cf g")
655
656 scriptPath := deployToNative(path, ps.Path)
657 if err := python.exec(scriptPath, path, resultPath).cwd( path).check(w); err != nil {
658 return errors.Annotate(err).Reason("failed to ex ecute [%(scriptPath)s]").D("scriptPath", scriptPath).Err()
659 }
660
661 switch err := unmarshalTextProtobuf(resultPath, &r); {
662 case err == nil, isNotExist(err):
663 return nil
664
665 default:
666 return errors.Annotate(err).Reason("failed to st at SourceInitResult [%(resultPath)s]").
667 D("resultPath", resultPath).Err()
668 }
669 })
670 return &r, err
671
672 default:
673 return nil, errors.Reason("unknown source init type %(type)T").D ("type", t).Err()
674 }
675 }
OLDNEW
« no previous file with comments | « deploytool/cmd/build.go ('k') | deploytool/cmd/config.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698