github.com/Cloud-Foundations/Dominator@v0.3.4/imagebuilder/builder/image.go (about) 1 package builder 2 3 import ( 4 "encoding/json" 5 "errors" 6 "fmt" 7 "io" 8 "io/ioutil" 9 stdlog "log" 10 "net/url" 11 "os" 12 "os/exec" 13 "path/filepath" 14 "strings" 15 "syscall" 16 "time" 17 18 "github.com/Cloud-Foundations/Dominator/lib/filesystem" 19 "github.com/Cloud-Foundations/Dominator/lib/filesystem/util" 20 "github.com/Cloud-Foundations/Dominator/lib/filter" 21 "github.com/Cloud-Foundations/Dominator/lib/format" 22 "github.com/Cloud-Foundations/Dominator/lib/fsutil" 23 "github.com/Cloud-Foundations/Dominator/lib/gitutil" 24 "github.com/Cloud-Foundations/Dominator/lib/image" 25 libjson "github.com/Cloud-Foundations/Dominator/lib/json" 26 objectclient "github.com/Cloud-Foundations/Dominator/lib/objectserver/client" 27 "github.com/Cloud-Foundations/Dominator/lib/srpc" 28 "github.com/Cloud-Foundations/Dominator/lib/tags" 29 "github.com/Cloud-Foundations/Dominator/lib/triggers" 30 proto "github.com/Cloud-Foundations/Dominator/proto/imaginator" 31 ) 32 33 type gitInfoType struct { 34 branch string 35 commitId string 36 gitUrl string 37 } 38 39 func (stream *imageStreamType) build(b *Builder, client srpc.ClientI, 40 request proto.BuildImageRequest, buildLog buildLogger) ( 41 *image.Image, error) { 42 manifestDirectory, gitInfo, err := stream.getManifest(b, request.StreamName, 43 request.GitBranch, request.Variables, buildLog) 44 if err != nil { 45 return nil, err 46 } 47 defer os.RemoveAll(manifestDirectory) 48 img, err := buildImageFromManifest(client, manifestDirectory, request, 49 b.bindMounts, stream, gitInfo, b.mtimesCopyFilter, buildLog) 50 if err != nil { 51 return nil, err 52 } 53 return img, nil 54 } 55 56 func (stream *imageStreamType) getenv() map[string]string { 57 envTable := make(map[string]string, len(stream.Variables)+3) 58 for key, value := range stream.Variables { 59 envTable[key] = expandExpression(value, func(name string) string { 60 if name == "IMAGE_STREAM" { 61 return stream.name 62 } 63 return "" 64 }) 65 } 66 envTable["IMAGE_STREAM"] = stream.name 67 envTable["IMAGE_STREAM_DIRECTORY_NAME"] = filepath.Dir(stream.name) 68 envTable["IMAGE_STREAM_LEAF_NAME"] = filepath.Base(stream.name) 69 return envTable 70 } 71 72 // getManifestLocation will expand variables and return the actual manifest 73 // location. These data may include secrets (i.e. username and password). 74 // If b is nil then secret variables are not expanded and thus the returned 75 // data do not contain secrets but may be incorrect. 76 func (stream *imageStreamType) getManifestLocation(b *Builder, 77 variables map[string]string) manifestLocationType { 78 var variableFunc func(string) string 79 if b == nil { 80 variableFunc = func(name string) string { 81 return stream.getenv()[name] 82 } 83 } else { 84 variableFunc = b.getVariableFunc(stream.getenv(), variables) 85 } 86 return manifestLocationType{ 87 directory: expandExpression(stream.ManifestDirectory, variableFunc), 88 url: expandExpression(stream.ManifestUrl, variableFunc), 89 } 90 } 91 92 func (stream *imageStreamType) getManifest(b *Builder, streamName string, 93 gitBranch string, variables map[string]string, 94 buildLog io.Writer) (string, *gitInfoType, error) { 95 if gitBranch == "" { 96 gitBranch = "master" 97 } 98 manifestRoot, err := makeTempDirectory("", 99 strings.Replace(streamName, "/", "_", -1)+".manifest") 100 if err != nil { 101 return "", nil, err 102 } 103 doCleanup := true 104 defer func() { 105 if doCleanup { 106 os.RemoveAll(manifestRoot) 107 } 108 }() 109 manifestLocation := stream.getManifestLocation(b, variables) 110 if rootDir, err := urlToLocal(manifestLocation.url); err != nil { 111 return "", nil, err 112 } else if rootDir != "" { 113 if gitBranch != "master" { 114 return "", nil, 115 fmt.Errorf("branch: %s is not master", gitBranch) 116 } 117 sourceTree := filepath.Join(rootDir, manifestLocation.directory) 118 fmt.Fprintf(buildLog, "Copying manifest tree: %s\n", sourceTree) 119 if err := fsutil.CopyTree(manifestRoot, sourceTree); err != nil { 120 return "", nil, fmt.Errorf("error copying manifest: %s", err) 121 } 122 doCleanup = false 123 return manifestRoot, nil, nil 124 } 125 var patterns []string 126 if manifestLocation.directory != "" { 127 patterns = append(patterns, manifestLocation.directory+"/*") 128 } 129 err = gitShallowClone(manifestRoot, manifestLocation.url, 130 stream.ManifestUrl, gitBranch, patterns, buildLog) 131 if err != nil { 132 return "", nil, err 133 } 134 gitDirectory := filepath.Join(manifestRoot, ".git") 135 var gitInfo *gitInfoType 136 // The specified branch/tag/commit will be in the "master" branch in the 137 // cloned repository. 138 commitId, err := gitutil.GetCommitIdOfRef(manifestRoot, "HEAD") 139 if err != nil { 140 return "", nil, err 141 } else { 142 gitInfo = &gitInfoType{ 143 branch: gitBranch, 144 commitId: commitId, 145 gitUrl: manifestLocation.url, 146 } 147 } 148 if err := os.RemoveAll(gitDirectory); err != nil { 149 return "", nil, err 150 } 151 if manifestLocation.directory != "" { 152 // Move manifest directory into manifestRoot, remove anything else. 153 err := os.Rename(filepath.Join(manifestRoot, 154 manifestLocation.directory), 155 gitDirectory) 156 if err != nil { 157 return "", nil, err 158 } 159 filenames, err := listDirectory(manifestRoot) 160 if err != nil { 161 return "", nil, err 162 } 163 for _, filename := range filenames { 164 if filename == ".git" { 165 continue 166 } 167 err := os.RemoveAll(filepath.Join(manifestRoot, filename)) 168 if err != nil { 169 return "", nil, err 170 } 171 } 172 filenames, err = listDirectory(gitDirectory) 173 if err != nil { 174 return "", nil, err 175 } 176 for _, filename := range filenames { 177 err := os.Rename(filepath.Join(gitDirectory, filename), 178 filepath.Join(manifestRoot, filename)) 179 if err != nil { 180 return "", nil, err 181 } 182 } 183 if err := os.Remove(gitDirectory); err != nil { 184 return "", nil, err 185 } 186 } 187 doCleanup = false 188 return manifestRoot, gitInfo, nil 189 } 190 191 func (stream *imageStreamType) getSourceImage(b *Builder, buildLog io.Writer) ( 192 string, string, *gitInfoType, []byte, *manifestConfigType, error) { 193 manifestDirectory, gitInfo, err := stream.getManifest(stream.builder, 194 stream.name, "", nil, buildLog) 195 if err != nil { 196 return "", "", nil, nil, nil, err 197 } 198 doRemove := true 199 defer func() { 200 if doRemove { 201 os.RemoveAll(manifestDirectory) 202 } 203 }() 204 manifestFilename := filepath.Join(manifestDirectory, "manifest") 205 manifestBytes, err := ioutil.ReadFile(manifestFilename) 206 if err != nil { 207 return "", "", nil, nil, nil, err 208 } 209 var manifest manifestConfigType 210 if err := json.Unmarshal(manifestBytes, &manifest); err != nil { 211 return "", "", nil, nil, nil, err 212 } 213 sourceImageName := expandExpression(manifest.SourceImage, 214 func(name string) string { 215 return stream.getenv()[name] 216 }) 217 doRemove = false 218 return manifestDirectory, sourceImageName, gitInfo, manifestBytes, 219 &manifest, nil 220 } 221 222 func listDirectory(directoryName string) ([]string, error) { 223 directory, err := os.Open(directoryName) 224 if err != nil { 225 return nil, err 226 } 227 defer directory.Close() 228 filenames, err := directory.Readdirnames(-1) 229 if err != nil { 230 return nil, err 231 } 232 return filenames, nil 233 } 234 235 func runCommand(buildLog io.Writer, cwd string, args ...string) error { 236 cmd := exec.Command(args[0], args[1:]...) 237 cmd.Dir = cwd 238 cmd.Stdout = buildLog 239 cmd.Stderr = buildLog 240 return cmd.Run() 241 } 242 243 func buildImageFromManifest(client srpc.ClientI, manifestDir string, 244 request proto.BuildImageRequest, bindMounts []string, 245 envGetter environmentGetter, gitInfo *gitInfoType, 246 mtimesCopyFilter *filter.Filter, buildLog buildLogger) ( 247 *image.Image, error) { 248 // First load all the various manifest files (fail early on error). 249 computedFilesList, addComputedFiles, err := loadComputedFiles(manifestDir) 250 if err != nil { 251 return nil, err 252 } 253 imageFilter, addFilter, err := loadFilter(manifestDir) 254 if err != nil { 255 return nil, err 256 } 257 tgs, err := loadTags(manifestDir) 258 if err != nil { 259 return nil, err 260 } 261 imageTriggers, addTriggers, err := loadTriggers(manifestDir) 262 if err != nil { 263 return nil, err 264 } 265 rootDir, err := makeTempDirectory("", 266 strings.Replace(request.StreamName, "/", "_", -1)+".root") 267 if err != nil { 268 return nil, err 269 } 270 defer os.RemoveAll(rootDir) 271 fmt.Fprintf(buildLog, "Created image working directory: %s\n", rootDir) 272 vGetter := variablesGetter(envGetter.getenv()).copy() 273 vGetter.merge(request.Variables) 274 if gitInfo != nil { 275 vGetter.add("MANIFEST_GIT_COMMIT_ID", gitInfo.commitId) 276 } 277 vGetter.add("REQUESTED_GIT_BRANCH", request.GitBranch) 278 request.Variables = vGetter 279 manifest, err := unpackImageAndProcessManifest(client, manifestDir, 280 request.MaxSourceAge, rootDir, bindMounts, false, vGetter, buildLog) 281 if err != nil { 282 return nil, err 283 } 284 ctimeResolution, err := getCtimeResolution() 285 if err != nil { 286 return nil, err 287 } 288 time.Sleep(ctimeResolution) 289 fmt.Fprintf(buildLog, "Waited %s (Ctime resolution)\n", 290 format.Duration(ctimeResolution)) 291 if fi, err := os.Lstat(filepath.Join(manifestDir, "tests")); err == nil { 292 if fi.IsDir() { 293 testsDir := filepath.Join(rootDir, "tests", request.StreamName) 294 if err := os.MkdirAll(testsDir, fsutil.DirPerms); err != nil { 295 return nil, err 296 } 297 err := copyFiles(manifestDir, "tests", testsDir, buildLog) 298 if err != nil { 299 return nil, err 300 } 301 } 302 } 303 if addComputedFiles { 304 computedFilesList = util.MergeComputedFiles( 305 manifest.sourceImageInfo.computedFiles, computedFilesList) 306 } 307 if addFilter { 308 mergeableFilter := &filter.MergeableFilter{} 309 mergeableFilter.Merge(manifest.sourceImageInfo.filter) 310 mergeableFilter.Merge(imageFilter) 311 imageFilter = mergeableFilter.ExportFilter() 312 } 313 if addTriggers { 314 mergeableTriggers := &triggers.MergeableTriggers{} 315 mergeableTriggers.Merge(manifest.sourceImageInfo.triggers) 316 mergeableTriggers.Merge(imageTriggers) 317 imageTriggers = mergeableTriggers.ExportTriggers() 318 } 319 if manifest.mtimesCopyFilter != nil { 320 mtimesCopyFilter = manifest.mtimesCopyFilter 321 } else if manifest.mtimesCopyAddFilter != nil { 322 mf := &filter.MergeableFilter{} 323 mf.Merge(mtimesCopyFilter) 324 mf.Merge(manifest.mtimesCopyAddFilter) 325 mtimesCopyFilter = mf.ExportFilter() 326 } 327 img, err := packImage(nil, client, request, rootDir, manifest.filter, 328 manifest.sourceImageInfo.treeCache, computedFilesList, imageFilter, 329 tgs, imageTriggers, mtimesCopyFilter, buildLog) 330 if err != nil { 331 return nil, err 332 } 333 if gitInfo != nil { 334 img.BuildBranch = gitInfo.branch 335 img.BuildCommitId = gitInfo.commitId 336 img.BuildGitUrl = gitInfo.gitUrl 337 } 338 img.SourceImage = manifest.sourceImageInfo.imageName 339 return img, nil 340 } 341 342 func buildImageFromManifestAndUpload(client srpc.ClientI, 343 options BuildLocalOptions, streamName string, expiresIn time.Duration, 344 buildLog buildLogger) (*image.Image, string, error) { 345 request := proto.BuildImageRequest{ 346 StreamName: streamName, 347 ExpiresIn: expiresIn, 348 } 349 img, err := buildImageFromManifest( 350 client, 351 options.ManifestDirectory, 352 request, 353 options.BindMounts, 354 &imageStreamType{ 355 name: streamName, 356 Variables: options.Variables, 357 }, 358 nil, 359 options.MtimesCopyFilter, 360 buildLog) 361 if err != nil { 362 return nil, "", err 363 } 364 name, err := addImage(client, request, img) 365 if err != nil { 366 return nil, "", err 367 } 368 return img, name, nil 369 } 370 371 func buildTreeCache(rootDir string, fs *filesystem.FileSystem, 372 buildLog io.Writer) (*treeCache, error) { 373 cache := treeCache{ 374 inodeTable: make(map[uint64]inodeData), 375 pathToInode: make(map[string]uint64), 376 } 377 filenameToInodeTable := fs.FilenameToInodeTable() 378 rootLength := len(rootDir) 379 startTime := time.Now() 380 err := filepath.Walk(rootDir, 381 func(path string, info os.FileInfo, err error) error { 382 if info.Mode()&os.ModeType != 0 { 383 return nil 384 } 385 rootedPath := path[rootLength:] 386 inum, ok := filenameToInodeTable[rootedPath] 387 if !ok { 388 return nil 389 } 390 gInode, ok := fs.InodeTable[inum] 391 if !ok { 392 return nil 393 } 394 rInode, ok := gInode.(*filesystem.RegularInode) 395 if !ok { 396 return nil 397 } 398 var stat syscall.Stat_t 399 if err := syscall.Stat(path, &stat); err != nil { 400 return err 401 } 402 cache.inodeTable[stat.Ino] = inodeData{ 403 ctime: stat.Ctim, 404 hash: rInode.Hash, 405 size: uint64(stat.Size), 406 } 407 cache.pathToInode[path] = uint64(stat.Ino) 408 return nil 409 }) 410 if err != nil { 411 return nil, err 412 } 413 fmt.Fprintf(buildLog, "Built tree cache in: %s\n", 414 format.Duration(time.Since(startTime))) 415 return &cache, nil 416 } 417 418 func buildTreeFromManifest(client srpc.ClientI, options BuildLocalOptions, 419 buildLog io.Writer) (string, error) { 420 rootDir, err := makeTempDirectory("", "tree") 421 if err != nil { 422 return "", err 423 } 424 _, err = unpackImageAndProcessManifest(client, 425 options.ManifestDirectory, 0, rootDir, options.BindMounts, true, 426 variablesGetter(options.Variables), buildLog) 427 if err != nil { 428 os.RemoveAll(rootDir) 429 return "", err 430 } 431 return rootDir, nil 432 } 433 434 func listComputedFiles(fs *filesystem.FileSystem) []util.ComputedFile { 435 var computedFiles []util.ComputedFile 436 fs.ForEachFile( 437 func(path string, _ uint64, inode filesystem.GenericInode) error { 438 if inode, ok := inode.(*filesystem.ComputedRegularInode); ok { 439 computedFiles = append(computedFiles, util.ComputedFile{ 440 Filename: path, 441 Source: inode.Source, 442 }) 443 } 444 return nil 445 }) 446 return computedFiles 447 } 448 449 func loadComputedFiles(manifestDir string) ([]util.ComputedFile, bool, error) { 450 computedFiles, err := util.LoadComputedFiles(filepath.Join(manifestDir, 451 "computed-files.json")) 452 if os.IsNotExist(err) { 453 computedFiles, err = util.LoadComputedFiles( 454 filepath.Join(manifestDir, "computed-files")) 455 } 456 if err != nil && !os.IsNotExist(err) { 457 return nil, false, err 458 } 459 haveComputedFiles := err == nil 460 addComputedFiles, err := util.LoadComputedFiles( 461 filepath.Join(manifestDir, "computed-files.add.json")) 462 if os.IsNotExist(err) { 463 addComputedFiles, err = util.LoadComputedFiles( 464 filepath.Join(manifestDir, "computed-files.add")) 465 } 466 if err != nil && !os.IsNotExist(err) { 467 return nil, false, err 468 } 469 haveAddComputedFiles := err == nil 470 if !haveComputedFiles && !haveAddComputedFiles { 471 return nil, false, nil 472 } else if haveComputedFiles && haveAddComputedFiles { 473 return nil, false, errors.New( 474 "computed-files and computed-files.add files both present") 475 } else if haveComputedFiles { 476 return computedFiles, false, nil 477 } else { 478 return addComputedFiles, true, nil 479 } 480 } 481 482 func loadFilter(manifestDir string) (*filter.Filter, bool, error) { 483 imageFilter, err := filter.Load(filepath.Join(manifestDir, "filter")) 484 if err != nil && !os.IsNotExist(err) { 485 return nil, false, err 486 } 487 addFilter, err := filter.Load(filepath.Join(manifestDir, "filter.add")) 488 if err != nil && !os.IsNotExist(err) { 489 return nil, false, err 490 } 491 if imageFilter == nil && addFilter == nil { 492 return nil, false, nil 493 } else if imageFilter != nil && addFilter != nil { 494 return nil, false, errors.New( 495 "filter and filter.add files both present") 496 } else if imageFilter != nil { 497 return imageFilter, false, nil 498 } else { 499 return addFilter, true, nil 500 } 501 } 502 503 func loadTags(manifestDir string) (tags.Tags, error) { 504 var tgs tags.Tags 505 err := libjson.ReadFromFile(filepath.Join(manifestDir, "tags.json"), &tgs) 506 if err != nil { 507 if os.IsNotExist(err) { 508 return nil, nil 509 } 510 return nil, err 511 } 512 if len(tgs) < 1 { 513 return nil, nil 514 } 515 return tgs, nil 516 } 517 518 func loadTriggers(manifestDir string) (*triggers.Triggers, bool, error) { 519 imageTriggers, err := triggers.Load(filepath.Join(manifestDir, "triggers")) 520 if err != nil && !os.IsNotExist(err) { 521 return nil, false, err 522 } 523 addTriggers, err := triggers.Load(filepath.Join(manifestDir, 524 "triggers.add")) 525 if err != nil && !os.IsNotExist(err) { 526 return nil, false, err 527 } 528 if imageTriggers == nil && addTriggers == nil { 529 return nil, false, nil 530 } else if imageTriggers != nil && addTriggers != nil { 531 return nil, false, errors.New( 532 "triggers and triggers.add files both present") 533 } else if imageTriggers != nil { 534 return imageTriggers, false, nil 535 } else { 536 return addTriggers, true, nil 537 } 538 } 539 540 func unpackImage(client srpc.ClientI, streamName, buildCommitId string, 541 sourceImageTagsToMatch tags.MatchTags, maxSourceAge time.Duration, 542 rootDir string, buildLog io.Writer) (*sourceImageInfoType, error) { 543 ctimeResolution, err := getCtimeResolution() 544 if err != nil { 545 return nil, err 546 } 547 imageName, sourceImage, err := getLatestImage(client, streamName, 548 buildCommitId, sourceImageTagsToMatch, buildLog) 549 if err != nil { 550 return nil, err 551 } 552 var specifiedStream string 553 if buildCommitId == "" { 554 specifiedStream = streamName 555 } else { 556 specifiedStream = streamName + "@gitCommitId:" + buildCommitId 557 } 558 if len(sourceImageTagsToMatch) > 0 { 559 specifiedStream += fmt.Sprintf("@tags:%v", sourceImageTagsToMatch) 560 } 561 if sourceImage == nil { 562 return nil, &buildErrorType{ 563 error: "no source image: " + specifiedStream, 564 needSourceImage: true, 565 sourceImage: streamName, 566 sourceImageGitCommitId: buildCommitId, 567 } 568 } 569 if maxSourceAge > 0 && time.Since(sourceImage.CreatedOn) > maxSourceAge { 570 return nil, &buildErrorType{ 571 error: "too old source image: " + specifiedStream, 572 needSourceImage: true, 573 sourceImage: streamName, 574 sourceImageGitCommitId: buildCommitId, 575 } 576 } 577 objClient := objectclient.AttachObjectClient(client) 578 defer objClient.Close() 579 err = util.Unpack(sourceImage.FileSystem, objClient, rootDir, 580 stdlog.New(buildLog, "", 0)) 581 if err != nil { 582 return nil, err 583 } 584 fmt.Fprintf(buildLog, "Source image: %s\n", imageName) 585 treeCache, err := buildTreeCache(rootDir, sourceImage.FileSystem, buildLog) 586 if err != nil { 587 return nil, err 588 } 589 time.Sleep(ctimeResolution) 590 fmt.Fprintf(buildLog, "Waited %s (Ctime resolution)\n", 591 format.Duration(ctimeResolution)) 592 return &sourceImageInfoType{ 593 computedFiles: listComputedFiles(sourceImage.FileSystem), 594 filter: sourceImage.Filter, 595 imageName: imageName, 596 treeCache: treeCache, 597 triggers: sourceImage.Triggers, 598 }, nil 599 } 600 601 func urlToLocal(urlValue string) (string, error) { 602 if parsedUrl, err := url.Parse(urlValue); err == nil { 603 if parsedUrl.Scheme == "dir" { 604 if parsedUrl.Path[0] != '/' { 605 return "", fmt.Errorf("missing leading slash: %s", 606 parsedUrl.Path) 607 } 608 return parsedUrl.Path, nil 609 } 610 } 611 return "", nil 612 }