From be3d70537b9891d3abd22fcd02a48935c203396e Mon Sep 17 00:00:00 2001 From: Zach Wasserman Date: Tue, 16 Feb 2021 18:05:18 -0800 Subject: [PATCH] macOS packaging works with launchd --- cmd/package/package.go | 8 +- pkg/packaging/deb.go | 30 ++--- pkg/packaging/macos.go | 191 +++++++++++++++++++++---------- pkg/packaging/macos_templates.go | 52 ++++++++- pkg/packaging/packaging.go | 32 ++++++ 5 files changed, 232 insertions(+), 81 deletions(-) diff --git a/cmd/package/package.go b/cmd/package/package.go index 355a0e274f..945334be17 100644 --- a/cmd/package/package.go +++ b/cmd/package/package.go @@ -55,6 +55,12 @@ func main() { Usage: "Disable TLS certificate verification", Destination: &opt.Insecure, }, + &cli.BoolFlag{ + Name: "service", + Usage: "Install orbit/osquery with a persistence service (launchd, systemd, etc.)", + Value: true, + Destination: &opt.StartService, + }, &cli.BoolFlag{ Name: "debug", Usage: "Enable debug logging", @@ -68,8 +74,6 @@ func main() { } app.Action = func(c *cli.Context) error { if opt.FleetURL != "" || opt.EnrollSecret != "" { - opt.StartService = true - if opt.FleetURL == "" || opt.EnrollSecret == "" { return errors.New("--enroll-secret and --fleet-url must be provided together") } diff --git a/pkg/packaging/deb.go b/pkg/packaging/deb.go index 49e43466dc..13b09bc0c5 100644 --- a/pkg/packaging/deb.go +++ b/pkg/packaging/deb.go @@ -36,21 +36,6 @@ func BuildDeb(opt Options) error { return errors.Wrap(err, "create orbit dir") } - // Write files - - if err := writeSystemdUnit(opt, filesystemRoot); err != nil { - return errors.Wrap(err, "write systemd unit") - } - - if err := writeEnvFile(opt, filesystemRoot); err != nil { - return errors.Wrap(err, "write env file") - } - - postInstallPath := filepath.Join(tmpDir, "postinstall.sh") - if err := writePostInstall(opt, postInstallPath); err != nil { - return errors.Wrap(err, "write postinstall script") - } - // Initialize autoupdate metadata localStore, err := filestore.New(filepath.Join(orbitRoot, "tuf-metadata.json")) @@ -76,6 +61,21 @@ func BuildDeb(opt Options) error { } log.Debug().Str("path", osquerydPath).Msg("got osqueryd") + // Write files + + if err := writeSystemdUnit(opt, filesystemRoot); err != nil { + return errors.Wrap(err, "write systemd unit") + } + + if err := writeEnvFile(opt, filesystemRoot); err != nil { + return errors.Wrap(err, "write env file") + } + + postInstallPath := filepath.Join(tmpDir, "postinstall.sh") + if err := writePostInstall(opt, postInstallPath); err != nil { + return errors.Wrap(err, "write postinstall script") + } + // Pick up all file contents contents := files.Contents{ diff --git a/pkg/packaging/macos.go b/pkg/packaging/macos.go index 294e9e3ccc..9c458e74e0 100644 --- a/pkg/packaging/macos.go +++ b/pkg/packaging/macos.go @@ -27,34 +27,18 @@ func BuildPkg(opt Options) error { if err != nil { return errors.Wrap(err, "failed to create temp dir") } - // TODO reenable - //defer os.RemoveAll(tmpDir) + defer os.RemoveAll(tmpDir) log.Debug().Str("path", tmpDir).Msg("created temp dir") filesystemRoot := filepath.Join(tmpDir, "root") if err := os.MkdirAll(filesystemRoot, constant.DefaultDirMode); err != nil { return errors.Wrap(err, "create root dir") } - // if err := os.MkdirAll( - // filepath.Join(filesystemRoot, "Resources", "en.lproj"), - // constant.DefaultDirMode, - // ); err != nil { - // return errors.Wrap(err, "create resources dir") - // } orbitRoot := filepath.Join(filesystemRoot, "var", "lib", "fleet", "orbit") if err := os.MkdirAll(orbitRoot, constant.DefaultDirMode); err != nil { return errors.Wrap(err, "create orbit dir") } - // Write files - - if err := writePackageInfo(opt, tmpDir); err != nil { - return errors.Wrap(err, "write PackageInfo") - } - if err := writeDistribution(opt, tmpDir); err != nil { - return errors.Wrap(err, "write Distribution") - } - // Initialize autoupdate metadata localStore, err := filestore.New(filepath.Join(orbitRoot, "tuf-metadata.json")) @@ -80,6 +64,30 @@ func BuildPkg(opt Options) error { } log.Debug().Str("path", osquerydPath).Msg("got osqueryd") + // Write files + + if err := writePackageInfo(opt, tmpDir); err != nil { + return errors.Wrap(err, "write PackageInfo") + } + if err := writeDistribution(opt, tmpDir); err != nil { + return errors.Wrap(err, "write Distribution") + } + if err := writeScripts(opt, tmpDir); err != nil { + return errors.Wrap(err, "write postinstall") + } + if opt.StartService { + if err := writeLaunchd(opt, filesystemRoot); err != nil { + return errors.Wrap(err, "write launchd") + } + } + if err := copyFile( + "./orbit", + filepath.Join(filesystemRoot, "var", "lib", "fleet", "orbit", "orbit"), + 0755, + ); err != nil { + return errors.Wrap(err, "write orbit") + } + // Build package if err := xarBom(opt, tmpDir); err != nil { return errors.Wrap(err, "build pkg") @@ -95,6 +103,7 @@ func BuildPkg(opt Options) error { } func writePackageInfo(opt Options, rootPath string) error { + // PackageInfo is metadata for the pkg path := filepath.Join(rootPath, "flat", "base.pkg", "PackageInfo") if err := os.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil { return errors.Wrap(err, "mkdir") @@ -112,7 +121,46 @@ func writePackageInfo(opt Options, rootPath string) error { return nil } +func writeScripts(opt Options, rootPath string) error { + // Postinstall script + path := filepath.Join(rootPath, "scripts", "postinstall") + if err := os.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil { + return errors.Wrap(err, "mkdir") + } + + var contents bytes.Buffer + if err := macosPostinstallTemplate.Execute(&contents, opt); err != nil { + return errors.Wrap(err, "execute template") + } + + if err := ioutil.WriteFile(path, contents.Bytes(), 0744); err != nil { + return errors.Wrap(err, "write file") + } + + return nil +} + +func writeLaunchd(opt Options, rootPath string) error { + // launchd is the service mechanism on macOS + path := filepath.Join(rootPath, "Library", "LaunchDaemons", "com.fleetdm.orbit.plist") + if err := os.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil { + return errors.Wrap(err, "mkdir") + } + + var contents bytes.Buffer + if err := macosLaunchdTemplate.Execute(&contents, opt); err != nil { + return errors.Wrap(err, "execute template") + } + + if err := ioutil.WriteFile(path, contents.Bytes(), 0644); err != nil { + return errors.Wrap(err, "write file") + } + + return nil +} + func writeDistribution(opt Options, rootPath string) error { + // Distribution file is metadata for the pkg path := filepath.Join(rootPath, "flat", "Distribution") if err := os.MkdirAll(filepath.Dir(path), constant.DefaultDirMode); err != nil { return errors.Wrap(err, "mkdir") @@ -136,52 +184,18 @@ func xarBom(opt Options, rootPath string) error { // Adapted from BSD licensed // https://github.com/go-flutter-desktop/hover/blob/v0.46.2/cmd/packaging/darwin-pkg.go - // Copy payload - payload, err := os.OpenFile(filepath.Join(rootPath, "flat", "base.pkg", "Payload"), os.O_RDWR|os.O_CREATE, 0755) - if err != nil { - return errors.Wrap(err, "open payload") + // Copy payload/scripts + if err := cpio( + filepath.Join(rootPath, "root"), + filepath.Join(rootPath, "flat", "base.pkg", "Payload"), + ); err != nil { + return errors.Wrap(err, "cpio Payload") } - - cmdFind := exec.Command("find", ".") - cmdFind.Dir = filepath.Join(rootPath, "root") - cmdCpio := exec.Command("cpio", "-o", "--format", "odc", "-R", "0:80") - cmdCpio.Dir = filepath.Join(rootPath, "root") - cmdGzip := exec.Command("gzip", "-c") - - // Pipes like this: find | cpio | gzip > Payload - cmdCpio.Stdin, err = cmdFind.StdoutPipe() - if err != nil { - return errors.Wrap(err, "pipe cpio") - } - cmdGzip.Stdin, err = cmdCpio.StdoutPipe() - if err != nil { - return errors.Wrap(err, "pipe gzip") - } - cmdGzip.Stdout = payload - - err = cmdGzip.Start() - if err != nil { - return errors.Wrap(err, "start gzip") - } - err = cmdCpio.Start() - if err != nil { - return errors.Wrap(err, "start cpio") - } - err = cmdFind.Run() - if err != nil { - return errors.Wrap(err, "run find") - } - err = cmdCpio.Wait() - if err != nil { - return errors.Wrap(err, "wait cpio") - } - err = cmdGzip.Wait() - if err != nil { - return errors.Wrap(err, "wait gzip") - } - err = payload.Close() - if err != nil { - return errors.Wrap(err, "close payload") + if err := cpio( + filepath.Join(rootPath, "scripts"), + filepath.Join(rootPath, "flat", "base.pkg", "Scripts"), + ); err != nil { + return errors.Wrap(err, "cpio Scripts") } // Make bom @@ -224,3 +238,56 @@ func xarBom(opt Options, rootPath string) error { return nil } + +func cpio(srcPath, dstPath string) error { + // This is the compression routine that is expected for pkg files. + dst, err := os.OpenFile(dstPath, os.O_RDWR|os.O_CREATE, 0755) + if err != nil { + return errors.Wrap(err, "open dst") + } + defer dst.Close() + + cmdFind := exec.Command("find", ".") + cmdFind.Dir = srcPath + cmdCpio := exec.Command("cpio", "-o", "--format", "odc", "-R", "0:80") + cmdCpio.Dir = srcPath + cmdGzip := exec.Command("gzip", "-c") + + // Pipes like this: find | cpio | gzip > dstPath + cmdCpio.Stdin, err = cmdFind.StdoutPipe() + if err != nil { + return errors.Wrap(err, "pipe cpio") + } + cmdGzip.Stdin, err = cmdCpio.StdoutPipe() + if err != nil { + return errors.Wrap(err, "pipe gzip") + } + cmdGzip.Stdout = dst + + err = cmdGzip.Start() + if err != nil { + return errors.Wrap(err, "start gzip") + } + err = cmdCpio.Start() + if err != nil { + return errors.Wrap(err, "start cpio") + } + err = cmdFind.Run() + if err != nil { + return errors.Wrap(err, "run find") + } + err = cmdCpio.Wait() + if err != nil { + return errors.Wrap(err, "wait cpio") + } + err = cmdGzip.Wait() + if err != nil { + return errors.Wrap(err, "wait gzip") + } + err = dst.Close() + if err != nil { + return errors.Wrap(err, "close dst") + } + + return nil +} diff --git a/pkg/packaging/macos_templates.go b/pkg/packaging/macos_templates.go index 415749e115..c70b7fe8f3 100644 --- a/pkg/packaging/macos_templates.go +++ b/pkg/packaging/macos_templates.go @@ -6,8 +6,11 @@ import "text/template" // http://s.sudre.free.fr/Stuff/Ivanhoe/FLAT.html var macosPackageInfoTemplate = template.Must(template.New("").Option("missingkey=error").Parse( ` - - + + + + + `)) @@ -26,3 +29,48 @@ var macosDistributionTemplate = template.Must(template.New("").Option("missingke #base.pkg `)) + +var macosPostinstallTemplate = template.Must(template.New("").Option("missingkey=error").Parse( + `#!/bin/bash + +ln -sf /var/lib/fleet/orbit/orbit /usr/local/bin/orbit + +{{ if .StartService -}} +launchctl stop com.fleetdm.orbit + +sleep 3 + +launchctl unload /Library/LaunchDaemons/com.fleetdm.orbit.plist +launchctl load /Library/LaunchDaemons/com.fleetdm.orbit.plist +{{- end }} +`)) + +// TODO set Nice? +var macosLaunchdTemplate = template.Must(template.New("").Option("missingkey=error").Parse( + ` + + + + Label + com.fleetdm.orbit + ProgramArguments + + /var/lib/fleet/orbit/orbit + + StandardOutPath + /var/log/orbit/orbit.stdout.log + StandardErrorPath + /var/log/orbit/orbit.stderr.log + EnvironmentVariables + + {{ if .Insecure }}ORBIT_INSECUREtrue{{ end }} + {{ if .FleetURL }}ORBIT_FLEET_URL{{.FleetURL}}{{ end }} + {{ if .EnrollSecret }}ORBIT_ENROLL_SECRET{{.EnrollSecret}}{{ end }} + + KeepAlive + RunAtLoad + ThrottleInterval + 10 + + +`)) diff --git a/pkg/packaging/packaging.go b/pkg/packaging/packaging.go index f482085d59..a2d1c84639 100644 --- a/pkg/packaging/packaging.go +++ b/pkg/packaging/packaging.go @@ -1,6 +1,14 @@ // package packaging provides tools for buildin Orbit installation packages. package packaging +import ( + "io" + "os" + "path/filepath" + + "github.com/pkg/errors" +) + // Options are the configurable options provided for the package. type Options struct { // FleetURL is the URL to the Fleet server. @@ -18,3 +26,27 @@ type Options struct { // Insecure enables insecure TLS connections for the generated package. Insecure bool } + +func copyFile(srcPath, dstPath string, perm os.FileMode) error { + src, err := os.Open(srcPath) + if err != nil { + return errors.Wrap(err, "open src for copy") + } + defer src.Close() + + if err := os.MkdirAll(filepath.Dir(dstPath), 0755); err != nil { + return errors.Wrap(err, "create dst dir for copy") + } + + dst, err := os.OpenFile(dstPath, os.O_RDWR|os.O_CREATE, perm) + if err != nil { + return errors.Wrap(err, "open dst for copy") + } + defer dst.Close() + + if _, err := io.Copy(dst, src); err != nil { + return errors.Wrap(err, "copy src to dst") + } + + return nil +}