github.com/cloud-foundations/dominator@v0.0.0-20221004181915-6e4fee580046/imagebuilder/builder/image.go (about) 1 package builder 2 3 import ( 4 "errors" 5 "fmt" 6 "io" 7 "io/ioutil" 8 stdlog "log" 9 "net/url" 10 "os" 11 "os/exec" 12 "path/filepath" 13 "strings" 14 "time" 15 16 "github.com/Cloud-Foundations/Dominator/lib/filesystem" 17 "github.com/Cloud-Foundations/Dominator/lib/filesystem/util" 18 "github.com/Cloud-Foundations/Dominator/lib/filter" 19 "github.com/Cloud-Foundations/Dominator/lib/format" 20 "github.com/Cloud-Foundations/Dominator/lib/fsutil" 21 "github.com/Cloud-Foundations/Dominator/lib/image" 22 objectclient "github.com/Cloud-Foundations/Dominator/lib/objectserver/client" 23 "github.com/Cloud-Foundations/Dominator/lib/srpc" 24 "github.com/Cloud-Foundations/Dominator/lib/triggers" 25 proto "github.com/Cloud-Foundations/Dominator/proto/imaginator" 26 ) 27 28 type gitInfoType struct { 29 branch string 30 commitId string 31 } 32 33 func (stream *imageStreamType) build(b *Builder, client *srpc.Client, 34 request proto.BuildImageRequest, buildLog buildLogger) ( 35 *image.Image, error) { 36 manifestDirectory, gitInfo, err := stream.getManifest(b, request.StreamName, 37 request.GitBranch, request.Variables, buildLog) 38 if err != nil { 39 return nil, err 40 } 41 defer os.RemoveAll(manifestDirectory) 42 img, err := buildImageFromManifest(client, manifestDirectory, request, 43 b.bindMounts, stream, gitInfo, buildLog) 44 if err != nil { 45 return nil, err 46 } 47 return img, nil 48 } 49 50 func (stream *imageStreamType) getenv() map[string]string { 51 envTable := make(map[string]string, 1) 52 envTable["IMAGE_STREAM"] = stream.name 53 envTable["IMAGE_STREAM_DIRECTORY_NAME"] = filepath.Dir(stream.name) 54 envTable["IMAGE_STREAM_LEAF_NAME"] = filepath.Base(stream.name) 55 return envTable 56 } 57 58 func (stream *imageStreamType) getManifest(b *Builder, streamName string, 59 gitBranch string, variables map[string]string, 60 buildLog io.Writer) (string, *gitInfoType, error) { 61 if gitBranch == "" { 62 gitBranch = "master" 63 } 64 variableFunc := b.getVariableFunc(stream.getenv(), variables) 65 manifestRoot, err := makeTempDirectory("", 66 strings.Replace(streamName, "/", "_", -1)+".manifest") 67 if err != nil { 68 return "", nil, err 69 } 70 doCleanup := true 71 defer func() { 72 if doCleanup { 73 os.RemoveAll(manifestRoot) 74 } 75 }() 76 manifestDirectory := os.Expand(stream.ManifestDirectory, variableFunc) 77 manifestUrl := os.Expand(stream.ManifestUrl, variableFunc) 78 if parsedUrl, err := url.Parse(manifestUrl); err == nil { 79 if parsedUrl.Scheme == "dir" { 80 if parsedUrl.Path[0] != '/' { 81 return "", nil, fmt.Errorf("missing leading slash: %s", 82 parsedUrl.Path) 83 } 84 if gitBranch != "master" { 85 return "", nil, 86 fmt.Errorf("branch: %s is not master", gitBranch) 87 } 88 sourceTree := filepath.Join(parsedUrl.Path, manifestDirectory) 89 fmt.Fprintf(buildLog, "Copying manifest tree: %s\n", sourceTree) 90 if err := fsutil.CopyTree(manifestRoot, sourceTree); err != nil { 91 return "", nil, fmt.Errorf("error copying manifest: %s", err) 92 } 93 doCleanup = false 94 return manifestRoot, nil, nil 95 } 96 } 97 fmt.Fprintf(buildLog, "Cloning repository: %s branch: %s\n", 98 stream.ManifestUrl, gitBranch) 99 err = runCommand(buildLog, "", "git", "init", manifestRoot) 100 if err != nil { 101 return "", nil, err 102 } 103 err = runCommand(buildLog, manifestRoot, "git", "remote", "add", "origin", 104 manifestUrl) 105 if err != nil { 106 return "", nil, err 107 } 108 err = runCommand(buildLog, manifestRoot, "git", "config", 109 "core.sparsecheckout", "true") 110 if err != nil { 111 return "", nil, err 112 } 113 directorySelector := "*\n" 114 if manifestDirectory != "" { 115 directorySelector = manifestDirectory + "/*\n" 116 } 117 err = ioutil.WriteFile( 118 filepath.Join(manifestRoot, ".git", "info", "sparse-checkout"), 119 []byte(directorySelector), 0644) 120 if err != nil { 121 return "", nil, err 122 } 123 startTime := time.Now() 124 err = runCommand(buildLog, manifestRoot, "git", "pull", "--depth=1", 125 "origin", gitBranch) 126 if err != nil { 127 return "", nil, err 128 } 129 if gitBranch != "master" { 130 err = runCommand(buildLog, manifestRoot, "git", "checkout", gitBranch) 131 if err != nil { 132 return "", nil, err 133 } 134 } 135 loadTime := time.Since(startTime) 136 repoSize, err := getTreeSize(manifestRoot) 137 if err != nil { 138 return "", nil, err 139 } 140 speed := float64(repoSize) / loadTime.Seconds() 141 fmt.Fprintf(buildLog, 142 "Downloaded partial repository in %s, size: %s (%s/s)\n", 143 format.Duration(loadTime), format.FormatBytes(repoSize), 144 format.FormatBytes(uint64(speed))) 145 gitDirectory := filepath.Join(manifestRoot, ".git") 146 var gitInfo *gitInfoType 147 filename := filepath.Join(gitDirectory, "refs", "heads", gitBranch) 148 if lines, err := fsutil.LoadLines(filename); err != nil { 149 return "", nil, err 150 } else if len(lines) != 1 { 151 return "", nil, fmt.Errorf("%s does not have only one line", filename) 152 } else { 153 gitInfo = &gitInfoType{ 154 branch: gitBranch, 155 commitId: strings.TrimSpace(lines[0]), 156 } 157 } 158 if err := os.RemoveAll(gitDirectory); err != nil { 159 return "", nil, err 160 } 161 if manifestDirectory != "" { 162 // Move manifestDirectory into manifestRoot, remove anything else. 163 err := os.Rename(filepath.Join(manifestRoot, manifestDirectory), 164 gitDirectory) 165 if err != nil { 166 return "", nil, err 167 } 168 filenames, err := listDirectory(manifestRoot) 169 if err != nil { 170 return "", nil, err 171 } 172 for _, filename := range filenames { 173 if filename == ".git" { 174 continue 175 } 176 err := os.RemoveAll(filepath.Join(manifestRoot, filename)) 177 if err != nil { 178 return "", nil, err 179 } 180 } 181 filenames, err = listDirectory(gitDirectory) 182 if err != nil { 183 return "", nil, err 184 } 185 for _, filename := range filenames { 186 err := os.Rename(filepath.Join(gitDirectory, filename), 187 filepath.Join(manifestRoot, filename)) 188 if err != nil { 189 return "", nil, err 190 } 191 } 192 if err := os.Remove(gitDirectory); err != nil { 193 return "", nil, err 194 } 195 } 196 doCleanup = false 197 return manifestRoot, gitInfo, nil 198 } 199 200 func getTreeSize(dirname string) (uint64, error) { 201 var size uint64 202 err := filepath.Walk(dirname, 203 func(path string, info os.FileInfo, err error) error { 204 if err != nil { 205 return err 206 } 207 size += uint64(info.Size()) 208 return nil 209 }) 210 if err != nil { 211 return 0, err 212 } 213 return size, nil 214 } 215 216 func listDirectory(directoryName string) ([]string, error) { 217 directory, err := os.Open(directoryName) 218 if err != nil { 219 return nil, err 220 } 221 defer directory.Close() 222 filenames, err := directory.Readdirnames(-1) 223 if err != nil { 224 return nil, err 225 } 226 return filenames, nil 227 } 228 229 func runCommand(buildLog io.Writer, cwd string, args ...string) error { 230 cmd := exec.Command(args[0], args[1:]...) 231 cmd.Dir = cwd 232 cmd.Stdout = buildLog 233 cmd.Stderr = buildLog 234 return cmd.Run() 235 } 236 237 func buildImageFromManifest(client *srpc.Client, manifestDir string, 238 request proto.BuildImageRequest, bindMounts []string, 239 envGetter environmentGetter, gitInfo *gitInfoType, 240 buildLog buildLogger) (*image.Image, error) { 241 // First load all the various manifest files (fail early on error). 242 computedFilesList, addComputedFiles, err := loadComputedFiles(manifestDir) 243 if err != nil { 244 return nil, err 245 } 246 imageFilter, addFilter, err := loadFilter(manifestDir) 247 if err != nil { 248 return nil, err 249 } 250 imageTriggers, addTriggers, err := loadTriggers(manifestDir) 251 if err != nil { 252 return nil, err 253 } 254 rootDir, err := makeTempDirectory("", 255 strings.Replace(request.StreamName, "/", "_", -1)+".root") 256 if err != nil { 257 return nil, err 258 } 259 defer os.RemoveAll(rootDir) 260 fmt.Fprintf(buildLog, "Created image working directory: %s\n", rootDir) 261 manifest, err := unpackImageAndProcessManifest(client, manifestDir, 262 rootDir, bindMounts, false, envGetter, buildLog) 263 if err != nil { 264 return nil, err 265 } 266 if fi, err := os.Lstat(filepath.Join(manifestDir, "tests")); err == nil { 267 if fi.IsDir() { 268 testsDir := filepath.Join(rootDir, "tests", request.StreamName) 269 if err := os.MkdirAll(testsDir, fsutil.DirPerms); err != nil { 270 return nil, err 271 } 272 err := copyFiles(manifestDir, "tests", testsDir, buildLog) 273 if err != nil { 274 return nil, err 275 } 276 } 277 } 278 if addComputedFiles { 279 computedFilesList = util.MergeComputedFiles( 280 manifest.sourceImageInfo.computedFiles, computedFilesList) 281 } 282 if addFilter { 283 mergeableFilter := &filter.MergeableFilter{} 284 mergeableFilter.Merge(manifest.sourceImageInfo.filter) 285 mergeableFilter.Merge(imageFilter) 286 imageFilter = mergeableFilter.ExportFilter() 287 } 288 if addTriggers { 289 mergeableTriggers := &triggers.MergeableTriggers{} 290 mergeableTriggers.Merge(manifest.sourceImageInfo.triggers) 291 mergeableTriggers.Merge(imageTriggers) 292 imageTriggers = mergeableTriggers.ExportTriggers() 293 } 294 img, err := packImage(client, request, rootDir, manifest.filter, 295 computedFilesList, imageFilter, imageTriggers, buildLog) 296 if err != nil { 297 return nil, err 298 } 299 if gitInfo != nil { 300 img.BuildBranch = gitInfo.branch 301 img.BuildCommitId = gitInfo.commitId 302 } 303 return img, nil 304 } 305 306 func buildImageFromManifestAndUpload(client *srpc.Client, manifestDir string, 307 request proto.BuildImageRequest, bindMounts []string, 308 envGetter environmentGetter, 309 buildLog buildLogger) (*image.Image, string, error) { 310 img, err := buildImageFromManifest(client, manifestDir, request, bindMounts, 311 envGetter, nil, buildLog) 312 if err != nil { 313 return nil, "", err 314 } 315 name, err := addImage(client, request, img) 316 if err != nil { 317 return nil, "", err 318 } 319 return img, name, nil 320 } 321 322 func buildTreeFromManifest(client *srpc.Client, manifestDir string, 323 bindMounts []string, envGetter environmentGetter, 324 buildLog io.Writer) (string, error) { 325 rootDir, err := makeTempDirectory("", "tree") 326 if err != nil { 327 return "", err 328 } 329 _, err = unpackImageAndProcessManifest(client, manifestDir, rootDir, 330 bindMounts, true, envGetter, buildLog) 331 if err != nil { 332 os.RemoveAll(rootDir) 333 return "", err 334 } 335 return rootDir, nil 336 } 337 338 func listComputedFiles(fs *filesystem.FileSystem) []util.ComputedFile { 339 var computedFiles []util.ComputedFile 340 fs.ForEachFile( 341 func(path string, _ uint64, inode filesystem.GenericInode) error { 342 if inode, ok := inode.(*filesystem.ComputedRegularInode); ok { 343 computedFiles = append(computedFiles, util.ComputedFile{ 344 Filename: path, 345 Source: inode.Source, 346 }) 347 } 348 return nil 349 }) 350 return computedFiles 351 } 352 353 func loadComputedFiles(manifestDir string) ([]util.ComputedFile, bool, error) { 354 computedFiles, err := util.LoadComputedFiles(filepath.Join(manifestDir, 355 "computed-files.json")) 356 if os.IsNotExist(err) { 357 computedFiles, err = util.LoadComputedFiles( 358 filepath.Join(manifestDir, "computed-files")) 359 } 360 if err != nil && !os.IsNotExist(err) { 361 return nil, false, err 362 } 363 haveComputedFiles := err == nil 364 addComputedFiles, err := util.LoadComputedFiles( 365 filepath.Join(manifestDir, "computed-files.add.json")) 366 if os.IsNotExist(err) { 367 addComputedFiles, err = util.LoadComputedFiles( 368 filepath.Join(manifestDir, "computed-files.add")) 369 } 370 if err != nil && !os.IsNotExist(err) { 371 return nil, false, err 372 } 373 haveAddComputedFiles := err == nil 374 if !haveComputedFiles && !haveAddComputedFiles { 375 return nil, false, nil 376 } else if haveComputedFiles && haveAddComputedFiles { 377 return nil, false, errors.New( 378 "computed-files and computed-files.add files both present") 379 } else if haveComputedFiles { 380 return computedFiles, false, nil 381 } else { 382 return addComputedFiles, true, nil 383 } 384 } 385 386 func loadFilter(manifestDir string) (*filter.Filter, bool, error) { 387 imageFilter, err := filter.Load(filepath.Join(manifestDir, "filter")) 388 if err != nil && !os.IsNotExist(err) { 389 return nil, false, err 390 } 391 addFilter, err := filter.Load(filepath.Join(manifestDir, "filter.add")) 392 if err != nil && !os.IsNotExist(err) { 393 return nil, false, err 394 } 395 if imageFilter == nil && addFilter == nil { 396 return nil, false, nil 397 } else if imageFilter != nil && addFilter != nil { 398 return nil, false, errors.New( 399 "filter and filter.add files both present") 400 } else if imageFilter != nil { 401 return imageFilter, false, nil 402 } else { 403 return addFilter, true, nil 404 } 405 } 406 407 func loadTriggers(manifestDir string) (*triggers.Triggers, bool, error) { 408 imageTriggers, err := triggers.Load(filepath.Join(manifestDir, "triggers")) 409 if err != nil && !os.IsNotExist(err) { 410 return nil, false, err 411 } 412 addTriggers, err := triggers.Load(filepath.Join(manifestDir, 413 "triggers.add")) 414 if err != nil && !os.IsNotExist(err) { 415 return nil, false, err 416 } 417 if imageTriggers == nil && addTriggers == nil { 418 return nil, false, nil 419 } else if imageTriggers != nil && addTriggers != nil { 420 return nil, false, errors.New( 421 "triggers and triggers.add files both present") 422 } else if imageTriggers != nil { 423 return imageTriggers, false, nil 424 } else { 425 return addTriggers, true, nil 426 } 427 } 428 429 func unpackImage(client *srpc.Client, streamName string, 430 maxSourceAge, expiresIn time.Duration, rootDir string, 431 buildLog io.Writer) (*sourceImageInfoType, error) { 432 imageName, sourceImage, err := getLatestImage(client, streamName, buildLog) 433 if err != nil { 434 return nil, err 435 } 436 if sourceImage == nil { 437 return nil, errors.New(errNoSourceImage + streamName) 438 } 439 if maxSourceAge > 0 && time.Since(sourceImage.CreatedOn) > maxSourceAge { 440 return nil, errors.New(errNoSourceImage + streamName) 441 } 442 objClient := objectclient.AttachObjectClient(client) 443 defer objClient.Close() 444 err = util.Unpack(sourceImage.FileSystem, objClient, rootDir, 445 stdlog.New(buildLog, "", 0)) 446 if err != nil { 447 return nil, err 448 } 449 fmt.Fprintf(buildLog, "Source image: %s\n", imageName) 450 return &sourceImageInfoType{ 451 listComputedFiles(sourceImage.FileSystem), 452 sourceImage.Filter, 453 sourceImage.Triggers, 454 }, nil 455 }