implement a docker image to package orbit natively in Linux (#6504)

Related to #6364 and #6363, this:

- Adds a new Docker image, `fleetdm/fleetctl` equipped with all necessary dependencies to build Fleet-osquery binaries for all platforms
- Modifies the package generation logic to special case this scenario via an environment variable `FLEETCTL_NATIVE_TOOLING`
- Adds a new GitHub workflow to test this

There are more details in the README, but part of the special-casing logic is in place to output the binaries to a folder named `build` when they are run with `FLEETCTL_NATIVE_TOOLING`, this is so we can persist the binary generated by the docker container via a bind mount:

```bash
docker run -v "$(pwd):/build" fleetdm/fleetctl package --type=msi
```

To test this changeset, I have generated packages for all platforms, both via the new Docker image and via the classic `fleetctl package`.
This commit is contained in:
Roberto Dip 2022-07-11 09:49:13 -03:00 committed by GitHub
parent a336ed61e5
commit f7dd8c86cd
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
13 changed files with 249 additions and 34 deletions

View File

@ -0,0 +1,69 @@
name: Test native tooling packaging
# This workflow tests packaging of Fleet-osquery with the
# `fleetdm/fleetctl` Docker image.
on:
push:
branches:
- main
- patch-*
pull_request:
paths:
- '**.go'
- 'tools/fleetctl-docker/**'
- 'tools/wix-docker/**'
- 'tools/bomutils-docker/**'
- '.github/workflows/test-native-tooling-packaging.yml'
workflow_dispatch: # Manual
permissions:
contents: read
jobs:
test-packaging:
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
go-version: ['^1.17.8']
runs-on: ${{ matrix.os }}
steps:
- name: Install Go
uses: actions/setup-go@b22fbbc2921299758641fab08929b4ac52b32923 # v2
with:
go-version: ${{ matrix.go-version }}
- name: Checkout Code
uses: actions/checkout@629c2de402a417ea7690ca6ce3f33229e27606a5 # v2
- name: Install Go Dependencies
run: make deps-go
- name: Build fleetdm/fleetctl
run: make fleetctl-docker
- name: Build DEB
run: docker run -v "$(pwd):/build" fleetdm/fleetctl package --type deb --enroll-secret=foo --fleet-url=https://localhost:8080
- name: Build DEB with Fleet Desktop
run: docker run -v "$(pwd):/build" fleetdm/fleetctl package --type deb --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
- name: Build RPM
run: docker run -v "$(pwd):/build" fleetdm/fleetctl --type rpm --enroll-secret=foo --fleet-url=https://localhost:8080
- name: Build RPM with Fleet Desktop
run: docker run -v "$(pwd):/build" fleetdm/fleetctl package --type rpm --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
- name: Build MSI
run: docker run -v "$(pwd):/build" fleetdm/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080
- name: Build MSI with Fleet Desktop
run: docker run -v "$(pwd):/build" fleetdm/fleetctl package --type msi --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop
- name: Build PKG
run: docker run -v "$(pwd):/build" fleetdm/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080
- name: Build PKG with Fleet Desktop
run: docker run -v "$(pwd):/build" fleetdm/fleetctl package --type pkg --enroll-secret=foo --fleet-url=https://localhost:8080 --fleet-desktop

View File

@ -1,7 +1,7 @@
name: Test packaging
# This workflow tests packaging of Fleet-osquery with the
# `fleetctl package' command. It fetches the targets: orbit,
# `fleetctl package` command. It fetches the targets: orbit,
# osquery and fleet-desktop from the default (Fleet's) TUF server,
# https://tuf.fleetctl.com.

View File

@ -205,6 +205,9 @@ docker-push-release: docker-build-release
docker push fleetdm/fleet:${VERSION}
docker push fleetdm/fleet:latest
fleetctl-docker: xp-fleetctl
docker build -t fleetdm/fleetctl --platform=linux/amd64 -f tools/fleetctl-docker/Dockerfile .
.pre-binary-bundle:
rm -rf build/binary-bundle
mkdir -p build/binary-bundle/linux

View File

@ -144,6 +144,12 @@ func packageCommand() *cli.Command {
Usage: "Disable opening the folder at the end",
Destination: &disableOpenFolder,
},
&cli.BoolFlag{
Name: "native-tooling",
Usage: "Build the package using native tooling (only available in Linux)",
EnvVars: []string{"FLEETCTL_NATIVE_TOOLING"},
Destination: &opt.NativeTooling,
},
},
Action: func(c *cli.Context) error {
if opt.FleetURL != "" || opt.EnrollSecret != "" {
@ -160,6 +166,10 @@ func packageCommand() *cli.Command {
return errors.New("Windows can only build MSI packages.")
}
if opt.NativeTooling && runtime.GOOS != "linux" {
return errors.New("native tooling is only available in Linux")
}
if opt.FleetCertificate != "" {
err := checkPEMCertificate(opt.FleetCertificate)
if err != nil {

View File

@ -5,6 +5,7 @@ import (
"io/ioutil"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/fleetdm/fleet/v4/orbit/pkg/packaging"
@ -38,6 +39,10 @@ func TestPackage(t *testing.T) {
require.NoError(t, err)
runAppCheckErr(t, []string{"package", "--type=deb", fmt.Sprintf("--fleet-certificate=%s", fleetCertificate)}, fmt.Sprintf("failed to read certificate %q: invalid PEM file", fleetCertificate))
if runtime.GOOS != "linux" {
runAppCheckErr(t, []string{"package", "--type=msi", "--native-tooling"}, "native on non-linux platforms fails")
}
t.Run("deb", func(t *testing.T) {
runAppForTest(t, []string{"package", "--type=deb", "--insecure", "--disable-open-folder"})
info, err := os.Stat(fmt.Sprintf("fleet-osquery_%s_amd64.deb", updatesData.OrbitVersion))

View File

@ -184,6 +184,9 @@ func buildNFPM(opt Options, pkger nfpm.Packager) (string, error) {
},
}
filename := pkger.ConventionalFileName(info)
if opt.NativeTooling {
filename = filepath.Join("build", filename)
}
out, err := secure.OpenFile(filename, os.O_CREATE|os.O_RDWR, constant.DefaultFileMode)
if err != nil {

View File

@ -131,6 +131,9 @@ func BuildPkg(opt Options) (string, error) {
}
filename := "fleet-osquery.pkg"
if opt.NativeTooling {
filename = filepath.Join("build", filename)
}
if err := file.Copy(generatedPath, filename, constant.DefaultFileMode); err != nil {
return "", fmt.Errorf("rename pkg: %w", err)
}
@ -248,8 +251,11 @@ func xarBom(opt Options, rootPath string) error {
// Make bom
var cmdMkbom *exec.Cmd
switch runtime.GOOS {
case "darwin":
var isDarwin = runtime.GOOS == "darwin"
var isLinuxNative = runtime.GOOS == "linux" && opt.NativeTooling
switch {
case isDarwin, isLinuxNative:
cmdMkbom = exec.Command("mkbom", filepath.Join(rootPath, "root"), filepath.Join("flat", "base.pkg", "Bom"))
cmdMkbom.Dir = rootPath
default:
@ -261,8 +267,8 @@ func xarBom(opt Options, rootPath string) error {
"/root/root", "/root/flat/base.pkg/Bom",
)
}
cmdMkbom.Stdout = os.Stdout
cmdMkbom.Stderr = os.Stderr
cmdMkbom.Stdout, cmdMkbom.Stderr = os.Stdout, os.Stderr
if err := cmdMkbom.Run(); err != nil {
return fmt.Errorf("mkbom: %w", err)
}
@ -286,8 +292,8 @@ func xarBom(opt Options, rootPath string) error {
// Make xar
var cmdXar *exec.Cmd
switch runtime.GOOS {
case "darwin":
switch {
case isDarwin, isLinuxNative:
cmdXar = exec.Command("xar", append([]string{"--compression", "none", "-cf", filepath.Join("..", "orbit.pkg")}, files...)...)
cmdXar.Dir = filepath.Join(rootPath, "flat")
default:
@ -297,9 +303,8 @@ func xarBom(opt Options, rootPath string) error {
)
cmdXar.Args = append(cmdXar.Args, append([]string{"--compression", "none", "-cf", "/root/orbit.pkg"}, files...)...)
}
cmdXar.Stdout = os.Stdout
cmdXar.Stderr = os.Stderr
cmdXar.Stdout, cmdXar.Stderr = os.Stdout, os.Stderr
if err := cmdXar.Run(); err != nil {
return fmt.Errorf("run xar: %w", err)
}

View File

@ -63,6 +63,9 @@ type Options struct {
// LegacyVarLibSymlink indicates whether Orbit is legacy (< 0.0.11),
// which assumes it is installed under /var/lib.
LegacyVarLibSymlink bool
// Native tooling is used to determine if the package should be built
// natively instead of via Docker images.
NativeTooling bool
}
func initializeTempDir() (string, error) {

View File

@ -100,7 +100,7 @@ func BuildMSI(opt Options) (string, error) {
}
}
if err := wix.Heat(tmpDir); err != nil {
if err := wix.Heat(tmpDir, opt.NativeTooling); err != nil {
return "", fmt.Errorf("package root files: %w", err)
}
@ -108,15 +108,18 @@ func BuildMSI(opt Options) (string, error) {
return "", fmt.Errorf("transform heat: %w", err)
}
if err := wix.Candle(tmpDir); err != nil {
if err := wix.Candle(tmpDir, opt.NativeTooling); err != nil {
return "", fmt.Errorf("build package: %w", err)
}
if err := wix.Light(tmpDir); err != nil {
if err := wix.Light(tmpDir, opt.NativeTooling); err != nil {
return "", fmt.Errorf("build package: %w", err)
}
filename := "fleet-osquery.msi"
if opt.NativeTooling {
filename = filepath.Join("build", filename)
}
if err := file.Copy(filepath.Join(tmpDir, "orbit.msi"), filename, constant.DefaultFileMode); err != nil {
return "", fmt.Errorf("rename msi: %w", err)
}

View File

@ -7,30 +7,54 @@ import (
"fmt"
"os"
"os/exec"
"strings"
)
const (
directoryReference = "ORBITROOT"
directoryReference = "ORBITROOT"
linuxPathSeparator = "/"
windowsPathSeparator = "\\"
)
// windowsJoin returns the result of replacing each slash ('/') character in
// each path with a Windows separator character ('\') and joining them using
// the Windows separator character.
//
// We can't use filepath.FromSlash because this func is run in a *nix
// machine.
func windowsJoin(paths ...string) string {
s := strings.Join(paths, windowsPathSeparator)
return strings.ReplaceAll(s, linuxPathSeparator, windowsPathSeparator)
}
// Heat runs the WiX Heat command on the provided directory.
//
// The Heat command creates XML fragments allowing WiX to include the entire
// directory. See
// https://wixtoolset.org/documentation/manual/v3/overview/heat.html.
func Heat(path string) error {
cmd := exec.Command(
"docker", "run", "--rm", "--platform", "linux/amd64",
"--volume", path+":/wix", // mount volume
"fleetdm/wix:latest", // image name
"heat", "dir", "root", // command in image
"-out", "heat.wxs",
func Heat(path string, native bool) error {
var args []string
if !native {
args = append(
args,
"docker", "run", "--rm", "--platform", "linux/amd64",
"--volume", path+":"+path, // mount volume
"fleetdm/wix:latest", // image name
)
}
args = append(args,
"heat", "dir", windowsJoin(path, "root"), // command
"-out", windowsJoin(path, "heat.wxs"),
"-gg", "-g1", // generate UUIDs (required by wix)
"-cg", "OrbitFiles", // set ComponentGroup name
"-scom", "-sfrag", "-srd", "-sreg", // suppress unneccesary generated items
"-dr", directoryReference, // set reference name
"-ke", // keep empty directories
)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
@ -44,15 +68,26 @@ func Heat(path string) error {
//
// See
// https://wixtoolset.org/documentation/manual/v3/overview/candle.html.
func Candle(path string) error {
cmd := exec.Command(
"docker", "run", "--rm", "--platform", "linux/amd64",
"--volume", path+":/wix", // mount volume
"fleetdm/wix:latest", // image name
"candle", "heat.wxs", "main.wxs", // command in image
func Candle(path string, native bool) error {
var args []string
if !native {
args = append(
args,
"docker", "run", "--rm", "--platform", "linux/amd64",
"--volume", path+":"+path, // mount volume
"fleetdm/wix:latest", // image name
)
}
args = append(args,
"candle", windowsJoin(path, "heat.wxs"), windowsJoin(path, "main.wxs"), // command
"-out", windowsJoin(path, ""),
"-ext", "WixUtilExtension",
"-arch", "x64",
)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {
@ -66,17 +101,27 @@ func Candle(path string) error {
//
// See
// https://wixtoolset.org/documentation/manual/v3/overview/light.html.
func Light(path string) error {
cmd := exec.Command(
"docker", "run", "--rm", "--platform", "linux/amd64",
"--volume", path+":/wix", // mount volume
"fleetdm/wix:latest", // image name
"light", "heat.wixobj", "main.wixobj", // command in image
func Light(path string, native bool) error {
var args []string
if !native {
args = append(
args,
"docker", "run", "--rm", "--platform", "linux/amd64",
"--volume", path+":"+path, // mount volume
"fleetdm/wix:latest", // image name
)
}
args = append(args,
"light", windowsJoin(path, "heat.wixobj"), windowsJoin(path, "main.wixobj"), // command
"-ext", "WixUtilExtension",
"-b", "root", // Set directory for finding heat files
"-out", "orbit.msi",
"-b", windowsJoin(path, "root"), // Set directory for finding heat files
"-out", windowsJoin(path, "orbit.msi"),
"-sval", // skip validation (otherwise Wine crashes)
)
cmd := exec.Command(args[0], args[1:]...)
cmd.Stdout, cmd.Stderr = os.Stdout, os.Stderr
if err := cmd.Run(); err != nil {

View File

@ -0,0 +1,23 @@
package wix
import (
"testing"
"github.com/stretchr/testify/require"
)
func TestWindowsJoin(t *testing.T) {
cases := []struct {
in []string
out string
}{
{[]string{"one\\two", "three"}, "one\\two\\three"},
{[]string{"one/two/three", "four.txt"}, "one\\two\\three\\four.txt"},
{[]string{"one", "two", "three"}, "one\\two\\three"},
{[]string{"one/two/three", "four/five.txt"}, "one\\two\\three\\four\\five.txt"},
}
for _, c := range cases {
require.Equal(t, c.out, windowsJoin(c.in...))
}
}

View File

@ -0,0 +1,21 @@
FROM debian:stable-slim
RUN apt-get update \
&& dpkg --add-architecture i386 \
&& apt update \
&& apt install -y --no-install-recommends ca-certificates cpio libxml2 wine wine32 libgtk-3-0 \
&& rm -rf /var/lib/apt/lists/*
# copy macOS dependencies
COPY --from=fleetdm/bomutils:latest /usr/bin/mkbom /usr/local/bin/xar /usr/bin/
COPY --from=fleetdm/bomutils:latest /usr/local/lib /usr/local/lib/
# copy Windows dependencies
COPY --from=fleetdm/wix:latest /home/wine /home/wine
# copy fleetctl
COPY build/binary-bundle/linux/fleetctl /usr/bin/fleetctl
ENV FLEETCTL_NATIVE_TOOLING=1 WINEPREFIX=/home/wine/.wine WINEARCH=win32 PATH="/home/wine/bin:$PATH" WINEDEBUG=-all
ENTRYPOINT ["fleetctl"]

View File

@ -0,0 +1,25 @@
## fleetdm/fleetctl
This docker image allows to run `fleetctl` in a Linux environment that has all
the necessary dependencies to package `msi`, `pkg`, `deb` and `rpm` packages.
### Usage
```
docker run fleetdm/fleetctl command [flags]
```
Build artifacts are generated at `/build`. To get a package using this image:
```
docker run -v "$(pwd):/build" fleetdm/fleetctl package --type=msi
```
### Building
This image needs to be built from the root of the repo in order for the build
context to have access to the `fleetctl` binary. To build the image, run:
```
make fleetctl-docker
```