Skip to content
Merged
Show file tree
Hide file tree
Changes from 14 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
26cfa6b
Merge branch 'main' of github.com:FlowFuse/device-agent into feat-dai…
ppawlowski Aug 6, 2025
fc33906
Merge branch 'feat-dai-no-otc-install' of github.com:FlowFuse/device-…
ppawlowski Aug 7, 2025
ea19752
Merge branch 'feat-dai-no-otc-install' of github.com:FlowFuse/device-…
ppawlowski Aug 18, 2025
884bc74
Add ValidateUninstallDirectory, adjust existing function to support c…
ppawlowski Aug 18, 2025
ad5dad5
Introduce getDefaultWorkingDirectory, adjust GetWorkingDirectory and …
ppawlowski Aug 18, 2025
3bba3e0
Add support for custom directory in config package
ppawlowski Aug 18, 2025
d02a4a6
Add support for custom directory in nodejs package
ppawlowski Aug 18, 2025
b1831c8
Add support for custom directory in service package
ppawlowski Aug 18, 2025
873e1f2
Add support for custom directory in the install package
ppawlowski Aug 18, 2025
9ef82d6
Add support for custom directory in the entry file
ppawlowski Aug 18, 2025
4b13b13
Improve help message output
ppawlowski Aug 18, 2025
bc54096
Update readme
ppawlowski Aug 18, 2025
87811a0
Merge branches 'feat-dai-custom-workdir' and 'feat-dai-no-otc-install…
ppawlowski Aug 18, 2025
1119cdc
Check both device.yaml and installer.conf before uninstall
ppawlowski Aug 20, 2025
2cb7eab
Ensure configuration command is executed within install directory
ppawlowski Aug 28, 2025
d4e7337
Adjust command line args debug output
ppawlowski Aug 28, 2025
7824dd0
Fix windows service app parameters
ppawlowski Aug 28, 2025
b5b4669
Add initial installer e2e testing scenarios description
ppawlowski Aug 28, 2025
7208b80
Fix nssm appParameter
ppawlowski Aug 29, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions installer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ powershell -c Unblock-File -Path .\flowfuse-device-agent-installer.exe
| `--nodejs-version` | `-n` | `20.19.1` | Node.js version to install (minimum) |
| `--agent-version` | `-a` | `latest` | Device agent version to install/update to |
| `--service-user` | `-s` | `flowfuse` | Username for the service account (linux/macos)|
| `--dir` | `-d` | `/opt/flowfuse-device` (Linux/macOS) or `C:\opt\flowfuse-device` (Windows) | Installation directory for the device agent |
| `--uninstall` | | `false` | Uninstall the device agent |
| `--update-nodejs` | | `false` | Update bundled Node.js to specified version |
| `--update-agent` | | `false` | Update the Device Agent package to specified version |
Expand Down
84 changes: 58 additions & 26 deletions installer/go/cmd/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,33 +30,34 @@ import (
// Parameters:
// - nodeVersion: The version of Node.js to install or use
// - agentVersion: The version of the FlowFuse Device Agent to install
// - installerDir: The directory where the installer files are located
// - url: The URL of the FlowFuse instance to connect to
// - otc: The one-time code (OTC) used for device registration
// - customWorkDir: Optional custom working directory path. If empty, uses default path.
// - update: Whether this is an update operation
//
// Returns:
// - error: An error object if any step of the installation fails, nil otherwise
//
// The function logs detailed information about each step of the process.
func Install(nodeVersion, agentVersion, installerDir, url, otc string, update bool) error {
func Install(nodeVersion, agentVersion, url, otc, customWorkDir string, update bool) error {
logger.LogFunctionEntry("Install", map[string]interface{}{
"nodeVersion": nodeVersion,
"agentVersion": agentVersion,
"installerDir": installerDir,
"url": url,
"otc": otc,
"nodeVersion": nodeVersion,
"agentVersion": agentVersion,
"url": url,
"otc": otc,
"customWorkDir": customWorkDir,
})

// Run pre-install validation
logger.Debug("Running pre-check...")
if err := validate.PreInstall("flowfuse-device-agent"); err != nil {
if err := validate.PreInstall("flowfuse-device-agent", customWorkDir); err != nil {
logger.LogFunctionExit("Install", nil, err)
return fmt.Errorf("pre-check failed: %w", err)
}

// Create working directory
logger.Debug("Creating working directory...")
workDir, err := utils.CreateWorkingDirectory()
workDir, err := utils.CreateWorkingDirectory(customWorkDir)
if err != nil {
logger.Error("Failed to create working directory: %v", err)
logger.LogFunctionExit("Install", nil, err)
Expand Down Expand Up @@ -122,7 +123,7 @@ func Install(nodeVersion, agentVersion, installerDir, url, otc string, update bo
// Save the configuration
if agentVersion == "latest" {
var err error
agentVersion, err = nodejs.GetLatestDeviceAgentVersion()
agentVersion, err = nodejs.GetLatestDeviceAgentVersion(workDir)
if err != nil {
return fmt.Errorf("failed to get latest device agent version: %v", err)
}
Expand All @@ -133,7 +134,7 @@ func Install(nodeVersion, agentVersion, installerDir, url, otc string, update bo
AgentVersion: agentVersion,
}
logger.Debug("Saving configuration: %+v", cfg)
if err := config.SaveConfig(cfg); err != nil {
if err := config.SaveConfig(cfg, workDir); err != nil {
logger.Error("Could not save configuration: %v", err)
}
logger.Info("")
Expand Down Expand Up @@ -171,8 +172,38 @@ func Install(nodeVersion, agentVersion, installerDir, url, otc string, update bo
// default values when the configuration cannot be loaded.
//
// Returns an error if any step in the uninstallation process fails.
func Uninstall() error {
logger.LogFunctionEntry("Uninstall", nil)
func Uninstall(customWorkDir string) error {
logger.LogFunctionEntry("Uninstall", map[string]interface{}{
"customWorkDir": customWorkDir,
})

// Get the working directory first to show it in the confirmation prompt
logger.Debug("Getting working directory...")
workDir, err := utils.GetWorkingDirectory(customWorkDir)
if err != nil {
logger.Error("Failed to get working directory: %v", err)
logger.LogFunctionExit("Uninstall", nil, err)
return fmt.Errorf("failed to get working directory: %w", err)
}

// Validate that this is actually a FlowFuse Device Agent installation
logger.Debug("Validating uninstall directory...")
if err := validate.ValidateUninstallDirectory(workDir); err != nil {
logger.Error("Uninstall validation failed: %v", err)
logger.LogFunctionExit("Uninstall", nil, err)
return fmt.Errorf("uninstall validation failed: %w", err)
}
logger.Debug("Uninstall directory validation passed")

// Show confirmation prompt with the directory path
logger.Info("This will uninstall the FlowFuse Device Agent from: %s\n", workDir)

confirmed := utils.PromptYesNo("Do you want to proceed with the removal?", false)
if !confirmed {
logger.Info("Uninstall cancelled by user")
logger.LogFunctionExit("Uninstall", "cancelled", nil)
return nil
}

logger.Debug("Running pre-check...")
if err := utils.CheckPermissions(); err != nil {
Expand All @@ -197,7 +228,7 @@ func Uninstall() error {

// Get the working directory
logger.Debug("Getting working directory...")
workDir, err := utils.GetWorkingDirectory()
workDir, err = utils.GetWorkingDirectory(customWorkDir)
if err != nil {
logger.Error("Failed to get working directory: %v", err)
logger.LogFunctionExit("Uninstall", nil, err)
Expand All @@ -217,7 +248,7 @@ func Uninstall() error {
// Load saved configuration to get the system username
logger.Debug("Loading saved configuration...")
savedUsername := ""
cfg, err := config.LoadConfig()
cfg, err := config.LoadConfig(customWorkDir)
if err != nil {
logger.Error("Could not load configuration: %v", err)
logger.Debug("Will use the current username setting for uninstallation.")
Expand Down Expand Up @@ -282,12 +313,13 @@ func Uninstall() error {
// - error: An error object if any step of the update fails, nil otherwise
//
// func Update(options UpdateOptions) error {
func Update(agentVersion, nodeVersion string, updateAgent, updateNode bool) error {
func Update(agentVersion, nodeVersion, customWorkDir string, updateAgent, updateNode bool) error {
logger.LogFunctionEntry("Update", map[string]interface{}{
"updateNode": updateNode,
"nodeVersion": nodeVersion,
"updateAgent": updateAgent,
"agentVersion": agentVersion,
"updateNode": updateNode,
"nodeVersion": nodeVersion,
"updateAgent": updateAgent,
"agentVersion": agentVersion,
"customWorkDir": customWorkDir,
})

// Validate that at least one update option is specified
Expand Down Expand Up @@ -316,7 +348,7 @@ func Update(agentVersion, nodeVersion string, updateAgent, updateNode bool) erro

// Get the working directory
logger.Debug("Getting working directory...")
workDir, err := utils.GetWorkingDirectory()
workDir, err := utils.GetWorkingDirectory(customWorkDir)
if err != nil {
logger.Error("Failed to get working directory: %v", err)
logger.LogFunctionExit("Update", nil, err)
Expand All @@ -341,7 +373,7 @@ func Update(agentVersion, nodeVersion string, updateAgent, updateNode bool) erro
}

if updateAgent {
isNeeded, err := nodejs.IsAgentUpdateRequired(agentVersion)
isNeeded, err := nodejs.IsAgentUpdateRequired(agentVersion, workDir)
if err != nil {
logger.Error("Failed to check if Device Agent update is needed: %v", err)
return fmt.Errorf("failed to check Device Agent update requirement: %w", err)
Expand Down Expand Up @@ -378,7 +410,7 @@ func Update(agentVersion, nodeVersion string, updateAgent, updateNode bool) erro
logger.LogFunctionExit("Update", nil, err)
return fmt.Errorf("node.js update failed: %w", err)
}
if err := config.UpdateConfigField("nodeVersion", nodeVersion); err != nil {
if err := config.UpdateConfigField("nodeVersion", nodeVersion, customWorkDir); err != nil {
logger.Error("Failed to update node version in configuration: %v", err)
logger.LogFunctionExit("Update", nil, err)
return fmt.Errorf("failed to update node version in configuration: %w", err)
Expand All @@ -389,7 +421,7 @@ func Update(agentVersion, nodeVersion string, updateAgent, updateNode bool) erro
// Load saved configuration
logger.Debug("Loading configuration...")
savedAgentVersion := ""
cfg, err := config.LoadConfig()
cfg, err := config.LoadConfig(customWorkDir)
if err != nil {
logger.Error("Could not load configuration: %v", err)
return fmt.Errorf("could not load configuration: %w", err)
Expand Down Expand Up @@ -425,12 +457,12 @@ func Update(agentVersion, nodeVersion string, updateAgent, updateNode bool) erro

if agentVersion == "latest" {
var err error
agentVersion, err = nodejs.GetLatestDeviceAgentVersion()
agentVersion, err = nodejs.GetLatestDeviceAgentVersion(workDir)
if err != nil {
return fmt.Errorf("failed to get latest device agent version: %v", err)
}
}
if err := config.UpdateConfigField("agentVersion", agentVersion); err != nil {
if err := config.UpdateConfigField("agentVersion", agentVersion, customWorkDir); err != nil {
logger.Error("Failed to update agent version in configuration: %v", err)
logger.LogFunctionExit("Update", nil, err)
return fmt.Errorf("failed to update agent version in configuration: %w", err)
Expand Down
44 changes: 22 additions & 22 deletions installer/go/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,12 @@ var (
flowfuseOneTimeCode string
nodeVersion string
serviceUsername string
instVersion string // Version of the installer, set during build
showVersion bool
installDir string
instVersion string
showVersion bool
help bool
uninstall bool
updateNode bool
updateNode bool
updateAgent bool
debugMode bool
)
Expand All @@ -32,6 +33,7 @@ func init() {
pflag.StringVarP(&serviceUsername, "service-user", "s", "flowfuse", "Username for the service account")
pflag.StringVarP(&flowfuseURL, "url", "u", "https://app.flowfuse.com", "FlowFuse URL")
pflag.StringVarP(&flowfuseOneTimeCode, "otc", "o", "", "FlowFuse one time code for authentication (optional for interactive installation)")
pflag.StringVarP(&installDir, "dir", "d", "", "Custom installation directory (default: /opt/flowfuse-device on Unix, c:\\opt\\flowfuse-device on Windows)")
pflag.BoolVarP(&showVersion, "version", "v", false, "Display installer version")
pflag.BoolVarP(&help, "help", "h", false, "Display help information")
pflag.BoolVar(&uninstall, "uninstall", false, "Uninstall the device agent")
Expand All @@ -41,18 +43,24 @@ func init() {
pflag.Parse()

if help {
exePath, err := os.Executable()
if err != nil || exePath == "" {
exePath = os.Args[0]
}
exeName := filepath.Base(exePath)
fmt.Println("FlowFuse Device Agent Installer")
fmt.Print("\n")
fmt.Println("Usage:")
fmt.Println(" Installation:")
fmt.Println(" ./installer --otc <one-time-code> [--agent-version <version>] [--nodejs-version <version>]")
fmt.Println(" ./installer [--agent-version <version>] [--nodejs-version <version>] (interactive mode)")
fmt.Printf(" %s --otc <one-time-code> [--agent-version <version>] [--nodejs-version <version>]\n", exeName)
fmt.Printf(" %s [--agent-version <version>] [--nodejs-version <version>] (interactive mode)\n", exeName)
fmt.Println(" Update:")
fmt.Println(" ./installer --update-agent [--agent-version <version>]")
fmt.Println(" ./installer --update-nodejs [--nodejs-version <version>]")
fmt.Println(" ./installer --update-agent --update-nodejs [--agent-version <version>] [--nodejs-version <version>]")
fmt.Printf(" %s --update-agent [--agent-version <version>]\n", exeName)
fmt.Printf(" %s --update-nodejs [--nodejs-version <version>]\n", exeName)
fmt.Printf(" %s --update-agent --update-nodejs [--agent-version <version>] [--nodejs-version <version>]\n", exeName)
fmt.Println(" Uninstall:")
fmt.Println(" ./installer --uninstall")
fmt.Printf(" %s --uninstall\n", exeName)
fmt.Printf(" %s --uninstall --dir <custom-working-directory>\n", exeName)
fmt.Print("\n")
fmt.Println("Options:")
pflag.PrintDefaults()
Expand All @@ -78,13 +86,8 @@ func init() {

func main() {
utils.ServiceUsername = serviceUsername

exePath, err := os.Executable()
if err != nil {
fmt.Println("Error determining executable path:", err)
os.Exit(1)
}
installerDir := filepath.Dir(exePath)
var err error
var exitCode int

// Initialize logger
if err := logger.Initialize(debugMode); err != nil {
Expand Down Expand Up @@ -112,18 +115,15 @@ func main() {
logger.Debug("FlowFuse Device Agent Installer version: %s", instVersion)
}

var exitCode int

if uninstall {
logger.Info("Uninstalling FlowFuse Device Agent...")
err = cmd.Uninstall()
err = cmd.Uninstall(installDir)
} else if updateNode || updateAgent {
logger.Info("Updating FlowFuse Device Agent...")
err = cmd.Update(agentVersion, nodeVersion, updateAgent, updateNode)
err = cmd.Update(agentVersion, nodeVersion, installDir, updateAgent, updateNode)
} else {
logger.Info("Installing FlowFuse Device Agent...")

err = cmd.Install(nodeVersion, agentVersion, installerDir, flowfuseURL, flowfuseOneTimeCode, false)
err = cmd.Install(nodeVersion, agentVersion, flowfuseURL, flowfuseOneTimeCode, installDir, false)
}

if err != nil {
Expand Down
37 changes: 25 additions & 12 deletions installer/go/pkg/config/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,15 @@ type InstallerConfig struct {
// It first retrieves the working directory using utils.GetWorkingDirectory()
// and then appends "installer.conf" to form the complete path.
// If retrieving the working directory fails, it returns an empty string and an error.
func GetConfigPath() (string, error) {
workDir, err := utils.GetWorkingDirectory()
//
// Parameters:
// - customWorkDir: Optional custom working directory path. If empty, uses default path.
//
// Returns:
// - string: The path to the configuration file
// - error: An error if retrieving the working directory fails
func GetConfigPath(customWorkDir string) (string, error) {
workDir, err := utils.GetWorkingDirectory(customWorkDir)
if err != nil {
return "", fmt.Errorf("failed to get working directory: %w", err)
}
Expand All @@ -40,11 +47,12 @@ func GetConfigPath() (string, error) {
//
// Parameters:
// - cfg: The InstallerConfig to be saved
// - customWorkDir: Optional custom working directory path. If empty, uses default path.
//
// Returns:
// - error: nil if successful, otherwise an error detailing what went wrong
func SaveConfig(cfg *InstallerConfig) error {
configPath, err := GetConfigPath()
func SaveConfig(cfg *InstallerConfig, customWorkDir string) error {
configPath, err := GetConfigPath(customWorkDir)
if err != nil {
return err
}
Expand Down Expand Up @@ -85,19 +93,22 @@ func SaveConfig(cfg *InstallerConfig) error {
return nil
}

// LoadConfig loads the installer configuration from the default configuration path.
// LoadConfig loads the installer configuration from the configuration path.
//
// It first attempts to get the path to the configuration file using GetConfigPath().
// If the configuration file doesn't exist, it returns a default configuration with
// the ServiceUsername set to the predefined utils.ServiceUsername value.
// If the file exists, it reads and parses the JSON content into an InstallerConfig struct.
//
// Parameters:
// - customWorkDir: Optional custom working directory path. If empty, uses default path.
//
// Returns:
// - *InstallerConfig: The loaded configuration or default if file doesn't exist
// - error: An error if the config path cannot be determined, the file cannot be read,
// or the JSON content cannot be parsed
func LoadConfig() (*InstallerConfig, error) {
configPath, err := GetConfigPath()
func LoadConfig(customWorkDir string) (*InstallerConfig, error) {
configPath, err := GetConfigPath(customWorkDir)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -134,16 +145,18 @@ func LoadConfig() (*InstallerConfig, error) {
// Parameters:
// - fieldName: The name of the field to update (case-sensitive)
// - value: The new value for the field
// - customWorkDir: Optional custom working directory path. If empty, uses default path.
//
// Returns:
// - error: nil if successful, otherwise an error detailing what went wrong
func UpdateConfigField(fieldName, value string) error {
func UpdateConfigField(fieldName, value, customWorkDir string) error {
logger.LogFunctionEntry("UpdateConfigField", map[string]interface{}{
"fieldName": fieldName,
"value": value,
"fieldName": fieldName,
"value": value,
"customWorkDir": customWorkDir,
})

cfg, err := LoadConfig()
cfg, err := LoadConfig(customWorkDir)
if err != nil {
return fmt.Errorf("failed to load existing config: %w", err)
}
Expand All @@ -162,7 +175,7 @@ func UpdateConfigField(fieldName, value string) error {
}

// Save the updated configuration
if err := SaveConfig(cfg); err != nil {
if err := SaveConfig(cfg, customWorkDir); err != nil {
logger.LogFunctionExit("UpdateConfigField", "error", err)
return fmt.Errorf("failed to save updated config: %w", err)
}
Expand Down
Loading