Chromium Code Reviews| Index: vpython/cipd/cipd.go |
| diff --git a/vpython/cipd/cipd.go b/vpython/cipd/cipd.go |
| new file mode 100644 |
| index 0000000000000000000000000000000000000000..b6b6b21f85c377d1d4e13dbffb90b9225ed81881 |
| --- /dev/null |
| +++ b/vpython/cipd/cipd.go |
| @@ -0,0 +1,168 @@ |
| +// Copyright 2017 The LUCI Authors. All rights reserved. |
| +// Use of this source code is governed under the Apache License, Version 2.0 |
| +// that can be found in the LICENSE file. |
| + |
| +package cipd |
| + |
| +import ( |
| + "bytes" |
| + "fmt" |
| + "io" |
| + |
| + "github.com/luci/luci-go/vpython/api/env" |
| + "github.com/luci/luci-go/vpython/venv" |
| + |
| + "github.com/luci/luci-go/cipd/client/cipd" |
| + "github.com/luci/luci-go/cipd/client/cipd/common" |
| + "github.com/luci/luci-go/cipd/client/cipd/ensure" |
| + "github.com/luci/luci-go/common/errors" |
| + "github.com/luci/luci-go/common/logging" |
| + |
| + "golang.org/x/net/context" |
| +) |
| + |
| +// PackageLoader is an implementation of venv.PackageLoader that uses the |
| +// CIPD service to fetch packages. |
| +// |
| +// Packages that use the CIPD loader use the CIPD package name as their Path |
| +// and a CIPD version/tag/ref as their Version. |
| +type PackageLoader struct { |
| + // Options are additional client options to use when generating CIPD clients. |
| + Options cipd.ClientOptions |
| +} |
| + |
| +var _ venv.PackageLoader = (*PackageLoader)(nil) |
| + |
| +// Resolve implements venv.PackageLoader. |
| +// |
| +// The resulting packages slice will be updated in-place with the resolves |
|
iannucci
2017/02/23 01:06:56
resolved
dnj
2017/03/11 16:47:24
Done.
|
| +// package name and instance ID. |
| +func (pl *PackageLoader) Resolve(c context.Context, root string, packages []*env.Spec_Package) error { |
| + if len(packages) == 0 { |
| + return nil |
| + } |
| + |
| + var ensureFile bytes.Buffer |
| + if err := writeEnsureFile(&ensureFile, packages); err != nil { |
| + return errors.Annotate(err).Reason("failed to generate manifest").Err() |
| + } |
| + |
| + // Generate a CIPD client. Use the supplied root. |
| + opts := pl.Options |
| + opts.Root = root |
| + client, err := cipd.NewClient(opts) |
| + if err != nil { |
| + return errors.Annotate(err).Reason("failed to generate CIPD client").Err() |
| + } |
| + |
| + // Start a CIPD client batch. |
| + client.BeginBatch(c) |
| + defer client.EndBatch(c) |
| + |
| + // Parse and resolve the CIPD ensure file. |
| + logging.Debugf(c, "Resolving CIPD manifest:\n%s", ensureFile.Bytes()) |
| + ef, err := ensure.ParseFile(&ensureFile) |
|
iannucci
2017/02/23 01:06:56
could just make an ensure.File directly rather tha
dnj
2017/03/11 16:47:24
Oh neat! I don't think this was exported the first
|
| + if err != nil { |
| + return errors.Annotate(err).Reason("failed to process ensure file").Err() |
| + } |
| + |
| + resolved, err := ef.Resolve(func(pkg, vers string) (common.Pin, error) { |
| + return client.ResolveVersion(c, pkg, vers) |
| + }) |
| + if err != nil { |
| + return errors.Annotate(err).Reason("failed to resolve ensure file").Err() |
| + } |
| + |
| + // Write the results to "packages". All of them should have been installed |
| + // into the root subdir. |
| + for i, pkg := range resolved.PackagesBySubdir[""] { |
| + packages[i].Path = pkg.PackageName |
|
iannucci
2017/02/23 01:06:56
why is it packages[i].Path? Shouldn't it be Name o
dnj
2017/03/11 16:47:24
In the protobuf, "package" is an intentionally gen
|
| + packages[i].Version = pkg.InstanceID |
| + } |
| + return nil |
| +} |
| + |
| +// Ensure implement venv.PackageLoader. |
| +// |
| +// The packages must be valid (PackageIsComplete). If they aren't, Ensure will |
| +// panic. |
| +// |
| +// The CIPD client that is used for the operation is generated from the supplied |
| +// options, opts. |
| +func (pl *PackageLoader) Ensure(c context.Context, root string, packages []*env.Spec_Package) error { |
| + pins, err := packagesToPins(packages) |
| + if err != nil { |
| + return errors.Annotate(err).Reason("failed to convert packages to CIPD pins").Err() |
| + } |
| + pinSlice := common.PinSliceBySubdir{ |
| + "": pins, |
| + } |
| + |
| + // Generate a CIPD client. Use the supplied root. |
| + opts := pl.Options |
| + opts.Root = root |
| + client, err := cipd.NewClient(opts) |
| + if err != nil { |
| + return errors.Annotate(err).Reason("failed to generate CIPD client").Err() |
| + } |
| + |
| + // Start a CIPD client batch. |
| + client.BeginBatch(c) |
| + defer client.EndBatch(c) |
| + |
| + actionMap, err := client.EnsurePackages(c, pinSlice, false) |
| + if err != nil { |
| + return errors.Annotate(err).Reason("failed to install CIPD packages").Err() |
| + } |
| + if len(actionMap) > 0 { |
| + errorCount := 0 |
| + for root, action := range actionMap { |
| + errorCount += len(action.Errors) |
| + for _, err := range action.Errors { |
| + logging.Errorf(c, "CIPD root %q action %q for pin %q encountered error: %s", root, err.Action, err.Pin, err) |
| + } |
| + } |
| + if errorCount > 0 { |
| + return errors.Reason("CIPD package installation encountered %(count)d error(s)"). |
| + D("count", errorCount). |
| + Err() |
| + } |
| + } |
| + return nil |
| +} |
| + |
| +func writeEnsureFile(out io.Writer, packages []*env.Spec_Package) error { |
| + for _, pkg := range packages { |
| + if err := validatePackage(pkg); err != nil { |
| + panic(errors.Annotate(err).Reason("invalid CIPD package").Err()) |
| + } |
| + if _, err := fmt.Fprintf(out, "%s %s\n", pkg.Path, pkg.Version); err != nil { |
| + return errors.Annotate(err).Reason("failed to write manifest line").Err() |
| + } |
| + } |
| + return nil |
| +} |
| + |
| +// validatePackage returns an error if the package does not have all of the |
| +// required fields to describe a CIPD package. |
| +func validatePackage(pkg *env.Spec_Package) error { |
|
iannucci
2017/02/23 01:06:56
tbh... Ensure will do this better anyway: https://
dnj
2017/03/11 16:47:24
Done.
|
| + switch { |
| + case pkg.Path == "": |
| + return errors.New("package must have a path") |
| + case pkg.Version == "": |
| + return errors.New("package must have a version") |
| + default: |
| + return nil |
| + } |
| +} |
| + |
| +func packagesToPins(packages []*env.Spec_Package) ([]common.Pin, error) { |
| + pins := make([]common.Pin, len(packages)) |
| + for i, pkg := range packages { |
| + pins[i] = common.Pin{ |
| + PackageName: pkg.Path, |
| + InstanceID: pkg.Version, |
| + } |
| + } |
| + return pins, nil |
| +} |