go.chromium.org/luci@v0.0.0-20240309015107-7cdc2e660f33/luciexe/build/start.go (about) 1 // Copyright 2020 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 build 16 17 import ( 18 "context" 19 "fmt" 20 21 "google.golang.org/protobuf/proto" 22 "google.golang.org/protobuf/types/known/timestamppb" 23 24 bbpb "go.chromium.org/luci/buildbucket/proto" 25 "go.chromium.org/luci/common/clock" 26 "go.chromium.org/luci/common/data/stringset" 27 "go.chromium.org/luci/common/errors" 28 ) 29 30 // Start is the 'inner' entrypoint to this library. 31 // 32 // If you're writing a standalone luciexe binary, see `Main` and 33 // `MainWithOutput`. 34 // 35 // This function clones `initial` as the basis of all state updates (see 36 // OptSend) and MakePropertyReader declarations. This also initializes the build 37 // State in `ctx` and returns the manipulable State object. 38 // 39 // You must End the returned State. To automatically map errors and panics to 40 // their correct visual representation, End the State like: 41 // 42 // var err error 43 // state, ctx := build.Start(ctx, initialBuild, ...) 44 // defer func() { state.End(err) }() 45 // 46 // err = opThatErrsOrPanics(ctx) 47 // 48 // NOTE: A panic will still crash the program as usual. This does NOT 49 // `recover()` the panic. Please use conventional Go error handling and control 50 // flow mechanisms. 51 func Start(ctx context.Context, initial *bbpb.Build, opts ...StartOption) (*State, context.Context, error) { 52 if initial == nil { 53 initial = &bbpb.Build{} 54 } 55 initial = proto.Clone(initial).(*bbpb.Build) 56 // initialize proto sections which other code in this module assumes exist. 57 proto.Merge(initial, &bbpb.Build{ 58 Output: &bbpb.Build_Output{}, 59 Input: &bbpb.Build_Input{}, 60 }) 61 62 outputReservationKeys := propModifierReservations.locs.snap() 63 64 logClosers := map[string]func() error{} 65 outputProps := make(map[string]*outputPropertyState, len(outputReservationKeys)) 66 ret := newState(initial, logClosers, outputProps) 67 68 for ns := range outputReservationKeys { 69 ret.outputProperties[ns] = &outputPropertyState{} 70 } 71 ret.ctx, ret.ctxCloser = context.WithCancel(ctx) 72 73 for _, opt := range opts { 74 opt(ret) 75 } 76 77 // in case our buildPb is unstarted, start it now. 78 if ret.buildPb.StartTime == nil { 79 ret.buildPb.StartTime = timestamppb.New(clock.Now(ctx)) 80 ret.buildPb.Status = bbpb.Status_STARTED 81 ret.buildPb.Output.Status = bbpb.Status_STARTED 82 } 83 84 // initialize all log names already in ret.buildPb; likely this includes 85 // stdout/stderr which may already be populated by our parent process, such as 86 // `bbagent`. 87 for _, l := range ret.buildPb.Output.Logs { 88 ret.logNames.resolveName(l.Name) 89 } 90 91 err := func() (err error) { 92 ret.reservedInputProperties, err = parseReservedInputProperties(initial.Input.Properties, ret.strictParse) 93 if err != nil { 94 return 95 } 96 if ret.topLevelInputProperties != nil { 97 if err := parseTopLevelProperties(ret.buildPb.Input.Properties, ret.strictParse, ret.topLevelInputProperties); err != nil { 98 return errors.Annotate(err, "parsing top-level properties").Err() 99 } 100 } 101 if tlo := ret.topLevelOutput; tlo != nil { 102 fields := tlo.msg.ProtoReflect().Descriptor().Fields() 103 topLevelOutputKeys := stringset.New(fields.Len()) 104 for i := 0; i < fields.Len(); i++ { 105 f := fields.Get(i) 106 topLevelOutputKeys.Add(f.TextName()) 107 topLevelOutputKeys.Add(f.JSONName()) 108 } 109 for reserved := range ret.outputProperties { 110 if topLevelOutputKeys.Has(reserved) { 111 return errors.Reason( 112 "output property %q conflicts with field in top-level output properties: reserved at %s", 113 reserved, propModifierReservations.locs.get(reserved)).Err() 114 } 115 } 116 } 117 return 118 }() 119 if err != nil { 120 err = AttachStatus(err, bbpb.Status_INFRA_FAILURE, nil) 121 ret.SetSummaryMarkdown(fmt.Sprintf("fatal error starting build: %s", err)) 122 ret.End(err) 123 return nil, ctx, err 124 } 125 126 return ret, setState(ctx, ctxState{ret, nil}), nil 127 }