github.com/Cloud-Foundations/Dominator@v0.3.4/imagebuilder/builder/getDirectedGraph.go (about)

     1  package builder
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strings"
    10  	"sync"
    11  	"time"
    12  
    13  	"github.com/Cloud-Foundations/Dominator/lib/concurrent"
    14  	"github.com/Cloud-Foundations/Dominator/lib/errors"
    15  	"github.com/Cloud-Foundations/Dominator/lib/format"
    16  	proto "github.com/Cloud-Foundations/Dominator/proto/imaginator"
    17  )
    18  
    19  type dependencyResultType struct {
    20  	fetchLog           []byte
    21  	fetchTime          time.Duration
    22  	resultTime         time.Time
    23  	streamToSource     map[string]string // K: stream name, V: source stream.
    24  	unbuildableSources map[string]struct{}
    25  }
    26  
    27  func isMatch(streamName string, patterns []string) bool {
    28  	for _, pattern := range patterns {
    29  		if len(streamName) < len(pattern) {
    30  			continue
    31  		}
    32  		if streamName == pattern {
    33  			return true
    34  		}
    35  		if len(streamName) <= len(pattern) {
    36  			continue
    37  		}
    38  		if streamName[len(pattern)] != '/' {
    39  			continue
    40  		}
    41  		if strings.HasPrefix(streamName, pattern) {
    42  			return true
    43  		}
    44  	}
    45  	return false
    46  }
    47  
    48  // computeExcludes will compute the set of excluded image streams. Dependent
    49  // streams are also excluded.
    50  func computeExcludes(streamToSource map[string]string,
    51  	bootstrapStreams []string, excludes []string,
    52  	includes []string) map[string]struct{} {
    53  	if len(excludes) < 1 && len(includes) < 1 {
    54  		return nil
    55  	}
    56  	allStreams := make(map[string]struct{})
    57  	excludedStreams := make(map[string]struct{})
    58  	streamToDependents := make(map[string][]string)
    59  	for _, stream := range bootstrapStreams {
    60  		allStreams[stream] = struct{}{}
    61  	}
    62  	for stream, source := range streamToSource {
    63  		allStreams[stream] = struct{}{}
    64  		streamToDependents[source] = append(streamToDependents[source], stream)
    65  	}
    66  	if len(excludes) > 0 {
    67  		for streamName := range allStreams {
    68  			if isMatch(streamName, excludes) {
    69  				walkDependents(streamToDependents, streamName,
    70  					func(name string) {
    71  						excludedStreams[name] = struct{}{}
    72  					})
    73  			}
    74  		}
    75  	}
    76  	if len(includes) < 1 {
    77  		return excludedStreams
    78  	}
    79  	includedStreams := make(map[string]struct{})
    80  	for streamName := range allStreams {
    81  		if isMatch(streamName, includes) {
    82  			walkDependents(streamToDependents, streamName,
    83  				func(name string) {
    84  					includedStreams[name] = struct{}{}
    85  				})
    86  			walkParents(streamToSource, streamName,
    87  				func(name string) {
    88  					includedStreams[name] = struct{}{}
    89  				})
    90  		}
    91  	}
    92  	for streamName := range allStreams {
    93  		if _, ok := includedStreams[streamName]; !ok {
    94  			excludedStreams[streamName] = struct{}{}
    95  		}
    96  	}
    97  	return excludedStreams
    98  }
    99  
   100  func walkDependents(streamToDependents map[string][]string, streamName string,
   101  	fn func(string)) {
   102  	for _, name := range streamToDependents[streamName] {
   103  		walkDependents(streamToDependents, name, fn)
   104  	}
   105  	fn(streamName)
   106  }
   107  
   108  func walkParents(streamToSource map[string]string, streamName string,
   109  	fn func(string)) {
   110  	if name, ok := streamToSource[streamName]; ok {
   111  		walkParents(streamToSource, name, fn)
   112  		fn(name)
   113  	}
   114  }
   115  
   116  func (b *Builder) dependencyGeneratorLoop(
   117  	generateDependencyTrigger <-chan chan<- struct{}) {
   118  	interval := time.Hour // The first configuration load should happen first.
   119  	timer := time.NewTimer(interval)
   120  	for {
   121  		var wakeChannel chan<- struct{}
   122  		select {
   123  		case wakeChannel = <-generateDependencyTrigger:
   124  			if !timer.Stop() {
   125  				<-timer.C
   126  			}
   127  		case <-timer.C:
   128  		}
   129  		dependencyResult, err := b.generateDependencyData()
   130  		if dependencyResult == nil {
   131  			dependencyResult = &dependencyResultType{fetchTime: 6 * time.Second}
   132  		}
   133  		if err != nil {
   134  			b.logger.Printf("failed to generate dependencies: %s\n", err)
   135  			dependencyData := dependencyDataType{
   136  				lastAttemptError:    err,
   137  				lastAttemptFetchLog: dependencyResult.fetchLog,
   138  				lastAttemptTime:     dependencyResult.resultTime,
   139  			}
   140  			b.dependencyDataLock.Lock()
   141  			if oldData := b.dependencyData; oldData != nil {
   142  				dependencyData.generatedAt = oldData.generatedAt
   143  				dependencyData.streamToSource = oldData.streamToSource
   144  				dependencyData.unbuildableSources = oldData.unbuildableSources
   145  			}
   146  			b.dependencyData = &dependencyData
   147  			b.dependencyDataLock.Unlock()
   148  		} else {
   149  			dependencyData := dependencyDataType{
   150  				generatedAt:         dependencyResult.resultTime,
   151  				lastAttemptFetchLog: dependencyResult.fetchLog,
   152  				lastAttemptTime:     dependencyResult.resultTime,
   153  				streamToSource:      dependencyResult.streamToSource,
   154  				unbuildableSources:  dependencyResult.unbuildableSources,
   155  			}
   156  			b.dependencyDataLock.Lock()
   157  			b.dependencyData = &dependencyData
   158  			b.dependencyDataLock.Unlock()
   159  		}
   160  		if wakeChannel != nil {
   161  			wakeChannel <- struct{}{}
   162  		}
   163  		interval = dependencyResult.fetchTime * 10
   164  		if interval < 10*time.Second {
   165  			interval = 10 * time.Second
   166  		}
   167  		for keepDraining := true; keepDraining; {
   168  			select {
   169  			case wakeChannel := <-generateDependencyTrigger:
   170  				if wakeChannel != nil {
   171  					wakeChannel <- struct{}{}
   172  				}
   173  			default:
   174  				keepDraining = false
   175  			}
   176  		}
   177  		timer.Reset(interval)
   178  	}
   179  }
   180  
   181  func (b *Builder) generateDependencyData() (*dependencyResultType, error) {
   182  	var directoriesToRemove []string
   183  	defer func() {
   184  		for _, directory := range directoriesToRemove {
   185  			os.RemoveAll(directory)
   186  		}
   187  	}()
   188  	streamToSource := make(map[string]string) // K: stream name, V: source.
   189  	urlToDirectory := make(map[string]string)
   190  	streamNames := b.listNormalStreamNames()
   191  	startTime := time.Now()
   192  	streams := make(map[string]*imageStreamType, len(streamNames))
   193  	// First pass to process local manifests and start Git fetches.
   194  	state := concurrent.NewState(0)
   195  	var lock sync.Mutex
   196  	fetchLog := bytes.NewBuffer(nil)
   197  	var serialisedFetchTime time.Duration
   198  	for _, streamName := range streamNames {
   199  		b.streamsLock.RLock()
   200  		stream := b.imageStreams[streamName]
   201  		b.streamsLock.RUnlock()
   202  		if stream == nil {
   203  			return nil, fmt.Errorf("stream: %s does not exist", streamName)
   204  		}
   205  		streams[streamName] = stream
   206  		manifestLocation := stream.getManifestLocation(b, nil)
   207  		if _, ok := urlToDirectory[manifestLocation.url]; ok {
   208  			continue // Git fetch has started.
   209  		} else if rootDir, err := urlToLocal(manifestLocation.url); err != nil {
   210  			return nil, err
   211  		} else if rootDir != "" {
   212  			manifestConfig, err := readManifestFile(
   213  				filepath.Join(rootDir, manifestLocation.directory), stream)
   214  			if err != nil {
   215  				return nil, err
   216  			}
   217  			streamToSource[streamName] = manifestConfig.SourceImage
   218  			delete(streams, streamName) // Mark as completed.
   219  		} else {
   220  			gitRoot, err := makeTempDirectory("",
   221  				strings.Replace(streamName, "/", "_", -1)+".manifest")
   222  			if err != nil {
   223  				return nil, err
   224  			}
   225  			directoriesToRemove = append(directoriesToRemove, gitRoot)
   226  			state.GoRun(func() error {
   227  				myFetchLog := bytes.NewBuffer(nil)
   228  				startTime := time.Now()
   229  				err := gitShallowClone(gitRoot, manifestLocation.url,
   230  					stream.ManifestUrl, "master", []string{"**/manifest"},
   231  					myFetchLog)
   232  				lock.Lock()
   233  				fetchLog.Write(myFetchLog.Bytes())
   234  				serialisedFetchTime += time.Since(startTime)
   235  				lock.Unlock()
   236  				return err
   237  			})
   238  			urlToDirectory[manifestLocation.url] = gitRoot // Mark fetch started
   239  		}
   240  	}
   241  	if err := state.Reap(); err != nil {
   242  		return &dependencyResultType{
   243  			fetchLog:   fetchLog.Bytes(),
   244  			fetchTime:  serialisedFetchTime,
   245  			resultTime: time.Now(),
   246  		}, err
   247  	}
   248  	// Second pass to process fetched manifests.
   249  	for streamName, stream := range streams {
   250  		manifestLocation := stream.getManifestLocation(b, nil)
   251  		manifestConfig, err := readManifestFile(
   252  			filepath.Join(urlToDirectory[manifestLocation.url],
   253  				manifestLocation.directory), stream)
   254  		if err != nil {
   255  			return nil, err
   256  		}
   257  		streamToSource[streamName] = manifestConfig.SourceImage
   258  	}
   259  	fmt.Fprintf(fetchLog, "Cumulative fetch time: %s\n",
   260  		format.Duration(serialisedFetchTime))
   261  	unbuildableSources := make(map[string]struct{})
   262  	for streamName, sourceName := range streamToSource {
   263  		if _, ok := streamToSource[sourceName]; ok {
   264  			continue
   265  		}
   266  		if b.getBootstrapStream(sourceName) != nil {
   267  			continue
   268  		}
   269  		unbuildableSources[sourceName] = struct{}{}
   270  		if b.getNumBootstrapStreams() > 0 {
   271  			b.logger.Printf("stream: %s has unbuildable source: %s\n",
   272  				streamName, sourceName)
   273  		}
   274  	}
   275  	finishedTime := time.Now()
   276  	timeTaken := format.Duration(finishedTime.Sub(startTime))
   277  	b.logger.Debugf(0, "generated dependencies in: %s (fetch: %s)\n",
   278  		timeTaken, format.Duration(serialisedFetchTime))
   279  	fmt.Fprintf(fetchLog, "Generated dependencies in: %s\n", timeTaken)
   280  	return &dependencyResultType{
   281  		fetchLog:           fetchLog.Bytes(),
   282  		fetchTime:          serialisedFetchTime,
   283  		resultTime:         finishedTime,
   284  		streamToSource:     streamToSource,
   285  		unbuildableSources: unbuildableSources,
   286  	}, nil
   287  }
   288  
   289  func (b *Builder) getDependencies(request proto.GetDependenciesRequest) (
   290  	proto.GetDependenciesResult, error) {
   291  	dependencyData := b.getDependencyData(request.MaxAge)
   292  	if dependencyData == nil {
   293  		return proto.GetDependenciesResult{}, nil
   294  	}
   295  	lastErrorString := errors.ErrorToString(dependencyData.lastAttemptError)
   296  	return proto.GetDependenciesResult{
   297  		FetchLog:           dependencyData.lastAttemptFetchLog,
   298  		GeneratedAt:        dependencyData.generatedAt,
   299  		LastAttemptAt:      dependencyData.lastAttemptTime,
   300  		LastAttemptError:   lastErrorString,
   301  		StreamToSource:     dependencyData.streamToSource,
   302  		UnbuildableSources: dependencyData.unbuildableSources,
   303  	}, nil
   304  
   305  }
   306  
   307  // getDependencyData returns the dependency data (possibly nil). If maxAge is
   308  // larger than zero, getDependencyData will wait until there is an attempt less
   309  // than maxAge ago.
   310  func (b *Builder) getDependencyData(maxAge time.Duration) *dependencyDataType {
   311  	if maxAge <= 0 {
   312  		b.dependencyDataLock.RLock()
   313  		dependencyData := b.dependencyData
   314  		b.dependencyDataLock.RUnlock()
   315  		return dependencyData
   316  	}
   317  	if maxAge < 2*time.Second {
   318  		maxAge = 2 * time.Second
   319  	}
   320  	for {
   321  		b.dependencyDataLock.RLock()
   322  		dependencyData := b.dependencyData
   323  		b.dependencyDataLock.RUnlock()
   324  		if time.Since(dependencyData.lastAttemptTime) < maxAge {
   325  			return dependencyData
   326  		}
   327  		waitChannel := make(chan struct{}, 1)
   328  		b.generateDependencyTrigger <- waitChannel // Trigger and wait.
   329  		<-waitChannel
   330  	}
   331  }
   332  
   333  func (b *Builder) getDirectedGraph(request proto.GetDirectedGraphRequest) (
   334  	proto.GetDirectedGraphResult, error) {
   335  	dependencyData := b.getDependencyData(request.MaxAge)
   336  	if dependencyData == nil {
   337  		return proto.GetDirectedGraphResult{}, nil
   338  	}
   339  	lastErrorString := errors.ErrorToString(dependencyData.lastAttemptError)
   340  	if dependencyData.generatedAt.IsZero() {
   341  		return proto.GetDirectedGraphResult{
   342  			FetchLog:         dependencyData.lastAttemptFetchLog,
   343  			LastAttemptAt:    dependencyData.lastAttemptTime,
   344  			LastAttemptError: lastErrorString,
   345  		}, nil
   346  	}
   347  	bootstrapStreams := b.listBootstrapStreamNames()
   348  	excludedStreams := computeExcludes(dependencyData.streamToSource,
   349  		bootstrapStreams, request.Excludes, request.Includes)
   350  	streamNames := make([]string, 0, len(dependencyData.streamToSource))
   351  	for streamName := range dependencyData.streamToSource {
   352  		if _, ok := excludedStreams[streamName]; !ok {
   353  			streamNames = append(streamNames, streamName)
   354  		}
   355  	}
   356  	sort.Strings(streamNames) // For consistent output.
   357  	buffer := bytes.NewBuffer(nil)
   358  	fmt.Fprintln(buffer, "digraph all {")
   359  	for _, streamName := range streamNames {
   360  		fmt.Fprintf(buffer, "  \"%s\" -> \"%s\"\n",
   361  			streamName, dependencyData.streamToSource[streamName])
   362  	}
   363  	// Mark streams with no source in red, to show they are unbuildable.
   364  	for streamName := range dependencyData.unbuildableSources {
   365  		if _, ok := excludedStreams[streamName]; !ok {
   366  			fmt.Fprintf(buffer, "  \"%s\" [fontcolor=red]\n", streamName)
   367  		}
   368  	}
   369  	// Mark streams which are auto rebuilt in bold.
   370  	for _, streamName := range bootstrapStreams {
   371  		if _, ok := excludedStreams[streamName]; !ok {
   372  			fmt.Fprintf(buffer, "  \"%s\" [style=bold]\n", streamName)
   373  		}
   374  	}
   375  	for _, streamName := range b.imageStreamsToAutoRebuild {
   376  		if _, ok := excludedStreams[streamName]; !ok {
   377  			fmt.Fprintf(buffer, "  \"%s\" [style=bold]\n", streamName)
   378  		}
   379  	}
   380  	fmt.Fprintln(buffer, "}")
   381  	return proto.GetDirectedGraphResult{
   382  		FetchLog:         dependencyData.lastAttemptFetchLog,
   383  		GeneratedAt:      dependencyData.generatedAt,
   384  		GraphvizDot:      buffer.Bytes(),
   385  		LastAttemptAt:    dependencyData.lastAttemptTime,
   386  		LastAttemptError: lastErrorString,
   387  	}, nil
   388  }
   389  
   390  func (b *Builder) triggerDependencyDataGeneration() {
   391  	select {
   392  	case b.generateDependencyTrigger <- nil:
   393  	default:
   394  	}
   395  }