github.com/Cloud-Foundations/Dominator@v0.3.4/imagebuilder/builder/build.go (about) 1 package builder 2 3 import ( 4 "bytes" 5 stderrors "errors" 6 "fmt" 7 "io" 8 "time" 9 10 buildclient "github.com/Cloud-Foundations/Dominator/imagebuilder/client" 11 "github.com/Cloud-Foundations/Dominator/imagebuilder/logarchiver" 12 imgclient "github.com/Cloud-Foundations/Dominator/imageserver/client" 13 "github.com/Cloud-Foundations/Dominator/lib/backoffdelay" 14 "github.com/Cloud-Foundations/Dominator/lib/errors" 15 "github.com/Cloud-Foundations/Dominator/lib/format" 16 "github.com/Cloud-Foundations/Dominator/lib/image" 17 "github.com/Cloud-Foundations/Dominator/lib/retry" 18 "github.com/Cloud-Foundations/Dominator/lib/srpc" 19 "github.com/Cloud-Foundations/Dominator/lib/srpc/retryclient" 20 "github.com/Cloud-Foundations/Dominator/lib/url/urlutil" 21 proto "github.com/Cloud-Foundations/Dominator/proto/imaginator" 22 ) 23 24 type dualBuildLogger struct { 25 buffer *bytes.Buffer 26 writer io.Writer 27 } 28 29 type buildErrorType struct { 30 error string 31 needSourceImage bool 32 sourceImage string 33 sourceImageBuildVariables map[string]string 34 sourceImageGitCommitId string 35 } 36 37 func copyClientLogs(clientAddress string, keepSlave bool, buildError error, 38 buildLog io.Writer) { 39 fmt.Fprintln(buildLog, 40 "*********************************************************************") 41 fmt.Fprintf(buildLog, "Logs for slave: %s, keepSlave: %v, err: %v\n", 42 clientAddress, keepSlave, buildError) 43 readCloser, err := urlutil.Open( 44 fmt.Sprintf("http://%s/logs/dump?name=latest", clientAddress)) 45 if err != nil { 46 fmt.Fprintf(buildLog, "Error opening logs: %s\n", err) 47 return 48 } 49 defer readCloser.Close() 50 io.Copy(buildLog, readCloser) 51 fmt.Fprintln(buildLog, "End logs for slave") 52 fmt.Fprintln(buildLog, 53 "*********************************************************************") 54 } 55 56 func dialServer(address string, retryTimeout time.Duration) ( 57 srpc.ClientI, error) { 58 return retryclient.DialHTTP(retryclient.Params{ 59 Address: address, 60 KeepAlive: true, 61 KeepAlivePeriod: time.Minute, 62 Network: "tcp", 63 Params: retry.Params{ 64 RetryTimeout: retryTimeout, 65 Sleeper: backoffdelay.NewExponential(0, 0, 1), 66 }, 67 }) 68 } 69 70 // checkToAutoExtend will return true if auto building images should have their 71 // lifetimes extended rather than built. Some conditions where it returns true: 72 // - Auto building has been disabled. 73 // - A slave driver is configured but failes to provide a slave in time 74 func (b *Builder) checkToAutoExtend() bool { 75 b.disableLock.RLock() 76 disableUntil := b.disableAutoBuildsUntil 77 b.disableLock.RUnlock() 78 if duration := time.Until(disableUntil); duration > 0 { 79 b.logger.Printf("Auto rebuilds disabled until %s (for %s), will extend image lifetimes\n", 80 disableUntil.Format(format.TimeFormatSeconds), 81 format.Duration(duration)) 82 return true 83 } 84 if b.slaveDriver == nil { 85 return false 86 } 87 slave, err := b.slaveDriver.GetSlaveWithTimeout(b.createSlaveTimeout) 88 if err == nil { 89 slave.Release() 90 return false 91 } 92 b.logger.Printf("Error getting slave: %s, will extend image lifetimes\n", 93 err) 94 return true 95 } 96 97 func (b *Builder) build(client srpc.ClientI, request proto.BuildImageRequest, 98 authInfo *srpc.AuthInformation, 99 logWriter io.Writer) (*image.Image, string, error) { 100 startTime := time.Now() 101 builder := b.getImageBuilderWithReload(request.StreamName) 102 if builder == nil { 103 return nil, "", errors.New("unknown stream: " + request.StreamName) 104 } 105 if err := b.checkPermission(builder, request, authInfo); err != nil { 106 return nil, "", err 107 } 108 buildLogBuffer := &bytes.Buffer{} 109 buildInfo := ¤tBuildInfo{ 110 buffer: buildLogBuffer, 111 startedAt: time.Now(), 112 } 113 b.buildResultsLock.Lock() 114 b.currentBuildInfos[request.StreamName] = buildInfo 115 b.buildResultsLock.Unlock() 116 var buildLog buildLogger 117 if logWriter == nil { 118 buildLog = buildLogBuffer 119 } else { 120 buildLog = &dualBuildLogger{ 121 buffer: buildLogBuffer, 122 writer: io.MultiWriter(buildLogBuffer, logWriter), 123 } 124 } 125 img, name, err := b.buildWithLogger(builder, client, request, authInfo, 126 startTime, &buildInfo.slaveAddress, buildLog) 127 finishTime := time.Now() 128 b.buildResultsLock.Lock() 129 defer b.buildResultsLock.Unlock() 130 delete(b.currentBuildInfos, request.StreamName) 131 b.lastBuildResults[request.StreamName] = buildResultType{ 132 name, startTime, finishTime, buildLog.Bytes(), err} 133 buildLogInfo := logarchiver.BuildInfo{ 134 Duration: finishTime.Sub(startTime), 135 Error: errors.ErrorToString(err), 136 } 137 buildLogImageName := name 138 if buildLogImageName == "" { 139 buildLogImageName = makeImageName(request.StreamName) 140 } 141 if authInfo != nil { 142 buildLogInfo.RequestorUsername = authInfo.Username 143 } 144 archiveError := b.buildLogArchiver.AddBuildLog(buildLogImageName, 145 buildLogInfo, buildLog.Bytes()) 146 if archiveError != nil { 147 b.logger.Printf("Error archiving build log: %s\n", archiveError) 148 } 149 if err == nil { 150 b.logger.Printf("Built image for stream: %s in %s\n", 151 request.StreamName, format.Duration(finishTime.Sub(startTime))) 152 } 153 return img, name, err 154 } 155 156 func (b *Builder) buildImage(request proto.BuildImageRequest, 157 authInfo *srpc.AuthInformation, 158 logWriter io.Writer) (*image.Image, string, error) { 159 b.disableLock.RLock() 160 disableUntil := b.disableBuildRequestsUntil 161 b.disableLock.RUnlock() 162 if duration := time.Until(disableUntil); duration > 0 { 163 return nil, "", fmt.Errorf("builds disabled until %s (for %s)", 164 disableUntil.Format(format.TimeFormatSeconds), 165 format.Duration(duration)) 166 } 167 if request.ExpiresIn < b.minimumExpiration { 168 return nil, "", fmt.Errorf("minimum expiration duration is %s", 169 format.Duration(b.minimumExpiration)) 170 } 171 if err := b.WaitForStreamsLoaded(time.Minute); err != nil { 172 return nil, "", err 173 } 174 client, err := dialServer(b.imageServerAddress, time.Minute) 175 if err != nil { 176 return nil, "", err 177 } 178 defer client.Close() 179 img, name, err := b.build(client, request, authInfo, logWriter) 180 if request.ReturnImage { 181 return img, "", err 182 } 183 return nil, name, err 184 } 185 186 func (b *Builder) buildLocal(builder imageBuilder, client srpc.ClientI, 187 request proto.BuildImageRequest, authInfo *srpc.AuthInformation, 188 buildLog buildLogger) (*image.Image, error) { 189 // Check the namespace to make sure it hasn't changed. This is to catch 190 // golang bugs. 191 currentNamespace, err := getNamespace() 192 if err != nil { 193 fmt.Fprintln(buildLog, err) 194 return nil, err 195 } 196 if currentNamespace != b.initialNamespace { 197 err := fmt.Errorf("namespace changed from: %s to: %s", 198 b.initialNamespace, currentNamespace) 199 fmt.Fprintln(buildLog, err) 200 return nil, err 201 } 202 if authInfo == nil { 203 b.logger.Printf("Auto building image for stream: %s\n", 204 request.StreamName) 205 } else { 206 b.logger.Printf("%s requested building image for stream: %s\n", 207 authInfo.Username, request.StreamName) 208 } 209 img, err := builder.build(b, client, request, buildLog) 210 if err != nil { 211 fmt.Fprintf(buildLog, "Error building image: %s\n", err) 212 return nil, err 213 } 214 return img, nil 215 } 216 217 func (b *Builder) buildOnSlave(client srpc.ClientI, 218 request proto.BuildImageRequest, authInfo *srpc.AuthInformation, 219 slaveAddress *string, buildLog buildLogger) (*image.Image, error) { 220 request.DisableRecursiveBuild = true 221 request.ReturnImage = true 222 request.StreamBuildLog = true 223 if len(request.Variables) < 1 { 224 request.Variables = b.variables 225 } else if len(b.variables) > 0 { 226 variables := make(map[string]string, 227 len(b.variables)+len(request.Variables)) 228 for key, value := range b.variables { 229 variables[key] = value 230 } 231 for key, value := range request.Variables { 232 variables[key] = value 233 } 234 request.Variables = variables 235 } 236 slave, err := b.slaveDriver.GetSlaveWithTimeout(b.createSlaveTimeout) 237 if err != nil { 238 return nil, fmt.Errorf("error getting slave: %s", err) 239 } 240 *slaveAddress = slave.GetClientAddress() 241 keepSlave := false 242 defer func() { 243 if keepSlave { 244 slave.Release() 245 } else { 246 slave.Destroy() 247 } 248 }() 249 if authInfo == nil { 250 b.logger.Printf("Auto building image on %s for stream: %s\n", 251 slave, request.StreamName) 252 fmt.Fprintf(buildLog, "Auto building image on %s for stream: %s\n", 253 slave, request.StreamName) 254 } else { 255 b.logger.Printf("%s requested building image on %s for stream: %s\n", 256 authInfo.Username, slave, request.StreamName) 257 fmt.Fprintf(buildLog, 258 "%s requested building image on %s for stream: %s\n", 259 authInfo.Username, slave, request.StreamName) 260 } 261 var reply proto.BuildImageResponse 262 err = buildclient.BuildImage(slave.GetClient(), request, &reply, buildLog) 263 copyClientLogs(slave.GetClientAddress(), keepSlave, err, buildLog) 264 if err != nil { 265 if reply.NeedSourceImage { 266 return nil, &buildErrorType{ 267 error: err.Error(), 268 needSourceImage: true, 269 sourceImage: reply.SourceImage, 270 sourceImageBuildVariables: reply.SourceImageBuildVariables, 271 sourceImageGitCommitId: reply.SourceImageGitCommitId, 272 } 273 } 274 return nil, err 275 } 276 return reply.Image, nil 277 } 278 279 func (b *Builder) buildSomewhere(builder imageBuilder, client srpc.ClientI, 280 request proto.BuildImageRequest, authInfo *srpc.AuthInformation, 281 slaveAddress *string, buildLog buildLogger) (*image.Image, error) { 282 if b.slaveDriver == nil { 283 return b.buildLocal(builder, client, request, authInfo, buildLog) 284 } else { 285 return b.buildOnSlave(client, request, authInfo, slaveAddress, buildLog) 286 } 287 } 288 289 func (b *Builder) buildWithLogger(builder imageBuilder, client srpc.ClientI, 290 request proto.BuildImageRequest, authInfo *srpc.AuthInformation, 291 startTime time.Time, slaveAddress *string, 292 buildLog buildLogger) (*image.Image, string, error) { 293 img, err := b.buildSomewhere(builder, client, request, authInfo, 294 slaveAddress, buildLog) 295 if err != nil { 296 var buildError *buildErrorType 297 if stderrors.As(err, &buildError) && buildError.needSourceImage { 298 if request.DisableRecursiveBuild { 299 return nil, "", err 300 } 301 // Try to build source image. 302 expiresIn := time.Hour 303 if request.ExpiresIn > 0 { 304 expiresIn = request.ExpiresIn 305 } 306 variables := variablesGetter(buildError.sourceImageBuildVariables) 307 variables.merge(request.Variables) 308 sourceReq := proto.BuildImageRequest{ 309 ExpiresIn: expiresIn, 310 GitBranch: buildError.sourceImageGitCommitId, 311 MaxSourceAge: request.MaxSourceAge, 312 StreamName: buildError.sourceImage, 313 Variables: variables, 314 } 315 if _, _, e := b.build(client, sourceReq, nil, buildLog); e != nil { 316 return nil, "", e 317 } 318 img, err = b.buildSomewhere(builder, client, request, authInfo, 319 slaveAddress, buildLog) 320 } 321 } 322 if err != nil { 323 return nil, "", err 324 } 325 if request.ReturnImage { 326 return img, "", nil 327 } 328 if authInfo != nil { 329 img.CreatedFor = authInfo.Username 330 } 331 uploadStartTime := time.Now() 332 if name, err := addImage(client, request, img); err != nil { 333 fmt.Fprintln(buildLog, err) 334 return nil, "", err 335 } else { 336 finishTime := time.Now() 337 fmt.Fprintf(buildLog, 338 "Uploaded %s in %s, total build duration: %s\n", 339 name, format.Duration(finishTime.Sub(uploadStartTime)), 340 format.Duration(finishTime.Sub(startTime))) 341 return img, name, nil 342 } 343 } 344 345 func (b *Builder) checkPermission(builder imageBuilder, 346 request proto.BuildImageRequest, authInfo *srpc.AuthInformation) error { 347 if authInfo == nil || authInfo.HaveMethodAccess { 348 if request.ExpiresIn > b.maximumExpirationPrivileged { 349 return fmt.Errorf("maximum expiration time is %s for you", 350 format.Duration(b.maximumExpirationPrivileged)) 351 } 352 return nil 353 } 354 if request.ExpiresIn > b.maximumExpiration { 355 return fmt.Errorf("maximum expiration time is %s", 356 format.Duration(b.maximumExpiration)) 357 } 358 if builder, ok := builder.(*imageStreamType); ok { 359 if _, ok := builder.builderUsers[authInfo.Username]; ok { 360 return nil 361 } 362 for _, group := range builder.BuilderGroups { 363 if _, ok := authInfo.GroupList[group]; ok { 364 return nil 365 } 366 } 367 } 368 return errors.New("no permission to build: " + request.StreamName) 369 } 370 371 func (b *Builder) disableAutoBuilds(disableFor time.Duration) ( 372 time.Time, error) { 373 if b.imageRebuildInterval > 0 { 374 if disableFor > b.imageRebuildInterval<<2 { 375 return time.Time{}, 376 fmt.Errorf("cannot disable auto building for more than: %s", 377 format.Duration(b.imageRebuildInterval<<2)) 378 } 379 } 380 b.disableLock.Lock() 381 defer b.disableLock.Unlock() 382 b.disableAutoBuildsUntil = time.Now().Add(disableFor) 383 return b.disableAutoBuildsUntil, nil 384 } 385 386 func (b *Builder) disableBuildRequests(disableFor time.Duration) ( 387 time.Time, error) { 388 if disableFor > 24*time.Hour { 389 return time.Time{}, 390 fmt.Errorf("cannot disable build requests for more than: 24h") 391 } 392 b.disableLock.Lock() 393 defer b.disableLock.Unlock() 394 b.disableBuildRequestsUntil = time.Now().Add(disableFor) 395 return b.disableBuildRequestsUntil, nil 396 } 397 398 // extendAutoRebuildImages will return true if image lifetimes were extended and 399 // thus auto rebuilding should be skipped. 400 func (b *Builder) extendAutoRebuildImages(client srpc.ClientI, 401 expiresIn time.Duration) bool { 402 if !b.checkToAutoExtend() { 403 return false 404 } 405 for _, streamName := range b.listStreamsToAutoRebuild() { 406 imageName, err := imgclient.FindLatestImage(client, streamName, false) 407 if err != nil { 408 b.logger.Printf("Error finding latest image for stream: %s: %s\n", 409 streamName, err) 410 continue 411 } 412 if imageName == "" { 413 continue 414 } 415 err = imgclient.ChangeImageExpiration(client, imageName, 416 time.Now().Add(expiresIn)) 417 if err != nil { 418 b.logger.Printf("Error extending expiration for image: %s: %s\n", 419 imageName, err) 420 } 421 } 422 return true 423 } 424 425 func (b *Builder) getCurrentBuildLog(streamName string) ([]byte, error) { 426 b.buildResultsLock.RLock() 427 defer b.buildResultsLock.RUnlock() 428 if result, ok := b.currentBuildInfos[streamName]; !ok { 429 return nil, errors.New("unknown image: " + streamName) 430 } else { 431 log := make([]byte, result.buffer.Len()) 432 copy(log, result.buffer.Bytes()) 433 return log, nil 434 } 435 } 436 437 func (b *Builder) getImageBuilder(streamName string) imageBuilder { 438 if stream := b.getBootstrapStream(streamName); stream != nil { 439 return stream 440 } 441 if stream := b.getNormalStream(streamName); stream != nil { 442 return stream 443 } 444 // Ensure a nil interface is returned, not a stream with value == nil. 445 return nil 446 } 447 448 func (b *Builder) getImageBuilderWithReload(streamName string) imageBuilder { 449 if stream := b.getImageBuilder(streamName); stream != nil { 450 return stream 451 } 452 if err := b.reloadNormalStreamsConfiguration(); err != nil { 453 b.logger.Printf("Error reloading configuration: %s\n", err) 454 return nil 455 } 456 return b.getImageBuilder(streamName) 457 } 458 459 func (b *Builder) getLatestBuildLog(streamName string) ([]byte, error) { 460 b.buildResultsLock.RLock() 461 defer b.buildResultsLock.RUnlock() 462 if result, ok := b.lastBuildResults[streamName]; !ok { 463 return nil, errors.New("unknown image: " + streamName) 464 } else { 465 log := make([]byte, len(result.buildLog)) 466 copy(log, result.buildLog) 467 return log, nil 468 } 469 } 470 471 func (b *Builder) rebuildImage(client srpc.ClientI, streamName string, 472 expiresIn time.Duration) { 473 _, _, err := b.build(client, proto.BuildImageRequest{ 474 StreamName: streamName, 475 ExpiresIn: expiresIn, 476 }, 477 nil, nil) 478 if err == nil { 479 return 480 } 481 imageName, e := imgclient.FindLatestImage(client, streamName, false) 482 if e != nil { 483 b.logger.Printf("Error finding latest image: %s: %s\n", streamName, e) 484 } else if imageName != "" { 485 e := imgclient.ChangeImageExpiration(client, imageName, 486 time.Now().Add(expiresIn)) 487 if e == nil { 488 b.logger.Printf("Error building image: %s: %s, extended: %s\n", 489 streamName, err, imageName) 490 return 491 } 492 b.logger.Printf( 493 "Error building image: %s: %s, failed to extend: %s: %s\n", 494 streamName, err, imageName, e) 495 return 496 } 497 b.logger.Printf("Error building image: %s: %s\n", streamName, err) 498 } 499 500 func (b *Builder) rebuildImages(minInterval time.Duration) { 501 if minInterval < 1 { 502 return 503 } 504 client, _ := dialServer(b.imageServerAddress, 0) 505 var sleepUntil time.Time 506 for ; ; time.Sleep(time.Until(sleepUntil)) { 507 b.logger.Println("Starting automatic image build cycle") 508 startTime := time.Now() 509 sleepUntil = startTime.Add(minInterval) 510 if b.extendAutoRebuildImages(client, minInterval*2) { 511 continue 512 } 513 for _, streamName := range b.listStreamsToAutoRebuild() { 514 b.rebuildImage(client, streamName, minInterval*2) 515 } 516 b.logger.Printf("Completed automatic image build cycle in %s\n", 517 format.Duration(time.Since(startTime))) 518 } 519 } 520 521 func (err *buildErrorType) Error() string { 522 return err.error 523 } 524 525 func (bl *dualBuildLogger) Bytes() []byte { 526 return bl.buffer.Bytes() 527 } 528 529 func (bl *dualBuildLogger) Write(p []byte) (int, error) { 530 return bl.writer.Write(p) 531 }