github.com/apptainer/singularity@v3.1.1+incompatible/internal/pkg/build/build.go (about) 1 // Copyright (c) 2019, Sylabs Inc. All rights reserved. 2 // This software is licensed under a 3-clause BSD license. Please consult the 3 // LICENSE.md file distributed with the sources of this project regarding your 4 // rights to use or distribute this software. 5 6 package build 7 8 import ( 9 "encoding/json" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "os" 14 "os/exec" 15 "os/signal" 16 "path/filepath" 17 "strconv" 18 "syscall" 19 "time" 20 21 specs "github.com/opencontainers/runtime-spec/specs-go" 22 "github.com/sylabs/singularity/internal/pkg/build/apps" 23 "github.com/sylabs/singularity/internal/pkg/build/assemblers" 24 "github.com/sylabs/singularity/internal/pkg/build/sources" 25 "github.com/sylabs/singularity/internal/pkg/buildcfg" 26 "github.com/sylabs/singularity/internal/pkg/runtime/engines/config" 27 "github.com/sylabs/singularity/internal/pkg/runtime/engines/config/oci" 28 imgbuildConfig "github.com/sylabs/singularity/internal/pkg/runtime/engines/imgbuild/config" 29 "github.com/sylabs/singularity/internal/pkg/sylog" 30 syexec "github.com/sylabs/singularity/internal/pkg/util/exec" 31 "github.com/sylabs/singularity/internal/pkg/util/uri" 32 "github.com/sylabs/singularity/pkg/build/types" 33 "github.com/sylabs/singularity/pkg/build/types/parser" 34 "github.com/sylabs/singularity/pkg/image" 35 ) 36 37 // Build is an abstracted way to look at the entire build process. 38 // For example calling NewBuild() will return this object. 39 // From there we can call Full() on this build object, which will: 40 // Call Bundle() to obtain all data needed to execute the specified build locally on the machine 41 // Execute all of a definition using AllSections() 42 // And finally call Assemble() to create our container image 43 type Build struct { 44 // dest is the location for container after build is complete 45 dest string 46 // format is the format of built container, e.g., SIF, sandbox 47 format string 48 // c Gets and Packs data needed to build a container into a Bundle from various sources 49 c ConveyorPacker 50 // a Assembles a container from the information stored in a Bundle into various formats 51 a Assembler 52 // b is an intermediate structure that encapsulates all information for the container, e.g., metadata, filesystems 53 b *types.Bundle 54 } 55 56 // NewBuild creates a new Build struct from a spec (URI, definition file, etc...) 57 func NewBuild(spec, dest, format string, libraryURL, authToken string, opts types.Options) (*Build, error) { 58 def, err := makeDef(spec, false) 59 if err != nil { 60 return nil, fmt.Errorf("unable to parse spec %v: %v", spec, err) 61 } 62 63 return newBuild(def, dest, format, libraryURL, authToken, opts) 64 } 65 66 // NewBuildJSON creates a new build struct from a JSON byte slice 67 func NewBuildJSON(r io.Reader, dest, format string, libraryURL, authToken string, opts types.Options) (*Build, error) { 68 def, err := types.NewDefinitionFromJSON(r) 69 if err != nil { 70 return nil, fmt.Errorf("unable to parse JSON: %v", err) 71 } 72 73 return newBuild(def, dest, format, libraryURL, authToken, opts) 74 } 75 76 func newBuild(d types.Definition, dest, format string, libraryURL, authToken string, opts types.Options) (*Build, error) { 77 var err error 78 79 syscall.Umask(0002) 80 81 // always build a sandbox if updating an existing sandbox 82 if opts.Update { 83 format = "sandbox" 84 } 85 86 b := &Build{ 87 format: format, 88 dest: dest, 89 } 90 91 b.b, err = types.NewBundle(opts.TmpDir, "sbuild") 92 if err != nil { 93 return nil, err 94 } 95 96 b.b.Recipe = d 97 b.b.Opts = opts 98 99 // dont need to get cp if we're skipping bootstrap 100 if !opts.Update || opts.Force { 101 if c, err := getcp(b.b.Recipe, libraryURL, authToken); err == nil { 102 b.c = c 103 } else { 104 return nil, fmt.Errorf("unable to get conveyorpacker: %s", err) 105 } 106 } 107 108 switch format { 109 case "sandbox": 110 b.a = &assemblers.SandboxAssembler{} 111 case "sif": 112 b.a = &assemblers.SIFAssembler{} 113 default: 114 return nil, fmt.Errorf("unrecognized output format %s", format) 115 } 116 117 return b, nil 118 } 119 120 // cleanUp removes remnants of build from file system unless NoCleanUp is specified 121 func (b Build) cleanUp() { 122 if b.b.Opts.NoCleanUp { 123 sylog.Infof("Build performed with no clean up option, build bundle located at: %v", b.b.Path) 124 return 125 } 126 sylog.Debugf("Build bundle cleanup: %v", b.b.Path) 127 os.RemoveAll(b.b.Path) 128 } 129 130 // Full runs a standard build from start to finish 131 func (b *Build) Full() error { 132 sylog.Infof("Starting build...") 133 134 // monitor build for termination signal and clean up 135 c := make(chan os.Signal) 136 signal.Notify(c, os.Interrupt, syscall.SIGTERM) 137 go func() { 138 <-c 139 b.cleanUp() 140 os.Exit(1) 141 }() 142 // clean up build normally 143 defer b.cleanUp() 144 145 if err := b.runPreScript(); err != nil { 146 return err 147 } 148 149 if b.b.Opts.Update && !b.b.Opts.Force { 150 //if updating, extract dest container to bundle 151 sylog.Infof("Building into existing container: %s", b.dest) 152 p, err := sources.GetLocalPacker(b.dest, b.b) 153 if err != nil { 154 return err 155 } 156 157 _, err = p.Pack() 158 if err != nil { 159 return err 160 } 161 } else { 162 //if force, start build from scratch 163 if err := b.c.Get(b.b); err != nil { 164 return fmt.Errorf("conveyor failed to get: %v", err) 165 } 166 167 _, err := b.c.Pack() 168 if err != nil { 169 return fmt.Errorf("packer failed to pack: %v", err) 170 } 171 } 172 173 // create apps in bundle 174 a := apps.New() 175 for k, v := range b.b.Recipe.CustomData { 176 a.HandleSection(k, v) 177 } 178 179 a.HandleBundle(b.b) 180 b.b.Recipe.BuildData.Post += a.HandlePost() 181 182 if engineRequired(b.b.Recipe) { 183 if err := b.runBuildEngine(); err != nil { 184 return fmt.Errorf("while running engine: %v", err) 185 } 186 } 187 188 sylog.Debugf("Inserting Metadata") 189 if err := b.insertMetadata(); err != nil { 190 return fmt.Errorf("While inserting metadata to bundle: %v", err) 191 } 192 193 sylog.Debugf("Calling assembler") 194 if err := b.Assemble(b.dest); err != nil { 195 return err 196 } 197 198 sylog.Infof("Build complete: %s", b.dest) 199 return nil 200 } 201 202 // engineRequired returns true if build definition is requesting to run scripts or copy files 203 func engineRequired(def types.Definition) bool { 204 return def.BuildData.Post != "" || def.BuildData.Setup != "" || def.BuildData.Test != "" || len(def.BuildData.Files) != 0 205 } 206 207 func (b *Build) copyFiles() error { 208 209 // iterate through files transfers 210 for _, transfer := range b.b.Recipe.BuildData.Files { 211 // sanity 212 if transfer.Src == "" { 213 sylog.Warningf("Attempt to copy file with no name...") 214 continue 215 } 216 // dest = source if not specified 217 if transfer.Dst == "" { 218 transfer.Dst = transfer.Src 219 } 220 sylog.Infof("Copying %v to %v", transfer.Src, transfer.Dst) 221 // copy each file into bundle rootfs 222 transfer.Dst = filepath.Join(b.b.Rootfs(), transfer.Dst) 223 copy := exec.Command("/bin/cp", "-fLr", transfer.Src, transfer.Dst) 224 if err := copy.Run(); err != nil { 225 return fmt.Errorf("While copying %v to %v: %v", transfer.Src, transfer.Dst, err) 226 } 227 } 228 229 return nil 230 } 231 232 func (b *Build) insertMetadata() (err error) { 233 // insert help 234 err = insertHelpScript(b.b) 235 if err != nil { 236 return fmt.Errorf("While inserting help script: %v", err) 237 } 238 239 // insert labels 240 err = insertLabelsJSON(b.b) 241 if err != nil { 242 return fmt.Errorf("While inserting labels JSON: %v", err) 243 } 244 245 // insert definition 246 err = insertDefinition(b.b) 247 if err != nil { 248 return fmt.Errorf("While inserting definition: %v", err) 249 } 250 251 // insert environment 252 err = insertEnvScript(b.b) 253 if err != nil { 254 return fmt.Errorf("While inserting environment script: %v", err) 255 } 256 257 // insert startscript 258 err = insertStartScript(b.b) 259 if err != nil { 260 return fmt.Errorf("While inserting startscript: %v", err) 261 } 262 263 // insert runscript 264 err = insertRunScript(b.b) 265 if err != nil { 266 return fmt.Errorf("While inserting runscript: %v", err) 267 } 268 269 // insert test script 270 err = insertTestScript(b.b) 271 if err != nil { 272 return fmt.Errorf("While inserting test script: %v", err) 273 } 274 275 return 276 } 277 278 func (b *Build) runPreScript() error { 279 if b.runPre() && b.b.Recipe.BuildData.Pre != "" { 280 if syscall.Getuid() != 0 { 281 return fmt.Errorf("Attempted to build with scripts as non-root user") 282 } 283 284 // Run %pre script here 285 pre := exec.Command("/bin/sh", "-cex", b.b.Recipe.BuildData.Pre) 286 pre.Stdout = os.Stdout 287 pre.Stderr = os.Stderr 288 289 sylog.Infof("Running pre scriptlet\n") 290 if err := pre.Start(); err != nil { 291 return fmt.Errorf("failed to start %%pre proc: %v", err) 292 } 293 if err := pre.Wait(); err != nil { 294 return fmt.Errorf("pre proc: %v", err) 295 } 296 } 297 return nil 298 } 299 300 // runBuildEngine creates an imgbuild engine and creates a container out of our bundle in order to execute %post %setup scripts in the bundle 301 func (b *Build) runBuildEngine() error { 302 if syscall.Getuid() != 0 { 303 return fmt.Errorf("Attempted to build with scripts as non-root user") 304 } 305 306 sylog.Debugf("Starting build engine") 307 env := []string{sylog.GetEnvVar()} 308 starter := filepath.Join(buildcfg.LIBEXECDIR, "/singularity/bin/starter") 309 progname := []string{"singularity image-build"} 310 ociConfig := &oci.Config{} 311 312 engineConfig := &imgbuildConfig.EngineConfig{ 313 Bundle: *b.b, 314 OciConfig: ociConfig, 315 } 316 317 // surface build specific environment variables for scripts 318 sRootfs := "SINGULARITY_ROOTFS=" + b.b.Rootfs() 319 sEnvironment := "SINGULARITY_ENVIRONMENT=" + "/.singularity.d/env/91-environment.sh" 320 321 ociConfig.Process = &specs.Process{} 322 ociConfig.Process.Env = append(os.Environ(), sRootfs, sEnvironment) 323 324 config := &config.Common{ 325 EngineName: imgbuildConfig.Name, 326 ContainerID: "image-build", 327 EngineConfig: engineConfig, 328 } 329 330 configData, err := json.Marshal(config) 331 if err != nil { 332 return fmt.Errorf("failed to marshal config.Common: %s", err) 333 } 334 335 starterCmd, err := syexec.PipeCommand(starter, progname, env, configData) 336 if err != nil { 337 return fmt.Errorf("failed to create cmd type: %v", err) 338 } 339 340 starterCmd.Stdout = os.Stdout 341 starterCmd.Stderr = os.Stderr 342 343 return starterCmd.Run() 344 } 345 346 func getcp(def types.Definition, libraryURL, authToken string) (ConveyorPacker, error) { 347 switch def.Header["bootstrap"] { 348 case "library": 349 return &sources.LibraryConveyorPacker{ 350 LibraryURL: libraryURL, 351 AuthToken: authToken, 352 }, nil 353 case "shub": 354 return &sources.ShubConveyorPacker{}, nil 355 case "docker", "docker-archive", "docker-daemon", "oci", "oci-archive": 356 return &sources.OCIConveyorPacker{}, nil 357 case "busybox": 358 return &sources.BusyBoxConveyorPacker{}, nil 359 case "debootstrap": 360 return &sources.DebootstrapConveyorPacker{}, nil 361 case "arch": 362 return &sources.ArchConveyorPacker{}, nil 363 case "localimage": 364 return &sources.LocalConveyorPacker{}, nil 365 case "yum": 366 return &sources.YumConveyorPacker{}, nil 367 case "zypper": 368 return &sources.ZypperConveyorPacker{}, nil 369 case "scratch": 370 return &sources.ScratchConveyorPacker{}, nil 371 case "": 372 return nil, fmt.Errorf("no bootstrap specification found") 373 default: 374 return nil, fmt.Errorf("invalid build source %s", def.Header["bootstrap"]) 375 } 376 } 377 378 // makeDef gets a definition object from a spec 379 func makeDef(spec string, remote bool) (types.Definition, error) { 380 if ok, err := uri.IsValid(spec); ok && err == nil { 381 // URI passed as spec 382 return types.NewDefinitionFromURI(spec) 383 } 384 385 // Check if spec is an image/sandbox 386 if _, err := image.Init(spec, false); err == nil { 387 return types.NewDefinitionFromURI("localimage" + "://" + spec) 388 } 389 390 // default to reading file as definition 391 defFile, err := os.Open(spec) 392 if err != nil { 393 return types.Definition{}, fmt.Errorf("unable to open file %s: %v", spec, err) 394 } 395 defer defFile.Close() 396 397 // must be root to build from a definition 398 if os.Getuid() != 0 && !remote { 399 sylog.Fatalf("You must be the root user to build from a Singularity recipe file") 400 } 401 402 d, err := parser.ParseDefinitionFile(defFile) 403 if err != nil { 404 return types.Definition{}, fmt.Errorf("While parsing definition: %s: %v", spec, err) 405 } 406 407 return d, nil 408 } 409 410 // runPre determines if %pre section was specified to be run from the CLI 411 func (b Build) runPre() bool { 412 for _, section := range b.b.Opts.Sections { 413 if section == "none" { 414 return false 415 } 416 if section == "all" || section == "pre" { 417 return true 418 } 419 } 420 return false 421 } 422 423 // MakeDef gets a definition object from a spec 424 func MakeDef(spec string, remote bool) (types.Definition, error) { 425 return makeDef(spec, remote) 426 } 427 428 // Assemble assembles the bundle to the specified path 429 func (b *Build) Assemble(path string) error { 430 return b.a.Assemble(b.b, path) 431 } 432 433 func insertEnvScript(b *types.Bundle) error { 434 if b.RunSection("environment") && b.Recipe.ImageData.Environment != "" { 435 sylog.Infof("Adding environment to container") 436 envScriptPath := filepath.Join(b.Rootfs(), "/.singularity.d/env/90-environment.sh") 437 _, err := os.Stat(envScriptPath) 438 if os.IsNotExist(err) { 439 err := ioutil.WriteFile(envScriptPath, []byte("#!/bin/sh\n\n"+b.Recipe.ImageData.Environment+"\n"), 0755) 440 if err != nil { 441 return err 442 } 443 } else { 444 // append to script if it already exists 445 f, err := os.OpenFile(envScriptPath, os.O_APPEND|os.O_WRONLY, 0755) 446 if err != nil { 447 return err 448 } 449 defer f.Close() 450 451 _, err = f.WriteString("\n" + b.Recipe.ImageData.Environment + "\n") 452 if err != nil { 453 return err 454 } 455 } 456 } 457 return nil 458 } 459 460 func insertRunScript(b *types.Bundle) error { 461 if b.RunSection("runscript") && b.Recipe.ImageData.Runscript != "" { 462 sylog.Infof("Adding runscript") 463 err := ioutil.WriteFile(filepath.Join(b.Rootfs(), "/.singularity.d/runscript"), []byte("#!/bin/sh\n\n"+b.Recipe.ImageData.Runscript+"\n"), 0755) 464 if err != nil { 465 return err 466 } 467 } 468 return nil 469 } 470 471 func insertStartScript(b *types.Bundle) error { 472 if b.RunSection("startscript") && b.Recipe.ImageData.Startscript != "" { 473 sylog.Infof("Adding startscript") 474 err := ioutil.WriteFile(filepath.Join(b.Rootfs(), "/.singularity.d/startscript"), []byte("#!/bin/sh\n\n"+b.Recipe.ImageData.Startscript+"\n"), 0755) 475 if err != nil { 476 return err 477 } 478 } 479 return nil 480 } 481 482 func insertTestScript(b *types.Bundle) error { 483 if b.RunSection("test") && b.Recipe.ImageData.Test != "" { 484 sylog.Infof("Adding testscript") 485 err := ioutil.WriteFile(filepath.Join(b.Rootfs(), "/.singularity.d/test"), []byte("#!/bin/sh\n\n"+b.Recipe.ImageData.Test+"\n"), 0755) 486 if err != nil { 487 return err 488 } 489 } 490 return nil 491 } 492 493 func insertHelpScript(b *types.Bundle) error { 494 if b.RunSection("help") && b.Recipe.ImageData.Help != "" { 495 _, err := os.Stat(filepath.Join(b.Rootfs(), "/.singularity.d/runscript.help")) 496 if err != nil || b.Opts.Force { 497 sylog.Infof("Adding help info") 498 err := ioutil.WriteFile(filepath.Join(b.Rootfs(), "/.singularity.d/runscript.help"), []byte(b.Recipe.ImageData.Help+"\n"), 0644) 499 if err != nil { 500 return err 501 } 502 } else { 503 sylog.Warningf("Help message already exists and force option is false, not overwriting") 504 } 505 } 506 return nil 507 } 508 509 func insertDefinition(b *types.Bundle) error { 510 511 // if update, check for existing definition and move it to bootstrap history 512 if b.Opts.Update { 513 if _, err := os.Stat(filepath.Join(b.Rootfs(), "/.singularity.d/Singularity")); err == nil { 514 // make bootstrap_history directory if it doesnt exist 515 if _, err := os.Stat(filepath.Join(b.Rootfs(), "/.singularity.d/bootstrap_history")); err != nil { 516 err = os.Mkdir(filepath.Join(b.Rootfs(), "/.singularity.d/bootstrap_history"), 0755) 517 if err != nil { 518 return err 519 } 520 } 521 522 // look at number of files in bootstrap_history to give correct file name 523 files, err := ioutil.ReadDir(filepath.Join(b.Rootfs(), "/.singularity.d/bootstrap_history")) 524 525 // name is "Singularity" concatenated with an index based on number of other files in bootstrap_history 526 len := strconv.Itoa(len(files)) 527 528 histName := "Singularity" + len 529 530 // move old definition into bootstrap_history 531 err = os.Rename(filepath.Join(b.Rootfs(), "/.singularity.d/Singularity"), filepath.Join(b.Rootfs(), "/.singularity.d/bootstrap_history", histName)) 532 if err != nil { 533 return err 534 } 535 } 536 537 } 538 539 err := ioutil.WriteFile(filepath.Join(b.Rootfs(), "/.singularity.d/Singularity"), b.Recipe.Raw, 0644) 540 if err != nil { 541 return err 542 } 543 544 return nil 545 } 546 547 func insertLabelsJSON(b *types.Bundle) (err error) { 548 var text []byte 549 labels := make(map[string]string) 550 551 if err = getExistingLabels(labels, b); err != nil { 552 return err 553 } 554 555 if err = addBuildLabels(labels, b); err != nil { 556 return err 557 } 558 559 if b.RunSection("labels") && len(b.Recipe.ImageData.Labels) > 0 { 560 sylog.Infof("Adding labels") 561 562 // add new labels to new map and check for collisions 563 for key, value := range b.Recipe.ImageData.Labels { 564 // check if label already exists 565 if _, ok := labels[key]; ok { 566 // overwrite collision if it exists and force flag is set 567 if b.Opts.Force { 568 labels[key] = value 569 } else { 570 sylog.Warningf("Label: %s already exists and force option is false, not overwriting", key) 571 } 572 } else { 573 // set if it doesnt 574 labels[key] = value 575 } 576 } 577 } 578 579 // make new map into json 580 text, err = json.MarshalIndent(labels, "", "\t") 581 if err != nil { 582 return err 583 } 584 585 err = ioutil.WriteFile(filepath.Join(b.Rootfs(), "/.singularity.d/labels.json"), []byte(text), 0644) 586 return err 587 } 588 589 func getExistingLabels(labels map[string]string, b *types.Bundle) error { 590 // check for existing labels in bundle 591 if _, err := os.Stat(filepath.Join(b.Rootfs(), "/.singularity.d/labels.json")); err == nil { 592 593 jsonFile, err := os.Open(filepath.Join(b.Rootfs(), "/.singularity.d/labels.json")) 594 if err != nil { 595 return err 596 } 597 defer jsonFile.Close() 598 599 jsonBytes, err := ioutil.ReadAll(jsonFile) 600 if err != nil { 601 return err 602 } 603 604 err = json.Unmarshal(jsonBytes, &labels) 605 if err != nil { 606 return err 607 } 608 } 609 return nil 610 } 611 612 func addBuildLabels(labels map[string]string, b *types.Bundle) error { 613 // schema version 614 labels["org.label-schema.schema-version"] = "1.0" 615 616 // build date and time, lots of time formatting 617 currentTime := time.Now() 618 year, month, day := currentTime.Date() 619 date := strconv.Itoa(day) + `_` + month.String() + `_` + strconv.Itoa(year) 620 hour, min, sec := currentTime.Clock() 621 time := strconv.Itoa(hour) + `:` + strconv.Itoa(min) + `:` + strconv.Itoa(sec) 622 zone, _ := currentTime.Zone() 623 timeString := currentTime.Weekday().String() + `_` + date + `_` + time + `_` + zone 624 labels["org.label-schema.build-date"] = timeString 625 626 // singularity version 627 labels["org.label-schema.usage.singularity.version"] = buildcfg.PACKAGE_VERSION 628 629 // help info if help exists in the definition and is run in the build 630 if b.RunSection("help") && b.Recipe.ImageData.Help != "" { 631 labels["org.label-schema.usage"] = "/.singularity.d/runscript.help" 632 labels["org.label-schema.usage.singularity.runscript.help"] = "/.singularity.d/runscript.help" 633 } 634 635 // bootstrap header info, only if this build actually bootstrapped 636 if !b.Opts.Update || b.Opts.Force { 637 for key, value := range b.Recipe.Header { 638 labels["org.label-schema.usage.singularity.deffile."+key] = value 639 } 640 } 641 642 return nil 643 }