Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
180 changes: 180 additions & 0 deletions installer/TESTING.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
# FlowFuse Device Agent Installer — Standardized Testing Scenarios

This document defines consistent, end-to-end scenarios for validating the installer. It covers the happy path (OTC only) and additional flows across Linux, macOS, and Windows.

Conventions
- Binary: `./flowfuse-device-agent-installer` (Linux/macOS) or `flowfuse-device-agent-installer.exe` (Windows)
- Placeholders: `<OTC>`, `<dir>`, `<port>`, `<nodeVer>`, `<agentVer>`

Artifacts to collect (per scenario)
- Installer logs (use `--debug` when helpful)
- Generated service files (systemd unit, SysV/OpenRC script, launchd plist, or NSSM params)
- Service status output and logs
- `installer.conf`

## A. Happy path – OTC only (defaults)
Steps
1) Run: `--otc <OTC>`
2) Wait for installation and configuration to complete

Expect
- Working directory created at default path
- `installer.conf` persisted (includes agent/node versions and default port)
- Service installed and running
- Logs available in `<dir>/logs`

## B. Interactive install – no OTC
Steps
1) Run with no flags and accept installation in interactive prompt
2) Choose to provide config now (manual) or skip (install-only)

Expect
- Manual path: prompts for YAML; saved as `<dir>/device.yml`; service installed and running
- Install-only: agent installed; service set up per mode; clear next steps printed

## C. Custom installation directory
Steps
1) Run: `--otc <OTC> --dir <dir>`

Expect
- All files created under `<dir>`
- Service runtime includes `--dir <dir>`

## D. Custom port (per-port services)
Steps
1) Run: `--otc <OTC> --port <port>`

Expect
- `installer.conf` contains `"port": <port>`
- Service name suffixed with `<port>` (e.g., `flowfuse-device-agent-1880`)
- Runtime command includes `--port <port>` and agent listens on `<port>`

## E. Multiple instances (optional)
Steps
1) Install two instances with distinct `--dir` and `--port` (e.g., 1880 and 1881)

Expect
- Both services exist and run concurrently without conflicts

## F. Idempotent reinstall
Steps
1) Run installer again with same `--dir` and (optionally) same `--port`

Expect
- Previous service replaced cleanly; ends in running state

## G. Update – Device Agent only
Prereq: Service installed and running
Steps
1) Run: `--update-agent [--agent-version <agentVer>] --dir <dir>`

Expect
- Correct service is stopped/started
- `installer.conf` agentVersion updated (or resolved latest recorded)

## H. Update – Node.js only
Steps
1) Run: `--update-nodejs --nodejs-version <nodeVer> --dir <dir>`

Expect
- Correct service is stopped/started
- Node.js updated and agent remains functional

## I. Update – both
Steps
1) Run: `--update-agent --update-nodejs --agent-version <agentVer> --nodejs-version <nodeVer> --dir <dir>`

Expect
- Both components updated; single stop/start cycle preferred

## J. Uninstall
Steps
1) Run: `--uninstall --dir <dir>` and confirm prompt

Expect
- Service removed (per-port if present; legacy name otherwise)
- Working directory cleaned up
- Service account removal logged (may no-op on some OSes)

## K. Legacy fallback
Scenario
- A legacy service named `flowfuse-device-agent` exists with no port suffix

1) Upgrade
Steps:
1) Install `flowfuse-device-agent` previous to latest version using the installer `v1.1.0`
2) Use the latest installer version to upgrade the `flowfuse-device-agent` to the latest version

Expect
- Update process should complete without errors, `flowfuse-device-agent` should be upgraded to the latest version

2) Uninstall
Steps:
1) Install `flowfuse-device-agent` using the installer `v1.1.0`
2) Use the latest installer version to uninstall the `flowfuse-device-agent`

Expect
- `flowfuse-device-agent` installation should be completely removed

## L. Help and version output
Steps
1) Run: `--help`
2) Run: `--version`

Expect
- `--port` shown and notes explain per-port service names
- Installer version printed

---

## OS-specific verification

Linux — systemd
- Unit at `/etc/systemd/system/flowfuse-device-agent-<port>.service`
- `ExecStart` contains `--dir <dir>` and `--port <port>`; `Restart=on-failure`
- Commands:
```bash
sudo systemctl status flowfuse-device-agent-<port>
sudo systemctl restart flowfuse-device-agent-<port>
sudo journalctl -u flowfuse-device-agent-<port> -e
```

Linux — SysVinit
- Script at `/etc/init.d/flowfuse-device-agent-<port>` with `DAEMON_ARGS="--dir <dir> --port <port>"`
```bash
sudo service flowfuse-device-agent-<port> status
```

Linux — OpenRC
- Script at `/etc/init.d/flowfuse-device-agent-<port>`; `command` includes `--dir` and `--port`
```bash
sudo rc-service flowfuse-device-agent-<port> status
```

macOS — launchd
- Plist at `/Library/LaunchDaemons/com.flowfuse.device-agent-<port>.plist`
- ProgramArguments include `--dir` and `--port`; `KeepAlive` true
```bash
sudo launchctl print system/com.flowfuse.device-agent-<port>
sudo launchctl kickstart -k system/com.flowfuse.device-agent-<port>
```

Windows — NSSM
- Service `flowfuse-device-agent-<port>`
- `AppParameters` shows `--dir <dir> --port <port>`; DisplayName `FlowFuse Device Agent (<port>)`
```powershell
sc.exe query flowfuse-device-agent-<port>
nssm get flowfuse-device-agent-<port> AppParameters
```

---

## Listening port verification
- Linux: `sudo ss -lntp | grep :<port>` or `sudo lsof -iTCP -sTCP:LISTEN | grep <port>`
- macOS: `sudo lsof -iTCP -sTCP:LISTEN | grep <port>`
- Windows (PowerShell): `Get-NetTCPConnection -LocalPort <port> -State Listen`

## Clean-up checklist
- Stop/uninstall services created during tests
- Remove test directories under `--dir`
- Restore environment where needed
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
Loading