macOS packaging works with launchd

This commit is contained in:
Zach Wasserman 2021-02-16 18:05:18 -08:00
parent b0ad96b228
commit be3d70537b
5 changed files with 232 additions and 81 deletions

View file

@ -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")
}

View file

@ -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{

View file

@ -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
}

View file

@ -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(
`<pkg-info format-version="2" identifier="{{.Identifier}}.base.pkg" version="{{.Version}}" install-location="/" auth="root">
<bundle-version>
</bundle-version>
<scripts>
<postinstall file="./postinstall"/>
</scripts>
<bundle-version>
</bundle-version>
</pkg-info>
`))
@ -26,3 +29,48 @@ var macosDistributionTemplate = template.Must(template.New("").Option("missingke
<pkg-ref id="{{.Identifier}}.base.pkg" version="{{.Version}}" auth="root">#base.pkg</pkg-ref>
</installer-gui-script>
`))
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(
`<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.fleetdm.orbit</string>
<key>ProgramArguments</key>
<array>
<string>/var/lib/fleet/orbit/orbit</string>
</array>
<key>StandardOutPath</key>
<string>/var/log/orbit/orbit.stdout.log</string>
<key>StandardErrorPath</key>
<string>/var/log/orbit/orbit.stderr.log</string>
<key>EnvironmentVariables</key>
<dict>
{{ if .Insecure }}<key>ORBIT_INSECURE</key><string>true</string>{{ end }}
{{ if .FleetURL }}<key>ORBIT_FLEET_URL</key><string>{{.FleetURL}}</string>{{ end }}
{{ if .EnrollSecret }}<key>ORBIT_ENROLL_SECRET</key><string>{{.EnrollSecret}}</string>{{ end }}
</dict>
<key>KeepAlive</key><true/>
<key>RunAtLoad</key><true/>
<key>ThrottleInterval</key>
<integer>10</integer>
</dict>
</plist>
`))

View file

@ -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
}