github.com/mgoltzsche/ctnr@v0.7.1-alpha/bundle/builder/specbuilder.go (about)

     1  // Copyright © 2017 Max Goltzsche
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package builder
    16  
    17  import (
    18  	"os"
    19  	"sort"
    20  	"strconv"
    21  	"strings"
    22  	"time"
    23  
    24  	"github.com/mgoltzsche/ctnr/pkg/idutils"
    25  	ispecs "github.com/opencontainers/image-spec/specs-go/v1"
    26  	"github.com/opencontainers/runc/libcontainer/specconv"
    27  	rspecs "github.com/opencontainers/runtime-spec/specs-go"
    28  	"github.com/opencontainers/runtime-tools/generate"
    29  	"github.com/opencontainers/runtime-tools/generate/seccomp"
    30  	"github.com/pkg/errors"
    31  	"github.com/syndtr/gocapability/capability"
    32  )
    33  
    34  type SpecBuilder struct {
    35  	generate.Generator
    36  	entrypoint    []string
    37  	cmd           []string
    38  	user          idutils.User
    39  	customSeccomp bool
    40  	proot         *prootOptions
    41  	rootless      bool
    42  }
    43  
    44  type prootOptions struct {
    45  	Path  string
    46  	Ports []string
    47  }
    48  
    49  func NewSpecBuilder() SpecBuilder {
    50  	return SpecBuilder{Generator: generate.New()}
    51  }
    52  
    53  func FromSpec(spec *rspecs.Spec) SpecBuilder {
    54  	user := idutils.User{"0", "0"}
    55  	if spec.Process != nil {
    56  		user.User = strconv.Itoa(int(spec.Process.User.UID))
    57  		user.Group = strconv.Itoa(int(spec.Process.User.GID))
    58  		// TODO: map additional gids
    59  	}
    60  	return SpecBuilder{Generator: generate.NewFromSpec(spec)}
    61  }
    62  
    63  func (b *SpecBuilder) ToRootless() {
    64  	specconv.ToRootless(b.Generator.Spec())
    65  	b.rootless = true
    66  }
    67  
    68  func (b *SpecBuilder) UseHostNetwork() {
    69  	b.RemoveLinuxNamespace(rspecs.NetworkNamespace)
    70  	b.SetHostname("") // empty hostname results in host's hostname
    71  	opts := []string{"bind", "mode=0444", "nosuid", "noexec", "nodev", "ro"}
    72  	b.AddBindMount("/etc/hosts", "/etc/hosts", opts)
    73  	b.AddBindMount("/etc/resolv.conf", "/etc/resolv.conf", opts)
    74  }
    75  
    76  func (b *SpecBuilder) SetProcessUser(user idutils.User) {
    77  	b.user = user
    78  }
    79  
    80  func (b *SpecBuilder) AddAllProcessCapabilities() {
    81  	// Add all capabilities
    82  	all := capability.List()
    83  	caps := make([]string, len(all))
    84  	for i, c := range all {
    85  		caps[i] = "CAP_" + strings.ToUpper(c.String())
    86  	}
    87  	c := b.Generator.Spec().Process.Capabilities
    88  	c.Effective = caps
    89  	c.Permitted = caps
    90  	c.Bounding = caps
    91  	c.Ambient = caps
    92  	c.Inheritable = caps
    93  }
    94  
    95  func (b *SpecBuilder) DropAllProcessCapabilities() {
    96  	caps := []string{}
    97  	c := b.Generator.Spec().Process.Capabilities
    98  	c.Effective = caps
    99  	c.Permitted = caps
   100  	c.Bounding = caps
   101  	c.Ambient = caps
   102  	c.Inheritable = caps
   103  }
   104  
   105  // Derives a sane default seccomp profile from the current spec.
   106  // See https://github.com/jessfraz/blog/blob/master/content/post/how-to-use-new-docker-seccomp-profiles.md
   107  // and https://github.com/jessfraz/docker/blob/52f32818df8bad647e4c331878fa44317e724939/docs/security/seccomp.md
   108  func (b *SpecBuilder) SetLinuxSeccompDefault() {
   109  	spec := b.Generator.Spec()
   110  	spec.Linux.Seccomp = seccomp.DefaultProfile(spec)
   111  }
   112  
   113  func (b *SpecBuilder) SetLinuxSeccompUnconfined() {
   114  	spec := b.Generator.Spec()
   115  	profile := seccomp.DefaultProfile(spec)
   116  	profile.DefaultAction = rspecs.ActAllow
   117  	profile.Syscalls = nil
   118  	spec.Linux.Seccomp = profile
   119  	b.customSeccomp = true
   120  }
   121  
   122  func (b *SpecBuilder) SetLinuxSeccomp(profile *rspecs.LinuxSeccomp) {
   123  	spec := b.Generator.Spec()
   124  	if spec.Linux == nil {
   125  		spec.Linux = &rspecs.Linux{}
   126  	}
   127  	spec.Linux.Seccomp = profile
   128  	b.customSeccomp = true
   129  }
   130  
   131  func (b *SpecBuilder) AddExposedPorts(ports []string) {
   132  	// Merge exposedPorts annotation
   133  	exposedPortsAnn := ""
   134  	spec := b.Generator.Spec()
   135  	if spec.Annotations != nil {
   136  		exposedPortsAnn = spec.Annotations["org.opencontainers.image.exposedPorts"]
   137  	}
   138  	exposed := map[string]bool{}
   139  	if exposedPortsAnn != "" {
   140  		for _, exposePortStr := range strings.Split(exposedPortsAnn, ",") {
   141  			exposed[strings.Trim(exposePortStr, " ")] = true
   142  		}
   143  	}
   144  	for _, e := range ports {
   145  		exposed[strings.Trim(e, " ")] = true
   146  	}
   147  	if len(exposed) > 0 {
   148  		exposecsv := make([]string, len(exposed))
   149  		i := 0
   150  		for k := range exposed {
   151  			exposecsv[i] = k
   152  			i++
   153  		}
   154  		sort.Strings(exposecsv)
   155  		b.AddAnnotation("org.opencontainers.image.exposedPorts", strings.Join(exposecsv, ","))
   156  	}
   157  }
   158  
   159  func (b *SpecBuilder) SetPRootPath(prootPath string) {
   160  	if b.proot == nil {
   161  		b.proot = &prootOptions{}
   162  	}
   163  	b.proot.Path = prootPath
   164  	// This has been derived from https://github.com/AkihiroSuda/runrootless/blob/b9a7df0120a7fee15c0223fd0fbc8c3885edd9b3/bundle/spec.go
   165  	b.AddTmpfsMount("/dev/proot", []string{"exec", "mode=755", "size=32256k"})
   166  	b.AddBindMount(prootPath, "/dev/proot/proot", []string{"bind", "ro"})
   167  	b.AddProcessEnv("PROOT_TMP_DIR", "/dev/proot")
   168  	b.AddProcessEnv("PROOT_NO_SECCOMP", "1")
   169  	b.AddProcessCapability("CAP_" + capability.CAP_SYS_PTRACE.String())
   170  }
   171  
   172  func (b *SpecBuilder) AddPRootPortMapping(published, target string) {
   173  	if b.proot == nil {
   174  		b.proot = &prootOptions{}
   175  	}
   176  	b.proot.Ports = append(b.proot.Ports, published+":"+target)
   177  }
   178  
   179  func (b *SpecBuilder) SetProcessEntrypoint(v []string) {
   180  	b.entrypoint = v
   181  	b.cmd = nil
   182  }
   183  
   184  func (b *SpecBuilder) SetProcessCmd(v []string) {
   185  	b.cmd = v
   186  }
   187  
   188  func (b *SpecBuilder) applyEntrypoint() {
   189  	var args []string
   190  	if b.entrypoint != nil || b.cmd != nil {
   191  		if b.entrypoint != nil && b.cmd != nil {
   192  			args = append(b.entrypoint, b.cmd...)
   193  		} else if b.entrypoint != nil {
   194  			args = b.entrypoint
   195  		} else {
   196  			args = b.cmd
   197  		}
   198  	} else {
   199  		args = []string{}
   200  	}
   201  	if b.proot != nil {
   202  		prootArgs := []string{"/dev/proot/proot", "--kill-on-exit", "-n"}
   203  		user := b.user.String()
   204  		if user == "0:0" {
   205  			prootArgs = append(prootArgs, "-0")
   206  		} else {
   207  			prootArgs = append(prootArgs, "-i", b.user.String())
   208  		}
   209  		for _, port := range b.proot.Ports {
   210  			prootArgs = append(prootArgs, "-p", port)
   211  		}
   212  		args = append(prootArgs, args...)
   213  	}
   214  	b.SetProcessArgs(args)
   215  }
   216  
   217  // See image to runtime spec conversion rules: https://github.com/opencontainers/image-spec/blob/master/conversion.md
   218  func (b *SpecBuilder) ApplyImage(img *ispecs.Image) {
   219  	cfg := &img.Config
   220  
   221  	// User
   222  	b.user = idutils.ParseUser(img.Config.User)
   223  
   224  	// Entrypoint
   225  	b.SetProcessEntrypoint(cfg.Entrypoint)
   226  	b.SetProcessCmd(cfg.Cmd)
   227  
   228  	// Env
   229  	if len(cfg.Env) > 0 {
   230  		for _, e := range cfg.Env {
   231  			kv := strings.SplitN(e, "=", 2)
   232  			k := kv[0]
   233  			v := ""
   234  			if len(kv) == 2 {
   235  				v = kv[1]
   236  			}
   237  			b.AddProcessEnv(k, v)
   238  		}
   239  	}
   240  
   241  	// Working dir
   242  	if cfg.WorkingDir != "" {
   243  		b.SetProcessCwd(cfg.WorkingDir)
   244  	}
   245  
   246  	// Annotations
   247  	if cfg.Labels != nil {
   248  		for k, v := range cfg.Labels {
   249  			b.AddAnnotation(k, v)
   250  		}
   251  	}
   252  	// TODO: extract annotations also from image index and manifest
   253  	if img.Author != "" {
   254  		b.AddAnnotation("org.opencontainers.image.author", img.Author)
   255  	}
   256  	if img.Created != nil && !time.Unix(0, 0).Equal(*img.Created) {
   257  		b.AddAnnotation("org.opencontainers.image.created", (*img.Created).String())
   258  	}
   259  	if img.Config.StopSignal != "" {
   260  		b.AddAnnotation("org.opencontainers.image.stopSignal", img.Config.StopSignal)
   261  	}
   262  	if cfg.ExposedPorts != nil {
   263  		ports := make([]string, len(cfg.ExposedPorts))
   264  		i := 0
   265  		for k := range cfg.ExposedPorts {
   266  			ports[i] = k
   267  			i++
   268  		}
   269  		b.AddAnnotation("org.opencontainers.image.exposedPorts", strings.Join(ports, ","))
   270  	}
   271  }
   272  
   273  // Returns the generated spec with resolved user/group names
   274  func (b *SpecBuilder) Spec(rootfs string) (spec *rspecs.Spec, err error) {
   275  	// Resolve user name
   276  	usr, err := b.user.Resolve(rootfs)
   277  	if err != nil {
   278  		return
   279  	}
   280  	b.user = usr.User()
   281  	if usr.Uid > 1<<32 {
   282  		return nil, errors.Errorf("uid %d exceeds range", usr.Uid)
   283  	}
   284  	if usr.Gid > 1<<32 {
   285  		return nil, errors.Errorf("gid %d exceeds range", usr.Gid)
   286  	}
   287  
   288  	// Check uid/gid constraints and proot support
   289  	if b.proot != nil {
   290  		if b.proot.Path == "" {
   291  			return nil, errors.New("proot user or port mappings specified but no proot path provided")
   292  		}
   293  		usr = idutils.UserIds{} // use 0 in native mapping
   294  	} else if b.rootless && (usr.Uid != 0 || usr.Gid != 0) {
   295  		return nil, errors.Errorf("rootless container: only user 0:0 supported but %s provided. hint: enable proot as a workaround", b.user.String())
   296  	}
   297  
   298  	// Apply entrypoint/command (using proot)
   299  	b.applyEntrypoint()
   300  
   301  	// Apply process uid/gid
   302  	b.SetProcessUID(uint32(usr.Uid))
   303  	b.SetProcessGID(uint32(usr.Gid))
   304  	// TODO: set additional gids
   305  
   306  	// Apply native process uid/gid mapping
   307  	if b.rootless {
   308  		b.ClearLinuxUIDMappings()
   309  		b.ClearLinuxGIDMappings()
   310  		b.AddLinuxUIDMapping(uint32(os.Geteuid()), uint32(usr.Uid), 1)
   311  		b.AddLinuxGIDMapping(uint32(os.Getegid()), uint32(usr.Gid), 1)
   312  	}
   313  
   314  	// Generate default seccomp profile
   315  	if !b.customSeccomp {
   316  		b.SetLinuxSeccompDefault()
   317  	}
   318  
   319  	return b.Generator.Spec(), nil
   320  }
   321  
   322  func containsNamespace(ns rspecs.LinuxNamespaceType, l []rspecs.LinuxNamespace) bool {
   323  	for _, e := range l {
   324  		if e.Type == ns {
   325  			return true
   326  		}
   327  	}
   328  	return false
   329  }