github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/pkg/platform/runtime/setup/buildlog/buildlog.go (about)

     1  package buildlog
     2  
     3  import (
     4  	"context"
     5  	"fmt"
     6  	"os"
     7  	"strings"
     8  	"sync"
     9  	"time"
    10  
    11  	"github.com/ActiveState/cli/pkg/buildplan"
    12  	"github.com/ActiveState/cli/pkg/platform/runtime/setup/events"
    13  	"github.com/go-openapi/strfmt"
    14  	"github.com/gorilla/websocket"
    15  
    16  	"github.com/ActiveState/cli/internal/constants"
    17  	"github.com/ActiveState/cli/internal/errs"
    18  	"github.com/ActiveState/cli/internal/locale"
    19  	"github.com/ActiveState/cli/internal/logging"
    20  	"github.com/ActiveState/cli/pkg/platform/api/buildlogstream"
    21  )
    22  
    23  // verboseLogging is true if the user provided an environment variable for it
    24  var verboseLogging = os.Getenv(constants.LogBuildVerboseEnvVarName) == "true"
    25  
    26  type recipeRequest struct {
    27  	RecipeID string `json:"recipeID"`
    28  }
    29  
    30  type artifactRequest struct {
    31  	ArtifactID string `json:"artifactID"`
    32  }
    33  
    34  // BuildLogConnector describes how to interact with a build log connection
    35  type BuildLogConnector interface {
    36  	ReadJSON(interface{}) error
    37  	WriteJSON(interface{}) error
    38  }
    39  
    40  type Events interface {
    41  	BuildStarting(total int)
    42  	BuildFinished()
    43  	ArtifactBuildStarting(artifactID strfmt.UUID)
    44  	ArtifactBuildCached(artifactID strfmt.UUID, logURI string)
    45  	ArtifactBuildCompleted(artifactID strfmt.UUID, logURI string)
    46  	ArtifactBuildFailed(artifactID strfmt.UUID, logURI string, errorMessage string)
    47  	ArtifactBuildProgress(artifact strfmt.UUID, timestamp string, message string, facility, pipeName, source string)
    48  	Heartbeat(time.Time)
    49  }
    50  
    51  // BuildError designates a build log build error.
    52  type BuildError struct {
    53  	*locale.LocalizedError
    54  }
    55  
    56  type ArtifactBuildError struct {
    57  	*errs.WrapperError
    58  	Artifact *buildplan.Artifact
    59  	Message  *ArtifactFailedMessage
    60  }
    61  
    62  // EventHandlerError designates an error in the event handler for reporting progress.
    63  type EventHandlerError struct {
    64  	*errs.WrapperError
    65  }
    66  
    67  // BuildLog is an implementation of a build log
    68  type BuildLog struct {
    69  	ch    chan *buildplan.Artifact
    70  	errCh chan error
    71  	conn  *websocket.Conn
    72  }
    73  
    74  // New creates a new BuildLog instance that allows us to wait for incoming build log information
    75  // artifactMap comprises all artifacts (from the runtime closure) that are in the recipe, alreadyBuilt is set of artifact IDs that have already been built in the past
    76  func New(ctx context.Context, artifactMap buildplan.ArtifactIDMap, eventHandler events.Handler, recipeID strfmt.UUID, logFilePath string) (*BuildLog, error) {
    77  	conn, err := buildlogstream.Connect(ctx)
    78  	if err != nil {
    79  		return nil, errs.Wrap(err, "Could not connect to build-log streamer build updates")
    80  	}
    81  	bl, err := NewWithCustomConnections(artifactMap, conn, eventHandler, recipeID, logFilePath)
    82  	if err != nil {
    83  		conn.Close()
    84  
    85  		return nil, err
    86  	}
    87  	bl.conn = conn
    88  	return bl, nil
    89  }
    90  
    91  // NewWithCustomConnections creates a new BuildLog instance with all physical connections managed by the caller
    92  func NewWithCustomConnections(artifactMap buildplan.ArtifactIDMap,
    93  	conn BuildLogConnector, eventHandler events.Handler,
    94  	recipeID strfmt.UUID, logFilePath string) (*BuildLog, error) {
    95  
    96  	ch := make(chan *buildplan.Artifact)
    97  	errCh := make(chan error)
    98  
    99  	if err := handleEvent(eventHandler, events.BuildStarted{logFilePath}); err != nil {
   100  		return nil, errs.Wrap(err, "Could not handle BuildStarted event")
   101  	}
   102  
   103  	go func() {
   104  		defer close(ch)
   105  		defer close(errCh)
   106  
   107  		// It is currently possible for the buildlogstreamer to send the same event twice.
   108  		// This happens, when nomad looses track of a build job and the HC re-schedules it.
   109  		// The following code is used to identify duplicate events.
   110  		uniqueEvents := make(map[string]struct{})
   111  		observed := func(id ...string) bool {
   112  			idStr := strings.Join(id, ".")
   113  			_, ok := uniqueEvents[idStr]
   114  			if !ok {
   115  				uniqueEvents[idStr] = struct{}{}
   116  			}
   117  			return ok
   118  		}
   119  
   120  		artifactsDone := make(map[strfmt.UUID]struct{})
   121  
   122  		// Set up log file
   123  		logMutex := &sync.Mutex{}
   124  		logfile, err := os.OpenFile(logFilePath, os.O_CREATE|os.O_WRONLY, 0644)
   125  		if err != nil {
   126  			errCh <- errs.Wrap(err, "Could not open build log file")
   127  			return
   128  		}
   129  		defer logfile.Close()
   130  		writeLogFile := func(artifactID strfmt.UUID, msg string) error {
   131  			logMutex.Lock()
   132  			defer logMutex.Unlock()
   133  			name := artifactID.String()
   134  			if a, ok := artifactMap[artifactID]; ok {
   135  				name = a.Name() + " (" + artifactID.String() + ")"
   136  			}
   137  			if name != "" {
   138  				name = name + ": "
   139  			}
   140  			if _, err := logfile.WriteString(name + msg + "\n"); err != nil {
   141  				return errs.Wrap(err, "Could not write string to build log file")
   142  			}
   143  			if err := logfile.Sync(); err != nil {
   144  				return errs.Wrap(err, "Could not sync build log file")
   145  			}
   146  			return nil
   147  		}
   148  
   149  		stillWaiting := func() []string {
   150  			result := []string{}
   151  			for id := range artifactMap {
   152  				if _, done := artifactsDone[id]; !done {
   153  					name := id.String()
   154  					if a, ok := artifactMap[id]; ok {
   155  						name = a.Name() + " (" + id.String() + ")"
   156  					}
   157  					result = append(result, name)
   158  				}
   159  			}
   160  			return result
   161  		}
   162  
   163  		buildSuccess := func() {
   164  			if err := writeLogFile("", "Build Succeeded"); err != nil {
   165  				errCh <- errs.Wrap(err, "Could not write to build log file")
   166  			}
   167  			if err := handleEvent(eventHandler, events.BuildSuccess{}); err != nil {
   168  				errCh <- errs.Wrap(err, "Could not handle BuildSuccess event")
   169  			}
   170  		}
   171  
   172  		var artifactErr error
   173  		for {
   174  			var msg Message
   175  			err := conn.ReadJSON(&msg)
   176  			if err != nil {
   177  				// This should bubble up and logging it is just an extra measure to help with debugging
   178  				logging.Debug("Encountered error: %s", errs.JoinMessage(err))
   179  				errCh <- err
   180  				return
   181  			}
   182  			if verboseLogging {
   183  				logging.Debug("Received response: %s", msg.MessageTypeValue())
   184  			}
   185  
   186  			switch msg.MessageType() {
   187  			case BuildStarted:
   188  				if observed(msg.MessageTypeValue()) {
   189  					break
   190  				}
   191  				if err := writeLogFile("", "Build Started"); err != nil {
   192  					errCh <- errs.Wrap(err, "Could not write to build log file")
   193  				}
   194  			case BuildFailed:
   195  				if observed(msg.MessageTypeValue()) {
   196  					break
   197  				}
   198  				m := msg.messager.(BuildFailedMessage)
   199  				if err := writeLogFile("", m.ErrorMessage); err != nil {
   200  					errCh <- errs.Wrap(err, "Could not write to build log file")
   201  				}
   202  				if err := handleEvent(eventHandler, events.BuildFailure{m.ErrorMessage}); err != nil {
   203  					errCh <- errs.Wrap(err, "Could not handle BuildFailure event")
   204  				}
   205  				errCh <- &BuildError{locale.WrapError(artifactErr, "err_logstream_build_failed", "Build failed with error message: {{.V0}}.", m.ErrorMessage)}
   206  				return
   207  			case BuildSucceeded:
   208  				if observed(msg.MessageTypeValue()) {
   209  					break
   210  				}
   211  				buildSuccess()
   212  				return
   213  			case ArtifactStarted:
   214  				m := msg.messager.(ArtifactMessage)
   215  				// NOTE: fix to ignore current noop "final pkg artifact"
   216  				if m.ArtifactID == recipeID {
   217  					break
   218  				}
   219  
   220  				_, ok := artifactMap[m.ArtifactID]
   221  				if !ok {
   222  					logging.Debug("Ignoring ArtifactStarted %s as we are not monitoring this artifact", m.ArtifactID)
   223  					break
   224  				}
   225  
   226  				if observed(msg.MessageTypeValue(), m.ArtifactID.String()) {
   227  					break
   228  				}
   229  
   230  				if err := writeLogFile(m.ArtifactID, "Artifact Build Started"); err != nil {
   231  					errCh <- errs.Wrap(err, "Could not write to build log file")
   232  				}
   233  
   234  				if err := handleEvent(eventHandler, events.ArtifactBuildStarted{m.ArtifactID, m.CacheHit}); err != nil {
   235  					errCh <- errs.Wrap(err, "Could not handle ArtifactBuildStarted event")
   236  				}
   237  
   238  				// if verbose build logging is requested: Also subscribe to log messages for this artifacts
   239  				// you don't want to do this by default as the log size can be quite large
   240  				if verboseLogging {
   241  					logging.Debug("requesting updates for artifact %s", m.ArtifactID.String())
   242  					request := artifactRequest{ArtifactID: m.ArtifactID.String()}
   243  					if err := conn.WriteJSON(request); err != nil {
   244  						errCh <- errs.Wrap(err, "Could not start artifact log request")
   245  						return
   246  					}
   247  				}
   248  			case ArtifactSucceeded:
   249  				m := msg.messager.(ArtifactSucceededMessage)
   250  
   251  				// NOTE: fix to ignore current noop "final pkg artifact"
   252  				if m.ArtifactID == recipeID {
   253  					break
   254  				}
   255  
   256  				ad, ok := artifactMap[m.ArtifactID]
   257  				if !ok {
   258  					logging.Debug("Ignoring ArtifactSucceeded %s as we are not monitoring this artifact", m.ArtifactID)
   259  					break
   260  				}
   261  
   262  				if observed(msg.MessageTypeValue(), m.ArtifactID.String()) {
   263  					break
   264  				}
   265  
   266  				artifactsDone[m.ArtifactID] = struct{}{}
   267  
   268  				if err := writeLogFile(m.ArtifactID, fmt.Sprintf(strings.TrimSpace(`
   269  Artifact Build Succeeded. 
   270  	Payload URI: %s
   271  	Log URI: %s
   272  	Used cache: %v
   273  `), m.ArtifactURI, m.LogURI, m.CacheHit)); err != nil {
   274  					errCh <- errs.Wrap(err, "Could not write to build log file")
   275  				}
   276  
   277  				if m.ArtifactURI == "" {
   278  					errCh <- errs.Wrap(err, "Received artifact succeeded event without artifact URL: %+v", m)
   279  					return
   280  				}
   281  
   282  				ad.SetDownload(m.ArtifactURI, m.ArtifactChecksum)
   283  
   284  				ch <- ad
   285  
   286  				if err := handleEvent(eventHandler, events.ArtifactBuildSuccess{m.ArtifactID, m.LogURI}); err != nil {
   287  					errCh <- errs.Wrap(err, "Could not handle ArtifactBuildSuccess event")
   288  					return
   289  				}
   290  
   291  				// Because we still use the recipe ID for buildlogstreamer we will end up waiting for artifacts that
   292  				// aren't actually required for our runtime. To address this we effectively send the success event
   293  				// and stop monitoring the buildlogstreamer when we've received events for all our artifacts.
   294  				// This can be dropped once buildlostreamer speaks buildplans.
   295  				if len(stillWaiting()) == 0 {
   296  					buildSuccess()
   297  					return
   298  				}
   299  			case ArtifactFailed:
   300  				m := msg.messager.(ArtifactFailedMessage)
   301  
   302  				ad, ok := artifactMap[m.ArtifactID]
   303  				if !ok {
   304  					logging.Debug("Ignoring ArtifactFailed %s as we are not monitoring this artifact", m.ArtifactID)
   305  					break
   306  				}
   307  
   308  				if observed(msg.MessageTypeValue(), m.ArtifactID.String()) {
   309  					break
   310  				}
   311  
   312  				artifactsDone[m.ArtifactID] = struct{}{}
   313  
   314  				if err := writeLogFile(m.ArtifactID, fmt.Sprintf(strings.TrimSpace(`
   315  Artifact Build Failed. 
   316  	Error Message: %s
   317  	Log URI: %s
   318  `), m.ErrorMessage, m.LogURI)); err != nil {
   319  					errCh <- errs.Wrap(err, "Could not write to build log file")
   320  				}
   321  
   322  				artifactErr = locale.WrapError(artifactErr, "err_artifact_failed", "Failed to build \"{{.V0}}\", error reported: {{.V1}}.", ad.Name(), m.ErrorMessage)
   323  
   324  				if err := handleEvent(eventHandler, events.ArtifactBuildFailure{m.ArtifactID, m.LogURI, m.ErrorMessage}); err != nil {
   325  					errCh <- errs.Wrap(err, "Could not handle ArtifactBuildFailure event")
   326  					return
   327  				}
   328  
   329  				errCh <- &ArtifactBuildError{
   330  					errs.New("artifact build failed"),
   331  					ad,
   332  					&m,
   333  				}
   334  
   335  			case ArtifactProgress:
   336  				m := msg.messager.(ArtifactProgressMessage)
   337  
   338  				_, ok := artifactMap[m.ArtifactID]
   339  				if !ok {
   340  					break
   341  				}
   342  
   343  				if _, ok := artifactsDone[m.ArtifactID]; ok {
   344  					// ignore progress reports for artifacts that have finished
   345  					break
   346  				}
   347  
   348  				if err := writeLogFile(m.ArtifactID, "Log: "+m.Body.Message); err != nil {
   349  					errCh <- errs.Wrap(err, "Could not write to log file")
   350  					return
   351  				}
   352  
   353  				if err := handleEvent(eventHandler, events.ArtifactBuildProgress{
   354  					m.ArtifactID,
   355  					m.Timestamp,
   356  					m.Body.Facility,
   357  					m.PipeName,
   358  					m.Body.Message,
   359  					m.Source,
   360  				}); err != nil {
   361  					errCh <- errs.Wrap(err, "Could not handle ArtifactBuildFailure event")
   362  					return
   363  				}
   364  			case Heartbeat:
   365  				waiting := stillWaiting()
   366  				msg := fmt.Sprintf("Heartbeat (still waiting for %d: %s)", len(waiting), strings.Join(waiting, ", "))
   367  				if err := writeLogFile("", msg); err != nil {
   368  					errCh <- errs.Wrap(err, "Could not write to log file")
   369  					return
   370  				}
   371  
   372  			}
   373  		}
   374  	}()
   375  
   376  	logging.Debug("sending websocket request for %s", recipeID.String())
   377  	request := recipeRequest{RecipeID: recipeID.String()}
   378  	if err := conn.WriteJSON(request); err != nil {
   379  		return nil, errs.Wrap(err, "Could not write websocket request")
   380  	}
   381  
   382  	return &BuildLog{
   383  		ch:    ch,
   384  		errCh: errCh,
   385  	}, nil
   386  }
   387  
   388  // Wait waits for the build log to close because the build is done and all downloadable artifacts are here
   389  func (bl *BuildLog) Wait() error {
   390  	var rerr error
   391  	var errors []error
   392  	for err := range bl.errCh {
   393  		if rerr == nil {
   394  			rerr = errs.New("failed build")
   395  		}
   396  		rerr = errs.Pack(rerr, err)
   397  	}
   398  	if len(errors) > 0 {
   399  		return errors[0]
   400  	}
   401  	return nil
   402  }
   403  
   404  func (bl *BuildLog) Close() error {
   405  	if bl.conn != nil {
   406  		if err := bl.conn.Close(); err != nil {
   407  			return errs.Wrap(err, "Failed to close websocket connection")
   408  		}
   409  	}
   410  	return nil
   411  }
   412  
   413  // BuiltArtifactsChannel returns the channel to listen for downloadable artifacts on
   414  func (bl *BuildLog) BuiltArtifactsChannel() <-chan *buildplan.Artifact {
   415  	return bl.ch
   416  }
   417  
   418  func handleEvent(handler events.Handler, ev events.Eventer) error {
   419  	err := handler.Handle(ev)
   420  	if err != nil {
   421  		return &EventHandlerError{errs.Wrap(err, "Error handling event: %v", errs.JoinMessage(err))}
   422  	}
   423  	return nil
   424  }