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

Side by Side Diff: go/src/infra/tools/cipd/ensure.go

Issue 1129043003: cipd: Refactor client to make it more readable. (Closed) Base URL: https://chromium.googlesource.com/infra/infra.git@master
Patch Set: Created 5 years, 7 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 | « go/src/infra/tools/cipd/doc.go ('k') | go/src/infra/tools/cipd/ensure_test.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 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 }
OLDNEW
« no previous file with comments | « go/src/infra/tools/cipd/doc.go ('k') | go/src/infra/tools/cipd/ensure_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698