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 }