From 671ef75476de71403d10693210438eb2767cb5ed Mon Sep 17 00:00:00 2001 From: Jahziel Villasana-Espinoza Date: Wed, 30 Apr 2025 17:18:55 -0400 Subject: [PATCH] use auto-generated scripts for non-exe installers if not included in gitops payload (#28680) > For #28561 # Checklist for submitter If some of the following don't apply, delete the relevant line. - [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 - [x] For unreleased bug fixes in a release candidate, confirmed that the fix is not expected to adversely impact load test results or alerted the release DRI if additional load testing is needed. --- ee/server/service/software_installers.go | 13 ++++ server/datastore/mysql/software_installers.go | 6 +- server/fleet/software_installer.go | 20 ++--- server/service/integration_enterprise_test.go | 73 +++++++++++++++++++ 4 files changed, 102 insertions(+), 10 deletions(-) diff --git a/ee/server/service/software_installers.go b/ee/server/service/software_installers.go index d296a1ecb8..257349280c 100644 --- a/ee/server/service/software_installers.go +++ b/ee/server/service/software_installers.go @@ -1739,6 +1739,7 @@ func (svc *Service) softwareBatchUpload( installer.BundleIdentifier = *foundInstaller.BundleIdentifier } installer.Title = foundInstaller.Title + installer.PackageIDs = foundInstaller.PackageIDs case !ok && len(teamIDs) > 0: // Installer exists, but for another team. We should copy it over to this team // (if we have access to the other team). @@ -1770,6 +1771,7 @@ func (svc *Service) softwareBatchUpload( } installer.Title = i.Title installer.StorageID = p.SHA256 + installer.PackageIDs = i.PackageIDs break } } @@ -1814,6 +1816,17 @@ func (svc *Service) softwareBatchUpload( } } + // custom scripts only for exe installers + if installer.Extension != "exe" { + if installer.InstallScript == "" { + installer.InstallScript = file.GetInstallScript(installer.Extension) + } + + if installer.UninstallScript == "" { + installer.UninstallScript = file.GetUninstallScript(installer.Extension) + } + } + // Update $PACKAGE_ID in uninstall script preProcessUninstallScript(installer) diff --git a/server/datastore/mysql/software_installers.go b/server/datastore/mysql/software_installers.go index ad011bd58c..c35343b679 100644 --- a/server/datastore/mysql/software_installers.go +++ b/server/datastore/mysql/software_installers.go @@ -2329,7 +2329,8 @@ SELECT si.platform AS platform, st.source AS source, st.bundle_identifier AS bundle_identifier, - st.name AS title + st.name AS title, + si.package_ids AS package_ids FROM software_installers si JOIN software_titles st ON si.title_id = st.id @@ -2359,6 +2360,9 @@ WHERE if _, ok := set[tmID]; ok { return nil, ctxerr.New(ctx, fmt.Sprintf("cannot have multiple installers with the same hash %q on one team", sha256)) } + if installer.PackageIDList != "" { + installer.PackageIDs = strings.Split(installer.PackageIDList, ",") + } set[tmID] = installer } diff --git a/server/fleet/software_installer.go b/server/fleet/software_installer.go index 076f057e4c..655df418ac 100644 --- a/server/fleet/software_installer.go +++ b/server/fleet/software_installer.go @@ -383,15 +383,17 @@ type UploadSoftwareInstallerPayload struct { } type ExistingSoftwareInstaller struct { - InstallerID uint `db:"installer_id"` - TeamID *uint `db:"team_id"` - Filename string `db:"filename"` - Extension string `db:"extension"` - Version string `db:"version"` - Platform string `db:"platform"` - Source string `db:"source"` - BundleIdentifier *string `db:"bundle_identifier"` - Title string `db:"title"` + InstallerID uint `db:"installer_id"` + TeamID *uint `db:"team_id"` + Filename string `db:"filename"` + Extension string `db:"extension"` + Version string `db:"version"` + Platform string `db:"platform"` + Source string `db:"source"` + BundleIdentifier *string `db:"bundle_identifier"` + Title string `db:"title"` + PackageIDList string `db:"package_ids"` + PackageIDs []string `` } type UpdateSoftwareInstallerPayload struct { diff --git a/server/service/integration_enterprise_test.go b/server/service/integration_enterprise_test.go index d5943217b3..58a6dcf67a 100644 --- a/server/service/integration_enterprise_test.go +++ b/server/service/integration_enterprise_test.go @@ -17199,6 +17199,13 @@ func (s *integrationEnterpriseTestSuite) TestBatchSoftwareUploadWithSHAs() { w.Header().Set("Content-Type", "application/vnd.microsoft.portable-executable") _, err = io.Copy(w, file) require.NoError(t, err) + case "/app.pkg": + file, err := os.Open(filepath.Join("testdata", "software-installers", "dummy_installer.pkg")) + require.NoError(t, err) + defer file.Close() + w.Header().Set("Content-Type", "application/x-newton-compatible-pkg") + _, err = io.Copy(w, file) + require.NoError(t, err) default: w.WriteHeader(http.StatusNotFound) @@ -17389,4 +17396,70 @@ func (s *integrationEnterpriseTestSuite) TestBatchSoftwareUploadWithSHAs() { s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusAccepted, &batchResponse, "team_name", team2.Name) errMsg = waitBatchSetSoftwareInstallersFailed(t, s, team2.Name, batchResponse.RequestUUID) require.Contains(t, errMsg, "Couldn't edit. Uninstall script is required for .exe packages.") + + // add both scripts to get a success + softwareToInstall[1].InstallScript = "echo install 2" + softwareToInstall[1].UninstallScript = "echo uninstall 2" + softwareToInstall[1].SHA256 = exeHash + + // add the pkg installer with some custom scripts + pkgURL := srv.URL + "/app.pkg" + softwareToInstall = append(softwareToInstall, &fleet.SoftwareInstallerPayload{ + URL: pkgURL, + InstallScript: "some install script", + UninstallScript: "some uninstall script", + }) + + s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusAccepted, &batchResponse, "team_name", team2.Name) + packages = waitBatchSetSoftwareInstallersCompleted(t, s, team2.Name, batchResponse.RequestUUID) + require.Len(t, packages, 3) + pkgTitleID := packages[2].TitleID + require.NotNil(t, pkgTitleID) + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", *pkgTitleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team2.ID)) + require.NotNil(t, stResp.SoftwareTitle.SoftwarePackage) + require.Equal(t, "DummyApp.app", stResp.SoftwareTitle.Name) + require.Equal(t, pkgURL, stResp.SoftwareTitle.SoftwarePackage.URL) + require.Equal(t, softwareToInstall[2].InstallScript, stResp.SoftwareTitle.SoftwarePackage.InstallScript) + require.Equal(t, softwareToInstall[2].UninstallScript, stResp.SoftwareTitle.SoftwarePackage.UninstallScript) + + expectedUninstallScript := `#!/bin/sh + +# Fleet extracts and saves package IDs. +pkg_ids=( + "com.example.dummy" +) + +# For each package id, get all .app folders associated with the package and remove them. +for pkg_id in "${pkg_ids[@]}" +do + # Get volume and location of the package. + volume=$(pkgutil --pkg-info "$pkg_id" | grep -i "volume" | awk '{if (NF>1) print $NF}') + location=$(pkgutil --pkg-info "$pkg_id" | grep -i "location" | awk '{if (NF>1) print $NF}') + # Check if this package id corresponds to a valid/installed package + if [[ ! -z "$volume" ]]; then + # Remove individual directories that end with ".app" belonging to the package. + # Only process directories that end with ".app" to prevent Fleet from removing top level directories. + pkgutil --only-dirs --files "$pkg_id" | grep "\.app$" | sed -e 's@^@'"$volume""$location"'/@' | tr '\n' '\0' | xargs -n 1 -0 rm -rf + # Remove receipts + pkgutil --forget "$pkg_id" + else + echo "WARNING: volume is empty for package ID $pkg_id" + fi +done +` + + // remove the custom scripts from the .pkg. We should get back the auto-generated ones. + softwareToInstall[2].InstallScript = "" + softwareToInstall[2].UninstallScript = "" + s.DoJSON("POST", "/api/latest/fleet/software/batch", batchSetSoftwareInstallersRequest{Software: softwareToInstall}, http.StatusAccepted, &batchResponse, "team_name", team2.Name) + packages = waitBatchSetSoftwareInstallersCompleted(t, s, team2.Name, batchResponse.RequestUUID) + require.Len(t, packages, 3) + pkgTitleID = packages[2].TitleID + require.NotNil(t, pkgTitleID) + s.DoJSON("GET", fmt.Sprintf("/api/latest/fleet/software/titles/%d", *pkgTitleID), getSoftwareTitleRequest{}, http.StatusOK, &stResp, "team_id", fmt.Sprint(team2.ID)) + require.NotNil(t, stResp.SoftwareTitle.SoftwarePackage) + require.Equal(t, "DummyApp.app", stResp.SoftwareTitle.Name) + require.Equal(t, pkgURL, stResp.SoftwareTitle.SoftwarePackage.URL) + require.Equal(t, file.GetInstallScript("pkg"), stResp.SoftwareTitle.SoftwarePackage.InstallScript) + require.Equal(t, expectedUninstallScript, stResp.SoftwareTitle.SoftwarePackage.UninstallScript) }