-
Notifications
You must be signed in to change notification settings - Fork 450
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
fix: update install script for FMAs to improve re-install process (#2…
…5238) > For #24148 # Checklist for submitter If some of the following don't apply, delete the relevant line. <!-- Note that API documentation changes are now addressed by the product design team. --> - [x] Changes file added for user-visible changes in `changes/`, `orbit/changes/` or `ee/fleetd-chrome/changes`. See [Changes files](https://github.com/fleetdm/fleet/blob/main/docs/Contributing/Committing-Changes.md#changes-files) for more information. - [x] Input data is properly validated, `SELECT *` is avoided, SQL injection is prevented (using placeholders for values in statements) - [x] If database migrations are included, checked table schema to confirm autoupdate - For database migrations: - [x] Checked schema for all modified table for columns that will auto-update timestamps during migration. - [x] Confirmed that updating the timestamps is acceptable, and will not cause unwanted side effects. - [x] Ensured the correct collation is explicitly set for character columns (`COLLATE utf8mb4_unicode_ci`). - [x] Added/updated automated tests - [x] A detailed QA plan exists on the associated ticket (if it isn't there, work with the product group's QA engineer to add it) - [x] Manual QA for all new/changed functionality
- Loading branch information
Showing
16 changed files
with
770 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
- Updated Fleet-maintained app install scripts for non-PKG-based installers to allow the apps to be installed over an existing installation. |
143 changes: 143 additions & 0 deletions
143
server/datastore/mysql/migrations/tables/20250109150150_UpdateFMAInstallScripts.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,143 @@ | ||
package tables | ||
|
||
import ( | ||
"database/sql" | ||
"errors" | ||
"fmt" | ||
"slices" | ||
"strings" | ||
|
||
"github.com/jmoiron/sqlx" | ||
"github.com/jmoiron/sqlx/reflectx" | ||
) | ||
|
||
func init() { | ||
MigrationClient.AddMigration(Up_20250109150150, Down_20250109150150) | ||
} | ||
|
||
const quitApplicationFunc = ` | ||
quit_application() { | ||
local bundle_id="$1" | ||
local timeout_duration=10 | ||
# check if the application is running | ||
if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then | ||
return | ||
fi | ||
local console_user | ||
console_user=$(stat -f "%Su" /dev/console) | ||
if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then | ||
echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." | ||
return | ||
fi | ||
echo "Quitting application '$bundle_id'..." | ||
# try to quit the application within the timeout period | ||
local quit_success=false | ||
SECONDS=0 | ||
while (( SECONDS < timeout_duration )); do | ||
if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then | ||
if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then | ||
echo "Application '$bundle_id' quit successfully." | ||
quit_success=true | ||
break | ||
fi | ||
fi | ||
sleep 1 | ||
done | ||
if [[ "$quit_success" = false ]]; then | ||
echo "Application '$bundle_id' did not quit." | ||
fi | ||
} | ||
` | ||
|
||
func Up_20250109150150(tx *sql.Tx) error { | ||
var scriptsToModify []struct { | ||
InstallScriptContents string `db:"contents"` | ||
AppName string `db:"name"` | ||
BundleID string `db:"bundle_identifier"` | ||
ScriptContentID uint `db:"script_content_id"` | ||
Token string `db:"token"` | ||
} | ||
|
||
// Note: we're not updating any install scripts that have been edited by users, only the | ||
// "original" script contents for FMAs that are created when the fleet_library_apps table is populated. | ||
selectStmt := ` | ||
SELECT | ||
sc.contents AS contents, | ||
fla.name AS name, | ||
fla.bundle_identifier AS bundle_identifier, | ||
sc.id AS script_content_id, | ||
fla.token AS token | ||
FROM | ||
fleet_library_apps fla | ||
JOIN script_contents sc | ||
ON fla.install_script_content_id = sc.id | ||
WHERE fla.token IN (?) | ||
` | ||
|
||
// This is the list of Fleet-maintained apps we want to update ("token" is an ID found on the brew | ||
// metadata) | ||
appTokens := []string{"1password", "brave-browser", "docker", "figma", "google-chrome", "visual-studio-code", "firefox", "notion", "slack", "whatsapp", "postman"} | ||
|
||
stmt, args, err := sqlx.In(selectStmt, appTokens) | ||
if err != nil { | ||
return fmt.Errorf("building SQL in statement for selecting fleet maintained apps: %w", err) | ||
} | ||
|
||
txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)} | ||
if err := txx.Select(&scriptsToModify, stmt, args...); err != nil { | ||
// if this migration is running on a brand-new Fleet deployment, then there won't be | ||
// anything in the fleet_library_apps table, so we can just exit. | ||
if errors.Is(err, sql.ErrNoRows) { | ||
return nil | ||
} | ||
|
||
return fmt.Errorf("selecting script contents: %w", err) | ||
} | ||
|
||
for _, sc := range scriptsToModify { | ||
lines := strings.Split(sc.InstallScriptContents, "\n") | ||
// Find the line where we copy the new .app file into the Applications folder. We want to | ||
// add our changes right before that line. | ||
var copyLineNumber int | ||
for i, l := range lines { | ||
if strings.Contains(l, `sudo cp -R "$TMPDIR/`) { | ||
copyLineNumber = i | ||
break | ||
} | ||
} | ||
|
||
appFileName := fmt.Sprintf("%s.app", sc.AppName) | ||
if sc.Token == "visual-studio-code" { | ||
// VSCode has the name "Microsoft Visual Studio Code" in fleet_library_apps, but the | ||
// .app name is "Visual Studio Code.app", so account for that here. | ||
appFileName = "Visual Studio Code.app" | ||
} | ||
|
||
// This line will move the old version of the .app (if it exists) to the temporary directory | ||
lines = slices.Insert(lines, copyLineNumber, fmt.Sprintf(`sudo [ -d "$APPDIR/%[1]s" ] && sudo mv "$APPDIR/%[1]s" "$TMPDIR/%[1]s.bkp"`, appFileName)) | ||
// Add a call to our "quit_application" function | ||
lines = slices.Insert(lines, copyLineNumber, fmt.Sprintf("quit_application %s", sc.BundleID)) | ||
// Add the "quit_application" function to the script | ||
lines = slices.Insert(lines, 2, quitApplicationFunc) | ||
|
||
updatedScript := strings.Join(lines, "\n") | ||
|
||
checksum := md5ChecksumScriptContent(updatedScript) | ||
|
||
if _, err = tx.Exec(`UPDATE script_contents SET contents = ?, md5_checksum = UNHEX(?) WHERE id = ?`, strings.Join(lines, "\n"), checksum, sc.ScriptContentID); err != nil { | ||
return fmt.Errorf("updating fma install script contents: %w", err) | ||
} | ||
} | ||
|
||
return nil | ||
} | ||
|
||
func Down_20250109150150(tx *sql.Tx) error { | ||
return nil | ||
} |
165 changes: 165 additions & 0 deletions
165
server/datastore/mysql/migrations/tables/20250109150150_UpdateFMAInstallScripts_test.go
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,165 @@ | ||
package tables | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/jmoiron/sqlx" | ||
"github.com/jmoiron/sqlx/reflectx" | ||
"github.com/stretchr/testify/require" | ||
) | ||
|
||
func TestUp_20250109150150(t *testing.T) { | ||
db := applyUpToPrev(t) | ||
|
||
// | ||
// Insert data to test the migration | ||
// | ||
// ... | ||
originalContents := ` | ||
#!/bin/sh | ||
# variables | ||
APPDIR="/Applications/" | ||
TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") | ||
# extract contents | ||
unzip "$INSTALLER_PATH" -d "$TMPDIR" | ||
# copy to the applications folder | ||
sudo cp -R "$TMPDIR/Figma.app" "$APPDIR" | ||
` | ||
|
||
tx, err := db.Begin() | ||
require.NoError(t, err) | ||
txx := sqlx.Tx{Tx: tx, Mapper: reflectx.NewMapperFunc("db", sqlx.NameMapper)} | ||
installScriptID, err := getOrInsertScript(txx, originalContents) | ||
require.NoError(t, err) | ||
uninstallScriptID, err := getOrInsertScript(txx, "echo uninstall") | ||
require.NoError(t, err) | ||
boxInstallScriptID, err := getOrInsertScript(txx, "echo install") | ||
require.NoError(t, err) | ||
boxUninstallScriptID, err := getOrInsertScript(txx, "echo uninstall") | ||
require.NoError(t, err) | ||
err = tx.Commit() | ||
require.NoError(t, err) | ||
|
||
// Insert Figma (one of our target FMAs) | ||
execNoErr( | ||
t, | ||
db, | ||
`INSERT INTO fleet_library_apps (name, token, version, platform, installer_url, sha256, bundle_identifier, install_script_content_id, uninstall_script_content_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, | ||
"Figma", | ||
"figma", | ||
"124.7.4", | ||
"darwin", | ||
"https://desktop.figma.com/mac-arm/Figma-124.7.4.zip", | ||
"3160c0cac00b8b81b7b62375f04b9598b11cbd9e5d42a5ad532e8b98fecc6b15", | ||
"com.figma.Desktop", | ||
installScriptID, | ||
uninstallScriptID, | ||
) | ||
|
||
// Insert Box Drive, should be unaffected | ||
execNoErr( | ||
t, | ||
db, | ||
`INSERT INTO fleet_library_apps (name, token, version, platform, installer_url, sha256, bundle_identifier, install_script_content_id, uninstall_script_content_id) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)`, | ||
"Box Drive", | ||
"box-drive", | ||
"2.42.212", | ||
"darwin", | ||
"https://e3.boxcdn.net/desktop/releases/mac/BoxDrive-2.42.212.pkg", | ||
"93550756150c434bc058c30b82352c294a21e978caf436ac99e0a5f431adfb6e", | ||
"com.box.desktop", | ||
boxInstallScriptID, | ||
boxUninstallScriptID, | ||
) | ||
|
||
// Apply current migration. | ||
applyNext(t, db) | ||
|
||
// | ||
// Check data, insert new entries, e.g. to verify migration is safe. | ||
// | ||
// ... | ||
var scriptContents struct { | ||
InstallScriptContents string `db:"contents"` | ||
Checksum string `db:"md5_checksum"` | ||
} | ||
|
||
selectStmt := ` | ||
SELECT | ||
sc.contents AS contents, | ||
HEX(sc.md5_checksum) AS md5_checksum | ||
FROM | ||
fleet_library_apps fla | ||
JOIN script_contents sc | ||
ON fla.install_script_content_id = sc.id | ||
WHERE fla.token = ?` | ||
|
||
err = sqlx.Get(db, &scriptContents, selectStmt, "figma") | ||
require.NoError(t, err) | ||
|
||
expectedContents := ` | ||
#!/bin/sh | ||
quit_application() { | ||
local bundle_id="$1" | ||
local timeout_duration=10 | ||
# check if the application is running | ||
if ! osascript -e "application id \"$bundle_id\" is running" 2>/dev/null; then | ||
return | ||
fi | ||
local console_user | ||
console_user=$(stat -f "%Su" /dev/console) | ||
if [[ $EUID -eq 0 && "$console_user" == "root" ]]; then | ||
echo "Not logged into a non-root GUI; skipping quitting application ID '$bundle_id'." | ||
return | ||
fi | ||
echo "Quitting application '$bundle_id'..." | ||
# try to quit the application within the timeout period | ||
local quit_success=false | ||
SECONDS=0 | ||
while (( SECONDS < timeout_duration )); do | ||
if osascript -e "tell application id \"$bundle_id\" to quit" >/dev/null 2>&1; then | ||
if ! pgrep -f "$bundle_id" >/dev/null 2>&1; then | ||
echo "Application '$bundle_id' quit successfully." | ||
quit_success=true | ||
break | ||
fi | ||
fi | ||
sleep 1 | ||
done | ||
if [[ "$quit_success" = false ]]; then | ||
echo "Application '$bundle_id' did not quit." | ||
fi | ||
} | ||
# variables | ||
APPDIR="/Applications/" | ||
TMPDIR=$(dirname "$(realpath $INSTALLER_PATH)") | ||
# extract contents | ||
unzip "$INSTALLER_PATH" -d "$TMPDIR" | ||
# copy to the applications folder | ||
quit_application com.figma.Desktop | ||
sudo [ -d "$APPDIR/Figma.app" ] && sudo mv "$APPDIR/Figma.app" "$TMPDIR/Figma.app.bkp" | ||
sudo cp -R "$TMPDIR/Figma.app" "$APPDIR" | ||
` | ||
|
||
expectedChecksum := md5ChecksumScriptContent(expectedContents) | ||
|
||
require.Equal(t, expectedContents, scriptContents.InstallScriptContents) | ||
require.Equal(t, expectedChecksum, scriptContents.Checksum) | ||
|
||
err = sqlx.Get(db, &scriptContents, selectStmt, "box-drive") | ||
require.NoError(t, err) | ||
require.Equal(t, "echo install", scriptContents.InstallScriptContents) | ||
require.Equal(t, md5ChecksumScriptContent("echo install"), scriptContents.Checksum) | ||
} |
Oops, something went wrong.