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  }