OLD | NEW |
| (Empty) |
1 // Copyright 2014 The Chromium Authors. All rights reserved. | |
2 // Use of this source code is governed by a BSD-style license that can be | |
3 // found in the LICENSE file. | |
4 | |
5 package cipd | |
6 | |
7 import ( | |
8 "crypto/sha1" | |
9 "encoding/base64" | |
10 "fmt" | |
11 "io/ioutil" | |
12 "os" | |
13 "path/filepath" | |
14 "sort" | |
15 "strings" | |
16 "sync" | |
17 "time" | |
18 ) | |
19 | |
20 // TODO(vadimsh): Make it work on Windows, verify it works on Mac. | |
21 | |
22 // TODO(vadimsh): How to handle path conflicts between two packages? Currently | |
23 // the last one installed wins. | |
24 | |
25 // File system layout of a site directory <root>: | |
26 // <root>/.cipd/pkgs/ | |
27 // <package name digest>/ | |
28 // _current -> symlink to fea3ab83440e9dfb813785e16d4101f331ed44f4 | |
29 // fea3ab83440e9dfb813785e16d4101f331ed44f4/ | |
30 // bin/ | |
31 // tool | |
32 // ... | |
33 // ... | |
34 // bin/ | |
35 // tool -> symlink to ../.cipd/pkgs/<package name digest>/_current/bin/tool | |
36 // ... | |
37 // | |
38 // Where <package name digest> is derived from a package name. It doesn't have | |
39 // to be reversible though, since the package name is still stored in the | |
40 // installed package manifest and can be read from there. | |
41 // | |
42 // Some efforts are made to make sure that during the deployment a window of | |
43 // inconsistency in the file system is as small as possible. | |
44 | |
45 // Subdirectory of site root to extract packages to. | |
46 const packagesDir = siteServiceDir + "/pkgs" | |
47 | |
48 // Name of a symlink that points to latest deployed version. | |
49 const currentSymlink = "_current" | |
50 | |
51 // PackageState contains information about single deployed package. | |
52 type PackageState struct { | |
53 // PackageName identifies the package. | |
54 PackageName string | |
55 // InstanceID is ID of the installed package instance (SHA1 of package c
ontents). | |
56 InstanceID string | |
57 } | |
58 | |
59 // DeployInstance installs a specific instance of a package (identified by | |
60 // InstanceID()) into a site root directory. It unpacks the package into | |
61 // <root>/.cipd/pkgs/*, and rearranges symlinks to point to unpacked files. | |
62 // It tries to make it as "atomic" as possibly. | |
63 func DeployInstance(root string, inst PackageInstance) (state PackageState, err
error) { | |
64 root, err = filepath.Abs(filepath.Clean(root)) | |
65 if err != nil { | |
66 return | |
67 } | |
68 log.Infof("Deploying %s:%s into %s", inst.PackageName(), inst.InstanceID
(), root) | |
69 | |
70 // Be paranoid. | |
71 err = ValidatePackageName(inst.PackageName()) | |
72 if err != nil { | |
73 return | |
74 } | |
75 err = ValidateInstanceID(inst.InstanceID()) | |
76 if err != nil { | |
77 return | |
78 } | |
79 | |
80 // Remember currently deployed version (to remove it later). Do not frea
k out | |
81 // if it's not there (prevID is "" in that case). | |
82 oldFiles := makeStringSet() | |
83 prevID := findDeployedInstance(root, inst.PackageName(), oldFiles) | |
84 | |
85 // Extract new version to a final destination. | |
86 newFiles := makeStringSet() | |
87 destPath, err := deployInstance(root, inst, newFiles) | |
88 if err != nil { | |
89 return | |
90 } | |
91 | |
92 // Switch '_current' symlink to point to a new package instance. It is a | |
93 // point of no return. The function must not fail going forward. | |
94 mainSymlinkPath := packagePath(root, inst.PackageName(), currentSymlink) | |
95 err = ensureSymlink(mainSymlinkPath, inst.InstanceID()) | |
96 if err != nil { | |
97 ensureDirectoryGone(destPath) | |
98 return | |
99 } | |
100 | |
101 // Asynchronously remove previous version (best effort). | |
102 wg := sync.WaitGroup{} | |
103 defer wg.Wait() | |
104 if prevID != "" && prevID != inst.InstanceID() { | |
105 wg.Add(1) | |
106 go func() { | |
107 defer wg.Done() | |
108 ensureDirectoryGone(packagePath(root, inst.PackageName()
, prevID)) | |
109 }() | |
110 } | |
111 | |
112 log.Infof("Adjusting symlinks for %s", inst.PackageName()) | |
113 | |
114 // Make symlinks in the site directory for all new files. Reference a pa
ckage | |
115 // root via '_current' symlink (instead of direct destPath), to make | |
116 // subsequent updates 'more atomic' (since they'll need to switch only | |
117 // '_current' symlink to update _all_ files in the site root at once). | |
118 linkFilesToRoot(root, mainSymlinkPath, newFiles) | |
119 | |
120 // Delete symlinks to files no longer needed i.e. set(old) - set(new). | |
121 for relPath := range oldFiles.diff(newFiles) { | |
122 ensureFileGone(filepath.Join(root, relPath)) | |
123 } | |
124 | |
125 // Verify it's all right, read the manifest. | |
126 state, err = CheckDeployed(root, inst.PackageName()) | |
127 if err == nil && state.InstanceID != inst.InstanceID() { | |
128 err = fmt.Errorf("Other package instance (%s) was deployed concu
rrently", state.InstanceID) | |
129 } | |
130 if err == nil { | |
131 log.Infof("Successfully deployed %s:%s", inst.PackageName(), ins
t.InstanceID()) | |
132 } else { | |
133 log.Errorf("Failed to deploy %s:%s: %s", inst.PackageName(), ins
t.InstanceID(), err.Error()) | |
134 } | |
135 return | |
136 } | |
137 | |
138 // CheckDeployed checks whether a given package is deployed and returns | |
139 // information about it if it is. | |
140 func CheckDeployed(root string, pkg string) (state PackageState, err error) { | |
141 state, err = readPackageState(packagePath(root, pkg)) | |
142 if err != nil { | |
143 return | |
144 } | |
145 if state.PackageName != pkg { | |
146 err = fmt.Errorf("Package path and package name in the manifest
do not match") | |
147 } | |
148 return | |
149 } | |
150 | |
151 // FindDeployed returns a list of packages deployed to a site root. | |
152 func FindDeployed(root string) (out []PackageState, err error) { | |
153 root, err = filepath.Abs(filepath.Clean(root)) | |
154 if err != nil { | |
155 return | |
156 } | |
157 | |
158 // Directories with packages are direct children of .cipd/pkgs/. | |
159 pkgs := filepath.Join(root, filepath.FromSlash(packagesDir)) | |
160 infos, err := ioutil.ReadDir(pkgs) | |
161 if err != nil { | |
162 if os.IsNotExist(err) { | |
163 err = nil | |
164 return | |
165 } | |
166 return | |
167 } | |
168 | |
169 // Read the package name from the package manifest. Skip broken stuff. | |
170 found := map[string]PackageState{} | |
171 keys := []string{} | |
172 for _, info := range infos { | |
173 // Attempt to read the manifest. If it is there -> valid package
is found. | |
174 if info.IsDir() { | |
175 state, err := readPackageState(filepath.Join(pkgs, info.
Name())) | |
176 if err == nil { | |
177 // Ignore duplicate entries, they can appear if
someone messes with | |
178 // pkgs/* structure manually. | |
179 if _, ok := found[state.PackageName]; !ok { | |
180 keys = append(keys, state.PackageName) | |
181 found[state.PackageName] = state | |
182 } | |
183 } | |
184 } | |
185 } | |
186 | |
187 // Sort by package name. | |
188 sort.Strings(keys) | |
189 out = make([]PackageState, len(found)) | |
190 for i, k := range keys { | |
191 out[i] = found[k] | |
192 } | |
193 return | |
194 } | |
195 | |
196 // RemoveDeployed deletes a package given its name. | |
197 func RemoveDeployed(root string, packageName string) error { | |
198 root, err := filepath.Abs(filepath.Clean(root)) | |
199 if err != nil { | |
200 return err | |
201 } | |
202 log.Infof("Removing %s from %s", packageName, root) | |
203 | |
204 // Be paranoid. | |
205 err = ValidatePackageName(packageName) | |
206 if err != nil { | |
207 return err | |
208 } | |
209 | |
210 // Grab list of files in currently deployed package to unlink them from
root. | |
211 files := makeStringSet() | |
212 instanceID := findDeployedInstance(root, packageName, files) | |
213 | |
214 // If was installed, removed symlinks pointing to the package files. | |
215 if instanceID != "" { | |
216 for relPath := range files { | |
217 ensureFileGone(filepath.Join(root, relPath)) | |
218 } | |
219 } | |
220 | |
221 // Ensure all garbage is gone even if instanceID == "" was returned. | |
222 return ensureDirectoryGone(packagePath(root, packageName)) | |
223 } | |
224 | |
225 //////////////////////////////////////////////////////////////////////////////// | |
226 // Utility functions. | |
227 | |
228 // findDeployedInstance returns instanceID of a currently deployed package | |
229 // instance and finds all files in it (adding them to 'files' set). Returns "" | |
230 // if nothing is deployed. File paths in 'files' are relative to package root. | |
231 func findDeployedInstance(root string, pkg string, files stringSet) string { | |
232 state, err := CheckDeployed(root, pkg) | |
233 if err != nil { | |
234 return "" | |
235 } | |
236 scanPackageDir(packagePath(root, pkg, state.InstanceID), files) | |
237 return state.InstanceID | |
238 } | |
239 | |
240 // deployInstance atomically extracts a package instance to its final | |
241 // destination and returns a path to it. It writes a list of extracted files | |
242 // to 'files'. File paths in 'files' are relative to package root. | |
243 func deployInstance(root string, inst PackageInstance, files stringSet) (string,
error) { | |
244 // Extract new version to a final destination. ExtractPackageInstance kn
ows | |
245 // how to build full paths and how to atomically extract a package. No n
eed | |
246 // to delete garbage if it fails. | |
247 destPath := packagePath(root, inst.PackageName(), inst.InstanceID()) | |
248 err := ExtractInstance(inst, NewFileSystemDestination(destPath)) | |
249 if err != nil { | |
250 return "", err | |
251 } | |
252 // Enumerate files inside. Nuke it and fail if it's unreadable. | |
253 err = scanPackageDir(packagePath(root, inst.PackageName(), inst.Instance
ID()), files) | |
254 if err != nil { | |
255 ensureDirectoryGone(destPath) | |
256 return "", err | |
257 } | |
258 return destPath, err | |
259 } | |
260 | |
261 // linkFilesToRoot makes symlinks in root that point to files in packageRoot. | |
262 // All targets are specified by 'files' as paths relative to packageRoot. This | |
263 // function is best effort and thus doesn't return errors. | |
264 func linkFilesToRoot(root string, packageRoot string, files stringSet) { | |
265 for relPath := range files { | |
266 // E.g <root>/bin/tool. | |
267 symlinkAbs := filepath.Join(root, relPath) | |
268 // E.g. <root>/.cipd/pkgs/name/_current/bin/tool. | |
269 targetAbs := filepath.Join(packageRoot, relPath) | |
270 // E.g. ../.cipd/pkgs/name/_current/bin/tool. | |
271 targetRel, err := filepath.Rel(filepath.Dir(symlinkAbs), targetA
bs) | |
272 if err != nil { | |
273 log.Warnf("Can't get relative path from %s to %s", filep
ath.Dir(symlinkAbs), targetAbs) | |
274 continue | |
275 } | |
276 err = ensureSymlink(symlinkAbs, targetRel) | |
277 if err != nil { | |
278 log.Warnf("Failed to create symlink for %s", relPath) | |
279 continue | |
280 } | |
281 } | |
282 } | |
283 | |
284 // packagePath joins paths together to return absolute path to .cipd/pkgs sub pa
th. | |
285 func packagePath(root string, pkg string, rest ...string) string { | |
286 root, err := filepath.Abs(filepath.Clean(root)) | |
287 if err != nil { | |
288 panic(fmt.Sprintf("Can't get absolute path of '%s'", root)) | |
289 } | |
290 root = filepath.Join(root, filepath.FromSlash(packagesDir), packageNameD
igest(pkg)) | |
291 result := filepath.Join(append([]string{root}, rest...)...) | |
292 | |
293 // Be paranoid and check that everything is inside .cipd directory. | |
294 abs, err := filepath.Abs(result) | |
295 if err != nil { | |
296 panic(fmt.Sprintf("Can't get absolute path of '%s'", result)) | |
297 } | |
298 if !isSubpath(abs, root) { | |
299 panic(fmt.Sprintf("Wrong path %s outside of root %s", abs, root)
) | |
300 } | |
301 return result | |
302 } | |
303 | |
304 // packageNameDigest returns a filename to use for naming a package directory in | |
305 // the file system. Using package names as is can introduce problems on file | |
306 // systems with path length limits (on Windows in particular). Returns last two | |
307 // components of the package name + stripped SHA1 of the whole package name. | |
308 func packageNameDigest(pkg string) string { | |
309 // Be paranoid. | |
310 err := ValidatePackageName(pkg) | |
311 if err != nil { | |
312 panic(err.Error()) | |
313 } | |
314 | |
315 // Grab stripped SHA1 of the full package name. | |
316 h := sha1.New() | |
317 h.Write([]byte(pkg)) | |
318 hash := base64.URLEncoding.EncodeToString(h.Sum(nil))[:10] | |
319 | |
320 // Grab last <= 2 components of the package path. | |
321 chunks := strings.Split(pkg, "/") | |
322 if len(chunks) > 2 { | |
323 chunks = chunks[len(chunks)-2:] | |
324 } | |
325 | |
326 // Join together with '_' as separator. | |
327 chunks = append(chunks, hash) | |
328 return strings.Join(chunks, "_") | |
329 } | |
330 | |
331 // readPackageState reads package manifest of a deployed package instance and | |
332 // returns corresponding PackageState object. | |
333 func readPackageState(packageDir string) (state PackageState, err error) { | |
334 // Resolve _current symlink to a concrete instance ID. | |
335 current, err := os.Readlink(filepath.Join(packageDir, currentSymlink)) | |
336 if err != nil { | |
337 return | |
338 } | |
339 err = ValidateInstanceID(current) | |
340 if err != nil { | |
341 err = fmt.Errorf("Symlink target doesn't look like a valid insta
nce id") | |
342 return | |
343 } | |
344 // Read the manifest from the instance directory. | |
345 manifestPath := filepath.Join(packageDir, current, filepath.FromSlash(ma
nifestName)) | |
346 r, err := os.Open(manifestPath) | |
347 if err != nil { | |
348 return | |
349 } | |
350 defer r.Close() | |
351 manifest, err := readManifest(r) | |
352 if err != nil { | |
353 return | |
354 } | |
355 state = PackageState{ | |
356 PackageName: manifest.PackageName, | |
357 InstanceID: current, | |
358 } | |
359 return | |
360 } | |
361 | |
362 // ensureSymlink atomically creates a symlink pointing to a target. It will | |
363 // create full directory path if necessary. | |
364 func ensureSymlink(symlink string, target string) error { | |
365 // Already set? | |
366 existing, err := os.Readlink(symlink) | |
367 if err != nil && existing == target { | |
368 return nil | |
369 } | |
370 | |
371 // Make sure path exists. | |
372 err = os.MkdirAll(filepath.Dir(symlink), 0777) | |
373 if err != nil { | |
374 return err | |
375 } | |
376 | |
377 // Create a new symlink file, can't modify existing one. | |
378 temp := fmt.Sprintf("%s_%v", symlink, time.Now().UnixNano()) | |
379 err = os.Symlink(target, temp) | |
380 if err != nil { | |
381 return err | |
382 } | |
383 | |
384 // Atomically replace current symlink with a new one. | |
385 err = os.Rename(temp, symlink) | |
386 if err != nil { | |
387 os.Remove(temp) | |
388 return err | |
389 } | |
390 | |
391 return nil | |
392 } | |
393 | |
394 // scanPackageDir finds a set of regular files (and symlinks) in a package | |
395 // instance directory to be symlinked into the site root. Adds paths relative | |
396 // to 'root' to 'out'. Skips package service directories (.cipdpkg and .cipd) | |
397 // since they contain package deployer gut files, not something that needs | |
398 // to be deployed. | |
399 func scanPackageDir(root string, out stringSet) error { | |
400 return filepath.Walk(root, func(path string, info os.FileInfo, err error
) error { | |
401 if err != nil { | |
402 return err | |
403 } | |
404 rel, err := filepath.Rel(root, path) | |
405 if err != nil { | |
406 return err | |
407 } | |
408 if rel == packageServiceDir || rel == siteServiceDir { | |
409 return filepath.SkipDir | |
410 } | |
411 if info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { | |
412 out.add(rel) | |
413 } | |
414 return nil | |
415 }) | |
416 } | |
417 | |
418 // ensureDirectoryGone removes the directory as instantly as possible by | |
419 // renaming it first and only then recursively deleting. | |
420 func ensureDirectoryGone(path string) error { | |
421 temp := fmt.Sprintf("%s_%v", path, time.Now().UnixNano()) | |
422 err := os.Rename(path, temp) | |
423 if err != nil { | |
424 if !os.IsNotExist(err) { | |
425 log.Warnf("Failed to rename directory %s: %v", path, err
) | |
426 return err | |
427 } | |
428 return nil | |
429 } | |
430 err = os.RemoveAll(temp) | |
431 if err != nil { | |
432 log.Warnf("Failed to remove directory %s: %v", temp, err) | |
433 return err | |
434 } | |
435 return nil | |
436 } | |
437 | |
438 // ensureFileGone removes file, logging the errors (if any). | |
439 func ensureFileGone(path string) error { | |
440 err := os.Remove(path) | |
441 if err != nil && !os.IsNotExist(err) { | |
442 log.Warnf("Failed to remove %s", path) | |
443 return err | |
444 } | |
445 return nil | |
446 } | |
447 | |
448 //////////////////////////////////////////////////////////////////////////////// | |
449 // Simple stringSet implementation for keeping a set of filenames. | |
450 | |
451 type stringSet map[string]struct{} | |
452 | |
453 func makeStringSet() stringSet { | |
454 return make(stringSet) | |
455 } | |
456 | |
457 // add adds an element to the string set. | |
458 func (a stringSet) add(elem string) { | |
459 a[elem] = struct{}{} | |
460 } | |
461 | |
462 // diff returns set(a) - set(b). | |
463 func (a stringSet) diff(b stringSet) stringSet { | |
464 out := makeStringSet() | |
465 for elem := range a { | |
466 if _, ok := b[elem]; !ok { | |
467 out.add(elem) | |
468 } | |
469 } | |
470 return out | |
471 } | |
OLD | NEW |