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 }