| // Copyright 2023 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| package cipd |
| |
| import ( |
| "context" |
| "fmt" |
| "os" |
| "os/exec" |
| "path/filepath" |
| |
| v1 "github.com/in-toto/attestation/go/v1" |
| "go.chromium.org/luci/cipd/client/cipd" |
| "go.chromium.org/luci/cipd/common" |
| "go.chromium.org/luci/common/logging" |
| "go.fuchsia.dev/infra/cmd/recipe_wrapper/bcid" |
| "go.fuchsia.dev/infra/cmd/recipe_wrapper/env" |
| ) |
| |
| const ( |
| SERVICE_URL = "https://chrome-infra-packages.appspot.com" |
| SERVICE_DEV_URL = "https://chrome-infra-packages-dev.appspot.com" |
| ) |
| |
| type Package struct { |
| name string |
| version string |
| binDir string |
| } |
| |
| func (p *Package) AsPin() *common.Pin { |
| return &common.Pin{ |
| PackageName: p.name, |
| InstanceID: p.version, |
| } |
| } |
| |
| var ( |
| VPythonPkg = Package{ |
| name: "infra/tools/luci/vpython/${platform}", |
| version: "git_revision:3d6ee7542a04be92a48ff1c5dc28cb3c5c15dd00", |
| } |
| CPythonPkg = Package{ |
| name: "infra/3pp/tools/cpython/${platform}", |
| version: "version:2@2.7.18.chromium.44", |
| binDir: "bin", |
| } |
| AttestToolPkg = Package{ |
| name: "fuchsia/infra/attestation_tool/${platform}", |
| version: "A5A21ls2KKN_H59cvEI2IKtIT3-TnNnNY49sguw56BoC", |
| } |
| ) |
| |
| func Install(ctx context.Context, dir string, packages ...Package) ([]string, error) { |
| cmd := exec.CommandContext(ctx, "cipd", "init", dir, "-force") |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Run(); err != nil { |
| return nil, fmt.Errorf("cipd init failed: %w", err) |
| } |
| |
| var binDirs []string |
| for _, pkg := range packages { |
| logging.Infof(ctx, "Installing %v / %v to %v", pkg.name, pkg.version, dir) |
| cmd := exec.CommandContext(ctx, "cipd", "install", pkg.name, pkg.version, "-root", dir) |
| cmd.Stdout = os.Stdout |
| cmd.Stderr = os.Stderr |
| if err := cmd.Run(); err != nil { |
| return nil, fmt.Errorf("failed to install %s: %w", pkg.name, err) |
| } |
| logging.Infof(ctx, "%v successfully installed to %v", pkg.name, pkg.binDir) |
| binDirs = append(binDirs, filepath.Join(dir, pkg.binDir)) |
| } |
| |
| return binDirs, nil |
| } |
| |
| type PkgFile struct { |
| PackageName string `json:"cipd_package"` //`json:"package"` |
| InstanceID string `json:"cipd_instance_id"` //`json:"instance_id"` |
| Path string `json:"path"` |
| } |
| |
| func (pf *PkgFile) AsPin() *common.Pin { |
| return &common.Pin{ |
| PackageName: pf.PackageName, |
| InstanceID: pf.InstanceID, |
| } |
| } |
| |
| type StmtBundle struct { |
| Package *PkgFile |
| Statement *v1.Statement |
| Bundle *bcid.InTotoBundle |
| } |
| |
| // AttestPkgs searches for packages that were registered to CIPD and |
| // generates attestations for them. |
| func AttestPkgs(pkgs []*PkgFile, keyID string, buildEnv *env.Build) ([]*StmtBundle, []error) { |
| var stmtBundles []*StmtBundle |
| var errs []error |
| for _, pkg := range pkgs { |
| data, err := os.Open(pkg.Path) |
| if err != nil { |
| errs = append(errs, fmt.Errorf("can't open package file %q: %v", pkg, err)) |
| continue |
| } |
| defer data.Close() |
| |
| stmt, err := bcid.NewStmt(&bcid.Subject{ |
| Pkg: pkg.AsPin(), |
| Data: data, |
| }, buildEnv) |
| if err != nil { |
| errs = append(errs, fmt.Errorf("can't create statement for package %q: %v", pkg, err)) |
| continue |
| } |
| b, err := bcid.Attest(context.TODO(), stmt, keyID) |
| if err != nil { |
| errs = append(errs, fmt.Errorf("can't attest package %q: %v", pkg, err)) |
| continue |
| } |
| stmtBundles = append(stmtBundles, &StmtBundle{ |
| Package: pkg, |
| Statement: stmt, |
| Bundle: b, |
| }) |
| } |
| |
| return stmtBundles, errs |
| } |
| |
| type CombinedOutputer interface { |
| CombinedOutput(context.Context, ...string) ([]byte, error) |
| } |
| |
| type cipdOutputter struct{} |
| |
| func (*cipdOutputter) CombinedOutput(ctx context.Context, args ...string) ([]byte, error) { |
| return exec.CommandContext(ctx, "cipd", args...).CombinedOutput() |
| } |
| |
| // AddMetadata wraps `cipd set-metadata`. |
| // The value is assumed to be a text/plain content type. |
| // It's named `Add` and not `Set` because using the same key multiple times |
| // will result in the key appearing multiple times, not once with the last updated |
| // value. |
| // Package and version name examples can be found at |
| // https://chromium.googlesource.com/infra/luci/luci-go/+/HEAD/cipd/README.md |
| func AddMetadata(ctx context.Context, pkg *common.Pin, md []cipd.Metadata) error { |
| return addMetadataWithOutputter(ctx, &cipdOutputter{}, SERVICE_URL, pkg, md) |
| } |
| |
| func addMetadataWithOutputter(ctx context.Context, cmd CombinedOutputer, serviceURL string, pkg *common.Pin, md []cipd.Metadata) error { |
| // TODO(cflewis): This code would be faster if the client was kept around rather than remade each time. |
| c, err := cipd.NewClientFromEnv(ctx, cipd.ClientOptions{ServiceURL: serviceURL}) |
| if err != nil { |
| return err |
| } |
| defer c.Close(ctx) |
| |
| return c.AttachMetadataWhenReady(ctx, *pkg, md) |
| } |