2020-11-18 21:16:18 +00:00
package main
import (
"archive/zip"
"bytes"
"crypto/tls"
2021-11-22 14:13:26 +00:00
"errors"
2020-11-18 21:16:18 +00:00
"fmt"
"io"
"io/ioutil"
"net/http"
"os"
"os/exec"
"path/filepath"
2021-10-15 21:23:06 +00:00
"strconv"
2020-11-18 21:16:18 +00:00
"strings"
2020-11-20 23:37:14 +00:00
"time"
2020-11-18 21:16:18 +00:00
"github.com/cenkalti/backoff/v4"
2021-10-15 21:23:06 +00:00
"github.com/fleetdm/fleet/v4/orbit/pkg/packaging"
"github.com/fleetdm/fleet/v4/orbit/pkg/update"
2021-11-24 20:56:54 +00:00
"github.com/fleetdm/fleet/v4/pkg/fleethttp"
2022-03-21 17:53:53 +00:00
"github.com/fleetdm/fleet/v4/pkg/open"
2022-08-05 22:07:32 +00:00
"github.com/fleetdm/fleet/v4/pkg/spec"
2021-11-15 14:11:38 +00:00
"github.com/fleetdm/fleet/v4/server/contexts/ctxerr"
2021-06-26 04:46:51 +00:00
"github.com/fleetdm/fleet/v4/server/service"
2021-11-15 21:12:25 +00:00
"github.com/mitchellh/go-ps"
2021-03-13 00:42:38 +00:00
"github.com/urfave/cli/v2"
2020-11-18 21:16:18 +00:00
)
2022-05-16 21:06:29 +00:00
type dockerComposeVersion int
2020-11-18 21:16:18 +00:00
const (
2021-10-12 14:20:28 +00:00
downloadUrl = "https://github.com/fleetdm/osquery-in-a-box/archive/%s.zip"
2021-09-21 23:43:27 +00:00
standardQueryLibraryUrl = "https://raw.githubusercontent.com/fleetdm/fleet/main/docs/01-Using-Fleet/standard-query-library/standard-query-library.yml"
2021-07-28 15:05:03 +00:00
licenseKeyFlagName = "license-key"
2021-09-22 00:08:58 +00:00
tagFlagName = "tag"
2021-10-12 14:20:28 +00:00
previewConfigFlagName = "preview-config"
2021-12-31 23:13:28 +00:00
noHostsFlagName = "no-hosts"
2022-01-14 21:25:26 +00:00
orbitChannel = "orbit-channel"
osquerydChannel = "osqueryd-channel"
2022-04-27 21:17:20 +00:00
updateURL = "update-url"
updateRootKeys = "update-roots"
2022-05-02 16:10:11 +00:00
stdQueryLibFilePath = "std-query-lib-file-path"
2022-05-10 14:52:33 +00:00
disableOpenBrowser = "disable-open-browser"
2022-05-16 21:06:29 +00:00
dockerComposeV1 dockerComposeVersion = 1
dockerComposeV2 dockerComposeVersion = 2
2020-11-18 21:16:18 +00:00
)
2022-05-16 21:06:29 +00:00
type dockerCompose struct {
version dockerComposeVersion
}
func ( d dockerCompose ) String ( ) string {
if d . version == dockerComposeV1 {
return "`docker-compose`"
}
return "`docker compose`"
}
func ( d dockerCompose ) Command ( arg ... string ) * exec . Cmd {
if d . version == dockerComposeV1 {
return exec . Command ( "docker-compose" , arg ... )
}
return exec . Command ( "docker" , append ( [ ] string { "compose" } , arg ... ) ... )
}
func newDockerCompose ( ) ( dockerCompose , error ) {
// first, check if `docker compose` is available
if err := exec . Command ( "docker compose" ) . Run ( ) ; err == nil {
return dockerCompose { dockerComposeV2 } , nil
}
// if not, try to use `docker-compose`
if _ , err := exec . LookPath ( "docker-compose" ) ; err == nil {
return dockerCompose { dockerComposeV1 } , nil
}
return dockerCompose { } , errors . New ( "`docker compose` is required for the fleetctl preview experience.\n\nPlease install `docker compose` (https://docs.docker.com/compose/install/)." )
}
2021-03-13 00:42:38 +00:00
func previewCommand ( ) * cli . Command {
return & cli . Command {
2022-01-20 06:12:28 +00:00
Name : "preview" ,
Aliases : [ ] string { "sandbox" } ,
Usage : "Start a sandbox deployment of the Fleet server" ,
2022-05-16 21:06:29 +00:00
Description : ` Start a sandbox deployment of the Fleet server using Docker and docker compose . Docker tools must be available in the environment .
2020-11-18 21:16:18 +00:00
2021-03-19 23:45:21 +00:00
Use the stop and reset subcommands to manage the server and dependencies once started . ` ,
Subcommands : [ ] * cli . Command {
previewStopCommand ( ) ,
previewResetCommand ( ) ,
} ,
2021-01-05 01:11:10 +00:00
Flags : [ ] cli . Flag {
configFlag ( ) ,
contextFlag ( ) ,
2021-02-03 02:55:16 +00:00
debugFlag ( ) ,
2021-06-10 16:17:49 +00:00
& cli . StringFlag {
Name : licenseKeyFlagName ,
2021-09-03 16:05:23 +00:00
Usage : "License key to enable Fleet Premium (optional)" ,
2021-06-10 16:17:49 +00:00
} ,
2021-09-22 00:08:58 +00:00
& cli . StringFlag {
Name : tagFlagName ,
Usage : "Run a specific version of Fleet" ,
Value : "latest" ,
} ,
2021-10-12 14:20:28 +00:00
& cli . StringFlag {
Name : previewConfigFlagName ,
Usage : "Run a specific branch of the preview repository" ,
Value : "production" ,
} ,
2021-12-31 23:13:28 +00:00
& cli . BoolFlag {
Name : noHostsFlagName ,
Usage : "Start the server without adding any hosts" ,
Value : false ,
} ,
2022-01-14 21:25:26 +00:00
& cli . StringFlag {
Name : orbitChannel ,
Usage : "Use a custom orbit channel" ,
Value : "stable" ,
} ,
& cli . StringFlag {
Name : osquerydChannel ,
Usage : "Use a custom osqueryd channel" ,
Value : "stable" ,
} ,
2022-04-27 21:17:20 +00:00
& cli . StringFlag {
Name : updateURL ,
Usage : "Use a custom update TUF URL" ,
Value : "" ,
} ,
& cli . StringFlag {
Name : updateRootKeys ,
Usage : "Use custom update TUF root keys" ,
Value : "" ,
} ,
2022-05-02 16:10:11 +00:00
& cli . StringFlag {
Name : stdQueryLibFilePath ,
Usage : "Use custom standard query library yml file" ,
Value : "" ,
} ,
2022-05-10 14:52:33 +00:00
& cli . BoolFlag {
Name : disableOpenBrowser ,
Usage : "Disable opening the browser" ,
} ,
2021-01-05 01:11:10 +00:00
} ,
2020-11-18 21:16:18 +00:00
Action : func ( c * cli . Context ) error {
2021-01-28 15:57:32 +00:00
if err := checkDocker ( ) ; err != nil {
return err
2020-11-18 21:16:18 +00:00
}
2022-05-16 21:06:29 +00:00
compose , err := newDockerCompose ( )
if err != nil {
return err
}
2021-03-19 23:45:21 +00:00
// Download files every time to ensure the user gets the most up to date versions
2021-01-28 15:57:32 +00:00
previewDir := previewDirectory ( )
2021-10-12 14:20:28 +00:00
osqueryBranch := c . String ( previewConfigFlagName )
fmt . Printf ( "Downloading dependencies from %s into %s...\n" , osqueryBranch , previewDir )
if err := downloadFiles ( osqueryBranch ) ; err != nil {
2022-06-30 20:24:19 +00:00
return fmt . Errorf ( "downloading dependencies: %w" , err )
2020-11-18 21:16:18 +00:00
}
2021-01-28 15:57:32 +00:00
if err := os . Chdir ( previewDir ) ; err != nil {
2020-11-18 21:16:18 +00:00
return err
}
2021-03-19 23:45:21 +00:00
if _ , err := os . Stat ( "docker-compose.yml" ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "docker-compose file not found in preview directory: %w" , err )
2021-03-19 23:45:21 +00:00
}
2020-11-18 21:16:18 +00:00
2021-02-13 16:41:46 +00:00
// Make sure the logs directory is writable, otherwise the Fleet
// server errors on startup. This can be a problem when running on
// Linux with a non-root user inside the container.
2022-03-21 17:53:53 +00:00
if err := os . Chmod ( filepath . Join ( previewDir , "logs" ) , 0 o777 ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "make logs writable: %w" , err )
2021-02-13 16:41:46 +00:00
}
2022-03-21 17:53:53 +00:00
if err := os . Chmod ( filepath . Join ( previewDir , "vulndb" ) , 0 o777 ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "make vulndb writable: %w" , err )
2021-10-12 14:20:28 +00:00
}
2021-02-13 16:41:46 +00:00
2021-09-22 00:08:58 +00:00
if err := os . Setenv ( "FLEET_VERSION" , c . String ( tagFlagName ) ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "failed to set Fleet version: %w" , err )
2021-09-22 00:08:58 +00:00
}
2021-03-19 23:45:21 +00:00
fmt . Println ( "Pulling Docker dependencies..." )
2022-05-16 21:06:29 +00:00
out , err := compose . Command ( "pull" ) . CombinedOutput ( )
2021-03-19 23:45:21 +00:00
if err != nil {
fmt . Println ( string ( out ) )
2022-05-16 21:06:29 +00:00
return fmt . Errorf ( "Failed to run %s" , compose )
2021-03-19 23:45:21 +00:00
}
2020-11-18 21:16:18 +00:00
fmt . Println ( "Starting Docker containers..." )
2022-05-16 21:06:29 +00:00
cmd := compose . Command ( "up" , "-d" , "--remove-orphans" , "mysql01" , "redis01" , "fleet01" )
2021-06-18 18:58:15 +00:00
cmd . Env = append ( os . Environ ( ) , "FLEET_LICENSE_KEY=" + c . String ( licenseKeyFlagName ) )
2021-06-10 16:17:49 +00:00
out , err = cmd . CombinedOutput ( )
2020-11-18 21:16:18 +00:00
if err != nil {
fmt . Println ( string ( out ) )
2022-05-16 21:06:29 +00:00
return fmt . Errorf ( "Failed to run %s" , compose )
2020-11-18 21:16:18 +00:00
}
2020-11-20 23:37:14 +00:00
fmt . Println ( "Waiting for server to start up..." )
2020-11-18 21:16:18 +00:00
if err := waitStartup ( ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "wait for server startup: %w" , err )
2020-11-18 21:16:18 +00:00
}
2021-03-22 22:04:19 +00:00
// Start fleet02 (UI server) after fleet01 (agent/fleetctl server)
// has finished starting up so that there is no conflict with
// running database migrations.
2022-05-16 21:06:29 +00:00
cmd = compose . Command ( "up" , "-d" , "--remove-orphans" , "fleet02" )
2021-06-18 18:58:15 +00:00
cmd . Env = append ( os . Environ ( ) , "FLEET_LICENSE_KEY=" + c . String ( licenseKeyFlagName ) )
2021-06-10 16:17:49 +00:00
out , err = cmd . CombinedOutput ( )
2021-03-22 22:04:19 +00:00
if err != nil {
fmt . Println ( string ( out ) )
2022-05-16 21:06:29 +00:00
return fmt . Errorf ( "Failed to run %s" , compose )
2021-03-22 22:04:19 +00:00
}
2021-01-05 01:11:10 +00:00
fmt . Println ( "Initializing server..." )
const (
address = "https://localhost:8412"
2021-06-24 20:42:29 +00:00
email = "admin@example.com"
2022-05-18 17:03:00 +00:00
password = "preview1337#"
2021-01-05 01:11:10 +00:00
)
2021-07-28 15:05:03 +00:00
fleetClient , err := service . NewClient ( address , true , "" , "" )
2021-01-05 01:11:10 +00:00
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Error creating Fleet API client handler: %w" , err )
2021-01-05 01:11:10 +00:00
}
2021-10-22 22:35:01 +00:00
token , err := fleetClient . Setup ( email , "Admin" , password , "Fleet for osquery" )
2021-01-05 01:11:10 +00:00
if err != nil {
2021-11-15 14:11:38 +00:00
switch ctxerr . Cause ( err ) . ( type ) {
2021-03-19 23:45:21 +00:00
case service . SetupAlreadyErr :
// Ignore this error
default :
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Error setting up Fleet: %w" , err )
2021-01-28 15:57:32 +00:00
}
2021-01-05 01:11:10 +00:00
}
2021-01-29 02:18:21 +00:00
configPath , context := c . String ( "config" ) , "default"
2021-01-29 01:15:38 +00:00
2021-01-29 02:18:21 +00:00
contextConfig := Context {
Address : address ,
2021-06-24 20:42:29 +00:00
Email : email ,
2021-01-29 02:18:21 +00:00
Token : token ,
TLSSkipVerify : true ,
}
config , err := readConfig ( configPath )
2021-01-05 01:11:10 +00:00
if err != nil {
2021-01-29 02:18:21 +00:00
// No existing config
2021-02-13 16:41:46 +00:00
config . Contexts = map [ string ] Context {
"default" : contextConfig ,
}
2021-01-29 02:18:21 +00:00
} else {
fmt . Println ( "Configured fleetctl in the 'preview' context to avoid overwriting existing config." )
context = "preview"
config . Contexts [ "preview" ] = contextConfig
2021-01-05 01:11:10 +00:00
}
2021-01-29 02:18:21 +00:00
c . Set ( "context" , context )
if err := writeConfig ( configPath , config ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Error writing fleetctl configuration: %w" , err )
2021-01-05 01:11:10 +00:00
}
2021-01-29 02:18:21 +00:00
// Create client and get enroll secret
client , err := unauthenticatedClientFromCLI ( c )
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Error making fleetctl client: %w" , err )
2021-01-29 02:18:21 +00:00
}
2021-06-24 20:42:29 +00:00
token , err = client . Login ( email , password )
2021-01-29 02:18:21 +00:00
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "fleetctl login failed: %w" , err )
2021-01-05 01:11:10 +00:00
}
2021-01-29 01:15:38 +00:00
if err := setConfigValue ( configPath , context , "token" , token ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Error setting token for the current context: %w" , err )
2021-01-05 01:11:10 +00:00
}
2021-01-29 02:18:21 +00:00
client . SetToken ( token )
2021-01-05 01:11:10 +00:00
2021-08-16 16:22:10 +00:00
fmt . Println ( "Loading standard query library..." )
2022-05-02 16:10:11 +00:00
var buf [ ] byte
if fp := c . String ( stdQueryLibFilePath ) ; fp != "" {
var err error
buf , err = ioutil . ReadFile ( fp )
if err != nil {
return fmt . Errorf ( "failed to read standard query library file %q: %w" , fp , err )
}
} else {
var err error
buf , err = downloadStandardQueryLibrary ( )
if err != nil {
return fmt . Errorf ( "failed to download standard query library: %w" , err )
}
2021-08-16 16:22:10 +00:00
}
2022-08-05 22:07:32 +00:00
specs , err := spec . GroupFromBytes ( buf )
if err != nil {
return err
}
logf := func ( format string , a ... interface { } ) {
fmt . Fprintf ( c . App . Writer , format , a ... )
}
err = client . ApplyGroup ( c . Context , specs , logf )
2021-08-16 16:22:10 +00:00
if err != nil {
2022-01-28 19:28:07 +00:00
return err
2021-08-16 16:22:10 +00:00
}
2022-08-03 18:44:34 +00:00
// disable analytics collection and enable software inventory for preview
2022-08-25 16:41:50 +00:00
// TODO(roperzh): replace `host_settings` with `features` once the
// Docker image used for preview (fleetdm/fleetctl:latest) is released
2021-10-07 12:02:49 +00:00
if err := client . ApplyAppConfig ( map [ string ] map [ string ] bool {
2021-10-12 14:20:28 +00:00
"host_settings" : { "enable_software_inventory" : true } ,
"server_settings" : { "enable_analytics" : false } ,
2021-10-07 12:02:49 +00:00
} ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "failed to apply updated app config: %w" , err )
2021-10-07 12:02:49 +00:00
}
2021-01-29 02:18:21 +00:00
secrets , err := client . GetEnrollSecretSpec ( )
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Error retrieving enroll secret: %w" , err )
2021-01-05 01:11:10 +00:00
}
2021-05-31 16:02:05 +00:00
if len ( secrets . Secrets ) != 1 {
2021-01-29 02:18:21 +00:00
return errors . New ( "Expected 1 active enroll secret" )
2021-01-05 01:11:10 +00:00
}
2022-08-03 18:44:34 +00:00
// disable analytics collection for preview
2021-10-15 21:23:06 +00:00
if err := client . ApplyAppConfig ( map [ string ] map [ string ] bool {
2021-10-22 22:35:01 +00:00
"server_settings" : { "enable_analytics" : false } ,
} ,
2021-10-15 21:23:06 +00:00
) ; err != nil {
2022-08-03 18:44:34 +00:00
return fmt . Errorf ( "Error disabling analytics collection in app config: %w" , err )
2021-10-15 21:23:06 +00:00
}
2021-12-31 23:13:28 +00:00
fmt . Println ( "Fleet will now log you into the UI automatically." )
2021-10-28 04:50:32 +00:00
fmt . Println ( "You can also open the UI at this URL: http://localhost:1337/previewlogin." )
fmt . Println ( "Email:" , email )
fmt . Println ( "Password:" , password )
2021-12-31 23:13:28 +00:00
if ! c . Bool ( noHostsFlagName ) {
fmt . Println ( "Enrolling local host..." )
2021-10-15 21:23:06 +00:00
2022-04-27 21:17:20 +00:00
if err := downloadOrbitAndStart ( previewDir , secrets . Secrets [ 0 ] . Secret , address , c . String ( orbitChannel ) , c . String ( osquerydChannel ) , c . String ( updateURL ) , c . String ( updateRootKeys ) ) ; err != nil {
2021-12-31 23:13:28 +00:00
return fmt . Errorf ( "downloading orbit and osqueryd: %w" , err )
}
2021-10-15 21:23:06 +00:00
2021-12-31 23:13:28 +00:00
// Give it a bit of time so the current device is the one with id 1
fmt . Println ( "Waiting for host to enroll..." )
if err := waitFirstHost ( client ) ; err != nil {
return fmt . Errorf ( "wait for current host: %w" , err )
}
2021-10-27 20:41:03 +00:00
2022-05-10 14:52:33 +00:00
if ! c . Bool ( disableOpenBrowser ) {
if err := open . Browser ( "http://localhost:1337/previewlogin" ) ; err != nil {
fmt . Println ( "Automatic browser open failed. Please navigate to http://localhost:1337/previewlogin." )
}
2021-12-31 23:13:28 +00:00
}
2021-10-28 04:50:32 +00:00
2021-12-31 23:13:28 +00:00
fmt . Println ( "Starting simulated Linux hosts..." )
2022-05-16 21:06:29 +00:00
cmd = compose . Command ( "up" , "-d" , "--remove-orphans" )
2021-12-31 23:13:28 +00:00
cmd . Dir = filepath . Join ( previewDir , "osquery" )
cmd . Env = append ( os . Environ ( ) ,
"ENROLL_SECRET=" + secrets . Secrets [ 0 ] . Secret ,
"FLEET_URL=" + address ,
)
out , err = cmd . CombinedOutput ( )
if err != nil {
fmt . Println ( string ( out ) )
2022-05-16 21:06:29 +00:00
return fmt . Errorf ( "Failed to run %s" , compose )
2021-12-31 23:13:28 +00:00
}
} else {
2022-05-10 14:52:33 +00:00
if ! c . Bool ( disableOpenBrowser ) {
if err := open . Browser ( "http://localhost:1337/previewlogin" ) ; err != nil {
fmt . Println ( "Automatic browser open failed. Please navigate to http://localhost:1337/previewlogin." )
}
2021-12-31 23:13:28 +00:00
}
2021-01-29 02:18:21 +00:00
}
fmt . Println ( "Preview environment complete. Enjoy using Fleet!" )
2020-11-18 21:16:18 +00:00
return nil
} ,
}
}
2021-10-07 13:19:10 +00:00
var testOverridePreviewDirectory string
2021-01-28 15:57:32 +00:00
func previewDirectory ( ) string {
2021-10-07 13:19:10 +00:00
if testOverridePreviewDirectory != "" {
return testOverridePreviewDirectory
}
2021-01-28 15:57:32 +00:00
homeDir , err := os . UserHomeDir ( )
if err != nil {
homeDir = "~"
}
return filepath . Join ( homeDir , ".fleet" , "preview" )
}
2021-10-12 14:20:28 +00:00
func downloadFiles ( branch string ) error {
resp , err := http . Get ( fmt . Sprintf ( downloadUrl , branch ) )
2020-11-18 21:16:18 +00:00
if err != nil {
return err
}
defer resp . Body . Close ( )
if resp . StatusCode != http . StatusOK {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "download got status %d" , resp . StatusCode )
2020-11-18 21:16:18 +00:00
}
zipContents , err := ioutil . ReadAll ( resp . Body )
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "read download contents: %w" , err )
2020-11-18 21:16:18 +00:00
}
zipReader , err := zip . NewReader ( bytes . NewReader ( zipContents ) , int64 ( len ( zipContents ) ) )
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "open download contents for unzip: %w" , err )
2020-11-18 21:16:18 +00:00
}
// zip.NewReader does not need to be closed (and cannot be)
2021-10-12 14:20:28 +00:00
if err := unzip ( zipReader , branch ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "unzip download contents: %w" , err )
2020-11-18 21:16:18 +00:00
}
return nil
}
2021-07-28 15:05:03 +00:00
func downloadStandardQueryLibrary ( ) ( [ ] byte , error ) {
resp , err := http . Get ( standardQueryLibraryUrl )
if err != nil {
return nil , err
}
if resp . StatusCode != http . StatusOK {
2021-11-22 14:13:26 +00:00
return nil , fmt . Errorf ( "status: %d" , resp . StatusCode )
2021-07-28 15:05:03 +00:00
}
buf , err := ioutil . ReadAll ( resp . Body )
if err != nil {
2021-11-22 14:13:26 +00:00
return nil , fmt . Errorf ( "read response body: %w" , err )
2021-07-28 15:05:03 +00:00
}
return buf , nil
}
2020-11-18 21:16:18 +00:00
// Adapted from https://stackoverflow.com/a/24792688/491710
2021-10-12 14:20:28 +00:00
func unzip ( r * zip . Reader , branch string ) error {
2021-01-28 15:57:32 +00:00
previewDir := previewDirectory ( )
2020-11-18 21:16:18 +00:00
// Closure to address file descriptors issue with all the deferred .Close()
// methods
2021-10-12 14:20:28 +00:00
replacePath := fmt . Sprintf ( "osquery-in-a-box-%s" , branch )
2020-11-18 21:16:18 +00:00
extractAndWriteFile := func ( f * zip . File ) error {
rc , err := f . Open ( )
if err != nil {
return err
}
defer rc . Close ( )
path := f . Name
2021-10-12 14:20:28 +00:00
path = strings . Replace ( path , replacePath , previewDir , 1 )
2020-11-18 21:16:18 +00:00
if f . FileInfo ( ) . IsDir ( ) {
if err := os . MkdirAll ( path , f . Mode ( ) ) ; err != nil {
return err
}
} else {
if err := os . MkdirAll ( filepath . Dir ( path ) , f . Mode ( ) ) ; err != nil {
return err
}
f , err := os . OpenFile ( path , os . O_WRONLY | os . O_CREATE | os . O_TRUNC , f . Mode ( ) )
if err != nil {
return err
}
defer f . Close ( )
_ , err = io . Copy ( f , rc )
if err != nil {
return err
}
}
return nil
}
for _ , f := range r . File {
2022-05-02 16:10:11 +00:00
// Prevent zip-slip attack.
if strings . Contains ( f . Name , ".." ) {
return fmt . Errorf ( "invalid path in zip: %q" , f . Name )
}
2020-11-18 21:16:18 +00:00
err := extractAndWriteFile ( f )
if err != nil {
return err
}
}
return nil
}
func waitStartup ( ) error {
2020-11-20 23:37:14 +00:00
retryStrategy := backoff . NewExponentialBackOff ( )
retryStrategy . MaxInterval = 1 * time . Second
2021-11-24 20:56:54 +00:00
client := fleethttp . NewClient ( fleethttp . WithTLSClientConfig ( & tls . Config { InsecureSkipVerify : true } ) )
2020-11-20 23:37:14 +00:00
2020-11-18 21:16:18 +00:00
if err := backoff . Retry (
func ( ) error {
resp , err := client . Get ( "https://localhost:8412/healthz" )
if err != nil {
return err
}
if resp . StatusCode != http . StatusOK {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "got status code %d" , resp . StatusCode )
2020-11-18 21:16:18 +00:00
}
return nil
} ,
2020-11-20 23:37:14 +00:00
retryStrategy ,
2020-11-18 21:16:18 +00:00
) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "checking server health: %w" , err )
2020-11-18 21:16:18 +00:00
}
return nil
}
2021-01-28 15:57:32 +00:00
2021-10-27 20:41:03 +00:00
func waitFirstHost ( client * service . Client ) error {
retryStrategy := backoff . NewExponentialBackOff ( )
retryStrategy . MaxInterval = 1 * time . Second
if err := backoff . Retry (
func ( ) error {
hosts , err := client . GetHosts ( "" )
if err != nil {
return err
}
if len ( hosts ) == 0 {
return errors . New ( "no hosts yet" )
}
return nil
} ,
retryStrategy ,
) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "checking host count: %w" , err )
2021-10-27 20:41:03 +00:00
}
return nil
}
2021-01-28 15:57:32 +00:00
func checkDocker ( ) error {
// Check installed
if _ , err := exec . LookPath ( "docker" ) ; err != nil {
return errors . New ( "Docker is required for the fleetctl preview experience.\n\nPlease install Docker (https://docs.docker.com/get-docker/)." )
}
// Check running
if err := exec . Command ( "docker" , "info" ) . Run ( ) ; err != nil {
return errors . New ( "Please start Docker daemon before running fleetctl preview." )
}
return nil
}
2021-03-19 23:45:21 +00:00
func previewStopCommand ( ) * cli . Command {
return & cli . Command {
Name : "stop" ,
Usage : "Stop the Fleet preview server and dependencies" ,
Flags : [ ] cli . Flag {
configFlag ( ) ,
contextFlag ( ) ,
debugFlag ( ) ,
} ,
Action : func ( c * cli . Context ) error {
if err := checkDocker ( ) ; err != nil {
return err
}
2022-05-16 21:06:29 +00:00
compose , err := newDockerCompose ( )
if err != nil {
return err
}
2021-03-19 23:45:21 +00:00
previewDir := previewDirectory ( )
if err := os . Chdir ( previewDir ) ; err != nil {
return err
}
if _ , err := os . Stat ( "docker-compose.yml" ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "docker-compose file not found in preview directory: %w" , err )
2021-03-19 23:45:21 +00:00
}
2022-05-16 21:06:29 +00:00
out , err := compose . Command ( "stop" ) . CombinedOutput ( )
2021-03-19 23:45:21 +00:00
if err != nil {
fmt . Println ( string ( out ) )
2022-05-16 21:06:29 +00:00
return fmt . Errorf ( "Failed to run %s stop for Fleet server and dependencies" , compose )
2021-05-29 00:19:31 +00:00
}
2022-05-16 21:06:29 +00:00
cmd := compose . Command ( "stop" )
2021-05-29 00:19:31 +00:00
cmd . Dir = filepath . Join ( previewDir , "osquery" )
2021-06-18 18:58:15 +00:00
cmd . Env = append ( os . Environ ( ) ,
2021-05-29 00:19:31 +00:00
// Note that these must be set even though they are unused while
// stopping because docker-compose will error otherwise.
"ENROLL_SECRET=empty" ,
"FLEET_URL=empty" ,
)
out , err = cmd . CombinedOutput ( )
if err != nil {
fmt . Println ( string ( out ) )
2022-05-16 21:06:29 +00:00
return fmt . Errorf ( "Failed to run %d stop for simulated hosts" , compose )
2021-03-19 23:45:21 +00:00
}
2021-10-15 21:23:06 +00:00
if err := stopOrbit ( previewDir ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Failed to stop orbit: %w" , err )
2021-10-15 21:23:06 +00:00
}
2021-03-19 23:45:21 +00:00
fmt . Println ( "Fleet preview server and dependencies stopped. Start again with fleetctl preview." )
return nil
} ,
}
}
func previewResetCommand ( ) * cli . Command {
return & cli . Command {
Name : "reset" ,
Usage : "Reset the Fleet preview server and dependencies" ,
Flags : [ ] cli . Flag {
configFlag ( ) ,
contextFlag ( ) ,
debugFlag ( ) ,
} ,
Action : func ( c * cli . Context ) error {
if err := checkDocker ( ) ; err != nil {
return err
}
2022-05-16 21:06:29 +00:00
compose , err := newDockerCompose ( )
if err != nil {
return err
}
2021-03-19 23:45:21 +00:00
previewDir := previewDirectory ( )
if err := os . Chdir ( previewDir ) ; err != nil {
return err
}
if _ , err := os . Stat ( "docker-compose.yml" ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "docker-compose file not found in preview directory: %w" , err )
2021-03-19 23:45:21 +00:00
}
2022-05-16 21:06:29 +00:00
out , err := compose . Command ( "rm" , "-sf" ) . CombinedOutput ( )
2021-03-19 23:45:21 +00:00
if err != nil {
fmt . Println ( string ( out ) )
2022-05-16 21:06:29 +00:00
return fmt . Errorf ( "Failed to run %s rm -sf for Fleet server and dependencies." , compose )
2021-05-29 00:19:31 +00:00
}
2022-05-16 21:06:29 +00:00
cmd := compose . Command ( "rm" , "-sf" )
2021-05-29 00:19:31 +00:00
cmd . Dir = filepath . Join ( previewDir , "osquery" )
2021-06-18 18:58:15 +00:00
cmd . Env = append ( os . Environ ( ) ,
2021-05-29 00:19:31 +00:00
// Note that these must be set even though they are unused while
// stopping because docker-compose will error otherwise.
"ENROLL_SECRET=empty" ,
"FLEET_URL=empty" ,
)
out , err = cmd . CombinedOutput ( )
if err != nil {
fmt . Println ( string ( out ) )
2022-05-16 21:06:29 +00:00
return fmt . Errorf ( "Failed to run %s rm -sf for simulated hosts." , compose )
2021-03-19 23:45:21 +00:00
}
2021-10-27 18:57:24 +00:00
if err := stopOrbit ( previewDir ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "Failed to stop orbit: %w" , err )
2021-10-27 18:57:24 +00:00
}
2022-04-27 21:17:20 +00:00
if err := os . RemoveAll ( filepath . Join ( previewDir , "tuf-metadata.json" ) ) ; err != nil {
return fmt . Errorf ( "failed to remove preview update metadata file: %w" , err )
}
if err := os . RemoveAll ( filepath . Join ( previewDir , "bin" ) ) ; err != nil {
return fmt . Errorf ( "failed to remove preview bin directory: %w" , err )
}
2021-03-19 23:45:21 +00:00
fmt . Println ( "Fleet preview server and dependencies reset. Start again with fleetctl preview." )
return nil
} ,
}
}
2021-10-15 21:23:06 +00:00
func storePidFile ( destDir string , pid int ) error {
2022-06-01 16:06:57 +00:00
pidFilePath := filepath . Join ( destDir , "orbit.pid" )
2022-03-21 17:53:53 +00:00
err := os . WriteFile ( pidFilePath , [ ] byte ( fmt . Sprint ( pid ) ) , os . FileMode ( 0 o644 ) )
2021-10-15 21:23:06 +00:00
if err != nil {
return fmt . Errorf ( "error writing pidfile %s: %s" , pidFilePath , err )
}
return nil
}
2021-10-29 14:27:12 +00:00
func readPidFromFile ( destDir string , what string ) ( int , error ) {
2022-06-01 16:06:57 +00:00
pidFilePath := filepath . Join ( destDir , what )
2021-10-15 21:23:06 +00:00
data , err := os . ReadFile ( pidFilePath )
if err != nil {
2021-11-15 21:12:25 +00:00
return 0 , fmt . Errorf ( "error reading pidfile %s: %w" , pidFilePath , err )
2021-10-15 21:23:06 +00:00
}
2021-11-15 21:12:25 +00:00
return strconv . Atoi ( strings . TrimSpace ( string ( data ) ) )
2021-10-15 21:23:06 +00:00
}
2021-11-15 21:12:25 +00:00
// processNameMatches returns whether the process running with the given pid matches
// the executable name (case insensitive).
//
// If there's no process running with the given pid then (false, nil) is returned.
func processNameMatches ( pid int , expectedPrefix string ) ( bool , error ) {
process , err := ps . FindProcess ( pid )
2021-10-27 18:57:24 +00:00
if err != nil {
2021-11-22 14:13:26 +00:00
return false , fmt . Errorf ( "find process: %d: %w" , pid , err )
2021-10-27 18:57:24 +00:00
}
2021-11-15 21:12:25 +00:00
if process == nil {
return false , nil
2021-10-27 18:57:24 +00:00
}
2021-11-15 21:12:25 +00:00
return strings . HasPrefix ( strings . ToLower ( process . Executable ( ) ) , strings . ToLower ( expectedPrefix ) ) , nil
2021-10-27 18:57:24 +00:00
}
2022-04-27 21:17:20 +00:00
func downloadOrbitAndStart ( destDir , enrollSecret , address , orbitChannel , osquerydChannel , updateURL , updateRoots string ) error {
2021-11-15 21:12:25 +00:00
// Stop any current intance of orbit running, otherwise the configured enroll secret
// won't match the generated in the preview run.
if err := stopOrbit ( destDir ) ; err != nil {
fmt . Println ( "Failed to stop an existing instance of orbit running: " , err )
return err
2021-10-27 18:57:24 +00:00
}
2021-10-29 14:27:12 +00:00
fmt . Println ( "Trying to clear orbit and osquery directories..." )
2022-06-01 16:06:57 +00:00
if err := os . RemoveAll ( filepath . Join ( destDir , "osquery.db" ) ) ; err != nil {
2021-10-29 14:27:12 +00:00
fmt . Println ( "Warning: clearing osquery db dir:" , err )
}
2022-06-01 16:06:57 +00:00
if err := os . RemoveAll ( filepath . Join ( destDir , "orbit.db" ) ) ; err != nil {
2021-10-29 14:27:12 +00:00
fmt . Println ( "Warning: clearing orbit db dir:" , err )
}
2022-03-22 21:00:00 +00:00
if err := cleanUpSocketFiles ( destDir ) ; err != nil {
fmt . Println ( "Warning: cleaning up socket files:" , err )
}
2021-10-29 14:27:12 +00:00
2021-10-15 21:23:06 +00:00
updateOpt := update . DefaultOptions
2022-03-15 19:04:12 +00:00
// Override default channels with the provided values.
2022-03-21 17:53:53 +00:00
updateOpt . Targets . SetTargetChannel ( "orbit" , orbitChannel )
updateOpt . Targets . SetTargetChannel ( "osqueryd" , osquerydChannel )
2022-03-15 19:04:12 +00:00
2021-10-15 21:23:06 +00:00
updateOpt . RootDirectory = destDir
2022-04-27 21:17:20 +00:00
if updateURL != "" {
updateOpt . ServerURL = updateURL
}
if updateRoots != "" {
updateOpt . RootKeys = updateRoots
}
2022-01-31 13:41:11 +00:00
if _ , err := packaging . InitializeUpdates ( updateOpt ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "initialize updates: %w" , err )
2021-10-15 21:23:06 +00:00
}
2022-03-15 19:04:12 +00:00
orbitPath , err := update . NewDisabled ( updateOpt ) . ExecutableLocalPath ( "orbit" )
if err != nil {
return fmt . Errorf ( "failed to locate executable for orbit: %w" , err )
}
cmd := exec . Command ( orbitPath ,
2021-10-15 21:23:06 +00:00
"--root-dir" , destDir ,
"--fleet-url" , address ,
"--insecure" ,
2021-10-29 14:27:12 +00:00
"--debug" ,
2021-10-15 21:23:06 +00:00
"--enroll-secret" , enrollSecret ,
2022-03-15 19:04:12 +00:00
"--orbit-channel" , orbitChannel ,
"--osqueryd-channel" , osquerydChannel ,
2022-06-01 16:06:57 +00:00
"--log-file" , filepath . Join ( destDir , "orbit.log" ) ,
2021-10-15 21:23:06 +00:00
)
if err := cmd . Start ( ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "starting orbit: %w" , err )
2021-10-15 21:23:06 +00:00
}
if err := storePidFile ( destDir , cmd . Process . Pid ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "saving pid file: %w" , err )
2021-10-15 21:23:06 +00:00
}
return nil
}
2022-03-22 21:00:00 +00:00
// cleanUpSocketFiles cleans up fleet-osqueryd's socket file
// ("orbit-osquery.em") and osquery extension socket files
// ("orbit-osquery.em.*").
func cleanUpSocketFiles ( path string ) error {
entries , err := os . ReadDir ( path )
if err != nil {
return fmt . Errorf ( "read dir: %w" , err )
}
for _ , entry := range entries {
if ! strings . HasPrefix ( entry . Name ( ) , "orbit-osquery.em" ) {
continue
}
entryPath := filepath . Join ( path , entry . Name ( ) )
if err := os . Remove ( entryPath ) ; err != nil {
return fmt . Errorf ( "remove %q: %w" , entryPath , err )
}
}
return nil
}
2021-10-15 21:23:06 +00:00
func stopOrbit ( destDir string ) error {
2021-11-15 21:12:25 +00:00
err := killFromPIDFile ( destDir , "osquery.pid" , "osqueryd" )
2021-10-29 14:27:12 +00:00
if err != nil {
return err
}
2021-11-15 21:12:25 +00:00
err = killFromPIDFile ( destDir , "orbit.pid" , "orbit" )
2021-10-29 14:27:12 +00:00
if err != nil {
return err
}
return nil
}
2021-11-15 21:12:25 +00:00
func killFromPIDFile ( destDir string , pidFileName string , expectedExecName string ) error {
pid , err := readPidFromFile ( destDir , pidFileName )
2021-10-29 14:27:12 +00:00
switch {
case err == nil :
// OK
case errors . Is ( err , os . ErrNotExist ) :
return nil // we assume it's not running
default :
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "reading pid from: %s: %w" , destDir , err )
2021-10-29 14:27:12 +00:00
}
2021-11-15 21:12:25 +00:00
matches , err := processNameMatches ( pid , expectedExecName )
2021-10-15 21:23:06 +00:00
if err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "inspecting process %d: %w" , pid , err )
2021-11-15 21:12:25 +00:00
}
if ! matches {
// Nothing to do, another process may be running with this pid
// (e.g. could happen after a restart).
return nil
}
if err := killPID ( pid ) ; err != nil {
2021-11-22 14:13:26 +00:00
return fmt . Errorf ( "killing %d: %w" , pid , err )
2021-10-15 21:23:06 +00:00
}
return nil
}