go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/cipd/appengine/impl/repo/processing/bootstrap.go (about)

     1  // Copyright 2021 The LUCI Authors.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //      http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package processing
    16  
    17  import (
    18  	"context"
    19  	"io"
    20  	"strings"
    21  
    22  	"go.chromium.org/luci/common/errors"
    23  	"go.chromium.org/luci/common/logging"
    24  	"go.chromium.org/luci/common/retry/transient"
    25  	"go.chromium.org/luci/gae/service/datastore"
    26  
    27  	api "go.chromium.org/luci/cipd/api/cipd/v1"
    28  	"go.chromium.org/luci/cipd/appengine/impl/bootstrap"
    29  	"go.chromium.org/luci/cipd/appengine/impl/cas"
    30  	"go.chromium.org/luci/cipd/appengine/impl/model"
    31  )
    32  
    33  // BootstrapPackageExtractorProcID is identifier of BootstrapPackageExtractor.
    34  const BootstrapPackageExtractorProcID = "bootstrap_extractor:v1"
    35  
    36  // BootstrapPackageExtractor is a processor that extracts files from bootstrap
    37  // packages (per bootstrap.cfg service config).
    38  type BootstrapPackageExtractor struct {
    39  	CAS cas.StorageServer
    40  
    41  	// For mocking in tests.
    42  	uploader func(ctx context.Context, size int64, uploadURL string) io.Writer
    43  }
    44  
    45  // BootstrapExtractorResult is stored as JSON in model.ProcessingResult.
    46  type BootstrapExtractorResult struct {
    47  	File       string // the name of the extracted file
    48  	HashAlgo   string // the hash algorithm used to calculate its hash for CAS
    49  	HashDigest string // its hex digest in the CAS
    50  	Size       int64  // the size of the extracted file
    51  }
    52  
    53  // ID is a part of the Processor interface.
    54  func (bs *BootstrapPackageExtractor) ID() string {
    55  	return BootstrapPackageExtractorProcID
    56  }
    57  
    58  // Applicable is a part of the Processor interface.
    59  func (bs *BootstrapPackageExtractor) Applicable(ctx context.Context, inst *model.Instance) (bool, error) {
    60  	cfg, err := bootstrap.BootstrapConfig(ctx, inst.Package.StringID())
    61  	return cfg != nil, err
    62  }
    63  
    64  // Run is a part of the Processor interface.
    65  func (bs *BootstrapPackageExtractor) Run(ctx context.Context, inst *model.Instance, pkg *PackageReader) (res Result, err error) {
    66  	// Put fatal errors into 'res' and return transient ones as is.
    67  	defer func() {
    68  		if err != nil && !transient.Tag.In(err) {
    69  			res.Err = err
    70  			err = nil
    71  		}
    72  	}()
    73  
    74  	// Bootstrap packages are expected to contain only one top-level file (which
    75  	// we assume is the executable used for the bootstrap). Note that all packages
    76  	// contain ".cipdpkg" directory with some CIPD metadata, which we skip.
    77  	//
    78  	// For windows, a bat shim might be produced. If this is the case, we ignore
    79  	// it here since it is not the executable.
    80  	executable := ""
    81  	for _, f := range pkg.Files() {
    82  		if strings.HasPrefix(f, ".cipdpkg/") {
    83  			continue
    84  		}
    85  		if strings.HasSuffix(f, ".bat") {
    86  			continue
    87  		}
    88  		if executable != "" {
    89  			err = errors.Reason("the package is marked as a bootstrap package, but it contains multiple files").Err()
    90  			return
    91  		}
    92  		executable = f
    93  	}
    94  	switch {
    95  	case executable == "":
    96  		err = errors.Reason("the package is marked as a bootstrap package, but it contains no files").Err()
    97  		return
    98  	case strings.Contains(executable, "/"):
    99  		err = errors.Reason("the package is marked as a bootstrap package, but its content is not at the package root").Err()
   100  		return
   101  	}
   102  
   103  	// Execute the extraction.
   104  	result, err := (&Extractor{
   105  		Reader:      pkg,
   106  		CAS:         bs.CAS,
   107  		PrimaryHash: api.HashAlgo_SHA256,
   108  		Uploader:    bs.uploader,
   109  	}).Run(ctx, executable)
   110  	if err != nil {
   111  		return
   112  	}
   113  
   114  	// Store the results in the appropriate format.
   115  	res.Result = BootstrapExtractorResult{
   116  		File:       executable,
   117  		HashAlgo:   result.Ref.HashAlgo.String(),
   118  		HashDigest: result.Ref.HexDigest,
   119  		Size:       result.Size,
   120  	}
   121  
   122  	logging.Infof(ctx, "Extracted the bootstrap executable %q from %s: %s %s (%d bytes)",
   123  		executable, inst.Package.StringID(), result.Ref.HashAlgo, result.Ref.HexDigest, result.Size)
   124  
   125  	return
   126  }
   127  
   128  // GetBootstrapExtractorResult returns results of BootstrapPackageExtractor.
   129  //
   130  // Returns:
   131  //
   132  //	(result, nil) on success.
   133  //	(nil, datastore.ErrNoSuchEntity) if results are not available.
   134  //	(nil, transient-tagged error) on retrieval errors.
   135  //	(nil, non-transient-tagged error) if the extractor failed.
   136  func GetBootstrapExtractorResult(ctx context.Context, inst *model.Instance) (*BootstrapExtractorResult, error) {
   137  	r := &model.ProcessingResult{
   138  		ProcID:   BootstrapPackageExtractorProcID,
   139  		Instance: datastore.KeyForObj(ctx, inst),
   140  	}
   141  	switch err := datastore.Get(ctx, r); {
   142  	case err == datastore.ErrNoSuchEntity:
   143  		return nil, err
   144  	case err != nil:
   145  		return nil, transient.Tag.Apply(err)
   146  	case !r.Success:
   147  		return nil, errors.Reason("bootstrap extraction failed: %s", r.Error).Err()
   148  	}
   149  	out := &BootstrapExtractorResult{}
   150  	if err := r.ReadResult(out); err != nil {
   151  		return nil, errors.Annotate(err, "failed to parse the extractor status").Err()
   152  	}
   153  	return out, nil
   154  }