github.com/apptainer/singularity@v3.1.1+incompatible/internal/pkg/build/sources/conveyorPacker_oci.go (about) 1 // Copyright (c) 2018-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 sources 7 8 import ( 9 "archive/tar" 10 "bufio" 11 "compress/gzip" 12 "context" 13 "encoding/json" 14 "fmt" 15 "io" 16 "io/ioutil" 17 "net/http" 18 "os" 19 "path/filepath" 20 "strings" 21 22 "github.com/containers/image/copy" 23 "github.com/containers/image/docker" 24 dockerarchive "github.com/containers/image/docker/archive" 25 dockerdaemon "github.com/containers/image/docker/daemon" 26 ociarchive "github.com/containers/image/oci/archive" 27 oci "github.com/containers/image/oci/layout" 28 "github.com/containers/image/signature" 29 "github.com/containers/image/types" 30 imgspecv1 "github.com/opencontainers/image-spec/specs-go/v1" 31 imagetools "github.com/opencontainers/image-tools/image" 32 ociclient "github.com/sylabs/singularity/internal/pkg/client/oci" 33 "github.com/sylabs/singularity/internal/pkg/sylog" 34 "github.com/sylabs/singularity/internal/pkg/util/shell" 35 sytypes "github.com/sylabs/singularity/pkg/build/types" 36 ) 37 38 // OCIConveyorPacker holds stuff that needs to be packed into the bundle 39 type OCIConveyorPacker struct { 40 srcRef types.ImageReference 41 b *sytypes.Bundle 42 tmpfsRef types.ImageReference 43 policyCtx *signature.PolicyContext 44 imgConfig imgspecv1.ImageConfig 45 sysCtx *types.SystemContext 46 } 47 48 // Get downloads container information from the specified source 49 func (cp *OCIConveyorPacker) Get(b *sytypes.Bundle) (err error) { 50 51 cp.b = b 52 53 policy := &signature.Policy{Default: []signature.PolicyRequirement{signature.NewPRInsecureAcceptAnything()}} 54 cp.policyCtx, err = signature.NewPolicyContext(policy) 55 if err != nil { 56 return err 57 } 58 59 cp.sysCtx = &types.SystemContext{ 60 OCIInsecureSkipTLSVerify: cp.b.Opts.NoHTTPS, 61 DockerInsecureSkipTLSVerify: cp.b.Opts.NoHTTPS, 62 DockerAuthConfig: cp.b.Opts.DockerAuthConfig, 63 OSChoice: "linux", 64 } 65 66 // add registry and namespace to reference if specified 67 ref := b.Recipe.Header["from"] 68 if b.Recipe.Header["namespace"] != "" { 69 ref = b.Recipe.Header["namespace"] + "/" + ref 70 } 71 if b.Recipe.Header["registry"] != "" { 72 ref = b.Recipe.Header["registry"] + "/" + ref 73 } 74 sylog.Debugf("Reference: %v", ref) 75 76 switch b.Recipe.Header["bootstrap"] { 77 case "docker": 78 ref = "//" + ref 79 cp.srcRef, err = docker.ParseReference(ref) 80 case "docker-archive": 81 cp.srcRef, err = dockerarchive.ParseReference(ref) 82 case "docker-daemon": 83 cp.srcRef, err = dockerdaemon.ParseReference(ref) 84 case "oci": 85 cp.srcRef, err = oci.ParseReference(ref) 86 case "oci-archive": 87 if os.Geteuid() == 0 { 88 // As root, the direct oci-archive handling will work 89 cp.srcRef, err = ociarchive.ParseReference(ref) 90 } else { 91 // As non-root we need to do a dumb tar extraction first 92 tmpDir, err := ioutil.TempDir("", "temp-oci-") 93 if err != nil { 94 return fmt.Errorf("could not create temporary oci directory: %v", err) 95 } 96 defer os.RemoveAll(tmpDir) 97 98 refParts := strings.SplitN(b.Recipe.Header["from"], ":", 2) 99 err = cp.extractArchive(refParts[0], tmpDir) 100 if err != nil { 101 return fmt.Errorf("error extracting the OCI archive file: %v", err) 102 } 103 // We may or may not have had a ':tag' in the source to handle 104 if len(refParts) == 2 { 105 cp.srcRef, err = oci.ParseReference(tmpDir + ":" + refParts[1]) 106 } else { 107 cp.srcRef, err = oci.ParseReference(tmpDir) 108 } 109 } 110 111 default: 112 return fmt.Errorf("OCI ConveyorPacker does not support %s", b.Recipe.Header["bootstrap"]) 113 } 114 115 if err != nil { 116 return fmt.Errorf("Invalid image source: %v", err) 117 } 118 119 // Grab the modified source ref from the cache 120 cp.srcRef, err = ociclient.ConvertReference(cp.srcRef, cp.sysCtx) 121 if err != nil { 122 return err 123 } 124 125 // To to do the RootFS extraction we also have to have a location that 126 // contains *only* this image 127 cp.tmpfsRef, err = oci.ParseReference(cp.b.Path + ":" + "tmp") 128 129 err = cp.fetch() 130 if err != nil { 131 return err 132 } 133 134 cp.imgConfig, err = cp.getConfig() 135 if err != nil { 136 return err 137 } 138 139 return nil 140 } 141 142 // Pack puts relevant objects in a Bundle! 143 func (cp *OCIConveyorPacker) Pack() (*sytypes.Bundle, error) { 144 err := cp.unpackTmpfs() 145 if err != nil { 146 return nil, fmt.Errorf("While unpacking tmpfs: %v", err) 147 } 148 149 err = cp.insertBaseEnv() 150 if err != nil { 151 return nil, fmt.Errorf("While inserting base environment: %v", err) 152 } 153 154 err = cp.insertRunScript() 155 if err != nil { 156 return nil, fmt.Errorf("While inserting runscript: %v", err) 157 } 158 159 err = cp.insertEnv() 160 if err != nil { 161 return nil, fmt.Errorf("While inserting docker specific environment: %v", err) 162 } 163 164 err = cp.insertOCIConfig() 165 if err != nil { 166 return nil, fmt.Errorf("While inserting oci config: %v", err) 167 } 168 169 return cp.b, nil 170 } 171 172 func (cp *OCIConveyorPacker) fetch() (err error) { 173 // cp.srcRef contains the cache source reference 174 err = copy.Image(context.Background(), cp.policyCtx, cp.tmpfsRef, cp.srcRef, ©.Options{ 175 ReportWriter: ioutil.Discard, 176 SourceCtx: cp.sysCtx, 177 }) 178 if err != nil { 179 return err 180 } 181 182 return nil 183 } 184 185 func (cp *OCIConveyorPacker) getConfig() (imgspecv1.ImageConfig, error) { 186 img, err := cp.srcRef.NewImage(context.Background(), cp.sysCtx) 187 if err != nil { 188 return imgspecv1.ImageConfig{}, err 189 } 190 defer img.Close() 191 192 imgSpec, err := img.OCIConfig(context.Background()) 193 if err != nil { 194 return imgspecv1.ImageConfig{}, err 195 } 196 197 return imgSpec.Config, nil 198 } 199 200 func (cp *OCIConveyorPacker) insertOCIConfig() error { 201 conf, err := json.Marshal(cp.imgConfig) 202 if err != nil { 203 return err 204 } 205 206 cp.b.JSONObjects["oci-config"] = conf 207 return nil 208 } 209 210 // Perform a dumb tar(gz) extraction with no chown, id remapping etc. 211 // This is needed for non-root handling of `oci-archive` as the extraction 212 // by containers/archive is failing when uid/gid don't match local machine 213 // and we're not root 214 func (cp *OCIConveyorPacker) extractArchive(src string, dst string) error { 215 f, err := os.Open(src) 216 if err != nil { 217 return err 218 } 219 defer f.Close() 220 221 r := bufio.NewReader(f) 222 header, err := r.Peek(10) //read a few bytes without consuming 223 if err != nil { 224 return err 225 } 226 gzipped := strings.Contains(http.DetectContentType(header), "x-gzip") 227 228 if gzipped { 229 r, err := gzip.NewReader(f) 230 if err != nil { 231 return err 232 } 233 defer r.Close() 234 } 235 236 tr := tar.NewReader(r) 237 238 for { 239 header, err := tr.Next() 240 241 switch { 242 243 // if no more files are found return 244 case err == io.EOF: 245 return nil 246 247 // return any other error 248 case err != nil: 249 return err 250 251 // if the header is nil, just skip it (not sure how this happens) 252 case header == nil: 253 continue 254 } 255 256 // ZipSlip protection - don't escape from dst 257 target := filepath.Join(dst, header.Name) 258 if !strings.HasPrefix(target, filepath.Clean(dst)+string(os.PathSeparator)) { 259 return fmt.Errorf("%s: illegal extraction path", target) 260 } 261 262 // check the file type 263 switch header.Typeflag { 264 // if its a dir and it doesn't exist create it 265 case tar.TypeDir: 266 if _, err := os.Stat(target); err != nil { 267 if err := os.MkdirAll(target, 0755); err != nil { 268 return err 269 } 270 } 271 // if it's a file create it 272 case tar.TypeReg: 273 f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode)) 274 if err != nil { 275 return err 276 } 277 defer f.Close() 278 279 // copy over contents 280 if _, err := io.Copy(f, tr); err != nil { 281 return err 282 } 283 } 284 } 285 } 286 287 func (cp *OCIConveyorPacker) unpackTmpfs() (err error) { 288 refs := []string{"name=tmp"} 289 err = imagetools.UnpackLayout(cp.b.Path, cp.b.Rootfs(), "amd64", refs) 290 return err 291 } 292 293 func (cp *OCIConveyorPacker) insertBaseEnv() (err error) { 294 if err = makeBaseEnv(cp.b.Rootfs()); err != nil { 295 sylog.Errorf("%v", err) 296 } 297 return 298 } 299 300 func (cp *OCIConveyorPacker) insertRunScript() (err error) { 301 f, err := os.Create(cp.b.Rootfs() + "/.singularity.d/runscript") 302 if err != nil { 303 return 304 } 305 306 defer f.Close() 307 308 _, err = f.WriteString("#!/bin/sh\n") 309 if err != nil { 310 return 311 } 312 313 if len(cp.imgConfig.Entrypoint) > 0 { 314 _, err = f.WriteString("OCI_ENTRYPOINT='" + shell.ArgsQuoted(cp.imgConfig.Entrypoint) + "'\n") 315 if err != nil { 316 return 317 } 318 } else { 319 _, err = f.WriteString("OCI_ENTRYPOINT=''\n") 320 if err != nil { 321 return 322 } 323 } 324 325 if len(cp.imgConfig.Cmd) > 0 { 326 _, err = f.WriteString("OCI_CMD='" + shell.ArgsQuoted(cp.imgConfig.Cmd) + "'\n") 327 if err != nil { 328 return 329 } 330 } else { 331 _, err = f.WriteString("OCI_CMD=''\n") 332 if err != nil { 333 return 334 } 335 } 336 337 _, err = f.WriteString(`CMDLINE_ARGS="" 338 # prepare command line arguments for evaluation 339 for arg in "$@"; do 340 CMDLINE_ARGS="${CMDLINE_ARGS} \"$arg\"" 341 done 342 343 # ENTRYPOINT only - run entrypoint plus args 344 if [ -z "$OCI_CMD" ] && [ -n "$OCI_ENTRYPOINT" ]; then 345 if [ $# -gt 0 ]; then 346 SINGULARITY_OCI_RUN="${OCI_ENTRYPOINT} ${CMDLINE_ARGS}" 347 else 348 SINGULARITY_OCI_RUN="${OCI_ENTRYPOINT}" 349 fi 350 fi 351 352 # CMD only - run CMD or override with args 353 if [ -n "$OCI_CMD" ] && [ -z "$OCI_ENTRYPOINT" ]; then 354 if [ $# -gt 0 ]; then 355 SINGULARITY_OCI_RUN="${CMDLINE_ARGS}" 356 else 357 SINGULARITY_OCI_RUN="${OCI_CMD}" 358 fi 359 fi 360 361 # ENTRYPOINT and CMD - run ENTRYPOINT with CMD as default args 362 # override with user provided args 363 if [ $# -gt 0 ]; then 364 SINGULARITY_OCI_RUN="${OCI_ENTRYPOINT} ${CMDLINE_ARGS}" 365 else 366 SINGULARITY_OCI_RUN="${OCI_ENTRYPOINT} ${OCI_CMD}" 367 fi 368 369 # Evaluate shell expressions first and set arguments accordingly, 370 # then execute final command as first container process 371 eval "set ${SINGULARITY_OCI_RUN}" 372 exec "$@" 373 374 `) 375 if err != nil { 376 return 377 } 378 379 f.Sync() 380 381 err = os.Chmod(cp.b.Rootfs()+"/.singularity.d/runscript", 0755) 382 if err != nil { 383 return 384 } 385 386 return nil 387 } 388 389 func (cp *OCIConveyorPacker) insertEnv() (err error) { 390 f, err := os.Create(cp.b.Rootfs() + "/.singularity.d/env/10-docker2singularity.sh") 391 if err != nil { 392 return 393 } 394 395 defer f.Close() 396 397 _, err = f.WriteString("#!/bin/sh\n") 398 if err != nil { 399 return 400 } 401 402 for _, element := range cp.imgConfig.Env { 403 export := "" 404 envParts := strings.SplitN(element, "=", 2) 405 if len(envParts) == 1 { 406 export = fmt.Sprintf("export %s=${%s:-}\n", envParts[0], envParts[0]) 407 } else { 408 if envParts[0] == "PATH" { 409 export = fmt.Sprintf("export %s=%q\n", envParts[0], shell.Escape(envParts[1])) 410 } else { 411 export = fmt.Sprintf("export %s=${%s:-%q}\n", envParts[0], envParts[0], shell.Escape(envParts[1])) 412 } 413 } 414 _, err = f.WriteString(export) 415 if err != nil { 416 return 417 } 418 } 419 420 f.Sync() 421 422 err = os.Chmod(cp.b.Rootfs()+"/.singularity.d/env/10-docker2singularity.sh", 0755) 423 if err != nil { 424 return 425 } 426 427 return nil 428 } 429 430 // CleanUp removes any tmpfs owned by the conveyorPacker on the filesystem 431 func (cp *OCIConveyorPacker) CleanUp() { 432 os.RemoveAll(cp.b.Path) 433 }