blob: aca896cf73a2a8f4fa40d9bfd8a92d226f2d1c5b [file] [log] [blame]
// 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)
}