blob: fe4393ef78f7926079068d02c7dd9a612e864c5c [file] [log] [blame]
// Copyright 2021 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 processing
import (
"context"
"io"
"strings"
"go.chromium.org/luci/common/errors"
"go.chromium.org/luci/common/logging"
"go.chromium.org/luci/common/retry/transient"
"go.chromium.org/luci/gae/service/datastore"
api "go.chromium.org/luci/cipd/api/cipd/v1"
"go.chromium.org/luci/cipd/appengine/impl/bootstrap"
"go.chromium.org/luci/cipd/appengine/impl/cas"
"go.chromium.org/luci/cipd/appengine/impl/model"
)
// BootstrapPackageExtractorProcID is identifier of BootstrapPackageExtractor.
const BootstrapPackageExtractorProcID = "bootstrap_extractor:v1"
// BootstrapPackageExtractor is a processor that extracts files from bootstrap
// packages (per bootstrap.cfg service config).
type BootstrapPackageExtractor struct {
CAS cas.StorageServer
// For mocking in tests.
uploader func(ctx context.Context, size int64, uploadURL string) io.Writer
}
// BootstrapExtractorResult is stored as JSON in model.ProcessingResult.
type BootstrapExtractorResult struct {
File string // the name of the extracted file
HashAlgo string // the hash algorithm used to calculate its hash for CAS
HashDigest string // its hex digest in the CAS
Size int64 // the size of the extracted file
}
// ID is a part of the Processor interface.
func (bs *BootstrapPackageExtractor) ID() string {
return BootstrapPackageExtractorProcID
}
// Applicable is a part of the Processor interface.
func (bs *BootstrapPackageExtractor) Applicable(ctx context.Context, inst *model.Instance) (bool, error) {
cfg, err := bootstrap.BootstrapConfig(ctx, inst.Package.StringID())
return cfg != nil, err
}
// Run is a part of the Processor interface.
func (bs *BootstrapPackageExtractor) Run(ctx context.Context, inst *model.Instance, pkg *PackageReader) (res Result, err error) {
// Put fatal errors into 'res' and return transient ones as is.
defer func() {
if err != nil && !transient.Tag.In(err) {
res.Err = err
err = nil
}
}()
// Bootstrap packages are expected to contain only one top-level file (which
// we assume is the executable used for the bootstrap). Note that all packages
// contain ".cipdpkg" directory with some CIPD metadata, which we skip.
//
// For windows, a bat shim might be produced. If this is the case, we ignore
// it here since it is not the executable.
executable := ""
for _, f := range pkg.Files() {
if strings.HasPrefix(f, ".cipdpkg/") {
continue
}
if strings.HasSuffix(f, ".bat") {
continue
}
if executable != "" {
err = errors.Reason("the package is marked as a bootstrap package, but it contains multiple files").Err()
return
}
executable = f
}
switch {
case executable == "":
err = errors.Reason("the package is marked as a bootstrap package, but it contains no files").Err()
return
case strings.Contains(executable, "/"):
err = errors.Reason("the package is marked as a bootstrap package, but its content is not at the package root").Err()
return
}
// Execute the extraction.
result, err := (&Extractor{
Reader: pkg,
CAS: bs.CAS,
PrimaryHash: api.HashAlgo_SHA256,
Uploader: bs.uploader,
}).Run(ctx, executable)
if err != nil {
return
}
// Store the results in the appropriate format.
res.Result = BootstrapExtractorResult{
File: executable,
HashAlgo: result.Ref.HashAlgo.String(),
HashDigest: result.Ref.HexDigest,
Size: result.Size,
}
logging.Infof(ctx, "Extracted the bootstrap executable %q from %s: %s %s (%d bytes)",
executable, inst.Package.StringID(), result.Ref.HashAlgo, result.Ref.HexDigest, result.Size)
return
}
// GetBootstrapExtractorResult returns results of BootstrapPackageExtractor.
//
// Returns:
//
// (result, nil) on success.
// (nil, datastore.ErrNoSuchEntity) if results are not available.
// (nil, transient-tagged error) on retrieval errors.
// (nil, non-transient-tagged error) if the extractor failed.
func GetBootstrapExtractorResult(ctx context.Context, inst *model.Instance) (*BootstrapExtractorResult, error) {
r := &model.ProcessingResult{
ProcID: BootstrapPackageExtractorProcID,
Instance: datastore.KeyForObj(ctx, inst),
}
switch err := datastore.Get(ctx, r); {
case err == datastore.ErrNoSuchEntity:
return nil, err
case err != nil:
return nil, transient.Tag.Apply(err)
case !r.Success:
return nil, errors.Reason("bootstrap extraction failed: %s", r.Error).Err()
}
out := &BootstrapExtractorResult{}
if err := r.ReadResult(out); err != nil {
return nil, errors.Annotate(err, "failed to parse the extractor status").Err()
}
return out, nil
}