github.com/mgoltzsche/ctnr@v0.7.1-alpha/model/oci/ocitransform.go (about)

     1  package oci
     2  
     3  import (
     4  	"encoding/json"
     5  	"io/ioutil"
     6  	"os"
     7  	"path/filepath"
     8  	"sort"
     9  	"strconv"
    10  	"strings"
    11  
    12  	"github.com/mgoltzsche/ctnr/bundle/builder"
    13  	"github.com/mgoltzsche/ctnr/model"
    14  	"github.com/mgoltzsche/ctnr/pkg/idutils"
    15  	"github.com/mgoltzsche/ctnr/pkg/sliceutils"
    16  	specs "github.com/opencontainers/runtime-spec/specs-go"
    17  	"github.com/pkg/errors"
    18  )
    19  
    20  const (
    21  	ANNOTATION_BUNDLE_IMAGE_NAME = "com.github.mgoltzsche.ctnr.bundle.image.name"
    22  	ANNOTATION_BUNDLE_CREATED    = "com.github.mgoltzsche.ctnr.bundle.created"
    23  	ANNOTATION_BUNDLE_ID         = "com.github.mgoltzsche.ctnr.bundle.id"
    24  )
    25  
    26  func ToSpec(service *model.Service, res model.ResourceResolver, rootless bool, ipamDataDir string, prootPath string, spec *builder.BundleBuilder) (err error) {
    27  	defer func() {
    28  		err = errors.Wrap(err, "generate OCI bundle spec")
    29  	}()
    30  
    31  	if rootless {
    32  		spec.ToRootless()
    33  	}
    34  
    35  	sp := spec.Generator.Spec()
    36  
    37  	if err = ToSpecProcess(&service.Process, prootPath, spec.SpecBuilder); err != nil {
    38  		return
    39  	}
    40  
    41  	// Readonly rootfs, mounts
    42  	spec.SetRootReadonly(service.ReadOnly)
    43  
    44  	if err = toMounts(service.Volumes, res, spec); err != nil {
    45  		return
    46  	}
    47  
    48  	// privileged
    49  	seccomp := service.Seccomp
    50  	cgroupsMount := service.MountCgroups
    51  	if service.Privileged {
    52  		if cgroupsMount == "" {
    53  			cgroupsMount = "rw"
    54  		}
    55  		if seccomp == "" {
    56  			seccomp = "unconfined"
    57  		}
    58  		spec.AddBindMount("/dev/net", "/dev/net", []string{"bind"})
    59  	}
    60  
    61  	// Mount cgroups
    62  	if cgroupsMount != "" {
    63  		if err = spec.AddCgroupsMount(cgroupsMount); err != nil {
    64  			return
    65  		}
    66  	}
    67  
    68  	// Annotations
    69  	if service.StopSignal != "" {
    70  		spec.AddAnnotation("org.opencontainers.image.stopSignal", service.StopSignal)
    71  	}
    72  	if service.Expose != nil {
    73  		// Merge exposedPorts annotation
    74  		exposedPortsAnn := ""
    75  		if sp.Annotations != nil {
    76  			exposedPortsAnn = sp.Annotations["org.opencontainers.image.exposedPorts"]
    77  		}
    78  		exposed := map[string]bool{}
    79  		if exposedPortsAnn != "" {
    80  			for _, exposePortStr := range strings.Split(exposedPortsAnn, ",") {
    81  				exposed[strings.Trim(exposePortStr, " ")] = true
    82  			}
    83  		}
    84  		for _, e := range service.Expose {
    85  			exposed[strings.Trim(e, " ")] = true
    86  		}
    87  		if len(exposed) > 0 {
    88  			exposecsv := make([]string, len(exposed))
    89  			i := 0
    90  			for k := range exposed {
    91  				exposecsv[i] = k
    92  				i++
    93  			}
    94  			sort.Strings(exposecsv)
    95  			spec.AddAnnotation("org.opencontainers.image.exposedPorts", strings.Join(exposecsv, ","))
    96  		}
    97  	}
    98  
    99  	// Seccomp
   100  	if seccomp == "" || seccomp == "default" {
   101  		// Derive seccomp configuration (must be called as last)
   102  		spec.SetLinuxSeccompDefault()
   103  	} else if seccomp == "unconfined" {
   104  		// Do not restrict operations with seccomp
   105  		spec.SetLinuxSeccompUnconfined()
   106  	} else {
   107  		// Use seccomp configuration from file
   108  		var j []byte
   109  		if j, err = ioutil.ReadFile(res.ResolveFile(seccomp)); err != nil {
   110  			return
   111  		}
   112  		seccomp := &specs.LinuxSeccomp{}
   113  		if err = json.Unmarshal(j, seccomp); err != nil {
   114  			return
   115  		}
   116  		spec.SetLinuxSeccomp(seccomp)
   117  	}
   118  
   119  	if !rootless {
   120  		// Limit resources
   121  		//spec.SetLinuxResourcesPidsLimit(32771)
   122  		//spec.AddLinuxResourcesHugepageLimit("2MB", 9223372036854772000)
   123  		// TODO: add options to limit memory, cpu and blockIO access
   124  
   125  		/*// Add network priority
   126  		spec.Linux.Resources.Network.ClassID = ""
   127  		spec.Linux.Resources.Network.Priorities = []specs.LinuxInterfacePriority{
   128  			{"eth0", 2},
   129  			{"lo", 1},
   130  		}*/
   131  	}
   132  
   133  	// Init network IDs or host mode
   134  	networks := service.Networks
   135  	useNoNetwork := sliceutils.Contains(networks, "none")
   136  	useHostNetwork := sliceutils.Contains(networks, "host")
   137  	if (useNoNetwork || useHostNetwork) && len(networks) > 1 {
   138  		return errors.New("transform: multiple networks are not supported when 'host' or 'none' network supplied")
   139  	}
   140  	if len(networks) == 0 {
   141  		if rootless {
   142  			networks = []string{}
   143  			useHostNetwork = true
   144  		} else {
   145  			networks = []string{"default"}
   146  		}
   147  	} else if useNoNetwork || useHostNetwork {
   148  		networks = []string{}
   149  	}
   150  
   151  	// Use host network by removing 'network' namespace
   152  	if useHostNetwork {
   153  		spec.UseHostNetwork()
   154  	} else {
   155  		spec.AddOrReplaceLinuxNamespace(specs.NetworkNamespace, "")
   156  	}
   157  
   158  	// Add hostname
   159  	if service.Hostname != "" {
   160  		spec.SetHostname(service.Hostname)
   161  	}
   162  
   163  	// Add network hook
   164  	if len(networks) > 0 {
   165  		spec.AddBindMountConfig("/etc/hostname")
   166  		spec.AddBindMountConfig("/etc/hosts")
   167  		spec.AddBindMountConfig("/etc/resolv.conf")
   168  		hook, err := builder.NewHookBuilderFromSpec(sp)
   169  		if err != nil {
   170  			return err
   171  		}
   172  		hook.SetIPAMDataDir(ipamDataDir)
   173  		for _, net := range networks {
   174  			hook.AddNetwork(net)
   175  		}
   176  		if service.Domainname != "" {
   177  			hook.SetDomainname(service.Domainname)
   178  		}
   179  		for _, dnsip := range service.Dns {
   180  			hook.AddDnsNameserver(dnsip)
   181  		}
   182  		for _, search := range service.DnsSearch {
   183  			hook.AddDnsSearch(search)
   184  		}
   185  		for _, opt := range service.DnsOptions {
   186  			hook.AddDnsOption(opt)
   187  		}
   188  		for _, e := range service.ExtraHosts {
   189  			hook.AddHost(e.Name, e.Ip)
   190  		}
   191  		for _, p := range service.Ports {
   192  			hook.AddPortMapEntry(builder.PortMapEntry{
   193  				Target:    p.Target,
   194  				Published: p.Published,
   195  				Protocol:  p.Protocol,
   196  				IP:        p.IP,
   197  			})
   198  		}
   199  		if err = hook.Build(&spec.Generator); err != nil {
   200  			return err
   201  		}
   202  	} else if len(service.Ports) > 0 {
   203  		if prootPath == "" {
   204  			return errors.New("transform: port mapping only supported with contained container network. hint: add contained network, remove port mapping or, when rootless, enable proot")
   205  		} else {
   206  			for _, port := range service.Ports {
   207  				if port.IP != "" {
   208  					return errors.New("IP is not supported in proot port mappings")
   209  				}
   210  				spec.AddPRootPortMapping(strconv.Itoa(int(port.Published)), strconv.Itoa(int(port.Target)))
   211  			}
   212  		}
   213  	}
   214  	// TODO: support healthcheck (as Hook)
   215  	return nil
   216  }
   217  
   218  func copyHostFile(file, rootDir string) error {
   219  	b, err := ioutil.ReadFile(file)
   220  	if err != nil {
   221  		return err
   222  	}
   223  	err = ioutil.WriteFile(filepath.Join(rootDir, file), b, 0644)
   224  	if err != nil {
   225  		return err
   226  	}
   227  	return nil
   228  }
   229  
   230  func mountHostFile(spec *specs.Spec, file string) error {
   231  	src := file
   232  	fi, err := os.Lstat(file)
   233  	if err != nil {
   234  		return err
   235  	}
   236  
   237  	if fi.Mode()&os.ModeSymlink != 0 {
   238  		src, err = os.Readlink(file)
   239  		if err != nil {
   240  			return err
   241  		}
   242  		if !filepath.IsAbs(src) {
   243  			src = filepath.Join(filepath.Dir(file), src)
   244  		}
   245  	}
   246  
   247  	spec.Mounts = append(spec.Mounts, specs.Mount{
   248  		Type:        "bind",
   249  		Source:      src,
   250  		Destination: file,
   251  		Options:     []string{"bind", "nodev", "mode=0444", "ro"},
   252  	})
   253  	return nil
   254  }
   255  
   256  func ToSpecProcess(p *model.Process, prootPath string, builder *builder.SpecBuilder) (err error) {
   257  	// Entrypoint & command
   258  	if p.Entrypoint != nil {
   259  		builder.SetProcessEntrypoint(p.Entrypoint)
   260  		builder.SetProcessCmd([]string{})
   261  	}
   262  	if p.Command != nil {
   263  		builder.SetProcessCmd(p.Command)
   264  	}
   265  	// Add proot
   266  	if p.PRoot {
   267  		if prootPath == "" {
   268  			return errors.New("proot enabled but no proot path configured")
   269  		}
   270  		builder.SetPRootPath(prootPath)
   271  	}
   272  
   273  	// Env
   274  	for k, v := range p.Environment {
   275  		builder.AddProcessEnv(k, v)
   276  	}
   277  
   278  	// Working dir
   279  	if p.Cwd != "" {
   280  		builder.SetProcessCwd(p.Cwd)
   281  	}
   282  
   283  	// Terminal
   284  	builder.SetProcessTerminal(p.Tty)
   285  
   286  	// User
   287  	if p.User != nil {
   288  		// TODO: map additional groups
   289  		builder.SetProcessUser(idutils.User{p.User.User, p.User.Group})
   290  	}
   291  
   292  	// Privileged
   293  	capAdd := p.CapAdd
   294  	if p.Privileged {
   295  		capAdd = []string{"ALL"}
   296  	}
   297  
   298  	// Capabilities
   299  	for _, addCap := range capAdd {
   300  		if strings.ToUpper(addCap) == "ALL" {
   301  			builder.AddAllProcessCapabilities()
   302  			break
   303  		} else if err = builder.AddProcessCapability("CAP_" + addCap); err != nil {
   304  			return
   305  		}
   306  	}
   307  	for _, dropCap := range p.CapDrop {
   308  		if err = builder.DropProcessCapability("CAP_" + dropCap); err != nil {
   309  			return
   310  		}
   311  	}
   312  
   313  	builder.SetProcessApparmorProfile(p.ApparmorProfile)
   314  	builder.SetProcessNoNewPrivileges(p.NoNewPrivileges)
   315  	builder.SetProcessSelinuxLabel(p.SelinuxLabel)
   316  	if p.OOMScoreAdj != nil {
   317  		builder.SetProcessOOMScoreAdj(*p.OOMScoreAdj)
   318  	}
   319  
   320  	return nil
   321  }
   322  
   323  func toMounts(mounts []model.VolumeMount, res model.ResourceResolver, spec *builder.BundleBuilder) error {
   324  	for _, m := range mounts {
   325  		src, err := res.ResolveMountSource(m)
   326  		if err != nil {
   327  			return err
   328  		}
   329  
   330  		t := m.Type
   331  		if t == "" || t == model.MOUNT_TYPE_VOLUME {
   332  			t = model.MOUNT_TYPE_BIND
   333  		}
   334  		opts := m.Options
   335  		if len(opts) == 0 {
   336  			// Apply default mount options. See man7.org/linux/man-pages/man8/mount.8.html
   337  			opts = []string{"bind", "nodev", "mode=0755"}
   338  		} else {
   339  			sliceutils.AddToSet(&opts, "bind")
   340  		}
   341  
   342  		sp := spec.Generator.Spec()
   343  		sp.Mounts = append(sp.Mounts, specs.Mount{
   344  			Type:        string(t),
   345  			Source:      src,
   346  			Destination: m.Target,
   347  			Options:     opts,
   348  		})
   349  	}
   350  	return nil
   351  }