func writeAppReaper(p *stage1commontypes.Pod, appName string) error { opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("%s Reaper", appName)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "StopWhenUnneeded", "yes"), unit.NewUnitOption("Unit", "Wants", "shutdown.service"), unit.NewUnitOption("Unit", "After", "shutdown.service"), unit.NewUnitOption("Unit", "Conflicts", "exit.target"), unit.NewUnitOption("Unit", "Conflicts", "halt.target"), unit.NewUnitOption("Unit", "Conflicts", "poweroff.target"), unit.NewUnitOption("Service", "RemainAfterExit", "yes"), unit.NewUnitOption("Service", "ExecStop", fmt.Sprintf("/reaper.sh %s", appName)), } unitsPath := filepath.Join(common.Stage1RootfsPath(p.Root), UnitsDir) file, err := os.OpenFile(filepath.Join(unitsPath, fmt.Sprintf("reaper-%s.service", appName)), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return fmt.Errorf("failed to create service unit file: %v", err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return fmt.Errorf("failed to write service unit file: %v", err) } return nil }
// WriteDefaultTarget writes the default.target unit file // which is responsible for bringing up the applications func WriteDefaultTarget(p *stage1commontypes.Pod) error { opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", "rkt apps target"), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), } for i := range p.Manifest.Apps { ra := &p.Manifest.Apps[i] serviceName := ServiceUnitName(ra.Name) opts = append(opts, unit.NewUnitOption("Unit", "After", serviceName)) opts = append(opts, unit.NewUnitOption("Unit", "Wants", serviceName)) } unitsPath := filepath.Join(common.Stage1RootfsPath(p.Root), UnitsDir) file, err := os.OpenFile(filepath.Join(unitsPath, "default.target"), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644) if err != nil { return err } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return err } return nil }
func GenerateNetworkInterfaceUnits(unitsPath string, netDescriptions []netDescriber) error { for i, netDescription := range netDescriptions { ifName := fmt.Sprintf(networking.IfNamePattern, i) netAddress := net.IPNet{ IP: netDescription.GuestIP(), Mask: net.IPMask(netDescription.Mask()), } address := netAddress.String() mac, err := generateMacAddress() if err != nil { return err } opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Network configuration for device: %v", ifName)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Service", "Type", "oneshot"), unit.NewUnitOption("Service", "RemainAfterExit", "true"), unit.NewUnitOption("Service", "ExecStartPre", downInterfaceCommand(ifName)), unit.NewUnitOption("Service", "ExecStartPre", setMacCommand(ifName, mac.String())), unit.NewUnitOption("Service", "ExecStartPre", upInterfaceCommand(ifName)), unit.NewUnitOption("Service", "ExecStart", addAddressCommand(address, ifName)), unit.NewUnitOption("Install", "RequiredBy", "default.target"), } for _, route := range netDescription.Routes() { gw := route.GW if gw == nil { gw = netDescription.Gateway() } opts = append( opts, unit.NewUnitOption( "Service", "ExecStartPost", addRouteCommand(route.Dst.String(), gw.String()), ), ) } unitName := fmt.Sprintf("interface-%s", ifName) + ".service" unitBytes, err := ioutil.ReadAll(unit.Serialize(opts)) if err != nil { return fmt.Errorf("failed to serialize network unit file to bytes %q: %v", unitName, err) } err = ioutil.WriteFile(filepath.Join(unitsPath, unitName), unitBytes, 0644) if err != nil { return fmt.Errorf("failed to create network unit file %q: %v", unitName, err) } log.Printf("network unit created: %q in %q (iface=%q, addr=%q)", unitName, unitsPath, ifName, address) } return nil }
// installNewMountUnit creates and installs new mount unit in default // systemd location (/usr/lib/systemd/system) in pod stage1 filesystem. // root is a stage1 relative to pod filesystem path like /var/lib/uuid/rootfs/ // (from Pod.Root). // beforeAndrequiredBy creates systemd unit dependency (can be space separated // for multi). func installNewMountUnit(root, what, where, fsType, options, beforeAndrequiredBy, unitsDir string) error { opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Mount unit for %s", where)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "Before", beforeAndrequiredBy), unit.NewUnitOption("Mount", "What", what), unit.NewUnitOption("Mount", "Where", where), unit.NewUnitOption("Mount", "Type", fsType), unit.NewUnitOption("Mount", "Options", options), unit.NewUnitOption("Install", "RequiredBy", beforeAndrequiredBy), } unitsPath := filepath.Join(root, unitsDir) unitName := unit.UnitNamePathEscape(where + ".mount") unitBytes, err := ioutil.ReadAll(unit.Serialize(opts)) if err != nil { return fmt.Errorf("failed to serialize mount unit file to bytes %q: %v", unitName, err) } err = ioutil.WriteFile(filepath.Join(unitsPath, unitName), unitBytes, 0644) if err != nil { return fmt.Errorf("failed to create mount unit file %q: %v", unitName, err) } log.Printf("mount unit created: %q in %q (what=%q, where=%q)", unitName, unitsPath, what, where) return nil }
func addCpuLimit(opts []*unit.UnitOption, limit *resource.Quantity) ([]*unit.UnitOption, error) { if limit.Value() > resource.MaxMilliValue { return nil, fmt.Errorf("cpu limit exceeds the maximum millivalue: %v", limit.String()) } quota := strconv.Itoa(int(limit.MilliValue()/10)) + "%" opts = append(opts, unit.NewUnitOption("Service", "CPUQuota", quota)) return opts, nil }
func addCpuLimit(opts []*unit.UnitOption, limit string) ([]*unit.UnitOption, error) { milliCores, err := strconv.Atoi(limit) if err != nil { return nil, err } quota := strconv.Itoa(milliCores/10) + "%" opts = append(opts, unit.NewUnitOption("Service", "CPUQuota", quota)) return opts, nil }
// installNewMountUnit creates and installs a new mount unit in the default // systemd location (/usr/lib/systemd/system) inside the pod stage1 filesystem. // root is pod's absolute stage1 path (from Pod.Root). // beforeAndrequiredBy creates a systemd unit dependency (can be space separated // for multi). // It returns the name of the generated unit. func installNewMountUnit(root, what, where, fsType, options, beforeAndrequiredBy, unitsDir string) (string, error) { opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Mount unit for %s", where)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "Before", beforeAndrequiredBy), unit.NewUnitOption("Mount", "What", what), unit.NewUnitOption("Mount", "Where", where), unit.NewUnitOption("Mount", "Type", fsType), unit.NewUnitOption("Mount", "Options", options), unit.NewUnitOption("Install", "RequiredBy", beforeAndrequiredBy), } unitsPath := filepath.Join(root, unitsDir) unitName := unit.UnitNamePathEscape(where + ".mount") if err := writeUnit(opts, filepath.Join(unitsPath, unitName)); err != nil { return "", err } log.Printf("mount unit created: %q in %q (what=%q, where=%q)", unitName, unitsPath, what, where) return unitName, nil }
// appToSystemd transforms the provided RuntimeApp+ImageManifest into systemd units func appToSystemd(p *stage1commontypes.Pod, ra *schema.RuntimeApp, interactive bool, flavor string, privateUsers string) error { app := ra.App appName := ra.Name image, ok := p.Images[appName.String()] if !ok { // This is impossible as we have updated the map in LoadPod(). panic(fmt.Sprintf("No images for app %q", ra.Name.String())) } imgName := image.Name if len(app.Exec) == 0 { return fmt.Errorf(`image %q has an empty "exec" (try --exec=BINARY)`, imgName) } workDir := "/" if app.WorkingDirectory != "" { workDir = app.WorkingDirectory } env := app.Environment env.Set("AC_APP_NAME", appName.String()) if p.MetadataServiceURL != "" { env.Set("AC_METADATA_URL", p.MetadataServiceURL) } if err := writeEnvFile(p, env, appName, privateUsers); err != nil { return fmt.Errorf("unable to write environment file: %v", err) } // This is a partial implementation for app.User and app.Group: // For now, only numeric ids (and the string "root") are supported. var uid, gid int var err error if app.User == "root" { uid = 0 } else { uid, err = strconv.Atoi(app.User) if err != nil { return fmt.Errorf("non-numerical user id not supported yet") } } if app.Group == "root" { gid = 0 } else { gid, err = strconv.Atoi(app.Group) if err != nil { return fmt.Errorf("non-numerical group id not supported yet") } } execWrap := []string{"/appexec", common.RelAppRootfsPath(appName), workDir, RelEnvFilePath(appName), strconv.Itoa(uid), generateGidArg(gid, app.SupplementaryGIDs)} execStart := quoteExec(append(execWrap, app.Exec...)) opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Application=%v Image=%v", appName, imgName)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "Wants", fmt.Sprintf("reaper-%s.service", appName)), unit.NewUnitOption("Service", "Restart", "no"), unit.NewUnitOption("Service", "ExecStart", execStart), unit.NewUnitOption("Service", "User", "0"), unit.NewUnitOption("Service", "Group", "0"), } if interactive { opts = append(opts, unit.NewUnitOption("Service", "StandardInput", "tty")) opts = append(opts, unit.NewUnitOption("Service", "StandardOutput", "tty")) opts = append(opts, unit.NewUnitOption("Service", "StandardError", "tty")) } else { opts = append(opts, unit.NewUnitOption("Service", "StandardOutput", "journal+console")) opts = append(opts, unit.NewUnitOption("Service", "StandardError", "journal+console")) opts = append(opts, unit.NewUnitOption("Service", "SyslogIdentifier", filepath.Base(app.Exec[0]))) } // When an app fails, we shut down the pod opts = append(opts, unit.NewUnitOption("Unit", "OnFailure", "halt.target")) for _, eh := range app.EventHandlers { var typ string switch eh.Name { case "pre-start": typ = "ExecStartPre" case "post-stop": typ = "ExecStopPost" default: return fmt.Errorf("unrecognized eventHandler: %v", eh.Name) } exec := quoteExec(append(execWrap, eh.Exec...)) opts = append(opts, unit.NewUnitOption("Service", typ, exec)) } // Some pre-start jobs take a long time, set the timeout to 0 opts = append(opts, unit.NewUnitOption("Service", "TimeoutStartSec", "0")) var saPorts []types.Port for _, p := range app.Ports { if p.SocketActivated { saPorts = append(saPorts, p) } } for _, i := range app.Isolators { switch v := i.Value().(type) { case *types.ResourceMemory: opts, err = cgroup.MaybeAddIsolator(opts, "memory", v.Limit()) if err != nil { return err } case *types.ResourceCPU: opts, err = cgroup.MaybeAddIsolator(opts, "cpu", v.Limit()) if err != nil { return err } } } if len(saPorts) > 0 { sockopts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Application=%v Image=%v %s", appName, imgName, "socket-activated ports")), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Socket", "BindIPv6Only", "both"), unit.NewUnitOption("Socket", "Service", ServiceUnitName(appName)), } for _, sap := range saPorts { var proto string switch sap.Protocol { case "tcp": proto = "ListenStream" case "udp": proto = "ListenDatagram" default: return fmt.Errorf("unrecognized protocol: %v", sap.Protocol) } sockopts = append(sockopts, unit.NewUnitOption("Socket", proto, fmt.Sprintf("%v", sap.Port))) } file, err := os.OpenFile(SocketUnitPath(p.Root, appName), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return fmt.Errorf("failed to create socket file: %v", err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(sockopts)); err != nil { return fmt.Errorf("failed to write socket unit file: %v", err) } if err = os.Symlink(path.Join("..", SocketUnitName(appName)), SocketWantPath(p.Root, appName)); err != nil { return fmt.Errorf("failed to link socket want: %v", err) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", SocketUnitName(appName))) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", InstantiatedPrepareAppUnitName(appName))) opts = append(opts, unit.NewUnitOption("Unit", "After", InstantiatedPrepareAppUnitName(appName))) file, err := os.OpenFile(ServiceUnitPath(p.Root, appName), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return fmt.Errorf("failed to create service unit file: %v", err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return fmt.Errorf("failed to write service unit file: %v", err) } if err = os.Symlink(path.Join("..", ServiceUnitName(appName)), ServiceWantPath(p.Root, appName)); err != nil { return fmt.Errorf("failed to link service want: %v", err) } if flavor == "kvm" { // bind mount all shared volumes from /mnt/volumeName (we don't use mechanism for bind-mounting given by nspawn) err := AppToSystemdMountUnits(common.Stage1RootfsPath(p.Root), appName, p.Manifest.Volumes, ra, UnitsDir) if err != nil { return fmt.Errorf("failed to prepare mount units: %v", err) } } if err = writeAppReaper(p, appName.String()); err != nil { return fmt.Errorf("Failed to write app %q reaper service: %v\n", appName, err) } return nil }
// WritePrepareAppTemplate writes service unit files for preparing the pod's applications func WritePrepareAppTemplate(p *stage1commontypes.Pod) error { opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", "Prepare minimum environment for chrooted applications"), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "OnFailureJobMode", "fail"), unit.NewUnitOption("Unit", "Requires", "systemd-journald.service"), unit.NewUnitOption("Unit", "After", "systemd-journald.service"), unit.NewUnitOption("Service", "Type", "oneshot"), unit.NewUnitOption("Service", "Restart", "no"), unit.NewUnitOption("Service", "ExecStart", "/prepare-app %I"), unit.NewUnitOption("Service", "User", "0"), unit.NewUnitOption("Service", "Group", "0"), unit.NewUnitOption("Service", "CapabilityBoundingSet", "CAP_SYS_ADMIN CAP_DAC_OVERRIDE"), } unitsPath := filepath.Join(common.Stage1RootfsPath(p.Root), UnitsDir) file, err := os.OpenFile(filepath.Join(unitsPath, "[email protected]"), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return fmt.Errorf("failed to create service unit file: %v", err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return fmt.Errorf("failed to write service unit file: %v", err) } return nil }
func addMemoryLimit(opts []*unit.UnitOption, limit *resource.Quantity) ([]*unit.UnitOption, error) { opts = append(opts, unit.NewUnitOption("Service", "MemoryLimit", strconv.Itoa(int(limit.Value())))) return opts, nil }
// appToSystemd transforms the provided RuntimeApp+ImageManifest into systemd units func (p *Pod) appToSystemd(ra *schema.RuntimeApp, interactive bool) error { name := ra.Name.String() id := ra.Image.ID app := ra.App workDir := "/" if app.WorkingDirectory != "" { workDir = app.WorkingDirectory } env := app.Environment env.Set("AC_APP_NAME", name) env.Set("AC_METADATA_URL", p.MetadataServiceURL) if err := p.writeEnvFile(env, id); err != nil { return fmt.Errorf("unable to write environment file: %v", err) } // This is a partial implementation for app.User and app.Group: // For now, only numeric ids (and the string "root") are supported. var uid, gid int var err error if app.User == "root" { uid = 0 } else { uid, err = strconv.Atoi(app.User) if err != nil { return fmt.Errorf("non-numerical user id not supported yet") } } if app.Group == "root" { gid = 0 } else { gid, err = strconv.Atoi(app.Group) if err != nil { return fmt.Errorf("non-numerical group id not supported yet") } } execWrap := []string{"/diagexec", common.RelAppRootfsPath(id), workDir, RelEnvFilePath(id), strconv.Itoa(uid), strconv.Itoa(gid)} execStart := quoteExec(append(execWrap, app.Exec...)) opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", name), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "OnFailure", "reaper.service"), unit.NewUnitOption("Unit", "Wants", "exit-watcher.service"), unit.NewUnitOption("Service", "Restart", "no"), unit.NewUnitOption("Service", "ExecStart", execStart), unit.NewUnitOption("Service", "User", "0"), unit.NewUnitOption("Service", "Group", "0"), } _, systemdStage1Version, err := p.getFlavor() if err != nil { return fmt.Errorf("Failed to get stage1 flavor: %v\n", err) } if interactive { opts = append(opts, unit.NewUnitOption("Service", "StandardInput", "tty")) opts = append(opts, unit.NewUnitOption("Service", "StandardOutput", "tty")) opts = append(opts, unit.NewUnitOption("Service", "StandardError", "tty")) } else if systemdSupportsJournalLinking(systemdStage1Version) { opts = append(opts, unit.NewUnitOption("Service", "StandardOutput", "journal+console")) opts = append(opts, unit.NewUnitOption("Service", "StandardError", "journal+console")) opts = append(opts, unit.NewUnitOption("Service", "SyslogIdentifier", filepath.Base(app.Exec[0]))) } for _, eh := range app.EventHandlers { var typ string switch eh.Name { case "pre-start": typ = "ExecStartPre" case "post-stop": typ = "ExecStopPost" default: return fmt.Errorf("unrecognized eventHandler: %v", eh.Name) } exec := quoteExec(append(execWrap, eh.Exec...)) opts = append(opts, unit.NewUnitOption("Service", typ, exec)) } saPorts := []types.Port{} for _, p := range app.Ports { if p.SocketActivated { saPorts = append(saPorts, p) } } for _, i := range app.Isolators { switch v := i.Value().(type) { case *types.ResourceMemory: limit := v.Limit().String() opts, err = cgroup.MaybeAddIsolator(opts, "memory", limit) if err != nil { return err } case *types.ResourceCPU: limit := v.Limit().String() opts, err = cgroup.MaybeAddIsolator(opts, "cpu", limit) if err != nil { return err } } } if len(saPorts) > 0 { sockopts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", name+" socket-activated ports"), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Socket", "BindIPv6Only", "both"), unit.NewUnitOption("Socket", "Service", ServiceUnitName(id)), } for _, sap := range saPorts { var proto string switch sap.Protocol { case "tcp": proto = "ListenStream" case "udp": proto = "ListenDatagram" default: return fmt.Errorf("unrecognized protocol: %v", sap.Protocol) } sockopts = append(sockopts, unit.NewUnitOption("Socket", proto, fmt.Sprintf("%v", sap.Port))) } file, err := os.OpenFile(SocketUnitPath(p.Root, id), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return fmt.Errorf("failed to create socket file: %v", err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(sockopts)); err != nil { return fmt.Errorf("failed to write socket unit file: %v", err) } if err = os.Symlink(path.Join("..", SocketUnitName(id)), SocketWantPath(p.Root, id)); err != nil { return fmt.Errorf("failed to link socket want: %v", err) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", SocketUnitName(id))) } opts = append(opts, unit.NewUnitOption("Unit", "Requires", InstantiatedPrepareAppUnitName(id))) opts = append(opts, unit.NewUnitOption("Unit", "After", InstantiatedPrepareAppUnitName(id))) file, err := os.OpenFile(ServiceUnitPath(p.Root, id), os.O_WRONLY|os.O_CREATE, 0644) if err != nil { return fmt.Errorf("failed to create service unit file: %v", err) } defer file.Close() if _, err = io.Copy(file, unit.Serialize(opts)); err != nil { return fmt.Errorf("failed to write service unit file: %v", err) } if err = os.Symlink(path.Join("..", ServiceUnitName(id)), ServiceWantPath(p.Root, id)); err != nil { return fmt.Errorf("failed to link service want: %v", err) } return nil }
// AppToSystemdMountUnits prepare bind mount unit for empty or host kind mounting // between stage1 rootfs and chrooted filesystem for application func AppToSystemdMountUnits(root string, appName types.ACName, volumes []types.Volume, ra *schema.RuntimeApp, unitsDir string) error { app := ra.App vols := make(map[types.ACName]types.Volume) for _, v := range volumes { vols[v.Name] = v } mounts, err := initcommon.GenerateMounts(ra, vols) if err != nil { return err } for _, m := range mounts { vol := vols[m.Volume] // source relative to stage1 rootfs to relative pod root whatPath := filepath.Join(stage1MntDir, vol.Name.String()) whatFullPath := filepath.Join(root, whatPath) // destination relative to stage1 rootfs and relative to pod root wherePath := filepath.Join(common.RelAppRootfsPath(appName), m.Path) whereFullPath := filepath.Join(root, wherePath) // assertion to make sure that "what" exists (created earlier by PodToSystemdHostMountUnits) log.Printf("checking required source path: %q", whatFullPath) if _, err := os.Stat(whatFullPath); os.IsNotExist(err) { return fmt.Errorf("bug: missing source for volume %v", vol.Name) } // optionally prepare app directory log.Printf("optionally preparing destination path: %q", whereFullPath) err := os.MkdirAll(whereFullPath, 0700) if err != nil { return fmt.Errorf("failed to prepare dir for mount %v: %v", m.Volume, err) } // install new mount unit for bind mount /mnt/volumeName -> /opt/stage2/{app-id}/rootfs/{{mountPoint.Path}} mu, err := installNewMountUnit( root, // where put a mount unit whatPath, // what - stage1 rootfs /mnt/VolumeName wherePath, // where - inside chroot app filesystem "bind", // fstype "bind", // options serviceUnitName(appName), unitsDir, ) if err != nil { return fmt.Errorf("cannot install new mount unit for app %q: %v", appName.String(), err) } // TODO(iaguis) when we update util-linux to 2.27, this code can go // away and we can bind-mount RO with one unit file. // http://ftp.kernel.org/pub/linux/utils/util-linux/v2.27/v2.27-ReleaseNotes if initcommon.IsMountReadOnly(vol, app.MountPoints) { opts := []*unit.UnitOption{ unit.NewUnitOption("Unit", "Description", fmt.Sprintf("Remount read-only unit for %s", wherePath)), unit.NewUnitOption("Unit", "DefaultDependencies", "false"), unit.NewUnitOption("Unit", "After", mu), unit.NewUnitOption("Unit", "Wants", mu), unit.NewUnitOption("Service", "ExecStart", fmt.Sprintf("/usr/bin/mount -o remount,ro %s", wherePath)), unit.NewUnitOption("Install", "RequiredBy", mu), } remountUnitPath := filepath.Join(root, unitsDir, unit.UnitNamePathEscape(wherePath+"-remount.service")) if err := writeUnit(opts, remountUnitPath); err != nil { return err } } } return nil }
func addMemoryLimit(opts []*unit.UnitOption, limit string) ([]*unit.UnitOption, error) { opts = append(opts, unit.NewUnitOption("Service", "MemoryLimit", limit)) return opts, nil }