Index: go/src/infra/tools/cipd/deployer.go |
diff --git a/go/src/infra/tools/cipd/deployer.go b/go/src/infra/tools/cipd/deployer.go |
deleted file mode 100644 |
index 68bace5278635e72b9970c813a849ddf1737f2ad..0000000000000000000000000000000000000000 |
--- a/go/src/infra/tools/cipd/deployer.go |
+++ /dev/null |
@@ -1,471 +0,0 @@ |
-// Copyright 2014 The Chromium Authors. All rights reserved. |
-// Use of this source code is governed by a BSD-style license that can be |
-// found in the LICENSE file. |
- |
-package cipd |
- |
-import ( |
- "crypto/sha1" |
- "encoding/base64" |
- "fmt" |
- "io/ioutil" |
- "os" |
- "path/filepath" |
- "sort" |
- "strings" |
- "sync" |
- "time" |
-) |
- |
-// TODO(vadimsh): Make it work on Windows, verify it works on Mac. |
- |
-// TODO(vadimsh): How to handle path conflicts between two packages? Currently |
-// the last one installed wins. |
- |
-// File system layout of a site directory <root>: |
-// <root>/.cipd/pkgs/ |
-// <package name digest>/ |
-// _current -> symlink to fea3ab83440e9dfb813785e16d4101f331ed44f4 |
-// fea3ab83440e9dfb813785e16d4101f331ed44f4/ |
-// bin/ |
-// tool |
-// ... |
-// ... |
-// bin/ |
-// tool -> symlink to ../.cipd/pkgs/<package name digest>/_current/bin/tool |
-// ... |
-// |
-// Where <package name digest> is derived from a package name. It doesn't have |
-// to be reversible though, since the package name is still stored in the |
-// installed package manifest and can be read from there. |
-// |
-// Some efforts are made to make sure that during the deployment a window of |
-// inconsistency in the file system is as small as possible. |
- |
-// Subdirectory of site root to extract packages to. |
-const packagesDir = siteServiceDir + "/pkgs" |
- |
-// Name of a symlink that points to latest deployed version. |
-const currentSymlink = "_current" |
- |
-// PackageState contains information about single deployed package. |
-type PackageState struct { |
- // PackageName identifies the package. |
- PackageName string |
- // InstanceID is ID of the installed package instance (SHA1 of package contents). |
- InstanceID string |
-} |
- |
-// DeployInstance installs a specific instance of a package (identified by |
-// InstanceID()) into a site root directory. It unpacks the package into |
-// <root>/.cipd/pkgs/*, and rearranges symlinks to point to unpacked files. |
-// It tries to make it as "atomic" as possibly. |
-func DeployInstance(root string, inst PackageInstance) (state PackageState, err error) { |
- root, err = filepath.Abs(filepath.Clean(root)) |
- if err != nil { |
- return |
- } |
- log.Infof("Deploying %s:%s into %s", inst.PackageName(), inst.InstanceID(), root) |
- |
- // Be paranoid. |
- err = ValidatePackageName(inst.PackageName()) |
- if err != nil { |
- return |
- } |
- err = ValidateInstanceID(inst.InstanceID()) |
- if err != nil { |
- return |
- } |
- |
- // Remember currently deployed version (to remove it later). Do not freak out |
- // if it's not there (prevID is "" in that case). |
- oldFiles := makeStringSet() |
- prevID := findDeployedInstance(root, inst.PackageName(), oldFiles) |
- |
- // Extract new version to a final destination. |
- newFiles := makeStringSet() |
- destPath, err := deployInstance(root, inst, newFiles) |
- if err != nil { |
- return |
- } |
- |
- // Switch '_current' symlink to point to a new package instance. It is a |
- // point of no return. The function must not fail going forward. |
- mainSymlinkPath := packagePath(root, inst.PackageName(), currentSymlink) |
- err = ensureSymlink(mainSymlinkPath, inst.InstanceID()) |
- if err != nil { |
- ensureDirectoryGone(destPath) |
- return |
- } |
- |
- // Asynchronously remove previous version (best effort). |
- wg := sync.WaitGroup{} |
- defer wg.Wait() |
- if prevID != "" && prevID != inst.InstanceID() { |
- wg.Add(1) |
- go func() { |
- defer wg.Done() |
- ensureDirectoryGone(packagePath(root, inst.PackageName(), prevID)) |
- }() |
- } |
- |
- log.Infof("Adjusting symlinks for %s", inst.PackageName()) |
- |
- // Make symlinks in the site directory for all new files. Reference a package |
- // root via '_current' symlink (instead of direct destPath), to make |
- // subsequent updates 'more atomic' (since they'll need to switch only |
- // '_current' symlink to update _all_ files in the site root at once). |
- linkFilesToRoot(root, mainSymlinkPath, newFiles) |
- |
- // Delete symlinks to files no longer needed i.e. set(old) - set(new). |
- for relPath := range oldFiles.diff(newFiles) { |
- ensureFileGone(filepath.Join(root, relPath)) |
- } |
- |
- // Verify it's all right, read the manifest. |
- state, err = CheckDeployed(root, inst.PackageName()) |
- if err == nil && state.InstanceID != inst.InstanceID() { |
- err = fmt.Errorf("Other package instance (%s) was deployed concurrently", state.InstanceID) |
- } |
- if err == nil { |
- log.Infof("Successfully deployed %s:%s", inst.PackageName(), inst.InstanceID()) |
- } else { |
- log.Errorf("Failed to deploy %s:%s: %s", inst.PackageName(), inst.InstanceID(), err.Error()) |
- } |
- return |
-} |
- |
-// CheckDeployed checks whether a given package is deployed and returns |
-// information about it if it is. |
-func CheckDeployed(root string, pkg string) (state PackageState, err error) { |
- state, err = readPackageState(packagePath(root, pkg)) |
- if err != nil { |
- return |
- } |
- if state.PackageName != pkg { |
- err = fmt.Errorf("Package path and package name in the manifest do not match") |
- } |
- return |
-} |
- |
-// FindDeployed returns a list of packages deployed to a site root. |
-func FindDeployed(root string) (out []PackageState, err error) { |
- root, err = filepath.Abs(filepath.Clean(root)) |
- if err != nil { |
- return |
- } |
- |
- // Directories with packages are direct children of .cipd/pkgs/. |
- pkgs := filepath.Join(root, filepath.FromSlash(packagesDir)) |
- infos, err := ioutil.ReadDir(pkgs) |
- if err != nil { |
- if os.IsNotExist(err) { |
- err = nil |
- return |
- } |
- return |
- } |
- |
- // Read the package name from the package manifest. Skip broken stuff. |
- found := map[string]PackageState{} |
- keys := []string{} |
- for _, info := range infos { |
- // Attempt to read the manifest. If it is there -> valid package is found. |
- if info.IsDir() { |
- state, err := readPackageState(filepath.Join(pkgs, info.Name())) |
- if err == nil { |
- // Ignore duplicate entries, they can appear if someone messes with |
- // pkgs/* structure manually. |
- if _, ok := found[state.PackageName]; !ok { |
- keys = append(keys, state.PackageName) |
- found[state.PackageName] = state |
- } |
- } |
- } |
- } |
- |
- // Sort by package name. |
- sort.Strings(keys) |
- out = make([]PackageState, len(found)) |
- for i, k := range keys { |
- out[i] = found[k] |
- } |
- return |
-} |
- |
-// RemoveDeployed deletes a package given its name. |
-func RemoveDeployed(root string, packageName string) error { |
- root, err := filepath.Abs(filepath.Clean(root)) |
- if err != nil { |
- return err |
- } |
- log.Infof("Removing %s from %s", packageName, root) |
- |
- // Be paranoid. |
- err = ValidatePackageName(packageName) |
- if err != nil { |
- return err |
- } |
- |
- // Grab list of files in currently deployed package to unlink them from root. |
- files := makeStringSet() |
- instanceID := findDeployedInstance(root, packageName, files) |
- |
- // If was installed, removed symlinks pointing to the package files. |
- if instanceID != "" { |
- for relPath := range files { |
- ensureFileGone(filepath.Join(root, relPath)) |
- } |
- } |
- |
- // Ensure all garbage is gone even if instanceID == "" was returned. |
- return ensureDirectoryGone(packagePath(root, packageName)) |
-} |
- |
-//////////////////////////////////////////////////////////////////////////////// |
-// Utility functions. |
- |
-// findDeployedInstance returns instanceID of a currently deployed package |
-// instance and finds all files in it (adding them to 'files' set). Returns "" |
-// if nothing is deployed. File paths in 'files' are relative to package root. |
-func findDeployedInstance(root string, pkg string, files stringSet) string { |
- state, err := CheckDeployed(root, pkg) |
- if err != nil { |
- return "" |
- } |
- scanPackageDir(packagePath(root, pkg, state.InstanceID), files) |
- return state.InstanceID |
-} |
- |
-// deployInstance atomically extracts a package instance to its final |
-// destination and returns a path to it. It writes a list of extracted files |
-// to 'files'. File paths in 'files' are relative to package root. |
-func deployInstance(root string, inst PackageInstance, files stringSet) (string, error) { |
- // Extract new version to a final destination. ExtractPackageInstance knows |
- // how to build full paths and how to atomically extract a package. No need |
- // to delete garbage if it fails. |
- destPath := packagePath(root, inst.PackageName(), inst.InstanceID()) |
- err := ExtractInstance(inst, NewFileSystemDestination(destPath)) |
- if err != nil { |
- return "", err |
- } |
- // Enumerate files inside. Nuke it and fail if it's unreadable. |
- err = scanPackageDir(packagePath(root, inst.PackageName(), inst.InstanceID()), files) |
- if err != nil { |
- ensureDirectoryGone(destPath) |
- return "", err |
- } |
- return destPath, err |
-} |
- |
-// linkFilesToRoot makes symlinks in root that point to files in packageRoot. |
-// All targets are specified by 'files' as paths relative to packageRoot. This |
-// function is best effort and thus doesn't return errors. |
-func linkFilesToRoot(root string, packageRoot string, files stringSet) { |
- for relPath := range files { |
- // E.g <root>/bin/tool. |
- symlinkAbs := filepath.Join(root, relPath) |
- // E.g. <root>/.cipd/pkgs/name/_current/bin/tool. |
- targetAbs := filepath.Join(packageRoot, relPath) |
- // E.g. ../.cipd/pkgs/name/_current/bin/tool. |
- targetRel, err := filepath.Rel(filepath.Dir(symlinkAbs), targetAbs) |
- if err != nil { |
- log.Warnf("Can't get relative path from %s to %s", filepath.Dir(symlinkAbs), targetAbs) |
- continue |
- } |
- err = ensureSymlink(symlinkAbs, targetRel) |
- if err != nil { |
- log.Warnf("Failed to create symlink for %s", relPath) |
- continue |
- } |
- } |
-} |
- |
-// packagePath joins paths together to return absolute path to .cipd/pkgs sub path. |
-func packagePath(root string, pkg string, rest ...string) string { |
- root, err := filepath.Abs(filepath.Clean(root)) |
- if err != nil { |
- panic(fmt.Sprintf("Can't get absolute path of '%s'", root)) |
- } |
- root = filepath.Join(root, filepath.FromSlash(packagesDir), packageNameDigest(pkg)) |
- result := filepath.Join(append([]string{root}, rest...)...) |
- |
- // Be paranoid and check that everything is inside .cipd directory. |
- abs, err := filepath.Abs(result) |
- if err != nil { |
- panic(fmt.Sprintf("Can't get absolute path of '%s'", result)) |
- } |
- if !isSubpath(abs, root) { |
- panic(fmt.Sprintf("Wrong path %s outside of root %s", abs, root)) |
- } |
- return result |
-} |
- |
-// packageNameDigest returns a filename to use for naming a package directory in |
-// the file system. Using package names as is can introduce problems on file |
-// systems with path length limits (on Windows in particular). Returns last two |
-// components of the package name + stripped SHA1 of the whole package name. |
-func packageNameDigest(pkg string) string { |
- // Be paranoid. |
- err := ValidatePackageName(pkg) |
- if err != nil { |
- panic(err.Error()) |
- } |
- |
- // Grab stripped SHA1 of the full package name. |
- h := sha1.New() |
- h.Write([]byte(pkg)) |
- hash := base64.URLEncoding.EncodeToString(h.Sum(nil))[:10] |
- |
- // Grab last <= 2 components of the package path. |
- chunks := strings.Split(pkg, "/") |
- if len(chunks) > 2 { |
- chunks = chunks[len(chunks)-2:] |
- } |
- |
- // Join together with '_' as separator. |
- chunks = append(chunks, hash) |
- return strings.Join(chunks, "_") |
-} |
- |
-// readPackageState reads package manifest of a deployed package instance and |
-// returns corresponding PackageState object. |
-func readPackageState(packageDir string) (state PackageState, err error) { |
- // Resolve _current symlink to a concrete instance ID. |
- current, err := os.Readlink(filepath.Join(packageDir, currentSymlink)) |
- if err != nil { |
- return |
- } |
- err = ValidateInstanceID(current) |
- if err != nil { |
- err = fmt.Errorf("Symlink target doesn't look like a valid instance id") |
- return |
- } |
- // Read the manifest from the instance directory. |
- manifestPath := filepath.Join(packageDir, current, filepath.FromSlash(manifestName)) |
- r, err := os.Open(manifestPath) |
- if err != nil { |
- return |
- } |
- defer r.Close() |
- manifest, err := readManifest(r) |
- if err != nil { |
- return |
- } |
- state = PackageState{ |
- PackageName: manifest.PackageName, |
- InstanceID: current, |
- } |
- return |
-} |
- |
-// ensureSymlink atomically creates a symlink pointing to a target. It will |
-// create full directory path if necessary. |
-func ensureSymlink(symlink string, target string) error { |
- // Already set? |
- existing, err := os.Readlink(symlink) |
- if err != nil && existing == target { |
- return nil |
- } |
- |
- // Make sure path exists. |
- err = os.MkdirAll(filepath.Dir(symlink), 0777) |
- if err != nil { |
- return err |
- } |
- |
- // Create a new symlink file, can't modify existing one. |
- temp := fmt.Sprintf("%s_%v", symlink, time.Now().UnixNano()) |
- err = os.Symlink(target, temp) |
- if err != nil { |
- return err |
- } |
- |
- // Atomically replace current symlink with a new one. |
- err = os.Rename(temp, symlink) |
- if err != nil { |
- os.Remove(temp) |
- return err |
- } |
- |
- return nil |
-} |
- |
-// scanPackageDir finds a set of regular files (and symlinks) in a package |
-// instance directory to be symlinked into the site root. Adds paths relative |
-// to 'root' to 'out'. Skips package service directories (.cipdpkg and .cipd) |
-// since they contain package deployer gut files, not something that needs |
-// to be deployed. |
-func scanPackageDir(root string, out stringSet) error { |
- return filepath.Walk(root, func(path string, info os.FileInfo, err error) error { |
- if err != nil { |
- return err |
- } |
- rel, err := filepath.Rel(root, path) |
- if err != nil { |
- return err |
- } |
- if rel == packageServiceDir || rel == siteServiceDir { |
- return filepath.SkipDir |
- } |
- if info.Mode().IsRegular() || info.Mode()&os.ModeSymlink != 0 { |
- out.add(rel) |
- } |
- return nil |
- }) |
-} |
- |
-// ensureDirectoryGone removes the directory as instantly as possible by |
-// renaming it first and only then recursively deleting. |
-func ensureDirectoryGone(path string) error { |
- temp := fmt.Sprintf("%s_%v", path, time.Now().UnixNano()) |
- err := os.Rename(path, temp) |
- if err != nil { |
- if !os.IsNotExist(err) { |
- log.Warnf("Failed to rename directory %s: %v", path, err) |
- return err |
- } |
- return nil |
- } |
- err = os.RemoveAll(temp) |
- if err != nil { |
- log.Warnf("Failed to remove directory %s: %v", temp, err) |
- return err |
- } |
- return nil |
-} |
- |
-// ensureFileGone removes file, logging the errors (if any). |
-func ensureFileGone(path string) error { |
- err := os.Remove(path) |
- if err != nil && !os.IsNotExist(err) { |
- log.Warnf("Failed to remove %s", path) |
- return err |
- } |
- return nil |
-} |
- |
-//////////////////////////////////////////////////////////////////////////////// |
-// Simple stringSet implementation for keeping a set of filenames. |
- |
-type stringSet map[string]struct{} |
- |
-func makeStringSet() stringSet { |
- return make(stringSet) |
-} |
- |
-// add adds an element to the string set. |
-func (a stringSet) add(elem string) { |
- a[elem] = struct{}{} |
-} |
- |
-// diff returns set(a) - set(b). |
-func (a stringSet) diff(b stringSet) stringSet { |
- out := makeStringSet() |
- for elem := range a { |
- if _, ok := b[elem]; !ok { |
- out.add(elem) |
- } |
- } |
- return out |
-} |