Your ROOT_URL in app.ini is unix://git.lalonde.me:3000/ but you are visiting https://git.lalonde.me/matth/subgraph-oz/commit/8ff505f2e3e831e57c185e9f68473f9eb87ea7ae?style=split&whitespace=ignore-all You should set ROOT_URL correctly, otherwise the web may not work correctly.

Oz setup utility, handling of launching from diverted binary, handling of launching in running sandbox

master
xSmurf 10 years ago
parent 00d1aabc25
commit 8ff505f2e3

@ -0,0 +1,311 @@
package main
import (
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"github.com/subgraph/oz"
"github.com/codegangsta/cli"
)
var PathDpkgDivert string
var PathDpkg string
var OzConfig *oz.Config
var OzProfiles *oz.Profiles
var OzProfile *oz.Profile
func init() {
checkRoot()
PathDpkgDivert = checkDpkgDivert()
PathDpkg = checkDpkg()
}
func main() {
app := cli.NewApp()
app.Name = "oz-utils"
app.Usage = "command line interface to install, remove, and create Oz sandboxes\nYou can specify a package name, a binary path, or a Oz profile file "
app.Author = "Subgraph"
app.Email = "info@subgraph.com"
app.Version = "0.0.1"
app.EnableBashCompletion = true
flagsHookMode := []cli.Flag{
cli.BoolFlag{
Name: "hook",
Usage: "Run in hook mode, not normally used by the end user",
},
}
app.Commands = []cli.Command{
{
Name: "config",
Usage: "check and show Oz configurations",
Subcommands: []cli.Command{
{
Name: "check",
Usage: "check oz configuration and profiles for errors",
Action: handleConfigcheck,
},
{
Name: "show",
Usage: "prints ouf oz configuration",
Action: handleConfigshow,
},
},
},
{
Name: "install",
Usage: "install binary diversion for a program",
Action: handleInstall,
Flags: flagsHookMode,
},
{
Name: "remove",
Usage: "remove a binary diversion for a program",
Action: handleRemove,
Flags: flagsHookMode,
},
{
Name: "status",
Usage: "show the status of a binary diversion for a program",
Action: handleStatus,
},
{
Name: "create",
Usage: "create a new sandbox profile",
Action: handleCreate,
},
}
app.Run(os.Args)
}
func handleConfigcheck(c *cli.Context) {
fmt.Println("Here be dragons!")
os.Exit(1)
}
func handleConfigshow(c *cli.Context) {
handleConfigcheck(c)
}
func handleInstall(c *cli.Context) {
OzConfig = loadConfig()
pname := c.Args()[0]
OzProfile, err := loadProfile(pname, OzConfig.ProfileDir)
if err != nil || OzProfile == nil {
installExit(c.Bool("hook"), fmt.Errorf("Unable to load profiles for %s.\n", pname))
return // For clarity
}
if OzConfig.DivertSuffix == "" {
installExit(c.Bool("hook"), fmt.Errorf("Divert requires a suffix to be set.\n"))
return // For clarity
}
isInstalled, err := isDivertInstalled(OzProfile.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "Unknown error: %+v\n", err)
os.Exit(1)
}
if isInstalled == true {
fmt.Println("Divert already installed for ", OzProfile.Path)
os.Exit(0)
}
dpkgArgs := []string{
"--add",
"--package",
"oz",
"--rename",
"--divert",
getBinaryPath(OzProfile.Path),
OzProfile.Path,
}
_, err = exec.Command(PathDpkgDivert, dpkgArgs...).Output()
if err != nil {
fmt.Fprintf(os.Stderr, "Dpkg divert command `%s %+s` failed: %s", PathDpkgDivert, dpkgArgs, err)
os.Exit(1)
}
err = syscall.Symlink(OzConfig.ClientPath, OzProfile.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "Failed to create symlink %s", err)
os.Exit(1)
}
fmt.Printf("Successfully installed Oz sandbox for: %s.\n", OzProfile.Path)
}
func handleRemove(c *cli.Context) {
OzConfig = loadConfig()
pname := c.Args()[0]
OzProfile, err := loadProfile(pname, OzConfig.ProfileDir)
if err != nil || OzProfile == nil {
installExit(c.Bool("hook"), fmt.Errorf("Unable to load profiles for %s.\n", pname))
return // For clarity
}
if OzConfig.DivertSuffix == "" {
installExit(c.Bool("hook"), fmt.Errorf("Divert requires a suffix to be set.\n"))
return // For clarity
}
isInstalled, err := isDivertInstalled(OzProfile.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "Unknown error: %+v\n", err)
os.Exit(1)
}
if isInstalled == false {
fmt.Println("Divert is not installed for ", OzProfile.Path)
os.Exit(0)
}
os.Remove(OzProfile.Path)
dpkgArgs := []string{
"--rename",
"--package",
"oz",
"--remove",
OzProfile.Path,
}
_, err = exec.Command(PathDpkgDivert, dpkgArgs...).Output()
if err != nil {
fmt.Fprintf(os.Stderr, "Dpkg divert command `%s %+s` failed: %s", PathDpkgDivert, dpkgArgs, err)
os.Exit(1)
}
fmt.Printf("Successfully remove jail for: %s.\n", OzProfile.Path)
}
func handleStatus(c *cli.Context) {
OzConfig = loadConfig()
pname := c.Args()[0]
OzProfile, err := loadProfile(pname, OzConfig.ProfileDir)
if err != nil || OzProfile == nil {
fmt.Fprintf(os.Stderr, "Unable to load profiles (%s).\n", err)
os.Exit(1)
}
if OzConfig.DivertSuffix == "" {
fmt.Fprintf(os.Stderr, "Divert requires a suffix to be set.\n")
os.Exit(1)
}
isInstalled, err := isDivertInstalled(OzProfile.Path)
if err != nil {
fmt.Fprintf(os.Stderr, "Unknown error: %+v\n", err)
os.Exit(1)
}
if isInstalled {
fmt.Println("Package divert is \033[0;32minstalled\033[0m for: ", OzProfile.Path)
} else {
fmt.Println("Package divert is \033[0;31mnot installed\033[0m for: ", OzProfile.Path)
}
}
func handleCreate(c *cli.Context) {
OzConfig = loadConfig()
fmt.Println("The weasels ran off with this command... please come back later!")
os.Exit(1)
}
/*
* UTILITIES
*/
func checkRoot() {
if os.Getuid() != 0 {
fmt.Fprintf(os.Stderr, "%s should be used as root.\n", os.Args[0])
os.Exit(1)
}
}
func checkDpkgDivert() string {
ddpath, err := exec.LookPath("dpkg-divert")
if err != nil {
fmt.Fprintln(os.Stderr, "You do not appear to have dpkg-divert, are you not running Debian/Ubuntu?")
os.Exit(1)
}
return ddpath
}
func checkDpkg() string {
dpath, err := exec.LookPath("dpkg")
if err != nil {
fmt.Fprintln(os.Stderr, "You do not appear to have dpkg, are you not running Debian/Ubuntu?")
os.Exit(1)
}
return dpath
}
func isDivertInstalled(bpath string) (bool, error) {
outp, err := exec.Command(PathDpkgDivert, "--truename", bpath).Output()
if err != nil {
return false, err
}
dpath := strings.TrimSpace(string(outp))
isInstalled := (dpath == getBinaryPath(string(bpath)))
if isInstalled {
_, err := os.Readlink(bpath)
if err != nil {
return false, fmt.Errorf("`%s` appears to be diverted but is not installed", dpath)
}
}
return isInstalled, nil
}
func getBinaryPath(bpath string) string {
bpath = strings.TrimSpace(string(bpath))
if strings.HasSuffix(bpath, "."+OzConfig.DivertSuffix) == false {
bpath += "." + OzConfig.DivertSuffix
}
return bpath
}
func loadConfig() *oz.Config {
config, err := oz.LoadConfig(oz.DefaultConfigPath)
if err != nil {
if os.IsNotExist(err) {
fmt.Fprintln(os.Stderr, "Configuration file (%s) is missing, using defaults.", oz.DefaultConfigPath)
config = oz.NewDefaultConfig()
} else {
fmt.Fprintln(os.Stderr, "Could not load configuration: %s", oz.DefaultConfigPath, err)
os.Exit(1)
}
}
return config
}
func loadProfile(name, profileDir string) (*oz.Profile, error) {
ps, err := oz.LoadProfiles(profileDir)
if err != nil {
return nil, err
}
return ps.GetProfileByName(name)
}
func installExit(hook bool, err error) {
if hook {
os.Exit(0)
} else {
fmt.Fprintf(os.Stderr, "%s", err)
os.Exit(1)
}
}

