Add (beta) support for Fleet Desktop to linux (#5221)

* Add (beta) support for Fleet Desktop to linux

* Add dependency for linux desktop

* Amend makefile uname check

* Clarify env vars used for linux in execuser

* Add final set of fixes

* Remove -it from docker run

* Add desktop to the update runner for Linux

* Re-arrange tag.gz and fix upgrade check for linux desktop
This commit is contained in:
Lucas Manuel Rodriguez
2022-05-04 11:14:12 -03:00
committed by GitHub
parent fba916e636
commit b6bbbbe186
15 changed files with 285 additions and 33 deletions
+25 -2
View File
@@ -63,7 +63,7 @@ jobs:
path: desktop.app.tar.gz
desktop-windows:
runs-on: macos-latest
runs-on: ubuntu-latest
steps:
- name: Install Go
@@ -83,4 +83,27 @@ jobs:
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v2
with:
name: fleet-desktop.exe
path: fleet-desktop.exe
path: fleet-desktop.exe
desktop-linux:
runs-on: ubuntu-latest
steps:
- name: Install Go
uses: actions/setup-go@f6164bd8c8acb4a71fb2791a8b6c4024ff038dab # v2
with:
go-version: '^1.17.0'
- name: Checkout
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
- name: Generate desktop.tar.gz
run: |
FLEET_DESKTOP_VERSION=$FLEET_DESKTOP_VERSION \
make desktop-linux
- name: Upload desktop.tar.gz
uses: actions/upload-artifact@6673cd052c4cd6fcf4b4e6e60ea986c889389535 # v2
with:
name: desktop.tar.gz
path: desktop.tar.gz
+1
View File
@@ -60,6 +60,7 @@ terraform.tfstate*
# generated installers
fleet-osquery*
desktop.app.tar.gz
desktop.tar.gz
# residual files when running the cpe command
cmd/cpe/etagenv
+19
View File
@@ -0,0 +1,19 @@
FROM golang:1.17.8-bullseye
LABEL maintainer="Fleet Developers <hello@fleetdm.com>"
RUN apt-get update && apt-get install -y \
gcc \
libgtk-3-dev \
libayatana-appindicator3-dev \
&& rm -rf /var/lib/apt/lists/*
RUN mkdir -p /usr/src/fleet
RUN mkdir -p /output
WORKDIR /usr/src/fleet
COPY orbit ./orbit
COPY pkg ./pkg
COPY go.mod go.sum ./
CMD /bin/bash
+32 -1
View File
@@ -175,6 +175,10 @@ deps-js:
deps-go:
go mod download
ifeq ($(shell uname -s),Linux)
# Dependency required for Linux Fleet Desktop.
sudo apt-get install gcc libgtk-3-dev libayatana-appindicator3-dev -y
endif
migration:
go run github.com/fleetdm/goose/cmd/goose -dir server/datastore/mysql/migrations/tables create $(name)
@@ -306,6 +310,8 @@ endif
#
# Usage:
# FLEET_DESKTOP_APPLE_AUTHORITY=foo FLEET_DESKTOP_VERSION=0.0.1 make desktop-app-tar-gz
#
# Output: desktop.app.tar.gz
desktop-app-tar-gz:
ifneq ($(shell uname), Darwin)
@echo "Makefile target desktop-app-tar-gz is only supported on macOS"
@@ -313,12 +319,37 @@ ifneq ($(shell uname), Darwin)
endif
go run ./tools/desktop macos
FLEET_DESKTOP_VERSION ?= unknown
# Build desktop executable for Windows.
#
# Usage:
# FLEET_DESKTOP_VERSION=0.0.1 make desktop-windows
#
# Output: fleet-desktop.exe
desktop-windows:
GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui" -o fleet-desktop.exe ./orbit/cmd/desktop
GOOS=windows GOARCH=amd64 go build -ldflags "-H=windowsgui -X=main.version=$(FLEET_DESKTOP_VERSION)" -o fleet-desktop.exe ./orbit/cmd/desktop
# Build desktop executable for Linux.
#
# Usage:
# FLEET_DESKTOP_VERSION=0.0.1 make desktop-linux
#
# Output: desktop.tar.gz
desktop-linux:
docker build -f Dockerfile-desktop-linux -t desktop-linux-builder .
docker run --rm -v $(shell pwd):/output desktop-linux-builder /bin/bash -c "\
mkdir /output/fleet-desktop && \
go build -o /output/fleet-desktop/fleet-desktop -ldflags "-X=main.version=$(FLEET_DESKTOP_VERSION)" /usr/src/fleet/orbit/cmd/desktop && \
cp /usr/lib/x86_64-linux-gnu/libayatana-appindicator3.so.1 \
/usr/lib/x86_64-linux-gnu/libayatana-ido3-0.4.so.0 \
/usr/lib/x86_64-linux-gnu/libayatana-indicator3.so.7 \
/lib/x86_64-linux-gnu/libm.so.6 \
/usr/lib/x86_64-linux-gnu/libdbusmenu-gtk3.so.4 \
/usr/lib/x86_64-linux-gnu/libdbusmenu-glib.so.4 \
/output/fleet-desktop && cd /output && \
tar czf desktop.tar.gz fleet-desktop && \
rm -r fleet-desktop"
# db-replica-setup setups one main and one read replica MySQL instance for dev/testing.
# - Assumes the docker containers are already running (tools/mysql-replica-testing/docker-compose.yml)
+1
View File
@@ -0,0 +1 @@
* Add (beta) support for Fleet Desktop to linux.
+3
View File
@@ -251,6 +251,9 @@ func updatesAddFunc(c *cli.Context) error {
case name == "desktop" && platform == "windows":
// This is a special case for the desktop target on Windows.
dstPath = filepath.Join(filepath.Dir(dstPath), constant.DesktopAppExecName+".exe")
case name == "desktop" && platform == "linux":
// This is a special case for the desktop target on Linux.
dstPath += ".tar.gz"
case strings.HasSuffix(target, ".exe"):
dstPath += ".exe"
case strings.HasSuffix(target, ".app.tar.gz"):
+3 -3
View File
@@ -1,6 +1,3 @@
//go:build darwin || windows
// +build darwin windows
package main
import (
@@ -17,12 +14,15 @@ import (
"github.com/getlantern/systray"
)
var version = "unknown"
func main() {
// Our TUF provided targets must support launching with "--help".
if len(os.Args) > 1 && os.Args[1] == "--help" {
fmt.Println("Fleet Desktop application executable")
return
}
log.Printf("fleet-desktop version=%s\n", version)
devURL := os.Getenv("FLEET_DESKTOP_DEVICE_URL")
if devURL == "" {
+2 -4
View File
@@ -1,7 +1,5 @@
//go:build darwin
// +build darwin
// TODO(lucas): Once we support Linux, amend the above build tags.
//go:build darwin || linux
// +build darwin linux
package main
+13 -7
View File
@@ -229,6 +229,8 @@ func main() {
opt.Targets["desktop"] = update.DesktopMacOSTarget
case "windows":
opt.Targets["desktop"] = update.DesktopWindowsTarget
case "linux":
opt.Targets["desktop"] = update.DesktopLinuxTarget
default:
log.Fatal().Str("GOOS", runtime.GOOS).Msg("unsupported GOOS for desktop target")
}
@@ -321,7 +323,7 @@ func main() {
if !c.Bool("disable-updates") {
targets := []string{"orbit", "osqueryd"}
if c.Bool("fleet-desktop") && (runtime.GOOS == "darwin" || runtime.GOOS == "windows") {
if c.Bool("fleet-desktop") {
targets = append(targets, "desktop")
}
updateRunner, err := update.NewRunner(updater, update.RunnerOptions{
@@ -483,7 +485,7 @@ func main() {
}))
g.Add(ext.Execute, ext.Interrupt)
if c.Bool("fleet-desktop") && (runtime.GOOS == "darwin" || runtime.GOOS == "windows") {
if c.Bool("fleet-desktop") {
desktopRunner := newDesktopRunner(desktopPath, fleetURL, deviceAuthToken, c.Bool("insecure"))
g.Add(desktopRunner.actor())
}
@@ -557,7 +559,7 @@ func (d *desktopRunner) execute() error {
for {
// First retry logic to start fleet-desktop.
if done := retry(30*time.Second, d.interruptCh, func() bool {
if done := retry(30*time.Second, false, d.interruptCh, func() bool {
// Orbit runs as root user on Unix and as SYSTEM (Windows Service) user on Windows.
// To be able to run the desktop application (mostly to register the icon in the system tray)
// we need to run the application as the login user.
@@ -572,7 +574,8 @@ func (d *desktopRunner) execute() error {
}
// Second retry logic to monitor fleet-desktop.
if done := retry(30*time.Second, d.interruptCh, func() bool {
// Call with waitFirst=true to give some time for the process to start.
if done := retry(30*time.Second, true, d.interruptCh, func() bool {
switch _, err := getProcessByName(constant.DesktopAppExecName); {
case err == nil:
return true // all good, process is running, retry.
@@ -589,14 +592,17 @@ func (d *desktopRunner) execute() error {
}
}
func retry(d time.Duration, done chan struct{}, fn func() bool) bool {
func retry(d time.Duration, waitFirst bool, done chan struct{}, fn func() bool) bool {
ticker := time.NewTicker(d)
defer ticker.Stop()
for {
if retry := fn(); !retry {
return false
if !waitFirst {
if retry := fn(); !retry {
return false
}
}
waitFirst = false
select {
case <-done:
return true
+110 -1
View File
@@ -1,5 +1,114 @@
package execuser
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"github.com/rs/zerolog/log"
)
// run uses sudo to run the given path as login user.
func run(path string, opts eopts) error {
panic("unimplemented")
user, err := getLoginUID()
if err != nil {
return fmt.Errorf("get user: %w", err)
}
log.Info().
Str("user", user.name).
Int64("id", user.id).
Msg("running sudo")
arg := []string{"-u", user.name, "-H"}
for _, nv := range opts.env {
arg = append(arg, fmt.Sprintf("%s=%s", nv[0], nv[1]))
}
arg = append(arg,
// TODO(lucas): Default to display 0, revisit when working on
// multi-user/multi-session support. This assumes there's only
// one desktop session and belongs to the user returned in `getLoginUID'.
"DISPLAY=:0",
// DBUS_SESSION_BUS_ADDRESS sets the location of the user login session bus.
// Required by the libayatana-appindicator3 library to display a tray icon
// on the desktop session.
fmt.Sprintf("DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/%d/bus", user.id),
fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", filepath.Dir(path), os.ExpandEnv("$LD_LIBRARY_PATH")),
path,
)
cmd := exec.Command("sudo", arg...)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
log.Printf("cmd=%s", cmd.String())
if err := cmd.Start(); err != nil {
return fmt.Errorf("open path %q: %w", path, err)
}
return nil
}
type user struct {
name string
id int64
}
// getLoginUID returns the name and uid of the first login user
// as reported by the `users' command.
//
// NOTE(lucas): It is always picking first login user as returned
// by `users', revisit when working on multi-user/multi-session support.
func getLoginUID() (*user, error) {
out, err := exec.Command("users").CombinedOutput()
if err != nil {
return nil, fmt.Errorf("users exec failed: %w", err)
}
usernames := parseUsersOutput(string(out))
username := usernames[0]
if username == "" {
return nil, fmt.Errorf("no user session found")
}
out, err = exec.Command("id", "-u", username).CombinedOutput()
if err != nil {
return nil, fmt.Errorf("id exec failed: %w", err)
}
uid, err := parseIDOutput(string(out))
if err != nil {
return nil, err
}
return &user{
name: username,
id: uid,
}, nil
}
// parseUsersOutput parses the output of the `users' command.
//
// `users' command prints on a single line a blank-separated list of user names of
// users currently logged in to the current host. Each user name
// corresponds to a login session, so if a user has more than one login
// session, that user's name will appear the same number of times in the
// output.
//
// Returns the list of usernames.
func parseUsersOutput(s string) []string {
var users []string
for _, userCol := range strings.Split(strings.TrimSpace(s), " ") {
users = append(users, userCol)
}
return users
}
// parseIDOutput parses the output of the `id' command.
//
// Returns the parsed uid.
func parseIDOutput(s string) (int64, error) {
uid, err := strconv.ParseInt(strings.TrimSpace(string(s)), 10, 0)
if err != nil {
return 0, fmt.Errorf("failed to parse uid: %w", err)
}
return uid, nil
}
+15 -4
View File
@@ -40,6 +40,12 @@ func buildNFPM(opt Options, pkger nfpm.Packager) (string, error) {
updateOpt.RootDirectory = orbitRoot
updateOpt.Targets = update.LinuxTargets
if opt.Desktop {
updateOpt.Targets["desktop"] = update.DesktopLinuxTarget
// Override default channel with the provided value.
updateOpt.Targets.SetTargetChannel("desktop", opt.DesktopChannel)
}
// Override default channels with the provided values.
updateOpt.Targets.SetTargetChannel("orbit", opt.OrbitChannel)
updateOpt.Targets.SetTargetChannel("osqueryd", opt.OsquerydChannel)
@@ -235,6 +241,10 @@ ORBIT_UPDATE_URL={{ .UpdateURL }}
ORBIT_ORBIT_CHANNEL={{ .OrbitChannel }}
ORBIT_OSQUERYD_CHANNEL={{ .OsquerydChannel }}
ORBIT_UPDATE_INTERVAL={{ .OrbitUpdateInterval }}
{{ if .Desktop }}
ORBIT_FLEET_DESKTOP=true
ORBIT_DESKTOP_CHANNEL={{ .DesktopChannel }}
{{ end }}
{{ if .Insecure }}ORBIT_INSECURE=true{{ end }}
{{ if .DisableUpdates }}ORBIT_DISABLE_UPDATES=true{{ end }}
{{ if .FleetURL }}ORBIT_FLEET_URL={{.FleetURL}}{{ end }}
@@ -295,12 +305,13 @@ func writePostInstall(opt Options, path string) error {
}
func writePreRemove(opt Options, path string) error {
// We add `|| true` in case the service is not running
// or has been manually disabled already. Otherwise,
// uninstallation fails.
if err := ioutil.WriteFile(path, []byte(`#!/bin/sh
set -e
systemctl stop orbit.service
systemctl disable orbit.service
systemctl stop orbit.service || true
systemctl disable orbit.service || true
`), constant.DefaultFileMode); err != nil {
return fmt.Errorf("write file: %w", err)
}
+23 -1
View File
@@ -1,6 +1,13 @@
package update
import "github.com/fleetdm/fleet/v4/orbit/pkg/constant"
import (
"fmt"
"os"
"os/exec"
"path/filepath"
"github.com/fleetdm/fleet/v4/orbit/pkg/constant"
)
// DefaultOptions are the default options to use when creating an update
// client.
@@ -59,4 +66,19 @@ var (
Channel: "stable",
TargetFile: constant.DesktopAppExecName + ".exe",
}
DesktopLinuxTarget = TargetInfo{
Platform: "linux",
Channel: "stable",
TargetFile: "desktop.tar.gz",
ExtractedExecSubPath: []string{"fleet-desktop", constant.DesktopAppExecName},
CustomCheckExec: func(execPath string) error {
cmd := exec.Command(execPath, "--help")
cmd.Env = append(cmd.Env, fmt.Sprintf("LD_LIBRARY_PATH=%s:%s", filepath.Dir(execPath), os.ExpandEnv("$LD_LIBRARY_PATH")))
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("exec new version: %s: %w", string(out), err)
}
return nil
},
}
)
+18 -9
View File
@@ -85,6 +85,8 @@ type TargetInfo struct {
// ExtractedExecSubPath is the path to the executable in case the
// target is a compressed file.
ExtractedExecSubPath []string
// CustomCheckExec allows for a custom method for checking a downloaded executable.
CustomCheckExec func(execPath string) error
}
// New creates a new updater given the provided options. All the necessary
@@ -286,7 +288,7 @@ func (u *Updater) Get(target string) (*LocalTarget, error) {
}
if err := checkFileHash(meta, localTarget.Path); err != nil {
log.Debug().Str("info", err.Error()).Msg("change detected")
if err := u.download(target, repoPath, localTarget.Path); err != nil {
if err := u.download(target, repoPath, localTarget.Path, localTarget.Info.CustomCheckExec); err != nil {
return nil, fmt.Errorf("download %q: %w", repoPath, err)
}
if strings.HasSuffix(localTarget.Path, ".tar.gz") {
@@ -299,7 +301,7 @@ func (u *Updater) Get(target string) (*LocalTarget, error) {
}
case errors.Is(err, os.ErrNotExist):
log.Debug().Err(err).Msg("stat file")
if err := u.download(target, repoPath, localTarget.Path); err != nil {
if err := u.download(target, repoPath, localTarget.Path, localTarget.Info.CustomCheckExec); err != nil {
return nil, fmt.Errorf("download %q: %w", repoPath, err)
}
default:
@@ -375,7 +377,7 @@ func (u *Updater) CopyDevBuild(target, devBuildPath string) {
// download downloads the target to the provided path. The file is deleted and
// an error is returned if the hash does not match.
func (u *Updater) download(target, repoPath, localPath string) error {
func (u *Updater) download(target, repoPath, localPath string, customCheckExec func(execPath string) error) error {
staging := filepath.Join(u.opt.RootDirectory, stagingDir)
if err := secure.MkdirAll(staging, constant.DefaultDirMode); err != nil {
@@ -422,7 +424,7 @@ func (u *Updater) download(target, repoPath, localPath string) error {
return fmt.Errorf("close tmp file: %w", err)
}
if err := u.checkExec(target, tmp.Name()); err != nil {
if err := u.checkExec(target, tmp.Name(), customCheckExec); err != nil {
return fmt.Errorf("exec check failed %q: %w", tmp.Name(), err)
}
@@ -452,7 +454,7 @@ func goosFromPlatform(platform string) (string, error) {
}
// checkExec checks/verifies a downloaded executable target by executing it.
func (u *Updater) checkExec(target, tmpPath string) error {
func (u *Updater) checkExec(target, tmpPath string, customCheckExec func(execPath string) error) error {
localTarget, err := u.localTarget(target)
if err != nil {
return err
@@ -477,11 +479,18 @@ func (u *Updater) checkExec(target, tmpPath string) error {
tmpPath = filepath.Join(append([]string{filepath.Dir(tmpPath)}, localTarget.Info.ExtractedExecSubPath...)...)
}
// Note that this would fail for any binary that returns nonzero for --help.
out, err := exec.Command(tmpPath, "--help").CombinedOutput()
if err != nil {
return fmt.Errorf("exec new version: %s: %w", string(out), err)
if customCheckExec != nil {
if err := customCheckExec(tmpPath); err != nil {
return fmt.Errorf("custom exec new version failed: %w", err)
}
} else {
// Note that this would fail for any binary that returns nonzero for --help.
cmd := exec.Command(tmpPath, "--help")
if out, err := cmd.CombinedOutput(); err != nil {
return fmt.Errorf("exec new version: %s: %w", string(out), err)
}
}
return nil
}
+5 -1
View File
@@ -147,7 +147,11 @@ func createMacOSApp(version, authority string, notarize bool) error {
}
/* #nosec G204 -- arguments are actually well defined */
buildExec := exec.Command("go", "build", "-o", filepath.Join(macOSDir, constant.DesktopAppExecName), "./"+filepath.Join("orbit", "cmd", "desktop"))
buildExec := exec.Command("go", "build",
"-o", filepath.Join(macOSDir, constant.DesktopAppExecName),
"-ldflags", os.ExpandEnv("-X=main.version=$FLEET_DESKTOP_VERSION"),
"./"+filepath.Join("orbit", "cmd", "desktop"),
)
buildExec.Env = append(os.Environ(), "CGO_ENABLED=1")
buildExec.Stderr = os.Stderr
buildExec.Stdout = os.Stdout
+15
View File
@@ -89,6 +89,19 @@ function create_repository() {
rm fleet-desktop.exe
fi
# Add Fleet Desktop application on (if enabled).
if [[ $system == "linux" && -n "$FLEET_DESKTOP" ]]; then
FLEET_DESKTOP_VERSION=42.0.0 \
make desktop-linux
./build/fleetctl updates add \
--path $TUF_PATH \
--target desktop.tar.gz \
--platform linux \
--name desktop \
--version 42.0.0 -t 42.0 -t 42 -t stable
rm desktop.tar.gz
fi
done
# Generate and add osqueryd .app bundle for macos-app.
@@ -146,6 +159,7 @@ if [ -n "$GENERATE_PKGS" ]; then
echo "Generating deb..."
./build/fleetctl package \
--type=deb \
${FLEET_DESKTOP:+--fleet-desktop} \
--fleet-url=https://$DEB_HOSTNAME:8080 \
--enroll-secret=$ENROLL_SECRET \
--insecure \
@@ -158,6 +172,7 @@ if [ -n "$GENERATE_PKGS" ]; then
echo "Generating rpm..."
./build/fleetctl package \
--type=rpm \
${FLEET_DESKTOP:+--fleet-desktop} \
--fleet-url=https://$RPM_HOSTNAME:8080 \
--enroll-secret=$ENROLL_SECRET \
--insecure \