| // Copyright 2018 The LUCI Authors. |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package ensure |
| |
| import ( |
| "bufio" |
| "fmt" |
| "io" |
| "sort" |
| "strings" |
| |
| "go.chromium.org/luci/common/errors" |
| "go.chromium.org/luci/common/iotools" |
| |
| "go.chromium.org/luci/cipd/common" |
| ) |
| |
| // VersionsFile contains a mapping "(package name, version) -> instance ID" used |
| // to resolve versions (instead of backend calls) for ensure files that have |
| // $ResolvedVersions directive. |
| // |
| // In serialized form it is represented as a set of triples separated by one |
| // or more new lines (there must be no new lines inside the triple): |
| // |
| // """ |
| // # Comments are allowed, they are skipped (not even considered as '\n'). |
| // <package name> |
| // <version> |
| // <resolved instance ID> |
| // |
| // <package name> |
| // <version> |
| // <resolved instance ID> |
| // """ |
| // |
| // Leading and trailing whitespace on a line is ignored. |
| // |
| // In the canonical serialization triples are ordered by (package, version). |
| // |
| // VersionsFile is safe for read-only concurrent use. Concurrent modifications |
| // should be protected by a lock. This is just a map in disguise. |
| type VersionsFile map[unresolvedVer]string |
| |
| type unresolvedVer struct { |
| pkg string |
| ver string |
| } |
| |
| // AddVersion adds (or overrides) an instance ID mapped to the given version. |
| // |
| // Returns an error if any of the arguments is invalid. |
| // |
| // If 'ver' is already an instance ID, just checks if it is equal to 'iid' and |
| // silently doesn't modify the map. |
| func (v VersionsFile) AddVersion(pkg, ver, iid string) error { |
| if err := common.ValidatePackageName(pkg); err != nil { |
| return err |
| } |
| if err := common.ValidateInstanceVersion(ver); err != nil { |
| return err |
| } |
| if err := common.ValidateInstanceID(iid, common.AnyHash); err != nil { |
| return err |
| } |
| |
| if common.ValidateInstanceID(ver, common.AnyHash) == nil { |
| if ver != iid { |
| return errors.Reason( |
| "version given as instance ID (%q) should resolve into that ID, not into %q", |
| ver, iid).Err() |
| } |
| return nil |
| } |
| |
| v[unresolvedVer{pkg, ver}] = iid |
| return nil |
| } |
| |
| // ResolveVersion returns a pin matching the given version or an error if such |
| // version is not in the map. |
| // |
| // If 'ver' is already an instance ID, returns it right away. |
| func (v VersionsFile) ResolveVersion(pkg, ver string) (common.Pin, error) { |
| if common.ValidateInstanceID(ver, common.AnyHash) == nil { |
| return common.Pin{PackageName: pkg, InstanceID: ver}, nil |
| } |
| if iid, ok := v[unresolvedVer{pkg, ver}]; ok { |
| return common.Pin{PackageName: pkg, InstanceID: iid}, nil |
| } |
| return common.Pin{}, errors.Reason("not in the versions file").Err() |
| } |
| |
| // Equal returns true if version files have same entries. |
| func (v VersionsFile) Equal(a VersionsFile) bool { |
| if len(v) != len(a) { |
| return false |
| } |
| for ver, iid := range v { |
| if a[ver] != iid { |
| return false |
| } |
| } |
| return true |
| } |
| |
| // Serialize writes the VersionsFile to an io.Writer in canonical order. |
| func (v VersionsFile) Serialize(w io.Writer) error { |
| keys := make([]unresolvedVer, 0, len(v)) |
| for k := range v { |
| keys = append(keys, k) |
| } |
| sort.Slice(keys, func(i, j int) bool { |
| l, r := keys[i], keys[j] |
| if l.pkg != r.pkg { |
| return l.pkg < r.pkg |
| } |
| return l.ver < r.ver |
| }) |
| |
| _, err := iotools.WriteTracker(w, func(w io.Writer) error { |
| fmt.Fprintf(w, "# This file is auto-generated by 'cipd ensure-file-resolve'.\n") |
| fmt.Fprintf(w, "# Do not modify manually. All changes will be overwritten.\n") |
| for _, key := range keys { |
| fmt.Fprintf(w, "\n%s\n\t%s\n\t%s\n", key.pkg, key.ver, v[key]) |
| } |
| return nil |
| }) |
| return err |
| } |
| |
| // ParseVersionsFile parses previously serialized versions file. |
| func ParseVersionsFile(r io.Reader) (VersionsFile, error) { |
| res := VersionsFile{} |
| |
| lineNo := 0 |
| makeError := func(fmtStr string, args ...interface{}) error { |
| args = append([]interface{}{lineNo}, args...) |
| return fmt.Errorf("failed to parse versions file (line %d): "+fmtStr, args...) |
| } |
| |
| const ( |
| stWaitingPkg = "a package name" |
| stWaitingVer = "a package version" |
| stWaitingIID = "an instance ID" |
| stWaitingNL = "a new line" |
| ) |
| state := stWaitingPkg |
| pkg := "" |
| ver := "" |
| iid := "" |
| |
| scanner := bufio.NewScanner(r) |
| for scanner.Scan() { |
| lineNo++ |
| |
| line := strings.TrimSpace(scanner.Text()) |
| |
| // Comments are grammatically insignificant (unlike empty lines), so skip |
| // the completely. |
| if len(line) > 0 && line[0] == '#' { |
| continue |
| } |
| |
| switch state { |
| case stWaitingPkg: |
| if line == "" { |
| continue // can have more than one empty line between triples |
| } |
| pkg = line |
| if err := common.ValidatePackageName(pkg); err != nil { |
| return nil, makeError("%s", err) |
| } |
| state = stWaitingVer |
| |
| case stWaitingVer: |
| if line == "" { |
| return nil, makeError("expecting a version name, not a new line") |
| } |
| ver = line |
| if err := common.ValidateInstanceVersion(ver); err != nil { |
| return nil, makeError("%s", err) |
| } |
| state = stWaitingIID |
| |
| case stWaitingIID: |
| if line == "" { |
| return nil, makeError("expecting an instance ID, not a new line") |
| } |
| iid = line |
| if err := common.ValidateInstanceID(iid, common.AnyHash); err != nil { |
| return nil, makeError("%s", err) |
| } |
| if err := res.AddVersion(pkg, ver, iid); err != nil { |
| panic(err) // impossible, everything has been validated already |
| } |
| pkg, ver, iid = "", "", "" |
| state = stWaitingNL |
| |
| case stWaitingNL: |
| if line == "" { |
| state = stWaitingPkg |
| continue |
| } |
| return nil, makeError("expecting an empty line between each version definition triple") |
| } |
| } |
| |
| if state != stWaitingPkg && state != stWaitingNL { |
| return nil, makeError("unexpected EOF, expecting %s", state) |
| } |
| |
| if err := scanner.Err(); err != nil { |
| return nil, err |
| } |
| return res, nil |
| } |