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 }