1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256
|
// Copyright (c) 2018-2022, Sylabs Inc. All rights reserved.
// This software is licensed under a 3-clause BSD license. Please consult the
// LICENSE.md file distributed with the sources of this project regarding your
// rights to use or distribute this software.
package assemblers
import (
"bytes"
"fmt"
"os"
"regexp"
"runtime"
"sort"
"strconv"
"syscall"
"github.com/sylabs/sif/v2/pkg/sif"
"github.com/sylabs/singularity/v4/internal/pkg/image/packer"
"github.com/sylabs/singularity/v4/internal/pkg/util/crypt"
"github.com/sylabs/singularity/v4/internal/pkg/util/machine"
"github.com/sylabs/singularity/v4/pkg/build/types"
"github.com/sylabs/singularity/v4/pkg/sylog"
"github.com/sylabs/singularity/v4/pkg/util/cryptkey"
)
// SIFAssembler doesn't store anything.
type SIFAssembler struct {
GzipFlag bool
MksquashfsProcs uint
MksquashfsMem string
MksquashfsPath string
}
type encryptionOptions struct {
keyInfo cryptkey.KeyInfo
plaintext []byte
}
func createSIF(path string, b *types.Bundle, squashfile string, encOpts *encryptionOptions, arch string) (err error) {
var dis []sif.DescriptorInput
// data we need to create a definition file descriptor
definput, err := sif.NewDescriptorInput(sif.DataDeffile, bytes.NewReader(b.Recipe.FullRaw))
if err != nil {
return err
}
// add this descriptor input element to creation descriptor slice
dis = append(dis, definput)
// add all JSON data object within SIF by alphabetical order
sorted := make([]string, 0, len(b.JSONObjects))
for name := range b.JSONObjects {
sorted = append(sorted, name)
}
sort.Strings(sorted)
for _, name := range sorted {
if len(b.JSONObjects[name]) > 0 {
// data we need to create a definition file descriptor
in, err := sif.NewDescriptorInput(sif.DataGenericJSON, bytes.NewReader(b.JSONObjects[name]),
sif.OptObjectName(name),
)
if err != nil {
return err
}
// add this descriptor input element to creation descriptor slice
dis = append(dis, in)
}
}
// open up the data object file for this descriptor
fp, err := os.Open(squashfile)
if err != nil {
return fmt.Errorf("while opening partition file: %s", err)
}
defer fp.Close()
fs := sif.FsSquash
if encOpts != nil {
fs = sif.FsEncryptedSquashfs
}
// data we need to create a system partition descriptor
parinput, err := sif.NewDescriptorInput(sif.DataPartition, fp,
sif.OptPartitionMetadata(fs, sif.PartPrimSys, arch),
)
if err != nil {
return err
}
// add this descriptor input element to the list
dis = append(dis, parinput)
if encOpts != nil {
data, err := cryptkey.EncryptKey(encOpts.keyInfo, encOpts.plaintext)
if err != nil {
return fmt.Errorf("while encrypting filesystem key: %s", err)
}
if data != nil {
syspartID := uint32(len(dis))
part, err := sif.NewDescriptorInput(sif.DataCryptoMessage, bytes.NewReader(data),
sif.OptLinkedID(syspartID),
sif.OptCryptoMessageMetadata(sif.FormatPEM, sif.MessageRSAOAEP),
)
if err != nil {
return err
}
dis = append(dis, part)
}
}
// remove anything that may exist at the build destination at last moment
os.RemoveAll(path)
f, err := sif.CreateContainerAtPath(path,
sif.OptCreateWithLaunchScript("#!/usr/bin/env run-singularity\n"),
sif.OptCreateWithDescriptors(dis...),
)
if err != nil {
return fmt.Errorf("while creating container: %w", err)
}
if err := f.UnloadContainer(); err != nil {
return fmt.Errorf("while unloading container: %w", err)
}
// chown the sif file to the calling user
if uid, gid, ok := changeOwner(); ok {
if err := os.Chown(path, uid, gid); err != nil {
return fmt.Errorf("while changing image ownership: %s", err)
}
}
return nil
}
// Assemble creates a SIF image from a Bundle.
func (a *SIFAssembler) Assemble(b *types.Bundle, path string) error {
sylog.Infof("Creating SIF file...")
s := packer.NewSquashfs()
s.MksquashfsPath = a.MksquashfsPath
f, err := os.CreateTemp(b.TmpDir, "squashfs-")
if err != nil {
return fmt.Errorf("while creating temporary file for squashfs: %v", err)
}
fsPath := f.Name()
f.Close()
defer os.Remove(fsPath)
flags := []string{"-noappend"}
// build squashfs with all-root flag when building as a user
if syscall.Getuid() != 0 {
flags = append(flags, "-all-root")
}
// specify compression if needed
if a.GzipFlag {
flags = append(flags, "-comp", "gzip")
}
if a.MksquashfsMem != "" {
flags = append(flags, "-mem", a.MksquashfsMem)
}
if a.MksquashfsProcs != 0 {
flags = append(flags, "-processors", fmt.Sprint(a.MksquashfsProcs))
}
arch := machine.ArchFromContainer(b.RootfsPath)
if arch == "" {
sylog.Infof("Architecture not recognized, use native")
arch = runtime.GOARCH
}
sylog.Verbosef("Set SIF container architecture to %s", arch)
if err := s.Create([]string{b.RootfsPath}, fsPath, flags); err != nil {
return fmt.Errorf("while creating squashfs: %v", err)
}
var encOpts *encryptionOptions
if b.Opts.EncryptionKeyInfo != nil {
plaintext, err := cryptkey.NewPlaintextKey(*b.Opts.EncryptionKeyInfo)
if err != nil {
return fmt.Errorf("unable to obtain encryption key: %+v", err)
}
// A dm-crypt device needs to be created with squashfs
cryptDev := &crypt.Device{}
// TODO (schebro): Fix #3876
// Detach the following code from the squashfs creation. SIF can be
// created first and encrypted after. This gives the flexibility to
// encrypt an existing SIF
loopPath, err := cryptDev.EncryptFilesystem(fsPath, plaintext)
if err != nil {
return fmt.Errorf("unable to encrypt filesystem at %s: %+v", fsPath, err)
}
defer os.Remove(loopPath)
fsPath = loopPath
encOpts = &encryptionOptions{
keyInfo: *b.Opts.EncryptionKeyInfo,
plaintext: plaintext,
}
}
err = createSIF(path, b, fsPath, encOpts, arch)
if err != nil {
return fmt.Errorf("while creating SIF: %v", err)
}
return nil
}
// changeOwner check the command being called with sudo with the environment
// variable SUDO_COMMAND. Pattern match that for the singularity bin.
func changeOwner() (int, int, bool) {
r := regexp.MustCompile("(singularity)")
sudoCmd := os.Getenv("SUDO_COMMAND")
if !r.MatchString(sudoCmd) {
return 0, 0, false
}
if os.Getenv("SUDO_USER") == "" || syscall.Getuid() != 0 {
return 0, 0, false
}
_uid := os.Getenv("SUDO_UID")
_gid := os.Getenv("SUDO_GID")
if _uid == "" || _gid == "" {
sylog.Warningf("Env vars SUDO_UID or SUDO_GID are not set, won't call chown over built SIF")
return 0, 0, false
}
uid, err := strconv.Atoi(_uid)
if err != nil {
sylog.Warningf("Error while calling strconv: %v", err)
return 0, 0, false
}
gid, err := strconv.Atoi(_gid)
if err != nil {
sylog.Warningf("Error while calling strconv : %v", err)
return 0, 0, false
}
return uid, gid, true
}
|