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, &copy.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  }