| // 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" |
| "encoding/json" |
| "fmt" |
| "io" |
| "os" |
| "os/exec" |
| "path/filepath" |
| |
| "go.fuchsia.dev/infra/cmd/recipe_wrapper/bcid" |
| "golang.org/x/exp/slices" |
| ) |
| |
| type Package struct { |
| name string |
| version string |
| binDir string |
| } |
| |
| var ( |
| VPythonPkg = Package{ |
| name: "infra/tools/luci/vpython/${platform}", |
| version: "git_revision:3d6ee7542a04be92a48ff1c5dc28cb3c5c15dd00", |
| binDir: "", |
| } |
| CPythonPkg = Package{ |
| name: "infra/3pp/tools/cpython/${platform}", |
| version: "version:2@2.7.18.chromium.44", |
| binDir: "bin", |
| } |
| ) |
| |
| 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 { |
| 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) |
| } |
| binDirs = append(binDirs, filepath.Join(dir, pkg.binDir)) |
| } |
| return binDirs, nil |
| } |
| |
| type DeferredStep struct { |
| Name string `json:"name"` |
| Command []string `json:"command"` |
| Environment map[string]string `json:"environment"` |
| } |
| |
| type deferredSteps struct { |
| Steps []DeferredStep `json:"steps"` |
| } |
| |
| func ParseDeferredSteps(data io.Reader) ([]DeferredStep, error) { |
| d, err := io.ReadAll(data) |
| if err != nil { |
| return nil, err |
| } |
| var steps deferredSteps |
| if err := json.Unmarshal(d, &steps); err != nil { |
| return nil, err |
| } |
| |
| return steps.Steps, nil |
| } |
| |
| // RegisteredPkgs returns the filepaths to CIPD packages that were |
| // registered during the deferred steps. |
| // Assumes the package filepath is immediately after the `pkg-deploy` command. |
| // Names are not returned. |
| func RegisteredPkgs(steps []*DeferredStep) []string { |
| var paths []string |
| for _, s := range steps { |
| if i := slices.Index(s.Command, "pkg-register"); i >= 0 { |
| fp, err := filepath.Abs(s.Command[i+1]) |
| // Don't attempt to handle the error. Garbage in, garbage out is sufficient here. |
| if err != nil { |
| continue |
| } |
| paths = append(paths, fp) |
| } |
| } |
| return paths |
| } |
| |
| // AttestPkgs searches for packages that were registered to CIPD and |
| // generates attestations for them. |
| func AttestPkgs(steps []*DeferredStep, keyID string) ([]*bcid.InTotoBundle, []error) { |
| var bundles []*bcid.InTotoBundle |
| var errs []error |
| for _, pkg := range RegisteredPkgs(steps) { |
| f, err := os.Open(pkg) |
| if err != nil { |
| errs = append(errs, fmt.Errorf("can't open package file %q: %v", pkg, err)) |
| continue |
| } |
| defer f.Close() |
| |
| stmt, err := bcid.NewStmt(&bcid.Subject{ |
| Name: pkg, |
| Data: f, |
| }) |
| 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 |
| } |
| bundles = append(bundles, b) |
| } |
| |
| return bundles, errs |
| } |