@ -12,8 +12,11 @@ import (
type Config struct { type Config struct {
ProfileDir string `json:"profile_dir"` ProfileDir string `json:"profile_dir"`
ShellPath string `json:"shell_path"` ShellPath string `json:"shell_path"`
InitPath string `json:"init_path"`
ClientPath string `json:"client_path"`
SandboxPath string `json:"sandbox_path"` SandboxPath string `json:"sandbox_path"`
BridgeMACAddr string `json:"bridge_mac"` BridgeMACAddr string `json:"bridge_mac"`
DivertSuffix string `json:"divert_suffix"`
NMIgnoreFile string `json:"nm_ignore_file"` NMIgnoreFile string `json:"nm_ignore_file"`
UseFullDev bool `json:"use_full_dev"` UseFullDev bool `json:"use_full_dev"`
AllowRootShell bool `json:"allow_root_shell"` AllowRootShell bool `json:"allow_root_shell"`
@ -27,9 +30,12 @@ func NewDefaultConfig() *Config {
return &Config{ return &Config{
ProfileDir: "/var/lib/oz/cells.d", ProfileDir: "/var/lib/oz/cells.d",
ShellPath: "/bin/bash", ShellPath: "/bin/bash",
InitPath: "/usr/local/bin/oz-init",
ClientPath: "/usr/local/bin/oz",
SandboxPath: "/srv/oz", SandboxPath: "/srv/oz",
NMIgnoreFile: "/etc/NetworkManager/conf.d/oz.conf", NMIgnoreFile: "/etc/NetworkManager/conf.d/oz.conf",
BridgeMACAddr: "6A:A8:2E:56:E8:9C", BridgeMACAddr: "6A:A8:2E:56:E8:9C",
DivertSuffix: "unsafe",
UseFullDev: false, UseFullDev: false,
AllowRootShell: false, AllowRootShell: false,
LogXpra: false, LogXpra: false,

@ -51,7 +51,7 @@ func ListSandboxes() ([]SandboxInfo, error) {
return body.Sandboxes, nil return body.Sandboxes, nil
} }
func Launch(arg string, env []string) error { func Launch(arg string, args, env []string) error {
idx, name, err := parseProfileArg(arg) idx, name, err := parseProfileArg(arg)
if err != nil { if err != nil {
return err return err
@ -59,6 +59,7 @@ func Launch(arg string, env []string) error {
resp, err := clientSend(&LaunchMsg{ resp, err := clientSend(&LaunchMsg{
Index: idx, Index: idx,
Name: name, Name: name,
Args: args,
Env: env, Env: env,
}) })
if err != nil { if err != nil {

@ -132,13 +132,18 @@ func (d *daemonState) handleLaunch(msg *LaunchMsg, m *ipc.Message) error {
if err != nil { if err != nil {
return m.Respond(&ErrorMsg{err.Error()}) return m.Respond(&ErrorMsg{err.Error()})
} }
if sbox := d.getRunningSandboxByName(p.Name); sbox != nil {
d.Info("Found running sandbox for `%s`, running program there", p.Name)
sbox.launchProgram(msg.Args, d.log)
} else {
d.Debug("Would launch %s", p.Name) d.Debug("Would launch %s", p.Name)
env := d.sanitizeEnvironment(p, msg.Env) env := d.sanitizeEnvironment(p, msg.Env)
_, err = d.launch(p, env, m.Ucred.Uid, m.Ucred.Gid, d.log) _, err = d.launch(p, msg.Args, env, m.Ucred.Uid, m.Ucred.Gid, d.log)
if err != nil { if err != nil {
d.Warning("launch of %s failed: %v", p.Name, err) d.Warning("Launch of %s failed: %v", p.Name, err)
return m.Respond(&ErrorMsg{err.Error()}) return m.Respond(&ErrorMsg{err.Error()})
} }
}
return m.Respond(&OkMsg{}) return m.Respond(&OkMsg{})
} }
@ -213,6 +218,16 @@ func (d *daemonState) getProfileByIdxOrName(index int, name string) (*oz.Profile
return nil, fmt.Errorf("could not find profile name '%s'", name) return nil, fmt.Errorf("could not find profile name '%s'", name)
} }
func (d *daemonState) getRunningSandboxByName(name string) *Sandbox {
for _, sb := range d.sandboxes {
if sb.profile.Name == name {
return sb
}
}
return nil
}
func (d *daemonState) handleListSandboxes(list *ListSandboxesMsg, msg *ipc.Message) error { func (d *daemonState) handleListSandboxes(list *ListSandboxesMsg, msg *ipc.Message) error {
r := new(ListSandboxesResp) r := new(ListSandboxesResp)
for _, sb := range d.sandboxes { for _, sb := range d.sandboxes {

@ -19,8 +19,6 @@ import (
"github.com/subgraph/oz/oz-init" "github.com/subgraph/oz/oz-init"
) )
const initPath = "/usr/local/bin/oz-init"
type Sandbox struct { type Sandbox struct {
daemon *daemonState daemon *daemonState
id int id int
@ -36,7 +34,7 @@ type Sandbox struct {
network *network.SandboxNetwork network *network.SandboxNetwork
} }
func createInitCommand(name, chroot string, env []string, uid uint32, display int, stn *network.SandboxNetwork, nettype string) *exec.Cmd { func createInitCommand(initPath, name, chroot string, env []string, uid uint32, display int, stn *network.SandboxNetwork, nettype string) *exec.Cmd {
cmd := exec.Command(initPath) cmd := exec.Command(initPath)
cmd.Dir = "/" cmd.Dir = "/"
@ -74,7 +72,7 @@ func createInitCommand(name, chroot string, env []string, uid uint32, display in
return cmd return cmd
} }
func (d *daemonState) launch(p *oz.Profile, env []string, uid, gid uint32, log *logging.Logger) (*Sandbox, error) { func (d *daemonState) launch(p *oz.Profile, args, env []string, uid, gid uint32, log *logging.Logger) (*Sandbox, error) {
u, err := user.LookupId(fmt.Sprintf("%d", uid)) u, err := user.LookupId(fmt.Sprintf("%d", uid))
if err != nil { if err != nil {
return nil, fmt.Errorf("failed to lookup user for uid=%d: %v", uid, err) return nil, fmt.Errorf("failed to lookup user for uid=%d: %v", uid, err)
@ -97,7 +95,7 @@ func (d *daemonState) launch(p *oz.Profile, env []string, uid, gid uint32, log *
} }
} }
cmd := createInitCommand(p.Name, fs.Root(), env, uid, display, stn, p.Networking.Nettype) cmd := createInitCommand(d.config.InitPath, p.Name, fs.Root(), env, uid, display, stn, p.Networking.Nettype)
log.Debug("Command environment: %+v", cmd.Env) log.Debug("Command environment: %+v", cmd.Env)
pp, err := cmd.StderrPipe() pp, err := cmd.StderrPipe()
if err != nil { if err != nil {
@ -134,6 +132,12 @@ func (d *daemonState) launch(p *oz.Profile, env []string, uid, gid uint32, log *
sbox.ready.Add(1) sbox.ready.Add(1)
go sbox.logMessages() go sbox.logMessages()
go func () {
sbox.ready.Wait()
go sbox.launchProgram(args, log)
}()
if sbox.profile.XServer.Enabled { if sbox.profile.XServer.Enabled {
go func() { go func() {
sbox.ready.Wait() sbox.ready.Wait()
@ -145,6 +149,13 @@ func (d *daemonState) launch(p *oz.Profile, env []string, uid, gid uint32, log *
return sbox, nil return sbox, nil
} }
func (sbox *Sandbox) launchProgram(args []string, log *logging.Logger) {
err := ozinit.RunProgram(sbox.addr, args)
if err != nil {
log.Error("start shell command failed: %v", err)
}
}
func (sbox *Sandbox) remove(log *logging.Logger) { func (sbox *Sandbox) remove(log *logging.Logger) {
sboxes := []*Sandbox{} sboxes := []*Sandbox{}
for _, sb := range sbox.daemon.sandboxes { for _, sb := range sbox.daemon.sandboxes {

@ -33,6 +33,7 @@ type ListProfilesResp struct {
type LaunchMsg struct { type LaunchMsg struct {
Index int "Launch" Index int "Launch"
Name string Name string
Args []string
Env []string Env []string
} }

@ -41,6 +41,28 @@ func Ping(addr string) error {
} }
} }
func RunProgram(addr string, args []string) error {
c, err := clientConnect(addr)
if err != nil {
return err
}
rr, err := c.ExchangeMsg(&RunProgramMsg{Args: args})
resp := <-rr.Chan()
rr.Done()
c.Close()
if err != nil {
return err
}
switch body := resp.Body.(type) {
case *ErrorMsg:
return errors.New(body.Msg)
case *OkMsg:
return nil
default:
return fmt.Errorf("Unexpected message type received: %+v", body)
}
}
func RunShell(addr, term string) (int, error) { func RunShell(addr, term string) (int, error) {
c, err := clientConnect(addr) c, err := clientConnect(addr)
if err != nil { if err != nil {

@ -181,7 +181,10 @@ func (st *initState) runInit() {
if syscall.Sethostname([]byte(st.profile.Name)) != nil { if syscall.Sethostname([]byte(st.profile.Name)) != nil {
st.log.Error("Failed to set hostname to (%s)", st.profile.Name) st.log.Error("Failed to set hostname to (%s)", st.profile.Name)
} }
st.log.Info("Hostname set to (%s)", st.profile.Name) if syscall.Setdomainname([]byte("local")) != nil {
st.log.Error("Failed to set domainname")
}
st.log.Info("Hostname set to (%s.local)", st.profile.Name)
if err := st.fs.OzInit(); err != nil { if err := st.fs.OzInit(); err != nil {
st.log.Error("Error: setting up filesystem failed: %v\n", err) st.log.Error("Error: setting up filesystem failed: %v\n", err)
@ -194,10 +197,10 @@ func (st *initState) runInit() {
st.startXpraServer() st.startXpraServer()
} }
st.xpraReady.Wait() st.xpraReady.Wait()
st.launchApplication()
s, err := ipc.NewServer(SocketAddress, messageFactory, st.log, s, err := ipc.NewServer(SocketAddress, messageFactory, st.log,
handlePing, handlePing,
st.handleRunProgram,
st.handleRunShell, st.handleRunShell,
) )
if err != nil { if err != nil {
@ -268,17 +271,21 @@ func (st *initState) readXpraOutput(r io.ReadCloser) {
} }
} }
func (st *initState) launchApplication() { func (st *initState) launchApplication(cmdArgs []string) (*exec.Cmd, error) {
cmd := exec.Command(st.profile.Path) suffix := ""
if st.config.DivertSuffix != "" {
suffix = "."+st.config.DivertSuffix
}
cmd := exec.Command(st.profile.Path+suffix)
stdout, err := cmd.StdoutPipe() stdout, err := cmd.StdoutPipe()
if err != nil { if err != nil {
st.log.Warning("Failed to create stdout pipe: %v", err) st.log.Warning("Failed to create stdout pipe: %v", err)
return return nil, err
} }
stderr, err := cmd.StderrPipe() stderr, err := cmd.StderrPipe()
if err != nil { if err != nil {
st.log.Warning("Failed to create stderr pipe: %v", err) st.log.Warning("Failed to create stderr pipe: %v", err)
return return nil, err
} }
cmd.SysProcAttr = &syscall.SysProcAttr{} cmd.SysProcAttr = &syscall.SysProcAttr{}
cmd.SysProcAttr.Credential = &syscall.Credential{ cmd.SysProcAttr.Credential = &syscall.Credential{
@ -286,14 +293,17 @@ func (st *initState) launchApplication() {
Gid: uint32(st.gid), Gid: uint32(st.gid),
} }
cmd.Env = append(cmd.Env, st.launchEnv...) cmd.Env = append(cmd.Env, st.launchEnv...)
cmd.Args = append(cmd.Args, cmdArgs...)
if err := cmd.Start(); err != nil { if err := cmd.Start(); err != nil {
st.log.Warning("Failed to start application (%s): %v", st.profile.Path, err) st.log.Warning("Failed to start application (%s): %v", st.profile.Path, err)
return return nil, err
} }
st.addChildProcess(cmd) st.addChildProcess(cmd)
go st.readApplicationOutput(stdout, "stdout") go st.readApplicationOutput(stdout, "stdout")
go st.readApplicationOutput(stderr, "stderr") go st.readApplicationOutput(stderr, "stderr")
return cmd, nil
} }
func (st *initState) readApplicationOutput(r io.ReadCloser, label string) { func (st *initState) readApplicationOutput(r io.ReadCloser, label string) {
@ -321,6 +331,18 @@ func handlePing(ping *PingMsg, msg *ipc.Message) error {
return msg.Respond(&PingMsg{Data: ping.Data}) return msg.Respond(&PingMsg{Data: ping.Data})
} }
func (st *initState) handleRunProgram(rp *RunProgramMsg, msg *ipc.Message) error {
st.log.Info("Run program message received: %+v", rp)
_, err := st.launchApplication(rp.Args)
if err != nil {
err := msg.Respond(&ErrorMsg{Msg: err.Error()})
return err
} else {
err := msg.Respond(&OkMsg{})
return err
}
}
func (st *initState) handleRunShell(rs *RunShellMsg, msg *ipc.Message) error { func (st *initState) handleRunShell(rs *RunShellMsg, msg *ipc.Message) error {
if msg.Ucred == nil { if msg.Ucred == nil {
return msg.Respond(&ErrorMsg{"No credentials received for RunShell command"}) return msg.Respond(&ErrorMsg{"No credentials received for RunShell command"})

@ -18,9 +18,14 @@ type RunShellMsg struct {
Term string "RunShell" Term string "RunShell"
} }
type RunProgramMsg struct {
Args []string "RunProgram"
}
var messageFactory = ipc.NewMsgFactory( var messageFactory = ipc.NewMsgFactory(
new(OkMsg), new(OkMsg),
new(ErrorMsg), new(ErrorMsg),
new(PingMsg), new(PingMsg),
new(RunShellMsg), new(RunShellMsg),
new(RunProgramMsg),
) )

@ -2,15 +2,53 @@ package main
import ( import (
"fmt" "fmt"
"github.com/codegangsta/cli"
"github.com/subgraph/oz/oz-daemon"
"github.com/subgraph/oz/oz-init"
"io" "io"
"os" "os"
"path"
"strconv" "strconv"
"github.com/subgraph/oz/oz-daemon"
"github.com/subgraph/oz/oz-init"
"github.com/codegangsta/cli"
) )
type fnRunType func()
var runFunc fnRunType
var runBasename string
func init() {
runBasename = path.Base(os.Args[0])
switch runBasename {
case "oz":
runFunc = runApplication
default:
// TODO: Exit if already inside sandbox should only happen
runFunc = runSandbox
}
}
func main() { func main() {
runFunc()
}
func runSandbox() {
hostname, _ := os.Hostname()
if runBasename == hostname {
fmt.Fprintf(os.Stderr, "Cannot launch from inside a sandbox.\n")
os.Exit(1)
}
err := daemon.Launch(runBasename, os.Args[1:], os.Environ())
if err != nil {
fmt.Fprintf(os.Stderr, "launch command failed: %v.\n", err)
os.Exit(1)
}
}
func runApplication() {
app := cli.NewApp() app := cli.NewApp()
app.Name = "oz" app.Name = "oz"
@ -75,9 +113,10 @@ func handleLaunch(c *cli.Context) {
fmt.Println("Argument needed to launch command") fmt.Println("Argument needed to launch command")
os.Exit(1) os.Exit(1)
} }
err := daemon.Launch(c.Args()[0], os.Environ()) err := daemon.Launch(c.Args()[0], c.Args()[1:], os.Environ())
if err != nil { if err != nil {
fmt.Printf("launch command failed: %v\n", err) fmt.Printf("launch command failed: %v\n", err)
os.Exit(1)
} }
} }

@ -97,6 +97,23 @@ func (ps Profiles) GetProfileByName(name string) (*Profile, error) {
return nil, nil return nil, nil
} }
func (ps Profiles) GetProfileByPath(bpath string) (*Profile, error) {
if loadedProfiles == nil {
ps, err := LoadProfiles(defaultProfileDirectory)
if err != nil {
return nil, err
}
loadedProfiles = ps
}
for _, p := range loadedProfiles {
if p.Path == bpath {
return p, nil
}
}
return nil, nil
}
func LoadProfiles(dir string) (Profiles, error) { func LoadProfiles(dir string) (Profiles, error) {
fs, err := ioutil.ReadDir(dir) fs, err := ioutil.ReadDir(dir)
if err != nil { if err != nil {

Loading…
Cancel
Save