github.com/GoogleContainerTools/skaffold@v1.39.18/pkg/skaffold/build/result.go (about)

     1  /*
     2  Copyright 2020 The Skaffold Authors
     3  
     4  Licensed under the Apache License, Version 2.0 (the "License");
     5  you may not use this file except in compliance with the License.
     6  You may obtain a copy of the License at
     7  
     8      http://www.apache.org/licenses/LICENSE-2.0
     9  
    10  Unless required by applicable law or agreed to in writing, software
    11  distributed under the License is distributed on an "AS IS" BASIS,
    12  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13  See the License for the specific language governing permissions and
    14  limitations under the License.
    15  */
    16  
    17  package build
    18  
    19  import (
    20  	"bufio"
    21  	"context"
    22  	"fmt"
    23  	"io"
    24  	"sync"
    25  
    26  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/graph"
    27  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output"
    28  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/output/log"
    29  	"github.com/GoogleContainerTools/skaffold/pkg/skaffold/schema/latest"
    30  )
    31  
    32  const bufferedLinesPerArtifact = 10000
    33  
    34  // For testing
    35  var (
    36  	buffSize = bufferedLinesPerArtifact
    37  )
    38  
    39  // logAggregator provides an interface to create an output writer for each artifact build and later aggregate the logs in build order.
    40  // The order of output is not guaranteed between multiple builds running concurrently.
    41  type logAggregator interface {
    42  	// GetWriter returns an output writer tracked by the logAggregator
    43  	GetWriter(ctx context.Context) (w io.Writer, close func(), err error)
    44  	// PrintInOrder prints the output from each allotted writer in build order.
    45  	// It blocks until the instantiated capacity of io writers have been all allotted and closed, or the context is cancelled.
    46  	PrintInOrder(ctx context.Context)
    47  }
    48  
    49  type logAggregatorImpl struct {
    50  	out        io.Writer
    51  	messages   chan chan string
    52  	size       int
    53  	capacity   int
    54  	countMutex sync.Mutex
    55  }
    56  
    57  func (l *logAggregatorImpl) GetWriter(ctx context.Context) (io.Writer, func(), error) {
    58  	if err := l.checkCapacity(); err != nil {
    59  		return nil, nil, err
    60  	}
    61  	r, w := io.Pipe()
    62  
    63  	writer := io.Writer(w)
    64  	if output.IsColorable(l.out) {
    65  		writer = output.GetWriter(ctx, writer, output.DefaultColorCode, false, false)
    66  	}
    67  	ch := make(chan string, buffSize)
    68  	l.messages <- ch
    69  	// write the build output to a buffered channel.
    70  	go l.writeToChannel(r, ch)
    71  	return writer, func() { w.Close() }, nil
    72  }
    73  
    74  func (l *logAggregatorImpl) PrintInOrder(ctx context.Context) {
    75  	go func() {
    76  		<-ctx.Done()
    77  		// we handle cancellation by passing a nil struct instead of closing the channel.
    78  		// This makes it easier to flush all pending messages on the buffered channel before returning and avoid any race with pending requests for new writers.
    79  		l.messages <- nil
    80  	}()
    81  	for i := 0; i < l.capacity; i++ {
    82  		ch := <-l.messages
    83  		if ch == nil {
    84  			return
    85  		}
    86  		// read from each build's message channel and write to the given output.
    87  		printResult(l.out, ch)
    88  	}
    89  }
    90  
    91  func (l *logAggregatorImpl) checkCapacity() error {
    92  	l.countMutex.Lock()
    93  	defer l.countMutex.Unlock()
    94  	if l.size == l.capacity {
    95  		return fmt.Errorf("failed to create writer: capacity exceeded")
    96  	}
    97  	l.size++
    98  	return nil
    99  }
   100  
   101  func printResult(out io.Writer, output chan string) {
   102  	for line := range output {
   103  		fmt.Fprintln(out, line)
   104  	}
   105  }
   106  
   107  func (l *logAggregatorImpl) writeToChannel(r io.Reader, lines chan string) {
   108  	scanner := bufio.NewScanner(r)
   109  	for scanner.Scan() {
   110  		lines <- scanner.Text()
   111  	}
   112  	if scanner.Err() != nil {
   113  		log.Entry(context.TODO()).Errorf("error occurred retrieving build logs: %v", scanner.Err())
   114  	}
   115  	close(lines)
   116  }
   117  
   118  // noopLogAggregatorImpl simply returns a single stored io.Writer, usually `os.Stdout` for every request.
   119  // This is useful when builds are sequential and logs can be outputted to standard output with color formatting.
   120  type noopLogAggregatorImpl struct {
   121  	out io.Writer
   122  }
   123  
   124  func (n *noopLogAggregatorImpl) GetWriter(context.Context) (io.Writer, func(), error) {
   125  	return n.out, func() {}, nil
   126  }
   127  
   128  func (n *noopLogAggregatorImpl) PrintInOrder(context.Context) {}
   129  
   130  func newLogAggregator(out io.Writer, capacity int, concurrency int) logAggregator {
   131  	if concurrency == 1 {
   132  		return &noopLogAggregatorImpl{out: out}
   133  	}
   134  	return &logAggregatorImpl{out: out, capacity: capacity, messages: make(chan chan string, capacity)}
   135  }
   136  
   137  // ArtifactStore stores the results of each artifact build.
   138  type ArtifactStore interface {
   139  	Record(a *latest.Artifact, tag string)
   140  	GetImageTag(imageName string) (tag string, found bool)
   141  	GetArtifacts(s []*latest.Artifact) ([]graph.Artifact, error)
   142  }
   143  
   144  func NewArtifactStore() ArtifactStore {
   145  	return &artifactStoreImpl{m: new(sync.Map)}
   146  }
   147  
   148  type artifactStoreImpl struct {
   149  	m *sync.Map
   150  }
   151  
   152  func (ba *artifactStoreImpl) Record(a *latest.Artifact, tag string) {
   153  	ba.m.Store(a.ImageName, tag)
   154  }
   155  
   156  func (ba *artifactStoreImpl) GetImageTag(imageName string) (string, bool) {
   157  	v, ok := ba.m.Load(imageName)
   158  	if !ok {
   159  		return "", false
   160  	}
   161  	t, ok := v.(string)
   162  	if !ok {
   163  		log.Entry(context.TODO()).Fatalf("invalid build output recorded for image %s", imageName)
   164  	}
   165  	return t, true
   166  }
   167  
   168  func (ba *artifactStoreImpl) GetArtifacts(s []*latest.Artifact) ([]graph.Artifact, error) {
   169  	var builds []graph.Artifact
   170  	for _, a := range s {
   171  		t, found := ba.GetImageTag(a.ImageName)
   172  		if !found {
   173  			return nil, fmt.Errorf("failed to retrieve build result for image %s", a.ImageName)
   174  		}
   175  		builds = append(builds, graph.Artifact{ImageName: a.ImageName, Tag: t})
   176  	}
   177  	return builds, nil
   178  }