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 |