| 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 "bufio" | |
| 9 "fmt" | |
| 10 "io" | |
| 11 "net/http" | |
| 12 "os" | |
| 13 "path/filepath" | |
| 14 "strings" | |
| 15 | |
| 16 "infra/libs/logging" | |
| 17 ) | |
| 18 | |
| 19 // ParseDesiredState parses text file that describes what should be installed | |
| 20 // by EnsurePackages function. It is a text file where each line has a form: | |
| 21 // <package name> <desired instance ID> | |
| 22 // Whitespaces are ignored. Lines that start with '#' are ignored. | |
| 23 func ParseDesiredState(r io.Reader) ([]PackageState, error) { | |
| 24 lineNo := 0 | |
| 25 makeError := func(msg string) error { | |
| 26 return fmt.Errorf("Failed to parse desired state (line %d): %s",
lineNo, msg) | |
| 27 } | |
| 28 | |
| 29 out := []PackageState{} | |
| 30 scanner := bufio.NewScanner(r) | |
| 31 for scanner.Scan() { | |
| 32 lineNo++ | |
| 33 | |
| 34 // Split each line into words, ignore white space. | |
| 35 tokens := []string{} | |
| 36 for _, chunk := range strings.Split(scanner.Text(), " ") { | |
| 37 chunk = strings.TrimSpace(chunk) | |
| 38 if chunk != "" { | |
| 39 tokens = append(tokens, chunk) | |
| 40 } | |
| 41 } | |
| 42 | |
| 43 // Skip empty lines or lines starting with '#'. | |
| 44 if len(tokens) == 0 || tokens[0][0] == '#' { | |
| 45 continue | |
| 46 } | |
| 47 | |
| 48 // Each line has a format "<package name> <instance id>". | |
| 49 if len(tokens) != 2 { | |
| 50 return nil, makeError("expecting '<package name> <instan
ce id>' line") | |
| 51 } | |
| 52 err := ValidatePackageName(tokens[0]) | |
| 53 if err != nil { | |
| 54 return nil, makeError(err.Error()) | |
| 55 } | |
| 56 err = ValidateInstanceID(tokens[1]) | |
| 57 if err != nil { | |
| 58 return nil, makeError(err.Error()) | |
| 59 } | |
| 60 | |
| 61 // Good enough. | |
| 62 out = append(out, PackageState{ | |
| 63 PackageName: tokens[0], | |
| 64 InstanceID: tokens[1], | |
| 65 }) | |
| 66 } | |
| 67 | |
| 68 return out, nil | |
| 69 } | |
| 70 | |
| 71 // EnsurePackagesOptions contains parameters for EnsurePackages calls. | |
| 72 type EnsurePackagesOptions struct { | |
| 73 // ServiceURL is root URL of the backend service, or "" to use default s
ervice. | |
| 74 ServiceURL string | |
| 75 // ClientFactory knows how to make authenticated http.Client when it is
needed. Called lazily. | |
| 76 ClientFactory func() (*http.Client, error) | |
| 77 // Log is a logger to use for logs, default is logging.DefaultLogger. | |
| 78 Log logging.Logger | |
| 79 | |
| 80 // Root is a site root directory to modify. Will be created if missing. | |
| 81 Root string | |
| 82 // Packages describes the desired state of the site root directory. | |
| 83 Packages []PackageState | |
| 84 } | |
| 85 | |
| 86 // EnsurePackages is high level interface for installing, removing and updating | |
| 87 // of packages inside some installation site root. Given a description of | |
| 88 // what packages (and versions) should be installed it will do all necessary | |
| 89 // actions to bring the state of the site root to desired one. | |
| 90 func EnsurePackages(opts EnsurePackagesOptions) error { | |
| 91 // Make sure a package is specified only once. | |
| 92 seen := make(map[string]bool, len(opts.Packages)) | |
| 93 for _, p := range opts.Packages { | |
| 94 if seen[p.PackageName] { | |
| 95 return fmt.Errorf("Package %s is specified twice", p.Pac
kageName) | |
| 96 } | |
| 97 seen[p.PackageName] = true | |
| 98 } | |
| 99 | |
| 100 // Fill in default options. | |
| 101 if opts.ServiceURL == "" { | |
| 102 opts.ServiceURL = DefaultServiceURL() | |
| 103 } | |
| 104 if opts.Log == nil { | |
| 105 opts.Log = logging.DefaultLogger | |
| 106 } | |
| 107 log := opts.Log | |
| 108 | |
| 109 // Ensure site root is a directory (or missing). | |
| 110 root, err := filepath.Abs(filepath.Clean(opts.Root)) | |
| 111 if err != nil { | |
| 112 return err | |
| 113 } | |
| 114 stat, err := os.Stat(root) | |
| 115 if err == nil && !stat.IsDir() { | |
| 116 return fmt.Errorf("Path %s is not a directory", opts.Root) | |
| 117 } | |
| 118 if err != nil && !os.IsNotExist(err) { | |
| 119 return err | |
| 120 } | |
| 121 rootExists := (err == nil) | |
| 122 | |
| 123 // Enumerate existing packages (only if root already exists). | |
| 124 existing := []PackageState{} | |
| 125 if rootExists { | |
| 126 existing, err = FindDeployed(root) | |
| 127 if err != nil { | |
| 128 log.Errorf("Failed to enumerate installed packages: %s",
err) | |
| 129 return err | |
| 130 } | |
| 131 } | |
| 132 | |
| 133 // Figure out what needs to be updated and deleted, log it. | |
| 134 toDeploy, toDelete := buildActionPlan(opts.Packages, existing) | |
| 135 if len(toDeploy) == 0 && len(toDelete) == 0 { | |
| 136 log.Infof("Everything is up-to-date.") | |
| 137 return nil | |
| 138 } | |
| 139 if len(toDeploy) != 0 { | |
| 140 log.Infof("Packages to be installed:") | |
| 141 for _, state := range toDeploy { | |
| 142 log.Infof(" %s:%s", state.PackageName, state.InstanceID
) | |
| 143 } | |
| 144 } | |
| 145 if len(toDelete) != 0 { | |
| 146 log.Infof("Packages to be removed:") | |
| 147 for _, state := range toDelete { | |
| 148 log.Infof(" %s", state.PackageName) | |
| 149 } | |
| 150 } | |
| 151 | |
| 152 // Create the site root directory before installing anything there. | |
| 153 if len(toDeploy) != 0 && !rootExists { | |
| 154 err = os.MkdirAll(root, 0777) | |
| 155 if err != nil { | |
| 156 return err | |
| 157 } | |
| 158 } | |
| 159 | |
| 160 // Updating packages requires interaction with the server, create the cl
ient. | |
| 161 client := http.DefaultClient | |
| 162 if len(toDeploy) != 0 && opts.ClientFactory != nil { | |
| 163 client, err = opts.ClientFactory() | |
| 164 if err != nil { | |
| 165 return err | |
| 166 } | |
| 167 } | |
| 168 | |
| 169 // Remove all unneeded stuff. | |
| 170 errors := []error{} | |
| 171 for _, state := range toDelete { | |
| 172 err = RemoveDeployed(root, state.PackageName) | |
| 173 if err != nil { | |
| 174 log.Errorf("Failed to remove %s - %s", state.PackageName
, err) | |
| 175 errors = append(errors, err) | |
| 176 } | |
| 177 } | |
| 178 | |
| 179 // Install all new stuff. | |
| 180 for _, state := range toDeploy { | |
| 181 err = FetchAndDeployInstance(root, FetchInstanceOptions{ | |
| 182 ServiceURL: opts.ServiceURL, | |
| 183 Client: client, | |
| 184 Log: opts.Log, | |
| 185 PackageName: state.PackageName, | |
| 186 InstanceID: state.InstanceID, | |
| 187 }) | |
| 188 if err != nil { | |
| 189 log.Errorf("Failed to install %s:%s - %s", state.Package
Name, state.InstanceID, err) | |
| 190 errors = append(errors, err) | |
| 191 } | |
| 192 } | |
| 193 | |
| 194 if len(errors) == 0 { | |
| 195 log.Infof("All changes applied.") | |
| 196 return nil | |
| 197 } | |
| 198 return fmt.Errorf("Some actions failed: %v", errors) | |
| 199 } | |
| 200 | |
| 201 func buildActionPlan(desired []PackageState, existing []PackageState) (toDeploy
[]PackageState, toDelete []PackageState) { | |
| 202 // Figure out what needs to be installed or updated. | |
| 203 for _, d := range desired { | |
| 204 alreadyGood := false | |
| 205 for _, e := range existing { | |
| 206 if e.PackageName == d.PackageName { | |
| 207 alreadyGood = e.InstanceID == d.InstanceID | |
| 208 break | |
| 209 } | |
| 210 } | |
| 211 if !alreadyGood { | |
| 212 toDeploy = append(toDeploy, d) | |
| 213 } | |
| 214 } | |
| 215 | |
| 216 // Figure out what needs to be removed. | |
| 217 for _, e := range existing { | |
| 218 keep := false | |
| 219 for _, d := range desired { | |
| 220 if e.PackageName == d.PackageName { | |
| 221 keep = true | |
| 222 break | |
| 223 } | |
| 224 } | |
| 225 if !keep { | |
| 226 toDelete = append(toDelete, e) | |
| 227 } | |
| 228 } | |
| 229 | |
| 230 return | |
| 231 } | |
| OLD | NEW |